mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-24 02:46:50 +01:00
Merge branch 'master' of github.com:Freeyourgadget/Gadgetbridge into fossil-q-hybrid
This commit is contained in:
commit
b20963348d
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.*
|
||||
|
||||
|
35
CHANGELOG.md
35
CHANGELOG.md
@ -1,7 +1,40 @@
|
||||
### Changelog
|
||||
|
||||
#### 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 firemare while phone is set to Spanish
|
||||
* Mi Band 1/2: Crash when updating firmware while phone is set to Spanish
|
||||
* Mi Band 4: Enable music info support (displays now on the band)
|
||||
* Mi Band 4: Support setting date format (for built-in watchfaces)
|
||||
* Amazfit Cor 2: Try to fix empty menu on device
|
||||
|
@ -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.36.2"
|
||||
versionCode 157
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
@ -66,8 +66,9 @@ dependencies {
|
||||
testImplementation "org.robolectric:robolectric:4.2.1"
|
||||
testImplementation "com.google.code.gson:gson:2.8.5"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.0.2"
|
||||
implementation "androidx.preference:preference:1.1.0-alpha05"
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
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"
|
||||
@ -78,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"
|
||||
@ -107,7 +108,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")
|
||||
@ -150,22 +151,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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,8 @@
|
||||
|
||||
<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 +60,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 +449,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 +461,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);
|
||||
}
|
@ -112,6 +112,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 +632,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 +709,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");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -36,6 +36,10 @@ import android.widget.DatePicker;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import java.io.File;
|
||||
@ -45,9 +49,6 @@ import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.ActivitySummariesAdapter;
|
||||
@ -240,10 +241,10 @@ public class ActivitySummariesActivity extends AbstractListActivity<BaseActivity
|
||||
date.set(year, monthOfYear, dayOfMonth);
|
||||
|
||||
long timestamp = date.getTimeInMillis() - 1000;
|
||||
SharedPreferences.Editor editor = GBApplication.getPrefs().getPreferences().edit();
|
||||
editor.remove(mGBDevice.getAddress() + "_" + "lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
|
||||
editor.putLong(mGBDevice.getAddress() + "_" + "lastSportsActivityTimeMillis", timestamp);
|
||||
editor.commit();
|
||||
SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(mGBDevice.getAddress()).edit();
|
||||
editor.remove("lastSportsActivityTimeMillis"); //FIXME: key reconstruction is BAD
|
||||
editor.putLong("lastSportsActivityTimeMillis", timestamp);
|
||||
editor.apply();
|
||||
}
|
||||
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -77,6 +78,9 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
|
||||
|
||||
private ScanCallback newLeScanCallback = null;
|
||||
|
||||
// Disabled for testing, it seems worse for a few people
|
||||
private final boolean disableNewBLEScanning = true;
|
||||
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private final BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
|
||||
@ -95,7 +99,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
|
||||
// continue with LE scan, if available
|
||||
if (isScanning == Scanning.SCANNING_BT) {
|
||||
checkAndRequestLocationPermission();
|
||||
if (GBApplication.isRunningLollipopOrLater()) {
|
||||
if (GBApplication.isRunningLollipopOrLater() && !disableNewBLEScanning) {
|
||||
startDiscovery(Scanning.SCANNING_NEW_BTLE);
|
||||
} else {
|
||||
startDiscovery(Scanning.SCANNING_BTLE);
|
||||
@ -297,7 +301,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelableArrayList("deviceCandidates", deviceCandidates);
|
||||
}
|
||||
@ -652,7 +656,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
|
||||
super.onPause();
|
||||
stopBTDiscovery();
|
||||
stopBTLEDiscovery();
|
||||
if (GBApplication.isRunningLollipopOrLater()) {
|
||||
if (GBApplication.isRunningLollipopOrLater() && !disableNewBLEScanning) {
|
||||
stopNewBTLEDiscovery();
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.ConfigActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
@ -97,6 +98,16 @@ public class SettingsActivity extends AbstractSettingsActivity {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
pref = findPreference("pref_charts");
|
||||
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
Intent enableIntent = new Intent(SettingsActivity.this, ChartsPreferencesActivity.class);
|
||||
startActivity(enableIntent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
pref = findPreference("pref_key_miband");
|
||||
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -429,7 +429,7 @@ public abstract class AbstractAppManagerFragment extends Fragment {
|
||||
startActivity(startIntent);
|
||||
return true;
|
||||
case R.id.appmanager_app_openinstore:
|
||||
String url = "https://pebble-appstore.romanport.com/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/0/?query=" + selectedApp.getName();
|
||||
String url = "https://apps.rebble.io/en_US/search/" + ((selectedApp.getType() == GBDeviceApp.Type.WATCHFACE) ? "watchfaces" : "watchapps") + "/1/?native=true&?query=" + Uri.encode(selectedApp.getName());
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse(url));
|
||||
startActivity(intent);
|
||||
|
@ -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;
|
||||
@ -48,6 +47,7 @@ import java.util.Calendar;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
@ -58,7 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
|
||||
|
||||
public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(AbstractWeekChartFragment.class);
|
||||
protected final int TOTAL_DAYS = 7;
|
||||
protected final int TOTAL_DAYS = getRangeDays();
|
||||
|
||||
private Locale mLocale;
|
||||
private int mTargetValue = 0;
|
||||
@ -87,6 +87,10 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
|
||||
setupLegend(mWeekChart);
|
||||
mTodayPieChart.setCenterText(mcd.getDayData().centerText);
|
||||
mTodayPieChart.setData(mcd.getDayData().data);
|
||||
//set custom renderer for 30days bar charts
|
||||
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
|
||||
mWeekChart.setRenderer(new AngledLabelsChartRenderer(mWeekChart, mWeekChart.getAnimator(), mWeekChart.getViewPortHandler()));
|
||||
}
|
||||
|
||||
mWeekChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
|
||||
mWeekChart.setData(mcd.getWeekBeforeData().getData());
|
||||
@ -102,6 +106,17 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
|
||||
// mBalanceView.setText(getBalanceMessage(balance));
|
||||
}
|
||||
|
||||
private String getWeeksChartsLabel(Calendar day){
|
||||
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
|
||||
//month, show day date
|
||||
return String.valueOf(day.get(Calendar.DAY_OF_MONTH));
|
||||
}
|
||||
else{
|
||||
//week, show short day name
|
||||
return day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale);
|
||||
}
|
||||
}
|
||||
|
||||
private WeekChartsData<BarData> refreshWeekBeforeData(DBHandler db, BarChart barChart, Calendar day, GBDevice device) {
|
||||
day = (Calendar) day.clone(); // do not modify the caller's argument
|
||||
day.add(Calendar.DATE, -TOTAL_DAYS);
|
||||
@ -114,7 +129,7 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
|
||||
|
||||
balance += calculateBalance(amounts);
|
||||
entries.add(new BarEntry(counter, getTotalsForActivityAmounts(amounts)));
|
||||
labels.add(day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
|
||||
labels.add(getWeeksChartsLabel(day));
|
||||
day.add(Calendar.DATE, 1);
|
||||
}
|
||||
|
||||
@ -130,7 +145,28 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
|
||||
barChart.getAxisLeft().removeAllLimitLines();
|
||||
barChart.getAxisLeft().addLimitLine(target);
|
||||
|
||||
return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue));
|
||||
float average = 0;
|
||||
if (TOTAL_DAYS > 0) {
|
||||
average = Math.abs(balance / TOTAL_DAYS);
|
||||
}
|
||||
LimitLine average_line = new LimitLine(average);
|
||||
average_line.setLabel(getString(R.string.average, getAverage(average)));
|
||||
|
||||
if (average > (mTargetValue)) {
|
||||
average_line.setLineColor(Color.GREEN);
|
||||
average_line.setTextColor(Color.GREEN);
|
||||
}
|
||||
else {
|
||||
average_line.setLineColor(Color.RED);
|
||||
average_line.setTextColor(Color.RED);
|
||||
}
|
||||
if (average > 0) {
|
||||
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
barChart.getAxisLeft().addLimitLine(average_line);
|
||||
}
|
||||
}
|
||||
|
||||
return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue));
|
||||
}
|
||||
|
||||
private DayData refreshDayPie(DBHandler db, Calendar day, GBDevice device) {
|
||||
@ -315,6 +351,16 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
|
||||
return amounts;
|
||||
}
|
||||
|
||||
private int getRangeDays(){
|
||||
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
|
||||
return 30;}
|
||||
else{
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
abstract String getAverage(float value);
|
||||
|
||||
abstract int getGoal();
|
||||
|
||||
abstract int getOffsetHours();
|
||||
@ -325,11 +371,11 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
|
||||
|
||||
abstract String[] getPieLabels();
|
||||
|
||||
abstract IValueFormatter getPieValueFormatter();
|
||||
abstract ValueFormatter getPieValueFormatter();
|
||||
|
||||
abstract IValueFormatter getBarValueFormatter();
|
||||
abstract ValueFormatter getBarValueFormatter();
|
||||
|
||||
abstract IAxisValueFormatter getYAxisFormatter();
|
||||
abstract ValueFormatter getYAxisFormatter();
|
||||
|
||||
abstract int[] getColors();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
@ -338,6 +350,24 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
||||
return 5;
|
||||
}
|
||||
|
||||
private String getSleepTitle() {
|
||||
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
|
||||
return getString(R.string.weeksleepchart_sleep_a_month);
|
||||
}
|
||||
else{
|
||||
return getString(R.string.weeksleepchart_sleep_a_week);
|
||||
}
|
||||
}
|
||||
|
||||
public String getStepsTitle() {
|
||||
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
|
||||
return getString(R.string.weekstepschart_steps_a_month);
|
||||
}
|
||||
else{
|
||||
return getString(R.string.weekstepschart_steps_a_week);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
@ -346,9 +376,9 @@ public class ChartsActivity extends AbstractGBFragmentActivity implements Charts
|
||||
case 1:
|
||||
return getString(R.string.sleepchart_your_sleep);
|
||||
case 2:
|
||||
return getString(R.string.weeksleepchart_sleep_a_week);
|
||||
return getSleepTitle();
|
||||
case 3:
|
||||
return getString(R.string.weekstepschart_steps_a_week);
|
||||
return getStepsTitle();
|
||||
case 4:
|
||||
return getString(R.string.stats_title);
|
||||
case 5:
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -40,7 +36,12 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
public class WeekSleepChartFragment extends AbstractWeekChartFragment {
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.weeksleepchart_sleep_a_week);
|
||||
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
|
||||
return getString(R.string.weeksleepchart_sleep_a_month);
|
||||
}
|
||||
else{
|
||||
return getString(R.string.weeksleepchart_sleep_a_week);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -110,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);
|
||||
}
|
||||
};
|
||||
@ -167,4 +168,10 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
|
||||
private String getHM(long value) {
|
||||
return DateTimeUtils.formatDurationHoursMinutes(value, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getAverage(float value) {
|
||||
return getHM((long)value);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,8 +18,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
|
||||
import com.github.mikephil.charting.formatter.IValueFormatter;
|
||||
import com.github.mikephil.charting.formatter.ValueFormatter;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
@ -30,7 +29,12 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
public class WeekStepsChartFragment extends AbstractWeekChartFragment {
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.weekstepschart_steps_a_week);
|
||||
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
|
||||
return getString(R.string.weekstepschart_steps_a_month);
|
||||
}
|
||||
else{
|
||||
return getString(R.string.weekstepschart_steps_a_week);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -77,17 +81,17 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
IValueFormatter getPieValueFormatter() {
|
||||
ValueFormatter getPieValueFormatter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
IValueFormatter getBarValueFormatter() {
|
||||
ValueFormatter getBarValueFormatter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
IAxisValueFormatter getYAxisFormatter() {
|
||||
ValueFormatter getYAxisFormatter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -113,4 +117,9 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
|
||||
} else
|
||||
return getString(R.string.no_data);
|
||||
}
|
||||
|
||||
@Override
|
||||
String getAverage(float value) {
|
||||
return String.format("%.0f", value);
|
||||
}
|
||||
}
|
||||
|
@ -16,18 +16,21 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
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.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;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB_OFF;
|
||||
@ -282,8 +285,10 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
addPreferenceHandlerFor(PREF_SWIPE_UNLOCK);
|
||||
addPreferenceHandlerFor(PREF_MI2_DATEFORMAT);
|
||||
addPreferenceHandlerFor("dateformat");
|
||||
addPreferenceHandlerFor(HuamiConst.PREF_DISPLAY_ITEMS);
|
||||
addPreferenceHandlerFor(PREF_DATEFORMAT);
|
||||
addPreferenceHandlerFor(PREF_DISPLAY_ITEMS);
|
||||
addPreferenceHandlerFor(PREF_LANGUAGE);
|
||||
addPreferenceHandlerFor(PREF_EXPOSE_HR_THIRDPARTY);
|
||||
|
||||
String displayOnLiftState = prefs.getString(PREF_ACTIVATE_DISPLAY_ON_LIFT, PREF_DO_NOT_DISTURB_OFF);
|
||||
boolean displayOnLiftScheduled = displayOnLiftState.equals(PREF_DO_NOT_DISTURB_SCHEDULED);
|
||||
|
@ -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);
|
||||
|
@ -59,6 +59,10 @@ public class HuamiConst {
|
||||
public static final String PREF_DISCONNECT_NOTIFICATION_END = "disconnect_notification_end";
|
||||
|
||||
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;
|
||||
|
||||
|
@ -49,6 +49,8 @@ public class HuamiService {
|
||||
// service uuid fee1
|
||||
public static final UUID UUID_CHARACTERISTIC_AUTH = UUID.fromString("00000009-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_DEVICEEVENT = UUID.fromString("00000010-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_AUDIO = UUID.fromString("00000012-0000-3512-2118-0009af100700");
|
||||
public static final UUID UUID_CHARACTERISTIC_AUDIODATA = UUID.fromString("00000013-0000-3512-2118-0009af100700");
|
||||
|
||||
public static final UUID UUID_CHARACTERISTIC_CHUNKEDTRANSFER = UUID.fromString("00000020-0000-3512-2118-0009af100700");
|
||||
|
||||
@ -137,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") || name.equalsIgnoreCase("Amazfit Bip Lite"))) {
|
||||
return DeviceType.AMAZFITBIP;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
@ -81,8 +81,10 @@ public class AmazfitBipCoordinator extends HuamiCoordinator {
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_amazfitbip,
|
||||
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
|
||||
};
|
||||
}
|
||||
|
@ -84,8 +84,11 @@ public class AmazfitCorCoordinator extends HuamiCoordinator {
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{
|
||||
R.xml.devicesettings_amazfitcor,
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -87,6 +87,7 @@ public class MiBand2Coordinator extends HuamiCoordinator {
|
||||
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
|
||||
};
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ public class MiBand3Coordinator extends HuamiCoordinator {
|
||||
R.xml.devicesettings_donotdisturb_withauto,
|
||||
R.xml.devicesettings_liftwrist_display,
|
||||
R.xml.devicesettings_swipeunlock,
|
||||
R.xml.devicesettings_expose_hr_thirdparty,
|
||||
R.xml.devicesettings_pairingkey
|
||||
};
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.EN
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService.ENDPOINT_DISPLAY_ITEMS;
|
||||
|
||||
public class MiBand3Service {
|
||||
public static final byte[] COMMAND_CHANGE_SCREENS = new byte[]{ENDPOINT_DISPLAY_ITEMS, DISPLAY_ITEM_BIT_CLOCK, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00};
|
||||
public static final byte[] COMMAND_CHANGE_SCREENS = new byte[]{ENDPOINT_DISPLAY_ITEMS, DISPLAY_ITEM_BIT_CLOCK, 0x30, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
public static final byte[] COMMAND_ENABLE_BAND_SCREEN_UNLOCK = new byte[]{ENDPOINT_DISPLAY, 0x16, 0x00, 0x01};
|
||||
public static final byte[] COMMAND_DISABLE_BAND_SCREEN_UNLOCK = new byte[]{ENDPOINT_DISPLAY, 0x16, 0x00, 0x00};
|
||||
public static final byte[] COMMAND_NIGHT_MODE_OFF = new byte[]{0x1a, 0x00};
|
||||
|
@ -90,9 +90,9 @@ public class MiBand4Coordinator extends HuamiCoordinator {
|
||||
R.xml.devicesettings_miband3,
|
||||
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
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,140 @@
|
||||
/* Copyright (C) 2019 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
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.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class MijiaLywsd02Coordinator extends AbstractDeviceCoordinator {
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
String name = candidate.getDevice().getName();
|
||||
if (name != null && name.equals("LYWSD02")) {
|
||||
return DeviceType.MIJIA_LYWSD02;
|
||||
}
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.MIJIA_LYWSD02;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle(GBDevice deviceCandidate) {
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Xiaomi";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getAppsManagementActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCalendarEvents() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
|
||||
// nothing to delete, yet
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
@ -34,6 +34,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class VibratissimoCoordinator extends AbstractDeviceCoordinator {
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
String name = candidate.getDevice().getName();
|
||||
@ -129,7 +130,7 @@ public class VibratissimoCoordinator 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) {
|
||||
// nothing to delete, yet
|
||||
}
|
||||
}
|
||||
|
@ -158,4 +158,7 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
|
||||
public int getBondingStyle(GBDevice device) {
|
||||
return BONDING_STYLE_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsUnicodeEmojis() { return true; }
|
||||
}
|
||||
|
@ -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
|
||||
/*
|
||||
|
@ -116,9 +116,9 @@ public class ActivityKind {
|
||||
case TYPE_NOT_MEASURED:
|
||||
return R.drawable.ic_activity_not_measured;
|
||||
case TYPE_LIGHT_SLEEP:
|
||||
return R.drawable.ic_activity_light_sleep;
|
||||
return R.drawable.ic_activity_sleep;
|
||||
case TYPE_DEEP_SLEEP:
|
||||
return R.drawable.ic_activity_deep_sleep;
|
||||
return R.drawable.ic_activity_sleep;
|
||||
case TYPE_RUNNING:
|
||||
return R.drawable.ic_activity_running;
|
||||
case TYPE_WALKING:
|
||||
@ -128,7 +128,9 @@ public class ActivityKind {
|
||||
case TYPE_TREADMILL:
|
||||
return R.drawable.ic_activity_walking;
|
||||
case TYPE_EXERCISE: // fall through
|
||||
return R.drawable.ic_activity_exercise;
|
||||
case TYPE_SWIMMING: // fall through
|
||||
return R.drawable.ic_activity_swimming;
|
||||
case TYPE_NOT_WORN: // fall through
|
||||
case TYPE_ACTIVITY: // fall through
|
||||
case TYPE_UNKNOWN: // fall through
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -57,6 +57,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),
|
||||
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);
|
||||
|
||||
private final int key;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -30,19 +30,20 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor2.AmazfitCor2Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband4.MiBand4Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.BFH16DeviceSupport;
|
||||
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.AmazfitBipSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor.AmazfitCorSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor2.AmazfitCor2Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband3.MiBand3Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.miband4.MiBand4Support;
|
||||
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.miband.MiBandSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaLywsd02Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
|
||||
@ -58,13 +59,13 @@ public class DeviceSupportFactory {
|
||||
private final BluetoothAdapter mBtAdapter;
|
||||
private final Context mContext;
|
||||
|
||||
public DeviceSupportFactory(Context context) {
|
||||
DeviceSupportFactory(Context context) {
|
||||
mContext = context;
|
||||
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
}
|
||||
|
||||
public synchronized DeviceSupport createDeviceSupport(GBDevice device) throws GBException {
|
||||
DeviceSupport deviceSupport = null;
|
||||
DeviceSupport deviceSupport;
|
||||
String deviceAddress = device.getAddress();
|
||||
int indexFirstColon = deviceAddress.indexOf(":");
|
||||
if (indexFirstColon > 0) {
|
||||
@ -196,6 +197,8 @@ public class DeviceSupportFactory {
|
||||
case BFH16:
|
||||
deviceSupport = new ServiceDeviceSupport(new BFH16DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
case MIJIA_LYWSD02:
|
||||
deviceSupport = new ServiceDeviceSupport(new MijiaLywsd02Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
}
|
||||
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();
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
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.service.devices.huami.amazfitbip;
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -33,43 +33,35 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class ActivityDetailsParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ActivityDetailsParser.class);
|
||||
public class HuamiActivityDetailsParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class);
|
||||
|
||||
private static final byte TYPE_GPS = 0;
|
||||
private static final byte TYPE_HR = 1;
|
||||
private static final byte TYPE_UNKNOWN2 = 2;
|
||||
private static final byte TYPE_PAUSE = 3;
|
||||
private static final byte TYPE_PAUSE = 2;
|
||||
private static final byte TYPE_RESUME = 3;
|
||||
private static final byte TYPE_SPEED4 = 4;
|
||||
private static final byte TYPE_SPEED5 = 5;
|
||||
private static final byte TYPE_GPS_SPEED6 = 6;
|
||||
private static final byte TYPE_SPEED6 = 6;
|
||||
private static final byte TYPE_SWIMMING = 8;
|
||||
|
||||
public static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal(3000000.0);
|
||||
private final BaseActivitySummary summary;
|
||||
private static final BigDecimal HUAMI_TO_DECIMAL_DEGREES_DIVISOR = new BigDecimal(3000000.0);
|
||||
private final ActivityTrack activityTrack;
|
||||
// private final int version;
|
||||
private final Date baseDate;
|
||||
private long baseLongitude;
|
||||
private long baseLatitude;
|
||||
private int baseAltitude;
|
||||
private ActivityPoint lastActivityPoint;
|
||||
|
||||
public boolean getSkipCounterByte() {
|
||||
return skipCounterByte;
|
||||
}
|
||||
|
||||
public void setSkipCounterByte(boolean skipCounterByte) {
|
||||
this.skipCounterByte = skipCounterByte;
|
||||
}
|
||||
|
||||
private boolean skipCounterByte;
|
||||
|
||||
public ActivityDetailsParser(BaseActivitySummary summary) {
|
||||
this.summary = summary;
|
||||
// this.version = version;
|
||||
// this.baseDate = baseDate;
|
||||
//
|
||||
public HuamiActivityDetailsParser(BaseActivitySummary summary) {
|
||||
this.baseLongitude = summary.getBaseLongitude();
|
||||
this.baseLatitude = summary.getBaseLatitude();
|
||||
this.baseAltitude = summary.getBaseAltitude();
|
||||
@ -109,21 +101,24 @@ public class ActivityDetailsParser {
|
||||
case TYPE_HR:
|
||||
i += consumeHeartRate(bytes, i, totalTimeOffset);
|
||||
break;
|
||||
case TYPE_UNKNOWN2:
|
||||
i += consumeUnknown2(bytes, i);
|
||||
break;
|
||||
case TYPE_PAUSE:
|
||||
i += consumePause(bytes, i);
|
||||
break;
|
||||
case TYPE_RESUME:
|
||||
i += consumeResume(bytes, i);
|
||||
break;
|
||||
case TYPE_SPEED4:
|
||||
i += consumeSpeed4(bytes, i);
|
||||
break;
|
||||
case TYPE_SPEED5:
|
||||
i += consumeSpeed5(bytes, i);
|
||||
break;
|
||||
case TYPE_GPS_SPEED6:
|
||||
case TYPE_SPEED6:
|
||||
i += consumeSpeed6(bytes, i);
|
||||
break;
|
||||
case TYPE_SWIMMING:
|
||||
i += consumeSwimming(bytes, i);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("unknown packet type" + type);
|
||||
i+=6;
|
||||
@ -213,7 +208,6 @@ public class ActivityDetailsParser {
|
||||
|
||||
if (v2 == 0 && v3 == 0 && v4 == 0 && v5 == 0 && v6 == 0) {
|
||||
// new version
|
||||
// LOG.info("detected heart rate in 'new' version, where version is: " + summary.getVersion());
|
||||
LOG.info("detected heart rate in 'new' version format");
|
||||
ActivityPoint ap = getActivityPointFor(timeOffsetSeconds);
|
||||
ap.setHeartRate(v1);
|
||||
@ -270,23 +264,33 @@ public class ActivityDetailsParser {
|
||||
}
|
||||
}
|
||||
|
||||
private int consumeUnknown2(byte[] bytes, int offset) {
|
||||
return 6; // just guessing...
|
||||
private int consumePause(byte[] bytes, int offset) {
|
||||
LOG.debug("got pause packet: " + GB.hexdump(bytes, offset, 6));
|
||||
return 6;
|
||||
}
|
||||
|
||||
private int consumePause(byte[] bytes, int i) {
|
||||
return 6; // just guessing...
|
||||
private int consumeResume(byte[] bytes, int offset) {
|
||||
LOG.debug("got resume package: " + GB.hexdump(bytes, offset, 6));
|
||||
return 6;
|
||||
}
|
||||
|
||||
private int consumeSpeed4(byte[] bytes, int offset) {
|
||||
LOG.debug("got packet type 4 (speed): " + GB.hexdump(bytes, offset, 6));
|
||||
return 6;
|
||||
}
|
||||
|
||||
private int consumeSpeed5(byte[] bytes, int offset) {
|
||||
LOG.debug("got packet type 5 (speed): " + GB.hexdump(bytes, offset, 6));
|
||||
return 6;
|
||||
}
|
||||
|
||||
private int consumeSpeed6(byte[] bytes, int offset) {
|
||||
LOG.debug("got packet type 6 (speed): " + GB.hexdump(bytes, offset, 6));
|
||||
return 6;
|
||||
}
|
||||
|
||||
private int consumeSwimming(byte[] bytes, int offset) {
|
||||
LOG.debug("got packet type 8 (swimming?): " + GB.hexdump(bytes, offset, 6));
|
||||
return 6;
|
||||
}
|
||||
}
|
@ -14,20 +14,20 @@
|
||||
|
||||
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.service.devices.huami.amazfitbip;
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
public enum BipActivityType {
|
||||
public enum HuamiSportsActivityType {
|
||||
Outdoor(1),
|
||||
Treadmill(2),
|
||||
Walking(3),
|
||||
Cycling(4),
|
||||
Exercise(5);
|
||||
|
||||
Exercise(5),
|
||||
Swimming(6);
|
||||
private final int code;
|
||||
|
||||
BipActivityType(final int code) {
|
||||
HuamiSportsActivityType(final int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
@ -43,20 +43,22 @@ public enum BipActivityType {
|
||||
return ActivityKind.TYPE_WALKING;
|
||||
case Exercise:
|
||||
return ActivityKind.TYPE_EXERCISE;
|
||||
case Swimming:
|
||||
return ActivityKind.TYPE_SWIMMING;
|
||||
}
|
||||
throw new RuntimeException("Not mapped activity kind for: " + this);
|
||||
}
|
||||
|
||||
public static BipActivityType fromCode(int bipCode) {
|
||||
for (BipActivityType type : values()) {
|
||||
if (type.code == bipCode) {
|
||||
public static HuamiSportsActivityType fromCode(int huamiCode) {
|
||||
for (HuamiSportsActivityType type : values()) {
|
||||
if (type.code == huamiCode) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("No matching BipActivityType for code: " + bipCode);
|
||||
throw new RuntimeException("No matching HuamiSportsActivityType for code: " + huamiCode);
|
||||
}
|
||||
|
||||
public static BipActivityType fromActivityKind(int activityKind) {
|
||||
public static HuamiSportsActivityType fromActivityKind(int activityKind) {
|
||||
switch (activityKind) {
|
||||
case ActivityKind.TYPE_RUNNING:
|
||||
return Outdoor;
|
||||
@ -68,6 +70,8 @@ public enum BipActivityType {
|
||||
return Walking;
|
||||
case ActivityKind.TYPE_EXERCISE:
|
||||
return Exercise;
|
||||
case ActivityKind.TYPE_SWIMMING:
|
||||
return Swimming;
|
||||
}
|
||||
throw new RuntimeException("No matching activity activityKind: " + activityKind);
|
||||
}
|
@ -42,11 +42,13 @@ 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;
|
||||
@ -66,6 +68,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 +98,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;
|
||||
@ -123,23 +127,12 @@ import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
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;
|
||||
@ -301,6 +294,8 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), enable);
|
||||
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_6_BATTERY_INFO), enable);
|
||||
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_DEVICEEVENT), enable);
|
||||
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIO), enable);
|
||||
builder.notify(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_AUDIODATA), enable);
|
||||
|
||||
return this;
|
||||
}
|
||||
@ -325,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) {
|
||||
@ -437,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,
|
||||
@ -561,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);
|
||||
@ -682,7 +628,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
sendCalendarEvents(builder);
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to set time on MI device", ex);
|
||||
LOG.error("Unable to set time on Huami device", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@ -701,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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -760,9 +706,8 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void sendMusicStateToDevice() {
|
||||
|
||||
|
||||
if (characteristicChunked == null) {
|
||||
return;
|
||||
}
|
||||
@ -878,7 +823,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
builder.write(characteristicHRControlPoint, startHeartMeasurementManual);
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to read heart rate with MI2", ex);
|
||||
LOG.error("Unable to read heart rate from Huami device", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@ -939,7 +884,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
try {
|
||||
new FetchActivityOperation(this).perform();
|
||||
} catch (IOException ex) {
|
||||
LOG.error("Unable to fetch MI activity data", ex);
|
||||
LOG.error("Unable to fetch activity data", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@ -969,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);
|
||||
@ -1557,9 +1502,15 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
case MiBandConst.PREF_SWIPE_UNLOCK:
|
||||
setBandScreenUnlock(builder);
|
||||
break;
|
||||
case "dateformat":
|
||||
case HuamiConst.PREF_DATEFORMAT:
|
||||
setDateFormat(builder);
|
||||
break;
|
||||
case HuamiConst.PREF_LANGUAGE:
|
||||
setLanguage(builder);
|
||||
break;
|
||||
case HuamiConst.PREF_EXPOSE_HR_THIRDPARTY:
|
||||
setExposeHRThridParty(builder);
|
||||
break;
|
||||
}
|
||||
builder.queue(getQueue());
|
||||
} catch (IOException e) {
|
||||
@ -1579,7 +1530,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) {
|
||||
@ -1904,6 +2038,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;
|
||||
@ -1932,6 +2080,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);
|
||||
@ -1954,6 +2138,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
|
||||
setInactivityWarnings(builder);
|
||||
setHeartrateSleepSupport(builder);
|
||||
setDisconnectNotification(builder);
|
||||
setExposeHRThridParty(builder);
|
||||
setHeartrateMeasurementInterval(builder, getHeartRateMeasurementInterval());
|
||||
}
|
||||
|
||||
|
@ -107,6 +107,11 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
|
||||
crcToVersion.put(60002, "1.1.5.04");
|
||||
crcToVersion.put(5229, "1.1.5.12");
|
||||
crcToVersion.put(32576, "1.1.5.16");
|
||||
crcToVersion.put(28893, "1.1.5.24");
|
||||
crcToVersion.put(61710, "1.1.5.56");
|
||||
|
||||
// Latin Firmware
|
||||
crcToVersion.put(52828, "1.1.5.36 (Latin)");
|
||||
|
||||
// resources
|
||||
crcToVersion.put(12586, "0.0.8.74");
|
||||
@ -130,7 +135,9 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
|
||||
crcToVersion.put(23073, "0.1.1.45");
|
||||
crcToVersion.put(59245, "1.0.2.00");
|
||||
crcToVersion.put(20591, "1.1.2.05");
|
||||
crcToVersion.put(5341, "1.1.5.02-16");
|
||||
crcToVersion.put(5341, "1.1.5.02-24");
|
||||
crcToVersion.put(22662, "1.1.5.36");
|
||||
crcToVersion.put(24045, "1.1.5.56");
|
||||
|
||||
// gps
|
||||
crcToVersion.put(61520, "9367,8f79a91,0,0,");
|
||||
|
@ -28,43 +28,30 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.SimpleTimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import cyanogenmod.weather.util.WeatherUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
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.AmazfitBipFWHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ConditionalWriteAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertNotificationProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiIcon;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.operations.AmazfitBipFetchLogsOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchActivityOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSportsSummaryOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Version;
|
||||
|
||||
public class AmazfitBipSupport extends HuamiSupport {
|
||||
|
||||
@ -252,187 +239,6 @@ public class AmazfitBipSupport extends HuamiSupport {
|
||||
builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (gbDevice.getType() != DeviceType.AMAZFITCOR) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (gbDevice.getType() == DeviceType.AMAZFITCOR) {
|
||||
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);
|
||||
|
||||
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
|
||||
builder.queue(getQueue());
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error sending current forecast location", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
try {
|
||||
@ -442,9 +248,8 @@ public class AmazfitBipSupport extends HuamiSupport {
|
||||
} else if (dataTypes == RecordedDataTypes.TYPE_GPS_TRACKS) {
|
||||
new FetchSportsSummaryOperation(this).perform();
|
||||
} else if (dataTypes == RecordedDataTypes.TYPE_DEBUGLOGS) {
|
||||
new AmazfitBipFetchLogsOperation(this).perform();
|
||||
}
|
||||
else {
|
||||
new HuamiFetchDebugLogsOperation(this).perform();
|
||||
} else {
|
||||
LOG.warn("fetching multiple data types at once is not supported yet");
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
|
@ -86,4 +86,10 @@ public class Mi2TextNotificationStrategy extends Mi2NotificationStrategy {
|
||||
NewAlert alert = new NewAlert(category, 1, simpleNotification.getMessage());
|
||||
profile.newAlert(builder, alert, OverflowStrategy.MAKE_MULTIPLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopCurrentNotification(TransactionBuilder builder) {
|
||||
BluetoothGattCharacteristic alert = getSupport().getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_NEW_ALERT);
|
||||
builder.write(alert, new byte[]{(byte) AlertCategory.IncomingCall.getId(), 0});
|
||||
}
|
||||
}
|
||||
|
@ -86,9 +86,13 @@ public class MiBand3Support extends AmazfitBipSupport {
|
||||
command[1] |= 0x80;
|
||||
command[10] = pos++;
|
||||
}
|
||||
if (pages.contains("nfc")) {
|
||||
command[2] |= 0x01;
|
||||
command[11] = pos++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 4; i <= 10; i++) {
|
||||
for (int i = 4; i <= 11; i++) {
|
||||
if (command[i] == 0) {
|
||||
command[i] = pos++;
|
||||
}
|
||||
|
@ -38,9 +38,13 @@ public class MiBand4FirmwareInfo extends HuamiFirmwareInfo {
|
||||
static {
|
||||
// firmware
|
||||
crcToVersion.put(8969, "1.0.5.22");
|
||||
crcToVersion.put(43437, "1.0.5.66");
|
||||
crcToVersion.put(31632, "1.0.6.00");
|
||||
|
||||
// resources
|
||||
crcToVersion.put(27412, "1.0.5.22");
|
||||
crcToVersion.put(5466, "1.0.5.66");
|
||||
crcToVersion.put(20047, "1.0.6.00");
|
||||
|
||||
// font
|
||||
crcToVersion.put(31978, "1");
|
||||
@ -65,7 +69,7 @@ public class MiBand4FirmwareInfo extends HuamiFirmwareInfo {
|
||||
return HuamiFirmwareType.WATCHFACE;
|
||||
}
|
||||
if (ArrayUtils.startsWith(bytes, NEWFT_HEADER)) {
|
||||
if (bytes[10] == 0x03) {
|
||||
if (bytes[10] == 0x03 || bytes[10] == 0x06) {
|
||||
return HuamiFirmwareType.FONT;
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ public class FetchActivityOperation extends AbstractFetchOperation {
|
||||
}
|
||||
|
||||
super.handleActivityFetchFinish(success);
|
||||
GB.signalActivityDataFinish();
|
||||
}
|
||||
|
||||
private boolean needsAnotherFetch(GregorianCalendar lastSyncTimestamp) {
|
||||
|
@ -40,7 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.ActivityDetailsParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiActivityDetailsParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
@ -86,7 +86,7 @@ public class FetchSportsDetailsOperation extends AbstractFetchOperation {
|
||||
|
||||
|
||||
if (success) {
|
||||
ActivityDetailsParser parser = new ActivityDetailsParser(summary);
|
||||
HuamiActivityDetailsParser parser = new HuamiActivityDetailsParser(summary);
|
||||
parser.setSkipCounterByte(false); // is already stripped
|
||||
try {
|
||||
ActivityTrack track = parser.parse(buffer.toByteArray());
|
||||
|
@ -43,8 +43,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSportsActivityType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.BipActivityType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
/**
|
||||
@ -141,7 +141,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((byte) (lastPacketCounter + 1) == value[0] ) {
|
||||
if ((byte) (lastPacketCounter + 1) == value[0]) {
|
||||
lastPacketCounter++;
|
||||
bufferActivityData(value);
|
||||
} else {
|
||||
@ -154,6 +154,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
|
||||
/**
|
||||
* Buffers the given activity summary data. If the total size is reached,
|
||||
* it is converted to an object and saved in the database.
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
@Override
|
||||
@ -166,14 +167,14 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(stream.toByteArray()).order(ByteOrder.LITTLE_ENDIAN);
|
||||
// summary.setVersion(BLETypeConversions.toUnsigned(buffer.getShort()));
|
||||
short version = buffer.getShort(); // version
|
||||
LOG.debug("Got verison " + version);
|
||||
LOG.debug("Got sport summary version " + version + "total bytes=" + buffer.capacity());
|
||||
int activityKind = ActivityKind.TYPE_UNKNOWN;
|
||||
try {
|
||||
int rawKind = BLETypeConversions.toUnsigned(buffer.getShort());
|
||||
BipActivityType activityType = BipActivityType.fromCode(rawKind);
|
||||
HuamiSportsActivityType activityType = HuamiSportsActivityType.fromCode(rawKind);
|
||||
activityKind = activityType.toActivityKind();
|
||||
} catch (Exception ex) {
|
||||
LOG.error("Error mapping acivity kind: " + ex.getMessage(), ex);
|
||||
LOG.error("Error mapping activity kind: " + ex.getMessage(), ex);
|
||||
}
|
||||
summary.setActivityKind(activityKind);
|
||||
|
||||
@ -197,44 +198,117 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
|
||||
summary.setBaseLongitude(baseLongitude);
|
||||
summary.setBaseLatitude(baseLatitude);
|
||||
summary.setBaseAltitude(baseAltitude);
|
||||
|
||||
// unused data (for now)
|
||||
float distanceMeters = buffer.getFloat();
|
||||
float ascentMeters = buffer.getFloat();
|
||||
float descentMeters = buffer.getFloat();
|
||||
float maxAltitude = buffer.getFloat();
|
||||
float minAltitude = buffer.getFloat();
|
||||
int maxLatitude = buffer.getInt(); // format?
|
||||
int minLatitude = buffer.getInt(); // format?
|
||||
int maxLongitude = buffer.getInt(); // format?
|
||||
int minLongitude = buffer.getInt(); // format?
|
||||
int steps = buffer.getInt();
|
||||
int activeSeconds = buffer.getInt();
|
||||
float caloriesBurnt = buffer.getFloat();
|
||||
float maxSpeed = buffer.getFloat();
|
||||
float minPace = buffer.getFloat(); // format?
|
||||
float maxPace = buffer.getFloat(); // format?
|
||||
float totalStride = buffer.getFloat();
|
||||
|
||||
buffer.getInt(); // unknown
|
||||
|
||||
if (activityKind == ActivityKind.TYPE_SWIMMING) {
|
||||
// 28 bytes
|
||||
float averageStrokeDistance = buffer.getFloat();
|
||||
float averageStrokesPerSecond = buffer.getFloat();
|
||||
float averageLapPace = buffer.getFloat();
|
||||
short strokes = buffer.getShort();
|
||||
short swolfIndex = buffer.getShort();
|
||||
byte swimStyle = buffer.get();
|
||||
byte laps = buffer.get();
|
||||
buffer.getInt(); // unknown
|
||||
buffer.getInt(); // unknown
|
||||
buffer.getShort(); // unknown
|
||||
|
||||
LOG.debug("unused swim data:" +
|
||||
"\naverageStrokeDistance=" + averageStrokeDistance +
|
||||
"\naverageStrokesPerSecond=" + averageStrokesPerSecond +
|
||||
"\naverageLapPace" + averageLapPace +
|
||||
"\nstrokes=" + strokes +
|
||||
"\nswolfIndex=" + swolfIndex +
|
||||
"\nswimStyle=" + swimStyle + // 1 = breast, 2 = freestyle
|
||||
"\nlaps=" + laps +
|
||||
""
|
||||
);
|
||||
} else {
|
||||
// 28 bytes
|
||||
buffer.getInt(); // unknown
|
||||
buffer.getInt(); // unknown
|
||||
int ascentSeconds = buffer.getInt() / 1000; //ms?
|
||||
buffer.getInt(); // unknown;
|
||||
int descentSeconds = buffer.getInt() / 1000; //ms?
|
||||
buffer.getInt(); // unknown;
|
||||
int flatSeconds = buffer.getInt() / 1000; // ms?
|
||||
LOG.debug("unused non-swim data:" +
|
||||
"\nascentSeconds=" + ascentSeconds +
|
||||
"\ndescentSeconds=" + descentSeconds +
|
||||
"\nflatSeconds=" + flatSeconds +
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
short averageHR = buffer.getShort();
|
||||
short averageKMPaceSeconds = buffer.getShort();
|
||||
short averageStride = buffer.getShort();
|
||||
|
||||
LOG.debug("unused common:" +
|
||||
"\ndistanceMeters=" + distanceMeters +
|
||||
"\nascentMeters=" + ascentMeters +
|
||||
"\ndescentMeters=" + descentMeters +
|
||||
"\nmaxAltitude=" + maxAltitude +
|
||||
"\nminAltitude=" + minAltitude +
|
||||
//"\nmaxLatitude=" + maxLatitude + // not useful
|
||||
//"\nminLatitude=" + minLatitude + // not useful
|
||||
//"\nmaxLongitude=" + maxLongitude + // not useful
|
||||
//"\nminLongitude=" + minLongitude + // not useful
|
||||
"\nsteps=" + steps +
|
||||
"\nactiveSeconds=" + activeSeconds +
|
||||
"\ncaloriesBurnt=" + caloriesBurnt +
|
||||
"\nmaxSpeed=" + maxSpeed +
|
||||
"\nminPace=" + minPace +
|
||||
"\nmaxPace=" + maxPace +
|
||||
"\ntotalStride=" + totalStride +
|
||||
"\naverageHR=" + averageHR +
|
||||
"\naverageKMPaceSeconds=" + averageKMPaceSeconds +
|
||||
"\naverageStride=" + averageStride +
|
||||
""
|
||||
);
|
||||
|
||||
// summary.setBaseCoordinate(new GPSCoordinate(baseLatitude, baseLongitude, baseAltitude));
|
||||
|
||||
// summary.setDistanceMeters(Float.intBitsToFloat(buffer.getInt()));
|
||||
// summary.setAscentMeters(Float.intBitsToFloat(buffer.getInt()));
|
||||
// summary.setDescentMeters(Float.intBitsToFloat(buffer.getInt()));
|
||||
//
|
||||
// summary.setMinAltitude(Float.intBitsToFloat(buffer.getInt()));
|
||||
// summary.setMaxAltitude(Float.intBitsToFloat(buffer.getInt()));
|
||||
// summary.setMinLatitude(buffer.getInt());
|
||||
// summary.setMaxLatitude(buffer.getInt());
|
||||
// summary.setMinLongitude(buffer.getInt());
|
||||
// summary.setMaxLongitude(buffer.getInt());
|
||||
//
|
||||
// summary.setSteps(BLETypeConversions.toUnsigned(buffer.getInt()));
|
||||
// summary.setActiveTimeSeconds(BLETypeConversions.toUnsigned(buffer.getInt()));
|
||||
//
|
||||
// summary.setCaloriesBurnt(Float.intBitsToFloat(buffer.get()));
|
||||
// summary.setMaxSpeed(Float.intBitsToFloat(buffer.get()));
|
||||
// summary.setMinPace(Float.intBitsToFloat(buffer.get()));
|
||||
// summary.setMaxPace(Float.intBitsToFloat(buffer.get()));
|
||||
// summary.setTotalStride(Float.intBitsToFloat(buffer.get()));
|
||||
|
||||
buffer.getInt(); //
|
||||
buffer.getInt(); //
|
||||
buffer.getInt(); //
|
||||
|
||||
// summary.setTimeAscent(BLETypeConversions.toUnsigned(buffer.getInt()));
|
||||
// buffer.getInt(); //
|
||||
// summary.setTimeDescent(BLETypeConversions.toUnsigned(buffer.getInt()));
|
||||
// buffer.getInt(); //
|
||||
// summary.setTimeFlat(BLETypeConversions.toUnsigned(buffer.getInt()));
|
||||
//
|
||||
// summary.setAverageHR(BLETypeConversions.toUnsigned(buffer.getShort()));
|
||||
//
|
||||
// summary.setAveragePace(BLETypeConversions.toUnsigned(buffer.getShort()));
|
||||
// summary.setAverageStride(BLETypeConversions.toUnsigned(buffer.getShort()));
|
||||
|
||||
buffer.getShort(); //
|
||||
// summary.setDistanceMeters(distanceMeters);
|
||||
// summary.setAscentMeters(ascentMeters);
|
||||
// summary.setDescentMeters(descentMeters);
|
||||
// summary.setMinAltitude(maxAltitude);
|
||||
// summary.setMaxAltitude(maxAltitude);
|
||||
// summary.setMinLatitude(minLatitude);
|
||||
// summary.setMaxLatitude(maxLatitude);
|
||||
// summary.setMinLongitude(minLatitude);
|
||||
// summary.setMaxLongitude(maxLatitude);
|
||||
// summary.setSteps(steps);
|
||||
// summary.setActiveTimeSeconds(secondsActive);
|
||||
// summary.setCaloriesBurnt(caloriesBurnt);
|
||||
// summary.setMaxSpeed(maxSpeed);
|
||||
// summary.setMinPace(minPace);
|
||||
// summary.setMaxPace(maxPace);
|
||||
// summary.setTotalStride(totalStride);
|
||||
// summary.setTimeAscent(BLETypeConversions.toUnsigned(ascentSeconds);
|
||||
// summary.setTimeDescent(BLETypeConversions.toUnsigned(descentSeconds);
|
||||
// summary.setTimeFlat(BLETypeConversions.toUnsigned(flatSeconds);
|
||||
// summary.setAverageHR(BLETypeConversions.toUnsigned(averageHR);
|
||||
// summary.setAveragePace(BLETypeConversions.toUnsigned(averagePace);
|
||||
// summary.setAverageStride(BLETypeConversions.toUnsigned(averageStride);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
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.service.devices.huami.amazfitbip.operations;
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
@ -36,18 +36,17 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipS
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.AbstractFetchOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipFetchLogsOperation.class);
|
||||
public class HuamiFetchDebugLogsOperation extends AbstractFetchOperation {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuamiFetchDebugLogsOperation.class);
|
||||
|
||||
private FileOutputStream logOutputStream;
|
||||
|
||||
public AmazfitBipFetchLogsOperation(AmazfitBipSupport support) {
|
||||
public HuamiFetchDebugLogsOperation(AmazfitBipSupport support) {
|
||||
super(support);
|
||||
setName("fetch logs");
|
||||
setName("fetch debug logs");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -60,7 +59,7 @@ public class AmazfitBipFetchLogsOperation extends AbstractFetchOperation {
|
||||
}
|
||||
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US);
|
||||
String filename = "amazfitbip_" + dateFormat.format(new Date()) + ".log";
|
||||
String filename = "huamidebug_" + dateFormat.format(new Date()) + ".log";
|
||||
|
||||
File outputFile = new File(dir, filename );
|
||||
try {
|
@ -49,9 +49,6 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
//import java.util.concurrent.Executors;
|
||||
//import java.util.concurrent.ScheduledExecutorService;
|
||||
//import java.util.concurrent.ScheduledFuture;
|
||||
|
||||
/**
|
||||
* An operation that fetches activity data. For every fetch, a new operation must
|
||||
@ -71,7 +68,7 @@ public class FetchActivityOperation extends AbstractMiBand1Operation {
|
||||
private final boolean hasPacketCounter;
|
||||
|
||||
private class ActivityStruct {
|
||||
private int maxDataPacketLength = 20;
|
||||
private int maxDataPacketLength;
|
||||
private int lastNotifiedProgress;
|
||||
private final byte[] activityDataHolder;
|
||||
private final int activityDataHolderSize;
|
||||
@ -197,6 +194,7 @@ public class FetchActivityOperation extends AbstractMiBand1Operation {
|
||||
activityStruct = null;
|
||||
operationFinished();
|
||||
unsetBusy();
|
||||
GB.signalActivityDataFinish();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,293 @@
|
||||
/* Copyright (C) 2019 Andreas Shimokawa
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.SimpleTimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile;
|
||||
|
||||
public class MijiaLywsd02Support extends AbstractBTLEDeviceSupport {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MijiaLywsd02Support.class);
|
||||
private final DeviceInfoProfile<MijiaLywsd02Support> deviceInfoProfile;
|
||||
private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
|
||||
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
|
||||
private final IntentListener mListener = new IntentListener() {
|
||||
@Override
|
||||
public void notify(Intent intent) {
|
||||
String s = intent.getAction();
|
||||
if (Objects.equals(s, DeviceInfoProfile.ACTION_DEVICE_INFO)) {
|
||||
handleDeviceInfo((nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public MijiaLywsd02Support() {
|
||||
super(LOG);
|
||||
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
|
||||
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
|
||||
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
|
||||
addSupportedService(UUID.fromString("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6"));
|
||||
deviceInfoProfile = new DeviceInfoProfile<>(this);
|
||||
deviceInfoProfile.addListener(mListener);
|
||||
addSupportedProfile(deviceInfoProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
requestDeviceInfo(builder);
|
||||
setTime(builder);
|
||||
setInitialized(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private void setTime(TransactionBuilder builder) {
|
||||
BluetoothGattCharacteristic timeCharacteristc = getCharacteristic(UUID.fromString("ebe0ccb7-7a0a-4b0c-8a1a-6ff2997da3a6"));
|
||||
long ts = System.currentTimeMillis();
|
||||
byte offsetHours = (byte) (SimpleTimeZone.getDefault().getOffset(ts) / (1000 * 60 * 60));
|
||||
ts /= 1000;
|
||||
builder.write(timeCharacteristc, new byte[]{
|
||||
(byte) (ts & 0xff),
|
||||
(byte) ((ts >> 8) & 0xff),
|
||||
(byte) ((ts >> 16) & 0xff),
|
||||
(byte) ((ts >> 24) & 0xff),
|
||||
offsetHours});
|
||||
}
|
||||
|
||||
private void requestDeviceInfo(TransactionBuilder builder) {
|
||||
LOG.debug("Requesting Device Info!");
|
||||
deviceInfoProfile.requestDeviceInfo(builder);
|
||||
}
|
||||
|
||||
private void setInitialized(TransactionBuilder builder) {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) {
|
||||
LOG.warn("Device info: " + info);
|
||||
versionCmd.hwVersion = info.getHardwareRevision();
|
||||
versionCmd.fwVersion = info.getFirmwareRevision();
|
||||
handleGBDeviceEvent(versionCmd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(int id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
// better only on connect for now
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(CallSpec callSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(MusicStateSpec stateSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(MusicSpec musicSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeSteps(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInfoReq() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppStart(UUID uuid, boolean start) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppDelete(UUID uuid) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppReorder(UUID[] uuids) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReset(int flags) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean start) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetConstantVibration(int intensity) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenshotReq() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableHeartRateSleepSupport(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetHeartRateMeasurementInterval(int seconds) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteCalendarEvent(byte type, long id) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic) {
|
||||
if (super.onCharacteristicChanged(gatt, characteristic)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
LOG.info("Unhandled characteristic changed: " + characteristicUUID);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicRead(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic, int status) {
|
||||
if (super.onCharacteristicRead(gatt, characteristic, status)) {
|
||||
return true;
|
||||
}
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
|
||||
LOG.info("Unhandled characteristic read: " + characteristicUUID);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadConfiguration(String config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
|
||||
}
|
||||
}
|
@ -628,7 +628,7 @@ public class No1F1Support extends AbstractBTLEDeviceSupport {
|
||||
GB.updateTransferNotification(null,"", false, 100, getContext());
|
||||
if (getDevice().isBusy()) {
|
||||
getDevice().unsetBusyTask();
|
||||
getDevice().sendDeviceUpdateIntent(getContext());
|
||||
GB.signalActivityDataFinish();
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
|
@ -105,7 +105,7 @@ class AppMessageHandlerM7S extends AppMessageHandler {
|
||||
return HAIL;
|
||||
} else if (conditionCode >= 907 && conditionCode < 957) {
|
||||
return WIND;
|
||||
} else if (conditionCode == 905 || (conditionCode >= 957 && conditionCode < 900)) {
|
||||
} else if (conditionCode == 905) {
|
||||
return EXTREME_WIND;
|
||||
} else if (conditionCode == 900) {
|
||||
return TORNADO;
|
||||
|
@ -36,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleMisfitSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMisfitSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
class AppMessageHandlerMisfit extends AppMessageHandler {
|
||||
@ -71,6 +72,7 @@ class AppMessageHandlerMisfit extends AppMessageHandler {
|
||||
LOG.info("incoming data start");
|
||||
break;
|
||||
case KEY_INCOMING_DATA_END:
|
||||
GB.signalActivityDataFinish();
|
||||
LOG.info("incoming data end");
|
||||
break;
|
||||
case KEY_INCOMING_DATA:
|
||||
|
@ -35,7 +35,6 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepMonitorResult;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleMorpheuzSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.PebbleMorpheuzSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
@ -70,7 +69,7 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AppMessageHandlerMorpheuz.class);
|
||||
|
||||
public AppMessageHandlerMorpheuz(UUID uuid, PebbleProtocol pebbleProtocol) {
|
||||
AppMessageHandlerMorpheuz(UUID uuid, PebbleProtocol pebbleProtocol) {
|
||||
super(uuid, pebbleProtocol);
|
||||
|
||||
try {
|
||||
@ -108,16 +107,11 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
|
||||
@Override
|
||||
public GBDeviceEvent[] handleMessage(ArrayList<Pair<Integer, Object>> pairs) {
|
||||
int ctrl_message = 0;
|
||||
GBDeviceEventSleepMonitorResult sleepMonitorResult = null;
|
||||
|
||||
for (Pair<Integer, Object> pair : pairs) {
|
||||
if (Objects.equals(pair.first, keyTransmit)) {
|
||||
sleepMonitorResult = new GBDeviceEventSleepMonitorResult();
|
||||
sleepMonitorResult.smartalarm_from = smartalarm_from;
|
||||
sleepMonitorResult.smartalarm_to = smartalarm_to;
|
||||
sleepMonitorResult.alarm_gone_off = alarm_gone_off;
|
||||
sleepMonitorResult.recording_base_timestamp = recording_base_timestamp;
|
||||
ctrl_message |= CTRL_TRANSMIT_DONE;
|
||||
GB.signalActivityDataFinish();
|
||||
} else if (pair.first.equals(keyGoneoff)) {
|
||||
alarm_gone_off = (int) pair.second;
|
||||
LOG.info("got gone off: " + alarm_gone_off / 60 + ":" + alarm_gone_off % 60);
|
||||
@ -187,7 +181,7 @@ class AppMessageHandlerMorpheuz extends AppMessageHandler {
|
||||
sendBytesCtrl.encodedBytes = encodeMorpheuzMessage(keyCtrl, ctrl_message);
|
||||
}
|
||||
|
||||
// ctrl and sleep monitor might be null, thats okay
|
||||
return new GBDeviceEvent[]{sendBytesAck, sendBytesCtrl, sleepMonitorResult};
|
||||
// ctrl might be null, thats okay
|
||||
return new GBDeviceEvent[]{sendBytesAck, sendBytesCtrl};
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ class AppMessageHandlerRealWeather extends AppMessageHandler {
|
||||
return CLOUD;
|
||||
} else if (conditionCode >= 907 && conditionCode < 957) {
|
||||
return STORM;
|
||||
} else if (conditionCode == 905 || (conditionCode >= 957 && conditionCode < 900)) {
|
||||
} else if (conditionCode == 905) {
|
||||
return STORM;
|
||||
} else if (conditionCode == 900) {
|
||||
return STORM;
|
||||
|
@ -88,7 +88,7 @@ private int getConditionForConditionCode(int conditionCode) {
|
||||
return HAIL;
|
||||
} else if (conditionCode >= 907 && conditionCode < 957) {
|
||||
return WIND;
|
||||
} else if (conditionCode == 905 || (conditionCode >= 957 && conditionCode < 900)) {
|
||||
} else if (conditionCode == 905) {
|
||||
return EXTREME_WIND;
|
||||
} else if (conditionCode == 900) {
|
||||
return TORNADO;
|
||||
|
@ -130,7 +130,7 @@ class AppMessageHandlerYWeather extends AppMessageHandler {
|
||||
return SLEET;
|
||||
} else if (conditionCode >= 907 && conditionCode < 957) {
|
||||
return STORM;
|
||||
} else if (conditionCode == 905 || (conditionCode >= 957 && conditionCode < 900)) {
|
||||
} else if (conditionCode == 905) {
|
||||
return STORM;
|
||||
} else if (conditionCode == 900) {
|
||||
return STORM;
|
||||
|
@ -65,6 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class PebbleProtocol extends GBDeviceProtocol {
|
||||
|
||||
@ -2269,6 +2270,9 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
dataLogging.tag = datalogSession.tag;
|
||||
devEvtsDataLogging = new GBDeviceEvent[]{dataLogging, null};
|
||||
}
|
||||
if (datalogSession.uuid.equals(UUID_ZERO) && (datalogSession.tag == 81 || datalogSession.tag == 83 || datalogSession.tag == 84)) {
|
||||
GB.signalActivityDataFinish();
|
||||
}
|
||||
mDatalogSessions.remove(id);
|
||||
}
|
||||
break;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -224,7 +224,7 @@ public class AndroidUtils {
|
||||
Uri contentUri = FileProvider.getUriForFile(context,
|
||||
context.getApplicationContext().getPackageName() + ".screenshot_provider", file);
|
||||
intent.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setData(contentUri);
|
||||
intent.setDataAndType(contentUri,"application/gpx+xml");
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
|
||||
@ -228,6 +229,7 @@ public class DeviceHelper {
|
||||
result.add(new Roidmi3Coordinator());
|
||||
result.add(new CasioGB6900DeviceCoordinator());
|
||||
result.add(new BFH16DeviceCoordinator());
|
||||
result.add(new MijiaLywsd02Coordinator());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -42,6 +42,8 @@ import java.nio.ByteOrder;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBEnvironment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
@ -492,4 +494,9 @@ public class GB {
|
||||
throw new AssertionError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public static void signalActivityDataFinish() {
|
||||
Intent intent = new Intent(GBApplication.ACTION_NEW_DATA);
|
||||
LocalBroadcastManager.getInstance(GBApplication.getContext()).sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M5,20.5A3.5,3.5 0 0,1 1.5,17A3.5,3.5 0 0,1 5,13.5A3.5,3.5 0 0,1 8.5,17A3.5,3.5 0 0,1 5,20.5M5,12A5,5 0 0,0 0,17A5,5 0 0,0 5,22A5,5 0 0,0 10,17A5,5 0 0,0 5,12M14.8,10H19V8.2H15.8L13.86,4.93C13.57,4.43 13,4.1 12.4,4.1C11.93,4.1 11.5,4.29 11.2,4.6L7.5,8.29C7.19,8.6 7,9 7,9.5C7,10.13 7.33,10.66 7.85,10.97L11.2,13V18H13V11.5L10.75,9.85L13.07,7.5M19,20.5A3.5,3.5 0 0,1 15.5,17A3.5,3.5 0 0,1 19,13.5A3.5,3.5 0 0,1 22.5,17A3.5,3.5 0 0,1 19,20.5M19,12A5,5 0 0,0 14,17A5,5 0 0,0 19,22A5,5 0 0,0 24,17A5,5 0 0,0 19,12M16,4.8C17,4.8 17.8,4 17.8,3C17.8,2 17,1.2 16,1.2C15,1.2 14.2,2 14.2,3C14.2,4 15,4.8 16,4.8Z" />
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M5,20.5A3.5,3.5 0 0,1 1.5,17A3.5,3.5 0 0,1 5,13.5A3.5,3.5 0 0,1 8.5,17A3.5,3.5 0 0,1 5,20.5M5,12A5,5 0 0,0 0,17A5,5 0 0,0 5,22A5,5 0 0,0 10,17A5,5 0 0,0 5,12M14.8,10H19V8.2H15.8L13.86,4.93C13.57,4.43 13,4.1 12.4,4.1C11.93,4.1 11.5,4.29 11.2,4.6L7.5,8.29C7.19,8.6 7,9 7,9.5C7,10.13 7.33,10.66 7.85,10.97L11.2,13V18H13V11.5L10.75,9.85L13.07,7.5M19,20.5A3.5,3.5 0 0,1 15.5,17A3.5,3.5 0 0,1 19,13.5A3.5,3.5 0 0,1 22.5,17A3.5,3.5 0 0,1 19,20.5M19,12A5,5 0 0,0 14,17A5,5 0 0,0 19,22A5,5 0 0,0 24,17A5,5 0 0,0 19,12M16,4.8C17,4.8 17.8,4 17.8,3C17.8,2 17,1.2 16,1.2C15,1.2 14.2,2 14.2,3C14.2,4 15,4.8 16,4.8Z" />
|
||||
</vector>
|
@ -1,7 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M23,12H17V10L20.39,6H17V4H23V6L19.62,10H23V12M15,16H9V14L12.39,10H9V8H15V10L11.62,14H15V16M7,20H1V18L4.39,14H1V12H7V14L3.62,18H7V20Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_activity_exercise.xml
Normal file
10
app/src/main/res/drawable/ic_activity_exercise.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,2c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2zM21,9h-6v13h-2v-6h-2v6L9,22L9,9L3,9L3,7h18v2z" />
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user