1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-09-26 16:26:52 +02:00
# Conflicts:
#	README.md
This commit is contained in:
Maciej Kuśnierz 2019-10-13 00:18:00 +02:00
commit 2acb65b745
181 changed files with 9701 additions and 2559 deletions

View File

@ -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.*

View File

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

View File

@ -1,5 +1,52 @@
### Changelog
#### Version 0.37.0
* Initial Makibes HR3 support
* Amazfit Bip Lite: Inittal working support, firmware update is disabled for now (we do not have any firmware for testing)
* Amazfit Cor 2: Enable Emoji Font setting and 3rd party HR access
* Find Phone now also vibration in addition to playing the ring tone
* ID115: All settings are now per-device
* Time format settings are now per-device for all supported devices
* Wrist location settings are now per-device for all supported devices
* Work around broken layout in database management activity
* Show toast in case no app is installed which can handle GPX files
* Mi Band 4/Amazfit Bip Lite: Trim white spaces and new lines from auth key
* Mi Band 4/Amazfit Bip Lite: Display a toast and do not try to pair if there was no auth key supplied
* Skip service scan if supported device could be recognized without uuids during discovery
#### Version 0.36.2
* Amazfit Bip: Untested support for Lite variant
* Force Lineage OS to ask for permission when Trust is used to fix non-working incoming calls
* Charts: List multiple sleep sessions per day
#### Version 0.36.1
* Mi Band 2/3/4, Amazfit Bip/Cor: Add setting to expose the HR sensor to 3rd party apps
* Mi Band 4: Really fix weather location not being updated on the Band
* Mi Band 4: Fix call notifcation not stopping when call gets answered or rejected on the phone
* Amazfit Bip/Cor: Support for custom emoji font
* ZeTime: Enable emoji support
* ZeTime: Make watch language the same as the phone language by default
* New status and alarms widget
* Fix crash when entering notification filter settings
* Make diagram settings accessible from charts activity
* Add option to hide the floating plus button in the main activity
* Fix a potential crash on Android 4.4 KitKat
#### Version 0.36.0
* Initial Mijia LYWSD02 support (Smart Clock with Humidity and Temperature Sensor), just for setting the time
* Mi Band 3/4: Allow enabling the NFC menu where supported (useless for now)
* Mi Band 3/4, Amazfit Cor/Bip: Set language immediately when changing it (not only on connect)
* Mi Band 3/4, Amazfir Cor/Bip: Add icons for "swimming" and "exercise"
* Mi Band 4: Support flashing the V2 font
* Mi Band 4: Fix weather location not being updated on the Band
* Mi Band 4: remove unsupported DND setting from settings menu
* Amazfit Bip/Cor: Fix resetting of last fetched date for sports activities
* Amazfit Bip: Fix sharing GPX files for some Apps
* Pebble: Use Rebble Store URI
* Support LineageOS 16.0 weather provider
* Add Averages to Charts
* Allow togging between weekly and monthly charts
#### Version 0.35.2
* Mi Band 1/2: Crash when updating firmware while phone is set to Spanish
* Mi Band 4: Enable music info support (displays now on the band)

View File

