mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
# Conflicts: # README.md
This commit is contained in:
commit
2acb65b745
19
.github/ISSUE_TEMPLATE.md
vendored
19
.github/ISSUE_TEMPLATE.md
vendored
@ -1,19 +0,0 @@
|
||||
#### Before opening an issue 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)
|
||||
|
||||
#### Your issue is:
|
||||
*In case of a bug, do not forget to attach logs!*
|
||||
|
||||
#### Your wearable device is:
|
||||
|
||||
*Please specify model and firmware version if possible*
|
||||
|
||||
#### Your android version is:
|
||||
|
||||
#### Your Gadgetbridge version is:
|
||||
|
||||
|
||||
|
||||
*New issues about already solved/documented topics could be closed without further comments. Same for too generic or incomplete reports.*
|
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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.*
|
||||
|
||||
|
47
CHANGELOG.md
47
CHANGELOG.md
@ -1,5 +1,52 @@
|
||||
### Changelog
|
||||
|
||||
#### Version 0.37.0
|
||||
* Initial Makibes HR3 support
|
||||
* Amazfit Bip Lite: Inittal 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)
|
||||
|
@ -45,7 +45,7 @@ public class GBDaoGenerator {
|
||||
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Schema schema = new Schema(20, MAIN_PACKAGE + ".entities");
|
||||
Schema schema = new Schema(21, MAIN_PACKAGE + ".entities");
|
||||
|
||||
Entity userAttributes = addUserAttributes(schema);
|
||||
Entity user = addUserInfo(schema, userAttributes);
|
||||
@ -60,6 +60,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);
|
||||
@ -186,6 +187,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();
|
||||
@ -363,7 +374,7 @@ public class GBDaoGenerator {
|
||||
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; }"
|
||||
"public boolean getRepetition(int dow) { return (this.repetition & dow) > 0; }"
|
||||
);
|
||||
alarm.addIntProperty("hour").notNull();
|
||||
alarm.addIntProperty("minute").notNull();
|
||||
|
56
README.md
56
README.md
@ -28,70 +28,37 @@ vendor's servers.
|
||||
|
||||
[List of changes](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/CHANGELOG.md)
|
||||
|
||||
## Supported Devices
|
||||
## 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 AND ROOT 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)
|
||||
* BFH-16
|
||||
* Casio GB-6900B (WIP)
|
||||
* Casio GB-6900B
|
||||
* HPlus Devices (e.g. ZeBand) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus)
|
||||
* ID115 (WIP)
|
||||
* Lenovo Watch 9 (WIP)
|
||||
* ID115
|
||||
* Lenovo Watch 9
|
||||
* Lenovo Watch X Plus (WIP)
|
||||
* Liveview (WIP)
|
||||
* 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 (WIP, NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4)
|
||||
* 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 (WIP)
|
||||
* 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 (WIP)
|
||||
* Teclast H10, H30
|
||||
* XWatch (Affordable Chinese Casio-like smartwatches)
|
||||
* Vibratissimo (experimental)
|
||||
* ZeTime (WIP) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
|
||||
* ZeTime [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/FEATURES.md)
|
||||
|
||||
## Getting Started (Pebble)
|
||||
|
||||
Please [this wiki article](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble-Getting-Started)
|
||||
|
||||
## How to use (Mi Band 1+2)
|
||||
|
||||
* Invoke the discovery activity 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://codeberg.org/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://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2).
|
||||
|
||||
## Features (Liveview)
|
||||
|
||||
* set time (automatically upon connection)
|
||||
* display notifications and vibrate
|
||||
|
||||
## Authors
|
||||
### Core Team (in order of first code contribution)
|
||||
|
||||
@ -110,6 +77,7 @@ Please [this wiki article](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki
|
||||
* Andreas Böhler (Casio GB-6900B)
|
||||
* Jean-François Greffier (Mi Scale 2)
|
||||
* Johannes Schmitt (BFH-16)
|
||||
* Lukas Schwichtenberg (Makibes HR3)
|
||||
|
||||
## Contribute
|
||||
|
||||
|
@ -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
|
||||
@ -25,8 +25,8 @@ android {
|
||||
targetSdkVersion 27
|
||||
|
||||
// Note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.35.2"
|
||||
versionCode 154
|
||||
versionName "0.37.0"
|
||||
versionCode 158
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
@ -67,8 +67,8 @@ dependencies {
|
||||
testImplementation "com.google.code.gson:gson:2.8.5"
|
||||
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "androidx.appcompat:appcompat:1.1.0-rc01"
|
||||
implementation "androidx.preference:preference:1.1.0-rc01"
|
||||
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.0.0"
|
||||
implementation "androidx.legacy:legacy-support-v4:1.0.0"
|
||||
@ -79,7 +79,7 @@ dependencies {
|
||||
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"
|
||||
@ -99,7 +99,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")
|
||||
@ -142,22 +142,32 @@ task pmd(type: Pmd) {
|
||||
}
|
||||
}
|
||||
|
||||
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/javac/release/compileReleaseJavaWithJavac/classes")
|
||||
source = fileTree("src/main/java/")
|
||||
classpath = files()
|
||||
reports {
|
||||
xml.enabled = false
|
||||
html.enabled = true
|
||||
xml {
|
||||
destination file ("$project.buildDir/reports/findbugs/findbugs-output.xml")
|
||||
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.xml")
|
||||
}
|
||||
html {
|
||||
destination file ("$project.buildDir/reports/findbugs/findbugs-output.html")
|
||||
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.html")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,9 +21,12 @@
|
||||
<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
|
||||
@ -58,6 +61,10 @@
|
||||
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"
|
||||
@ -443,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" />
|
||||
@ -454,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"
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
19
app/src/main/aidl/lineageos/weather/RequestInfo.aidl
Normal file
19
app/src/main/aidl/lineageos/weather/RequestInfo.aidl
Normal 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;
|
19
app/src/main/aidl/lineageos/weather/WeatherInfo.aidl
Normal file
19
app/src/main/aidl/lineageos/weather/WeatherInfo.aidl
Normal 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;
|
19
app/src/main/aidl/lineageos/weather/WeatherLocation.aidl
Normal file
19
app/src/main/aidl/lineageos/weather/WeatherLocation.aidl
Normal 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;
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
31
app/src/main/java/lineageos/app/LineageContextConstants.java
Normal file
31
app/src/main/java/lineageos/app/LineageContextConstants.java
Normal 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";
|
||||
}
|
||||
}
|
32
app/src/main/java/lineageos/os/Build.java
Normal file
32
app/src/main/java/lineageos/os/Build.java
Normal 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;
|
||||
}
|
||||
}
|
153
app/src/main/java/lineageos/os/Concierge.java
Normal file
153
app/src/main/java/lineageos/os/Concierge.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
245
app/src/main/java/lineageos/providers/WeatherContract.java
Normal file
245
app/src/main/java/lineageos/providers/WeatherContract.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
435
app/src/main/java/lineageos/weather/LineageWeatherManager.java
Normal file
435
app/src/main/java/lineageos/weather/LineageWeatherManager.java
Normal 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);
|
||||
}
|
||||
}
|
379
app/src/main/java/lineageos/weather/RequestInfo.java
Normal file
379
app/src/main/java/lineageos/weather/RequestInfo.java
Normal 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;
|
||||
}
|
||||
}
|
642
app/src/main/java/lineageos/weather/WeatherInfo.java
Executable file
642
app/src/main/java/lineageos/weather/WeatherInfo.java
Executable 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;
|
||||
}
|
||||
}
|
274
app/src/main/java/lineageos/weather/WeatherLocation.java
Normal file
274
app/src/main/java/lineageos/weather/WeatherLocation.java
Normal 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;
|
||||
}
|
||||
}
|
84
app/src/main/java/lineageos/weather/util/WeatherUtils.java
Normal file
84
app/src/main/java/lineageos/weather/util/WeatherUtils.java
Normal 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°F or XX°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;
|
||||
}
|
||||
}
|
||||
}
|
161
app/src/main/java/lineageos/weatherservice/ServiceRequest.java
Normal file
161
app/src/main/java/lineageos/weatherservice/ServiceRequest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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><weather-provider-service></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);
|
||||
}
|
@ -78,9 +78,13 @@ 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;
|
||||
|
||||
@ -99,7 +103,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 = 4;
|
||||
private static final int CURRENT_PREFS_VERSION = 5;
|
||||
private static LimitedQueue mIDSenderLookup = new LimitedQueue(16);
|
||||
private static Prefs prefs;
|
||||
private static GBPrefs gbPrefs;
|
||||
@ -112,6 +116,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;
|
||||
|
||||
@ -631,8 +636,9 @@ public class GBApplication extends Application {
|
||||
DaoSession daoSession = db.getDaoSession();
|
||||
List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
|
||||
for (Device dbDevice : activeDevices) {
|
||||
SharedPreferences.Editor deviceSharedPrefsEdit = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier()).edit();
|
||||
if (sharedPrefs != null) {
|
||||
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) {
|
||||
@ -707,9 +713,8 @@ public class GBApplication extends Application {
|
||||
if (newLanguage != null) {
|
||||
deviceSharedPrefsEdit.putString("language", newLanguage);
|
||||
}
|
||||
deviceSharedPrefsEdit.apply();
|
||||
}
|
||||
|
||||
deviceSharedPrefsEdit.apply();
|
||||
}
|
||||
editor.remove("amazfitbip_language");
|
||||
editor.remove("bip_display_items");
|
||||
@ -760,6 +765,67 @@ public class GBApplication extends Application {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
||||
editor.apply();
|
||||
}
|
||||
|
@ -0,0 +1,225 @@
|
||||
/* Copyright (C) 2016-2019 0nse, Andreas Shimokawa, Carsten Pfeiffer
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ 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;
|
||||
updateAlarmsFromDB();
|
||||
|
@ -27,17 +27,11 @@ import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@ -50,6 +44,15 @@ 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;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.cketti.library.changelog.ChangeLog;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
@ -73,9 +76,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) {
|
||||
@ -103,14 +111,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);
|
||||
@ -132,6 +132,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(
|
||||
@ -230,6 +240,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) {
|
||||
|
||||
@ -239,7 +258,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);
|
||||
@ -253,6 +272,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;
|
||||
@ -277,7 +299,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));
|
||||
}
|
||||
@ -286,6 +309,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<>();
|
||||
@ -321,7 +356,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) {
|
||||
|
@ -17,12 +17,18 @@
|
||||
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.provider.DocumentsContract;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@ -42,10 +48,14 @@ 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 {
|
||||
@ -97,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");
|
||||
}
|
||||
|
@ -35,6 +35,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;
|
||||
@ -195,8 +196,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);
|
||||
}
|
||||
};
|
||||
@ -338,6 +338,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()) {
|
||||
@ -349,7 +355,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) {
|
||||
@ -359,7 +365,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);
|
||||
@ -374,7 +380,9 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
|
||||
deviceCandidates.add(candidate);
|
||||
}
|
||||
cadidateListAdapter.notifyDataSetChanged();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -616,6 +624,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);
|
||||
@ -623,7 +642,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);
|
||||
|
@ -25,7 +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.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
@ -58,6 +61,7 @@ public class FindPhoneActivity extends AbstractGBActivity {
|
||||
}
|
||||
};
|
||||
|
||||
Vibrator mVibrator;
|
||||
AudioManager mAudioManager;
|
||||
int userVolume;
|
||||
MediaPlayer mp;
|
||||
@ -79,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);
|
||||
@ -107,7 +127,11 @@ public class FindPhoneActivity extends AbstractGBActivity {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@ -116,7 +140,10 @@ public class FindPhoneActivity extends AbstractGBActivity {
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
stopVibration();
|
||||
stopSound();
|
||||
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
unregisterReceiver(mReceiver);
|
||||
}
|
||||
|
@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
|
||||
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;
|
||||
@ -96,6 +97,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) {
|
||||
|
@ -0,0 +1,129 @@
|
||||
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);
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -26,16 +26,20 @@ import android.os.Bundle;
|
||||
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;
|
||||
@ -51,10 +55,6 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
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.AbstractGBFragment;
|
||||
@ -572,7 +572,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);
|
||||
}
|
||||
|
||||
@ -753,14 +753,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 +769,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 +781,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 +791,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 +799,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);
|
||||
|
@ -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;
|
||||
@ -88,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());
|
||||
@ -368,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();
|
||||
|
||||
|
@ -28,15 +28,15 @@ 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);
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,30 @@
|
||||
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();
|
||||
}}
|
@ -255,12 +255,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;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* Copyright (C) 2015-2019 Andreas Shimokawa
|
||||
/* Copyright (C) 2015-2019 Andreas Shimokawa, Carsten Pfeiffer, Christian
|
||||
Fischer, Daniele Gobbetti, José Rebelo, Szymon Tomasz Stefanek
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -346,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);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,102 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -32,29 +32,27 @@ 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.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
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;
|
||||
@ -83,44 +81,40 @@ public class SleepChartFragment extends AbstractChartFragment {
|
||||
}
|
||||
|
||||
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 +126,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 +290,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 +313,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -17,13 +17,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;
|
||||
@ -115,30 +111,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);
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
@ -82,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;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,8 @@
|
||||
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";
|
||||
}
|
@ -16,19 +16,21 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Constants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreferenceFragment;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_ACTIVATE_DISPLAY_ON_LIFT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DATEFORMAT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISCONNECT_NOTIFICATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISCONNECT_NOTIFICATION_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISCONNECT_NOTIFICATION_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ITEMS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ON_LIFT_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ON_LIFT_START;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_EXPOSE_HR_THIRDPARTY;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_LANGUAGE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB_END;
|
||||
@ -287,6 +289,10 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
|
||||
addPreferenceHandlerFor(PREF_DATEFORMAT);
|
||||
addPreferenceHandlerFor(PREF_DISPLAY_ITEMS);
|
||||
addPreferenceHandlerFor(PREF_LANGUAGE);
|
||||
addPreferenceHandlerFor(PREF_EXPOSE_HR_THIRDPARTY);
|
||||
addPreferenceHandlerFor(PREF_WEARLOCATION);
|
||||
addPreferenceHandlerFor(PREF_SCREEN_ORIENTATION);
|
||||
addPreferenceHandlerFor(PREF_TIMEFORMAT);
|
||||
|
||||
String displayOnLiftState = prefs.getString(PREF_ACTIVATE_DISPLAY_ON_LIFT, PREF_DO_NOT_DISTURB_OFF);
|
||||
boolean displayOnLiftScheduled = displayOnLiftState.equals(PREF_DO_NOT_DISTURB_SCHEDULED);
|
||||
@ -364,15 +370,25 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
|
||||
});
|
||||
}
|
||||
|
||||
EditTextPreference pref = findPreference(MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS);
|
||||
if (pref != null) {
|
||||
pref.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
|
||||
EditTextPreference mibandTimeOffset = findPreference(MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS);
|
||||
if (mibandTimeOffset != null) {
|
||||
mibandTimeOffset.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
|
||||
@Override
|
||||
public void onBindEditText(@NonNull EditText editText) {
|
||||
editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EditTextPreference findPhoneDuration = findPreference(MakibesHR3Constants.PREF_FIND_PHONE_DURATION);
|
||||
if (findPhoneDuration != null) {
|
||||
findPhoneDuration.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
|
||||
@Override
|
||||
public void onBindEditText(@NonNull EditText editText) {
|
||||
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static DeviceSpecificSettingsFragment newInstance(String settingsFileSuffix, @NonNull int[] supportedSettings) {
|
||||
|
@ -33,11 +33,14 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getPrefs;
|
||||
|
||||
public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractDeviceCoordinator.class);
|
||||
@ -69,6 +72,17 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
if (gbDevice.isConnected() || gbDevice.isConnecting()) {
|
||||
GBApplication.deviceService().disconnect();
|
||||
}
|
||||
Prefs prefs = getPrefs();
|
||||
String lastDevice = prefs.getPreferences().getString("last_device_address","");
|
||||
if (gbDevice.getAddress() == lastDevice){
|
||||
LOG.debug("#1605 removing last device");
|
||||
prefs.getPreferences().edit().remove("last_device_address").apply();
|
||||
}
|
||||
String macAddress = prefs.getPreferences().getString(MiBandConst.PREF_MIBAND_ADDRESS,"");
|
||||
if (gbDevice.getAddress() == macAddress){
|
||||
LOG.debug("#1605 removing devel miband");
|
||||
prefs.getPreferences().edit().remove(MiBandConst.PREF_MIBAND_ADDRESS).apply();
|
||||
}
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
DaoSession session = dbHandler.getDaoSession();
|
||||
Device device = DBHelper.findDevice(gbDevice, session);
|
||||
@ -122,7 +136,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice device) {
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_ASK;
|
||||
}
|
||||
|
||||
@ -145,6 +159,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public int[] getColorPresets() {
|
||||
return new int[0];
|
||||
|
@ -29,7 +29,6 @@ import java.util.Collection;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
@ -63,6 +62,11 @@ public interface DeviceCoordinator {
|
||||
*/
|
||||
int BONDING_STYLE_ASK = 2;
|
||||
|
||||
/**
|
||||
* A secret key has to be entered before connecting
|
||||
*/
|
||||
int BONDING_STYLE_REQUIRE_KEY = 3;
|
||||
|
||||
/**
|
||||
* Checks whether this coordinator handles the given candidate.
|
||||
* Returns the supported device type for the given candidate or
|
||||
@ -224,9 +228,8 @@ public interface DeviceCoordinator {
|
||||
|
||||
/**
|
||||
* Returns how/if the given device should be bonded before connecting to it.
|
||||
* @param device
|
||||
*/
|
||||
int getBondingStyle(GBDevice device);
|
||||
int getBondingStyle();
|
||||
|
||||
/**
|
||||
* Indicates whether the device has some kind of calender we can sync to.
|
||||
|
@ -93,6 +93,7 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
sampleProvider = new UnknownSampleProvider();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
return DeviceType.UNKNOWN;
|
||||
@ -197,6 +198,7 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public int[] getColorPresets() {
|
||||
return new int[0];
|
||||
|
@ -58,7 +58,7 @@ public class CasioGB6900DeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice deviceCandidate){
|
||||
public int getBondingStyle(){
|
||||
return BONDING_STYLE_BOND;
|
||||
}
|
||||
|
||||
|
@ -128,8 +128,6 @@ public final class HPlusConstants {
|
||||
|
||||
public static final String PREF_HPLUS_SCREENTIME = "hplus_screentime";
|
||||
public static final String PREF_HPLUS_ALLDAYHR = "hplus_alldayhr";
|
||||
public static final String PREF_HPLUS_TIMEFORMAT = "hplus_timeformat";
|
||||
public static final String PREF_HPLUS_WRIST = "hplus_wrist";
|
||||
public static final String PREF_HPLUS_SIT_START_TIME = "hplus_sit_start_time";
|
||||
public static final String PREF_HPLUS_SIT_END_TIME = "hplus_sit_end_time";
|
||||
public static final String PREF_HPLUS_UNICODE = "hplus_unicode";
|
||||
|
@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
@ -83,7 +84,7 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice deviceCandidate){
|
||||
public int getBondingStyle(){
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@ -196,10 +197,10 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
public static byte getTimeMode(String address) {
|
||||
String tmode = prefs.getString(HPlusConstants.PREF_HPLUS_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h));
|
||||
public static byte getTimeMode(String deviceAddress) {
|
||||
String tmode = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress).getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, "24h");
|
||||
|
||||
if(tmode.equals(getContext().getString(R.string.p_timeformat_24h))) {
|
||||
if ("24h".equals(tmode)) {
|
||||
return HPlusConstants.ARG_TIMEMODE_24H;
|
||||
}else{
|
||||
return HPlusConstants.ARG_TIMEMODE_12H;
|
||||
@ -269,12 +270,14 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
|
||||
return (byte) 255;
|
||||
}
|
||||
|
||||
public static byte getUserWrist(String address) {
|
||||
String value = prefs.getString(HPlusConstants.PREF_HPLUS_WRIST, getContext().getString(R.string.left));
|
||||
//FIXME: unused
|
||||
public static byte getUserWrist(String deviceAddress) {
|
||||
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
|
||||
String value = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left");
|
||||
|
||||
if(value.equals(getContext().getString(R.string.left))){
|
||||
if ("left".equals(value)) {
|
||||
return HPlusConstants.ARG_WRIST_LEFT;
|
||||
}else{
|
||||
} else {
|
||||
return HPlusConstants.ARG_WRIST_RIGHT;
|
||||
}
|
||||
}
|
||||
@ -290,10 +293,19 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
|
||||
public static void setUnicodeSupport(String address, boolean state){
|
||||
SharedPreferences.Editor editor = prefs.getPreferences().edit();
|
||||
editor.putBoolean(HPlusConstants.PREF_HPLUS_UNICODE + "_" + address, state);
|
||||
editor.commit();
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public static boolean getUnicodeSupport(String address){
|
||||
return (prefs.getBoolean(HPlusConstants.PREF_HPLUS_UNICODE + "_" + address, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
//R.xml.devicesettings_wearlocation, // disabled, since it is never used in code
|
||||
R.xml.devicesettings_timeformat
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -60,8 +60,8 @@ public class HuamiConst {
|
||||
|
||||
public static final String PREF_DISPLAY_ITEMS = "display_items";
|
||||
public static final String PREF_LANGUAGE = "language";
|
||||
public static final String PREF_DATEFORMAT = "dateformat";
|
||||
|
||||
public static final String PREF_EXPOSE_HR_THIRDPARTY = "expose_hr_thirdparty";
|
||||
public static final String PREF_USE_CUSTOM_FONT = "use_custom_font";
|
||||
|
||||
public static int toActivityKind(int rawType) {
|
||||
switch (rawType) {
|
||||
|
@ -197,6 +197,11 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator {
|
||||
return prefs.getStringSet(HuamiConst.PREF_DISPLAY_ITEMS, null);
|
||||
}
|
||||
|
||||
public static boolean getUseCustomFont(String deviceAddress) {
|
||||
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
|
||||
return prefs.getBoolean(HuamiConst.PREF_USE_CUSTOM_FONT, false);
|
||||
}
|
||||
|
||||
public static boolean getGoalNotification() {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
return prefs.getBoolean(MiBandConst.PREF_MI2_GOAL_NOTIFICATION, false);
|
||||
@ -251,6 +256,11 @@ public abstract class HuamiCoordinator extends AbstractDeviceCoordinator {
|
||||
return prefs.getBoolean(MiBandConst.PREF_SWIPE_UNLOCK, false);
|
||||
}
|
||||
|
||||
public static boolean getExposeHRThirdParty(String deviceAddress) {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
return prefs.getBoolean(HuamiConst.PREF_EXPOSE_HR_THIRDPARTY, false);
|
||||
}
|
||||
|
||||
protected static Date getTimePreference(String key, String defaultValue, String deviceAddress) {
|
||||
Prefs prefs;
|
||||
|
||||
|
@ -139,6 +139,8 @@ public class HuamiService {
|
||||
public static final byte[] DATEFORMAT_TIME_12_HOURS = new byte[] {ENDPOINT_DISPLAY, 0x02, 0x0, 0x0 };
|
||||
public static final byte[] DATEFORMAT_TIME_24_HOURS = new byte[] {ENDPOINT_DISPLAY, 0x02, 0x0, 0x1 };
|
||||
public static final byte[] DATEFORMAT_DATE_MM_DD_YYYY = new byte[]{ENDPOINT_DISPLAY, 30, 0x00, 'M', 'M', '/', 'd', 'd', '/', 'y', 'y', 'y', 'y'};
|
||||
public static final byte[] COMMAND_ENBALE_HR_CONNECTION = new byte[]{ENDPOINT_DISPLAY, 0x01, 0x00, 0x01};
|
||||
public static final byte[] COMMAND_DISABLE_HR_CONNECTION = new byte[]{ENDPOINT_DISPLAY, 0x01, 0x00, 0x00};
|
||||
public static final byte[] COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x01};
|
||||
public static final byte[] COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x00};
|
||||
public static final byte[] COMMAND_SCHEDULE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};
|
||||
|
@ -47,7 +47,7 @@ public class AmazfitBipCoordinator extends HuamiCoordinator {
|
||||
try {
|
||||
BluetoothDevice device = candidate.getDevice();
|
||||
String name = device.getName();
|
||||
if (name != null && name.equalsIgnoreCase("Amazfit Bip Watch")) {
|
||||
if (name != null && (name.equalsIgnoreCase("Amazfit Bip Watch"))) {
|
||||
return DeviceType.AMAZFITBIP;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
@ -81,8 +81,11 @@ public class AmazfitBipCoordinator extends HuamiCoordinator {
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_amazfitbip,
|
||||
R.xml.devicesettings_wearlocation,
|
||||
R.xml.devicesettings_custom_emoji_font,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_disconnectnotification,
|
||||
R.xml.devicesettings_expose_hr_thirdparty,
|
||||
R.xml.devicesettings_pairingkey
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
/* Copyright (C) 2017-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, João Paulo Barraca
|
||||
|
||||
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.devices.huami.amazfitbip;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class AmazfitBipLiteCoordinator extends AmazfitBipCoordinator {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipLiteCoordinator.class);
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.AMAZFITBIP_LITE;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
BluetoothDevice device = candidate.getDevice();
|
||||
String name = device.getName();
|
||||
if (name != null && name.equalsIgnoreCase("Amazfit Bip Lite")) {
|
||||
return DeviceType.AMAZFITBIP_LITE;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.error("unable to check device support", ex);
|
||||
}
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_REQUIRE_KEY;
|
||||
}
|
||||
}
|
@ -84,8 +84,12 @@ public class AmazfitCorCoordinator extends HuamiCoordinator {
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_amazfitcor,
|
||||
R.xml.devicesettings_wearlocation,
|
||||
R.xml.devicesettings_custom_emoji_font,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_disconnectnotification,
|
||||
R.xml.devicesettings_pairingkey};
|
||||
R.xml.devicesettings_expose_hr_thirdparty,
|
||||
R.xml.devicesettings_pairingkey
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -86,8 +86,12 @@ public class AmazfitCor2Coordinator extends HuamiCoordinator {
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_amazfitcor,
|
||||
R.xml.devicesettings_wearlocation,
|
||||
R.xml.devicesettings_custom_emoji_font,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_disconnectnotification,
|
||||
R.xml.devicesettings_pairingkey};
|
||||
R.xml.devicesettings_expose_hr_thirdparty,
|
||||
R.xml.devicesettings_pairingkey
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -46,11 +46,6 @@ public class MiBand2Coordinator extends HuamiCoordinator {
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
if (candidate.supportsService(HuamiService.UUID_SERVICE_MIBAND2_SERVICE)) {
|
||||
return DeviceType.MIBAND2;
|
||||
}
|
||||
|
||||
// and a heuristic for now
|
||||
try {
|
||||
BluetoothDevice device = candidate.getDevice();
|
||||
String name = device.getName();
|
||||
@ -84,9 +79,11 @@ public class MiBand2Coordinator extends HuamiCoordinator {
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_miband2,
|
||||
R.xml.devicesettings_wearlocation,
|
||||
R.xml.devicesettings_donotdisturb_withauto,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_rotatewrist_cycleinfo,
|
||||
R.xml.devicesettings_expose_hr_thirdparty,
|
||||
R.xml.devicesettings_pairingkey
|
||||
};
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
|
||||
@ -75,4 +77,15 @@ public class MiBand2HRXCoordinator extends HuamiCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_miband2,
|
||||
R.xml.devicesettings_wearlocation,
|
||||
R.xml.devicesettings_donotdisturb_withauto,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_rotatewrist_cycleinfo,
|
||||
R.xml.devicesettings_pairingkey
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -103,11 +103,13 @@ public class MiBand3Coordinator extends HuamiCoordinator {
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_miband3,
|
||||
R.xml.devicesettings_wearlocation,
|
||||
R.xml.devicesettings_dateformat,
|
||||
R.xml.devicesettings_nightmode,
|
||||
R.xml.devicesettings_donotdisturb_withauto,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_swipeunlock,
|
||||
R.xml.devicesettings_expose_hr_thirdparty,
|
||||
R.xml.devicesettings_pairingkey
|
||||
};
|
||||
}
|
||||
|
@ -88,12 +88,18 @@ public class MiBand4Coordinator extends HuamiCoordinator {
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_miband3,
|
||||
R.xml.devicesettings_wearlocation,
|
||||
R.xml.devicesettings_dateformat,
|
||||
R.xml.devicesettings_nightmode,
|
||||
R.xml.devicesettings_donotdisturb_withauto,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_swipeunlock,
|
||||
R.xml.devicesettings_expose_hr_thirdparty,
|
||||
R.xml.devicesettings_pairingkey
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_REQUIRE_KEY;
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport.BASE_UUID;
|
||||
|
||||
public class ID115Constants {
|
||||
public static final String PREF_WRIST = "id115_wrist";
|
||||
public static final String PREF_SCREEN_ORIENTATION = "id115_screen_orientation";
|
||||
|
||||
public static final UUID UUID_SERVICE_ID115 = UUID.fromString(String.format(BASE_UUID, "0AF0"));
|
||||
public static final UUID UUID_CHARACTERISTIC_WRITE_NORMAL = UUID.fromString(String.format(BASE_UUID, "0AF6"));
|
||||
public static final UUID UUID_CHARACTERISTIC_NOTIFY_NORMAL = UUID.fromString(String.format(BASE_UUID, "0AF7"));
|
||||
|
@ -31,6 +31,7 @@ import java.util.Collections;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
@ -65,7 +66,7 @@ public class ID115Coordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice deviceCandidate){
|
||||
public int getBondingStyle(){
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@ -154,4 +155,12 @@ public class ID115Coordinator extends AbstractDeviceCoordinator {
|
||||
public boolean supportsFindDevice() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_wearlocation,
|
||||
R.xml.devicesettings_screenorientation
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ public class BFH16DeviceCoordinator extends AbstractDeviceCoordinator
|
||||
return Collections.singletonList(filter);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
|
||||
@ -85,7 +86,7 @@ public class BFH16DeviceCoordinator extends AbstractDeviceCoordinator
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice deviceCandidate){
|
||||
public int getBondingStyle(){
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ public class TeclastH30Coordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice deviceCandidate){
|
||||
public int getBondingStyle(){
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,349 @@
|
||||
/* Copyright (C) 2016-2019 Andreas Shimokawa, Carsten Pfeiffer, João
|
||||
Paulo Barraca
|
||||
|
||||
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.devices.makibeshr3;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public final class MakibesHR3Constants {
|
||||
|
||||
// TODO: This doesn't belong here, but I don't want to touch other files to avoid
|
||||
// TODO: breaking someone else's device support.
|
||||
public static final String PREF_HEADS_UP_SCREEN = "activate_display_on_lift_wrist";
|
||||
public static final String PREF_LOST_REMINDER = "disconnect_notification";
|
||||
public static final String PREF_DO_NOT_DISTURB = "do_not_disturb_no_auto";
|
||||
public static final String PREF_DO_NOT_DISTURB_START = "do_not_disturb_no_auto_start";
|
||||
public static final String PREF_DO_NOT_DISTURB_END = "do_not_disturb_no_auto_end";
|
||||
public static final String PREF_FIND_PHONE = "prefs_find_phone";
|
||||
public static final String PREF_FIND_PHONE_DURATION = "prefs_find_phone_duration";
|
||||
|
||||
public static final UUID UUID_SERVICE = UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final UUID UUID_CHARACTERISTIC_CONTROL = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final UUID UUID_CHARACTERISTIC_REPORT = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
|
||||
// Services and Characteristics
|
||||
// 00001801-0000-1000-8000-00805f9b34fb
|
||||
// 00002a05-0000-1000-8000-00805f9b34fb
|
||||
// 00001800-0000-1000-8000-00805f9b34fb
|
||||
// 00002a00-0000-1000-8000-00805f9b34fb
|
||||
// 00002a01-0000-1000-8000-00805f9b34fb
|
||||
// 00002a02-0000-1000-8000-00805f9b34fb
|
||||
// 00002a04-0000-1000-8000-00805f9b34fb
|
||||
// 00002aa6-0000-1000-8000-00805f9b34fb
|
||||
// 6e400001-b5a3-f393-e0a9-e50e24dcca9e // Nordic UART Service
|
||||
// 6e400002-b5a3-f393-e0a9-e50e24dcca9e // control (RX)
|
||||
// 6e400003-b5a3-f393-e0a9-e50e24dcca9e // report
|
||||
// 0000fee7-0000-1000-8000-00805f9b34fb
|
||||
// 0000fec9-0000-1000-8000-00805f9b34fb
|
||||
// 0000fea1-0000-1000-8000-00805f9b34fb
|
||||
// 0000fea2-0000-1000-8000-00805f9b34fb
|
||||
|
||||
// Command structure
|
||||
// ab 00 [argument_count] ff [command] 80 [arguments]
|
||||
// where [argument_count] is [arguments].length + 3
|
||||
// 80 might by different.
|
||||
|
||||
|
||||
public static final byte[] DATA_TEMPLATE = {
|
||||
(byte) 0xab,
|
||||
(byte) 0x00,
|
||||
(byte) 0, // argument_count
|
||||
(byte) 0xff,
|
||||
(byte) 0, // command
|
||||
(byte) 0x80
|
||||
// ,arguments
|
||||
};
|
||||
|
||||
public static final int DATA_ARGUMENT_COUNT_INDEX = 2;
|
||||
public static final int DATA_COMMAND_INDEX = 4;
|
||||
public static final int DATA_ARGUMENTS_INDEX = 6;
|
||||
|
||||
// blood oxygen percentage
|
||||
public static final byte[] RPRT_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x12 };
|
||||
|
||||
|
||||
// blood oxygen percentage
|
||||
// blood oxygen percentage
|
||||
public static final byte[] RPRT_SINGLE_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x11 };
|
||||
|
||||
|
||||
// steps might take up more bytes. I don't know which ones and I won't walk that much.
|
||||
// Only sent after we send CMD_51
|
||||
// 00 (maybe also used for steps)
|
||||
// [steps hi]
|
||||
// [steps lo]
|
||||
// 00
|
||||
// 00
|
||||
// 01 (also was 0b. Maybe minutes of activity.)
|
||||
// 00
|
||||
// 00
|
||||
// 00
|
||||
// 00
|
||||
// 00
|
||||
public static final byte[] RPRT_FITNESS = new byte[]{ (byte) 0x51, 0x08 };
|
||||
|
||||
|
||||
// year (+2000)
|
||||
// month
|
||||
// day
|
||||
// hour
|
||||
// minute
|
||||
// heart rate
|
||||
// heart rate
|
||||
public static final byte[] RPRT_HEART_RATE_SAMPLE = new byte[]{ (byte) 0x51, (byte) 0x11 };
|
||||
|
||||
|
||||
// WearFit says "walking" in the step details. This is probably also in here, but
|
||||
// I don't run :O
|
||||
// year (+2000)
|
||||
// month
|
||||
// day
|
||||
// hour (start of measurement. interval is 1h. Might be longer when running.)
|
||||
// 00 (either used for steps or minute)
|
||||
// accumulated steps (hi)
|
||||
// accumulated steps (lo)
|
||||
// 00
|
||||
// 00
|
||||
// ?? (changes whenever steps change. Ranges from 00 to 16.)
|
||||
// 00
|
||||
// 00
|
||||
// 00
|
||||
// 00
|
||||
public static final byte[] RPRT_STEPS_SAMPLE = new byte[]{ (byte) 0x51, (byte) 0x20 };
|
||||
|
||||
|
||||
// enable (00/01)
|
||||
public static final byte RPRT_REVERSE_FIND_DEVICE = (byte) 0x7d;
|
||||
|
||||
|
||||
// The proximity sensor sees air..
|
||||
public static final byte ARG_HEARTRATE_NO_TARGET = (byte) 0xff;
|
||||
// The hr sensor didn't find the heart rate yet.
|
||||
public static final byte ARG_HEARTRATE_NO_READING = (byte) 0x00;
|
||||
|
||||
// heart rate
|
||||
public static final byte RPRT_HEARTRATE = (byte) 0x84;
|
||||
|
||||
|
||||
// charging (00/01)
|
||||
// battery percentage (step size is 20).
|
||||
public static final byte RPRT_BATTERY = (byte) 0x91;
|
||||
|
||||
// firmware_major
|
||||
// firmware_minor
|
||||
// 37
|
||||
// 00
|
||||
// 00
|
||||
// 00
|
||||
// 00
|
||||
// 00
|
||||
// 00
|
||||
// 20
|
||||
// 0e
|
||||
public static final byte RPRT_SOFTWARE = (byte) 0x92;
|
||||
|
||||
// 00
|
||||
public static final byte CMD_FACTORY_RESET = (byte) 0x23;
|
||||
|
||||
|
||||
// enable (00/01)
|
||||
public static final byte[] CMD_SET_REAL_TIME_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x12 };
|
||||
|
||||
|
||||
// After disabling, the watch replies with RPRT_SINGLE_BLOOD_OXYGEN
|
||||
// enable (00/01)
|
||||
public static final byte[] CMD_SET_SINGLE_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x11 };
|
||||
|
||||
// device replies with
|
||||
// {@link MakibesHR3Constants#RPRT_HEART_RATE_SAMPLE}
|
||||
// {@link MakibesHR3Constants#RPRT_STEPS_SAMPLE} (Only if steps are non-zero)
|
||||
// {@link MakibesHR3Constants#RPRT_FITNESS}
|
||||
// there are also multiple 6 * 00 reports
|
||||
// 00
|
||||
// year (+2000) steps after
|
||||
// month steps after
|
||||
// day steps after
|
||||
// hour steps after
|
||||
// minute steps after
|
||||
// year (+2000) heart rate after
|
||||
// month heart rate after
|
||||
// day heart rate after
|
||||
// hour heart rate after
|
||||
// minute heart rate after
|
||||
public static final byte CMD_REQUEST_FITNESS = (byte) 0x51;
|
||||
|
||||
|
||||
// Manually sending this doesn't yield a reply. The heart rate history is sent in response to
|
||||
// CMD_CMD_REQUEST_FITNESS.
|
||||
// 00
|
||||
// year (+2000) (probably not current)
|
||||
// month (not current!)
|
||||
// day (not current!)
|
||||
// hour (current)
|
||||
// minute (current)
|
||||
public static final byte CMD_52 = (byte) 0x52;
|
||||
|
||||
|
||||
// vibrates 6 times
|
||||
public static final byte CMD_FIND_DEVICE = (byte) 0x71;
|
||||
|
||||
|
||||
// WearFit writes uses other sources as well. They don't do anything though.
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_CALL = (byte) 0x01;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_STOP_CALL = (byte) 0x02;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_MESSAGE = (byte) 0x03;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_QQ = (byte) 0x07;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_WECHAT = (byte) 0x09;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_WHATSAPP = (byte) 0x0a;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_LINE = (byte) 0x0e;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_TWITTER = (byte) 0x0f;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_FACEBOOK = (byte) 0x10;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_FACEBOOK2 = (byte) 0x11;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_WEIBO = (byte) 0x13;
|
||||
public static final byte ARG_SEND_NOTIFICATION_SOURCE_KAKOTALK = (byte) 0x14;
|
||||
// ARG_SET_NOTIFICATION_SOURCE_*
|
||||
// 02 (This is 00 and 01 during connection. I don't know what it does. Maybe clears notifications?)
|
||||
// ASCII
|
||||
public static final byte CMD_SEND_NOTIFICATION = (byte) 0x72;
|
||||
|
||||
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_WEEKDAY = (byte) 0x1F;
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_EVERY_DAY = (byte) 0x7F;
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_ONE_TIME = (byte) 0x80;
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_MONDAY = (byte) 0x01;
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_TUESDAY = (byte) 0x02;
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_WEDNESDAY = (byte) 0x04;
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_THURSDAY = (byte) 0x08;
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_FRIDAY = (byte) 0x10;
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_SATURDAY = (byte) 0x20;
|
||||
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_SUNDAY = (byte) 0x40;
|
||||
|
||||
// reminder id starting at 0
|
||||
// enable (00/01)
|
||||
// hour
|
||||
// minute
|
||||
// bit field of ARG_SET_ALARM_REMINDER_REPEAT_*
|
||||
public static final byte CMD_SET_ALARM_REMINDER = (byte) 0x73;
|
||||
|
||||
|
||||
public static final byte ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_MILES = (byte) 0x00;
|
||||
public static final byte ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_KILOMETERS = (byte) 0x01;
|
||||
// step length (in/cm)
|
||||
// step length (in/cm)
|
||||
// age (years)
|
||||
// height (in/cm)
|
||||
// weight (lb/kg)
|
||||
// ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_*
|
||||
// target step count (kilo)
|
||||
// 5a
|
||||
// 82
|
||||
// 3c
|
||||
// 5a
|
||||
// 28
|
||||
// b4
|
||||
// 5d
|
||||
// 64
|
||||
public static final byte CMD_SET_PERSONAL_INFORMATION = (byte) 0x74;
|
||||
|
||||
|
||||
// enable (00/01)
|
||||
// start hour
|
||||
// start minute
|
||||
// end hour
|
||||
// end minute
|
||||
// 2d
|
||||
public static final byte CMD_SET_SEDENTARY_REMINDER = (byte) 0x75;
|
||||
|
||||
|
||||
// enable (00/01)
|
||||
// start hour
|
||||
// start minute
|
||||
// end hour
|
||||
// end minute
|
||||
public static final byte CMD_SET_QUITE_HOURS = (byte) 0x76;
|
||||
|
||||
|
||||
// enable (00/01)
|
||||
public static final byte CMD_SET_HEADS_UP_SCREEN = (byte) 0x77;
|
||||
|
||||
|
||||
// Looks like enable/disable.
|
||||
public static final byte CMD_78 = (byte) 0x78;
|
||||
|
||||
|
||||
// The watch enters photograph mode, but doesn't appear to send a trigger signal.
|
||||
// enable (00/01)
|
||||
public static final byte CMD_SET_PHOTOGRAPH_MODE = (byte) 0x79;
|
||||
|
||||
|
||||
// enable (00/01)
|
||||
public static final byte CMD_SET_LOST_REMINDER = (byte) 0x7a;
|
||||
|
||||
|
||||
// 7b has 1 argument. Looks like enable/disable.
|
||||
|
||||
public static final byte ARG_SET_TIMEMODE_24H = 0x00;
|
||||
public static final byte ARG_SET_TIMEMODE_12H = 0x01;
|
||||
// ARG_SET_TIMEMODE_*
|
||||
public static final byte CMD_SET_TIMEMODE = (byte) 0x7c;
|
||||
|
||||
|
||||
// 14 arguments. Watch might reply with RPRT_BATTERY.
|
||||
public static final byte CMD_7e = (byte) 0x7e;
|
||||
|
||||
|
||||
// 01
|
||||
// fall hour
|
||||
// fall minute
|
||||
// awake hour
|
||||
// awake minute
|
||||
public static final byte CMD_SET_SLEEP_TIME = (byte) 0x7f;
|
||||
|
||||
|
||||
// enable (00/01)
|
||||
public static final byte CMD_SET_REAL_TIME_HEART_RATE = (byte) 0x84;
|
||||
|
||||
|
||||
// looks like enable/disable.
|
||||
public static final byte CMD_85 = (byte) 0x85;
|
||||
|
||||
|
||||
// 00
|
||||
// year hi
|
||||
// year lo
|
||||
// month
|
||||
// day
|
||||
// hour
|
||||
// minute
|
||||
// second
|
||||
public static final byte CMD_SET_DATE_TIME = (byte) 0x93;
|
||||
|
||||
// 3 arguments. Sent when saving personal information.
|
||||
public static final byte CMD_95 = (byte) 0x95;
|
||||
|
||||
// looks like enable/disable.
|
||||
public static final byte CMD_96 = (byte) 0x96;
|
||||
|
||||
|
||||
// looks like enable/disable.
|
||||
public static final byte CMD_e5 = (byte) 0xe5;
|
||||
|
||||
|
||||
// If this is sent after {@link CMD_FACTORY_RESET}, it's a shutdown, not a reboot.
|
||||
// Rebooting resets the watch face and wallpaper.
|
||||
public static final byte CMD_REBOOT = (byte) 0xff;
|
||||
}
|
@ -0,0 +1,264 @@
|
||||
/* Copyright (C) 2017-2019 Daniele Gobbetti, João Paulo Barraca, tiparega
|
||||
|
||||
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.devices.makibeshr3;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext;
|
||||
|
||||
|
||||
public class MakibesHR3Coordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
public static final int FindPhone_ON = -1;
|
||||
public static final int FindPhone_OFF = 0;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MakibesHR3Coordinator.class);
|
||||
|
||||
|
||||
public static boolean shouldEnableHeadsUpScreen(SharedPreferences sharedPrefs) {
|
||||
String liftMode = sharedPrefs.getString(MakibesHR3Constants.PREF_HEADS_UP_SCREEN, getContext().getString(R.string.p_on));
|
||||
|
||||
// Makibes HR3 doesn't support scheduled intervals. Treat it as "on".
|
||||
return !liftMode.equals(getContext().getString(R.string.p_off));
|
||||
}
|
||||
|
||||
public static boolean shouldEnableLostReminder(SharedPreferences sharedPrefs) {
|
||||
String lostReminder = sharedPrefs.getString(MakibesHR3Constants.PREF_LOST_REMINDER, getContext().getString(R.string.p_on));
|
||||
|
||||
// Makibes HR3 doesn't support scheduled intervals. Treat it as "on".
|
||||
return !lostReminder.equals(getContext().getString(R.string.p_off));
|
||||
}
|
||||
|
||||
public static byte getTimeMode(SharedPreferences sharedPrefs) {
|
||||
String timeMode = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h));
|
||||
|
||||
if (timeMode.equals(getContext().getString(R.string.p_timeformat_24h))) {
|
||||
return MakibesHR3Constants.ARG_SET_TIMEMODE_24H;
|
||||
} else {
|
||||
return MakibesHR3Constants.ARG_SET_TIMEMODE_12H;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param startOut out Only hour/minute are used.
|
||||
* @param endOut out Only hour/minute are used.
|
||||
* @return True if quite hours are enabled.
|
||||
*/
|
||||
public static boolean getQuiteHours(SharedPreferences sharedPrefs, Calendar startOut, Calendar endOut) {
|
||||
String doNotDisturb = sharedPrefs.getString(MakibesHR3Constants.PREF_DO_NOT_DISTURB, getContext().getString(R.string.p_off));
|
||||
|
||||
if (doNotDisturb.equals(getContext().getString(R.string.p_off))) {
|
||||
return false;
|
||||
} else {
|
||||
String start = sharedPrefs.getString(MakibesHR3Constants.PREF_DO_NOT_DISTURB_START, "00:00");
|
||||
String end = sharedPrefs.getString(MakibesHR3Constants.PREF_DO_NOT_DISTURB_END, "00:00");
|
||||
|
||||
DateFormat df = new SimpleDateFormat("HH:mm");
|
||||
|
||||
try {
|
||||
startOut.setTime(df.parse(start));
|
||||
endOut.setTime(df.parse(end));
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unexpected exception in MiBand2Coordinator.getTime: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@link #FindPhone_OFF}, {@link #FindPhone_ON}, or the duration
|
||||
*/
|
||||
public static int getFindPhone(SharedPreferences sharedPrefs) {
|
||||
String findPhone = sharedPrefs.getString(MakibesHR3Constants.PREF_FIND_PHONE, getContext().getString(R.string.p_off));
|
||||
|
||||
if (findPhone.equals(getContext().getString(R.string.p_off))) {
|
||||
return FindPhone_OFF;
|
||||
} else if (findPhone.equals(getContext().getString(R.string.p_on))) {
|
||||
return FindPhone_ON;
|
||||
} else { // Duration
|
||||
String duration = sharedPrefs.getString(MakibesHR3Constants.PREF_FIND_PHONE_DURATION, "0");
|
||||
|
||||
try {
|
||||
int iDuration;
|
||||
|
||||
try {
|
||||
iDuration = Integer.valueOf(duration);
|
||||
} catch (Exception ex) {
|
||||
LOG.warn(ex.getMessage());
|
||||
iDuration = 60;
|
||||
}
|
||||
|
||||
return iDuration;
|
||||
} catch (Exception e) {
|
||||
LOG.error("Unexpected exception in MiBand2Coordinator.getTime: " + e.getMessage());
|
||||
return FindPhone_ON;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
String name = candidate.getDevice().getName();
|
||||
|
||||
// TODO: Device discovery
|
||||
if ((name != null) && name.equals("Y808")) {
|
||||
return DeviceType.MAKIBESHR3;
|
||||
}
|
||||
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
Long deviceId = device.getId();
|
||||
QueryBuilder<?> qb = session.getMakibesHR3ActivitySampleDao().queryBuilder();
|
||||
qb.where(MakibesHR3ActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.MAKIBESHR3;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new MakibesHR3SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Makibes";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_timeformat,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_disconnectnotification,
|
||||
R.xml.devicesettings_donotdisturb_no_auto,
|
||||
R.xml.devicesettings_find_phone
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/* Copyright (C) 2018-2019 Daniele Gobbetti, Sebastian Kranz
|
||||
|
||||
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.devices.makibeshr3;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class MakibesHR3SampleProvider extends AbstractSampleProvider<MakibesHR3ActivitySample> {
|
||||
|
||||
private GBDevice mDevice;
|
||||
private DaoSession mSession;
|
||||
|
||||
public MakibesHR3SampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
|
||||
mSession = session;
|
||||
mDevice = device;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int normalizeType(int rawType) {
|
||||
return rawType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(int activityKind) {
|
||||
return activityKind;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(int rawIntensity) {
|
||||
return rawIntensity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MakibesHR3ActivitySample createActivitySample() {
|
||||
return new MakibesHR3ActivitySample();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<MakibesHR3ActivitySample, ?> getSampleDao() {
|
||||
return getSession().getMakibesHR3ActivitySampleDao();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return MakibesHR3ActivitySampleDao.Properties.RawKind;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return MakibesHR3ActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return MakibesHR3ActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
}
|
@ -17,17 +17,12 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Version;
|
||||
|
||||
public final class MiBandConst {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MiBandConst.class);
|
||||
|
||||
public static final String PREF_USER_ALIAS = "mi_user_alias";
|
||||
public static final String PREF_MIBAND_WEARSIDE = "mi_wearside";
|
||||
public static final String PREF_MIBAND_ADDRESS = "development_miaddr"; // FIXME: should be prefixed mi_
|
||||
public static final String PREF_MIBAND_ALARMS = "mi_alarms";
|
||||
public static final String PREF_MIBAND_DONT_ACK_TRANSFER = "mi_dont_ack_transfer";
|
||||
|
@ -22,7 +22,6 @@ import android.app.Activity;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelUuid;
|
||||
@ -38,6 +37,7 @@ import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
@ -228,10 +228,10 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
|
||||
return info;
|
||||
}
|
||||
|
||||
public static int getWearLocation(String miBandAddress) throws IllegalArgumentException {
|
||||
public static int getWearLocation(String deviceAddress) throws IllegalArgumentException {
|
||||
int location = 0; //left hand
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
if ("right".equals(prefs.getString(MiBandConst.PREF_MIBAND_WEARSIDE, "left"))) {
|
||||
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
|
||||
if ("right".equals(prefs.getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left"))) {
|
||||
location = 1; // right hand
|
||||
}
|
||||
return location;
|
||||
@ -261,6 +261,7 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_wearlocation,
|
||||
R.xml.devicesettings_lowlatency_fwupdate,
|
||||
R.xml.devicesettings_fake_timeoffset
|
||||
};
|
||||
|
@ -49,7 +49,7 @@ public class MijiaLywsd02Coordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice deviceCandidate) {
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ public class MiScale2DeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice device) {
|
||||
public int getBondingStyle() {
|
||||
return super.BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ public class No1F1Coordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice deviceCandidate) {
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ public abstract class RoidmiCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice device) {
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_BOND;
|
||||
}
|
||||
|
||||
@ -133,6 +133,7 @@ public abstract class RoidmiCoordinator extends AbstractDeviceCoordinator {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public int[] getColorPresets() {
|
||||
return RoidmiConst.COLOR_PRESETS;
|
||||
|
@ -77,7 +77,7 @@ public class Watch9DeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice deviceCandidate) {
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,6 @@ public class ZeTimeConstants {
|
||||
public static final byte INACTIVITY_TYPE = (byte) 0x08;
|
||||
public static final byte LOW_POWER_TYPE = (byte) 0x09;
|
||||
// watch settings
|
||||
public static final String PREF_WRIST = "zetime_wrist";
|
||||
public static final byte WEAR_ON_LEFT_WRIST = (byte) 0x00;
|
||||
public static final byte WEAR_ON_RIGHT_WRIST = (byte) 0x01;
|
||||
|
||||
@ -160,7 +159,6 @@ public class ZeTimeConstants {
|
||||
public static final String PREF_ACTIVITY_TRACKING = "zetime_activity_tracking";
|
||||
public static final String PREF_HANDMOVE_DISPLAY = "zetime_handmove_display";
|
||||
public static final String PREF_CALORIES_TYPE = "zetime_calories_type";
|
||||
public static final String PREF_TIME_FORMAT = "zetime_time_format";
|
||||
public static final String PREF_DATE_FORMAT = "zetime_date_format";
|
||||
|
||||
public static final String PREF_ALARM_SIGNALING = "zetime_alarm_signaling";
|
||||
|
@ -22,11 +22,12 @@ import android.bluetooth.le.ScanFilter;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
@ -37,9 +38,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
/**
|
||||
* Created by lightwars on 06.02.18.
|
||||
*/
|
||||
|
||||
public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
|
||||
@Override
|
||||
@ -135,7 +133,7 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
|
||||
|
||||
}
|
||||
|
||||
@ -155,7 +153,18 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice device) {
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUnicodeEmojis() { return true; }
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_timeformat,
|
||||
R.xml.devicesettings_wearlocation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import android.preference.Preference;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
|
||||
public class ZeTimePreferenceActivity extends AbstractSettingsActivity {
|
||||
@Override
|
||||
@ -43,8 +44,6 @@ public class ZeTimePreferenceActivity extends AbstractSettingsActivity {
|
||||
|
||||
addPreferenceHandlerFor(ZeTimeConstants.PREF_SCREENTIME);
|
||||
|
||||
addPreferenceHandlerFor(ZeTimeConstants.PREF_WRIST);
|
||||
|
||||
addPreferenceHandlerFor(ZeTimeConstants.PREF_ANALOG_MODE);
|
||||
|
||||
addPreferenceHandlerFor(ZeTimeConstants.PREF_ACTIVITY_TRACKING);
|
||||
@ -57,8 +56,6 @@ public class ZeTimePreferenceActivity extends AbstractSettingsActivity {
|
||||
|
||||
addPreferenceHandlerFor(ZeTimeConstants.PREF_CALORIES_TYPE);
|
||||
|
||||
addPreferenceHandlerFor(ZeTimeConstants.PREF_TIME_FORMAT);
|
||||
|
||||
addPreferenceHandlerFor(ZeTimeConstants.PREF_DATE_FORMAT);
|
||||
|
||||
addPreferenceHandlerFor(ZeTimeConstants.PREF_INACTIVITY_ENABLE);
|
||||
|
@ -61,7 +61,7 @@ public class BluetoothPairingRequestReceiver extends BroadcastReceiver {
|
||||
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
|
||||
try {
|
||||
if (coordinator.getBondingStyle(gbDevice) == DeviceCoordinator.BONDING_STYLE_NONE) {
|
||||
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_NONE) {
|
||||
LOG.info("Aborting unwanted pairing request");
|
||||
abortBroadcast();
|
||||
}
|
||||
|
@ -0,0 +1,225 @@
|
||||
/* Copyright (C) 2017-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.externalevents;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import lineageos.weather.LineageWeatherManager;
|
||||
import lineageos.weather.WeatherInfo;
|
||||
import lineageos.weather.WeatherLocation;
|
||||
import lineageos.weather.util.WeatherUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
import static lineageos.providers.WeatherContract.WeatherColumns.TempUnit.FAHRENHEIT;
|
||||
import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.ISOLATED_THUNDERSHOWERS;
|
||||
import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.NOT_AVAILABLE;
|
||||
import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.SCATTERED_SNOW_SHOWERS;
|
||||
import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.SCATTERED_THUNDERSTORMS;
|
||||
import static lineageos.providers.WeatherContract.WeatherColumns.WeatherCode.SHOWERS;
|
||||
import static lineageos.providers.WeatherContract.WeatherColumns.WindSpeedUnit.MPH;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public class LineageOsWeatherReceiver extends BroadcastReceiver implements LineageWeatherManager.WeatherUpdateRequestListener, LineageWeatherManager.LookupCityRequestListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(LineageOsWeatherReceiver.class);
|
||||
|
||||
private WeatherLocation weatherLocation = null;
|
||||
private Context mContext;
|
||||
private PendingIntent mPendingIntent = null;
|
||||
|
||||
public LineageOsWeatherReceiver() {
|
||||
mContext = GBApplication.getContext();
|
||||
final LineageWeatherManager weatherManager = LineageWeatherManager.getInstance(mContext);
|
||||
if (weatherManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
|
||||
String city = prefs.getString("weather_city", null);
|
||||
String cityId = prefs.getString("weather_cityid", null);
|
||||
|
||||
if ((cityId == null || cityId.equals("")) && city != null && !city.equals("")) {
|
||||
lookupCity(city);
|
||||
} else if (city != null && cityId != null) {
|
||||
weatherLocation = new WeatherLocation.Builder(cityId, city).build();
|
||||
enablePeriodicAlarm(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void lookupCity(String city) {
|
||||
final LineageWeatherManager weatherManager = LineageWeatherManager.getInstance(mContext);
|
||||
if (weatherManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (city != null && !city.equals("")) {
|
||||
if (weatherManager.getActiveWeatherServiceProviderLabel() != null) {
|
||||
weatherManager.lookupCity(city, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enablePeriodicAlarm(boolean enable) {
|
||||
if ((mPendingIntent != null && enable) || (mPendingIntent == null && !enable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AlarmManager am = (AlarmManager) (mContext.getSystemService(Context.ALARM_SERVICE));
|
||||
if (am == null) {
|
||||
LOG.warn("could not get alarm manager!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (enable) {
|
||||
Intent intent = new Intent("GB_UPDATE_WEATHER");
|
||||
intent.setPackage(BuildConfig.APPLICATION_ID);
|
||||
mPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
|
||||
am.setInexactRepeating(AlarmManager.RTC_WAKEUP, Calendar.getInstance().getTimeInMillis() + 10000, AlarmManager.INTERVAL_HOUR, mPendingIntent);
|
||||
} else {
|
||||
am.cancel(mPendingIntent);
|
||||
mPendingIntent = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
|
||||
String city = prefs.getString("weather_city", null);
|
||||
String cityId = prefs.getString("weather_cityid", null);
|
||||
|
||||
if (city != null && !city.equals("") && cityId == null) {
|
||||
lookupCity(city);
|
||||
} else {
|
||||
requestWeather();
|
||||
}
|
||||
}
|
||||
|
||||
private void requestWeather() {
|
||||
final LineageWeatherManager weatherManager = LineageWeatherManager.getInstance(GBApplication.getContext());
|
||||
if (weatherManager.getActiveWeatherServiceProviderLabel() != null && weatherLocation != null) {
|
||||
weatherManager.requestWeatherUpdate(weatherLocation, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWeatherRequestCompleted(int status, WeatherInfo weatherInfo) {
|
||||
if (weatherInfo != null) {
|
||||
LOG.info("weather: " + weatherInfo.toString());
|
||||
WeatherSpec weatherSpec = new WeatherSpec();
|
||||
weatherSpec.timestamp = (int) (weatherInfo.getTimestamp() / 1000);
|
||||
weatherSpec.location = weatherInfo.getCity();
|
||||
|
||||
if (weatherInfo.getTemperatureUnit() == FAHRENHEIT) {
|
||||
weatherSpec.currentTemp = (int) WeatherUtils.fahrenheitToCelsius(weatherInfo.getTemperature()) + 273;
|
||||
weatherSpec.todayMaxTemp = (int) WeatherUtils.fahrenheitToCelsius(weatherInfo.getTodaysHigh()) + 273;
|
||||
weatherSpec.todayMinTemp = (int) WeatherUtils.fahrenheitToCelsius(weatherInfo.getTodaysLow()) + 273;
|
||||
} else {
|
||||
weatherSpec.currentTemp = (int) weatherInfo.getTemperature() + 273;
|
||||
weatherSpec.todayMaxTemp = (int) weatherInfo.getTodaysHigh() + 273;
|
||||
weatherSpec.todayMinTemp = (int) weatherInfo.getTodaysLow() + 273;
|
||||
}
|
||||
if (weatherInfo.getWindSpeedUnit() == MPH) {
|
||||
weatherSpec.windSpeed = (float) weatherInfo.getWindSpeed() * 1.609344f;
|
||||
} else {
|
||||
weatherSpec.windSpeed = (float) weatherInfo.getWindSpeed();
|
||||
}
|
||||
weatherSpec.windDirection = (int) weatherInfo.getWindDirection();
|
||||
|
||||
weatherSpec.currentConditionCode = Weather.mapToOpenWeatherMapCondition(LineageOstoYahooCondintion(weatherInfo.getConditionCode()));
|
||||
weatherSpec.currentCondition = Weather.getConditionString(weatherSpec.currentConditionCode);
|
||||
weatherSpec.currentHumidity = (int) weatherInfo.getHumidity();
|
||||
|
||||
weatherSpec.forecasts = new ArrayList<>();
|
||||
List<WeatherInfo.DayForecast> forecasts = weatherInfo.getForecasts();
|
||||
for (int i = 1; i < forecasts.size(); i++) {
|
||||
WeatherInfo.DayForecast cmForecast = forecasts.get(i);
|
||||
WeatherSpec.Forecast gbForecast = new WeatherSpec.Forecast();
|
||||
if (weatherInfo.getTemperatureUnit() == FAHRENHEIT) {
|
||||
gbForecast.maxTemp = (int) WeatherUtils.fahrenheitToCelsius(cmForecast.getHigh()) + 273;
|
||||
gbForecast.minTemp = (int) WeatherUtils.fahrenheitToCelsius(cmForecast.getLow()) + 273;
|
||||
} else {
|
||||
gbForecast.maxTemp = (int) cmForecast.getHigh() + 273;
|
||||
gbForecast.minTemp = (int) cmForecast.getLow() + 273;
|
||||
}
|
||||
gbForecast.conditionCode = Weather.mapToOpenWeatherMapCondition(LineageOstoYahooCondintion(cmForecast.getConditionCode()));
|
||||
weatherSpec.forecasts.add(gbForecast);
|
||||
}
|
||||
Weather.getInstance().setWeatherSpec(weatherSpec);
|
||||
GBApplication.deviceService().onSendWeather(weatherSpec);
|
||||
} else {
|
||||
LOG.info("request has returned null for WeatherInfo");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cmCondition
|
||||
* @return
|
||||
*/
|
||||
private int LineageOstoYahooCondintion(int cmCondition) {
|
||||
int yahooCondition;
|
||||
if (cmCondition <= SHOWERS) {
|
||||
yahooCondition = cmCondition;
|
||||
} else if (cmCondition <= SCATTERED_THUNDERSTORMS) {
|
||||
yahooCondition = cmCondition + 1;
|
||||
} else if (cmCondition <= SCATTERED_SNOW_SHOWERS) {
|
||||
yahooCondition = cmCondition + 2;
|
||||
} else if (cmCondition <= ISOLATED_THUNDERSHOWERS) {
|
||||
yahooCondition = cmCondition + 3;
|
||||
} else {
|
||||
yahooCondition = NOT_AVAILABLE;
|
||||
}
|
||||
return yahooCondition;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLookupCityRequestCompleted(int result, List<WeatherLocation> list) {
|
||||
if (list != null) {
|
||||
weatherLocation = list.get(0);
|
||||
String cityId = weatherLocation.getCityId();
|
||||
String city = weatherLocation.getCity();
|
||||
|
||||
SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit();
|
||||
editor.putString("weather_city", city).apply();
|
||||
editor.putString("weather_cityid", cityId).apply();
|
||||
enablePeriodicAlarm(true);
|
||||
requestWeather();
|
||||
} else {
|
||||
enablePeriodicAlarm(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -27,13 +27,11 @@ import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaMetadata;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.os.Bundle;
|
||||
import android.os.PowerManager;
|
||||
import android.os.RemoteException;
|
||||
@ -44,6 +42,12 @@ import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.RemoteInput;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.palette.graphics.Palette;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -53,11 +57,6 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.RemoteInput;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.palette.graphics.Palette;
|
||||
import de.greenrobot.dao.query.Query;
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
@ -65,11 +64,11 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleColor;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntryDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.AppNotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
@ -242,9 +241,11 @@ public class NotificationListener extends NotificationListenerService {
|
||||
public void onNotificationPosted(StatusBarNotification sbn) {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
|
||||
if ("call".equals(sbn.getNotification().category) && prefs.getBoolean("notification_support_voip_calls", false)) {
|
||||
handleCallNotification(sbn);
|
||||
return;
|
||||
if (GBApplication.isRunningLollipopOrLater()) {
|
||||
if ("call".equals(sbn.getNotification().category) && prefs.getBoolean("notification_support_voip_calls", false)) {
|
||||
handleCallNotification(sbn);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (shouldIgnore(sbn)) {
|
||||
LOG.info("Ignore notification");
|
||||
@ -531,6 +532,9 @@ public class NotificationListener extends NotificationListenerService {
|
||||
Bundle extras = NotificationCompat.getExtras(notification);
|
||||
|
||||
//dumpExtras(extras);
|
||||
if (extras == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
CharSequence title = extras.getCharSequence(Notification.EXTRA_TITLE);
|
||||
if (title != null) {
|
||||
@ -582,13 +586,13 @@ public class NotificationListener extends NotificationListenerService {
|
||||
stateSpec.repeat = 1;
|
||||
stateSpec.shuffle = 1;
|
||||
switch (s.getState()) {
|
||||
case PlaybackState.STATE_PLAYING:
|
||||
case PlaybackStateCompat.STATE_PLAYING:
|
||||
stateSpec.state = MusicStateSpec.STATE_PLAYING;
|
||||
break;
|
||||
case PlaybackState.STATE_STOPPED:
|
||||
case PlaybackStateCompat.STATE_STOPPED:
|
||||
stateSpec.state = MusicStateSpec.STATE_STOPPED;
|
||||
break;
|
||||
case PlaybackState.STATE_PAUSED:
|
||||
case PlaybackStateCompat.STATE_PAUSED:
|
||||
stateSpec.state = MusicStateSpec.STATE_PAUSED;
|
||||
break;
|
||||
default:
|
||||
@ -624,12 +628,15 @@ public class NotificationListener extends NotificationListenerService {
|
||||
|
||||
@Override
|
||||
public void onNotificationRemoved(StatusBarNotification sbn) {
|
||||
LOG.info("Notification removed: " + sbn.getPackageName() + ": " + sbn.getNotification().category);
|
||||
if(Notification.CATEGORY_CALL.equals(sbn.getNotification().category) && activeCallPostTime == sbn.getPostTime()) {
|
||||
activeCallPostTime = 0;
|
||||
CallSpec callSpec = new CallSpec();
|
||||
callSpec.command = CallSpec.CALL_END;
|
||||
GBApplication.deviceService().onSetCallState(callSpec);
|
||||
LOG.info("Notification removed: " + sbn.getPackageName());
|
||||
if (GBApplication.isRunningLollipopOrLater()) {
|
||||
LOG.info("Notification removed: " + sbn.getPackageName() + ", category: " + sbn.getNotification().category);
|
||||
if (Notification.CATEGORY_CALL.equals(sbn.getNotification().category) && activeCallPostTime == sbn.getPostTime()) {
|
||||
activeCallPostTime = 0;
|
||||
CallSpec callSpec = new CallSpec();
|
||||
callSpec.command = CallSpec.CALL_END;
|
||||
GBApplication.deviceService().onSetCallState(callSpec);
|
||||
}
|
||||
}
|
||||
// FIXME: DISABLED for now
|
||||
/*
|
||||
|
@ -82,6 +82,10 @@ public class GBDevice implements Parcelable {
|
||||
private List<ItemWithDetails> mDeviceInfos;
|
||||
private HashMap<String, Object> mExtraInfos;
|
||||
|
||||
private int mNotificationIconConnected = R.drawable.ic_notification;
|
||||
private int mNotificationIconDisconnected = R.drawable.ic_notification_disconnected;
|
||||
private int mNotificationIconLowBattery = R.drawable.ic_notification_low_battery;
|
||||
|
||||
public GBDevice(String address, String name, DeviceType deviceType) {
|
||||
this(address, null, name, deviceType);
|
||||
}
|
||||
@ -110,6 +114,9 @@ public class GBDevice implements Parcelable {
|
||||
mBusyTask = in.readString();
|
||||
mDeviceInfos = in.readArrayList(getClass().getClassLoader());
|
||||
mExtraInfos = (HashMap) in.readSerializable();
|
||||
mNotificationIconConnected = in.readInt();
|
||||
mNotificationIconDisconnected = in.readInt();
|
||||
mNotificationIconLowBattery = in.readInt();
|
||||
|
||||
validate();
|
||||
}
|
||||
@ -131,6 +138,9 @@ public class GBDevice implements Parcelable {
|
||||
dest.writeString(mBusyTask);
|
||||
dest.writeList(mDeviceInfos);
|
||||
dest.writeSerializable(mExtraInfos);
|
||||
dest.writeInt(mNotificationIconConnected);
|
||||
dest.writeInt(mNotificationIconDisconnected);
|
||||
dest.writeInt(mNotificationIconLowBattery);
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
@ -221,6 +231,30 @@ public class GBDevice implements Parcelable {
|
||||
return mBusyTask;
|
||||
}
|
||||
|
||||
public int getNotificationIconConnected() {
|
||||
return mNotificationIconConnected;
|
||||
}
|
||||
|
||||
public void setNotificationIconConnected(int mNotificationIconConnected) {
|
||||
this.mNotificationIconConnected = mNotificationIconConnected;
|
||||
}
|
||||
|
||||
public int getNotificationIconDisconnected() {
|
||||
return mNotificationIconDisconnected;
|
||||
}
|
||||
|
||||
public void setNotificationIconDisconnected(int notificationIconDisconnected) {
|
||||
this.mNotificationIconDisconnected = notificationIconDisconnected;
|
||||
}
|
||||
|
||||
public int getNotificationIconLowBattery() {
|
||||
return mNotificationIconLowBattery;
|
||||
}
|
||||
|
||||
public void setNotificationIconLowBattery(int mNotificationIconLowBattery) {
|
||||
this.mNotificationIconLowBattery = mNotificationIconLowBattery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the device as busy, performing a certain task. While busy, no other operations will
|
||||
* be performed on the device.
|
||||
|
@ -0,0 +1,149 @@
|
||||
/* Copyright (C) 2017-2019 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.model;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityAnalysis;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
|
||||
public class DailyTotals {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DailyTotals.class);
|
||||
|
||||
|
||||
public int[] getDailyTotalsForAllDevices(Calendar day) {
|
||||
Context context = GBApplication.getContext();
|
||||
//get today's steps for all devices in GB
|
||||
int all_steps = 0;
|
||||
int all_sleep = 0;
|
||||
|
||||
|
||||
if (context instanceof GBApplication) {
|
||||
GBApplication gbApp = (GBApplication) context;
|
||||
List<? extends GBDevice> devices = gbApp.getDeviceManager().getDevices();
|
||||
for (GBDevice device : devices) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
if (!coordinator.supportsActivityDataFetching()) {
|
||||
continue;
|
||||
}
|
||||
int[] all_daily = getDailyTotalsForDevice(device, day);
|
||||
all_steps += all_daily[0];
|
||||
all_sleep += all_daily[1] + all_daily[2];
|
||||
}
|
||||
}
|
||||
LOG.debug("gbwidget daily totals, all steps:" + all_steps);
|
||||
LOG.debug("gbwidget daily totals, all sleep:" + all_sleep);
|
||||
return new int[]{all_steps, all_sleep};
|
||||
}
|
||||
|
||||
|
||||
public int[] getDailyTotalsForDevice(GBDevice device, Calendar day) {
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
ActivityAnalysis analysis = new ActivityAnalysis();
|
||||
ActivityAmounts amountsSteps;
|
||||
ActivityAmounts amountsSleep;
|
||||
|
||||
amountsSteps = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, 0, device));
|
||||
amountsSleep = analysis.calculateActivityAmounts(getSamplesOfDay(handler, day, -12, device));
|
||||
|
||||
int[] Sleep = getTotalsSleepForActivityAmounts(amountsSleep);
|
||||
int Steps = getTotalsStepsForActivityAmounts(amountsSteps);
|
||||
|
||||
return new int[]{Steps, Sleep[0], Sleep[1]};
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
GB.toast("Error loading activity summaries.", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
return new int[]{0, 0, 0};
|
||||
}
|
||||
}
|
||||
|
||||
private int[] getTotalsSleepForActivityAmounts(ActivityAmounts activityAmounts) {
|
||||
long totalSecondsDeepSleep = 0;
|
||||
long totalSecondsLightSleep = 0;
|
||||
for (ActivityAmount amount : activityAmounts.getAmounts()) {
|
||||
if (amount.getActivityKind() == ActivityKind.TYPE_DEEP_SLEEP) {
|
||||
totalSecondsDeepSleep += amount.getTotalSeconds();
|
||||
} else if (amount.getActivityKind() == ActivityKind.TYPE_LIGHT_SLEEP) {
|
||||
totalSecondsLightSleep += amount.getTotalSeconds();
|
||||
}
|
||||
}
|
||||
int totalMinutesDeepSleep = (int) (totalSecondsDeepSleep / 60);
|
||||
int totalMinutesLightSleep = (int) (totalSecondsLightSleep / 60);
|
||||
return new int[]{totalMinutesDeepSleep, totalMinutesLightSleep};
|
||||
}
|
||||
|
||||
|
||||
private int getTotalsStepsForActivityAmounts(ActivityAmounts activityAmounts) {
|
||||
int totalSteps = 0;
|
||||
|
||||
for (ActivityAmount amount : activityAmounts.getAmounts()) {
|
||||
totalSteps += amount.getTotalSteps();
|
||||
}
|
||||
return totalSteps;
|
||||
}
|
||||
|
||||
|
||||
private List<? extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) {
|
||||
int startTs;
|
||||
int endTs;
|
||||
|
||||
day = (Calendar) day.clone(); // do not modify the caller's argument
|
||||
day.set(Calendar.HOUR_OF_DAY, 0);
|
||||
day.set(Calendar.MINUTE, 0);
|
||||
day.set(Calendar.SECOND, 0);
|
||||
day.add(Calendar.HOUR, offsetHours);
|
||||
|
||||
startTs = (int) (day.getTimeInMillis() / 1000);
|
||||
endTs = startTs + 24 * 60 * 60 - 1;
|
||||
|
||||
return getSamples(db, device, startTs, endTs);
|
||||
}
|
||||
|
||||
|
||||
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
return getAllSamples(db, device, tsFrom, tsTo);
|
||||
}
|
||||
|
||||
|
||||
protected SampleProvider<? extends AbstractActivitySample> getProvider(DBHandler db, GBDevice device) {
|
||||
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device);
|
||||
return coordinator.getSampleProvider(device, db.getDaoSession());
|
||||
}
|
||||
|
||||
|
||||
protected List<? extends ActivitySample> getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends ActivitySample> provider = getProvider(db, device);
|
||||
return provider.getAllActivitySamples(tsFrom, tsTo);
|
||||
}
|
||||
}
|
@ -39,6 +39,7 @@ public enum DeviceType {
|
||||
MIBAND3(14, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband3),
|
||||
AMAZFITCOR2(15, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_amazfit_cor2),
|
||||
MIBAND4(16, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband4),
|
||||
AMAZFITBIP_LITE(17, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_amazfit_bip_lite),
|
||||
VIBRATISSIMO(20, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_vibratissimo),
|
||||
LIVEVIEW(30, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_liveview),
|
||||
HPLUS(40, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_hplus),
|
||||
@ -58,6 +59,7 @@ public enum DeviceType {
|
||||
CASIOGB6900(120, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_casiogb6900),
|
||||
MISCALE2(131, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_miscale2),
|
||||
BFH16(140, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_bfh16),
|
||||
MAKIBESHR3(150, R.drawable.ic_device_default, R.drawable.ic_device_hplus_disabled, R.string.devicetype_makibes_hr3),
|
||||
MIJIA_LYWSD02(200, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_mijia_lywsd02),
|
||||
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
|
||||
|
||||
|
@ -58,7 +58,6 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepMonitorResult;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
@ -151,8 +150,6 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
handleGBDeviceEvent((GBDeviceEventVersionInfo) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceEventAppInfo) {
|
||||
handleGBDeviceEvent((GBDeviceEventAppInfo) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceEventSleepMonitorResult) {
|
||||
handleGBDeviceEvent((GBDeviceEventSleepMonitorResult) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceEventScreenshot) {
|
||||
handleGBDeviceEvent((GBDeviceEventScreenshot) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceEventNotificationControl) {
|
||||
@ -258,18 +255,6 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(appInfoIntent);
|
||||
}
|
||||
|
||||
private void handleGBDeviceEvent(GBDeviceEventSleepMonitorResult sleepMonitorResult) {
|
||||
Context context = getContext();
|
||||
LOG.info("Got event for SLEEP_MONIOR_RES");
|
||||
Intent sleepMonitorIntent = new Intent(ChartsHost.REFRESH);
|
||||
sleepMonitorIntent.putExtra("smartalarm_from", sleepMonitorResult.smartalarm_from);
|
||||
sleepMonitorIntent.putExtra("smartalarm_to", sleepMonitorResult.smartalarm_to);
|
||||
sleepMonitorIntent.putExtra("recording_base_timestamp", sleepMonitorResult.recording_base_timestamp);
|
||||
sleepMonitorIntent.putExtra("alarm_gone_off", sleepMonitorResult.alarm_gone_off);
|
||||
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(sleepMonitorIntent);
|
||||
}
|
||||
|
||||
private void handleGBDeviceEvent(GBDeviceEventScreenshot screenshot) {
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-hhmmss", Locale.US);
|
||||
String filename = "screenshot_" + dateFormat.format(new Date()) + ".bmp";
|
||||
@ -409,4 +394,8 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(messageIntent);
|
||||
}
|
||||
|
||||
public String customStringFilter(String inputString) {
|
||||
return inputString;
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,10 @@ import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -43,19 +47,18 @@ import java.util.ArrayList;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmClockReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothPairingRequestReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.CMWeatherReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.LineageOsWeatherReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.MusicPlaybackReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.OmniJawsObserver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.PebbleReceiver;
|
||||
@ -78,6 +81,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.EmojiConverter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_ADD_CALENDAREVENT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_APP_CONFIGURE;
|
||||
@ -96,12 +100,12 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_FI
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_HEARTRATE_TEST;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_INSTALL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_NOTIFICATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_READ_CONFIGURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_APPINFO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_DEVICEINFO;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_REQUEST_SCREENSHOT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_RESET;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SEND_CONFIGURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_READ_CONFIGURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SEND_WEATHER;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETCANNEDMESSAGES;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_SETMUSICINFO;
|
||||
@ -193,8 +197,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
private AlarmReceiver mAlarmReceiver = null;
|
||||
private CalendarReceiver mCalendarReceiver = null;
|
||||
private CMWeatherReceiver mCMWeatherReceiver = null;
|
||||
private LineageOsWeatherReceiver mLineageOsWeatherReceiver = null;
|
||||
private OmniJawsObserver mOmniJawsObserver = null;
|
||||
private Random mRandom = new Random();
|
||||
|
||||
private final String[] mMusicActions = {
|
||||
"com.android.music.metachanged",
|
||||
@ -365,8 +369,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
if (text == null || text.length() == 0)
|
||||
return text;
|
||||
|
||||
if (!mCoordinator.supportsUnicodeEmojis())
|
||||
text = mDeviceSupport.customStringFilter(text);
|
||||
|
||||
if (!mCoordinator.supportsUnicodeEmojis()) {
|
||||
return EmojiConverter.convertUnicodeEmojiToAscii(text, getApplicationContext());
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
@ -733,10 +740,17 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
mCMWeatherReceiver = new CMWeatherReceiver();
|
||||
registerReceiver(mCMWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER"));
|
||||
}
|
||||
if (GBApplication.isRunningOreoOrLater()) {
|
||||
if (mLineageOsWeatherReceiver == null && coordinator != null && coordinator.supportsWeather()) {
|
||||
|
||||
mLineageOsWeatherReceiver = new LineageOsWeatherReceiver();
|
||||
registerReceiver(mLineageOsWeatherReceiver, new IntentFilter("GB_UPDATE_WEATHER"));
|
||||
}
|
||||
}
|
||||
if (mOmniJawsObserver == null && coordinator != null && coordinator.supportsWeather()) {
|
||||
try {
|
||||
mOmniJawsObserver = new OmniJawsObserver(new Handler());
|
||||
getContentResolver().registerContentObserver(mOmniJawsObserver.WEATHER_URI, true, mOmniJawsObserver);
|
||||
getContentResolver().registerContentObserver(OmniJawsObserver.WEATHER_URI, true, mOmniJawsObserver);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
//Nothing wrong, it just means we're not running on omnirom.
|
||||
}
|
||||
@ -784,6 +798,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
unregisterReceiver(mCMWeatherReceiver);
|
||||
mCMWeatherReceiver = null;
|
||||
}
|
||||
if (mLineageOsWeatherReceiver != null) {
|
||||
unregisterReceiver(mLineageOsWeatherReceiver);
|
||||
mLineageOsWeatherReceiver = null;
|
||||
}
|
||||
if (mOmniJawsObserver != null) {
|
||||
getContentResolver().unregisterContentObserver(mOmniJawsObserver);
|
||||
}
|
||||
|
@ -130,4 +130,9 @@ public interface DeviceSupport extends EventHandler {
|
||||
* Returns the Android context to use, e.g. to look up resources.
|
||||
*/
|
||||
Context getContext();
|
||||
|
||||
/**
|
||||
* converts String in a device specific way, e.g. re-map characters for a custom font
|
||||
*/
|
||||
String customStringFilter(String inputString);
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.casiogb6900.CasioGB6900DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipLiteSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor.AmazfitCorSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor2.AmazfitCor2Support;
|
||||
@ -42,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.id115.ID115Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.BFH16DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.makibeshr3.MakibesHR3DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaLywsd02Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport;
|
||||
@ -136,6 +138,9 @@ public class DeviceSupportFactory {
|
||||
case AMAZFITBIP:
|
||||
deviceSupport = new ServiceDeviceSupport(new AmazfitBipSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
case AMAZFITBIP_LITE:
|
||||
deviceSupport = new ServiceDeviceSupport(new AmazfitBipLiteSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
case AMAZFITCOR:
|
||||
deviceSupport = new ServiceDeviceSupport(new AmazfitCorSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
@ -195,6 +200,10 @@ public class DeviceSupportFactory {
|
||||
break;
|
||||
case MIJIA_LYWSD02:
|
||||
deviceSupport = new ServiceDeviceSupport(new MijiaLywsd02Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
case MAKIBESHR3:
|
||||
deviceSupport = new ServiceDeviceSupport(new MakibesHR3DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
}
|
||||
if (deviceSupport != null) {
|
||||
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);
|
||||
|
@ -113,6 +113,11 @@ public class ServiceDeviceSupport implements DeviceSupport {
|
||||
return delegate.getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String customStringFilter(String inputString) {
|
||||
return delegate.customStringFilter(inputString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return delegate.useAutoConnect();
|
||||
|
@ -242,6 +242,7 @@ public final class BtLEQueue {
|
||||
mBluetoothGattServer.addService(service);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (mGattMonitor) {
|
||||
// connectGatt with true doesn't really work ;( too often connection problems
|
||||
if (GBApplication.isRunningMarshmallowOrLater()) {
|
||||
@ -259,6 +260,7 @@ public final class BtLEQueue {
|
||||
|
||||
private void setDeviceConnectionState(State newState) {
|
||||
LOG.debug("new device connection state: " + newState);
|
||||
|
||||
mGbDevice.setState(newState);
|
||||
mGbDevice.sendDeviceUpdateIntent(mContext);
|
||||
if (mConnectionLatch != null && newState == State.CONNECTED) {
|
||||
|
@ -42,15 +42,18 @@ import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.SimpleTimeZone;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import cyanogenmod.weather.util.WeatherUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
@ -66,6 +69,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiWeatherConditions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2FWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Coordinator;
|
||||
@ -95,6 +99,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
@ -125,21 +130,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Version;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COUNT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_DURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_COUNT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_DURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PAUSE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PROFILE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COLOUR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COUNT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_DURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_ORIGINAL_COLOUR;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_DURATION;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PAUSE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefIntValue;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefStringValue;
|
||||
@ -327,23 +320,6 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom notification to the given transaction builder
|
||||
* @param vibrationProfile specifies how and how often the Band shall vibrate.
|
||||
* @param simpleNotification
|
||||
* @param flashTimes
|
||||
* @param flashColour
|
||||
* @param originalColour
|
||||
* @param flashDuration
|
||||
* @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example.
|
||||
* @param builder
|
||||
*/
|
||||
private HuamiSupport sendCustomNotification(VibrationProfile vibrationProfile, SimpleNotification simpleNotification, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) {
|
||||
getNotificationStrategy().sendCustomNotification(vibrationProfile, simpleNotification, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder);
|
||||
LOG.info("Sending notification to MiBand");
|
||||
return this;
|
||||
}
|
||||
|
||||
public NotificationStrategy getNotificationStrategy() {
|
||||
String firmwareVersion = gbDevice.getFirmwareVersion();
|
||||
if (firmwareVersion != null) {
|
||||
@ -439,7 +415,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
int userid = alias.hashCode(); // hash from alias like mi1
|
||||
|
||||
// FIXME: Do encoding like in PebbleProtocol, this is ugly
|
||||
byte bytes[] = new byte[]{
|
||||
byte[] bytes = new byte[]{
|
||||
HuamiService.COMMAND_SET_USERINFO,
|
||||
0,
|
||||
0,
|
||||
@ -563,58 +539,26 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
}
|
||||
|
||||
protected void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
|
||||
private void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized(task);
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
int vibrateDuration = getPreferredVibrateDuration(notificationOrigin, prefs);
|
||||
int vibratePause = getPreferredVibratePause(notificationOrigin, prefs);
|
||||
short vibrateTimes = getPreferredVibrateCount(notificationOrigin, prefs);
|
||||
VibrationProfile profile = getPreferredVibrateProfile(notificationOrigin, prefs, vibrateTimes);
|
||||
profile.setAlertLevel(alertLevel);
|
||||
|
||||
int flashTimes = getPreferredFlashCount(notificationOrigin, prefs);
|
||||
int flashColour = getPreferredFlashColour(notificationOrigin, prefs);
|
||||
int originalColour = getPreferredOriginalColour(notificationOrigin, prefs);
|
||||
int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs);
|
||||
getNotificationStrategy().sendCustomNotification(profile, simpleNotification, 0, 0, 0, 0, extraAction, builder);
|
||||
|
||||
sendCustomNotification(profile, simpleNotification, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder);
|
||||
|
||||
// sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder);
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to send notification to MI device", ex);
|
||||
LOG.error("Unable to send notification to device", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private int getPreferredFlashDuration(String notificationOrigin, Prefs prefs) {
|
||||
return getNotificationPrefIntValue(FLASH_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_DURATION);
|
||||
}
|
||||
|
||||
private int getPreferredOriginalColour(String notificationOrigin, Prefs prefs) {
|
||||
return getNotificationPrefIntValue(FLASH_ORIGINAL_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR);
|
||||
}
|
||||
|
||||
private int getPreferredFlashColour(String notificationOrigin, Prefs prefs) {
|
||||
return getNotificationPrefIntValue(FLASH_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COLOUR);
|
||||
}
|
||||
|
||||
private int getPreferredFlashCount(String notificationOrigin, Prefs prefs) {
|
||||
return getNotificationPrefIntValue(FLASH_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COUNT);
|
||||
}
|
||||
|
||||
private int getPreferredVibratePause(String notificationOrigin, Prefs prefs) {
|
||||
return getNotificationPrefIntValue(VIBRATION_PAUSE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PAUSE);
|
||||
}
|
||||
|
||||
private short getPreferredVibrateCount(String notificationOrigin, Prefs prefs) {
|
||||
return (short) Math.min(Short.MAX_VALUE, getNotificationPrefIntValue(VIBRATION_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_COUNT));
|
||||
}
|
||||
|
||||
private int getPreferredVibrateDuration(String notificationOrigin, Prefs prefs) {
|
||||
return getNotificationPrefIntValue(VIBRATION_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_DURATION);
|
||||
}
|
||||
|
||||
private VibrationProfile getPreferredVibrateProfile(String notificationOrigin, Prefs prefs, short repeat) {
|
||||
String profileId = getNotificationPrefStringValue(VIBRATION_PROFILE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PROFILE);
|
||||
return VibrationProfile.getProfile(profileId, repeat);
|
||||
@ -703,17 +647,17 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, simpleNotification, HuamiService.ALERT_LEVEL_PHONE_CALL, abortAction);
|
||||
} else if ((callSpec.command == CallSpec.CALL_START) || (callSpec.command == CallSpec.CALL_END)) {
|
||||
telephoneRinging = false;
|
||||
stopCurrentNotification();
|
||||
stopCurrentCallNotification();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopCurrentNotification() {
|
||||
private void stopCurrentCallNotification() {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("stop notification");
|
||||
getNotificationStrategy().stopCurrentNotification(builder);
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error stopping notification");
|
||||
LOG.error("Error stopping call notification");
|
||||
}
|
||||
}
|
||||
|
||||
@ -762,9 +706,8 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void sendMusicStateToDevice() {
|
||||
|
||||
|
||||
if (characteristicChunked == null) {
|
||||
return;
|
||||
}
|
||||
@ -971,7 +914,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
private byte[] getLatency(int minConnectionInterval, int maxConnectionInterval, int latency, int timeout, int advertisementInterval) {
|
||||
byte result[] = new byte[12];
|
||||
byte[] result = new byte[12];
|
||||
result[0] = (byte) (minConnectionInterval & 0xff);
|
||||
result[1] = (byte) (0xff & minConnectionInterval >> 8);
|
||||
result[2] = (byte) (maxConnectionInterval & 0xff);
|
||||
@ -1559,12 +1502,18 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
case MiBandConst.PREF_SWIPE_UNLOCK:
|
||||
setBandScreenUnlock(builder);
|
||||
break;
|
||||
case HuamiConst.PREF_DATEFORMAT:
|
||||
case DeviceSettingsPreferenceConst.PREF_DATEFORMAT:
|
||||
setDateFormat(builder);
|
||||
break;
|
||||
case HuamiConst.PREF_LANGUAGE:
|
||||
setLanguage(builder);
|
||||
break;
|
||||
case HuamiConst.PREF_EXPOSE_HR_THIRDPARTY:
|
||||
setExposeHRThridParty(builder);
|
||||
break;
|
||||
case DeviceSettingsPreferenceConst.PREF_WEARLOCATION:
|
||||
setWearLocation(builder);
|
||||
break;
|
||||
}
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException e) {
|
||||
@ -1584,7 +1533,190 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
// FIXME: currently HuamiSupport *is* MiBand2 support, so return if we are using Mi Band 2
|
||||
if (gbDevice.getType() == DeviceType.MIBAND2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gbDevice.getFirmwareVersion() == null) {
|
||||
LOG.warn("Device not initialized yet, so not sending weather info");
|
||||
return;
|
||||
}
|
||||
boolean supportsConditionString = false;
|
||||
|
||||
Version version = new Version(gbDevice.getFirmwareVersion());
|
||||
if (version.compareTo(new Version("0.0.8.74")) >= 0) {
|
||||
supportsConditionString = true;
|
||||
}
|
||||
|
||||
MiBandConst.DistanceUnit unit = HuamiCoordinator.getDistanceUnit();
|
||||
int tz_offset_hours = SimpleTimeZone.getDefault().getOffset(weatherSpec.timestamp * 1000L) / (1000 * 60 * 60);
|
||||
try {
|
||||
TransactionBuilder builder;
|
||||
builder = performInitialized("Sending current temp");
|
||||
|
||||
byte condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode);
|
||||
|
||||
int length = 8;
|
||||
if (supportsConditionString) {
|
||||
length += weatherSpec.currentCondition.getBytes().length + 1;
|
||||
}
|
||||
ByteBuffer buf = ByteBuffer.allocate(length);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buf.put((byte) 2);
|
||||
buf.putInt(weatherSpec.timestamp);
|
||||
buf.put((byte) (tz_offset_hours * 4));
|
||||
buf.put(condition);
|
||||
|
||||
int currentTemp = weatherSpec.currentTemp - 273;
|
||||
if (unit == MiBandConst.DistanceUnit.IMPERIAL) {
|
||||
currentTemp = (int) WeatherUtils.celsiusToFahrenheit(currentTemp);
|
||||
}
|
||||
buf.put((byte) currentTemp);
|
||||
|
||||
if (supportsConditionString) {
|
||||
buf.put(weatherSpec.currentCondition.getBytes());
|
||||
buf.put((byte) 0);
|
||||
}
|
||||
|
||||
if (characteristicChunked != null) {
|
||||
writeToChunked(builder, 1, buf.array());
|
||||
} else {
|
||||
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
|
||||
}
|
||||
|
||||
builder.queue(getQueue());
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error sending current weather", ex);
|
||||
}
|
||||
|
||||
try {
|
||||
TransactionBuilder builder;
|
||||
builder = performInitialized("Sending air quality index");
|
||||
int length = 8;
|
||||
String aqiString = "(n/a)";
|
||||
if (supportsConditionString) {
|
||||
length += aqiString.getBytes().length + 1;
|
||||
}
|
||||
ByteBuffer buf = ByteBuffer.allocate(length);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put((byte) 4);
|
||||
buf.putInt(weatherSpec.timestamp);
|
||||
buf.put((byte) (tz_offset_hours * 4));
|
||||
buf.putShort((short) 0);
|
||||
if (supportsConditionString) {
|
||||
buf.put(aqiString.getBytes());
|
||||
buf.put((byte) 0);
|
||||
}
|
||||
|
||||
if (characteristicChunked != null) {
|
||||
writeToChunked(builder, 1, buf.array());
|
||||
} else {
|
||||
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
|
||||
}
|
||||
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Error sending air quality");
|
||||
}
|
||||
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("Sending weather forecast");
|
||||
|
||||
final byte NR_DAYS = (byte) (1 + weatherSpec.forecasts.size());
|
||||
int bytesPerDay = 4;
|
||||
|
||||
int conditionsLength = 0;
|
||||
if (supportsConditionString) {
|
||||
bytesPerDay = 5;
|
||||
conditionsLength = weatherSpec.currentCondition.getBytes().length;
|
||||
for (WeatherSpec.Forecast forecast : weatherSpec.forecasts) {
|
||||
conditionsLength += Weather.getConditionString(forecast.conditionCode).getBytes().length;
|
||||
}
|
||||
}
|
||||
|
||||
int length = 7 + bytesPerDay * NR_DAYS + conditionsLength;
|
||||
ByteBuffer buf = ByteBuffer.allocate(length);
|
||||
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put((byte) 1);
|
||||
buf.putInt(weatherSpec.timestamp);
|
||||
buf.put((byte) (tz_offset_hours * 4));
|
||||
|
||||
buf.put(NR_DAYS);
|
||||
|
||||
byte condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode);
|
||||
buf.put(condition);
|
||||
buf.put(condition);
|
||||
|
||||
int todayMaxTemp = weatherSpec.todayMaxTemp - 273;
|
||||
int todayMinTemp = weatherSpec.todayMinTemp - 273;
|
||||
if (unit == MiBandConst.DistanceUnit.IMPERIAL) {
|
||||
todayMaxTemp = (int) WeatherUtils.celsiusToFahrenheit(todayMaxTemp);
|
||||
todayMinTemp = (int) WeatherUtils.celsiusToFahrenheit(todayMinTemp);
|
||||
}
|
||||
buf.put((byte) todayMaxTemp);
|
||||
buf.put((byte) todayMinTemp);
|
||||
|
||||
if (supportsConditionString) {
|
||||
buf.put(weatherSpec.currentCondition.getBytes());
|
||||
buf.put((byte) 0);
|
||||
}
|
||||
|
||||
for (WeatherSpec.Forecast forecast : weatherSpec.forecasts) {
|
||||
condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(forecast.conditionCode);
|
||||
buf.put(condition);
|
||||
buf.put(condition);
|
||||
|
||||
int forecastMaxTemp = forecast.maxTemp - 273;
|
||||
int forecastMinTemp = forecast.minTemp - 273;
|
||||
if (unit == MiBandConst.DistanceUnit.IMPERIAL) {
|
||||
forecastMaxTemp = (int) WeatherUtils.celsiusToFahrenheit(forecastMaxTemp);
|
||||
forecastMinTemp = (int) WeatherUtils.celsiusToFahrenheit(forecastMinTemp);
|
||||
}
|
||||
buf.put((byte) forecastMaxTemp);
|
||||
buf.put((byte) forecastMinTemp);
|
||||
|
||||
if (supportsConditionString) {
|
||||
buf.put(Weather.getConditionString(forecast.conditionCode).getBytes());
|
||||
buf.put((byte) 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (characteristicChunked != null) {
|
||||
writeToChunked(builder, 1, buf.array());
|
||||
} else {
|
||||
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
|
||||
}
|
||||
|
||||
builder.queue(getQueue());
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error sending weather forecast", ex);
|
||||
}
|
||||
|
||||
try {
|
||||
TransactionBuilder builder;
|
||||
builder = performInitialized("Sending forecast location");
|
||||
|
||||
int length = 2 + weatherSpec.location.getBytes().length;
|
||||
ByteBuffer buf = ByteBuffer.allocate(length);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put((byte) 8);
|
||||
buf.put(weatherSpec.location.getBytes());
|
||||
buf.put((byte) 0);
|
||||
|
||||
|
||||
if (characteristicChunked != null) {
|
||||
writeToChunked(builder, 1, buf.array());
|
||||
} else {
|
||||
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
|
||||
}
|
||||
|
||||
builder.queue(getQueue());
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error sending current forecast location", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private HuamiSupport setDateDisplay(TransactionBuilder builder) {
|
||||
@ -1909,6 +2041,20 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
private HuamiSupport setExposeHRThridParty(TransactionBuilder builder) {
|
||||
boolean enable = HuamiCoordinator.getExposeHRThirdParty(gbDevice.getAddress());
|
||||
LOG.info("Setting exposure of HR to third party apps to: " + enable);
|
||||
|
||||
if (enable) {
|
||||
builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_ENBALE_HR_CONNECTION);
|
||||
} else {
|
||||
builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DISABLE_HR_CONNECTION);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
protected void writeToChunked(TransactionBuilder builder, int type, byte[] data) {
|
||||
final int MAX_CHUNKLENGTH = 17;
|
||||
int remaining = data.length;
|
||||
@ -1937,6 +2083,42 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String customStringFilter(String inputString) {
|
||||
if (HuamiCoordinator.getUseCustomFont(gbDevice.getAddress())) {
|
||||
return convertEmojiToCustomFont(inputString);
|
||||
}
|
||||
return inputString;
|
||||
}
|
||||
|
||||
|
||||
private String convertEmojiToCustomFont(String str) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int i = 0;
|
||||
while (i < str.length()) {
|
||||
char charAt = str.charAt(i);
|
||||
if (Character.isHighSurrogate(charAt)) {
|
||||
int i2 = i + 1;
|
||||
try {
|
||||
int codePoint = Character.toCodePoint(charAt, str.charAt(i2));
|
||||
if (codePoint < 127744 || codePoint > 129510) {
|
||||
sb.append(charAt);
|
||||
} else {
|
||||
sb.append((char) (codePoint - 83712));
|
||||
i = i2;
|
||||
}
|
||||
} catch (StringIndexOutOfBoundsException e) {
|
||||
LOG.warn("error while converting emoji to custom font", e);
|
||||
sb.append(charAt);
|
||||
}
|
||||
} else {
|
||||
sb.append(charAt);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void phase2Initialize(TransactionBuilder builder) {
|
||||
LOG.info("phase2Initialize...");
|
||||
requestBatteryInfo(builder);
|
||||
@ -1959,6 +2141,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
setInactivityWarnings(builder);
|
||||
setHeartrateSleepSupport(builder);
|
||||
setDisconnectNotification(builder);
|
||||
setExposeHRThridParty(builder);
|
||||
setHeartrateMeasurementInterval(builder, getHeartRateMeasurementInterval());
|
||||
}
|
||||
|
||||
|
@ -112,6 +112,7 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
|
||||
|
||||
// Latin Firmware
|
||||
crcToVersion.put(52828, "1.1.5.36 (Latin)");
|
||||
crcToVersion.put(60625, "1.1.6.30 (Latin)");
|
||||
|
||||
// resources
|
||||
crcToVersion.put(12586, "0.0.8.74");
|
||||
@ -138,6 +139,7 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
|
||||
crcToVersion.put(5341, "1.1.5.02-24");
|
||||
crcToVersion.put(22662, "1.1.5.36");
|
||||
crcToVersion.put(24045, "1.1.5.56");
|
||||
crcToVersion.put(37677, "1.1.6.30");
|
||||
|
||||
// gps
|
||||
crcToVersion.put(61520, "9367,8f79a91,0,0,");
|
||||
@ -149,7 +151,8 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
|
||||
|
||||
// font
|
||||
crcToVersion.put(61054, "8");
|
||||
crcToVersion.put(62291, "9 (Latin)");
|
||||
crcToVersion.put(62291, "9 (old Latin)");
|
||||
crcToVersion.put(59577, "9 (Latin)");
|
||||
}
|
||||
|
||||
public AmazfitBipFirmwareInfo(byte[] bytes) {
|
||||
@ -182,7 +185,7 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
|
||||
if (ArrayUtils.startsWith(bytes, NEWFT_HEADER)) {
|
||||
if (bytes[10] == 0x01) {
|
||||
return HuamiFirmwareType.FONT;
|
||||
} else if (bytes[10] == 0x02) {
|
||||
} else if (bytes[10] == 0x02 || bytes[10] == 0x0A) {
|
||||
return HuamiFirmwareType.FONT_LATIN;
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user