@ -45,7 +45,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
Schema schema = new Schema(20, MAIN_PACKAGE + ".entities");
Schema schema = new Schema(21, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@ -60,6 +60,7 @@ public class GBDaoGenerator {
Entity tag = addTag(schema);
Entity userDefinedActivityOverlay = addActivityDescription(schema, tag, user);
addMakibesHR3ActivitySample(schema, user, device);
addMiBandActivitySample(schema, user, device);
addPebbleHealthActivitySample(schema, user, device);
addPebbleHealthActivityKindOverlay(schema, user, device);
@ -186,6 +187,16 @@ public class GBDaoGenerator {
return deviceAttributes;
}
private static Entity addMakibesHR3ActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "MakibesHR3ActivitySample");
activitySample.implementsSerializable();
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
addHeartRateProperties(activitySample);
return activitySample;
}
private static Entity addMiBandActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "MiBandActivitySample");
activitySample.implementsSerializable();
@ -363,7 +374,7 @@ public class GBDaoGenerator {
alarm.addBooleanProperty("smartWakeup").notNull();
alarm.addIntProperty("repetition").notNull().codeBeforeGetter(
"public boolean isRepetitive() { return getRepetition() != ALARM_ONCE; } " +
"public boolean getRepetition(int dow) { return (this.repetition & dow) > 0; }"
"public boolean getRepetition(int dow) { return (this.repetition & dow) > 0; }"
);
alarm.addIntProperty("hour").notNull();
alarm.addIntProperty("minute").notNull();

View File

@ -28,70 +28,37 @@ vendor's servers.
[List of changes](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/CHANGELOG.md)
## Supported Devices
## Supported Devices (Some of them WIP and some of them without maintainer)
* Amazfit Bip [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip)
* Amazfit Bip Lite (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip-Lite)
* Amazfit Cor [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor)
* Amazfit Cor 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor-2)
* BFH-16
* Casio GB-6900B (WIP)
* Casio GB-6900B
* HPlus Devices (e.g. ZeBand) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus)
* ID115 (WIP)
* Lenovo Watch 9 (WIP)
* ID115
* Lenovo Watch 9
* Lenovo Watch X Plus (WIP)
* Liveview (WIP)
* Liveview
* Makibes HR3
* Mi Band, Mi Band 1A, Mi Band 1S [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
* Mi Band 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2)
* Mi Band 3 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-3)
* Mi Band 4 (WIP, NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4)
* Mi Band 4 (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4)
* Mi Scale 2 (currently only displays a toast after stepping on the scale)
* NO.1 F1 (WIP)
* NO.1 F1
* Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Pebble 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble)
* Teclast H10, H30 (WIP)
* Teclast H10, H30
* XWatch (Affordable Chinese Casio-like smartwatches)
* Vibratissimo (experimental)
* ZeTime (WIP) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
* ZeTime [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime)
## Features
Please see [FEATURES.md](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/FEATURES.md)
## Getting Started (Pebble)
Please [this wiki article](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble-Getting-Started)
## How to use (Mi Band 1+2)
* Invoke the discovery activity manually via the "+" button. It will ask you for some personal info that appears
to be needed for proper steps calculation on the band. If you do not provide these,
some hardcoded default "dummy" values will be used instead.
When your Mi Band starts to vibrate and blink during the pairing process,
tap it quickly a few times in a row to confirm the pairing with the band.
1. Configure other notifications as desired
2. Go back to the "Gadgetbridge" activity
3. Tap the Mi Band item to connect if you're not connected yet
4. To test, chose "Debug" from the menu and play around
**Known Issues:**
* The initial connection to a Mi Band sometimes takes a little patience. Try to connect a few times, wait,
and try connecting again. This only happens until you have "bonded" with the Mi Band, i.e. until it
knows your MAC address. This behavior may also only occur with older firmware versions.
* If you use other apps like Mi Fit, and "bonding" with Gadgetbridge does not work, please
try to unpair the band in the other app and try again with Gadgetbridge.
* While all Mi Band devices are supported, some firmware versions might work better than others.
You can consult the [projects wiki pages](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band)
to check if your firmware version is fully supported or if an upgrade/downgrade might be beneficial.
* In order to display text notifications on the Mi Band 2, you have to [install a font on the band](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2).
## Features (Liveview)
* set time (automatically upon connection)
* display notifications and vibrate
## Authors
### Core Team (in order of first code contribution)
@ -110,6 +77,7 @@ Please [this wiki article](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki
* Andreas Böhler (Casio GB-6900B)
* Jean-François Greffier (Mi Scale 2)
* Johannes Schmitt (BFH-16)
* Lukas Schwichtenberg (Makibes HR3)
## Contribute

View File

@ -1,5 +1,5 @@
apply plugin: "com.android.application"
apply plugin: "findbugs"
apply plugin: "com.github.spotbugs"
apply plugin: "pmd"
def ABORT_ON_CHECK_FAILURE = false
@ -25,8 +25,8 @@ android {
targetSdkVersion 27
// Note: always bump BOTH versionCode and versionName!
versionName "0.35.2"
versionCode 154
versionName "0.37.0"
versionCode 158
vectorDrawables.useSupportLibrary = true
}
buildTypes {
@ -67,8 +67,8 @@ dependencies {
testImplementation "com.google.code.gson:gson:2.8.5"
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "androidx.appcompat:appcompat:1.1.0-rc01"
implementation "androidx.preference:preference:1.1.0-rc01"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.preference:preference:1.1.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.0.0"
implementation "androidx.legacy:legacy-support-v4:1.0.0"
@ -79,7 +79,7 @@ dependencies {
exclude group: "com.google.android", module: "android"
}
implementation "org.slf4j:slf4j-api:1.7.12"
implementation "com.github.Freeyourgadget:MPAndroidChart:5e5bd6c1d3e95c515d4853647ae554e48ee1d593"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation "com.github.pfichtner:durationformatter:0.1.1"
implementation "de.cketti.library.changelog:ckchangelog:1.2.2"
implementation "net.e175.klaus:solarpositioning:0.0.9"
@ -99,7 +99,7 @@ gradle.beforeProject {
preBuild.dependsOn(":GBDaoGenerator:genSources")
}
check.dependsOn "findbugs", "pmd", "lint"
check.dependsOn "spotbugsMain", "pmd", "lint"
task pmd(type: Pmd) {
ruleSetFiles = files("${project.rootDir}/config/pmd/pmd-ruleset.xml")
@ -142,22 +142,32 @@ task pmd(type: Pmd) {
}
}
task findbugs(type: FindBugs) {
// this is just for spotbugs to let the plugin create the task
sourceSets {
main {
java.srcDirs = []
}
}
spotbugs {
toolVersion = "3.1.12"
ignoreFailures = !ABORT_ON_CHECK_FAILURE
effort = "default"
reportLevel = "medium"
}
tasks.withType(com.github.spotbugs.SpotBugsTask) {
source = fileTree('src/main/java')
classes = files("${project.rootDir}/app/build/intermediates/javac/debug/classes")
excludeFilter = new File("${project.rootDir}/config/findbugs/findbugs-filter.xml")
classes = files("${project.rootDir}/app/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes")
source = fileTree("src/main/java/")
classpath = files()
reports {
xml.enabled = false
html.enabled = true
xml {
destination file ("$project.buildDir/reports/findbugs/findbugs-output.xml")
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.xml")
}
html {
destination file ("$project.buildDir/reports/findbugs/findbugs-output.html")
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.html")
}
}
}

View File

@ -21,9 +21,12 @@
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" /> <!-- Used for reverse find device -->
<uses-permission android:name="cyanogenmod.permission.ACCESS_WEATHER_MANAGER" />
<uses-permission android:name="cyanogenmod.permission.READ_WEATHER" />
<uses-permission android:name="lineageos.permission.ACCESS_WEATHER_MANAGER" />
<uses-permission android:name="lineageos.permission.READ_WEATHER" />
<uses-permission android:name="org.omnirom.omnijaws.READ_WEATHER" />
<uses-feature
@ -58,6 +61,10 @@
android:name=".activities.SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.charts.ChartsPreferencesActivity"
android:label="@string/activity_prefs_charts"
android:parentActivityName=".activities.charts.ChartsPreferencesActivity" />
<activity
android:name=".devices.miband.MiBandPreferencesActivity"
android:label="@string/preferences_miband_settings"
@ -443,7 +450,8 @@
android:resource="@xml/shared_paths" />
</provider>
<receiver android:name=".SleepAlarmWidget">
<receiver android:name=".SleepAlarmWidget"
android:label="@string/appwidget_sleep_alarm_widget_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="nodomain.freeyourgadget.gadgetbridge.SLEEP_ALARM_WIDGET_CLICK" />
@ -454,6 +462,26 @@
android:resource="@xml/sleep_alarm_widget_info" />
</receiver>
<receiver
android:name=".Widget"
android:label="@string/widget_listing_label">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="nodomain.freeyourgadget.gadgetbridge.WidgetClick" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
<activity
android:name=".activities.WidgetAlarmsActivity"
android:launchMode="singleInstance"
android:theme="@style/Theme.AppCompat.Light.Dialog"
android:excludeFromRecents="true"/>
<activity
android:launchMode="singleTask"
android:allowTaskReparenting="true"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -78,9 +78,13 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITBIP;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITCOR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.AMAZFITCOR2;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.HPLUS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.ID115;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND2;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND3;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.MIBAND4;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.ZETIME;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceType.fromKey;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_CHANNEL_ID;
@ -99,7 +103,7 @@ public class GBApplication extends Application {
private static SharedPreferences sharedPrefs;
private static final String PREFS_VERSION = "shared_preferences_version";
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
private static final int CURRENT_PREFS_VERSION = 4;
private static final int CURRENT_PREFS_VERSION = 5;
private static LimitedQueue mIDSenderLookup = new LimitedQueue(16);
private static Prefs prefs;
private static GBPrefs gbPrefs;
@ -112,6 +116,7 @@ public class GBApplication extends Application {
public static final String ACTION_QUIT
= "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.quit";
public static final String ACTION_LANGUAGE_CHANGE = "nodomain.freeyourgadget.gadgetbridge.gbapplication.action.language_change";
public static final String ACTION_NEW_DATA = "nodomain.freeyourgadget.gadgetbridge.action.new_data";
private static GBApplication app;
@ -631,8 +636,9 @@ public class GBApplication extends Application {
DaoSession daoSession = db.getDaoSession();
List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (Device dbDevice : activeDevices) {
SharedPreferences.Editor deviceSharedPrefsEdit = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier()).edit();
if (sharedPrefs != null) {
SharedPreferences deviceSpecificSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
if (deviceSpecificSharedPrefs != null) {
SharedPreferences.Editor deviceSharedPrefsEdit = deviceSpecificSharedPrefs.edit();
String preferenceKey = dbDevice.getIdentifier() + "_lastSportsActivityTimeMillis";
long lastSportsActivityTimeMillis = sharedPrefs.getLong(preferenceKey, 0);
if (lastSportsActivityTimeMillis != 0) {
@ -707,9 +713,8 @@ public class GBApplication extends Application {
if (newLanguage != null) {
deviceSharedPrefsEdit.putString("language", newLanguage);
}
deviceSharedPrefsEdit.apply();
}
deviceSharedPrefsEdit.apply();
}
editor.remove("amazfitbip_language");
editor.remove("bip_display_items");
@ -760,6 +765,67 @@ public class GBApplication extends Application {
Log.w(TAG, "error acquiring DB lock");
}
}
if (oldVersion < 5) {
try (DBHandler db = acquireDB()) {
DaoSession daoSession = db.getDaoSession();
List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (Device dbDevice : activeDevices) {
SharedPreferences deviceSpecificSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
if (deviceSpecificSharedPrefs != null) {
SharedPreferences.Editor deviceSharedPrefsEdit = deviceSpecificSharedPrefs.edit();
DeviceType deviceType = fromKey(dbDevice.getType());
String newWearside = null;
String newOrientation = null;
String newTimeformat = null;
switch (deviceType) {
case AMAZFITBIP:
case AMAZFITCOR:
case AMAZFITCOR2:
case MIBAND:
case MIBAND2:
case MIBAND3:
case MIBAND4:
newWearside = prefs.getString("mi_wearside", "left");
break;
case HPLUS:
newWearside = prefs.getString("hplus_wrist", "left");
newTimeformat = prefs.getString("hplus_timeformat", "24h");
break;
case ID115:
newWearside = prefs.getString("id115_wrist", "left");
newOrientation = prefs.getString("id115_screen_orientation", "horizontal");
break;
case ZETIME:
newWearside = prefs.getString("zetime_wrist", "left");
newTimeformat = prefs.getInt("zetime_timeformat", 1) == 2 ? "am/pm" : "24h";
break;
}
if (newWearside != null) {
deviceSharedPrefsEdit.putString("wearlocation", newWearside);
}
if (newOrientation != null) {
deviceSharedPrefsEdit.putString("screen_orientation", newOrientation);
}
if (newTimeformat != null) {
deviceSharedPrefsEdit.putString("timeformat", newTimeformat);
}
deviceSharedPrefsEdit.apply();
}
}
editor.remove("hplus_timeformat");
editor.remove("hplus_wrist");
editor.remove("id115_wrist");
editor.remove("id115_screen_orientation");
editor.remove("mi_wearside");
editor.remove("zetime_timeformat");
editor.remove("zetime_wrist");
} catch (Exception e) {
Log.w(TAG, "error acquiring DB lock");
}
}
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
editor.apply();
}

View File

@ -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);
}
}
}

View File

@ -80,6 +80,7 @@ public class ConfigureAlarms extends AbstractGBActivity {
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQ_CONFIGURE_ALARM) {
avoidSendAlarmsToDevice = false;
updateAlarmsFromDB();

View File

@ -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) {

View File

@ -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");
}

View File

@ -35,6 +35,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
@ -195,8 +196,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
LOG.warn(device.getName() + ": " + ((scanRecord != null) ? scanRecord.length : -1));
logMessageContent(scanRecord);
//logMessageContent(scanRecord);
handleDeviceFound(device, (short) rssi);
}
};
@ -338,6 +338,12 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
}
private void handleDeviceFound(BluetoothDevice device, short rssi) {
if (device.getName() != null) {
if (handleDeviceFound(device,rssi, null)) {
LOG.info("found supported device " + device.getName() + " without scanning services, skipping service scan.");
return;
}
}
ParcelUuid[] uuids = device.getUuids();
if (uuids == null) {
if (device.fetchUuidsWithSdp()) {
@ -349,7 +355,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
}
private void handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) {
private boolean handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) {
LOG.debug("found device: " + device.getName() + ", " + device.getAddress());
if (LOG.isDebugEnabled()) {
if (uuids != null && uuids.length > 0) {
@ -359,7 +365,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
}
}
if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
return; // ignore already bonded devices
return true; // ignore already bonded devices
}
GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids);
@ -374,7 +380,9 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
deviceCandidates.add(candidate);
}
cadidateListAdapter.notifyDataSetChanged();
return true;
}
return false;
}
/**
@ -616,6 +624,17 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
stopDiscovery();
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate);
LOG.info("Using device candidate " + deviceCandidate + " with coordinator: " + coordinator.getClass());
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) {
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress());
String authKey = sharedPrefs.getString("authkey", null);
if (authKey == null || authKey.isEmpty() || authKey.getBytes().length < 34 || !authKey.substring(0, 2).equals("0x")) {
GB.toast(DiscoveryActivity.this, getString(R.string.discovery_need_to_enter_authkey), Toast.LENGTH_LONG, GB.WARN);
return;
}
}
Class<? extends Activity> pairingActivity = coordinator.getPairingActivity();
if (pairingActivity != null) {
Intent intent = new Intent(this, pairingActivity);
@ -623,7 +642,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView
startActivity(intent);
} else {
GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate);
int bondingStyle = coordinator.getBondingStyle(device);
int bondingStyle = coordinator.getBondingStyle();
if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) {
LOG.info("No bonding needed, according to coordinator, so connecting right away");
connectAndFinish(device);

View File

@ -25,7 +25,10 @@ import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.view.View;
import android.widget.Button;
@ -58,6 +61,7 @@ public class FindPhoneActivity extends AbstractGBActivity {
}
};
Vibrator mVibrator;
AudioManager mAudioManager;
int userVolume;
MediaPlayer mp;
@ -79,10 +83,26 @@ public class FindPhoneActivity extends AbstractGBActivity {
finish();
}
});
vibrate();
playRingtone();
}
public void playRingtone(){
private void vibrate(){
mVibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
long[] vibrationPattern = new long[]{ 1000, 1000 };
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect vibrationEffect = VibrationEffect.createWaveform(vibrationPattern, 0);
mVibrator.vibrate(vibrationEffect);
} else {
mVibrator.vibrate(vibrationPattern, 0);
}
}
private void playRingtone(){
mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
if (mAudioManager != null) {
userVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM);
@ -107,7 +127,11 @@ public class FindPhoneActivity extends AbstractGBActivity {
}
}
public void stopSound() {
private void stopVibration() {
mVibrator.cancel();
}
private void stopSound() {
mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, userVolume, AudioManager.FLAG_PLAY_SOUND);
mp.stop();
mp.reset();
@ -116,7 +140,10 @@ public class FindPhoneActivity extends AbstractGBActivity {
@Override
protected void onDestroy() {
super.onDestroy();
stopVibration();
stopSound();
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
unregisterReceiver(mReceiver);
}

View File

@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
@ -96,6 +97,16 @@ public class SettingsActivity extends AbstractSettingsActivity {
return true;
}
});
pref = findPreference("pref_charts");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent enableIntent = new Intent(SettingsActivity.this, ChartsPreferencesActivity.class);
startActivity(enableIntent);
return true;
}
});
pref = findPreference("pref_key_miband");
pref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {

View File

@ -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);
}
}

View File

@ -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);

View File

@ -37,8 +37,7 @@ import com.github.mikephil.charting.data.ChartData;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.formatter.ValueFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -88,6 +87,10 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
setupLegend(mWeekChart);
mTodayPieChart.setCenterText(mcd.getDayData().centerText);
mTodayPieChart.setData(mcd.getDayData().data);
//set custom renderer for 30days bar charts
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
mWeekChart.setRenderer(new AngledLabelsChartRenderer(mWeekChart, mWeekChart.getAnimator(), mWeekChart.getViewPortHandler()));
}
mWeekChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mWeekChart.setData(mcd.getWeekBeforeData().getData());
@ -368,11 +371,11 @@ public abstract class AbstractWeekChartFragment extends AbstractChartFragment {
abstract String[] getPieLabels();
abstract IValueFormatter getPieValueFormatter();
abstract ValueFormatter getPieValueFormatter();
abstract IValueFormatter getBarValueFormatter();
abstract ValueFormatter getBarValueFormatter();
abstract IAxisValueFormatter getYAxisFormatter();
abstract ValueFormatter getYAxisFormatter();
abstract int[] getColors();

View File

@ -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);

View File

@ -143,7 +143,7 @@ public class ActivitySleepChartFragment extends AbstractChartFragment {
@Override
protected void renderCharts() {
mChart.animateX(ANIM_TIME, Easing.EasingOption.EaseInOutQuart);
mChart.animateX(ANIM_TIME, Easing.EaseInOutQuart);
// mChart.invalidate();
}

View File

@ -0,0 +1,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();
}}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -346,7 +346,7 @@ public class LiveActivityFragment extends AbstractChartFragment {
renderCharts();
// have to enable it again and again to keep it measureing
// have to enable it again and again to keep it measuring
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

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

View File

@ -17,13 +17,9 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.AxisBase;
import com.github.mikephil.charting.components.Legend;
import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.utils.ViewPortHandler;
import com.github.mikephil.charting.formatter.ValueFormatter;
import java.util.ArrayList;
import java.util.List;
@ -115,30 +111,30 @@ public class WeekSleepChartFragment extends AbstractWeekChartFragment {
}
@Override
IValueFormatter getPieValueFormatter() {
return new IValueFormatter() {
ValueFormatter getPieValueFormatter() {
return new ValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
public String getFormattedValue(float value) {
return formatPieValue((long) value);
}
};
}
@Override
IValueFormatter getBarValueFormatter() {
return new IValueFormatter() {
ValueFormatter getBarValueFormatter() {
return new ValueFormatter() {
@Override
public String getFormattedValue(float value, Entry entry, int dataSetIndex, ViewPortHandler viewPortHandler) {
public String getFormattedValue(float value) {
return DateTimeUtils.minutesToHHMM((int) value);
}
};
}
@Override
IAxisValueFormatter getYAxisFormatter() {
return new IAxisValueFormatter() {
ValueFormatter getYAxisFormatter() {
return new ValueFormatter() {
@Override
public String getFormattedValue(float value, AxisBase axis) {
public String getFormattedValue(float value) {
return DateTimeUtils.minutesToHHMM((int) value);
}
};

View File

@ -18,8 +18,7 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.formatter.IAxisValueFormatter;
import com.github.mikephil.charting.formatter.IValueFormatter;
import com.github.mikephil.charting.formatter.ValueFormatter;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
@ -82,17 +81,17 @@ public class WeekStepsChartFragment extends AbstractWeekChartFragment {
}
@Override
IValueFormatter getPieValueFormatter() {
ValueFormatter getPieValueFormatter() {
return null;
}
@Override
IValueFormatter getBarValueFormatter() {
ValueFormatter getBarValueFormatter() {
return null;
}
@Override
IAxisValueFormatter getYAxisFormatter() {
ValueFormatter getYAxisFormatter() {
return null;
}

View File

@ -0,0 +1,8 @@
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
public class DeviceSettingsPreferenceConst {
public static final String PREF_DATEFORMAT = "dateformat";
public static final String PREF_TIMEFORMAT = "timeformat";
public static final String PREF_WEARLOCATION = "wearlocation";
public static final String PREF_SCREEN_ORIENTATION = "screen_orientation";
}

View File

@ -16,19 +16,21 @@ import org.slf4j.LoggerFactory;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Constants;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference;
import nodomain.freeyourgadget.gadgetbridge.util.XTimePreferenceFragment;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_ACTIVATE_DISPLAY_ON_LIFT;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DATEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISCONNECT_NOTIFICATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISCONNECT_NOTIFICATION_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISCONNECT_NOTIFICATION_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ITEMS;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ON_LIFT_END;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ON_LIFT_START;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_EXPOSE_HR_THIRDPARTY;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_LANGUAGE;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_DO_NOT_DISTURB_END;
@ -287,6 +289,10 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
addPreferenceHandlerFor(PREF_DATEFORMAT);
addPreferenceHandlerFor(PREF_DISPLAY_ITEMS);
addPreferenceHandlerFor(PREF_LANGUAGE);
addPreferenceHandlerFor(PREF_EXPOSE_HR_THIRDPARTY);
addPreferenceHandlerFor(PREF_WEARLOCATION);
addPreferenceHandlerFor(PREF_SCREEN_ORIENTATION);
addPreferenceHandlerFor(PREF_TIMEFORMAT);
String displayOnLiftState = prefs.getString(PREF_ACTIVATE_DISPLAY_ON_LIFT, PREF_DO_NOT_DISTURB_OFF);
boolean displayOnLiftScheduled = displayOnLiftState.equals(PREF_DO_NOT_DISTURB_SCHEDULED);
@ -364,15 +370,25 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
});
}
EditTextPreference pref = findPreference(MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS);
if (pref != null) {
pref.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
EditTextPreference mibandTimeOffset = findPreference(MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS);
if (mibandTimeOffset != null) {
mibandTimeOffset.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
@Override
public void onBindEditText(@NonNull EditText editText) {
editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
}
});
}
EditTextPreference findPhoneDuration = findPreference(MakibesHR3Constants.PREF_FIND_PHONE_DURATION);
if (findPhoneDuration != null) {
findPhoneDuration.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() {
@Override
public void onBindEditText(@NonNull EditText editText) {
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
}
});
}
}
static DeviceSpecificSettingsFragment newInstance(String settingsFileSuffix, @NonNull int[] supportedSettings) {

View File

@ -33,11 +33,14 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getPrefs;
public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDeviceCoordinator.class);
@ -69,6 +72,17 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
if (gbDevice.isConnected() || gbDevice.isConnecting()) {
GBApplication.deviceService().disconnect();
}
Prefs prefs = getPrefs();
String lastDevice = prefs.getPreferences().getString("last_device_address","");
if (gbDevice.getAddress() == lastDevice){
LOG.debug("#1605 removing last device");
prefs.getPreferences().edit().remove("last_device_address").apply();
}
String macAddress = prefs.getPreferences().getString(MiBandConst.PREF_MIBAND_ADDRESS,"");
if (gbDevice.getAddress() == macAddress){
LOG.debug("#1605 removing devel miband");
prefs.getPreferences().edit().remove(MiBandConst.PREF_MIBAND_ADDRESS).apply();
}
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
Device device = DBHelper.findDevice(gbDevice, session);
@ -122,7 +136,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice device) {
public int getBondingStyle() {
return BONDING_STYLE_ASK;
}
@ -145,6 +159,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return false;
}
@NonNull
@Override
public int[] getColorPresets() {
return new int[0];

View File

@ -29,7 +29,6 @@ import java.util.Collection;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsFragment;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
@ -63,6 +62,11 @@ public interface DeviceCoordinator {
*/
int BONDING_STYLE_ASK = 2;
/**
* A secret key has to be entered before connecting
*/
int BONDING_STYLE_REQUIRE_KEY = 3;
/**
* Checks whether this coordinator handles the given candidate.
* Returns the supported device type for the given candidate or
@ -224,9 +228,8 @@ public interface DeviceCoordinator {
/**
* Returns how/if the given device should be bonded before connecting to it.
* @param device
*/
int getBondingStyle(GBDevice device);
int getBondingStyle();
/**
* Indicates whether the device has some kind of calender we can sync to.

View File

@ -93,6 +93,7 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
sampleProvider = new UnknownSampleProvider();
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
return DeviceType.UNKNOWN;
@ -197,6 +198,7 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
return false;
}
@NonNull
@Override
public int[] getColorPresets() {
return new int[0];

View File

@ -58,7 +58,7 @@ public class CasioGB6900DeviceCoordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice deviceCandidate){
public int getBondingStyle(){
return BONDING_STYLE_BOND;
}

View File

@ -128,8 +128,6 @@ public final class HPlusConstants {
public static final String PREF_HPLUS_SCREENTIME = "hplus_screentime";
public static final String PREF_HPLUS_ALLDAYHR = "hplus_alldayhr";
public static final String PREF_HPLUS_TIMEFORMAT = "hplus_timeformat";
public static final String PREF_HPLUS_WRIST = "hplus_wrist";
public static final String PREF_HPLUS_SIT_START_TIME = "hplus_sit_start_time";
public static final String PREF_HPLUS_SIT_END_TIME = "hplus_sit_end_time";
public static final String PREF_HPLUS_UNICODE = "hplus_unicode";

View File

@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
@ -83,7 +84,7 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice deviceCandidate){
public int getBondingStyle(){
return BONDING_STYLE_NONE;
}
@ -196,10 +197,10 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
}
}
public static byte getTimeMode(String address) {
String tmode = prefs.getString(HPlusConstants.PREF_HPLUS_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h));
public static byte getTimeMode(String deviceAddress) {
String tmode = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress).getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, "24h");
if(tmode.equals(getContext().getString(R.string.p_timeformat_24h))) {
if ("24h".equals(tmode)) {
return HPlusConstants.ARG_TIMEMODE_24H;
}else{
return HPlusConstants.ARG_TIMEMODE_12H;
@ -269,12 +270,14 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
return (byte) 255;
}
public static byte getUserWrist(String address) {
String value = prefs.getString(HPlusConstants.PREF_HPLUS_WRIST, getContext().getString(R.string.left));
//FIXME: unused
public static byte getUserWrist(String deviceAddress) {
SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress);
String value = sharedPreferences.getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left");
if(value.equals(getContext().getString(R.string.left))){
if ("left".equals(value)) {
return HPlusConstants.ARG_WRIST_LEFT;
}else{
} else {
return HPlusConstants.ARG_WRIST_RIGHT;
}
}
@ -290,10 +293,19 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator {
public static void setUnicodeSupport(String address, boolean state){
SharedPreferences.Editor editor = prefs.getPreferences().edit();
editor.putBoolean(HPlusConstants.PREF_HPLUS_UNICODE + "_" + address, state);
editor.commit();
editor.apply();
}
public static boolean getUnicodeSupport(String address){
return (prefs.getBoolean(HPlusConstants.PREF_HPLUS_UNICODE + "_" + address, false));
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
//R.xml.devicesettings_wearlocation, // disabled, since it is never used in code
R.xml.devicesettings_timeformat
};
}
}

View File

@ -60,8 +60,8 @@ public class HuamiConst {
public static final String PREF_DISPLAY_ITEMS = "display_items";
public static final String PREF_LANGUAGE = "language";
public static final String PREF_DATEFORMAT = "dateformat";
public static final String PREF_EXPOSE_HR_THIRDPARTY = "expose_hr_thirdparty";
public static final String PREF_USE_CUSTOM_FONT = "use_custom_font";
public static int toActivityKind(int rawType) {
switch (rawType) {

View File

@ -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;

View File

@ -139,6 +139,8 @@ public class HuamiService {
public static final byte[] DATEFORMAT_TIME_12_HOURS = new byte[] {ENDPOINT_DISPLAY, 0x02, 0x0, 0x0 };
public static final byte[] DATEFORMAT_TIME_24_HOURS = new byte[] {ENDPOINT_DISPLAY, 0x02, 0x0, 0x1 };
public static final byte[] DATEFORMAT_DATE_MM_DD_YYYY = new byte[]{ENDPOINT_DISPLAY, 30, 0x00, 'M', 'M', '/', 'd', 'd', '/', 'y', 'y', 'y', 'y'};
public static final byte[] COMMAND_ENBALE_HR_CONNECTION = new byte[]{ENDPOINT_DISPLAY, 0x01, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_HR_CONNECTION = new byte[]{ENDPOINT_DISPLAY, 0x01, 0x00, 0x00};
public static final byte[] COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x01};
public static final byte[] COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x00};
public static final byte[] COMMAND_SCHEDULE_DISPLAY_ON_LIFT_WRIST = new byte[]{ENDPOINT_DISPLAY, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00};

View File

@ -47,7 +47,7 @@ public class AmazfitBipCoordinator extends HuamiCoordinator {
try {
BluetoothDevice device = candidate.getDevice();
String name = device.getName();
if (name != null && name.equalsIgnoreCase("Amazfit Bip Watch")) {
if (name != null && (name.equalsIgnoreCase("Amazfit Bip Watch"))) {
return DeviceType.AMAZFITBIP;
}
} catch (Exception ex) {
@ -81,8 +81,11 @@ public class AmazfitBipCoordinator extends HuamiCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitbip,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_custom_emoji_font,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}

View File

@ -0,0 +1,65 @@
/* Copyright (C) 2017-2019 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, João Paulo Barraca
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class AmazfitBipLiteCoordinator extends AmazfitBipCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(AmazfitBipLiteCoordinator.class);
@Override
public DeviceType getDeviceType() {
return DeviceType.AMAZFITBIP_LITE;
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
try {
BluetoothDevice device = candidate.getDevice();
String name = device.getName();
if (name != null && name.equalsIgnoreCase("Amazfit Bip Lite")) {
return DeviceType.AMAZFITBIP_LITE;
}
} catch (Exception ex) {
LOG.error("unable to check device support", ex);
}
return DeviceType.UNKNOWN;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_REQUIRE_KEY;
}
}

View File

@ -84,8 +84,12 @@ public class AmazfitCorCoordinator extends HuamiCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitcor,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_custom_emoji_font,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_pairingkey};
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}
}

View File

@ -86,8 +86,12 @@ public class AmazfitCor2Coordinator extends HuamiCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_amazfitcor,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_custom_emoji_font,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_pairingkey};
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}
}

View File

@ -46,11 +46,6 @@ public class MiBand2Coordinator extends HuamiCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
if (candidate.supportsService(HuamiService.UUID_SERVICE_MIBAND2_SERVICE)) {
return DeviceType.MIBAND2;
}
// and a heuristic for now
try {
BluetoothDevice device = candidate.getDevice();
String name = device.getName();
@ -84,9 +79,11 @@ public class MiBand2Coordinator extends HuamiCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_miband2,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_donotdisturb_withauto,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_rotatewrist_cycleinfo,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}

View File

@ -25,6 +25,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
@ -75,4 +77,15 @@ public class MiBand2HRXCoordinator extends HuamiCoordinator {
return false;
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_miband2,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_donotdisturb_withauto,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_rotatewrist_cycleinfo,
R.xml.devicesettings_pairingkey
};
}
}

View File

@ -103,11 +103,13 @@ public class MiBand3Coordinator extends HuamiCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_miband3,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_dateformat,
R.xml.devicesettings_nightmode,
R.xml.devicesettings_donotdisturb_withauto,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_swipeunlock,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}

View File

@ -88,12 +88,18 @@ public class MiBand4Coordinator extends HuamiCoordinator {
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_miband3,
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_dateformat,
R.xml.devicesettings_nightmode,
R.xml.devicesettings_donotdisturb_withauto,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_swipeunlock,
R.xml.devicesettings_expose_hr_thirdparty,
R.xml.devicesettings_pairingkey
};
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_REQUIRE_KEY;
}
}

View File

@ -23,9 +23,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import static nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport.BASE_UUID;
public class ID115Constants {
public static final String PREF_WRIST = "id115_wrist";
public static final String PREF_SCREEN_ORIENTATION = "id115_screen_orientation";
public static final UUID UUID_SERVICE_ID115 = UUID.fromString(String.format(BASE_UUID, "0AF0"));
public static final UUID UUID_CHARACTERISTIC_WRITE_NORMAL = UUID.fromString(String.format(BASE_UUID, "0AF6"));
public static final UUID UUID_CHARACTERISTIC_NOTIFY_NORMAL = UUID.fromString(String.format(BASE_UUID, "0AF7"));

View File

@ -31,6 +31,7 @@ import java.util.Collections;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
@ -65,7 +66,7 @@ public class ID115Coordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice deviceCandidate){
public int getBondingStyle(){
return BONDING_STYLE_NONE;
}
@ -154,4 +155,12 @@ public class ID115Coordinator extends AbstractDeviceCoordinator {
public boolean supportsFindDevice() {
return false;
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_screenorientation
};
}
}

View File

@ -70,6 +70,7 @@ public class BFH16DeviceCoordinator extends AbstractDeviceCoordinator
return Collections.singletonList(filter);
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
@ -85,7 +86,7 @@ public class BFH16DeviceCoordinator extends AbstractDeviceCoordinator
}
@Override
public int getBondingStyle(GBDevice deviceCandidate){
public int getBondingStyle(){
return BONDING_STYLE_NONE;
}

View File

@ -82,7 +82,7 @@ public class TeclastH30Coordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice deviceCandidate){
public int getBondingStyle(){
return BONDING_STYLE_NONE;
}

View File

@ -0,0 +1,349 @@
/* Copyright (C) 2016-2019 Andreas Shimokawa, Carsten Pfeiffer, João
Paulo Barraca
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3;
import java.util.UUID;
public final class MakibesHR3Constants {
// TODO: This doesn't belong here, but I don't want to touch other files to avoid
// TODO: breaking someone else's device support.
public static final String PREF_HEADS_UP_SCREEN = "activate_display_on_lift_wrist";
public static final String PREF_LOST_REMINDER = "disconnect_notification";
public static final String PREF_DO_NOT_DISTURB = "do_not_disturb_no_auto";
public static final String PREF_DO_NOT_DISTURB_START = "do_not_disturb_no_auto_start";
public static final String PREF_DO_NOT_DISTURB_END = "do_not_disturb_no_auto_end";
public static final String PREF_FIND_PHONE = "prefs_find_phone";
public static final String PREF_FIND_PHONE_DURATION = "prefs_find_phone_duration";
public static final UUID UUID_SERVICE = UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
public static final UUID UUID_CHARACTERISTIC_CONTROL = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
public static final UUID UUID_CHARACTERISTIC_REPORT = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
// Services and Characteristics
// 00001801-0000-1000-8000-00805f9b34fb
// 00002a05-0000-1000-8000-00805f9b34fb
// 00001800-0000-1000-8000-00805f9b34fb
// 00002a00-0000-1000-8000-00805f9b34fb
// 00002a01-0000-1000-8000-00805f9b34fb
// 00002a02-0000-1000-8000-00805f9b34fb
// 00002a04-0000-1000-8000-00805f9b34fb
// 00002aa6-0000-1000-8000-00805f9b34fb
// 6e400001-b5a3-f393-e0a9-e50e24dcca9e // Nordic UART Service
// 6e400002-b5a3-f393-e0a9-e50e24dcca9e // control (RX)
// 6e400003-b5a3-f393-e0a9-e50e24dcca9e // report
// 0000fee7-0000-1000-8000-00805f9b34fb
// 0000fec9-0000-1000-8000-00805f9b34fb
// 0000fea1-0000-1000-8000-00805f9b34fb
// 0000fea2-0000-1000-8000-00805f9b34fb
// Command structure
// ab 00 [argument_count] ff [command] 80 [arguments]
// where [argument_count] is [arguments].length + 3
// 80 might by different.
public static final byte[] DATA_TEMPLATE = {
(byte) 0xab,
(byte) 0x00,
(byte) 0, // argument_count
(byte) 0xff,
(byte) 0, // command
(byte) 0x80
// ,arguments
};
public static final int DATA_ARGUMENT_COUNT_INDEX = 2;
public static final int DATA_COMMAND_INDEX = 4;
public static final int DATA_ARGUMENTS_INDEX = 6;
// blood oxygen percentage
public static final byte[] RPRT_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x12 };
// blood oxygen percentage
// blood oxygen percentage
public static final byte[] RPRT_SINGLE_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x11 };
// steps might take up more bytes. I don't know which ones and I won't walk that much.
// Only sent after we send CMD_51
// 00 (maybe also used for steps)
// [steps hi]
// [steps lo]
// 00
// 00
// 01 (also was 0b. Maybe minutes of activity.)
// 00
// 00
// 00
// 00
// 00
public static final byte[] RPRT_FITNESS = new byte[]{ (byte) 0x51, 0x08 };
// year (+2000)
// month
// day
// hour
// minute
// heart rate
// heart rate
public static final byte[] RPRT_HEART_RATE_SAMPLE = new byte[]{ (byte) 0x51, (byte) 0x11 };
// WearFit says "walking" in the step details. This is probably also in here, but
// I don't run :O
// year (+2000)
// month
// day
// hour (start of measurement. interval is 1h. Might be longer when running.)
// 00 (either used for steps or minute)
// accumulated steps (hi)
// accumulated steps (lo)
// 00
// 00
// ?? (changes whenever steps change. Ranges from 00 to 16.)
// 00
// 00
// 00
// 00
public static final byte[] RPRT_STEPS_SAMPLE = new byte[]{ (byte) 0x51, (byte) 0x20 };
// enable (00/01)
public static final byte RPRT_REVERSE_FIND_DEVICE = (byte) 0x7d;
// The proximity sensor sees air..
public static final byte ARG_HEARTRATE_NO_TARGET = (byte) 0xff;
// The hr sensor didn't find the heart rate yet.
public static final byte ARG_HEARTRATE_NO_READING = (byte) 0x00;
// heart rate
public static final byte RPRT_HEARTRATE = (byte) 0x84;
// charging (00/01)
// battery percentage (step size is 20).
public static final byte RPRT_BATTERY = (byte) 0x91;
// firmware_major
// firmware_minor
// 37
// 00
// 00
// 00
// 00
// 00
// 00
// 20
// 0e
public static final byte RPRT_SOFTWARE = (byte) 0x92;
// 00
public static final byte CMD_FACTORY_RESET = (byte) 0x23;
// enable (00/01)
public static final byte[] CMD_SET_REAL_TIME_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x12 };
// After disabling, the watch replies with RPRT_SINGLE_BLOOD_OXYGEN
// enable (00/01)
public static final byte[] CMD_SET_SINGLE_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x11 };
// device replies with
// {@link MakibesHR3Constants#RPRT_HEART_RATE_SAMPLE}
// {@link MakibesHR3Constants#RPRT_STEPS_SAMPLE} (Only if steps are non-zero)
// {@link MakibesHR3Constants#RPRT_FITNESS}
// there are also multiple 6 * 00 reports
// 00
// year (+2000) steps after
// month steps after
// day steps after
// hour steps after
// minute steps after
// year (+2000) heart rate after
// month heart rate after
// day heart rate after
// hour heart rate after
// minute heart rate after
public static final byte CMD_REQUEST_FITNESS = (byte) 0x51;
// Manually sending this doesn't yield a reply. The heart rate history is sent in response to
// CMD_CMD_REQUEST_FITNESS.
// 00
// year (+2000) (probably not current)
// month (not current!)
// day (not current!)
// hour (current)
// minute (current)
public static final byte CMD_52 = (byte) 0x52;
// vibrates 6 times
public static final byte CMD_FIND_DEVICE = (byte) 0x71;
// WearFit writes uses other sources as well. They don't do anything though.
public static final byte ARG_SEND_NOTIFICATION_SOURCE_CALL = (byte) 0x01;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_STOP_CALL = (byte) 0x02;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_MESSAGE = (byte) 0x03;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_QQ = (byte) 0x07;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_WECHAT = (byte) 0x09;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_WHATSAPP = (byte) 0x0a;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_LINE = (byte) 0x0e;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_TWITTER = (byte) 0x0f;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_FACEBOOK = (byte) 0x10;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_FACEBOOK2 = (byte) 0x11;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_WEIBO = (byte) 0x13;
public static final byte ARG_SEND_NOTIFICATION_SOURCE_KAKOTALK = (byte) 0x14;
// ARG_SET_NOTIFICATION_SOURCE_*
// 02 (This is 00 and 01 during connection. I don't know what it does. Maybe clears notifications?)
// ASCII
public static final byte CMD_SEND_NOTIFICATION = (byte) 0x72;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_WEEKDAY = (byte) 0x1F;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_EVERY_DAY = (byte) 0x7F;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_ONE_TIME = (byte) 0x80;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_MONDAY = (byte) 0x01;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_TUESDAY = (byte) 0x02;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_WEDNESDAY = (byte) 0x04;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_THURSDAY = (byte) 0x08;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_FRIDAY = (byte) 0x10;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_SATURDAY = (byte) 0x20;
public static final byte ARG_SET_ALARM_REMINDER_REPEAT_SUNDAY = (byte) 0x40;
// reminder id starting at 0
// enable (00/01)
// hour
// minute
// bit field of ARG_SET_ALARM_REMINDER_REPEAT_*
public static final byte CMD_SET_ALARM_REMINDER = (byte) 0x73;
public static final byte ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_MILES = (byte) 0x00;
public static final byte ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_KILOMETERS = (byte) 0x01;
// step length (in/cm)
// step length (in/cm)
// age (years)
// height (in/cm)
// weight (lb/kg)
// ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_*
// target step count (kilo)
// 5a
// 82
// 3c
// 5a
// 28
// b4
// 5d
// 64
public static final byte CMD_SET_PERSONAL_INFORMATION = (byte) 0x74;
// enable (00/01)
// start hour
// start minute
// end hour
// end minute
// 2d
public static final byte CMD_SET_SEDENTARY_REMINDER = (byte) 0x75;
// enable (00/01)
// start hour
// start minute
// end hour
// end minute
public static final byte CMD_SET_QUITE_HOURS = (byte) 0x76;
// enable (00/01)
public static final byte CMD_SET_HEADS_UP_SCREEN = (byte) 0x77;
// Looks like enable/disable.
public static final byte CMD_78 = (byte) 0x78;
// The watch enters photograph mode, but doesn't appear to send a trigger signal.
// enable (00/01)
public static final byte CMD_SET_PHOTOGRAPH_MODE = (byte) 0x79;
// enable (00/01)
public static final byte CMD_SET_LOST_REMINDER = (byte) 0x7a;
// 7b has 1 argument. Looks like enable/disable.
public static final byte ARG_SET_TIMEMODE_24H = 0x00;
public static final byte ARG_SET_TIMEMODE_12H = 0x01;
// ARG_SET_TIMEMODE_*
public static final byte CMD_SET_TIMEMODE = (byte) 0x7c;
// 14 arguments. Watch might reply with RPRT_BATTERY.
public static final byte CMD_7e = (byte) 0x7e;
// 01
// fall hour
// fall minute
// awake hour
// awake minute
public static final byte CMD_SET_SLEEP_TIME = (byte) 0x7f;
// enable (00/01)
public static final byte CMD_SET_REAL_TIME_HEART_RATE = (byte) 0x84;
// looks like enable/disable.
public static final byte CMD_85 = (byte) 0x85;
// 00
// year hi
// year lo
// month
// day
// hour
// minute
// second
public static final byte CMD_SET_DATE_TIME = (byte) 0x93;
// 3 arguments. Sent when saving personal information.
public static final byte CMD_95 = (byte) 0x95;
// looks like enable/disable.
public static final byte CMD_96 = (byte) 0x96;
// looks like enable/disable.
public static final byte CMD_e5 = (byte) 0xe5;
// If this is sent after {@link CMD_FACTORY_RESET}, it's a shutdown, not a reboot.
// Rebooting resets the watch face and wallpaper.
public static final byte CMD_REBOOT = (byte) 0xff;
}

View File

@ -0,0 +1,264 @@
/* Copyright (C) 2017-2019 Daniele Gobbetti, João Paulo Barraca, tiparega
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext;
public class MakibesHR3Coordinator extends AbstractDeviceCoordinator {
public static final int FindPhone_ON = -1;
public static final int FindPhone_OFF = 0;
private static final Logger LOG = LoggerFactory.getLogger(MakibesHR3Coordinator.class);
public static boolean shouldEnableHeadsUpScreen(SharedPreferences sharedPrefs) {
String liftMode = sharedPrefs.getString(MakibesHR3Constants.PREF_HEADS_UP_SCREEN, getContext().getString(R.string.p_on));
// Makibes HR3 doesn't support scheduled intervals. Treat it as "on".
return !liftMode.equals(getContext().getString(R.string.p_off));
}
public static boolean shouldEnableLostReminder(SharedPreferences sharedPrefs) {
String lostReminder = sharedPrefs.getString(MakibesHR3Constants.PREF_LOST_REMINDER, getContext().getString(R.string.p_on));
// Makibes HR3 doesn't support scheduled intervals. Treat it as "on".
return !lostReminder.equals(getContext().getString(R.string.p_off));
}
public static byte getTimeMode(SharedPreferences sharedPrefs) {
String timeMode = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h));
if (timeMode.equals(getContext().getString(R.string.p_timeformat_24h))) {
return MakibesHR3Constants.ARG_SET_TIMEMODE_24H;
} else {
return MakibesHR3Constants.ARG_SET_TIMEMODE_12H;
}
}
/**
* @param startOut out Only hour/minute are used.
* @param endOut out Only hour/minute are used.
* @return True if quite hours are enabled.
*/
public static boolean getQuiteHours(SharedPreferences sharedPrefs, Calendar startOut, Calendar endOut) {
String doNotDisturb = sharedPrefs.getString(MakibesHR3Constants.PREF_DO_NOT_DISTURB, getContext().getString(R.string.p_off));
if (doNotDisturb.equals(getContext().getString(R.string.p_off))) {
return false;
} else {
String start = sharedPrefs.getString(MakibesHR3Constants.PREF_DO_NOT_DISTURB_START, "00:00");
String end = sharedPrefs.getString(MakibesHR3Constants.PREF_DO_NOT_DISTURB_END, "00:00");
DateFormat df = new SimpleDateFormat("HH:mm");
try {
startOut.setTime(df.parse(start));
endOut.setTime(df.parse(end));
return true;
} catch (Exception e) {
LOG.error("Unexpected exception in MiBand2Coordinator.getTime: " + e.getMessage());
return false;
}
}
}
/**
* @return {@link #FindPhone_OFF}, {@link #FindPhone_ON}, or the duration
*/
public static int getFindPhone(SharedPreferences sharedPrefs) {
String findPhone = sharedPrefs.getString(MakibesHR3Constants.PREF_FIND_PHONE, getContext().getString(R.string.p_off));
if (findPhone.equals(getContext().getString(R.string.p_off))) {
return FindPhone_OFF;
} else if (findPhone.equals(getContext().getString(R.string.p_on))) {
return FindPhone_ON;
} else { // Duration
String duration = sharedPrefs.getString(MakibesHR3Constants.PREF_FIND_PHONE_DURATION, "0");
try {
int iDuration;
try {
iDuration = Integer.valueOf(duration);
} catch (Exception ex) {
LOG.warn(ex.getMessage());
iDuration = 60;
}
return iDuration;
} catch (Exception e) {
LOG.error("Unexpected exception in MiBand2Coordinator.getTime: " + e.getMessage());
return FindPhone_ON;
}
}
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
// TODO: Device discovery
if ((name != null) && name.equals("Y808")) {
return DeviceType.MAKIBESHR3;
}
return DeviceType.UNKNOWN;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
Long deviceId = device.getId();
QueryBuilder<?> qb = session.getMakibesHR3ActivitySampleDao().queryBuilder();
qb.where(MakibesHR3ActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_NONE;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return true;
}
@Override
public boolean supportsWeather() {
return false;
}
@Override
public boolean supportsFindDevice() {
return true;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.MAKIBESHR3;
}
@Nullable
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return false;
}
@Override
public boolean supportsActivityTracking() {
return true;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new MakibesHR3SampleProvider(device, session);
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public int getAlarmSlotCount() {
return 8;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public String getManufacturer() {
return "Makibes";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_timeformat,
R.xml.devicesettings_liftwrist_display,
R.xml.devicesettings_disconnectnotification,
R.xml.devicesettings_donotdisturb_no_auto,
R.xml.devicesettings_find_phone
};
}
}

View File

@ -0,0 +1,84 @@
/* Copyright (C) 2018-2019 Daniele Gobbetti, Sebastian Kranz
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class MakibesHR3SampleProvider extends AbstractSampleProvider<MakibesHR3ActivitySample> {
private GBDevice mDevice;
private DaoSession mSession;
public MakibesHR3SampleProvider(GBDevice device, DaoSession session) {
super(device, session);
mSession = session;
mDevice = device;
}
@Override
public int normalizeType(int rawType) {
return rawType;
}
@Override
public int toRawActivityKind(int activityKind) {
return activityKind;
}
@Override
public float normalizeIntensity(int rawIntensity) {
return rawIntensity;
}
@Override
public MakibesHR3ActivitySample createActivitySample() {
return new MakibesHR3ActivitySample();
}
@Override
public AbstractDao<MakibesHR3ActivitySample, ?> getSampleDao() {
return getSession().getMakibesHR3ActivitySampleDao();
}
@Nullable
@Override
protected Property getRawKindSampleProperty() {
return MakibesHR3ActivitySampleDao.Properties.RawKind;
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return MakibesHR3ActivitySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return MakibesHR3ActivitySampleDao.Properties.DeviceId;
}
}

View File

@ -17,17 +17,12 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.miband;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
public final class MiBandConst {
private static final Logger LOG = LoggerFactory.getLogger(MiBandConst.class);
public static final String PREF_USER_ALIAS = "mi_user_alias";
public static final String PREF_MIBAND_WEARSIDE = "mi_wearside";
public static final String PREF_MIBAND_ADDRESS = "development_miaddr"; // FIXME: should be prefixed mi_
public static final String PREF_MIBAND_ALARMS = "mi_alarms";
public static final String PREF_MIBAND_DONT_ACK_TRANSFER = "mi_dont_ack_transfer";

View File

@ -22,7 +22,6 @@ import android.app.Activity;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelUuid;
@ -38,6 +37,7 @@ import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
@ -228,10 +228,10 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
return info;
}
public static int getWearLocation(String miBandAddress) throws IllegalArgumentException {
public static int getWearLocation(String deviceAddress) throws IllegalArgumentException {
int location = 0; //left hand
Prefs prefs = GBApplication.getPrefs();
if ("right".equals(prefs.getString(MiBandConst.PREF_MIBAND_WEARSIDE, "left"))) {
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(deviceAddress));
if ("right".equals(prefs.getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left"))) {
location = 1; // right hand
}
return location;
@ -261,6 +261,7 @@ public class MiBandCoordinator extends AbstractDeviceCoordinator {
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_wearlocation,
R.xml.devicesettings_lowlatency_fwupdate,
R.xml.devicesettings_fake_timeoffset
};

View File

@ -49,7 +49,7 @@ public class MijiaLywsd02Coordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice deviceCandidate) {
public int getBondingStyle() {
return BONDING_STYLE_NONE;
}

View File

@ -85,7 +85,7 @@ public class MiScale2DeviceCoordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice device) {
public int getBondingStyle() {
return super.BONDING_STYLE_NONE;
}

View File

@ -71,7 +71,7 @@ public class No1F1Coordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice deviceCandidate) {
public int getBondingStyle() {
return BONDING_STYLE_NONE;
}

View File

@ -44,7 +44,7 @@ public abstract class RoidmiCoordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice device) {
public int getBondingStyle() {
return BONDING_STYLE_BOND;
}
@ -133,6 +133,7 @@ public abstract class RoidmiCoordinator extends AbstractDeviceCoordinator {
return true;
}
@NonNull
@Override
public int[] getColorPresets() {
return RoidmiConst.COLOR_PRESETS;

View File

@ -77,7 +77,7 @@ public class Watch9DeviceCoordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice deviceCandidate) {
public int getBondingStyle() {
return BONDING_STYLE_NONE;
}

View File

@ -124,7 +124,6 @@ public class ZeTimeConstants {
public static final byte INACTIVITY_TYPE = (byte) 0x08;
public static final byte LOW_POWER_TYPE = (byte) 0x09;
// watch settings
public static final String PREF_WRIST = "zetime_wrist";
public static final byte WEAR_ON_LEFT_WRIST = (byte) 0x00;
public static final byte WEAR_ON_RIGHT_WRIST = (byte) 0x01;
@ -160,7 +159,6 @@ public class ZeTimeConstants {
public static final String PREF_ACTIVITY_TRACKING = "zetime_activity_tracking";
public static final String PREF_HANDMOVE_DISPLAY = "zetime_handmove_display";
public static final String PREF_CALORIES_TYPE = "zetime_calories_type";
public static final String PREF_TIME_FORMAT = "zetime_time_format";
public static final String PREF_DATE_FORMAT = "zetime_date_format";
public static final String PREF_ALARM_SIGNALING = "zetime_alarm_signaling";

View File

@ -22,11 +22,12 @@ import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
import java.util.Collection;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import java.util.Collection;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
@ -37,9 +38,6 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
/**
* Created by lightwars on 06.02.18.
*/
public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
@Override
@ -135,7 +133,7 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) {
}
@ -155,7 +153,18 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator {
}
@Override
public int getBondingStyle(GBDevice device) {
public int getBondingStyle() {
return BONDING_STYLE_NONE;
}
@Override
public boolean supportsUnicodeEmojis() { return true; }
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_timeformat,
R.xml.devicesettings_wearlocation,
};
}
}

View File

@ -22,6 +22,7 @@ import android.preference.Preference;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
public class ZeTimePreferenceActivity extends AbstractSettingsActivity {
@Override
@ -43,8 +44,6 @@ public class ZeTimePreferenceActivity extends AbstractSettingsActivity {
addPreferenceHandlerFor(ZeTimeConstants.PREF_SCREENTIME);
addPreferenceHandlerFor(ZeTimeConstants.PREF_WRIST);
addPreferenceHandlerFor(ZeTimeConstants.PREF_ANALOG_MODE);
addPreferenceHandlerFor(ZeTimeConstants.PREF_ACTIVITY_TRACKING);
@ -57,8 +56,6 @@ public class ZeTimePreferenceActivity extends AbstractSettingsActivity {
addPreferenceHandlerFor(ZeTimeConstants.PREF_CALORIES_TYPE);
addPreferenceHandlerFor(ZeTimeConstants.PREF_TIME_FORMAT);
addPreferenceHandlerFor(ZeTimeConstants.PREF_DATE_FORMAT);
addPreferenceHandlerFor(ZeTimeConstants.PREF_INACTIVITY_ENABLE);

View File

@ -61,7 +61,7 @@ public class BluetoothPairingRequestReceiver extends BroadcastReceiver {
DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
try {
if (coordinator.getBondingStyle(gbDevice) == DeviceCoordinator.BONDING_STYLE_NONE) {
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_NONE) {
LOG.info("Aborting unwanted pairing request");
abortBroadcast();
}

View File

@ -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);
}
}
}

View File

@ -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
/*

View File

@ -82,6 +82,10 @@ public class GBDevice implements Parcelable {
private List<ItemWithDetails> mDeviceInfos;
private HashMap<String, Object> mExtraInfos;
private int mNotificationIconConnected = R.drawable.ic_notification;
private int mNotificationIconDisconnected = R.drawable.ic_notification_disconnected;
private int mNotificationIconLowBattery = R.drawable.ic_notification_low_battery;
public GBDevice(String address, String name, DeviceType deviceType) {
this(address, null, name, deviceType);
}
@ -110,6 +114,9 @@ public class GBDevice implements Parcelable {
mBusyTask = in.readString();
mDeviceInfos = in.readArrayList(getClass().getClassLoader());
mExtraInfos = (HashMap) in.readSerializable();
mNotificationIconConnected = in.readInt();
mNotificationIconDisconnected = in.readInt();
mNotificationIconLowBattery = in.readInt();
validate();
}
@ -131,6 +138,9 @@ public class GBDevice implements Parcelable {
dest.writeString(mBusyTask);
dest.writeList(mDeviceInfos);
dest.writeSerializable(mExtraInfos);
dest.writeInt(mNotificationIconConnected);
dest.writeInt(mNotificationIconDisconnected);
dest.writeInt(mNotificationIconLowBattery);
}
private void validate() {
@ -221,6 +231,30 @@ public class GBDevice implements Parcelable {
return mBusyTask;
}
public int getNotificationIconConnected() {
return mNotificationIconConnected;
}
public void setNotificationIconConnected(int mNotificationIconConnected) {
this.mNotificationIconConnected = mNotificationIconConnected;
}
public int getNotificationIconDisconnected() {
return mNotificationIconDisconnected;
}
public void setNotificationIconDisconnected(int notificationIconDisconnected) {
this.mNotificationIconDisconnected = notificationIconDisconnected;
}
public int getNotificationIconLowBattery() {
return mNotificationIconLowBattery;
}
public void setNotificationIconLowBattery(int mNotificationIconLowBattery) {
this.mNotificationIconLowBattery = mNotificationIconLowBattery;
}
/**
* Marks the device as busy, performing a certain task. While busy, no other operations will
* be performed on the device.

View File

@ -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);
}
}

View File

@ -39,6 +39,7 @@ public enum DeviceType {
MIBAND3(14, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband3),
AMAZFITCOR2(15, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_amazfit_cor2),
MIBAND4(16, R.drawable.ic_device_miband2, R.drawable.ic_device_miband2_disabled, R.string.devicetype_miband4),
AMAZFITBIP_LITE(17, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_amazfit_bip_lite),
VIBRATISSIMO(20, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_vibratissimo),
LIVEVIEW(30, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_liveview),
HPLUS(40, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_hplus),
@ -58,6 +59,7 @@ public enum DeviceType {
CASIOGB6900(120, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_casiogb6900),
MISCALE2(131, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_miscale2),
BFH16(140, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_bfh16),
MAKIBESHR3(150, R.drawable.ic_device_default, R.drawable.ic_device_hplus_disabled, R.string.devicetype_makibes_hr3),
MIJIA_LYWSD02(200, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_mijia_lywsd02),
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -33,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.casiogb6900.CasioGB6900DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipLiteSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip.AmazfitBipSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor.AmazfitCorSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitcor2.AmazfitCor2Support;
@ -42,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.id115.ID115Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.BFH16DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.liveview.LiveviewSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.makibeshr3.MakibesHR3DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaLywsd02Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport;
@ -136,6 +138,9 @@ public class DeviceSupportFactory {
case AMAZFITBIP:
deviceSupport = new ServiceDeviceSupport(new AmazfitBipSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case AMAZFITBIP_LITE:
deviceSupport = new ServiceDeviceSupport(new AmazfitBipLiteSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case AMAZFITCOR:
deviceSupport = new ServiceDeviceSupport(new AmazfitCorSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
@ -195,6 +200,10 @@ public class DeviceSupportFactory {
break;
case MIJIA_LYWSD02:
deviceSupport = new ServiceDeviceSupport(new MijiaLywsd02Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
case MAKIBESHR3:
deviceSupport = new ServiceDeviceSupport(new MakibesHR3DeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
}
if (deviceSupport != null) {
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);

View File

@ -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();

View File

@ -242,6 +242,7 @@ public final class BtLEQueue {
mBluetoothGattServer.addService(service);
}
}
synchronized (mGattMonitor) {
// connectGatt with true doesn't really work ;( too often connection problems
if (GBApplication.isRunningMarshmallowOrLater()) {
@ -259,6 +260,7 @@ public final class BtLEQueue {
private void setDeviceConnectionState(State newState) {
LOG.debug("new device connection state: " + newState);
mGbDevice.setState(newState);
mGbDevice.sendDeviceUpdateIntent(mContext);
if (mConnectionLatch != null && newState == State.CONNECTED) {

View File

@ -42,15 +42,18 @@ import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.SimpleTimeZone;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import cyanogenmod.weather.util.WeatherUtils;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
@ -66,6 +69,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiWeatherConditions;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitbip.AmazfitBipService;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2FWHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Coordinator;
@ -95,6 +99,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
@ -125,21 +130,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.Version;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PAUSE;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PROFILE;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COLOUR;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_ORIGINAL_COLOUR;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_DURATION;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PAUSE;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefIntValue;
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefStringValue;
@ -327,23 +320,6 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
return this;
}
/**
* Adds a custom notification to the given transaction builder
* @param vibrationProfile specifies how and how often the Band shall vibrate.
* @param simpleNotification
* @param flashTimes
* @param flashColour
* @param originalColour
* @param flashDuration
* @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example.
* @param builder
*/
private HuamiSupport sendCustomNotification(VibrationProfile vibrationProfile, SimpleNotification simpleNotification, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) {
getNotificationStrategy().sendCustomNotification(vibrationProfile, simpleNotification, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder);
LOG.info("Sending notification to MiBand");
return this;
}
public NotificationStrategy getNotificationStrategy() {
String firmwareVersion = gbDevice.getFirmwareVersion();
if (firmwareVersion != null) {
@ -439,7 +415,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
int userid = alias.hashCode(); // hash from alias like mi1
// FIXME: Do encoding like in PebbleProtocol, this is ugly
byte bytes[] = new byte[]{
byte[] bytes = new byte[]{
HuamiService.COMMAND_SET_USERINFO,
0,
0,
@ -563,58 +539,26 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
}
}
protected void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
private void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) {
try {
TransactionBuilder builder = performInitialized(task);
Prefs prefs = GBApplication.getPrefs();
int vibrateDuration = getPreferredVibrateDuration(notificationOrigin, prefs);
int vibratePause = getPreferredVibratePause(notificationOrigin, prefs);
short vibrateTimes = getPreferredVibrateCount(notificationOrigin, prefs);
VibrationProfile profile = getPreferredVibrateProfile(notificationOrigin, prefs, vibrateTimes);
profile.setAlertLevel(alertLevel);
int flashTimes = getPreferredFlashCount(notificationOrigin, prefs);
int flashColour = getPreferredFlashColour(notificationOrigin, prefs);
int originalColour = getPreferredOriginalColour(notificationOrigin, prefs);
int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs);
getNotificationStrategy().sendCustomNotification(profile, simpleNotification, 0, 0, 0, 0, extraAction, builder);
sendCustomNotification(profile, simpleNotification, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder);
// sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder);
builder.queue(getQueue());
} catch (IOException ex) {
LOG.error("Unable to send notification to MI device", ex);
LOG.error("Unable to send notification to device", ex);
}
}
private int getPreferredFlashDuration(String notificationOrigin, Prefs prefs) {
return getNotificationPrefIntValue(FLASH_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_DURATION);
}
private int getPreferredOriginalColour(String notificationOrigin, Prefs prefs) {
return getNotificationPrefIntValue(FLASH_ORIGINAL_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR);
}
private int getPreferredFlashColour(String notificationOrigin, Prefs prefs) {
return getNotificationPrefIntValue(FLASH_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COLOUR);
}
private int getPreferredFlashCount(String notificationOrigin, Prefs prefs) {
return getNotificationPrefIntValue(FLASH_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COUNT);
}
private int getPreferredVibratePause(String notificationOrigin, Prefs prefs) {
return getNotificationPrefIntValue(VIBRATION_PAUSE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PAUSE);
}
private short getPreferredVibrateCount(String notificationOrigin, Prefs prefs) {
return (short) Math.min(Short.MAX_VALUE, getNotificationPrefIntValue(VIBRATION_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_COUNT));
}
private int getPreferredVibrateDuration(String notificationOrigin, Prefs prefs) {
return getNotificationPrefIntValue(VIBRATION_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_DURATION);
}
private VibrationProfile getPreferredVibrateProfile(String notificationOrigin, Prefs prefs, short repeat) {
String profileId = getNotificationPrefStringValue(VIBRATION_PROFILE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PROFILE);
return VibrationProfile.getProfile(profileId, repeat);
@ -703,17 +647,17 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, simpleNotification, HuamiService.ALERT_LEVEL_PHONE_CALL, abortAction);
} else if ((callSpec.command == CallSpec.CALL_START) || (callSpec.command == CallSpec.CALL_END)) {
telephoneRinging = false;
stopCurrentNotification();
stopCurrentCallNotification();
}
}
private void stopCurrentNotification() {
private void stopCurrentCallNotification() {
try {
TransactionBuilder builder = performInitialized("stop notification");
getNotificationStrategy().stopCurrentNotification(builder);
builder.queue(getQueue());
} catch (IOException e) {
LOG.error("Error stopping notification");
LOG.error("Error stopping call notification");
}
}
@ -762,9 +706,8 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
}
private void sendMusicStateToDevice() {
if (characteristicChunked == null) {
return;
}
@ -971,7 +914,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
}
private byte[] getLatency(int minConnectionInterval, int maxConnectionInterval, int latency, int timeout, int advertisementInterval) {
byte result[] = new byte[12];
byte[] result = new byte[12];
result[0] = (byte) (minConnectionInterval & 0xff);
result[1] = (byte) (0xff & minConnectionInterval >> 8);
result[2] = (byte) (maxConnectionInterval & 0xff);
@ -1559,12 +1502,18 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
case MiBandConst.PREF_SWIPE_UNLOCK:
setBandScreenUnlock(builder);
break;
case HuamiConst.PREF_DATEFORMAT:
case DeviceSettingsPreferenceConst.PREF_DATEFORMAT:
setDateFormat(builder);
break;
case HuamiConst.PREF_LANGUAGE:
setLanguage(builder);
break;
case HuamiConst.PREF_EXPOSE_HR_THIRDPARTY:
setExposeHRThridParty(builder);
break;
case DeviceSettingsPreferenceConst.PREF_WEARLOCATION:
setWearLocation(builder);
break;
}
builder.queue(getQueue());
} catch (IOException e) {
@ -1584,7 +1533,190 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
// FIXME: currently HuamiSupport *is* MiBand2 support, so return if we are using Mi Band 2
if (gbDevice.getType() == DeviceType.MIBAND2) {
return;
}
if (gbDevice.getFirmwareVersion() == null) {
LOG.warn("Device not initialized yet, so not sending weather info");
return;
}
boolean supportsConditionString = false;
Version version = new Version(gbDevice.getFirmwareVersion());
if (version.compareTo(new Version("0.0.8.74")) >= 0) {
supportsConditionString = true;
}
MiBandConst.DistanceUnit unit = HuamiCoordinator.getDistanceUnit();
int tz_offset_hours = SimpleTimeZone.getDefault().getOffset(weatherSpec.timestamp * 1000L) / (1000 * 60 * 60);
try {
TransactionBuilder builder;
builder = performInitialized("Sending current temp");
byte condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode);
int length = 8;
if (supportsConditionString) {
length += weatherSpec.currentCondition.getBytes().length + 1;
}
ByteBuffer buf = ByteBuffer.allocate(length);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put((byte) 2);
buf.putInt(weatherSpec.timestamp);
buf.put((byte) (tz_offset_hours * 4));
buf.put(condition);
int currentTemp = weatherSpec.currentTemp - 273;
if (unit == MiBandConst.DistanceUnit.IMPERIAL) {
currentTemp = (int) WeatherUtils.celsiusToFahrenheit(currentTemp);
}
buf.put((byte) currentTemp);
if (supportsConditionString) {
buf.put(weatherSpec.currentCondition.getBytes());
buf.put((byte) 0);
}
if (characteristicChunked != null) {
writeToChunked(builder, 1, buf.array());
} else {
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
}
builder.queue(getQueue());
} catch (Exception ex) {
LOG.error("Error sending current weather", ex);
}
try {
TransactionBuilder builder;
builder = performInitialized("Sending air quality index");
int length = 8;
String aqiString = "(n/a)";
if (supportsConditionString) {
length += aqiString.getBytes().length + 1;
}
ByteBuffer buf = ByteBuffer.allocate(length);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put((byte) 4);
buf.putInt(weatherSpec.timestamp);
buf.put((byte) (tz_offset_hours * 4));
buf.putShort((short) 0);
if (supportsConditionString) {
buf.put(aqiString.getBytes());
buf.put((byte) 0);
}
if (characteristicChunked != null) {
writeToChunked(builder, 1, buf.array());
} else {
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
}
builder.queue(getQueue());
} catch (IOException ex) {
LOG.error("Error sending air quality");
}
try {
TransactionBuilder builder = performInitialized("Sending weather forecast");
final byte NR_DAYS = (byte) (1 + weatherSpec.forecasts.size());
int bytesPerDay = 4;
int conditionsLength = 0;
if (supportsConditionString) {
bytesPerDay = 5;
conditionsLength = weatherSpec.currentCondition.getBytes().length;
for (WeatherSpec.Forecast forecast : weatherSpec.forecasts) {
conditionsLength += Weather.getConditionString(forecast.conditionCode).getBytes().length;
}
}
int length = 7 + bytesPerDay * NR_DAYS + conditionsLength;
ByteBuffer buf = ByteBuffer.allocate(length);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put((byte) 1);
buf.putInt(weatherSpec.timestamp);
buf.put((byte) (tz_offset_hours * 4));
buf.put(NR_DAYS);
byte condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode);
buf.put(condition);
buf.put(condition);
int todayMaxTemp = weatherSpec.todayMaxTemp - 273;
int todayMinTemp = weatherSpec.todayMinTemp - 273;
if (unit == MiBandConst.DistanceUnit.IMPERIAL) {
todayMaxTemp = (int) WeatherUtils.celsiusToFahrenheit(todayMaxTemp);
todayMinTemp = (int) WeatherUtils.celsiusToFahrenheit(todayMinTemp);
}
buf.put((byte) todayMaxTemp);
buf.put((byte) todayMinTemp);
if (supportsConditionString) {
buf.put(weatherSpec.currentCondition.getBytes());
buf.put((byte) 0);
}
for (WeatherSpec.Forecast forecast : weatherSpec.forecasts) {
condition = HuamiWeatherConditions.mapToAmazfitBipWeatherCode(forecast.conditionCode);
buf.put(condition);
buf.put(condition);
int forecastMaxTemp = forecast.maxTemp - 273;
int forecastMinTemp = forecast.minTemp - 273;
if (unit == MiBandConst.DistanceUnit.IMPERIAL) {
forecastMaxTemp = (int) WeatherUtils.celsiusToFahrenheit(forecastMaxTemp);
forecastMinTemp = (int) WeatherUtils.celsiusToFahrenheit(forecastMinTemp);
}
buf.put((byte) forecastMaxTemp);
buf.put((byte) forecastMinTemp);
if (supportsConditionString) {
buf.put(Weather.getConditionString(forecast.conditionCode).getBytes());
buf.put((byte) 0);
}
}
if (characteristicChunked != null) {
writeToChunked(builder, 1, buf.array());
} else {
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
}
builder.queue(getQueue());
} catch (Exception ex) {
LOG.error("Error sending weather forecast", ex);
}
try {
TransactionBuilder builder;
builder = performInitialized("Sending forecast location");
int length = 2 + weatherSpec.location.getBytes().length;
ByteBuffer buf = ByteBuffer.allocate(length);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.put((byte) 8);
buf.put(weatherSpec.location.getBytes());
buf.put((byte) 0);
if (characteristicChunked != null) {
writeToChunked(builder, 1, buf.array());
} else {
builder.write(getCharacteristic(AmazfitBipService.UUID_CHARACTERISTIC_WEATHER), buf.array());
}
builder.queue(getQueue());
} catch (Exception ex) {
LOG.error("Error sending current forecast location", ex);
}
}
private HuamiSupport setDateDisplay(TransactionBuilder builder) {
@ -1909,6 +2041,20 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
return this;
}
private HuamiSupport setExposeHRThridParty(TransactionBuilder builder) {
boolean enable = HuamiCoordinator.getExposeHRThirdParty(gbDevice.getAddress());
LOG.info("Setting exposure of HR to third party apps to: " + enable);
if (enable) {
builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_ENBALE_HR_CONNECTION);
} else {
builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), HuamiService.COMMAND_DISABLE_HR_CONNECTION);
}
return this;
}
protected void writeToChunked(TransactionBuilder builder, int type, byte[] data) {
final int MAX_CHUNKLENGTH = 17;
int remaining = data.length;
@ -1937,6 +2083,42 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
}
}
@Override
public String customStringFilter(String inputString) {
if (HuamiCoordinator.getUseCustomFont(gbDevice.getAddress())) {
return convertEmojiToCustomFont(inputString);
}
return inputString;
}
private String convertEmojiToCustomFont(String str) {
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < str.length()) {
char charAt = str.charAt(i);
if (Character.isHighSurrogate(charAt)) {
int i2 = i + 1;
try {
int codePoint = Character.toCodePoint(charAt, str.charAt(i2));
if (codePoint < 127744 || codePoint > 129510) {
sb.append(charAt);
} else {
sb.append((char) (codePoint - 83712));
i = i2;
}
} catch (StringIndexOutOfBoundsException e) {
LOG.warn("error while converting emoji to custom font", e);
sb.append(charAt);
}
} else {
sb.append(charAt);
}
i++;
}
return sb.toString();
}
public void phase2Initialize(TransactionBuilder builder) {
LOG.info("phase2Initialize...");
requestBatteryInfo(builder);
@ -1959,6 +2141,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport {
setInactivityWarnings(builder);
setHeartrateSleepSupport(builder);
setDisconnectNotification(builder);
setExposeHRThridParty(builder);
setHeartrateMeasurementInterval(builder, getHeartRateMeasurementInterval());
}

View File

@ -112,6 +112,7 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
// Latin Firmware
crcToVersion.put(52828, "1.1.5.36 (Latin)");
crcToVersion.put(60625, "1.1.6.30 (Latin)");
// resources
crcToVersion.put(12586, "0.0.8.74");
@ -138,6 +139,7 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
crcToVersion.put(5341, "1.1.5.02-24");
crcToVersion.put(22662, "1.1.5.36");
crcToVersion.put(24045, "1.1.5.56");
crcToVersion.put(37677, "1.1.6.30");
// gps
crcToVersion.put(61520, "9367,8f79a91,0,0,");
@ -149,7 +151,8 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
// font
crcToVersion.put(61054, "8");
crcToVersion.put(62291, "9 (Latin)");
crcToVersion.put(62291, "9 (old Latin)");
crcToVersion.put(59577, "9 (Latin)");
}
public AmazfitBipFirmwareInfo(byte[] bytes) {
@ -182,7 +185,7 @@ public class AmazfitBipFirmwareInfo extends HuamiFirmwareInfo {
if (ArrayUtils.startsWith(bytes, NEWFT_HEADER)) {
if (bytes[10] == 0x01) {
return HuamiFirmwareType.FONT;
} else if (bytes[10] == 0x02) {
} else if (bytes[10] == 0x02 || bytes[10] == 0x0A) {
return HuamiFirmwareType.FONT_LATIN;
}
}

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