Repo created
This commit is contained in:
parent
368e9c2100
commit
4fa38b691e
262 changed files with 17567 additions and 2 deletions
1
app/.gitignore
vendored
Normal file
1
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
103
app/build.gradle.kts
Normal file
103
app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import com.android.build.gradle.internal.tasks.factory.dependsOn
|
||||
|
||||
private val readAndUnderstoodLicense = false
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.github.domi04151309.home"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "io.github.domi04151309.home"
|
||||
minSdk = 23
|
||||
//noinspection EditedTargetSdkVersion
|
||||
targetSdk = 34
|
||||
versionCode = 1120
|
||||
versionName = "1.12.0"
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
testOptions {
|
||||
unitTests.isIncludeAndroidResources = true
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
detekt {
|
||||
config.setFrom(file("detekt-config.yml"))
|
||||
buildUponDefaultConfig = true
|
||||
basePath = rootProject.projectDir.absolutePath
|
||||
}
|
||||
lint {
|
||||
disable += "MissingTranslation"
|
||||
}
|
||||
project.tasks.preBuild.dependsOn("license")
|
||||
}
|
||||
|
||||
tasks.register("license") {
|
||||
doFirst {
|
||||
val data =
|
||||
file("./src/main/res/xml/pref_about.xml")
|
||||
.readText()
|
||||
.contains("app:key=\"license\"")
|
||||
if (!data) {
|
||||
throw Exception(
|
||||
"Please note that removing the license from the about page is not allowed if you " +
|
||||
"plan to publish your modified version of this app. " +
|
||||
"Please read the project's LICENSE.",
|
||||
)
|
||||
}
|
||||
if (!(
|
||||
android.defaultConfig.applicationId?.contains("domi04151309") == true ||
|
||||
readAndUnderstoodLicense
|
||||
)
|
||||
) {
|
||||
throw Exception(
|
||||
"Please make sure you have read and understood the LICENSE!",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("androidx.annotation:annotation:1.9.1")
|
||||
implementation("com.android.volley:volley:1.2.1")
|
||||
implementation("androidx.security:security-crypto-ktx:1.1.0-beta01")
|
||||
implementation("com.github.skydoves:colorpickerview:2.3.0")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.robolectric:robolectric:4.14.1")
|
||||
}
|
||||
784
app/detekt-config.yml
Normal file
784
app/detekt-config.yml
Normal file
|
|
@ -0,0 +1,784 @@
|
|||
build:
|
||||
maxIssues: 0
|
||||
excludeCorrectable: true
|
||||
weights:
|
||||
# complexity: 2
|
||||
# LongParameterList: 1
|
||||
# style: 1
|
||||
# comments: 1
|
||||
|
||||
config:
|
||||
validation: true
|
||||
warningsAsErrors: true
|
||||
checkExhaustiveness: true
|
||||
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
|
||||
excludes: ''
|
||||
|
||||
processors:
|
||||
active: true
|
||||
exclude:
|
||||
- 'DetektProgressListener'
|
||||
# - 'KtFileCountProcessor'
|
||||
# - 'PackageCountProcessor'
|
||||
# - 'ClassCountProcessor'
|
||||
# - 'FunctionCountProcessor'
|
||||
# - 'PropertyCountProcessor'
|
||||
# - 'ProjectComplexityProcessor'
|
||||
# - 'ProjectCognitiveComplexityProcessor'
|
||||
# - 'ProjectLLOCProcessor'
|
||||
# - 'ProjectCLOCProcessor'
|
||||
# - 'ProjectLOCProcessor'
|
||||
# - 'ProjectSLOCProcessor'
|
||||
# - 'LicenseHeaderLoaderExtension'
|
||||
|
||||
console-reports:
|
||||
active: true
|
||||
exclude:
|
||||
- 'ProjectStatisticsReport'
|
||||
- 'ComplexityReport'
|
||||
- 'NotificationReport'
|
||||
- 'FindingsReport'
|
||||
- 'FileBasedFindingsReport'
|
||||
# - 'LiteFindingsReport'
|
||||
|
||||
output-reports:
|
||||
active: true
|
||||
exclude:
|
||||
# - 'TxtOutputReport'
|
||||
# - 'XmlOutputReport'
|
||||
# - 'HtmlOutputReport'
|
||||
# - 'MdOutputReport'
|
||||
# - 'SarifOutputReport'
|
||||
|
||||
comments:
|
||||
active: true
|
||||
AbsentOrWrongFileLicense:
|
||||
active: false
|
||||
licenseTemplateFile: 'license.template'
|
||||
licenseTemplateIsRegex: true
|
||||
CommentOverPrivateFunction:
|
||||
active: true
|
||||
CommentOverPrivateProperty:
|
||||
active: true
|
||||
DeprecatedBlockTag:
|
||||
active: true
|
||||
EndOfSentenceFormat:
|
||||
active: true
|
||||
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
|
||||
KDocReferencesNonPublicProperty:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
OutdatedDocumentation:
|
||||
active: true
|
||||
matchTypeParameters: true
|
||||
matchDeclarationsOrder: true
|
||||
allowParamOnConstructorProperties: true
|
||||
UndocumentedPublicClass:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
searchInNestedClass: true
|
||||
searchInInnerClass: true
|
||||
searchInInnerObject: true
|
||||
searchInInnerInterface: true
|
||||
searchInProtectedClass: true
|
||||
UndocumentedPublicFunction:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
searchProtectedFunction: true
|
||||
UndocumentedPublicProperty:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
searchProtectedProperty: true
|
||||
|
||||
complexity:
|
||||
active: true
|
||||
CognitiveComplexMethod:
|
||||
active: true
|
||||
threshold: 15
|
||||
ComplexCondition:
|
||||
active: true
|
||||
threshold: 4
|
||||
ComplexInterface:
|
||||
active: true
|
||||
threshold: 10
|
||||
includeStaticDeclarations: true
|
||||
includePrivateDeclarations: true
|
||||
ignoreOverloaded: true
|
||||
CyclomaticComplexMethod:
|
||||
active: true
|
||||
threshold: 15
|
||||
ignoreSingleWhenExpression: true
|
||||
ignoreSimpleWhenEntries: true
|
||||
ignoreNestingFunctions: true
|
||||
nestingFunctions:
|
||||
- 'also'
|
||||
- 'apply'
|
||||
- 'forEach'
|
||||
- 'isNotNull'
|
||||
- 'ifNull'
|
||||
- 'let'
|
||||
- 'run'
|
||||
- 'use'
|
||||
- 'with'
|
||||
LabeledExpression:
|
||||
active: true
|
||||
ignoredLabels: []
|
||||
LargeClass:
|
||||
active: true
|
||||
threshold: 600
|
||||
LongMethod:
|
||||
active: true
|
||||
threshold: 60
|
||||
LongParameterList:
|
||||
active: true
|
||||
functionThreshold: 6
|
||||
constructorThreshold: 7
|
||||
ignoreDefaultParameters: true
|
||||
ignoreDataClasses: true
|
||||
ignoreAnnotatedParameter: []
|
||||
MethodOverloading:
|
||||
active: true
|
||||
threshold: 6
|
||||
NamedArguments:
|
||||
active: true
|
||||
threshold: 3
|
||||
ignoreArgumentsMatchingNames: true
|
||||
NestedBlockDepth:
|
||||
active: true
|
||||
threshold: 4
|
||||
NestedScopeFunctions:
|
||||
active: true
|
||||
threshold: 1
|
||||
functions:
|
||||
- 'kotlin.apply'
|
||||
- 'kotlin.run'
|
||||
- 'kotlin.with'
|
||||
- 'kotlin.let'
|
||||
- 'kotlin.also'
|
||||
ReplaceSafeCallChainWithRun:
|
||||
active: true
|
||||
StringLiteralDuplication:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
threshold: 3
|
||||
ignoreAnnotation: true
|
||||
excludeStringsWithLessThan5Characters: true
|
||||
ignoreStringsRegex: '$^'
|
||||
TooManyFunctions:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
thresholdInFiles: 11
|
||||
thresholdInClasses: 11
|
||||
thresholdInInterfaces: 11
|
||||
thresholdInObjects: 11
|
||||
thresholdInEnums: 11
|
||||
ignoreDeprecated: true
|
||||
ignorePrivate: true
|
||||
ignoreOverridden: true
|
||||
|
||||
coroutines:
|
||||
active: true
|
||||
GlobalCoroutineUsage:
|
||||
active: true
|
||||
InjectDispatcher:
|
||||
active: true
|
||||
dispatcherNames:
|
||||
- 'IO'
|
||||
- 'Default'
|
||||
- 'Unconfined'
|
||||
RedundantSuspendModifier:
|
||||
active: true
|
||||
SleepInsteadOfDelay:
|
||||
active: true
|
||||
SuspendFunSwallowedCancellation:
|
||||
active: true
|
||||
SuspendFunWithCoroutineScopeReceiver:
|
||||
active: true
|
||||
SuspendFunWithFlowReturnType:
|
||||
active: true
|
||||
|
||||
empty-blocks:
|
||||
active: true
|
||||
EmptyCatchBlock:
|
||||
active: true
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
EmptyClassBlock:
|
||||
active: true
|
||||
EmptyDefaultConstructor:
|
||||
active: true
|
||||
EmptyDoWhileBlock:
|
||||
active: true
|
||||
EmptyElseBlock:
|
||||
active: true
|
||||
EmptyFinallyBlock:
|
||||
active: true
|
||||
EmptyForBlock:
|
||||
active: true
|
||||
EmptyFunctionBlock:
|
||||
active: true
|
||||
ignoreOverridden: true
|
||||
EmptyIfBlock:
|
||||
active: true
|
||||
EmptyInitBlock:
|
||||
active: true
|
||||
EmptyKtFile:
|
||||
active: true
|
||||
EmptySecondaryConstructor:
|
||||
active: true
|
||||
EmptyTryBlock:
|
||||
active: true
|
||||
EmptyWhenBlock:
|
||||
active: true
|
||||
EmptyWhileBlock:
|
||||
active: true
|
||||
|
||||
exceptions:
|
||||
active: true
|
||||
ExceptionRaisedInUnexpectedLocation:
|
||||
active: true
|
||||
methodNames:
|
||||
- 'equals'
|
||||
- 'finalize'
|
||||
- 'hashCode'
|
||||
- 'toString'
|
||||
InstanceOfCheckForException:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
NotImplementedDeclaration:
|
||||
active: true
|
||||
ObjectExtendsThrowable:
|
||||
active: true
|
||||
PrintStackTrace:
|
||||
active: true
|
||||
RethrowCaughtException:
|
||||
active: true
|
||||
ReturnFromFinally:
|
||||
active: true
|
||||
ignoreLabeled: true
|
||||
SwallowedException:
|
||||
active: true
|
||||
ignoredExceptionTypes:
|
||||
- 'InterruptedException'
|
||||
- 'MalformedURLException'
|
||||
- 'NumberFormatException'
|
||||
- 'ParseException'
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
ThrowingExceptionFromFinally:
|
||||
active: true
|
||||
ThrowingExceptionInMain:
|
||||
active: true
|
||||
ThrowingExceptionsWithoutMessageOrCause:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
exceptions:
|
||||
- 'ArrayIndexOutOfBoundsException'
|
||||
- 'Exception'
|
||||
- 'IllegalArgumentException'
|
||||
- 'IllegalMonitorStateException'
|
||||
- 'IllegalStateException'
|
||||
- 'IndexOutOfBoundsException'
|
||||
- 'NullPointerException'
|
||||
- 'RuntimeException'
|
||||
- 'Throwable'
|
||||
ThrowingNewInstanceOfSameException:
|
||||
active: true
|
||||
TooGenericExceptionCaught:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
exceptionNames:
|
||||
- 'ArrayIndexOutOfBoundsException'
|
||||
- 'Error'
|
||||
- 'Exception'
|
||||
- 'IllegalMonitorStateException'
|
||||
- 'IndexOutOfBoundsException'
|
||||
- 'NullPointerException'
|
||||
- 'RuntimeException'
|
||||
- 'Throwable'
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
TooGenericExceptionThrown:
|
||||
active: true
|
||||
exceptionNames:
|
||||
- 'Error'
|
||||
- 'Exception'
|
||||
- 'RuntimeException'
|
||||
- 'Throwable'
|
||||
|
||||
naming:
|
||||
active: true
|
||||
BooleanPropertyNaming:
|
||||
active: true
|
||||
allowedPattern: '^(is|has|are)'
|
||||
ClassNaming:
|
||||
active: true
|
||||
classPattern: '[A-Z][a-zA-Z0-9]*'
|
||||
ConstructorParameterNaming:
|
||||
active: true
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
privateParameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
EnumNaming:
|
||||
active: true
|
||||
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
|
||||
ForbiddenClassName:
|
||||
active: true
|
||||
forbiddenName: []
|
||||
FunctionMaxLength:
|
||||
active: true
|
||||
maximumFunctionNameLength: 30
|
||||
FunctionMinLength:
|
||||
active: true
|
||||
minimumFunctionNameLength: 3
|
||||
FunctionNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
functionPattern: '[a-z][a-zA-Z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
FunctionParameterNaming:
|
||||
active: true
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
InvalidPackageDeclaration:
|
||||
active: true
|
||||
rootPackage: ''
|
||||
requireRootInDeclaration: false
|
||||
LambdaParameterNaming:
|
||||
active: true
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*|_'
|
||||
MatchingDeclarationName:
|
||||
active: true
|
||||
mustBeFirst: true
|
||||
MemberNameEqualsClassName:
|
||||
active: true
|
||||
ignoreOverridden: true
|
||||
NoNameShadowing:
|
||||
active: true
|
||||
NonBooleanPropertyPrefixedWithIs:
|
||||
active: true
|
||||
ObjectPropertyNaming:
|
||||
active: true
|
||||
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
|
||||
PackageNaming:
|
||||
active: true
|
||||
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
|
||||
TopLevelPropertyNaming:
|
||||
active: true
|
||||
constantPattern: '[A-Z][_A-Z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
|
||||
VariableMaxLength:
|
||||
active: true
|
||||
maximumVariableNameLength: 64
|
||||
VariableMinLength:
|
||||
active: true
|
||||
minimumVariableNameLength: 1
|
||||
VariableNaming:
|
||||
active: true
|
||||
variablePattern: '[a-z][A-Za-z0-9]*'
|
||||
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
|
||||
performance:
|
||||
active: true
|
||||
ArrayPrimitive:
|
||||
active: true
|
||||
CouldBeSequence:
|
||||
active: true
|
||||
threshold: 3
|
||||
ForEachOnRange:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
SpreadOperator:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
UnnecessaryPartOfBinaryExpression:
|
||||
active: true
|
||||
UnnecessaryTemporaryInstantiation:
|
||||
active: true
|
||||
|
||||
potential-bugs:
|
||||
active: true
|
||||
AvoidReferentialEquality:
|
||||
active: true
|
||||
forbiddenTypePatterns:
|
||||
- 'kotlin.String'
|
||||
CastNullableToNonNullableType:
|
||||
active: true
|
||||
CastToNullableType:
|
||||
active: true
|
||||
Deprecation:
|
||||
active: true
|
||||
DontDowncastCollectionTypes:
|
||||
active: true
|
||||
DoubleMutabilityForCollection:
|
||||
active: true
|
||||
mutableTypes:
|
||||
- 'kotlin.collections.MutableList'
|
||||
- 'kotlin.collections.MutableMap'
|
||||
- 'kotlin.collections.MutableSet'
|
||||
- 'java.util.ArrayList'
|
||||
- 'java.util.LinkedHashSet'
|
||||
- 'java.util.HashSet'
|
||||
- 'java.util.LinkedHashMap'
|
||||
- 'java.util.HashMap'
|
||||
ElseCaseInsteadOfExhaustiveWhen:
|
||||
active: true
|
||||
ignoredSubjectTypes: []
|
||||
EqualsAlwaysReturnsTrueOrFalse:
|
||||
active: true
|
||||
EqualsWithHashCodeExist:
|
||||
active: true
|
||||
ExitOutsideMain:
|
||||
active: true
|
||||
ExplicitGarbageCollectionCall:
|
||||
active: true
|
||||
HasPlatformType:
|
||||
active: true
|
||||
IgnoredReturnValue:
|
||||
active: true
|
||||
restrictToConfig: true
|
||||
returnValueAnnotations:
|
||||
- 'CheckResult'
|
||||
- '*.CheckResult'
|
||||
- 'CheckReturnValue'
|
||||
- '*.CheckReturnValue'
|
||||
ignoreReturnValueAnnotations:
|
||||
- 'CanIgnoreReturnValue'
|
||||
- '*.CanIgnoreReturnValue'
|
||||
returnValueTypes:
|
||||
- 'kotlin.sequences.Sequence'
|
||||
- 'kotlinx.coroutines.flow.*Flow'
|
||||
- 'java.util.stream.*Stream'
|
||||
ignoreFunctionCall: []
|
||||
ImplicitDefaultLocale:
|
||||
active: true
|
||||
ImplicitUnitReturnType:
|
||||
active: true
|
||||
allowExplicitReturnType: true
|
||||
InvalidRange:
|
||||
active: true
|
||||
IteratorHasNextCallsNextMethod:
|
||||
active: true
|
||||
IteratorNotThrowingNoSuchElementException:
|
||||
active: true
|
||||
LateinitUsage:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
ignoreOnClassesPattern: ''
|
||||
MapGetWithNotNullAssertionOperator:
|
||||
active: true
|
||||
MissingPackageDeclaration:
|
||||
active: true
|
||||
excludes: ['**/*.kts']
|
||||
NullCheckOnMutableProperty:
|
||||
active: true
|
||||
NullableToStringCall:
|
||||
active: true
|
||||
PropertyUsedBeforeDeclaration:
|
||||
active: true
|
||||
UnconditionalJumpStatementInLoop:
|
||||
active: true
|
||||
UnnecessaryNotNullCheck:
|
||||
active: true
|
||||
UnnecessaryNotNullOperator:
|
||||
active: true
|
||||
UnnecessarySafeCall:
|
||||
active: true
|
||||
UnreachableCatchBlock:
|
||||
active: true
|
||||
UnreachableCode:
|
||||
active: true
|
||||
UnsafeCallOnNullableType:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
UnsafeCast:
|
||||
active: true
|
||||
UnusedUnaryOperator:
|
||||
active: true
|
||||
UselessPostfixExpression:
|
||||
active: true
|
||||
WrongEqualsTypeParameter:
|
||||
active: true
|
||||
|
||||
style:
|
||||
active: true
|
||||
AlsoCouldBeApply:
|
||||
active: true
|
||||
BracesOnIfStatements:
|
||||
active: true
|
||||
singleLine: 'never'
|
||||
multiLine: 'always'
|
||||
BracesOnWhenStatements:
|
||||
active: true
|
||||
singleLine: 'necessary'
|
||||
multiLine: 'consistent'
|
||||
CanBeNonNullable:
|
||||
active: true
|
||||
CascadingCallWrapping:
|
||||
active: true
|
||||
includeElvis: true
|
||||
ClassOrdering:
|
||||
active: true
|
||||
CollapsibleIfStatements:
|
||||
active: true
|
||||
DataClassContainsFunctions:
|
||||
active: true
|
||||
conversionFunctionPrefix:
|
||||
- 'to'
|
||||
allowOperators: true
|
||||
DataClassShouldBeImmutable:
|
||||
active: true
|
||||
DestructuringDeclarationWithTooManyEntries:
|
||||
active: true
|
||||
maxDestructuringEntries: 3
|
||||
DoubleNegativeLambda:
|
||||
active: true
|
||||
negativeFunctions:
|
||||
- reason: 'Use `takeIf` instead.'
|
||||
value: 'takeUnless'
|
||||
- reason: 'Use `all` instead.'
|
||||
value: 'none'
|
||||
negativeFunctionNameParts:
|
||||
- 'not'
|
||||
- 'non'
|
||||
EqualsNullCall:
|
||||
active: true
|
||||
EqualsOnSignatureLine:
|
||||
active: true
|
||||
ExplicitCollectionElementAccessMethod:
|
||||
active: true
|
||||
ExplicitItLambdaParameter:
|
||||
active: true
|
||||
ExpressionBodySyntax:
|
||||
active: true
|
||||
includeLineWrapping: true
|
||||
ForbiddenAnnotation:
|
||||
active: true
|
||||
annotations:
|
||||
- reason: 'it is a java annotation. Use `Suppress` instead.'
|
||||
value: 'java.lang.SuppressWarnings'
|
||||
- reason: 'it is a java annotation. Use `kotlin.Deprecated` instead.'
|
||||
value: 'java.lang.Deprecated'
|
||||
- reason: 'it is a java annotation. Use `kotlin.annotation.MustBeDocumented` instead.'
|
||||
value: 'java.lang.annotation.Documented'
|
||||
- reason: 'it is a java annotation. Use `kotlin.annotation.Target` instead.'
|
||||
value: 'java.lang.annotation.Target'
|
||||
- reason: 'it is a java annotation. Use `kotlin.annotation.Retention` instead.'
|
||||
value: 'java.lang.annotation.Retention'
|
||||
- reason: 'it is a java annotation. Use `kotlin.annotation.Repeatable` instead.'
|
||||
value: 'java.lang.annotation.Repeatable'
|
||||
- reason: 'Kotlin does not support @Inherited annotation, see https://youtrack.jetbrains.com/issue/KT-22265'
|
||||
value: 'java.lang.annotation.Inherited'
|
||||
ForbiddenComment:
|
||||
active: true
|
||||
comments:
|
||||
- reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
|
||||
value: 'FIXME:'
|
||||
- reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
|
||||
value: 'STOPSHIP:'
|
||||
- reason: 'Forbidden TODO todo marker in comment, please do the changes.'
|
||||
value: 'TODO:'
|
||||
allowedPatterns: ''
|
||||
ForbiddenImport:
|
||||
active: true
|
||||
imports: []
|
||||
forbiddenPatterns: ''
|
||||
ForbiddenMethodCall:
|
||||
active: true
|
||||
methods:
|
||||
- reason: 'print does not allow you to configure the output stream. Use a logger instead.'
|
||||
value: 'kotlin.io.print'
|
||||
- reason: 'println does not allow you to configure the output stream. Use a logger instead.'
|
||||
value: 'kotlin.io.println'
|
||||
ForbiddenSuppress:
|
||||
active: true
|
||||
rules: []
|
||||
ForbiddenVoid:
|
||||
active: true
|
||||
ignoreOverridden: true
|
||||
ignoreUsageInGenerics: true
|
||||
FunctionOnlyReturningConstant:
|
||||
active: true
|
||||
ignoreOverridableFunction: true
|
||||
ignoreActualFunction: true
|
||||
excludedFunctions: []
|
||||
LoopWithTooManyJumpStatements:
|
||||
active: true
|
||||
maxJumpCount: 1
|
||||
MagicNumber:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/androidUnitTest/**', '**/androidInstrumentedTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts']
|
||||
ignoreNumbers:
|
||||
- '-1'
|
||||
- '0'
|
||||
- '1'
|
||||
- '2'
|
||||
ignoreHashCodeFunction: true
|
||||
ignorePropertyDeclaration: true
|
||||
ignoreLocalVariableDeclaration: true
|
||||
ignoreConstantDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ignoreAnnotation: true
|
||||
ignoreNamedArgument: true
|
||||
ignoreEnums: true
|
||||
ignoreRanges: true
|
||||
ignoreExtensionFunctions: true
|
||||
MandatoryBracesLoops:
|
||||
active: true
|
||||
MaxChainedCallsOnSameLine:
|
||||
active: true
|
||||
maxChainedCalls: 5
|
||||
MaxLineLength:
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
excludePackageStatements: true
|
||||
excludeImportStatements: true
|
||||
excludeCommentStatements: true
|
||||
excludeRawStrings: true
|
||||
MayBeConst:
|
||||
active: true
|
||||
ModifierOrder:
|
||||
active: true
|
||||
MultilineLambdaItParameter:
|
||||
active: true
|
||||
MultilineRawStringIndentation:
|
||||
active: false
|
||||
indentSize: 4
|
||||
trimmingMethods:
|
||||
- 'trimIndent'
|
||||
- 'trimMargin'
|
||||
NestedClassesVisibility:
|
||||
active: true
|
||||
NewLineAtEndOfFile:
|
||||
active: true
|
||||
NoTabs:
|
||||
active: true
|
||||
NullableBooleanCheck:
|
||||
active: true
|
||||
ObjectLiteralToLambda:
|
||||
active: true
|
||||
OptionalAbstractKeyword:
|
||||
active: true
|
||||
OptionalUnit:
|
||||
active: true
|
||||
PreferToOverPairSyntax:
|
||||
active: true
|
||||
ProtectedMemberInFinalClass:
|
||||
active: true
|
||||
RedundantExplicitType:
|
||||
active: true
|
||||
RedundantHigherOrderMapUsage:
|
||||
active: true
|
||||
RedundantVisibilityModifierRule:
|
||||
active: true
|
||||
ReturnCount:
|
||||
active: true
|
||||
max: 2
|
||||
excludedFunctions:
|
||||
- 'equals'
|
||||
excludeLabeled: true
|
||||
excludeReturnFromLambda: true
|
||||
excludeGuardClauses: true
|
||||
SafeCast:
|
||||
active: true
|
||||
SerialVersionUIDInSerializableClass:
|
||||
active: true
|
||||
SpacingBetweenPackageAndImports:
|
||||
active: true
|
||||
StringShouldBeRawString:
|
||||
active: true
|
||||
maxEscapedCharacterCount: 2
|
||||
ignoredCharacters: []
|
||||
ThrowsCount:
|
||||
active: true
|
||||
max: 2
|
||||
excludeGuardClauses: true
|
||||
TrailingWhitespace:
|
||||
active: true
|
||||
TrimMultilineRawString:
|
||||
active: false
|
||||
trimmingMethods:
|
||||
- 'trimIndent'
|
||||
- 'trimMargin'
|
||||
UnderscoresInNumericLiterals:
|
||||
active: true
|
||||
acceptableLength: 4
|
||||
allowNonStandardGrouping: true
|
||||
UnnecessaryAbstractClass:
|
||||
active: true
|
||||
UnnecessaryAnnotationUseSiteTarget:
|
||||
active: true
|
||||
UnnecessaryApply:
|
||||
active: true
|
||||
UnnecessaryBackticks:
|
||||
active: true
|
||||
UnnecessaryBracesAroundTrailingLambda:
|
||||
active: true
|
||||
UnnecessaryFilter:
|
||||
active: true
|
||||
UnnecessaryInheritance:
|
||||
active: true
|
||||
UnnecessaryInnerClass:
|
||||
active: true
|
||||
UnnecessaryLet:
|
||||
active: true
|
||||
UnnecessaryParentheses:
|
||||
active: true
|
||||
allowForUnclearPrecedence: true
|
||||
UntilInsteadOfRangeTo:
|
||||
active: true
|
||||
UnusedImports:
|
||||
active: true
|
||||
UnusedParameter:
|
||||
active: true
|
||||
allowedNames: 'ignored|expected'
|
||||
UnusedPrivateClass:
|
||||
active: true
|
||||
UnusedPrivateMember:
|
||||
active: true
|
||||
allowedNames: ''
|
||||
UnusedPrivateProperty:
|
||||
active: true
|
||||
allowedNames: '_|ignored|expected|serialVersionUID'
|
||||
UseAnyOrNoneInsteadOfFind:
|
||||
active: true
|
||||
UseArrayLiteralsInAnnotations:
|
||||
active: true
|
||||
UseCheckNotNull:
|
||||
active: true
|
||||
UseCheckOrError:
|
||||
active: true
|
||||
UseDataClass:
|
||||
active: true
|
||||
allowVars: true
|
||||
UseEmptyCounterpart:
|
||||
active: true
|
||||
UseIfEmptyOrIfBlank:
|
||||
active: true
|
||||
UseIfInsteadOfWhen:
|
||||
active: true
|
||||
ignoreWhenContainingVariableDeclaration: true
|
||||
UseIsNullOrEmpty:
|
||||
active: true
|
||||
UseLet:
|
||||
active: true
|
||||
UseOrEmpty:
|
||||
active: true
|
||||
UseRequire:
|
||||
active: true
|
||||
UseRequireNotNull:
|
||||
active: true
|
||||
UseSumOfInsteadOfFlatMapSize:
|
||||
active: true
|
||||
UselessCallOnNotNull:
|
||||
active: true
|
||||
UtilityClassWithPublicConstructor:
|
||||
active: true
|
||||
VarCouldBeVal:
|
||||
active: true
|
||||
ignoreLateinitVar: true
|
||||
WildcardImport:
|
||||
active: true
|
||||
excludeImports:
|
||||
- 'java.util.*'
|
||||
27
app/proguard-rules.pro
vendored
Normal file
27
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-dontobfuscate
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn com.google.errorprone.annotations.Immutable
|
||||
-dontwarn javax.annotation.concurrent.GuardedBy
|
||||
-dontwarn javax.annotation.Nullable
|
||||
157
app/src/main/AndroidManifest.xml
Normal file
157
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="preferExternal">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<queries>
|
||||
<package android:name="com.philips.lighting.hue2" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".Application"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Material3.DayNight"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules">
|
||||
<activity
|
||||
android:name=".activities.HueSceneActivity"
|
||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar"
|
||||
android:parentActivityName=".activities.MainActivity" />
|
||||
<activity
|
||||
android:name=".activities.AboutActivity"
|
||||
android:label="@string/about"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.LibraryActivity"
|
||||
android:label="@string/about_libraries"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".activities.AboutActivity" />
|
||||
<activity
|
||||
android:name=".activities.SearchDevicesActivity"
|
||||
android:label="@string/pref_add"
|
||||
android:parentActivityName=".activities.DevicesActivity" />
|
||||
<activity
|
||||
android:name=".activities.HueConnectActivity"
|
||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.HueLampActivity"
|
||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar"
|
||||
android:parentActivityName=".activities.MainActivity" />
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/pref"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".activities.MainActivity" />
|
||||
<activity
|
||||
android:name=".activities.WebActivity"
|
||||
android:label="@string/webView"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:parentActivityName=".activities.MainActivity" />
|
||||
<activity
|
||||
android:name=".activities.DevicesActivity"
|
||||
android:label="@string/pref"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.EditDeviceActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar"
|
||||
android:parentActivityName=".activities.DevicesActivity" />
|
||||
<activity
|
||||
android:name=".activities.DeviceInfoActivity"
|
||||
android:label="@string/device_config_info"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".activities.EditDeviceActivity" />
|
||||
<activity
|
||||
android:name=".activities.ControlInfoActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/Theme.Material3.DayNight.NoActionBar" />
|
||||
<activity
|
||||
android:name=".activities.ShortcutDeviceActivity"
|
||||
android:label="@string/pref_add_name_empty"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.ShortcutHueRoomActivity"
|
||||
android:label="@string/hue_room"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.ShortcutHueSceneActivity"
|
||||
android:label="@string/hue_scene_shortcut"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.ShortcutHueSceneActionActivity"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:exported="true" />
|
||||
<activity
|
||||
android:name=".activities.ShortcutTasmotaActivity"
|
||||
android:label="@string/tasmota"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.ShortcutTasmotaActionActivity"
|
||||
android:taskAffinity=""
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:exported="true" />
|
||||
<service
|
||||
android:name=".services.ControlService"
|
||||
android:permission="android.permission.BIND_CONTROLS"
|
||||
android:exported="true"
|
||||
tools:targetApi="30">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.controls.ControlsProviderService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher-playstore.png
Normal file
BIN
app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
110
app/src/main/java/com/rine/upnpdiscovery/UPnPDevice.kt
Normal file
110
app/src/main/java/com/rine/upnpdiscovery/UPnPDevice.kt
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package com.rine.upnpdiscovery
|
||||
|
||||
import android.util.Log
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import org.xmlpull.v1.XmlPullParserFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
class UPnPDevice internal constructor(val hostAddress: String, header: String) {
|
||||
internal val location: String
|
||||
val server: String
|
||||
|
||||
// XML content
|
||||
private var descriptionXML: String = ""
|
||||
|
||||
// From description XML
|
||||
var friendlyName: String = ""
|
||||
private var deviceType: String = ""
|
||||
private var presentationURL: String = ""
|
||||
private var serialNumber: String = ""
|
||||
private var modelName: String = ""
|
||||
private var modelNumber: String = ""
|
||||
private var modelURL: String = ""
|
||||
private var manufacturer: String = ""
|
||||
private var manufacturerURL: String = ""
|
||||
private var udn: String = ""
|
||||
private var urlBase: String = ""
|
||||
|
||||
init {
|
||||
location = parseHeader(header, "LOCATION: ")
|
||||
server = parseHeader(header, "SERVER: ")
|
||||
}
|
||||
|
||||
internal fun update(xml: String) {
|
||||
descriptionXML = xml
|
||||
try {
|
||||
xmlParse(xml)
|
||||
} catch (e: XmlPullParserException) {
|
||||
Log.w(UPnPDiscovery.TAG, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String =
|
||||
"FriendlyName: " + friendlyName + LINE_END +
|
||||
"ModelName: " + modelName + LINE_END +
|
||||
"HostAddress: " + hostAddress + LINE_END +
|
||||
"Location: " + location + LINE_END +
|
||||
"DeviceType: " + deviceType + LINE_END +
|
||||
"PresentationURL: " + presentationURL + LINE_END +
|
||||
"SerialNumber: " + serialNumber + LINE_END +
|
||||
"ModelURL: " + modelURL + LINE_END +
|
||||
"ModelNumber: " + modelNumber + LINE_END +
|
||||
"Manufacturer: " + manufacturer + LINE_END +
|
||||
"ManufacturerURL: " + manufacturerURL + LINE_END +
|
||||
"UDN: " + udn + LINE_END +
|
||||
"URLBase: " + urlBase
|
||||
|
||||
private fun parseHeader(
|
||||
mSearchAnswer: String,
|
||||
whatSearch: String,
|
||||
): String {
|
||||
var result = ""
|
||||
var searchLinePos = mSearchAnswer.indexOf(whatSearch)
|
||||
if (searchLinePos != -1) {
|
||||
searchLinePos += whatSearch.length
|
||||
val locColon = mSearchAnswer.indexOf(LINE_END, searchLinePos)
|
||||
result = mSearchAnswer.substring(searchLinePos, locColon)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun readText(parser: XmlPullParser): String {
|
||||
var result = ""
|
||||
if (parser.next() == XmlPullParser.TEXT) {
|
||||
result = parser.text
|
||||
parser.nextTag()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun xmlParse(xml: String) {
|
||||
val xmlFactoryObject = XmlPullParserFactory.newInstance()
|
||||
val parser = xmlFactoryObject.newPullParser()
|
||||
parser.setInput(ByteArrayInputStream(xml.toByteArray(Charsets.UTF_8)), null)
|
||||
var event = parser.eventType
|
||||
while (event != XmlPullParser.END_DOCUMENT) {
|
||||
val name = parser.name
|
||||
if (event == XmlPullParser.START_TAG) {
|
||||
when (name) {
|
||||
"friendlyName" -> friendlyName = readText(parser)
|
||||
"deviceType" -> deviceType = readText(parser)
|
||||
"presentationURL" -> presentationURL = readText(parser)
|
||||
"serialNumber" -> serialNumber = readText(parser)
|
||||
"modelName" -> modelName = readText(parser)
|
||||
"modelNumber" -> modelNumber = readText(parser)
|
||||
"modelURL" -> modelURL = readText(parser)
|
||||
"manufacturer" -> manufacturer = readText(parser)
|
||||
"manufacturerURL" -> manufacturerURL = readText(parser)
|
||||
"UDN" -> udn = readText(parser)
|
||||
"URLBase" -> urlBase = readText(parser)
|
||||
}
|
||||
}
|
||||
event = parser.next()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LINE_END = "\r\n"
|
||||
}
|
||||
}
|
||||
178
app/src/main/java/com/rine/upnpdiscovery/UPnPDiscovery.kt
Normal file
178
app/src/main/java/com/rine/upnpdiscovery/UPnPDiscovery.kt
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package com.rine.upnpdiscovery
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.AsyncTask
|
||||
import android.util.Log
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.toolbox.StringRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import java.io.IOException
|
||||
import java.net.DatagramPacket
|
||||
import java.net.DatagramSocket
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
class UPnPDiscovery : AsyncTask<Activity, UPnPDiscovery.OnDiscoveryListener, Void> {
|
||||
private val devices = HashSet<UPnPDevice>()
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private val mContext: Context
|
||||
private var mThreadsCount: Int = 0
|
||||
private val mCustomQuery: String
|
||||
private val mInternetAddress: String
|
||||
private val mPort: Int
|
||||
|
||||
private val mListener: OnDiscoveryListener
|
||||
|
||||
interface OnDiscoveryListener {
|
||||
fun onStart()
|
||||
|
||||
fun onFoundNewDevice(device: UPnPDevice)
|
||||
|
||||
fun onFinish(devices: HashSet<UPnPDevice>)
|
||||
|
||||
fun onError(e: Exception)
|
||||
}
|
||||
|
||||
private constructor(activity: Activity, listener: OnDiscoveryListener) {
|
||||
mContext = activity.applicationContext
|
||||
mListener = listener
|
||||
mThreadsCount = 0
|
||||
mCustomQuery = DEFAULT_QUERY
|
||||
mInternetAddress = DEFAULT_ADDRESS
|
||||
mPort = 1900
|
||||
}
|
||||
|
||||
private constructor(
|
||||
activity: Activity,
|
||||
listener: OnDiscoveryListener,
|
||||
customQuery: String,
|
||||
address: String,
|
||||
port: Int,
|
||||
) {
|
||||
mContext = activity.applicationContext
|
||||
mListener = listener
|
||||
mThreadsCount = 0
|
||||
mCustomQuery = customQuery
|
||||
mInternetAddress = address
|
||||
mPort = port
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun doInBackground(vararg p0: Activity?): Void? {
|
||||
mListener.onStart()
|
||||
val wifi = mContext.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
|
||||
val lock = wifi.createMulticastLock("The Lock")
|
||||
lock.acquire()
|
||||
var socket: DatagramSocket? = null
|
||||
try {
|
||||
val group = InetAddress.getByName(mInternetAddress)
|
||||
val port = mPort
|
||||
val query = mCustomQuery
|
||||
socket = DatagramSocket(null)
|
||||
socket.reuseAddress = true
|
||||
socket.broadcast = true
|
||||
socket.bind(InetSocketAddress(port))
|
||||
|
||||
val datagramPacketRequest = DatagramPacket(query.toByteArray(), query.length, group, port)
|
||||
socket.send(datagramPacketRequest)
|
||||
|
||||
val time = System.currentTimeMillis()
|
||||
var curTime = System.currentTimeMillis()
|
||||
|
||||
while (curTime - time < 1000) {
|
||||
val datagramPacket = DatagramPacket(ByteArray(1024), 1024)
|
||||
socket.receive(datagramPacket)
|
||||
val response = String(datagramPacket.data, 0, datagramPacket.length)
|
||||
if (response.substring(0, 12).uppercase() == "HTTP/1.1 200") {
|
||||
val device = UPnPDevice(datagramPacket.address.hostAddress ?: continue, response)
|
||||
mThreadsCount++
|
||||
getData(device.location, device)
|
||||
}
|
||||
curTime = System.currentTimeMillis()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
mListener.onError(e)
|
||||
} finally {
|
||||
socket?.close()
|
||||
}
|
||||
lock.release()
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getData(
|
||||
url: String,
|
||||
device: UPnPDevice,
|
||||
) {
|
||||
val stringRequest =
|
||||
StringRequest(
|
||||
Request.Method.GET,
|
||||
url,
|
||||
{ response ->
|
||||
device.update(response)
|
||||
mListener.onFoundNewDevice(device)
|
||||
devices.add(device)
|
||||
mThreadsCount--
|
||||
if (mThreadsCount == 0) {
|
||||
mListener.onFinish(devices)
|
||||
}
|
||||
},
|
||||
{
|
||||
mThreadsCount--
|
||||
Log.e(TAG, "URL: $url get content error!")
|
||||
},
|
||||
)
|
||||
stringRequest.tag = TAG + "SSDP description request"
|
||||
Volley.newRequestQueue(mContext).add(stringRequest)
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal val TAG: String = UPnPDiscovery::class.java.simpleName
|
||||
|
||||
private const val DISCOVER_TIMEOUT = 1500
|
||||
private const val LINE_END = "\r\n"
|
||||
private const val DEFAULT_QUERY =
|
||||
"M-SEARCH * HTTP/1.1" + LINE_END +
|
||||
"HOST: 239.255.255.250:1900" + LINE_END +
|
||||
"MAN: \"ssdp:discover\"" + LINE_END +
|
||||
"MX: 1" + LINE_END +
|
||||
"ST: ssdp:all" + LINE_END +
|
||||
LINE_END
|
||||
private const val DEFAULT_ADDRESS = "239.255.255.250"
|
||||
|
||||
fun discoveryDevices(
|
||||
activity: Activity,
|
||||
listener: OnDiscoveryListener,
|
||||
): Boolean {
|
||||
val discover = UPnPDiscovery(activity, listener)
|
||||
discover.execute()
|
||||
return try {
|
||||
Thread.sleep(DISCOVER_TIMEOUT.toLong())
|
||||
true
|
||||
} catch (e: InterruptedException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun discoveryDevices(
|
||||
activity: Activity,
|
||||
listener: OnDiscoveryListener,
|
||||
customQuery: String,
|
||||
address: String,
|
||||
port: Int,
|
||||
): Boolean {
|
||||
val discover = UPnPDiscovery(activity, listener, customQuery, address, port)
|
||||
discover.execute()
|
||||
return try {
|
||||
Thread.sleep(DISCOVER_TIMEOUT.toLong())
|
||||
true
|
||||
} catch (e: InterruptedException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
app/src/main/java/io/github/domi04151309/home/Application.kt
Normal file
10
app/src/main/java/io/github/domi04151309/home/Application.kt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package io.github.domi04151309.home
|
||||
|
||||
import com.google.android.material.color.DynamicColors
|
||||
|
||||
class Application : android.app.Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.github.domi04151309.home.BuildConfig
|
||||
import io.github.domi04151309.home.R
|
||||
|
||||
class AboutActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, GeneralPreferenceFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
class GeneralPreferenceFragment : PreferenceFragmentCompat() {
|
||||
@Suppress("SameReturnValue")
|
||||
private fun onIconsClicked(): Boolean {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.about_icons)
|
||||
.setItems(resources.getStringArray(R.array.about_icons_array)) { _, which ->
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
when (which) {
|
||||
0 -> "https://icons8.com/"
|
||||
1 -> "https://fonts.google.com/icons?selected=Material+Icons"
|
||||
else -> "about:blank"
|
||||
}.toUri(),
|
||||
),
|
||||
)
|
||||
}
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
|
||||
@Suppress("SameReturnValue")
|
||||
private fun onExternalClicked(link: String): Boolean {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.about_privacy)
|
||||
.setMessage(R.string.about_privacy_desc)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
link.toUri(),
|
||||
),
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.setNeutralButton(R.string.about_privacy_policy) { _, _ ->
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"https://docs.github.com/en/github/site-policy/github-privacy-statement".toUri(),
|
||||
),
|
||||
)
|
||||
}
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
addPreferencesFromResource(R.xml.pref_about)
|
||||
findPreference<Preference>("app_version")?.apply {
|
||||
summary =
|
||||
requireContext().getString(
|
||||
R.string.about_app_version_desc,
|
||||
BuildConfig.VERSION_NAME,
|
||||
BuildConfig.VERSION_CODE,
|
||||
)
|
||||
setOnPreferenceClickListener {
|
||||
onExternalClicked("$REPOSITORY_URL/releases")
|
||||
}
|
||||
}
|
||||
findPreference<Preference>("github")?.apply {
|
||||
summary = REPOSITORY_URL
|
||||
setOnPreferenceClickListener {
|
||||
onExternalClicked(REPOSITORY_URL)
|
||||
}
|
||||
}
|
||||
findPreference<Preference>("license")?.setOnPreferenceClickListener {
|
||||
onExternalClicked("$REPOSITORY_URL/blob/$BRANCH/LICENSE")
|
||||
}
|
||||
findPreference<Preference>("icons")?.setOnPreferenceClickListener {
|
||||
onIconsClicked()
|
||||
}
|
||||
findPreference<Preference>("contributors")?.setOnPreferenceClickListener {
|
||||
onExternalClicked("$REPOSITORY_URL/graphs/contributors")
|
||||
}
|
||||
findPreference<Preference>("libraries")?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(requireContext(), LibraryActivity::class.java))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REPOSITORY: String = "Domi04151309/HomeApp"
|
||||
private const val BRANCH: String = "main"
|
||||
private const val REPOSITORY_URL: String = "https://github.com/$REPOSITORY"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import io.github.domi04151309.home.R
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (resources.configuration.uiMode.and(
|
||||
Configuration.UI_MODE_NIGHT_MASK,
|
||||
) != Configuration.UI_MODE_NIGHT_YES
|
||||
) {
|
||||
setTheme(R.style.LightStatusBarOverlay)
|
||||
}
|
||||
val color = SurfaceColors.SURFACE_2.getColor(this)
|
||||
window.statusBarColor = color
|
||||
window.navigationBarColor = color
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.fragments.ControlInfoFragment
|
||||
import io.github.domi04151309.home.fragments.HueColorFragment
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.interfaces.HueRoomInterface
|
||||
|
||||
class ControlInfoActivity : BaseActivity() {
|
||||
private var hueRoom: ControlInfoActivityHueRoom? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
|
||||
window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this)
|
||||
|
||||
val id = intent.getStringExtra(EXTRA_ID)
|
||||
if (id === null) {
|
||||
return
|
||||
}
|
||||
|
||||
val device = Devices(this).getDeviceById(id.substring(0, id.indexOf('@')))
|
||||
|
||||
if (device.mode == Global.HUE_API) {
|
||||
showHueFragment(id, device)
|
||||
return
|
||||
}
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, ControlInfoFragment(device, intent.getStringExtra(EXTRA_TITLE) ?: ""))
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
hueRoom?.onStart()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
hueRoom?.onStop()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
hueRoom?.onDestroy()
|
||||
}
|
||||
|
||||
private fun showHueFragment(
|
||||
id: String,
|
||||
device: DeviceItem,
|
||||
) {
|
||||
hueRoom = ControlInfoActivityHueRoom(this, device, id.substring(id.indexOf('@') + 1))
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, HueColorFragment(hueRoom as HueRoomInterface))
|
||||
.commit()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ID: String = "EXTRA_ID"
|
||||
const val EXTRA_TITLE: String = "EXTRA_TITLE"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Context
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.LightStates
|
||||
import io.github.domi04151309.home.helpers.HueLightListener
|
||||
import io.github.domi04151309.home.helpers.HueUtils.MIN_COLOR_TEMPERATURE
|
||||
import io.github.domi04151309.home.helpers.UpdateHandler
|
||||
import io.github.domi04151309.home.interfaces.HueRoomInterface
|
||||
import org.json.JSONArray
|
||||
|
||||
class ControlInfoActivityHueRoom : HueRoomInterface {
|
||||
override var lights: JSONArray?
|
||||
override var lampData: HueLightListener
|
||||
override var id: String
|
||||
override var device: DeviceItem
|
||||
override var addressPrefix: String
|
||||
override var canReceiveRequest: Boolean
|
||||
|
||||
private var updateDataRequest: JsonObjectRequest? = null
|
||||
private var updateHandler: UpdateHandler = UpdateHandler()
|
||||
|
||||
constructor(context: Context, device: DeviceItem, id: String) {
|
||||
val hueApi = HueAPI(context, device.id)
|
||||
val queue = Volley.newRequestQueue(context)
|
||||
|
||||
this.lights = null
|
||||
this.lampData = HueLightListener()
|
||||
this.id = id
|
||||
this.device = device
|
||||
this.addressPrefix = device.address + "api/" + hueApi.getUsername()
|
||||
this.canReceiveRequest = false
|
||||
|
||||
updateDataRequest = getUpdateRequest()
|
||||
updateHandler.setUpdateFunction {
|
||||
if (canReceiveRequest && hueApi.readyForRequest) {
|
||||
queue.add(updateDataRequest)
|
||||
}
|
||||
}
|
||||
|
||||
onStart()
|
||||
}
|
||||
|
||||
fun onStart() {
|
||||
canReceiveRequest = true
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
canReceiveRequest = false
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
updateHandler.stop()
|
||||
}
|
||||
|
||||
override fun onColorChanged(color: Int) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private fun getUpdateRequest() =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"$addressPrefix/groups/$id",
|
||||
null,
|
||||
{ response ->
|
||||
lights = response.getJSONArray("lights")
|
||||
val action = response.getJSONObject("action")
|
||||
val light = LightStates.Light()
|
||||
|
||||
light.ct =
|
||||
if (action.has("ct")) {
|
||||
action.getInt("ct") - MIN_COLOR_TEMPERATURE
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
|
||||
if (action.has("hue") && action.has("sat")) {
|
||||
light.hue = action.getInt("hue")
|
||||
light.sat = action.getInt("sat")
|
||||
} else {
|
||||
light.hue = -1
|
||||
light.sat = -1
|
||||
}
|
||||
|
||||
light.on = response.getJSONObject("state").getBoolean("any_on")
|
||||
|
||||
lampData.state = light
|
||||
},
|
||||
{
|
||||
canReceiveRequest = false
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.RequestQueue
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.SimpleListAdapter
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.api.HueAPIParser
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.SimpleListItem
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
import org.json.JSONObject
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class DeviceInfoActivity : BaseActivity(), RecyclerViewHelperInterface {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_devices)
|
||||
|
||||
val devices = Devices(this)
|
||||
val id = intent.getStringExtra(Devices.INTENT_EXTRA_DEVICE) ?: ""
|
||||
if (!devices.idExists(id)) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val device = devices.getDeviceById(id)
|
||||
val queue = Volley.newRequestQueue(this)
|
||||
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
|
||||
val items = mutableListOf<SimpleListItem>()
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
items.add(
|
||||
SimpleListItem(
|
||||
device.name,
|
||||
device.address,
|
||||
icon = device.iconId,
|
||||
),
|
||||
)
|
||||
|
||||
when (device.mode) {
|
||||
Global.HUE_API -> showHueInfo(device, queue, items, recyclerView)
|
||||
Global.SHELLY_GEN_2 -> showShelly2Info(device, queue, items, recyclerView)
|
||||
Global.SHELLY_GEN_3 -> showShelly2Info(device, queue, items, recyclerView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
private fun boolToString(bool: Boolean): String =
|
||||
resources.getString(
|
||||
if (bool) R.string.str_on else R.string.str_off,
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun rssiToPercent(rssi: Int): Int =
|
||||
if (rssi <= -100) {
|
||||
0
|
||||
} else if (rssi >= -50) {
|
||||
100
|
||||
} else {
|
||||
2 * (rssi + 100)
|
||||
}
|
||||
|
||||
private fun formatUptime(uptime: Long) =
|
||||
String.format(
|
||||
Locale.getDefault(),
|
||||
"%02d:%02d:%02d",
|
||||
TimeUnit.SECONDS.toHours(uptime),
|
||||
TimeUnit.SECONDS.toMinutes(uptime) -
|
||||
TimeUnit.HOURS.toMinutes(
|
||||
TimeUnit.SECONDS.toHours(
|
||||
uptime,
|
||||
),
|
||||
),
|
||||
TimeUnit.SECONDS.toSeconds(uptime) -
|
||||
TimeUnit.MINUTES.toSeconds(
|
||||
TimeUnit.SECONDS.toMinutes(
|
||||
uptime,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private fun showHueInfo(
|
||||
device: DeviceItem,
|
||||
queue: RequestQueue,
|
||||
items: MutableList<SimpleListItem>,
|
||||
recyclerView: RecyclerView,
|
||||
) {
|
||||
val hueAPI = HueAPI(this, device.id)
|
||||
val addressPrefix = device.address + "api/" + hueAPI.getUsername()
|
||||
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"$addressPrefix/config",
|
||||
null,
|
||||
{ response ->
|
||||
items.addAll(HueAPIParser.parseHueConfig(resources, response))
|
||||
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"$addressPrefix/sensors",
|
||||
null,
|
||||
{ innerResponse ->
|
||||
items.addAll(HueAPIParser.parseHueSensors(resources, innerResponse))
|
||||
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"$addressPrefix/lights",
|
||||
null,
|
||||
{ innerInnerResponse ->
|
||||
items.addAll(HueAPIParser.parseHueLights(resources, innerInnerResponse))
|
||||
recyclerView.adapter = SimpleListAdapter(items, this)
|
||||
},
|
||||
{ },
|
||||
),
|
||||
)
|
||||
},
|
||||
{ },
|
||||
),
|
||||
)
|
||||
},
|
||||
{ },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun parseShelly2Info(response: JSONObject) =
|
||||
listOf(
|
||||
SimpleListItem(summary = resources.getString(R.string.device_config_info_status)),
|
||||
SimpleListItem(
|
||||
(response.optJSONObject("wifi") ?: JSONObject()).run {
|
||||
optString("ssid") + " (" + rssiToPercent(optInt("rssi")) + " %)"
|
||||
},
|
||||
resources.getString(R.string.shelly_wifi),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
boolToString(
|
||||
(
|
||||
response.optJSONObject("mqtt")
|
||||
?: JSONObject()
|
||||
).optBoolean("connected"),
|
||||
),
|
||||
resources.getString(R.string.shelly_mqtt),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
boolToString(
|
||||
(
|
||||
response.optJSONObject("cloud")
|
||||
?: JSONObject()
|
||||
).optBoolean("connected"),
|
||||
),
|
||||
resources.getString(R.string.shelly_cloud),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
formatUptime((response.optJSONObject("sys") ?: JSONObject()).optLong("uptime")),
|
||||
resources.getString(R.string.shelly_uptime),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
(response.optJSONObject("sys") ?: JSONObject()).run {
|
||||
"${(optInt("fs_free") / optInt("fs_size").toFloat() * TO_PERCENT).toInt()} %"
|
||||
},
|
||||
resources.getString(R.string.shelly_storage),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
(response.optJSONObject("sys") ?: JSONObject()).run {
|
||||
"${(optInt("ram_free") / optInt("ram_size").toFloat() * TO_PERCENT).toInt()} %"
|
||||
},
|
||||
resources.getString(R.string.shelly_ram),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
resources.getString(
|
||||
if ((
|
||||
(
|
||||
response.optJSONObject("sys")
|
||||
?: JSONObject()
|
||||
).optJSONObject("available_updates")
|
||||
?: JSONObject()
|
||||
).has("stable")
|
||||
) {
|
||||
R.string.str_yes
|
||||
} else {
|
||||
R.string.str_no
|
||||
},
|
||||
),
|
||||
resources.getString(R.string.shelly_update),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
)
|
||||
|
||||
private fun showShelly2Info(
|
||||
device: DeviceItem,
|
||||
queue: RequestQueue,
|
||||
items: MutableList<SimpleListItem>,
|
||||
recyclerView: RecyclerView,
|
||||
) {
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
device.address + "rpc/Shelly.GetStatus",
|
||||
null,
|
||||
{ response ->
|
||||
items.addAll(parseShelly2Info(response))
|
||||
recyclerView.adapter = SimpleListAdapter(items, this)
|
||||
},
|
||||
{ },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TO_PERCENT = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.DeviceListAdapter
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.SimpleListItem
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterfaceAdvanced
|
||||
|
||||
class DevicesActivity : BaseActivity(), RecyclerViewHelperInterfaceAdvanced {
|
||||
private var reset = true
|
||||
private lateinit var devices: Devices
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var itemTouchHelper: ItemTouchHelper
|
||||
|
||||
private val itemTouchHelperCallback =
|
||||
object : ItemTouchHelper.Callback() {
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
): Int =
|
||||
if (
|
||||
viewHolder.adapterPosition == (recyclerView.adapter?.itemCount ?: -1) - 1
|
||||
) {
|
||||
makeMovementFlags(0, 0)
|
||||
} else {
|
||||
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
|
||||
}
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder,
|
||||
): Boolean {
|
||||
val adapter = recyclerView.adapter ?: return false
|
||||
return if (target.adapterPosition == adapter.itemCount - 1) {
|
||||
false
|
||||
} else {
|
||||
recyclerView.adapter?.notifyItemMoved(
|
||||
viewHolder.adapterPosition,
|
||||
target.adapterPosition,
|
||||
)
|
||||
devices.moveDevice(viewHolder.adapterPosition, target.adapterPosition)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun isLongPressDragEnabled(): Boolean = true
|
||||
|
||||
override fun onSwiped(
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
direction: Int,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun clearView(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
devices.saveChanges()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_devices)
|
||||
|
||||
devices = Devices(this)
|
||||
recyclerView = findViewById(R.id.recyclerView)
|
||||
itemTouchHelper = ItemTouchHelper(itemTouchHelperCallback)
|
||||
|
||||
itemTouchHelper.attachToRecyclerView(recyclerView)
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
}
|
||||
|
||||
private fun loadDevices() {
|
||||
val listItems: ArrayList<SimpleListItem> = ArrayList(devices.length)
|
||||
var currentDevice: DeviceItem
|
||||
for (i in 0 until devices.length) {
|
||||
currentDevice = devices.getDeviceByIndex(i)
|
||||
listItems +=
|
||||
SimpleListItem(
|
||||
title = currentDevice.name,
|
||||
summary =
|
||||
if (currentDevice.hide) {
|
||||
resources.getString(R.string.device_config_hidden) + " · " + currentDevice.address
|
||||
} else {
|
||||
currentDevice.address
|
||||
},
|
||||
hidden = "edit#${currentDevice.id}",
|
||||
icon = currentDevice.iconId,
|
||||
)
|
||||
}
|
||||
listItems +=
|
||||
SimpleListItem(
|
||||
title = resources.getString(R.string.pref_add),
|
||||
summary = resources.getString(R.string.pref_add_summary),
|
||||
hidden = "add",
|
||||
icon = R.drawable.ic_add,
|
||||
)
|
||||
|
||||
recyclerView.adapter = DeviceListAdapter(listItems, this)
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
) {
|
||||
val action = view.findViewById<TextView>(R.id.hidden).text
|
||||
if (action.contains("edit")) {
|
||||
reset = true
|
||||
startActivity(
|
||||
Intent(this, EditDeviceActivity::class.java)
|
||||
.putExtra("deviceId", action.substring(action.indexOf('#') + 1)),
|
||||
)
|
||||
} else if (action == "add") {
|
||||
reset = true
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.pref_add_method)
|
||||
.setItems(resources.getStringArray(R.array.pref_add_method_array)) { _, which ->
|
||||
if (which == 0) {
|
||||
startActivity(Intent(this, EditDeviceActivity::class.java))
|
||||
} else if (which == 1) {
|
||||
startActivity(Intent(this, SearchDevicesActivity::class.java))
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemHandleTouched(viewHolder: RecyclerView.ViewHolder) {
|
||||
itemTouchHelper.startDrag(viewHolder)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (reset) {
|
||||
reset = false
|
||||
loadDevices()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.IconSpinnerAdapter
|
||||
import io.github.domi04151309.home.custom.TextWatcher
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.helpers.DeviceSecrets
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
|
||||
class EditDeviceActivity : BaseActivity() {
|
||||
private lateinit var devices: Devices
|
||||
private lateinit var deviceId: String
|
||||
private lateinit var deviceSecrets: DeviceSecrets
|
||||
private lateinit var deviceIcon: ImageView
|
||||
private lateinit var nameText: TextView
|
||||
private lateinit var nameBox: TextInputLayout
|
||||
private lateinit var addressBox: TextInputLayout
|
||||
private lateinit var iconSpinner: AutoCompleteTextView
|
||||
private lateinit var modeSpinner: AutoCompleteTextView
|
||||
private lateinit var specialDivider: View
|
||||
private lateinit var specialSection: LinearLayout
|
||||
private lateinit var usernameBox: TextInputLayout
|
||||
private lateinit var passwordBox: TextInputLayout
|
||||
private lateinit var configHide: CheckBox
|
||||
private lateinit var configDirectView: CheckBox
|
||||
private lateinit var configButton: Button
|
||||
private lateinit var infoButton: Button
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_edit_device)
|
||||
|
||||
window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this)
|
||||
|
||||
devices = Devices(this)
|
||||
var deviceId = intent.getStringExtra("deviceId")
|
||||
val editing =
|
||||
if (deviceId == null) {
|
||||
deviceId = devices.generateNewId()
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
this.deviceId = deviceId
|
||||
|
||||
deviceSecrets = DeviceSecrets(this, deviceId)
|
||||
|
||||
deviceIcon = findViewById(R.id.deviceIcn)
|
||||
nameText = findViewById(R.id.nameTxt)
|
||||
nameBox = findViewById(R.id.nameBox)
|
||||
addressBox = findViewById(R.id.addressBox)
|
||||
iconSpinner = findViewById<TextInputLayout>(R.id.iconSpinner).editText as AutoCompleteTextView
|
||||
modeSpinner = findViewById<TextInputLayout>(R.id.modeSpinner).editText as AutoCompleteTextView
|
||||
specialDivider = findViewById(R.id.specialDivider)
|
||||
specialSection = findViewById(R.id.specialSection)
|
||||
usernameBox = findViewById(R.id.usernameBox)
|
||||
passwordBox = findViewById(R.id.passwordBox)
|
||||
configHide = findViewById(R.id.configHide)
|
||||
configDirectView = findViewById(R.id.configDirectView)
|
||||
configButton = findViewById(R.id.configBtn)
|
||||
infoButton = findViewById(R.id.infoBtn)
|
||||
|
||||
findViewById<TextView>(R.id.idTxt).text = resources.getString(R.string.pref_add_id, deviceId)
|
||||
|
||||
iconSpinner.addTextChangedListener(getIconTextWatcher())
|
||||
modeSpinner.addTextChangedListener(getModeTextWatcher(editing))
|
||||
nameBox.editText?.addTextChangedListener(getNameTextWatcher())
|
||||
|
||||
if (editing) {
|
||||
onEditDevice()
|
||||
} else {
|
||||
onCreateDevice()
|
||||
}
|
||||
|
||||
iconSpinner.setAdapter(IconSpinnerAdapter(resources.getStringArray(R.array.pref_icons)))
|
||||
modeSpinner.setAdapter(
|
||||
ArrayAdapter(
|
||||
this,
|
||||
R.layout.dropdown_item,
|
||||
resources.getStringArray(R.array.pref_add_mode_array),
|
||||
),
|
||||
)
|
||||
|
||||
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener {
|
||||
onFloatingActionButtonClicked()
|
||||
}
|
||||
|
||||
findViewById<MaterialToolbar>(R.id.toolbar).apply {
|
||||
setNavigationIcon(R.drawable.ic_arrow_back)
|
||||
setNavigationOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getIconTextWatcher() =
|
||||
TextWatcher {
|
||||
deviceIcon.setImageResource(Global.getIcon(it))
|
||||
}
|
||||
|
||||
private fun showExternalInfoBasedOnMode(mode: String) {
|
||||
configButton.visibility =
|
||||
if (HAS_CONFIG.contains(mode)) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
infoButton.visibility =
|
||||
if (HAS_INFO.contains(mode)) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexCondition")
|
||||
private fun getModeTextWatcher(editing: Boolean) =
|
||||
TextWatcher {
|
||||
val specialVisibility =
|
||||
if (
|
||||
it == Global.FRITZ_AUTO_LOGIN ||
|
||||
it == Global.GRAFANA_AUTO_LOGIN ||
|
||||
it == Global.PI_HOLE_AUTO_LOGIN ||
|
||||
it == Global.SHELLY_GEN_1
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
val usernameVisibility =
|
||||
if (
|
||||
it == Global.GRAFANA_AUTO_LOGIN ||
|
||||
it == Global.SHELLY_GEN_1
|
||||
) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
specialDivider.visibility = specialVisibility
|
||||
specialSection.visibility = specialVisibility
|
||||
usernameBox.visibility = usernameVisibility
|
||||
|
||||
if (SUPPORTS_DIRECT_VIEW.contains(it)) {
|
||||
configDirectView.isEnabled = true
|
||||
} else {
|
||||
configDirectView.isEnabled = false
|
||||
configDirectView.isChecked = false
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
showExternalInfoBasedOnMode(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNameTextWatcher() =
|
||||
TextWatcher {
|
||||
if (it == "") {
|
||||
nameText.text = resources.getString(R.string.pref_add_name_empty)
|
||||
} else {
|
||||
nameText.text = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEditDevice() {
|
||||
val device = devices.getDeviceById(deviceId)
|
||||
nameBox.editText?.setText(device.name)
|
||||
addressBox.editText?.setText(device.address)
|
||||
iconSpinner.setText(device.iconName)
|
||||
modeSpinner.setText(device.mode)
|
||||
usernameBox.editText?.setText(deviceSecrets.username)
|
||||
passwordBox.editText?.setText(deviceSecrets.password)
|
||||
configHide.isChecked = device.hide
|
||||
configDirectView.isChecked = device.directView
|
||||
|
||||
configButton.setOnClickListener {
|
||||
onConfigButtonClicked()
|
||||
}
|
||||
|
||||
infoButton.setOnClickListener {
|
||||
startActivity(Intent(this, DeviceInfoActivity::class.java).putExtra(Devices.INTENT_EXTRA_DEVICE, deviceId))
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.shortcutBtn).setOnClickListener {
|
||||
onShortcutButtonClicked(device)
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.deleteBtn).setOnClickListener {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.str_delete)
|
||||
.setMessage(R.string.pref_delete_device_question)
|
||||
.setPositiveButton(R.string.str_delete) { _, _ ->
|
||||
devices.deleteDevice(deviceId)
|
||||
finish()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onConfigButtonClicked() {
|
||||
when (modeSpinner.text.toString()) {
|
||||
Global.ESP_EASY, Global.SHELLY_GEN_1, Global.SHELLY_GEN_2, Global.SHELLY_GEN_3 -> {
|
||||
startActivity(
|
||||
Intent(this, WebActivity::class.java)
|
||||
.putExtra("URI", addressBox.editText?.text.toString())
|
||||
.putExtra("title", resources.getString(R.string.pref_device_config)),
|
||||
)
|
||||
}
|
||||
Global.NODE_RED -> {
|
||||
startActivity(
|
||||
Intent(this, WebActivity::class.java)
|
||||
.putExtra("URI", formatNodeREDAddress(addressBox.editText?.text.toString()))
|
||||
.putExtra("title", resources.getString(R.string.pref_device_config)),
|
||||
)
|
||||
}
|
||||
Global.HUE_API -> {
|
||||
val huePackageName = "com.philips.lighting.hue2"
|
||||
val launchIntent = packageManager.getLaunchIntentForPackage(huePackageName)
|
||||
if (launchIntent == null) {
|
||||
try {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"market://details?id=$huePackageName".toUri(),
|
||||
),
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.w(EditDeviceActivity::class.simpleName, e)
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"https://play.google.com/store/apps/details?id=$huePackageName".toUri(),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
startActivity(launchIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShortcutButtonClicked(device: DeviceItem) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val shortcutManager = this.getSystemService(ShortcutManager::class.java)
|
||||
if (shortcutManager != null) {
|
||||
if (shortcutManager.isRequestPinShortcutSupported) {
|
||||
val shortcut =
|
||||
ShortcutInfo.Builder(this, deviceId)
|
||||
.setShortLabel(
|
||||
device.name.ifEmpty {
|
||||
resources.getString(R.string.pref_add_name_empty)
|
||||
},
|
||||
)
|
||||
.setLongLabel(
|
||||
device.name.ifEmpty {
|
||||
resources.getString(R.string.pref_add_name_empty)
|
||||
},
|
||||
)
|
||||
.setIcon(Icon.createWithResource(this, device.iconId))
|
||||
.setIntent(
|
||||
Intent(this, MainActivity::class.java)
|
||||
.putExtra(Devices.INTENT_EXTRA_DEVICE, deviceId)
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
|
||||
)
|
||||
.build()
|
||||
shortcutManager.requestPinShortcut(shortcut, null)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.pref_add_shortcut_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.pref_add_shortcut_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCreateDevice() {
|
||||
iconSpinner.setText(resources.getStringArray(R.array.pref_icons)[0])
|
||||
modeSpinner.setText(resources.getStringArray(R.array.pref_add_mode_array)[0])
|
||||
findViewById<View>(R.id.editDivider).visibility = View.GONE
|
||||
findViewById<LinearLayout>(R.id.editSection).visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun onFloatingActionButtonClicked() {
|
||||
val name = nameBox.editText?.text.toString()
|
||||
if (name == "") {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.err_missing_name)
|
||||
.setMessage(R.string.err_missing_name_summary)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
return
|
||||
} else if (addressBox.editText?.text.toString() == "") {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.err_missing_address)
|
||||
.setMessage(R.string.err_missing_address_summary)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
return
|
||||
}
|
||||
|
||||
val tempAddress =
|
||||
if (modeSpinner.text.toString() == Global.NODE_RED) {
|
||||
formatNodeREDAddress(addressBox.editText?.text.toString())
|
||||
} else {
|
||||
addressBox.editText?.text.toString()
|
||||
}
|
||||
|
||||
val newItem =
|
||||
DeviceItem(
|
||||
deviceId,
|
||||
name,
|
||||
modeSpinner.text.toString(),
|
||||
iconSpinner.text.toString(),
|
||||
configHide.isChecked,
|
||||
configDirectView.isChecked,
|
||||
)
|
||||
newItem.address = tempAddress
|
||||
devices.addDevice(newItem)
|
||||
deviceSecrets.username = usernameBox.editText?.text.toString()
|
||||
deviceSecrets.password = passwordBox.editText?.text.toString()
|
||||
deviceSecrets.updateDeviceSecrets()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun formatNodeREDAddress(url: String): String {
|
||||
var result = url
|
||||
if (!result.contains(":1880")) {
|
||||
if (result.endsWith('/')) result = result.dropLast(1)
|
||||
result += ":1880/"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val SUPPORTS_DIRECT_VIEW =
|
||||
arrayOf(
|
||||
Global.ESP_EASY,
|
||||
Global.HUE_API,
|
||||
Global.SHELLY_GEN_1,
|
||||
Global.SHELLY_GEN_2,
|
||||
Global.SHELLY_GEN_3,
|
||||
Global.SIMPLE_HOME_API,
|
||||
Global.TASMOTA,
|
||||
)
|
||||
private val HAS_CONFIG =
|
||||
arrayOf(
|
||||
Global.HUE_API,
|
||||
Global.ESP_EASY,
|
||||
Global.NODE_RED,
|
||||
Global.SHELLY_GEN_1,
|
||||
Global.SHELLY_GEN_2,
|
||||
Global.SHELLY_GEN_3,
|
||||
)
|
||||
private val HAS_INFO =
|
||||
arrayOf(
|
||||
Global.HUE_API,
|
||||
Global.SHELLY_GEN_2,
|
||||
Global.SHELLY_GEN_3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Button
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.RequestQueue
|
||||
import com.android.volley.toolbox.Volley
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.custom.CustomJsonArrayRequest
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.UpdateHandler
|
||||
import org.json.JSONObject
|
||||
|
||||
class HueConnectActivity : BaseActivity() {
|
||||
private val updateHandler = UpdateHandler()
|
||||
private var success = false
|
||||
private lateinit var queue: RequestQueue
|
||||
private lateinit var requestToRegisterUser: CustomJsonArrayRequest
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_hue_connect)
|
||||
|
||||
queue = Volley.newRequestQueue(this)
|
||||
val deviceId = intent.getStringExtra("deviceId") ?: ""
|
||||
val jsonRequestObject = JSONObject("""{ "devicetype": "Home App#${android.os.Build.PRODUCT}" }""")
|
||||
requestToRegisterUser =
|
||||
CustomJsonArrayRequest(
|
||||
Request.Method.POST, Devices(this).getDeviceById(deviceId).address + "api", jsonRequestObject,
|
||||
{ response ->
|
||||
val responseObject = response.getJSONObject(0)
|
||||
if (responseObject.has("success") && !success) {
|
||||
success = true
|
||||
val username = responseObject.getJSONObject("success").getString("username")
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit {
|
||||
putString(
|
||||
deviceId,
|
||||
username,
|
||||
)
|
||||
}
|
||||
startActivity(
|
||||
Intent(this, MainActivity::class.java)
|
||||
.putExtra(Devices.INTENT_EXTRA_DEVICE, deviceId)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
|
||||
)
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
Toast.makeText(this, R.string.err, Toast.LENGTH_LONG).show()
|
||||
Log.e(Global.LOG_TAG, error.toString())
|
||||
},
|
||||
)
|
||||
|
||||
findViewById<Button>(R.id.cancel_btn).setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
updateHandler.setUpdateFunction {
|
||||
if (!success) queue.add(requestToRegisterUser)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
updateHandler.stop()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.RequestQueue
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.HueDetailsTabAdapter
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.LightStates
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.HueLightListener
|
||||
import io.github.domi04151309.home.helpers.HueUtils
|
||||
import io.github.domi04151309.home.helpers.HueUtils.MIN_COLOR_TEMPERATURE
|
||||
import io.github.domi04151309.home.helpers.SliderUtils
|
||||
import io.github.domi04151309.home.helpers.UpdateHandler
|
||||
import io.github.domi04151309.home.interfaces.HueRoomInterface
|
||||
import org.json.JSONArray
|
||||
|
||||
class HueLampActivity : BaseActivity(), HueRoomInterface, Toolbar.OnMenuItemClickListener {
|
||||
override var addressPrefix: String = ""
|
||||
override var id: String = ""
|
||||
override var lights: JSONArray? = null
|
||||
override var canReceiveRequest: Boolean = false
|
||||
override var lampData: HueLightListener = HueLightListener()
|
||||
override lateinit var device: DeviceItem
|
||||
|
||||
private var lampName: String = ""
|
||||
private var updateDataRequest: JsonObjectRequest? = null
|
||||
private var updateHandler: UpdateHandler = UpdateHandler()
|
||||
|
||||
private lateinit var hueAPI: HueAPI
|
||||
private lateinit var queue: RequestQueue
|
||||
private lateinit var lampIcon: ImageView
|
||||
private lateinit var nameText: TextView
|
||||
private lateinit var brightnessText: TextView
|
||||
private lateinit var brightnessBar: Slider
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_hue_lamp)
|
||||
|
||||
window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this)
|
||||
|
||||
id = intent.getStringExtra("id") ?: "0"
|
||||
if (intent.hasExtra(Devices.INTENT_EXTRA_DEVICE)) {
|
||||
val extraDevice = intent.getStringExtra(Devices.INTENT_EXTRA_DEVICE) ?: ""
|
||||
val devices = Devices(this)
|
||||
if (devices.idExists(extraDevice)) {
|
||||
device = devices.getDeviceById(extraDevice)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.main_device_nonexistent, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hueAPI = HueAPI(this, device.id)
|
||||
addressPrefix = device.address + "api/" + hueAPI.getUsername()
|
||||
queue = Volley.newRequestQueue(this)
|
||||
lampIcon = findViewById(R.id.lampIcon)
|
||||
nameText = findViewById(R.id.nameTxt)
|
||||
brightnessText = findViewById(R.id.briTxt)
|
||||
brightnessBar = findViewById(R.id.briBar)
|
||||
|
||||
setupViews()
|
||||
|
||||
val viewPager = findViewById<ViewPager2>(R.id.viewPager)
|
||||
viewPager.isUserInputEnabled = false
|
||||
viewPager.adapter = HueDetailsTabAdapter(this, this)
|
||||
viewPager.setCurrentItem(1, false)
|
||||
|
||||
val tabIcons =
|
||||
arrayOf(
|
||||
ResourcesCompat.getDrawable(resources, R.drawable.ic_color_palette, theme),
|
||||
ResourcesCompat.getDrawable(resources, R.drawable.ic_scene_white, theme),
|
||||
ResourcesCompat.getDrawable(resources, R.drawable.ic_device_lamp, theme),
|
||||
)
|
||||
TabLayoutMediator(findViewById(R.id.tabBar), viewPager) { tab, position ->
|
||||
tab.icon = tabIcons[position]
|
||||
}.attach()
|
||||
|
||||
updateDataRequest = getUpdateRequest()
|
||||
updateHandler.setUpdateFunction {
|
||||
if (canReceiveRequest && hueAPI.readyForRequest) {
|
||||
queue.add(updateDataRequest)
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<MaterialToolbar>(R.id.toolbar).apply {
|
||||
setNavigationIcon(R.drawable.ic_arrow_back)
|
||||
setNavigationOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
inflateMenu(R.menu.activity_hue_lamp_actions)
|
||||
setOnMenuItemClickListener(this@HueLampActivity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
// Slider labels
|
||||
brightnessBar.setLabelFormatter { value: Float ->
|
||||
HueUtils.briToPercent(value.toInt())
|
||||
}
|
||||
|
||||
// Lamp tint
|
||||
ImageViewCompat.setImageTintList(
|
||||
lampIcon,
|
||||
ColorStateList.valueOf(Color.WHITE),
|
||||
)
|
||||
lampData.addOnDataChangedListener {
|
||||
ImageViewCompat.setImageTintList(
|
||||
lampIcon,
|
||||
ColorStateList.valueOf(
|
||||
if (it.hue != -1 && it.sat != -1) {
|
||||
HueUtils.hueSatToRGB(it.hue, it.sat)
|
||||
} else if (it.ct != -1) {
|
||||
HueUtils.ctToRGB(it.ct + MIN_COLOR_TEMPERATURE)
|
||||
} else {
|
||||
Color.WHITE
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.onBtn).setOnClickListener {
|
||||
hueAPI.switchGroupById(id, true)
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.offBtn).setOnClickListener {
|
||||
hueAPI.switchGroupById(id, false)
|
||||
}
|
||||
|
||||
brightnessBar.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
canReceiveRequest = false
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
hueAPI.changeBrightnessOfGroup(id, slider.value.toInt())
|
||||
canReceiveRequest = true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUpdateRequest() =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"$addressPrefix/groups/$id",
|
||||
null,
|
||||
{ response ->
|
||||
lights = response.getJSONArray("lights")
|
||||
lampName = response.getString("name")
|
||||
nameText.text = lampName
|
||||
val action = response.getJSONObject("action")
|
||||
val light = LightStates.Light()
|
||||
|
||||
if (action.has("bri")) {
|
||||
SliderUtils.setProgress(brightnessBar, action.getInt("bri"))
|
||||
} else {
|
||||
brightnessText.visibility = View.GONE
|
||||
brightnessBar.visibility = View.GONE
|
||||
}
|
||||
light.ct =
|
||||
if (action.has("ct")) {
|
||||
action.getInt("ct") - MIN_COLOR_TEMPERATURE
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
|
||||
if (action.has("hue") && action.has("sat")) {
|
||||
light.hue = action.getInt("hue")
|
||||
light.sat = action.getInt("sat")
|
||||
} else {
|
||||
light.hue = -1
|
||||
light.sat = -1
|
||||
}
|
||||
|
||||
light.on = response.getJSONObject("state").getBoolean("any_on")
|
||||
brightnessBar.isEnabled = light.on
|
||||
|
||||
lampData.state = light
|
||||
},
|
||||
{
|
||||
canReceiveRequest = false
|
||||
updateHandler.stop()
|
||||
finish()
|
||||
},
|
||||
)
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
canReceiveRequest = true
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
canReceiveRequest = false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
updateHandler.stop()
|
||||
}
|
||||
|
||||
override fun onColorChanged(color: Int) {
|
||||
ImageViewCompat.setImageTintList(
|
||||
lampIcon,
|
||||
ColorStateList.valueOf(color),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
if (item.itemId != R.id.action_add_shortcut) return super.onOptionsItemSelected(item)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val shortcutManager = getSystemService(ShortcutManager::class.java) ?: return true
|
||||
if (!shortcutManager.isRequestPinShortcutSupported) {
|
||||
Toast.makeText(this, R.string.pref_add_shortcut_failed, Toast.LENGTH_LONG).show()
|
||||
return true
|
||||
}
|
||||
val shortcut =
|
||||
ShortcutInfo.Builder(this, device.id + lampName)
|
||||
.setShortLabel(lampName)
|
||||
.setLongLabel(lampName)
|
||||
.setIcon(Icon.createWithResource(this, device.iconId))
|
||||
.setIntent(
|
||||
Intent(this, HueLampActivity::class.java)
|
||||
.putExtra("id", id)
|
||||
.putExtra(Devices.INTENT_EXTRA_DEVICE, device.id)
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
|
||||
)
|
||||
.build()
|
||||
shortcutManager.requestPinShortcut(shortcut, null)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.pref_add_shortcut_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,362 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.RequestQueue
|
||||
import com.android.volley.Response
|
||||
import com.android.volley.VolleyError
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.HueSceneLampListAdapter
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.custom.CustomJsonArrayRequest
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.LightStates
|
||||
import io.github.domi04151309.home.data.SceneListItem
|
||||
import io.github.domi04151309.home.fragments.HueColorSheet
|
||||
import io.github.domi04151309.home.fragments.HueScenesFragment
|
||||
import io.github.domi04151309.home.helpers.ColorUtils
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.HueUtils
|
||||
import io.github.domi04151309.home.helpers.HueUtils.MAX_BRIGHTNESS
|
||||
import io.github.domi04151309.home.helpers.SliderUtils
|
||||
import io.github.domi04151309.home.interfaces.HueAdvancedLampInterface
|
||||
import io.github.domi04151309.home.interfaces.SceneRecyclerViewHelperInterface
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class HueSceneActivity :
|
||||
BaseActivity(),
|
||||
SceneRecyclerViewHelperInterface,
|
||||
HueAdvancedLampInterface,
|
||||
Response.Listener<JSONArray>,
|
||||
Response.ErrorListener {
|
||||
private var editing = false
|
||||
private val lightStates = LightStates()
|
||||
private val listItems = mutableListOf<SceneListItem>()
|
||||
private var groupId = "0"
|
||||
private var sceneId = ""
|
||||
private var defaultText = ""
|
||||
private lateinit var hueAPI: HueAPI
|
||||
private lateinit var adapter: HueSceneLampListAdapter
|
||||
private lateinit var queue: RequestQueue
|
||||
private lateinit var nameBox: TextInputLayout
|
||||
private lateinit var briBar: Slider
|
||||
|
||||
override var id: String = ""
|
||||
override var canReceiveRequest: Boolean = true
|
||||
override lateinit var device: DeviceItem
|
||||
override lateinit var addressPrefix: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_hue_scene)
|
||||
|
||||
window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this)
|
||||
|
||||
val nameTxt = findViewById<TextView>(R.id.nameTxt)
|
||||
|
||||
device = Devices(this).getDeviceById(intent.getStringExtra("deviceId") ?: "")
|
||||
hueAPI = HueAPI(this, device.id)
|
||||
addressPrefix = device.address +
|
||||
"api/" + hueAPI.getUsername()
|
||||
queue = Volley.newRequestQueue(this)
|
||||
nameBox = findViewById(R.id.nameBox)
|
||||
briBar = findViewById(R.id.briBar)
|
||||
|
||||
editing = intent.hasExtra("scene")
|
||||
adapter = HueSceneLampListAdapter(listItems, this)
|
||||
groupId = intent.getStringExtra("room") ?: "0"
|
||||
sceneId = intent.getStringExtra("scene") ?: ""
|
||||
|
||||
findViewById<RecyclerView>(R.id.recyclerView).apply {
|
||||
layoutManager = LinearLayoutManager(this@HueSceneActivity)
|
||||
adapter = this@HueSceneActivity.adapter
|
||||
}
|
||||
briBar.setLabelFormatter { value: Float ->
|
||||
HueUtils.briToPercent(value.toInt())
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
onEditScene()
|
||||
} else {
|
||||
onCreateScene()
|
||||
}
|
||||
|
||||
nameBox.editText?.addTextChangedListener(
|
||||
object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(
|
||||
s: CharSequence,
|
||||
start: Int,
|
||||
count: Int,
|
||||
after: Int,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onTextChanged(
|
||||
s: CharSequence,
|
||||
start: Int,
|
||||
before: Int,
|
||||
count: Int,
|
||||
) {
|
||||
val string = s.toString()
|
||||
if (string == "") {
|
||||
nameTxt.text = defaultText
|
||||
} else {
|
||||
nameTxt.text = string
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
briBar.addOnSliderTouchListener(
|
||||
object : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
hueAPI.changeBrightnessOfGroup(groupId, slider.value.toInt())
|
||||
adapter.changeSceneBrightness(HueUtils.briToPercent(slider.value.toInt()))
|
||||
lightStates.setSceneBrightness(slider.value.toInt())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener {
|
||||
onFloatingActionButtonClicked()
|
||||
}
|
||||
|
||||
findViewById<MaterialToolbar>(R.id.toolbar).apply {
|
||||
setNavigationIcon(R.drawable.ic_arrow_back)
|
||||
setNavigationOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEditScene() {
|
||||
defaultText = resources.getString(R.string.hue_scene)
|
||||
hueAPI.activateSceneOfGroup(groupId, sceneId)
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"$addressPrefix/scenes/$sceneId",
|
||||
null,
|
||||
{ response ->
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"$addressPrefix/lights",
|
||||
null,
|
||||
{ secondResponse ->
|
||||
nameBox.editText?.setText(response.optString("name"))
|
||||
val lights =
|
||||
response.optJSONObject("lightstates") ?: JSONObject()
|
||||
var lightObj: JSONObject
|
||||
val brightness = Array(2) { 0 }
|
||||
for (i in lights.keys()) {
|
||||
lightObj = lights.getJSONObject(i)
|
||||
lightStates.addLight(i, lightObj)
|
||||
listItems +=
|
||||
generateListItem(
|
||||
i,
|
||||
(secondResponse.optJSONObject(i) ?: JSONObject())
|
||||
.optString("name"),
|
||||
lightObj,
|
||||
)
|
||||
if (lightObj.has("bri")) {
|
||||
brightness[0] += lightObj.getInt("bri")
|
||||
brightness[1]++
|
||||
}
|
||||
}
|
||||
|
||||
SliderUtils.setProgress(
|
||||
briBar,
|
||||
if (brightness[1] > 0) brightness[0] / brightness[1] else 0,
|
||||
)
|
||||
listItems.sortBy { it.title }
|
||||
adapter.notifyDataSetChanged()
|
||||
},
|
||||
this,
|
||||
),
|
||||
)
|
||||
},
|
||||
this,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCreateScene() {
|
||||
defaultText = resources.getString(R.string.hue_new_scene)
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"$addressPrefix/groups/$groupId",
|
||||
null,
|
||||
{ response ->
|
||||
val lightIds = response.getJSONArray("lights")
|
||||
SliderUtils.setProgress(
|
||||
briBar,
|
||||
(response.optJSONObject("action") ?: JSONObject()).optInt("bri"),
|
||||
)
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"$addressPrefix/lights",
|
||||
null,
|
||||
{ secondResponse ->
|
||||
var lightObj: JSONObject
|
||||
for (i in 0 until lightIds.length()) {
|
||||
lightObj =
|
||||
secondResponse.getJSONObject(lightIds.getString(i))
|
||||
val state = lightObj.getJSONObject("state")
|
||||
listItems +=
|
||||
generateListItem(
|
||||
lightIds.getString(i),
|
||||
lightObj.getString("name"),
|
||||
state,
|
||||
)
|
||||
}
|
||||
|
||||
listItems.sortBy { it.title }
|
||||
adapter.notifyDataSetChanged()
|
||||
},
|
||||
this,
|
||||
),
|
||||
)
|
||||
},
|
||||
this,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun generateListItem(
|
||||
id: String,
|
||||
title: String,
|
||||
state: JSONObject,
|
||||
): SceneListItem =
|
||||
SceneListItem(
|
||||
title,
|
||||
id,
|
||||
state.optBoolean("on"),
|
||||
HueUtils.briToPercent(state.optInt("bri", MAX_BRIGHTNESS)),
|
||||
if (state.has("xy")) {
|
||||
val xyArray = state.getJSONArray("xy")
|
||||
ColorUtils.xyToRGB(
|
||||
xyArray.getDouble(0),
|
||||
xyArray.getDouble(1),
|
||||
)
|
||||
} else if (state.has("hue") && state.has("sat")) {
|
||||
HueUtils.hueSatToRGB(state.getInt("hue"), state.getInt("sat"))
|
||||
} else if (state.has("ct")) {
|
||||
HueUtils.ctToRGB(state.getInt("ct"))
|
||||
} else {
|
||||
"#FFFFFF".toColorInt()
|
||||
},
|
||||
)
|
||||
|
||||
private fun onFloatingActionButtonClicked() {
|
||||
val name = nameBox.editText?.text.toString()
|
||||
if (name == "") {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.err_missing_name)
|
||||
.setMessage(R.string.err_missing_name_summary)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
return
|
||||
}
|
||||
queue.add(
|
||||
if (editing) {
|
||||
CustomJsonArrayRequest(
|
||||
Request.Method.PUT,
|
||||
"$addressPrefix/scenes/$sceneId",
|
||||
JSONObject("""{ "name": "$name", "lightstates": $lightStates }"""),
|
||||
this,
|
||||
this,
|
||||
)
|
||||
} else {
|
||||
CustomJsonArrayRequest(
|
||||
Request.Method.POST,
|
||||
"$addressPrefix/scenes",
|
||||
JSONObject(
|
||||
"""{ "name": "$name", "recycle": false, "group": "$groupId", "type": "GroupScene" }""",
|
||||
),
|
||||
this,
|
||||
this,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResponse(response: JSONArray) {
|
||||
HueScenesFragment.scenesChanged = true
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onErrorResponse(error: VolleyError) {
|
||||
Toast.makeText(this, Global.volleyError(this, error), Toast.LENGTH_LONG).show()
|
||||
Log.e(Global.LOG_TAG, error.toString())
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
data: SceneListItem,
|
||||
) {
|
||||
id = data.hidden
|
||||
HueColorSheet(this).show(supportFragmentManager, HueColorSheet::class.simpleName)
|
||||
}
|
||||
|
||||
override fun onStateChanged(
|
||||
view: View,
|
||||
data: SceneListItem,
|
||||
state: Boolean,
|
||||
) {
|
||||
hueAPI.switchLightById(data.hidden, state)
|
||||
lightStates.switchLight(data.hidden, state)
|
||||
}
|
||||
|
||||
override fun onColorChanged(color: Int) {
|
||||
adapter.updateColor(id, color)
|
||||
}
|
||||
|
||||
override fun onBrightnessChanged(brightness: Int) {
|
||||
lightStates.setLightBrightness(id, brightness)
|
||||
adapter.updateBrightness(id, HueUtils.briToPercent(brightness))
|
||||
}
|
||||
|
||||
override fun onHueSatChanged(
|
||||
hue: Int,
|
||||
sat: Int,
|
||||
) {
|
||||
lightStates.setLightHue(id, hue)
|
||||
lightStates.setLightSat(id, sat)
|
||||
}
|
||||
|
||||
override fun onCtChanged(ct: Int) {
|
||||
lightStates.setLightCt(id, ct)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import io.github.domi04151309.home.R
|
||||
|
||||
class LibraryActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, GeneralPreferenceFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
class GeneralPreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
addPreferencesFromResource(R.xml.pref_about_list)
|
||||
preferenceScreen.removeAll()
|
||||
val libraries = resources.getStringArray(R.array.about_libraries)
|
||||
val licenses = resources.getStringArray(R.array.about_libraries_licenses)
|
||||
if (libraries.size != licenses.size) error("Library array size does not match license array size.")
|
||||
for (index in libraries.indices) {
|
||||
preferenceScreen.addPreference(
|
||||
Preference(requireContext()).apply {
|
||||
icon =
|
||||
ResourcesCompat.getDrawable(
|
||||
requireContext().resources,
|
||||
R.drawable.ic_about_library,
|
||||
requireContext().theme,
|
||||
)
|
||||
title = libraries[index]
|
||||
summary = licenses[index]
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,616 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.ContextMenu
|
||||
import android.view.Gravity
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageSwitcher
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextSwitcher
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.MainListAdapter
|
||||
import io.github.domi04151309.home.api.UnifiedAPI
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.Global.checkNetwork
|
||||
import io.github.domi04151309.home.helpers.P
|
||||
import io.github.domi04151309.home.helpers.TasmotaHelper
|
||||
import io.github.domi04151309.home.helpers.UpdateHandler
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class MainActivity : BaseActivity() {
|
||||
private var tasmotaPosition: Int = 0
|
||||
private var shouldReset: Boolean = false
|
||||
private val updateHandler = UpdateHandler()
|
||||
private var isDeviceSelected = false
|
||||
private var canReceiveRequest = false
|
||||
private var currentView: View? = null
|
||||
internal lateinit var devices: Devices
|
||||
internal lateinit var adapter: MainListAdapter
|
||||
private lateinit var deviceIcon: ImageSwitcher
|
||||
private lateinit var deviceName: TextSwitcher
|
||||
private lateinit var fab: FloatingActionButton
|
||||
|
||||
private var columns: Int? = null
|
||||
|
||||
/*
|
||||
* Unified callbacks
|
||||
*/
|
||||
private var unified: UnifiedAPI? = null
|
||||
private val unifiedRequestCallback =
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
if (holder.response != null) {
|
||||
val device = devices.getDeviceById(holder.deviceId)
|
||||
deviceIcon.setImageResource(device.iconId)
|
||||
deviceName.setText(device.name)
|
||||
adapter.updateData(holder.response, recyclerViewInterface)
|
||||
fab.hide()
|
||||
isDeviceSelected = true
|
||||
} else {
|
||||
if (currentView == null) {
|
||||
loadDeviceList()
|
||||
Toast.makeText(this@MainActivity, holder.errorMessage, Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
currentView?.findViewById<TextView>(R.id.summary)?.text = holder.errorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
showExecutionResult(result)
|
||||
if (shouldRefresh) unified?.loadList(this)
|
||||
}
|
||||
}
|
||||
private val unifiedHelperInterface =
|
||||
object : HomeRecyclerViewHelperInterface {
|
||||
override fun onStateChanged(
|
||||
view: View,
|
||||
data: ListViewItem,
|
||||
state: Boolean,
|
||||
) {
|
||||
if (data.hidden.isEmpty()) return
|
||||
unified?.changeSwitchState(data.hidden, state)
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
data: ListViewItem,
|
||||
) {
|
||||
unified?.execute(data.hidden, unifiedRequestCallback)
|
||||
}
|
||||
}
|
||||
private val unifiedRealTimeStatesCallback =
|
||||
object : UnifiedAPI.RealTimeStatesCallback {
|
||||
override fun onStatesLoaded(
|
||||
states: List<ListViewItem>,
|
||||
offset: Int,
|
||||
) {
|
||||
for (i in states.indices) {
|
||||
adapter.updateItem(
|
||||
i + offset,
|
||||
states[i],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Things related to Tasmota
|
||||
*/
|
||||
private val tasmotaHelperInterface =
|
||||
object : HomeRecyclerViewHelperInterface {
|
||||
override fun onStateChanged(
|
||||
view: View,
|
||||
data: ListViewItem,
|
||||
state: Boolean,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
data: ListViewItem,
|
||||
) {
|
||||
val helper = TasmotaHelper(this@MainActivity, unified ?: return)
|
||||
when (data.hidden) {
|
||||
"add" -> helper.addToList(unifiedRequestCallback)
|
||||
"execute_once" -> helper.executeOnce(unifiedRequestCallback)
|
||||
else ->
|
||||
unified?.execute(
|
||||
view.findViewById<TextView>(R.id.summary).text.toString(),
|
||||
unifiedRequestCallback,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Things related to the main menu
|
||||
*/
|
||||
private val mainHelperInterface =
|
||||
object : HomeRecyclerViewHelperInterface {
|
||||
override fun onStateChanged(
|
||||
view: View,
|
||||
data: ListViewItem,
|
||||
state: Boolean,
|
||||
) {
|
||||
if (data.hidden.isEmpty()) return
|
||||
|
||||
val deviceId = data.hidden.substring(0, data.hidden.indexOf('@'))
|
||||
Global.getCorrectAPI(
|
||||
this@MainActivity,
|
||||
devices.getDeviceById(deviceId).mode,
|
||||
deviceId,
|
||||
).changeSwitchState(data.hidden.substring(deviceId.length + 1), state)
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
data: ListViewItem,
|
||||
) {
|
||||
currentView = view
|
||||
if (data.title == resources.getString(R.string.main_no_devices)) {
|
||||
startActivityAndReset(Intent(this@MainActivity, DevicesActivity::class.java))
|
||||
} else if (data.title == resources.getString(R.string.err_wrong_format)) {
|
||||
startActivityAndReset(Intent(this@MainActivity, SettingsActivity::class.java))
|
||||
} else if (data.hidden.contains('@')) {
|
||||
val deviceId = data.hidden.substring(0, data.hidden.indexOf('@'))
|
||||
val api = Global.getCorrectAPI(this@MainActivity, devices.getDeviceById(deviceId).mode, deviceId)
|
||||
api.execute(
|
||||
data.hidden.substring(deviceId.length + 1),
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
showExecutionResult(result)
|
||||
if (shouldRefresh) {
|
||||
api.loadList(
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
adapter.updateDirectView(
|
||||
deviceId,
|
||||
holder.response ?: listOf(),
|
||||
adapter.getDirectViewPos(deviceId),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
if (checkNetwork(this@MainActivity)) {
|
||||
view.findViewById<TextView>(R.id.summary).text =
|
||||
resources.getString(R.string.main_connecting)
|
||||
selectDevice(data.hidden)
|
||||
} else {
|
||||
view.findViewById<TextView>(R.id.summary).text =
|
||||
resources.getString(R.string.main_network_not_secure)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getColumns(): Int? =
|
||||
(
|
||||
PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getString(P.PREF_COLUMNS, P.PREF_COLUMNS_DEFAULT)
|
||||
?: P.PREF_COLUMNS_DEFAULT
|
||||
).toIntOrNull()
|
||||
|
||||
/*
|
||||
* Activity methods
|
||||
*/
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
window.statusBarColor = SurfaceColors.SURFACE_0.getColor(this)
|
||||
|
||||
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
|
||||
devices = Devices(this)
|
||||
deviceIcon = findViewById(R.id.deviceIcon)
|
||||
deviceName = findViewById(R.id.deviceName)
|
||||
fab = findViewById(R.id.fab)
|
||||
columns = getColumns()
|
||||
|
||||
setupHeader()
|
||||
|
||||
adapter = MainListAdapter(recyclerView)
|
||||
recyclerView.layoutManager = GridLayoutManager(this, numberOfRows())
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
fab.setOnClickListener {
|
||||
startActivityAndReset(Intent(this, DevicesActivity::class.java))
|
||||
}
|
||||
|
||||
findViewById<MaterialToolbar>(R.id.toolbar).setOnMenuItemClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
this@MainActivity,
|
||||
SettingsActivity::class.java,
|
||||
),
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
// Handle shortcut
|
||||
if (intent.hasExtra(Devices.INTENT_EXTRA_DEVICE)) {
|
||||
val deviceId = intent.getStringExtra(Devices.INTENT_EXTRA_DEVICE) ?: ""
|
||||
if (devices.idExists(deviceId)) {
|
||||
if (checkNetwork(this)) {
|
||||
val device = devices.getDeviceById(deviceId)
|
||||
deviceIcon.setImageResource(device.iconId)
|
||||
deviceName.setText(device.name)
|
||||
selectDevice(deviceId)
|
||||
} else {
|
||||
loadDeviceList()
|
||||
Toast.makeText(this, R.string.main_network_not_secure, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
loadDeviceList()
|
||||
Toast.makeText(this, R.string.main_device_nonexistent, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
} else {
|
||||
loadDeviceList()
|
||||
}
|
||||
|
||||
onBackPressedDispatcher.addCallback {
|
||||
if (isDeviceSelected) {
|
||||
loadDeviceList()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupHeader() {
|
||||
deviceIcon.setFactory {
|
||||
val view = ImageView(this@MainActivity)
|
||||
view.layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
view
|
||||
}
|
||||
deviceName.setFactory {
|
||||
val view = TextView(this@MainActivity)
|
||||
view.layoutParams =
|
||||
FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
view.setTextAppearance(androidx.appcompat.R.style.TextAppearance_AppCompat_Large)
|
||||
view.setTextColor(ContextCompat.getColor(this, android.R.color.white))
|
||||
view.gravity = Gravity.CENTER_VERTICAL
|
||||
view.ellipsize = TextUtils.TruncateAt.END
|
||||
view.maxLines = 1
|
||||
view
|
||||
}
|
||||
|
||||
val inAnimation = AnimationUtils.loadAnimation(this, android.R.anim.fade_in)
|
||||
val outAnimation = AnimationUtils.loadAnimation(this, android.R.anim.fade_out)
|
||||
inAnimation.duration /= 2
|
||||
outAnimation.duration /= 2
|
||||
deviceIcon.inAnimation = inAnimation
|
||||
deviceIcon.outAnimation = outAnimation
|
||||
deviceName.inAnimation = inAnimation
|
||||
deviceName.outAnimation = outAnimation
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(
|
||||
menu: ContextMenu?,
|
||||
v: View?,
|
||||
menuInfo: ContextMenu.ContextMenuInfo?,
|
||||
) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo)
|
||||
val hidden = v?.findViewById<TextView>(R.id.hidden)?.text ?: return
|
||||
if (hidden.contains("tasmota_command")) {
|
||||
tasmotaPosition = hidden.substring(hidden.lastIndexOf('#') + 1).toInt()
|
||||
menuInflater.inflate(R.menu.activity_main_tasmota_context, menu)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
val helper = TasmotaHelper(this, unified ?: return super.onContextItemSelected(item))
|
||||
return when (item.title) {
|
||||
resources.getString(R.string.str_edit) -> {
|
||||
helper.updateItem(unifiedRequestCallback, tasmotaPosition)
|
||||
true
|
||||
}
|
||||
resources.getString(R.string.str_delete) -> {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.str_delete)
|
||||
.setMessage(R.string.tasmota_delete_command)
|
||||
.setPositiveButton(R.string.str_delete) { _, _ ->
|
||||
helper.removeFromList(unifiedRequestCallback, tasmotaPosition)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
super.onContextItemSelected(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (getColumns() != columns) {
|
||||
columns = getColumns()
|
||||
recreate()
|
||||
}
|
||||
if (shouldReset) {
|
||||
loadDeviceList()
|
||||
shouldReset = false
|
||||
}
|
||||
canReceiveRequest = true
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
canReceiveRequest = false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
updateHandler.stop()
|
||||
}
|
||||
|
||||
private fun numberOfRows(): Int {
|
||||
if (columns != null) return columns ?: 1
|
||||
val displayMetrics: DisplayMetrics = resources.displayMetrics
|
||||
val horizontal: Int =
|
||||
(
|
||||
displayMetrics.widthPixels / displayMetrics.density / COLUMN_COUNT_FRACTION
|
||||
).toInt()
|
||||
val vertical: Int =
|
||||
(
|
||||
displayMetrics.heightPixels / displayMetrics.density / COLUMN_COUNT_FRACTION
|
||||
).toInt()
|
||||
return max(1, min(horizontal, vertical))
|
||||
}
|
||||
|
||||
internal fun selectDevice(deviceId: String) {
|
||||
val deviceObj = devices.getDeviceById(deviceId)
|
||||
when {
|
||||
WEB_MODES.contains(deviceObj.mode) -> {
|
||||
val intent =
|
||||
Intent(this, WebActivity::class.java)
|
||||
.putExtra("title", deviceObj.name)
|
||||
|
||||
when (deviceObj.mode) {
|
||||
Global.FRITZ_AUTO_LOGIN -> {
|
||||
intent.putExtra("URI", deviceObj.address)
|
||||
intent.putExtra("fritz_auto_login", deviceObj.id)
|
||||
}
|
||||
Global.GRAFANA_AUTO_LOGIN -> {
|
||||
intent.putExtra("URI", deviceObj.address)
|
||||
intent.putExtra("grafana_auto_login", deviceObj.id)
|
||||
}
|
||||
Global.PI_HOLE_AUTO_LOGIN -> {
|
||||
intent.putExtra("URI", deviceObj.address)
|
||||
intent.putExtra("pi_hole_auto_login", deviceObj.id)
|
||||
}
|
||||
Global.NODE_RED -> {
|
||||
intent.putExtra("URI", deviceObj.address + "ui/")
|
||||
}
|
||||
Global.WEBSITE -> {
|
||||
intent.putExtra("URI", deviceObj.address)
|
||||
}
|
||||
}
|
||||
startActivityAndReset(intent)
|
||||
}
|
||||
Global.UNIFIED_MODES.contains(deviceObj.mode) -> {
|
||||
unified =
|
||||
Global.getCorrectAPI(
|
||||
this,
|
||||
deviceObj.mode,
|
||||
deviceId,
|
||||
unifiedHelperInterface,
|
||||
tasmotaHelperInterface,
|
||||
)
|
||||
unified?.loadList(unifiedRequestCallback, true)
|
||||
updateHandler.setUpdateFunction {
|
||||
if (canReceiveRequest && unified?.needsRealTimeData == true) {
|
||||
unified?.loadStates(unifiedRealTimeStatesCallback, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Toast.makeText(this, R.string.main_unknown_mode, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDirectView(
|
||||
currentDevice: DeviceItem,
|
||||
position: Int,
|
||||
registeredForUpdates: HashMap<Int, UnifiedAPI?>,
|
||||
) {
|
||||
val api = Global.getCorrectAPI(this, currentDevice.mode, currentDevice.id)
|
||||
api.loadList(
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
if (holder.response != null) {
|
||||
Thread {
|
||||
while (!updateHandler.running) Thread.sleep(TINY_DELAY)
|
||||
runOnUiThread {
|
||||
adapter.updateDirectView(
|
||||
currentDevice.id,
|
||||
holder.response,
|
||||
position,
|
||||
)
|
||||
}
|
||||
registeredForUpdates[position] = api
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDeviceItem(device: DeviceItem) =
|
||||
ListViewItem(
|
||||
title = device.name,
|
||||
summary = resources.getString(R.string.main_tap_to_connect),
|
||||
hidden = device.id,
|
||||
icon = device.iconId,
|
||||
)
|
||||
|
||||
private fun updateStates(registeredForUpdates: HashMap<Int, UnifiedAPI?>) {
|
||||
if (canReceiveRequest) {
|
||||
for (i in registeredForUpdates.keys) {
|
||||
if (registeredForUpdates[i]?.needsRealTimeData == true) {
|
||||
registeredForUpdates[i]?.loadStates(
|
||||
unifiedRealTimeStatesCallback,
|
||||
adapter.getOffset(i),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun loadDeviceList() {
|
||||
updateHandler.stop()
|
||||
val registeredForUpdates: HashMap<Int, UnifiedAPI?> = hashMapOf()
|
||||
val listItems: ArrayList<ListViewItem> = ArrayList(devices.length)
|
||||
|
||||
if (devices.length == 0) {
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = resources.getString(R.string.main_no_devices),
|
||||
summary = resources.getString(R.string.main_no_devices_summary),
|
||||
icon = R.drawable.ic_info,
|
||||
)
|
||||
}
|
||||
var actualPosition = 0
|
||||
for (i in 0 until devices.length) {
|
||||
val currentDevice = devices.getDeviceByIndex(i)
|
||||
if (!currentDevice.hide) {
|
||||
if (
|
||||
currentDevice.directView &&
|
||||
Global.UNIFIED_MODES.contains(currentDevice.mode) &&
|
||||
checkNetwork(this)
|
||||
) {
|
||||
onDirectView(currentDevice, actualPosition, registeredForUpdates)
|
||||
}
|
||||
listItems += getDeviceItem(currentDevice)
|
||||
actualPosition++
|
||||
}
|
||||
}
|
||||
|
||||
adapter.updateData(listItems, mainHelperInterface)
|
||||
deviceIcon.setImageResource(R.drawable.ic_home_white)
|
||||
deviceName.setText(resources.getString(R.string.main_device_name))
|
||||
fab.show()
|
||||
isDeviceSelected = false
|
||||
updateHandler.setUpdateFunction {
|
||||
updateStates(registeredForUpdates)
|
||||
}
|
||||
unified = null
|
||||
}
|
||||
|
||||
internal fun startActivityAndReset(intent: Intent) {
|
||||
shouldReset = true
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
internal fun showExecutionResult(result: String) {
|
||||
if (result.length < MAX_RESPONSE_LENGTH) {
|
||||
Toast.makeText(this, result, Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
Snackbar
|
||||
.make(
|
||||
findViewById(android.R.id.content),
|
||||
R.string.main_execution_completed,
|
||||
Snackbar.LENGTH_LONG,
|
||||
)
|
||||
.setAction(R.string.str_show) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.main_execution_completed)
|
||||
.setMessage(result)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val WEB_MODES =
|
||||
arrayOf(
|
||||
Global.FRITZ_AUTO_LOGIN,
|
||||
Global.GRAFANA_AUTO_LOGIN,
|
||||
Global.PI_HOLE_AUTO_LOGIN,
|
||||
Global.NODE_RED,
|
||||
Global.WEBSITE,
|
||||
)
|
||||
private const val TINY_DELAY = 100L
|
||||
private const val COLUMN_COUNT_FRACTION = 240
|
||||
private const val MAX_RESPONSE_LENGTH = 64
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.rine.upnpdiscovery.UPnPDiscovery
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.DeviceDiscoveryListAdapter
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.discovery.NetworkServiceDiscoveryListener
|
||||
import io.github.domi04151309.home.discovery.NetworkServiceResolveListener
|
||||
import io.github.domi04151309.home.discovery.UPnPListener
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
|
||||
class SearchDevicesActivity : BaseActivity(), RecyclerViewHelperInterface {
|
||||
private lateinit var adapter: DeviceDiscoveryListAdapter
|
||||
private lateinit var devices: Devices
|
||||
private lateinit var nsdManager: NsdManager
|
||||
private lateinit var discoveryListenerHttp: NsdManager.DiscoveryListener
|
||||
private lateinit var discoveryListenerSimpleHome: NsdManager.DiscoveryListener
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_devices)
|
||||
|
||||
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
|
||||
adapter =
|
||||
DeviceDiscoveryListAdapter(
|
||||
mutableListOf(
|
||||
ListViewItem(
|
||||
title = resources.getString(R.string.pref_add_search),
|
||||
summary = resources.getString(R.string.pref_add_search_summary),
|
||||
icon = R.drawable.ic_search,
|
||||
),
|
||||
),
|
||||
this,
|
||||
)
|
||||
devices = Devices(this)
|
||||
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
// Device variables
|
||||
val manager = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
|
||||
val routerIp = intToIp(manager.dhcpInfo.gateway)
|
||||
|
||||
Thread {
|
||||
// Add Router
|
||||
adapter.add(
|
||||
ListViewItem(
|
||||
title = resources.getString(R.string.pref_device_router),
|
||||
summary = routerIp,
|
||||
hidden = "Website#Router",
|
||||
icon = R.drawable.ic_device_router,
|
||||
state = devices.addressExists(routerIp),
|
||||
),
|
||||
)
|
||||
|
||||
// Get compatible devices
|
||||
UPnPDiscovery.discoveryDevices(
|
||||
this,
|
||||
UPnPListener(this, adapter),
|
||||
)
|
||||
}.start()
|
||||
|
||||
val resolveListener = NetworkServiceResolveListener(this, adapter)
|
||||
nsdManager = getSystemService(NSD_SERVICE) as NsdManager
|
||||
discoveryListenerHttp = NetworkServiceDiscoveryListener(this, resolveListener)
|
||||
discoveryListenerSimpleHome = NetworkServiceDiscoveryListener(this, resolveListener)
|
||||
nsdManager.discoverServices("_http._tcp", NsdManager.PROTOCOL_DNS_SD, discoveryListenerHttp)
|
||||
nsdManager.discoverServices("_simplehome._tcp", NsdManager.PROTOCOL_DNS_SD, discoveryListenerSimpleHome)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun intToIp(address: Int): String =
|
||||
(address and 0xFF).toString() + "." + (address shr 8 and 0xFF) + "." +
|
||||
(address shr 16 and 0xFF) + "." + (address shr 24 and 0xFF)
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
) {
|
||||
val name = view.findViewById<TextView>(R.id.title).text.toString()
|
||||
val hidden = view.findViewById<TextView>(R.id.hidden).text.toString()
|
||||
if (hidden != "") {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.pref_add_dialog)
|
||||
.setMessage(resources.getString(R.string.pref_add_dialog_message, name))
|
||||
.setPositiveButton(R.string.str_add) { _, _ ->
|
||||
val newItem =
|
||||
DeviceItem(
|
||||
devices.generateNewId(),
|
||||
name,
|
||||
hidden.substring(0, hidden.indexOf('#')),
|
||||
hidden.substring(hidden.lastIndexOf('#') + 1),
|
||||
)
|
||||
newItem.address = view.findViewById<TextView>(R.id.summary).text.toString()
|
||||
devices.addDevice(newItem)
|
||||
adapter.changeState(position, true)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
nsdManager.stopServiceDiscovery(discoveryListenerHttp)
|
||||
nsdManager.stopServiceDiscovery(discoveryListenerSimpleHome)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.P
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_settings)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, GeneralPreferenceFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
class GeneralPreferenceFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(
|
||||
savedInstanceState: Bundle?,
|
||||
rootKey: String?,
|
||||
) {
|
||||
addPreferencesFromResource(R.xml.pref_general)
|
||||
findPreference<Preference>(P.PREF_CONTROLS_AUTH)?.isVisible =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
||||
findPreference<Preference>("devices")?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(context, DevicesActivity::class.java))
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("devices_json")?.setOnPreferenceClickListener {
|
||||
Devices.reloadFromPreferences()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("reset_json")?.setOnPreferenceClickListener {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.pref_reset)
|
||||
.setMessage(R.string.pref_reset_question)
|
||||
.setPositiveButton(R.string.str_delete) { _, _ ->
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit {
|
||||
putString("devices_json", Global.DEFAULT_JSON)
|
||||
}
|
||||
Toast.makeText(context, R.string.pref_reset_toast, Toast.LENGTH_LONG).show()
|
||||
Devices.reloadFromPreferences()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("about")?.setOnPreferenceClickListener {
|
||||
startActivity(Intent(context, AboutActivity::class.java))
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("wiki")?.setOnPreferenceClickListener {
|
||||
val uri = "https://github.com/Domi04151309/HomeApp/wiki"
|
||||
startActivity(
|
||||
Intent(context, WebActivity::class.java).putExtra("URI", uri)
|
||||
.putExtra("title", resources.getString(R.string.pref_info_wiki)),
|
||||
)
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("header")?.setOnPreferenceClickListener {
|
||||
startActivity(
|
||||
Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
"https://unsplash.com/photos/mx4mSkK9zeo".toUri(),
|
||||
),
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.SimpleListAdapter
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.SimpleListItem
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
|
||||
class ShortcutDeviceActivity : BaseActivity(), RecyclerViewHelperInterface {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_devices)
|
||||
|
||||
val devices = Devices(this)
|
||||
val recyclerView: RecyclerView = findViewById(R.id.recyclerView)
|
||||
val listItems: ArrayList<SimpleListItem> = ArrayList(devices.length)
|
||||
var currentDevice: DeviceItem
|
||||
for (i in 0 until devices.length) {
|
||||
currentDevice = devices.getDeviceByIndex(i)
|
||||
listItems +=
|
||||
SimpleListItem(
|
||||
title = currentDevice.name,
|
||||
summary = currentDevice.address,
|
||||
hidden = currentDevice.id,
|
||||
icon = currentDevice.iconId,
|
||||
)
|
||||
}
|
||||
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
recyclerView.adapter = SimpleListAdapter(listItems, this)
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
) {
|
||||
val device = Devices(this).getDeviceByIndex(position)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val shortcutManager = this.getSystemService(ShortcutManager::class.java)
|
||||
if (shortcutManager != null) {
|
||||
setResult(
|
||||
RESULT_OK,
|
||||
shortcutManager.createShortcutResultIntent(
|
||||
ShortcutInfo.Builder(this, device.id)
|
||||
.setShortLabel(
|
||||
device.name.ifEmpty {
|
||||
resources.getString(R.string.pref_add_name_empty)
|
||||
},
|
||||
)
|
||||
.setLongLabel(
|
||||
device.name.ifEmpty {
|
||||
resources.getString(R.string.pref_add_name_empty)
|
||||
},
|
||||
)
|
||||
.setIcon(Icon.createWithResource(this, device.iconId))
|
||||
.setIntent(
|
||||
Intent(this, MainActivity::class.java)
|
||||
.putExtra(Devices.INTENT_EXTRA_DEVICE, device.id)
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
|
||||
)
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.pref_add_shortcut_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.SimpleListAdapter
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.api.UnifiedAPI
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.SimpleListItem
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
|
||||
class ShortcutHueRoomActivity : BaseActivity(), RecyclerViewHelperInterface {
|
||||
private var deviceId: String? = null
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_devices)
|
||||
|
||||
recyclerView = findViewById(R.id.recyclerView)
|
||||
|
||||
val devices = Devices(this)
|
||||
val listItems: ArrayList<SimpleListItem> = ArrayList(devices.length)
|
||||
var currentDevice: DeviceItem
|
||||
for (i in 0 until devices.length) {
|
||||
currentDevice = devices.getDeviceByIndex(i)
|
||||
if (currentDevice.mode == Global.HUE_API) {
|
||||
listItems +=
|
||||
SimpleListItem(
|
||||
title = currentDevice.name,
|
||||
summary = currentDevice.address,
|
||||
hidden = currentDevice.id,
|
||||
icon = currentDevice.iconId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
recyclerView.adapter = SimpleListAdapter(listItems, this)
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
) {
|
||||
if (deviceId == null) {
|
||||
deviceId = view.findViewById<TextView>(R.id.hidden).text.toString()
|
||||
HueAPI(this, deviceId ?: error("Impossible state.")).loadList(
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
if (holder.response != null) {
|
||||
recyclerView.adapter =
|
||||
SimpleListAdapter(
|
||||
holder.response as List<SimpleListItem>,
|
||||
this@ShortcutHueRoomActivity,
|
||||
)
|
||||
} else {
|
||||
deviceId = null
|
||||
Toast.makeText(
|
||||
this@ShortcutHueRoomActivity,
|
||||
holder.errorMessage,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
val device = Devices(this).getDeviceById(deviceId ?: error("Impossible state."))
|
||||
val lampName = view.findViewById<TextView>(R.id.title).text
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val shortcutManager = this.getSystemService(ShortcutManager::class.java)
|
||||
if (shortcutManager != null) {
|
||||
setResult(
|
||||
RESULT_OK,
|
||||
shortcutManager.createShortcutResultIntent(
|
||||
ShortcutInfo.Builder(this, device.id + lampName)
|
||||
.setShortLabel(lampName)
|
||||
.setLongLabel(lampName)
|
||||
.setIcon(Icon.createWithResource(this, device.iconId))
|
||||
.setIntent(
|
||||
Intent(this, HueLampActivity::class.java)
|
||||
.putExtra("id", view.findViewById<TextView>(R.id.hidden).text)
|
||||
.putExtra(Devices.INTENT_EXTRA_DEVICE, device.id)
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
|
||||
)
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.pref_add_shortcut_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
|
||||
class ShortcutHueSceneActionActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (intent.hasExtra("scene") && intent.hasExtra("group") && intent.hasExtra(Devices.INTENT_EXTRA_DEVICE)) {
|
||||
HueAPI(
|
||||
this,
|
||||
intent.getStringExtra(Devices.INTENT_EXTRA_DEVICE) ?: error(IMPOSSIBLE_STATE),
|
||||
).activateSceneOfGroup(
|
||||
intent.getStringExtra("group") ?: error(IMPOSSIBLE_STATE),
|
||||
intent.getStringExtra("scene") ?: error(IMPOSSIBLE_STATE),
|
||||
)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val IMPOSSIBLE_STATE = "Impossible state."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.SimpleListAdapter
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.api.UnifiedAPI
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.SimpleListItem
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
import org.json.JSONObject
|
||||
|
||||
class ShortcutHueSceneActivity : BaseActivity(), RecyclerViewHelperInterface {
|
||||
private var deviceId: String? = null
|
||||
private var group: String? = null
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
|
||||
private val device: DeviceItem
|
||||
get() = Devices(this).getDeviceById(deviceId ?: error("Device ID is null."))
|
||||
|
||||
private val api: HueAPI
|
||||
get() = HueAPI(this, deviceId ?: error("Device ID is null."))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_devices)
|
||||
|
||||
recyclerView = findViewById(R.id.recyclerView)
|
||||
|
||||
val devices = Devices(this)
|
||||
val listItems: ArrayList<SimpleListItem> = ArrayList(devices.length)
|
||||
var currentDevice: DeviceItem
|
||||
for (i in 0 until devices.length) {
|
||||
currentDevice = devices.getDeviceByIndex(i)
|
||||
if (currentDevice.mode == Global.HUE_API) {
|
||||
listItems +=
|
||||
SimpleListItem(
|
||||
title = currentDevice.name,
|
||||
summary = currentDevice.address,
|
||||
hidden = currentDevice.id,
|
||||
icon = currentDevice.iconId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
recyclerView.adapter = SimpleListAdapter(listItems, this)
|
||||
}
|
||||
|
||||
private fun loadDevice(view: View) {
|
||||
deviceId = view.findViewById<TextView>(R.id.hidden).text.toString()
|
||||
api.loadList(
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
if (holder.response != null) {
|
||||
recyclerView.adapter =
|
||||
SimpleListAdapter(
|
||||
holder.response as List<SimpleListItem>,
|
||||
this@ShortcutHueSceneActivity,
|
||||
)
|
||||
} else {
|
||||
deviceId = null
|
||||
Toast.makeText(
|
||||
this@ShortcutHueSceneActivity,
|
||||
holder.errorMessage,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadGroup(view: View) {
|
||||
group = view.findViewById<TextView>(R.id.hidden).text.toString()
|
||||
Volley.newRequestQueue(this).add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
device.address + "api/" + api.getUsername() + "/scenes/",
|
||||
null,
|
||||
{ response ->
|
||||
val listItems: ArrayList<SimpleListItem> = ArrayList(response.length() / 2)
|
||||
var currentObject: JSONObject
|
||||
for (i in response.keys()) {
|
||||
currentObject = response.getJSONObject(i)
|
||||
if (currentObject.optString("group") == group) {
|
||||
listItems.add(
|
||||
SimpleListItem(
|
||||
currentObject.optString("name"),
|
||||
resources.getString(R.string.hue_tap),
|
||||
i,
|
||||
R.drawable.ic_scene,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
listItems.sortBy { it.title }
|
||||
recyclerView.adapter = SimpleListAdapter(listItems, this)
|
||||
},
|
||||
{ error ->
|
||||
group = null
|
||||
Toast.makeText(this, Global.volleyError(this, error), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createShortcut(view: View) {
|
||||
val lampName = view.findViewById<TextView>(R.id.title).text
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val shortcutManager = this.getSystemService(ShortcutManager::class.java)
|
||||
if (shortcutManager != null) {
|
||||
setResult(
|
||||
RESULT_OK,
|
||||
shortcutManager.createShortcutResultIntent(
|
||||
ShortcutInfo.Builder(this, device.id + lampName)
|
||||
.setShortLabel(view.findViewById<TextView>(R.id.title).text)
|
||||
.setLongLabel(view.findViewById<TextView>(R.id.title).text)
|
||||
.setIcon(Icon.createWithResource(this, device.iconId))
|
||||
.setIntent(
|
||||
Intent(this, ShortcutHueSceneActionActivity::class.java)
|
||||
.putExtra(
|
||||
"scene",
|
||||
view.findViewById<TextView>(R.id.hidden).text,
|
||||
)
|
||||
.putExtra("group", group)
|
||||
.putExtra(Devices.INTENT_EXTRA_DEVICE, device.id)
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
|
||||
)
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.pref_add_shortcut_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
) {
|
||||
if (deviceId == null) {
|
||||
loadDevice(view)
|
||||
} else if (group == null) {
|
||||
loadGroup(view)
|
||||
} else {
|
||||
createShortcut(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import io.github.domi04151309.home.api.Tasmota
|
||||
import io.github.domi04151309.home.api.UnifiedAPI
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
|
||||
class ShortcutTasmotaActionActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (intent.hasExtra("command") && intent.hasExtra(Devices.INTENT_EXTRA_DEVICE)) {
|
||||
Tasmota(
|
||||
this,
|
||||
intent.getStringExtra(Devices.INTENT_EXTRA_DEVICE) ?: error("Impossible state."),
|
||||
null,
|
||||
).execute(
|
||||
intent.getStringExtra("command") ?: error("Impossible state."),
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
Toast.makeText(
|
||||
this@ShortcutTasmotaActionActivity,
|
||||
result,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutInfo
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.SimpleListAdapter
|
||||
import io.github.domi04151309.home.api.Tasmota
|
||||
import io.github.domi04151309.home.api.UnifiedAPI
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.SimpleListItem
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
|
||||
class ShortcutTasmotaActivity : BaseActivity(), RecyclerViewHelperInterface {
|
||||
private var deviceId: String? = null
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_devices)
|
||||
|
||||
recyclerView = findViewById(R.id.recyclerView)
|
||||
|
||||
val devices = Devices(this)
|
||||
val listItems: ArrayList<SimpleListItem> = ArrayList(devices.length)
|
||||
var currentDevice: DeviceItem
|
||||
for (i in 0 until devices.length) {
|
||||
currentDevice = devices.getDeviceByIndex(i)
|
||||
if (currentDevice.mode == Global.TASMOTA) {
|
||||
listItems +=
|
||||
SimpleListItem(
|
||||
title = currentDevice.name,
|
||||
summary = currentDevice.address,
|
||||
hidden = currentDevice.id,
|
||||
icon = currentDevice.iconId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
recyclerView.adapter = SimpleListAdapter(listItems, this)
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
) {
|
||||
if (deviceId == null) {
|
||||
deviceId = view.findViewById<TextView>(R.id.hidden).text.toString()
|
||||
Tasmota(this, deviceId ?: error("Impossible state."), null).loadList(
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
if (holder.response != null) {
|
||||
recyclerView.adapter =
|
||||
SimpleListAdapter(
|
||||
holder.response as List<SimpleListItem>,
|
||||
this@ShortcutTasmotaActivity,
|
||||
)
|
||||
} else {
|
||||
deviceId = null
|
||||
Toast.makeText(
|
||||
this@ShortcutTasmotaActivity,
|
||||
holder.errorMessage,
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
val device = Devices(this).getDeviceById(deviceId ?: error("Impossible state."))
|
||||
val lampName = view.findViewById<TextView>(R.id.title).text
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val shortcutManager = this.getSystemService(ShortcutManager::class.java)
|
||||
if (shortcutManager != null) {
|
||||
setResult(
|
||||
RESULT_OK,
|
||||
shortcutManager.createShortcutResultIntent(
|
||||
ShortcutInfo.Builder(this, device.id + lampName)
|
||||
.setShortLabel(lampName)
|
||||
.setLongLabel(lampName)
|
||||
.setIcon(Icon.createWithResource(this, device.iconId))
|
||||
.setIntent(
|
||||
Intent(this, ShortcutTasmotaActionActivity::class.java)
|
||||
.putExtra(
|
||||
"command",
|
||||
view.findViewById<TextView>(R.id.summary).text,
|
||||
)
|
||||
.putExtra(Devices.INTENT_EXTRA_DEVICE, device.id)
|
||||
.setAction(Intent.ACTION_MAIN)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
|
||||
)
|
||||
.build(),
|
||||
),
|
||||
)
|
||||
finish()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.pref_add_shortcut_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.webkit.ValueCallback
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import android.widget.ProgressBar
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.net.toUri
|
||||
import io.github.domi04151309.home.R
|
||||
|
||||
class WebActivity : BaseActivity() {
|
||||
private var valueCallback: ValueCallback<Array<Uri>>? = null
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var webViewClient: WebActivityWebViewClient
|
||||
private lateinit var resultLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_web)
|
||||
|
||||
webViewClient =
|
||||
WebActivityWebViewClient(
|
||||
this,
|
||||
intent,
|
||||
findViewById<ProgressBar>(R.id.progressBar),
|
||||
findViewById<ProgressBar>(R.id.error),
|
||||
)
|
||||
|
||||
webView = findViewById(R.id.webView)
|
||||
webView.settings.javaScriptEnabled = true
|
||||
webView.settings.domStorageEnabled = true
|
||||
webView.webViewClient = webViewClient
|
||||
|
||||
resultLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val path = result.data?.data
|
||||
valueCallback?.onReceiveValue(
|
||||
if (path == null) {
|
||||
arrayOf()
|
||||
} else {
|
||||
arrayOf(path)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
webView.webChromeClient =
|
||||
object : WebChromeClient() {
|
||||
override fun onShowFileChooser(
|
||||
webView: WebView?,
|
||||
filePathCallback: ValueCallback<Array<Uri>>?,
|
||||
fileChooserParams: FileChooserParams?,
|
||||
): Boolean = showFileChooser(filePathCallback)
|
||||
}
|
||||
|
||||
webView.setDownloadListener { url, _, _, _, _ ->
|
||||
onDownload(url)
|
||||
}
|
||||
|
||||
webView.loadUrl(intent.getStringExtra("URI") ?: ABOUT_BLANK)
|
||||
title = intent.getStringExtra("title")
|
||||
}
|
||||
|
||||
internal fun showFileChooser(filePathCallback: ValueCallback<Array<Uri>>?): Boolean {
|
||||
valueCallback = filePathCallback
|
||||
resultLauncher.launch(
|
||||
Intent(Intent.ACTION_CHOOSER)
|
||||
.putExtra(
|
||||
Intent.EXTRA_INTENT,
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = "*/*"
|
||||
},
|
||||
)
|
||||
.putExtra(Intent.EXTRA_TITLE, "Image Chooser"),
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun onDownload(url: String) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
) != PackageManager.PERMISSION_GRANTED ||
|
||||
ActivityCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
),
|
||||
1,
|
||||
)
|
||||
}
|
||||
|
||||
val uri = url.toUri()
|
||||
(getSystemService(DOWNLOAD_SERVICE) as DownloadManager).enqueue(
|
||||
DownloadManager.Request(uri).apply {
|
||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
uri.lastPathSegment,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.activity_web_actions, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.action_open) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, webView.url?.toUri()))
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onKeyDown(
|
||||
keyCode: Int,
|
||||
event: KeyEvent,
|
||||
): Boolean {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack() && !webViewClient.hasError) {
|
||||
webView.goBack()
|
||||
return true
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ABOUT_BLANK = "about:blank"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package io.github.domi04151309.home.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.http.SslError
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.HttpAuthHandler
|
||||
import android.webkit.SslErrorHandler
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.helpers.DeviceSecrets
|
||||
|
||||
class WebActivityWebViewClient(
|
||||
private val context: Context,
|
||||
private val intent: Intent,
|
||||
private val progressView: View,
|
||||
private val errorView: View,
|
||||
) :
|
||||
WebViewClient() {
|
||||
var hasError: Boolean = false
|
||||
private set
|
||||
|
||||
private var isFirstLoad: Boolean = true
|
||||
private val nullParent: ViewGroup? = null
|
||||
|
||||
override fun onPageFinished(
|
||||
view: WebView,
|
||||
url: String,
|
||||
) {
|
||||
if (url == ABOUT_BLANK) {
|
||||
view.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
if (isFirstLoad) {
|
||||
if (intent.hasExtra("fritz_auto_login")) {
|
||||
injectFritzLogin(
|
||||
view,
|
||||
DeviceSecrets(
|
||||
context,
|
||||
intent.getStringExtra("fritz_auto_login") ?: "",
|
||||
).password,
|
||||
)
|
||||
} else if (intent.hasExtra("grafana_auto_login")) {
|
||||
val secrets =
|
||||
DeviceSecrets(
|
||||
context,
|
||||
intent.getStringExtra("grafana_auto_login") ?: "",
|
||||
)
|
||||
injectGrafanaLogin(
|
||||
view,
|
||||
secrets.username,
|
||||
secrets.password,
|
||||
)
|
||||
} else if (intent.hasExtra("pi_hole_auto_login")) {
|
||||
injectPiHoleLogin(
|
||||
view,
|
||||
DeviceSecrets(
|
||||
context,
|
||||
intent.getStringExtra("pi_hole_auto_login") ?: "",
|
||||
).password,
|
||||
)
|
||||
}
|
||||
isFirstLoad = false
|
||||
}
|
||||
|
||||
progressView.visibility = View.GONE
|
||||
view.visibility = View.VISIBLE
|
||||
super.onPageFinished(view, url)
|
||||
}
|
||||
|
||||
override fun onReceivedHttpAuthRequest(
|
||||
view: WebView,
|
||||
handler: HttpAuthHandler,
|
||||
host: String,
|
||||
realm: String,
|
||||
) {
|
||||
val dialogView =
|
||||
LayoutInflater.from(context)
|
||||
.inflate(R.layout.dialog_web_authentication, nullParent, false)
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.webView_authentication)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
handler.proceed(
|
||||
dialogView.findViewById<EditText>(R.id.username).text.toString(),
|
||||
dialogView.findViewById<EditText>(R.id.password).text.toString(),
|
||||
)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
@SuppressLint("WebViewClientOnReceivedSslError")
|
||||
override fun onReceivedSslError(
|
||||
view: WebView,
|
||||
handler: SslErrorHandler,
|
||||
error: SslError,
|
||||
) {
|
||||
Toast.makeText(context, R.string.webView_ssl_error, Toast.LENGTH_LONG)
|
||||
.show()
|
||||
handler.proceed()
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError,
|
||||
) {
|
||||
view.loadUrl(ABOUT_BLANK)
|
||||
hasError = true
|
||||
progressView.visibility = View.GONE
|
||||
errorView.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun injectFritzLogin(
|
||||
webView: WebView,
|
||||
password: String,
|
||||
) {
|
||||
webView.evaluateJavascript(
|
||||
"""
|
||||
document.getElementById('uiPass').value = '$password';
|
||||
document.getElementById('submitLoginBtn').click();
|
||||
""",
|
||||
) {}
|
||||
}
|
||||
|
||||
private fun injectGrafanaLogin(
|
||||
webView: WebView,
|
||||
username: String,
|
||||
password: String,
|
||||
) {
|
||||
webView.evaluateJavascript(
|
||||
"""
|
||||
function setNativeValue(element, value) {
|
||||
const proto = Object.getPrototypeOf(element);
|
||||
const descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
|
||||
const setter = descriptor && descriptor.set;
|
||||
|
||||
if (setter) {
|
||||
setter.call(element, value);
|
||||
} else {
|
||||
element.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
const check = setInterval(() => {
|
||||
const username = document.querySelector('input[name=user]');
|
||||
const password = document.querySelector('input[name=password]');
|
||||
if (username && password) {
|
||||
clearInterval(check);
|
||||
|
||||
setNativeValue(username, '$username');
|
||||
setNativeValue(password, '$password');
|
||||
|
||||
['input', 'change'].forEach(eventName => {
|
||||
username.dispatchEvent(new Event(eventName, { bubbles: true }));
|
||||
password.dispatchEvent(new Event(eventName, { bubbles: true }));
|
||||
});
|
||||
|
||||
document.querySelector('button[type=submit]').click();
|
||||
}
|
||||
}, 100);
|
||||
""",
|
||||
) {}
|
||||
}
|
||||
|
||||
private fun injectPiHoleLogin(
|
||||
webView: WebView,
|
||||
password: String,
|
||||
) {
|
||||
webView.evaluateJavascript(
|
||||
"""
|
||||
document.getElementById('current-password').value = '$password';
|
||||
document.querySelector('button[type=submit]').click();
|
||||
""",
|
||||
) {}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ABOUT_BLANK = "about:blank"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package io.github.domi04151309.home.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
|
||||
class DeviceDiscoveryListAdapter(
|
||||
private val items: MutableList<ListViewItem>,
|
||||
private val helperInterface: RecyclerViewHelperInterface,
|
||||
) : RecyclerView.Adapter<DeviceDiscoveryListAdapter.ViewHolder>() {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder =
|
||||
ViewHolder(
|
||||
LayoutInflater
|
||||
.from(parent.context)
|
||||
.inflate(R.layout.list_item_device_discovery, parent, false),
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
holder.drawable.setImageResource(items[position].icon)
|
||||
holder.title.text = items[position].title
|
||||
holder.summary.text = items[position].summary
|
||||
holder.hidden.text = items[position].hidden
|
||||
holder.stateDrawable.setImageResource(
|
||||
if (items[position].state == true) {
|
||||
R.drawable.ic_done
|
||||
} else {
|
||||
android.R.color.transparent
|
||||
},
|
||||
)
|
||||
holder.itemView.setOnClickListener { helperInterface.onItemClicked(holder.itemView, position) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
fun add(item: ListViewItem): Int {
|
||||
items.add(item)
|
||||
notifyItemInserted(items.size - 1)
|
||||
return items.size - 1
|
||||
}
|
||||
|
||||
fun changeState(
|
||||
i: Int,
|
||||
state: Boolean,
|
||||
) {
|
||||
items[i].state = state
|
||||
notifyItemChanged(i)
|
||||
}
|
||||
|
||||
fun changeTitle(
|
||||
i: Int,
|
||||
title: String,
|
||||
) {
|
||||
items[i].title = title
|
||||
notifyItemChanged(i)
|
||||
}
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val drawable: ImageView = view.findViewById(R.id.drawable)
|
||||
val title: TextView = view.findViewById(R.id.title)
|
||||
val summary: TextView = view.findViewById(R.id.summary)
|
||||
val hidden: TextView = view.findViewById(R.id.hidden)
|
||||
val stateDrawable: ImageView = view.findViewById(R.id.state)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package io.github.domi04151309.home.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.SimpleListItem
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterfaceAdvanced
|
||||
|
||||
class DeviceListAdapter(
|
||||
private val items: List<SimpleListItem>,
|
||||
private val helperInterface: RecyclerViewHelperInterfaceAdvanced,
|
||||
) : RecyclerView.Adapter<DeviceListAdapter.ViewHolder>() {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder =
|
||||
ViewHolder(
|
||||
LayoutInflater
|
||||
.from(parent.context)
|
||||
.inflate(R.layout.list_item_devices, parent, false),
|
||||
)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
holder.drawable.setImageResource(items[position].icon)
|
||||
holder.title.text = items[position].title
|
||||
holder.summary.text = items[position].summary
|
||||
holder.hidden.text = items[position].hidden
|
||||
holder.itemView.setOnClickListener { helperInterface.onItemClicked(holder.itemView, position) }
|
||||
if (position == itemCount - 1) {
|
||||
holder.handle.visibility = View.GONE
|
||||
} else {
|
||||
holder.handle.setOnTouchListener { view, event ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
helperInterface.onItemHandleTouched(holder)
|
||||
}
|
||||
view.performClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val drawable: ImageView = view.findViewById(R.id.drawable)
|
||||
val title: TextView = view.findViewById(R.id.title)
|
||||
val summary: TextView = view.findViewById(R.id.summary)
|
||||
val hidden: TextView = view.findViewById(R.id.hidden)
|
||||
val handle: ImageView = view.findViewById(R.id.handle)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package io.github.domi04151309.home.adapters
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import io.github.domi04151309.home.fragments.HueColorFragment
|
||||
import io.github.domi04151309.home.fragments.HueLampsFragment
|
||||
import io.github.domi04151309.home.fragments.HueScenesFragment
|
||||
import io.github.domi04151309.home.interfaces.HueRoomInterface
|
||||
|
||||
class HueDetailsTabAdapter(
|
||||
activity: FragmentActivity,
|
||||
private val lampInterface: HueRoomInterface,
|
||||
) : FragmentStateAdapter(activity) {
|
||||
override fun createFragment(position: Int): Fragment =
|
||||
when (position) {
|
||||
0 -> HueColorFragment(lampInterface)
|
||||
1 -> HueScenesFragment(lampInterface)
|
||||
2 -> HueLampsFragment(lampInterface)
|
||||
else -> Fragment()
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 3
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package io.github.domi04151309.home.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
|
||||
class HueLampListAdapter(
|
||||
private val stateListener: CompoundButton.OnCheckedChangeListener,
|
||||
private val helperInterface: RecyclerViewHelperInterface,
|
||||
) : RecyclerView.Adapter<HueLampListAdapter.ViewHolder>() {
|
||||
private var items: List<ListViewItem> = mutableListOf()
|
||||
private var colors: List<Int> = mutableListOf()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder =
|
||||
ViewHolder(
|
||||
LayoutInflater
|
||||
.from(parent.context)
|
||||
.inflate(R.layout.list_item, parent, false),
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
val context = holder.itemView.context
|
||||
val finalDrawable =
|
||||
LayerDrawable(
|
||||
arrayOf(
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_hue_lamp_base),
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_hue_lamp_color),
|
||||
),
|
||||
)
|
||||
finalDrawable.getDrawable(1).setTint(colors[position])
|
||||
holder.drawable.setImageDrawable(finalDrawable)
|
||||
holder.title.text = items[position].title
|
||||
holder.summary.text = items[position].summary
|
||||
holder.hidden.text = items[position].hidden
|
||||
holder.stateSwitch.isChecked = items[position].state == true
|
||||
holder.stateSwitch.setOnCheckedChangeListener(stateListener)
|
||||
holder.itemView.setOnClickListener { helperInterface.onItemClicked(holder.itemView, position) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateData(
|
||||
recyclerView: RecyclerView,
|
||||
newItems: List<ListViewItem>,
|
||||
newColors: List<Int>,
|
||||
) {
|
||||
if (newItems.size != items.size) {
|
||||
items = newItems
|
||||
colors = newColors
|
||||
notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
|
||||
val changed = mutableListOf<Int>()
|
||||
for (i in items.indices) {
|
||||
if (items[i].hidden != newItems[i].hidden) {
|
||||
changed.add(i)
|
||||
} else {
|
||||
val holder = (recyclerView.findViewHolderForAdapterPosition(i) ?: return) as ViewHolder
|
||||
if (items[i].summary != newItems[i].summary) {
|
||||
holder.summary.text = newItems[i].summary
|
||||
}
|
||||
if (items[i].state != newItems[i].state) {
|
||||
holder.stateSwitch.isChecked = newItems[i].state == true
|
||||
}
|
||||
if (colors[i] != newColors[i]) {
|
||||
val context = holder.itemView.context
|
||||
val finalDrawable =
|
||||
LayerDrawable(
|
||||
arrayOf(
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_hue_lamp_base),
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_hue_lamp_color),
|
||||
),
|
||||
)
|
||||
finalDrawable.getDrawable(1).setTint(newColors[i])
|
||||
holder.drawable.setImageDrawable(finalDrawable)
|
||||
}
|
||||
}
|
||||
}
|
||||
items = newItems
|
||||
colors = newColors
|
||||
changed.forEach(::notifyItemChanged)
|
||||
}
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val drawable: ImageView = view.findViewById(R.id.drawable)
|
||||
val title: TextView = view.findViewById(R.id.title)
|
||||
val summary: TextView = view.findViewById(R.id.summary)
|
||||
val hidden: TextView = view.findViewById(R.id.hidden)
|
||||
val stateSwitch: MaterialSwitch = view.findViewById(R.id.state)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package io.github.domi04151309.home.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.SceneGridItem
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
|
||||
class HueSceneGridAdapter(
|
||||
private val contextMenuListener: View.OnCreateContextMenuListener,
|
||||
private val helperInterface: RecyclerViewHelperInterface,
|
||||
) : RecyclerView.Adapter<HueSceneGridAdapter.ViewHolder>() {
|
||||
private var items: MutableList<SceneGridItem> = mutableListOf()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder =
|
||||
ViewHolder(
|
||||
LayoutInflater
|
||||
.from(parent.context)
|
||||
.inflate(R.layout.grid_item, parent, false),
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
val context = holder.itemView.context
|
||||
holder.title.text = items[position].name
|
||||
holder.hidden.text = items[position].hidden
|
||||
if (items[position].color == null) {
|
||||
holder.drawable.setImageResource(R.drawable.ic_hue_scene_add)
|
||||
} else {
|
||||
val finalDrawable =
|
||||
LayerDrawable(
|
||||
arrayOf(
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_hue_scene_base),
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_hue_scene_color),
|
||||
),
|
||||
)
|
||||
finalDrawable.getDrawable(1).setTint(items[position].color ?: Color.WHITE)
|
||||
holder.drawable.setImageDrawable(finalDrawable)
|
||||
}
|
||||
holder.itemView.setOnClickListener { helperInterface.onItemClicked(holder.itemView, position) }
|
||||
holder.itemView.setOnCreateContextMenuListener(contextMenuListener)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateData(newItems: MutableList<SceneGridItem>) {
|
||||
if (newItems.size != items.size) {
|
||||
items = newItems
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
val changed = mutableListOf<Int>()
|
||||
for (i in items.indices) {
|
||||
if (items[i] != newItems[i]) changed.add(i)
|
||||
}
|
||||
items = newItems
|
||||
changed.forEach(::notifyItemChanged)
|
||||
}
|
||||
}
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val drawable: ImageView = view.findViewById(R.id.drawable)
|
||||
val title: TextView = view.findViewById(R.id.title)
|
||||
val hidden: TextView = view.findViewById(R.id.hidden)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package io.github.domi04151309.home.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.ImageViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.SceneListItem
|
||||
import io.github.domi04151309.home.interfaces.SceneRecyclerViewHelperInterface
|
||||
|
||||
class HueSceneLampListAdapter(
|
||||
private var items: List<SceneListItem>,
|
||||
private var helperInterface: SceneRecyclerViewHelperInterface,
|
||||
) : RecyclerView.Adapter<HueSceneLampListAdapter.ViewHolder>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long =
|
||||
(position.toString() + '#' + items[position].hidden)
|
||||
.hashCode()
|
||||
.toLong()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder =
|
||||
ViewHolder(
|
||||
LayoutInflater
|
||||
.from(parent.context)
|
||||
.inflate(R.layout.list_item, parent, false),
|
||||
)
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
val id = getItemId(position)
|
||||
holder.drawable.setImageResource(R.drawable.ic_circle)
|
||||
holder.title.text = items[position].title
|
||||
holder.summary.text = generateSummary(holder.itemView.resources, items[position])
|
||||
holder.hidden.text = items[position].hidden
|
||||
holder.stateSwitch.isChecked = items[position].state
|
||||
holder.stateSwitch.setOnCheckedChangeListener { compoundButton, b ->
|
||||
items[getPosFromId(id)].state = b
|
||||
holder.summary.text = generateSummary(holder.itemView.resources, items[getPosFromId(id)])
|
||||
if (compoundButton.isPressed) {
|
||||
helperInterface.onStateChanged(
|
||||
holder.itemView,
|
||||
items[getPosFromId(id)],
|
||||
b,
|
||||
)
|
||||
}
|
||||
}
|
||||
ImageViewCompat.setImageTintList(
|
||||
holder.drawable,
|
||||
ColorStateList.valueOf(items[position].color),
|
||||
)
|
||||
holder.itemView.setOnClickListener {
|
||||
helperInterface.onItemClicked(holder.itemView, items[getPosFromId(id)])
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
fun changeSceneBrightness(brightness: String) {
|
||||
for (i in items.indices) {
|
||||
items[i].brightness = brightness
|
||||
if (items[i].state) notifyItemChanged(i)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBrightness(
|
||||
id: String,
|
||||
brightness: String,
|
||||
) {
|
||||
val i = items.indexOfFirst { it.hidden == id }
|
||||
items[i].brightness = brightness
|
||||
if (items[i].state) notifyItemChanged(i)
|
||||
}
|
||||
|
||||
fun updateColor(
|
||||
id: String,
|
||||
color: Int,
|
||||
) {
|
||||
val i = items.indexOfFirst { it.hidden == id }
|
||||
items[i].color = color
|
||||
notifyItemChanged(i)
|
||||
}
|
||||
|
||||
private fun generateSummary(
|
||||
resources: Resources,
|
||||
item: SceneListItem,
|
||||
): String =
|
||||
resources.getString(R.string.hue_brightness) +
|
||||
": " + if (item.state) item.brightness else "0 %"
|
||||
|
||||
private fun getPosFromId(id: Long): Int = items.indices.indexOfFirst { getItemId(it) == id }
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val drawable: ImageView = view.findViewById(R.id.drawable)
|
||||
val title: TextView = view.findViewById(R.id.title)
|
||||
val summary: TextView = view.findViewById(R.id.summary)
|
||||
val hidden: TextView = view.findViewById(R.id.hidden)
|
||||
val stateSwitch: MaterialSwitch = view.findViewById(R.id.state)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package io.github.domi04151309.home.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
|
||||
internal class IconSpinnerAdapter(
|
||||
private var itemArray: Array<String>,
|
||||
) : BaseAdapter(), Filterable {
|
||||
override fun getCount(): Int = itemArray.size
|
||||
|
||||
override fun getItem(position: Int): String = itemArray[position]
|
||||
|
||||
override fun getItemId(position: Int): Long = position.toLong()
|
||||
|
||||
override fun getFilter(): Filter = ItemFilter()
|
||||
|
||||
override fun getView(
|
||||
position: Int,
|
||||
convertView: View?,
|
||||
parent: ViewGroup,
|
||||
): View {
|
||||
val vi: View =
|
||||
convertView
|
||||
?: LayoutInflater.from(parent.context).inflate(R.layout.icon_dropdown_item, parent, false)
|
||||
vi.findViewById<ImageView>(R.id.drawable).setImageResource(Global.getIcon(itemArray[position]))
|
||||
vi.findViewById<TextView>(R.id.title).text = itemArray[position]
|
||||
return vi
|
||||
}
|
||||
|
||||
inner class ItemFilter : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence): FilterResults {
|
||||
val results = FilterResults()
|
||||
val search = constraint.toString().lowercase()
|
||||
|
||||
val items: ArrayList<String> = ArrayList(itemArray.size)
|
||||
|
||||
for (i in itemArray.indices) {
|
||||
if (itemArray[i].lowercase().contains(search)) items.add(itemArray[i])
|
||||
}
|
||||
|
||||
results.values = items.toArray()
|
||||
results.count = items.size
|
||||
return results
|
||||
}
|
||||
|
||||
override fun publishResults(
|
||||
constraint: CharSequence,
|
||||
results: FilterResults,
|
||||
) {
|
||||
itemArray = (results.values as Array<*>).filterIsInstance<String>().toTypedArray()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
package io.github.domi04151309.home.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationSet
|
||||
import android.view.animation.TranslateAnimation
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class MainListAdapter(private var attachedTo: RecyclerView) : RecyclerView.Adapter<MainListAdapter.ViewHolder>() {
|
||||
private var items: MutableList<ListViewItem> = mutableListOf()
|
||||
private var helperInterface: HomeRecyclerViewHelperInterface? = null
|
||||
private var animate: Boolean = true
|
||||
private var offsets: IntArray = intArrayOf()
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long =
|
||||
(position.toString() + '#' + items[position].hidden)
|
||||
.hashCode()
|
||||
.toLong()
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder =
|
||||
ViewHolder(
|
||||
LayoutInflater
|
||||
.from(parent.context)
|
||||
.inflate(R.layout.list_item, parent, false),
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
holder.drawable.setImageResource(items[position].icon)
|
||||
holder.title.text = items[position].title
|
||||
holder.summary.text = items[position].summary
|
||||
holder.hidden.text = items[position].hidden
|
||||
|
||||
val id = getItemId(position)
|
||||
if (items[position].state != null) {
|
||||
holder.stateSwitch.isChecked = items[position].state == true
|
||||
holder.stateSwitch.setOnCheckedChangeListener { compoundButton, b ->
|
||||
if (compoundButton.isPressed) {
|
||||
helperInterface?.onStateChanged(
|
||||
holder.itemView,
|
||||
items[getPosFromId(id)],
|
||||
b,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
holder.stateSwitch.visibility = View.GONE
|
||||
}
|
||||
holder.itemView.setOnClickListener {
|
||||
helperInterface?.onItemClicked(holder.itemView, items[getPosFromId(id)])
|
||||
}
|
||||
|
||||
holder.itemView.setOnCreateContextMenuListener(holder.itemView.context as Activity)
|
||||
if (animate) playAnimation(holder.itemView)
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: ViewHolder) {
|
||||
holder.stateSwitch.visibility = View.VISIBLE
|
||||
super.onViewRecycled(holder)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
attachedTo = recyclerView
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun updateData(
|
||||
newItems: List<ListViewItem>,
|
||||
newHelperInterface: HomeRecyclerViewHelperInterface? = null,
|
||||
preferredAnimationState: Boolean? = null,
|
||||
) {
|
||||
offsets = IntArray(newItems.size) { 1 }
|
||||
if (newHelperInterface != null) {
|
||||
attachedTo.layoutManager?.scrollToPosition(0)
|
||||
animate = preferredAnimationState != false
|
||||
items.clear()
|
||||
items.addAll(newItems)
|
||||
helperInterface = newHelperInterface
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
animate = preferredAnimationState == true
|
||||
val changed = mutableListOf<Int>()
|
||||
items.clear()
|
||||
for (i in 0 until items.size) {
|
||||
if (items[i] != newItems[i]) changed.add(i)
|
||||
items.add(newItems[i])
|
||||
}
|
||||
changed.forEach(::notifyItemChanged)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateItem(
|
||||
position: Int,
|
||||
item: ListViewItem,
|
||||
) {
|
||||
if (position > items.size - 1) {
|
||||
Log.w(Global.LOG_TAG, "The position $position is larger than the item count")
|
||||
return
|
||||
}
|
||||
|
||||
if (items[position].summary == item.summary && items[position].state == item.state) {
|
||||
return
|
||||
}
|
||||
|
||||
items[position].summary = item.summary
|
||||
items[position].state = item.state
|
||||
|
||||
val viewHolder = attachedTo.findViewHolderForAdapterPosition(position) as? ViewHolder
|
||||
if (viewHolder == null) {
|
||||
notifyItemChanged(position)
|
||||
} else {
|
||||
viewHolder.summary.text = item.summary
|
||||
viewHolder.stateSwitch.isChecked = item.state == true
|
||||
}
|
||||
}
|
||||
|
||||
fun getOffset(pos: Int): Int = offsets.copyOfRange(0, pos).sum()
|
||||
|
||||
fun updateDirectView(
|
||||
id: String,
|
||||
newItems: List<ListViewItem>,
|
||||
directViewPos: Int,
|
||||
) {
|
||||
newItems.forEach { it.hidden = id + '@' + it.hidden }
|
||||
|
||||
val correctOffset = getOffset(directViewPos)
|
||||
val directViewSize = offsets[directViewPos]
|
||||
for (i in 0 until directViewSize) {
|
||||
items.removeAt(correctOffset)
|
||||
notifyItemRemoved(correctOffset)
|
||||
}
|
||||
offsets[directViewPos] = newItems.size
|
||||
items.addAll(correctOffset, newItems)
|
||||
notifyItemRangeInserted(correctOffset, newItems.size)
|
||||
}
|
||||
|
||||
private fun getPosFromId(id: Long): Int = items.indices.indexOfFirst { getItemId(it) == id }
|
||||
|
||||
fun getDirectViewPos(deviceId: String): Int {
|
||||
var currentPos = 0
|
||||
for (i in offsets.indices) {
|
||||
if (items[currentPos].hidden.contains(deviceId)) return i
|
||||
currentPos += offsets[i]
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun playAnimation(v: View) {
|
||||
val set = AnimationSet(true)
|
||||
|
||||
val firstAnimation: Animation = AlphaAnimation(0.0f, 1.0f)
|
||||
firstAnimation.duration = ANIMATION_DURATION
|
||||
set.addAnimation(firstAnimation)
|
||||
|
||||
val secondAnimation =
|
||||
TranslateAnimation(
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
-1.0f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.0f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.0f,
|
||||
Animation.RELATIVE_TO_SELF,
|
||||
0.0f,
|
||||
)
|
||||
secondAnimation.duration = ANIMATION_DURATION
|
||||
set.addAnimation(secondAnimation)
|
||||
|
||||
v.startAnimation(set)
|
||||
}
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val drawable: ImageView = view.findViewById(R.id.drawable)
|
||||
val title: TextView = view.findViewById(R.id.title)
|
||||
val summary: TextView = view.findViewById(R.id.summary)
|
||||
val hidden: TextView = view.findViewById(R.id.hidden)
|
||||
val stateSwitch: MaterialSwitch = view.findViewById(R.id.state)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ANIMATION_DURATION = 300L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package io.github.domi04151309.home.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.SimpleListItem
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
|
||||
class SimpleListAdapter(
|
||||
private val items: List<SimpleListItem>,
|
||||
private val helperInterface: RecyclerViewHelperInterface,
|
||||
) : RecyclerView.Adapter<SimpleListAdapter.ViewHolder>() {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int,
|
||||
): ViewHolder =
|
||||
ViewHolder(
|
||||
LayoutInflater
|
||||
.from(parent.context)
|
||||
.inflate(R.layout.list_item_simple, parent, false),
|
||||
)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(
|
||||
holder: ViewHolder,
|
||||
position: Int,
|
||||
) {
|
||||
holder.drawable.setImageResource(items[position].icon)
|
||||
holder.title.text = items[position].title
|
||||
holder.summary.text = items[position].summary
|
||||
holder.hidden.text = items[position].hidden
|
||||
holder.itemView.setOnClickListener { helperInterface.onItemClicked(holder.itemView, position) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val drawable: ImageView = view.findViewById(R.id.drawable)
|
||||
val title: TextView = view.findViewById(R.id.title)
|
||||
val summary: TextView = view.findViewById(R.id.summary)
|
||||
val hidden: TextView = view.findViewById(R.id.hidden)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
|
||||
class EspEasyAPI(
|
||||
c: Context,
|
||||
deviceId: String,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) : UnifiedAPI(c, deviceId, recyclerViewInterface) {
|
||||
private val parser = EspEasyAPIParser(c.resources, this)
|
||||
|
||||
override fun loadList(
|
||||
callback: CallbackInterface,
|
||||
extended: Boolean,
|
||||
) {
|
||||
super.loadList(callback, extended)
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "json",
|
||||
null,
|
||||
{ infoResponse ->
|
||||
val listItems = parser.parseResponse(infoResponse)
|
||||
updateCache(listItems)
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
listItems,
|
||||
deviceId,
|
||||
),
|
||||
recyclerViewInterface,
|
||||
)
|
||||
},
|
||||
{ error ->
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
null,
|
||||
deviceId,
|
||||
Global.volleyError(c, error),
|
||||
),
|
||||
null,
|
||||
)
|
||||
},
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
override fun loadStates(
|
||||
callback: RealTimeStatesCallback,
|
||||
offset: Int,
|
||||
) {
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "json",
|
||||
null,
|
||||
{ infoResponse ->
|
||||
callback.onStatesLoaded(
|
||||
parser.parseResponse(infoResponse),
|
||||
offset,
|
||||
)
|
||||
},
|
||||
{ },
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
override fun changeSwitchState(
|
||||
id: String,
|
||||
state: Boolean,
|
||||
) {
|
||||
val switchUrl = url + "control?cmd=GPIO," + id + "," + if (state) "1" else "0"
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
switchUrl,
|
||||
{ },
|
||||
{ e -> Log.e(Global.LOG_TAG, e.toString()) },
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.res.Resources
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
class EspEasyAPIParser(resources: Resources, api: UnifiedAPI?) : UnifiedAPI.Parser(resources, api) {
|
||||
override fun parseResponse(response: JSONObject): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
|
||||
// sensors
|
||||
val sensors = response.optJSONArray("Sensors") ?: JSONArray()
|
||||
for (sensorId in 0 until sensors.length()) {
|
||||
val currentSensor = sensors.getJSONObject(sensorId)
|
||||
if (currentSensor.optString("TaskEnabled", FALSE).equals(FALSE)) {
|
||||
continue
|
||||
}
|
||||
|
||||
val type = currentSensor.optString("Type")
|
||||
if (type.startsWith("Environment")) {
|
||||
listItems.addAll(parseEnvironment(type, currentSensor))
|
||||
} else if (type.startsWith("Switch")) {
|
||||
listItems.addAll(parseSwitch(type, currentSensor))
|
||||
}
|
||||
}
|
||||
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun parseEnvironment(
|
||||
type: String,
|
||||
currentSensor: JSONObject,
|
||||
): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
var taskIcons = intArrayOf()
|
||||
when (type) {
|
||||
"Environment - BMx280" -> {
|
||||
taskIcons += R.drawable.ic_device_thermometer
|
||||
taskIcons += R.drawable.ic_device_hygrometer
|
||||
taskIcons += R.drawable.ic_device_gauge
|
||||
}
|
||||
"Environment - DHT11/12/22 SONOFF2301/7021" -> {
|
||||
taskIcons += R.drawable.ic_device_thermometer
|
||||
taskIcons += R.drawable.ic_device_hygrometer
|
||||
}
|
||||
"Environment - DS18b20" -> {
|
||||
taskIcons += R.drawable.ic_device_thermometer
|
||||
}
|
||||
}
|
||||
|
||||
val taskName = currentSensor.getString("TaskName")
|
||||
for (taskId in taskIcons.indices) {
|
||||
val currentTask = currentSensor.getJSONArray(TASK_VALUES).getJSONObject(taskId)
|
||||
val currentValue = currentTask.getString(VALUE)
|
||||
if (!currentValue.equals("nan")) {
|
||||
val suffix =
|
||||
when (taskIcons[taskId]) {
|
||||
R.drawable.ic_device_thermometer -> " °C"
|
||||
R.drawable.ic_device_hygrometer -> " %"
|
||||
R.drawable.ic_device_gauge -> " hPa"
|
||||
else -> ""
|
||||
}
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = currentValue + suffix,
|
||||
summary = taskName + ": " + currentTask.getString("Name"),
|
||||
icon = taskIcons[taskId],
|
||||
)
|
||||
}
|
||||
}
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun parseSwitch(
|
||||
type: String,
|
||||
currentSensor: JSONObject,
|
||||
): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
when (type) {
|
||||
"Switch input - Switch" -> {
|
||||
val currentState = currentSensor.getJSONArray(TASK_VALUES).getJSONObject(0).getInt(VALUE) > 0
|
||||
var taskName = currentSensor.getString("TaskName")
|
||||
var gpioId = ""
|
||||
val gpioFinder = Regex("~GPIO~([0-9]+)$")
|
||||
val matchResult = gpioFinder.find(taskName)
|
||||
if (matchResult != null && matchResult.groupValues.size > 1) {
|
||||
gpioId = matchResult.groupValues[1]
|
||||
taskName = taskName.replace("~GPIO~$gpioId", "")
|
||||
}
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = taskName,
|
||||
summary =
|
||||
resources.getString(
|
||||
if (currentState) {
|
||||
R.string.switch_summary_on
|
||||
} else {
|
||||
R.string.switch_summary_off
|
||||
},
|
||||
),
|
||||
hidden = gpioId,
|
||||
state = currentState,
|
||||
icon = R.drawable.ic_do,
|
||||
)
|
||||
api?.needsRealTimeData = true
|
||||
}
|
||||
}
|
||||
return listItems
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FALSE = "false"
|
||||
private const val TASK_VALUES = "TaskValues"
|
||||
private const val VALUE = "Value"
|
||||
}
|
||||
}
|
||||
275
app/src/main/java/io/github/domi04151309/home/api/HueAPI.kt
Normal file
275
app/src/main/java/io/github/domi04151309/home/api/HueAPI.kt
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.android.volley.ParseError
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import io.github.domi04151309.home.activities.HueConnectActivity
|
||||
import io.github.domi04151309.home.activities.HueLampActivity
|
||||
import io.github.domi04151309.home.custom.CustomJsonArrayRequest
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.Global.volleyError
|
||||
import io.github.domi04151309.home.helpers.HueUtils
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class HueAPI(
|
||||
c: Context,
|
||||
deviceId: String,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface? = null,
|
||||
) : UnifiedAPI(c, deviceId, recyclerViewInterface) {
|
||||
private val parser = HueAPIParser(c.resources)
|
||||
var readyForRequest: Boolean = true
|
||||
|
||||
init {
|
||||
needsRealTimeData = true
|
||||
}
|
||||
|
||||
interface RequestCallback {
|
||||
fun onLightsLoaded(response: JSONObject?)
|
||||
}
|
||||
|
||||
fun getUsername(): String =
|
||||
PreferenceManager.getDefaultSharedPreferences(c)
|
||||
.getString(deviceId, "")
|
||||
?: ""
|
||||
|
||||
// For unified API
|
||||
override fun loadList(
|
||||
callback: CallbackInterface,
|
||||
extended: Boolean,
|
||||
) {
|
||||
super.loadList(callback, extended)
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "api/${getUsername()}/groups",
|
||||
null,
|
||||
{ response ->
|
||||
val listItems = parser.parseResponse(response)
|
||||
updateCache(listItems)
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
listItems,
|
||||
deviceId,
|
||||
),
|
||||
recyclerViewInterface,
|
||||
)
|
||||
},
|
||||
{ error ->
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
null,
|
||||
deviceId,
|
||||
volleyError(c, error),
|
||||
),
|
||||
null,
|
||||
)
|
||||
if (error is ParseError) {
|
||||
c.startActivity(
|
||||
Intent(
|
||||
c,
|
||||
HueConnectActivity::class.java,
|
||||
).putExtra("deviceId", deviceId),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
override fun loadStates(
|
||||
callback: RealTimeStatesCallback,
|
||||
offset: Int,
|
||||
) {
|
||||
if (!readyForRequest) return
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "api/${getUsername()}/groups",
|
||||
null,
|
||||
{ response ->
|
||||
callback.onStatesLoaded(parser.parseResponse(response), offset)
|
||||
},
|
||||
{ },
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
override fun execute(
|
||||
path: String,
|
||||
callback: CallbackInterface,
|
||||
) {
|
||||
c.startActivity(
|
||||
Intent(c, HueLampActivity::class.java)
|
||||
.putExtra("id", path)
|
||||
.putExtra(Devices.INTENT_EXTRA_DEVICE, deviceId),
|
||||
)
|
||||
}
|
||||
|
||||
override fun changeSwitchState(
|
||||
id: String,
|
||||
state: Boolean,
|
||||
) {
|
||||
switchGroupById(id, state)
|
||||
}
|
||||
|
||||
override fun changePercentage(
|
||||
id: String,
|
||||
percentage: Float,
|
||||
) {
|
||||
changeBrightnessOfGroup(id, (percentage / MAX_PERCENTAGE * HueUtils.MAX_BRIGHTNESS).toInt())
|
||||
}
|
||||
|
||||
fun loadLightsByIds(
|
||||
lightIds: JSONArray,
|
||||
callback: RequestCallback,
|
||||
) {
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "api/${getUsername()}/lights",
|
||||
null,
|
||||
{ response ->
|
||||
val returnObject = JSONObject()
|
||||
var lightId: String
|
||||
for (i in 0 until lightIds.length()) {
|
||||
lightId = lightIds.getString(i)
|
||||
returnObject.put(lightId, response.getJSONObject(lightId))
|
||||
}
|
||||
callback.onLightsLoaded(returnObject)
|
||||
},
|
||||
{ callback.onLightsLoaded(null) },
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
fun switchLightById(
|
||||
lightId: String,
|
||||
on: Boolean,
|
||||
) {
|
||||
putObject(getLightPath(lightId), "{\"on\":$on}")
|
||||
}
|
||||
|
||||
fun changeBrightness(
|
||||
lightId: String,
|
||||
bri: Int,
|
||||
) {
|
||||
putObject(getLightPath(lightId), "{\"bri\":$bri}")
|
||||
}
|
||||
|
||||
fun changeColorTemperature(
|
||||
lightId: String,
|
||||
ct: Int,
|
||||
) {
|
||||
putObject(getLightPath(lightId), "{\"ct\":$ct}")
|
||||
}
|
||||
|
||||
fun changeHue(
|
||||
lightId: String,
|
||||
hue: Int,
|
||||
) {
|
||||
putObject(getLightPath(lightId), "{\"hue\":$hue}")
|
||||
}
|
||||
|
||||
fun changeSaturation(
|
||||
lightId: String,
|
||||
sat: Int,
|
||||
) {
|
||||
putObject(getLightPath(lightId), "{\"sat\":$sat}")
|
||||
}
|
||||
|
||||
fun changeHueSat(
|
||||
lightId: String,
|
||||
hue: Int,
|
||||
sat: Int,
|
||||
) {
|
||||
putObject(getLightPath(lightId), """{ "hue": $hue, "sat": $sat }""")
|
||||
}
|
||||
|
||||
fun switchGroupById(
|
||||
groupId: String,
|
||||
on: Boolean,
|
||||
) {
|
||||
putObject(getGroupPath(groupId), "{\"on\":$on}")
|
||||
}
|
||||
|
||||
fun changeBrightnessOfGroup(
|
||||
groupId: String,
|
||||
bri: Int,
|
||||
) {
|
||||
putObject(getGroupPath(groupId), "{\"bri\":$bri}")
|
||||
}
|
||||
|
||||
fun changeColorTemperatureOfGroup(
|
||||
groupId: String,
|
||||
ct: Int,
|
||||
) {
|
||||
putObject(getGroupPath(groupId), "{\"ct\":$ct}")
|
||||
}
|
||||
|
||||
fun changeHueOfGroup(
|
||||
groupId: String,
|
||||
hue: Int,
|
||||
) {
|
||||
putObject(getGroupPath(groupId), "{\"hue\":$hue}")
|
||||
}
|
||||
|
||||
fun changeSaturationOfGroup(
|
||||
groupId: String,
|
||||
sat: Int,
|
||||
) {
|
||||
putObject(getGroupPath(groupId), "{\"sat\":$sat}")
|
||||
}
|
||||
|
||||
fun changeHueSatOfGroup(
|
||||
groupId: String,
|
||||
hue: Int,
|
||||
sat: Int,
|
||||
) {
|
||||
putObject(getGroupPath(groupId), """{ "hue": $hue, "sat": $sat }""")
|
||||
}
|
||||
|
||||
fun activateSceneOfGroup(
|
||||
groupId: String,
|
||||
scene: String,
|
||||
) {
|
||||
putObject(getGroupPath(groupId), """{ "scene": $scene }""")
|
||||
}
|
||||
|
||||
private fun getLightPath(lightId: String) = "/lights/$lightId/state"
|
||||
|
||||
private fun getGroupPath(groupId: String) = "/groups/$groupId/action"
|
||||
|
||||
private fun putObject(
|
||||
address: String,
|
||||
requestObject: String,
|
||||
) {
|
||||
val request =
|
||||
CustomJsonArrayRequest(
|
||||
Request.Method.PUT,
|
||||
url + "api/${getUsername()}$address",
|
||||
JSONObject(requestObject),
|
||||
{ },
|
||||
{ e -> Log.e(Global.LOG_TAG, e.toString()) },
|
||||
)
|
||||
if (readyForRequest) {
|
||||
readyForRequest = false
|
||||
queue.add(request)
|
||||
Handler(Looper.getMainLooper()).postDelayed({ readyForRequest = true }, UPDATE_DELAY)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UPDATE_DELAY = 100L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.res.Resources
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.data.SimpleListItem
|
||||
import io.github.domi04151309.home.helpers.HueUtils
|
||||
import org.json.JSONObject
|
||||
import java.util.TreeMap
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class HueAPIParser(resources: Resources) : UnifiedAPI.Parser(resources) {
|
||||
override fun parseResponse(response: JSONObject): List<ListViewItem> {
|
||||
val listItems: ArrayList<ListViewItem> = ArrayList(response.length())
|
||||
val rooms: TreeMap<String, Pair<String, JSONObject>> = TreeMap()
|
||||
val zones: TreeMap<String, Pair<String, JSONObject>> = TreeMap()
|
||||
var currentObject: JSONObject
|
||||
for (i in response.keys()) {
|
||||
currentObject = response.getJSONObject(i)
|
||||
when (currentObject.getString("type")) {
|
||||
"Room" -> rooms[currentObject.getString("name")] = Pair(i, currentObject)
|
||||
"Zone" -> zones[currentObject.getString("name")] = Pair(i, currentObject)
|
||||
}
|
||||
}
|
||||
for (i in rooms.keys) listItems.add(
|
||||
parseGroupObj(
|
||||
rooms[i] ?: error("Room $i does not exist."),
|
||||
false,
|
||||
),
|
||||
)
|
||||
for (i in zones.keys) listItems.add(
|
||||
parseGroupObj(
|
||||
zones[i] ?: error("Zone $i does not exist."),
|
||||
true,
|
||||
),
|
||||
)
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun parseGroupObj(
|
||||
pair: Pair<String, JSONObject>,
|
||||
isZone: Boolean,
|
||||
): ListViewItem {
|
||||
val state = pair.second.optJSONObject(STATE)?.optBoolean(ANY_ON)
|
||||
val value =
|
||||
pair.second.optJSONObject(ACTION)?.optInt(
|
||||
BRI,
|
||||
HueUtils.MAX_BRIGHTNESS,
|
||||
) ?: HueUtils.MAX_BRIGHTNESS
|
||||
return ListViewItem(
|
||||
title = pair.second.getString("name"),
|
||||
summary =
|
||||
resources.getString(R.string.hue_brightness) +
|
||||
": " + if (state == true) HueUtils.briToPercent(value) else "0 %",
|
||||
hidden = pair.first,
|
||||
icon = if (isZone) R.drawable.ic_zone else R.drawable.ic_room,
|
||||
state = state,
|
||||
percentage = (value / HueUtils.MAX_BRIGHTNESS.toFloat() * 100).toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STATE = "state"
|
||||
private const val ANY_ON = "any_on"
|
||||
|
||||
private const val ACTION = "action"
|
||||
private const val BRI = "bri"
|
||||
|
||||
fun parseHueConfig(
|
||||
resources: Resources,
|
||||
response: JSONObject,
|
||||
): List<SimpleListItem> =
|
||||
listOf(
|
||||
SimpleListItem(summary = resources.getString(R.string.hue_bridge)),
|
||||
SimpleListItem(
|
||||
response.optString("name"),
|
||||
resources.getString(R.string.hue_bridge_name),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
response.optString("modelid"),
|
||||
resources.getString(R.string.hue_bridge_model),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
response.optString("bridgeid"),
|
||||
resources.getString(R.string.hue_bridge_id),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
response.optString("swversion"),
|
||||
resources.getString(R.string.hue_bridge_software),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
response.optString("zigbeechannel"),
|
||||
resources.getString(R.string.hue_bridge_zigbee),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
SimpleListItem(
|
||||
response.optString("timezone"),
|
||||
resources.getString(R.string.hue_bridge_time_zone),
|
||||
icon = R.drawable.ic_about_info,
|
||||
),
|
||||
)
|
||||
|
||||
fun parseHueSensors(
|
||||
resources: Resources,
|
||||
response: JSONObject,
|
||||
): List<SimpleListItem> {
|
||||
val sensorItems = mutableListOf<SimpleListItem>()
|
||||
for (i in response.keys()) {
|
||||
val current = response.optJSONObject(i) ?: JSONObject()
|
||||
val config = current.optJSONObject("config") ?: JSONObject()
|
||||
if (config.has("battery")) {
|
||||
sensorItems.add(
|
||||
SimpleListItem(
|
||||
current.optString("name"),
|
||||
config.optString("battery") + "%",
|
||||
icon =
|
||||
if (config.optBoolean("reachable")) {
|
||||
R.drawable.ic_device_raspberry_pi
|
||||
} else {
|
||||
R.drawable.ic_warning
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val items = mutableListOf(SimpleListItem(summary = resources.getString(R.string.hue_controls)))
|
||||
items.addAll(sensorItems.sortedBy { it.title })
|
||||
return items
|
||||
}
|
||||
|
||||
fun parseHueLights(
|
||||
resources: Resources,
|
||||
response: JSONObject,
|
||||
): List<SimpleListItem> {
|
||||
val lightItems = mutableListOf<SimpleListItem>()
|
||||
for (i in response.keys()) {
|
||||
val current =
|
||||
response.optJSONObject(i)
|
||||
?: JSONObject()
|
||||
val state =
|
||||
current.optJSONObject(STATE) ?: JSONObject()
|
||||
lightItems.add(
|
||||
SimpleListItem(
|
||||
current.optString("name"),
|
||||
(
|
||||
if (state.optBoolean("on")) {
|
||||
resources.getString(
|
||||
R.string.str_on,
|
||||
)
|
||||
} else {
|
||||
resources.getString(R.string.str_off)
|
||||
}
|
||||
) +
|
||||
" · " +
|
||||
current.optString("productname"),
|
||||
icon =
|
||||
if (state.optBoolean("reachable")) {
|
||||
R.drawable.ic_device_lamp
|
||||
} else {
|
||||
R.drawable.ic_warning
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
val items = mutableListOf(SimpleListItem(summary = resources.getString(R.string.hue_lights)))
|
||||
items.addAll(lightItems.sortedBy { it.title })
|
||||
return items
|
||||
}
|
||||
}
|
||||
}
|
||||
244
app/src/main/java/io/github/domi04151309/home/api/ShellyAPI.kt
Normal file
244
app/src/main/java/io/github/domi04151309/home/api/ShellyAPI.kt
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.Response
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import io.github.domi04151309.home.custom.JsonObjectRequestAuth
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.DeviceSecrets
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
|
||||
class ShellyAPI(
|
||||
c: Context,
|
||||
deviceId: String,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
private val version: Int,
|
||||
) : UnifiedAPI(c, deviceId, recyclerViewInterface) {
|
||||
private val secrets = DeviceSecrets(c, deviceId)
|
||||
private val parser = ShellyAPIParser(c.resources, version)
|
||||
|
||||
init {
|
||||
needsRealTimeData = true
|
||||
}
|
||||
|
||||
override fun loadList(
|
||||
callback: CallbackInterface,
|
||||
extended: Boolean,
|
||||
) {
|
||||
super.loadList(callback, extended)
|
||||
val jsonObjectRequest =
|
||||
when (version) {
|
||||
1 -> listRequestV1(callback)
|
||||
2 -> listRequestV2(callback)
|
||||
else -> null
|
||||
}
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
private fun listRequestV1(callback: CallbackInterface) =
|
||||
JsonObjectRequestAuth(
|
||||
Request.Method.GET,
|
||||
url + SETTINGS,
|
||||
secrets,
|
||||
null,
|
||||
{ settingsResponse ->
|
||||
queue.add(
|
||||
JsonObjectRequestAuth(
|
||||
Request.Method.GET,
|
||||
url + "status",
|
||||
secrets,
|
||||
null,
|
||||
{ statusResponse ->
|
||||
val listItems = parser.parseResponse(settingsResponse, statusResponse)
|
||||
updateCache(listItems)
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
listItems,
|
||||
deviceId,
|
||||
),
|
||||
recyclerViewInterface,
|
||||
)
|
||||
},
|
||||
{ error ->
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
null,
|
||||
deviceId,
|
||||
Global.volleyError(c, error),
|
||||
),
|
||||
null,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
{ error ->
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
null,
|
||||
deviceId,
|
||||
Global.volleyError(c, error),
|
||||
),
|
||||
null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
private fun listRequestV2(callback: CallbackInterface) =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "rpc/Shelly.GetConfig",
|
||||
null,
|
||||
{ configResponse ->
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "rpc/Shelly.GetStatus",
|
||||
null,
|
||||
{ statusResponse ->
|
||||
val listItems = parser.parseResponse(configResponse, statusResponse)
|
||||
updateCache(listItems)
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
listItems,
|
||||
deviceId,
|
||||
),
|
||||
recyclerViewInterface,
|
||||
)
|
||||
},
|
||||
{ error ->
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
null,
|
||||
deviceId,
|
||||
Global.volleyError(c, error),
|
||||
),
|
||||
null,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
{ error ->
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
null,
|
||||
deviceId,
|
||||
Global.volleyError(c, error),
|
||||
),
|
||||
null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
override fun loadStates(
|
||||
callback: RealTimeStatesCallback,
|
||||
offset: Int,
|
||||
) {
|
||||
val jsonObjectRequest =
|
||||
when (version) {
|
||||
1 ->
|
||||
JsonObjectRequestAuth(
|
||||
Request.Method.GET,
|
||||
url + SETTINGS,
|
||||
secrets,
|
||||
null,
|
||||
{ settingsResponse ->
|
||||
queue.add(
|
||||
JsonObjectRequestAuth(
|
||||
Request.Method.GET,
|
||||
url + "status",
|
||||
secrets,
|
||||
null,
|
||||
{ statusResponse ->
|
||||
callback.onStatesLoaded(
|
||||
parser.parseResponse(settingsResponse, statusResponse),
|
||||
offset,
|
||||
)
|
||||
},
|
||||
{ },
|
||||
),
|
||||
)
|
||||
},
|
||||
{ },
|
||||
)
|
||||
2 ->
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "rpc/Shelly.GetConfig",
|
||||
null,
|
||||
{ configResponse ->
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "rpc/Shelly.GetStatus",
|
||||
null,
|
||||
{ statusResponse ->
|
||||
callback.onStatesLoaded(
|
||||
parser.parseResponse(configResponse, statusResponse),
|
||||
offset,
|
||||
)
|
||||
},
|
||||
{ },
|
||||
),
|
||||
)
|
||||
},
|
||||
{ },
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
override fun changeSwitchState(
|
||||
id: String,
|
||||
state: Boolean,
|
||||
) {
|
||||
val requestUrl = url + "relay/$id?turn=" + if (state) "on" else "off"
|
||||
val jsonObjectRequest =
|
||||
when (version) {
|
||||
1 ->
|
||||
JsonObjectRequestAuth(
|
||||
Request.Method.GET,
|
||||
requestUrl,
|
||||
secrets,
|
||||
null,
|
||||
{ },
|
||||
{ e -> Log.e(Global.LOG_TAG, e.toString()) },
|
||||
)
|
||||
2 ->
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
requestUrl,
|
||||
null,
|
||||
{ },
|
||||
{ e -> Log.e(Global.LOG_TAG, e.toString()) },
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SETTINGS = "settings"
|
||||
|
||||
/**
|
||||
* Detect the name of the shelly device during discovery.
|
||||
*/
|
||||
fun loadName(
|
||||
url: String,
|
||||
version: Int,
|
||||
listener: Response.Listener<String>,
|
||||
): JsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
url + if (version == 1) SETTINGS else "shelly",
|
||||
{ statusResponse ->
|
||||
listener.onResponse(if (statusResponse.isNull("name")) "" else statusResponse.optString("name"))
|
||||
},
|
||||
{},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.res.Resources
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.DecimalFormat
|
||||
|
||||
class ShellyAPIParser(resources: Resources, private val version: Int) :
|
||||
UnifiedAPI.Parser(resources) {
|
||||
fun parseResponse(
|
||||
config: JSONObject,
|
||||
status: JSONObject,
|
||||
): List<ListViewItem> =
|
||||
if (version == 1) {
|
||||
parseResponseV1(config, status)
|
||||
} else {
|
||||
parseResponseV2(config, status)
|
||||
}
|
||||
|
||||
private fun parseResponseV1(
|
||||
settings: JSONObject,
|
||||
status: JSONObject,
|
||||
): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
listItems.addAll(parseSwitchesAndMetersV1(settings, status))
|
||||
listItems.addAll(parseTemperatureSensorsV1(status))
|
||||
listItems.addAll(parseHumiditySensorsV1(status))
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun parseSwitchesAndMetersV1(
|
||||
settings: JSONObject,
|
||||
status: JSONObject,
|
||||
): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
|
||||
// switches
|
||||
val relays = settings.optJSONArray("relays") ?: JSONArray()
|
||||
var currentRelay: JSONObject
|
||||
var currentState: Boolean
|
||||
var hideMeters = false
|
||||
for (relayId in 0 until relays.length()) {
|
||||
currentRelay = relays.getJSONObject(relayId)
|
||||
currentState = currentRelay.getBoolean("ison")
|
||||
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title =
|
||||
nameOrDefault(
|
||||
if (currentRelay.isNull("name")) "" else currentRelay.optString("name"),
|
||||
relayId,
|
||||
),
|
||||
summary =
|
||||
resources.getString(
|
||||
if (currentState) {
|
||||
R.string.switch_summary_on
|
||||
} else {
|
||||
R.string.switch_summary_off
|
||||
},
|
||||
),
|
||||
hidden = relayId.toString(),
|
||||
state = currentState,
|
||||
icon = Global.getIcon(currentRelay.optString("appliance_type"), R.drawable.ic_do),
|
||||
)
|
||||
// Shelly1 has the "user power constant" setting, but no actual meter
|
||||
hideMeters = currentRelay.has("power")
|
||||
}
|
||||
|
||||
// power meters
|
||||
val meters = if (hideMeters) JSONArray() else status.optJSONArray("meters") ?: JSONArray()
|
||||
var currentMeter: JSONObject
|
||||
for (meterId in 0 until meters.length()) {
|
||||
currentMeter = meters.getJSONObject(meterId)
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = "${currentMeter.getDouble("power")} W",
|
||||
summary = resources.getString(R.string.shelly_powermeter_summary),
|
||||
icon = R.drawable.ic_device_electricity,
|
||||
)
|
||||
}
|
||||
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun parseTemperatureSensorsV1(status: JSONObject): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
val tempSensors = status.optJSONObject("ext_temperature") ?: JSONObject()
|
||||
for (sensorId in tempSensors.keys()) {
|
||||
val currentSensor = tempSensors.getJSONObject(sensorId)
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = "${currentSensor.getDouble("tC")} °C",
|
||||
summary = resources.getString(R.string.shelly_temperature_sensor_summary),
|
||||
icon = R.drawable.ic_device_thermometer,
|
||||
)
|
||||
}
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun parseHumiditySensorsV1(status: JSONObject): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
val humSensors = status.optJSONObject("ext_humidity") ?: JSONObject()
|
||||
for (sensorId in humSensors.keys()) {
|
||||
val currentSensor = humSensors.getJSONObject(sensorId)
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = "${currentSensor.getDouble("hum")}%",
|
||||
summary = resources.getString(R.string.shelly_humidity_sensor_summary),
|
||||
icon = R.drawable.ic_device_hygrometer,
|
||||
)
|
||||
}
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun parseResponseV2(
|
||||
config: JSONObject,
|
||||
status: JSONObject,
|
||||
): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
for (switchKey in config.keys()) {
|
||||
if (switchKey.startsWith("switch:")) {
|
||||
listItems.addAll(
|
||||
parseSwitchV2(
|
||||
config.getJSONObject(switchKey),
|
||||
status.getJSONObject(switchKey),
|
||||
config,
|
||||
),
|
||||
)
|
||||
} else if (switchKey.startsWith("pm1:")) {
|
||||
listItems.addAll(parsePowermeter1V2(config.getJSONObject(switchKey), status.getJSONObject(switchKey)))
|
||||
}
|
||||
}
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun parsePowermeter1V2(
|
||||
pm1Config: JSONObject,
|
||||
pm1Status: JSONObject,
|
||||
): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
val currentId = pm1Config.getInt("id").toString()
|
||||
val format = DecimalFormat("#.###")
|
||||
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = "${format.format(pm1Status.getDouble("apower"))} W",
|
||||
summary = resources.getString(R.string.shelly_powermeter_summary),
|
||||
hidden = currentId,
|
||||
icon = R.drawable.ic_device_electricity,
|
||||
)
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = "${format.format(pm1Status.getDouble("current"))} A",
|
||||
summary = resources.getString(R.string.shelly_powermeter_current),
|
||||
hidden = currentId + "c",
|
||||
)
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = "${format.format(pm1Status.getDouble("voltage"))} V",
|
||||
summary = resources.getString(R.string.shelly_powermeter_voltage),
|
||||
hidden = currentId + "v",
|
||||
)
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = "${format.format(pm1Status.getJSONObject("aenergy").getDouble("total") / KILO)} kWh",
|
||||
summary = resources.getString(R.string.shelly_powermeter_energy),
|
||||
hidden = currentId + "e",
|
||||
)
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = "${format.format(pm1Status.getJSONObject("ret_aenergy").getDouble("total") / KILO)} kWh",
|
||||
summary = resources.getString(R.string.shelly_powermeter_return_energy),
|
||||
hidden = currentId + "rete",
|
||||
)
|
||||
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun parseSwitchV2(
|
||||
switchConfig: JSONObject,
|
||||
switchStatus: JSONObject,
|
||||
config: JSONObject,
|
||||
): List<ListViewItem> {
|
||||
val listItems = mutableListOf<ListViewItem>()
|
||||
val currentId = switchConfig.getInt("id")
|
||||
val currentState = switchStatus.getBoolean("output")
|
||||
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title =
|
||||
nameOrDefault(
|
||||
if (switchConfig.isNull("name")) "" else switchConfig.getString("name"),
|
||||
currentId,
|
||||
),
|
||||
summary =
|
||||
resources.getString(
|
||||
if (currentState) {
|
||||
R.string.switch_summary_on
|
||||
} else {
|
||||
R.string.switch_summary_off
|
||||
},
|
||||
),
|
||||
hidden = currentId.toString(),
|
||||
state = currentState,
|
||||
icon =
|
||||
Global.getIcon(
|
||||
config.optJSONObject("sys")?.optJSONObject("ui_data")
|
||||
?.optJSONArray("consumption_types")
|
||||
?.getString(currentId)
|
||||
?: "",
|
||||
R.drawable.ic_do,
|
||||
),
|
||||
)
|
||||
return listItems
|
||||
}
|
||||
|
||||
private fun nameOrDefault(
|
||||
name: String,
|
||||
id: Int,
|
||||
): String =
|
||||
if (name.trim().isEmpty()) {
|
||||
resources.getString(R.string.shelly_switch_title, id + 1)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KILO = 1000
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.Global.volleyError
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
import java.net.URLEncoder
|
||||
|
||||
class SimpleHomeAPI(
|
||||
c: Context,
|
||||
deviceId: String,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) : UnifiedAPI(c, deviceId, recyclerViewInterface) {
|
||||
private val parser = SimpleHomeAPIParser(c.resources, this)
|
||||
|
||||
override fun loadList(
|
||||
callback: CallbackInterface,
|
||||
extended: Boolean,
|
||||
) {
|
||||
super.loadList(callback, extended)
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "commands",
|
||||
null,
|
||||
{ response ->
|
||||
val listItems = parser.parseResponse(response)
|
||||
updateCache(listItems)
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(
|
||||
listItems,
|
||||
deviceId,
|
||||
),
|
||||
recyclerViewInterface,
|
||||
)
|
||||
},
|
||||
{ error ->
|
||||
callback.onItemsLoaded(UnifiedRequestCallback(null, deviceId, volleyError(c, error)), null)
|
||||
},
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
override fun loadStates(
|
||||
callback: RealTimeStatesCallback,
|
||||
offset: Int,
|
||||
) {
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + "commands",
|
||||
null,
|
||||
{ infoResponse ->
|
||||
callback.onStatesLoaded(
|
||||
parser.parseResponse(infoResponse),
|
||||
offset,
|
||||
)
|
||||
},
|
||||
{ },
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
|
||||
override fun execute(
|
||||
path: String,
|
||||
callback: CallbackInterface,
|
||||
) {
|
||||
val splitCharPos = path.lastIndexOf('@')
|
||||
val realPath = path.substring(splitCharPos + 1)
|
||||
when (path.substring(0, splitCharPos)) {
|
||||
"none", "switch" -> { }
|
||||
"input" -> {
|
||||
val nullParent: ViewGroup? = null
|
||||
val view = LayoutInflater.from(c).inflate(R.layout.dialog_input, nullParent, false)
|
||||
val input = view.findViewById<EditText>(R.id.input)
|
||||
MaterialAlertDialogBuilder(c)
|
||||
.setTitle(R.string.input_title)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.str_send) { _, _ ->
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + realPath + "?input=" + URLEncoder.encode(input.text.toString(), "utf-8"),
|
||||
null,
|
||||
{ },
|
||||
{ e -> Log.e(Global.LOG_TAG, e.toString()) },
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
else -> {
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + realPath,
|
||||
null,
|
||||
{ response ->
|
||||
callback.onExecuted(
|
||||
response.optString("toast", c.resources.getString(R.string.main_execution_completed)),
|
||||
response.optBoolean("refresh", false),
|
||||
)
|
||||
},
|
||||
{ error ->
|
||||
callback.onExecuted(volleyError(c, error))
|
||||
},
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun changeSwitchState(
|
||||
id: String,
|
||||
state: Boolean,
|
||||
) {
|
||||
val jsonObjectRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
url + id.substring(id.lastIndexOf('@') + 1) + "?input=" + if (state) 1 else 0,
|
||||
null,
|
||||
{ },
|
||||
{ e -> Log.e(Global.LOG_TAG, e.toString()) },
|
||||
)
|
||||
queue.add(jsonObjectRequest)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.res.Resources
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import org.json.JSONObject
|
||||
|
||||
class SimpleHomeAPIParser(resources: Resources, api: UnifiedAPI?) : UnifiedAPI.Parser(resources, api) {
|
||||
override fun parseResponse(response: JSONObject): List<ListViewItem> {
|
||||
val listItems: ArrayList<ListViewItem> = ArrayList(response.length())
|
||||
val commands = response.optJSONObject("commands") ?: return listItems
|
||||
var currentObject: JSONObject
|
||||
var currentMode: String
|
||||
for (i in commands.keys()) {
|
||||
currentObject = commands.getJSONObject(i)
|
||||
currentMode = currentObject.optString("mode", "action")
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = currentObject.optString("title"),
|
||||
summary = currentObject.optString("summary"),
|
||||
hidden = "$currentMode@$i",
|
||||
icon = Global.getIcon(currentObject.optString("icon"), R.drawable.ic_do),
|
||||
state = if (currentMode == SWITCH) currentObject.optBoolean("data", false) else null,
|
||||
)
|
||||
if (currentMode == SWITCH) api?.needsRealTimeData = true
|
||||
}
|
||||
return listItems
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SWITCH = "switch"
|
||||
}
|
||||
}
|
||||
97
app/src/main/java/io/github/domi04151309/home/api/Tasmota.kt
Normal file
97
app/src/main/java/io/github/domi04151309/home/api/Tasmota.kt
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.toolbox.StringRequest
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.TasmotaHelper
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class Tasmota(
|
||||
c: Context,
|
||||
deviceId: String,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) : UnifiedAPI(c, deviceId, recyclerViewInterface) {
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(c)
|
||||
|
||||
override fun loadList(
|
||||
callback: CallbackInterface,
|
||||
extended: Boolean,
|
||||
) {
|
||||
super.loadList(callback, extended)
|
||||
val list = JSONArray(prefs.getString(deviceId, TasmotaHelper.EMPTY_ARRAY))
|
||||
val listItems: ArrayList<ListViewItem> = ArrayList(list.length())
|
||||
if (list.length() == 0) {
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = c.resources.getString(R.string.tasmota_empty_list),
|
||||
summary = c.resources.getString(R.string.tasmota_empty_list_summary),
|
||||
icon = R.drawable.ic_warning,
|
||||
)
|
||||
} else {
|
||||
var currentItem: JSONObject
|
||||
for (i in 0 until list.length()) {
|
||||
try {
|
||||
currentItem = list.optJSONObject(i) ?: JSONObject()
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = currentItem.optString("title"),
|
||||
summary = currentItem.optString("command"),
|
||||
hidden = "tasmota_command#$i",
|
||||
icon = R.drawable.ic_do,
|
||||
)
|
||||
} catch (e: JSONException) {
|
||||
Log.e(Global.LOG_TAG, e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (extended) {
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = c.resources.getString(R.string.tasmota_add_command),
|
||||
summary = c.resources.getString(R.string.tasmota_add_command_summary),
|
||||
icon = R.drawable.ic_add,
|
||||
hidden = "add",
|
||||
)
|
||||
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = c.resources.getString(R.string.tasmota_execute_once),
|
||||
summary = c.resources.getString(R.string.tasmota_execute_once_summary),
|
||||
icon = R.drawable.ic_edit,
|
||||
hidden = "execute_once",
|
||||
)
|
||||
}
|
||||
|
||||
updateCache(listItems)
|
||||
callback.onItemsLoaded(UnifiedRequestCallback(listItems, deviceId), recyclerViewInterface)
|
||||
}
|
||||
|
||||
override fun execute(
|
||||
path: String,
|
||||
callback: CallbackInterface,
|
||||
) {
|
||||
val request =
|
||||
StringRequest(
|
||||
Request.Method.GET,
|
||||
url + path,
|
||||
{ response ->
|
||||
callback.onExecuted(response)
|
||||
},
|
||||
{ error ->
|
||||
Toast.makeText(c, Global.volleyError(c, error), Toast.LENGTH_LONG).show()
|
||||
},
|
||||
)
|
||||
queue.add(request)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package io.github.domi04151309.home.api
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import com.android.volley.RequestQueue
|
||||
import com.android.volley.toolbox.Volley
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
import org.json.JSONObject
|
||||
|
||||
open class UnifiedAPI(
|
||||
protected val c: Context,
|
||||
val deviceId: String,
|
||||
protected val recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
interface CallbackInterface {
|
||||
fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
)
|
||||
|
||||
fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean = false,
|
||||
)
|
||||
}
|
||||
|
||||
interface RealTimeStatesCallback {
|
||||
fun onStatesLoaded(
|
||||
states: List<ListViewItem>,
|
||||
offset: Int,
|
||||
)
|
||||
}
|
||||
|
||||
var needsRealTimeData: Boolean = false
|
||||
|
||||
protected val url: String = Devices(c).getDeviceById(deviceId).address
|
||||
protected val queue: RequestQueue = Volley.newRequestQueue(c)
|
||||
|
||||
protected fun updateCache(items: List<ListViewItem>) {
|
||||
listCache[deviceId] = Pair(System.currentTimeMillis(), items)
|
||||
}
|
||||
|
||||
open fun loadList(
|
||||
callback: CallbackInterface,
|
||||
extended: Boolean = false,
|
||||
) {
|
||||
if (System.currentTimeMillis() - (listCache[deviceId]?.first ?: 0) < LIST_REQUEST_TIMEOUT) {
|
||||
callback.onItemsLoaded(
|
||||
UnifiedRequestCallback(listCache[deviceId]?.second, deviceId),
|
||||
recyclerViewInterface,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
open fun loadStates(
|
||||
callback: RealTimeStatesCallback,
|
||||
offset: Int,
|
||||
) {}
|
||||
|
||||
open fun execute(
|
||||
path: String,
|
||||
callback: CallbackInterface,
|
||||
) {}
|
||||
|
||||
open fun changeSwitchState(
|
||||
id: String,
|
||||
state: Boolean,
|
||||
) {}
|
||||
|
||||
open fun changePercentage(
|
||||
id: String,
|
||||
percentage: Float,
|
||||
) {}
|
||||
|
||||
open class Parser(protected val resources: Resources, protected val api: UnifiedAPI? = null) {
|
||||
open fun parseResponse(response: JSONObject): List<ListViewItem> = listOf()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LIST_REQUEST_TIMEOUT = 1000
|
||||
private val listCache: MutableMap<String, Pair<Long, List<ListViewItem>>> = mutableMapOf()
|
||||
|
||||
protected const val MAX_PERCENTAGE = 100f
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package io.github.domi04151309.home.custom
|
||||
|
||||
import com.android.volley.NetworkResponse
|
||||
import com.android.volley.ParseError
|
||||
import com.android.volley.Response
|
||||
import com.android.volley.toolbox.HttpHeaderParser
|
||||
import com.android.volley.toolbox.JsonRequest
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class CustomJsonArrayRequest(
|
||||
method: Int,
|
||||
url: String,
|
||||
jsonRequest: JSONObject?,
|
||||
listener: Response.Listener<JSONArray>,
|
||||
errorListener: Response.ErrorListener,
|
||||
) : JsonRequest<JSONArray>(method, url, jsonRequest?.toString(), listener, errorListener) {
|
||||
override fun parseNetworkResponse(response: NetworkResponse): Response<JSONArray> =
|
||||
try {
|
||||
val jsonString = String(response.data, Charset.forName(HttpHeaderParser.parseCharset(response.headers)))
|
||||
Response.success(
|
||||
JSONArray(jsonString),
|
||||
HttpHeaderParser.parseCacheHeaders(response),
|
||||
)
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
Response.error(ParseError(e))
|
||||
} catch (e: JSONException) {
|
||||
Response.error(ParseError(e))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package io.github.domi04151309.home.custom
|
||||
|
||||
import android.util.Base64
|
||||
import com.android.volley.Response
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import io.github.domi04151309.home.helpers.DeviceSecrets
|
||||
import org.json.JSONObject
|
||||
|
||||
class JsonObjectRequestAuth(
|
||||
method: Int,
|
||||
url: String,
|
||||
private val secrets: DeviceSecrets,
|
||||
jsonRequest: JSONObject?,
|
||||
listener: Response.Listener<JSONObject>,
|
||||
errorListener: Response.ErrorListener,
|
||||
) : JsonObjectRequest(method, url, jsonRequest, listener, errorListener) {
|
||||
override fun getHeaders(): MutableMap<String, String> {
|
||||
val params = HashMap<String, String>()
|
||||
params["Authorization"] = "Basic " +
|
||||
Base64.encodeToString(
|
||||
"${secrets.username}:${secrets.password}".toByteArray(),
|
||||
Base64.NO_WRAP,
|
||||
)
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package io.github.domi04151309.home.custom
|
||||
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
|
||||
class TextWatcher(private val lambda: (text: String) -> Unit) : TextWatcher {
|
||||
override fun beforeTextChanged(
|
||||
p0: CharSequence,
|
||||
p1: Int,
|
||||
p2: Int,
|
||||
p3: Int,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onTextChanged(
|
||||
p0: CharSequence,
|
||||
p1: Int,
|
||||
p2: Int,
|
||||
p3: Int,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun afterTextChanged(text: Editable) {
|
||||
lambda(text.toString())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package io.github.domi04151309.home.data
|
||||
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
|
||||
class DeviceItem(
|
||||
val id: String,
|
||||
val name: String = "Device",
|
||||
val mode: String = "Default",
|
||||
val iconName: String = "Lamp",
|
||||
val hide: Boolean = false,
|
||||
val directView: Boolean = false,
|
||||
) {
|
||||
var address: String = "http://127.0.0.1/"
|
||||
set(value) {
|
||||
field = formatAddress(value)
|
||||
}
|
||||
val iconId: Int get() = Global.getIcon(iconName)
|
||||
|
||||
companion object {
|
||||
fun formatAddress(address: String): String {
|
||||
var url = address
|
||||
if (!(url.startsWith("https://") || url.startsWith("http://"))) {
|
||||
url = "http://$url"
|
||||
}
|
||||
if (!url.endsWith("/")) {
|
||||
url += "/"
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package io.github.domi04151309.home.data
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
class LightStates {
|
||||
private val lights: MutableMap<String, Light> = mutableMapOf()
|
||||
|
||||
fun addLight(
|
||||
id: String,
|
||||
state: JSONObject,
|
||||
) {
|
||||
lights[id] =
|
||||
Light(
|
||||
state.optBoolean("on"),
|
||||
if (state.has("bri")) state.getInt("bri") else -1,
|
||||
if (state.has("xy")) state.getJSONArray("xy") else null,
|
||||
ct = if (state.has("ct")) state.getInt("ct") else -1,
|
||||
)
|
||||
}
|
||||
|
||||
fun setSceneBrightness(bri: Int) {
|
||||
for (i in lights) {
|
||||
i.value.bri = bri
|
||||
}
|
||||
}
|
||||
|
||||
fun setLightBrightness(
|
||||
id: String,
|
||||
bri: Int,
|
||||
) {
|
||||
lights[id]?.bri = bri
|
||||
}
|
||||
|
||||
fun setLightHue(
|
||||
id: String,
|
||||
hue: Int,
|
||||
) {
|
||||
lights[id]?.xy = null
|
||||
lights[id]?.hue = hue
|
||||
}
|
||||
|
||||
fun setLightSat(
|
||||
id: String,
|
||||
sat: Int,
|
||||
) {
|
||||
lights[id]?.xy = null
|
||||
lights[id]?.sat = sat
|
||||
}
|
||||
|
||||
fun setLightCt(
|
||||
id: String,
|
||||
ct: Int,
|
||||
) {
|
||||
lights[id]?.xy = null
|
||||
lights[id]?.ct = ct
|
||||
}
|
||||
|
||||
fun switchLight(
|
||||
id: String,
|
||||
on: Boolean,
|
||||
) {
|
||||
lights[id]?.on = on
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val json = JSONObject()
|
||||
for ((key, value) in lights) {
|
||||
val light = JSONObject()
|
||||
light.put("on", value.on)
|
||||
if (value.bri != -1) light.put("bri", value.bri)
|
||||
if (value.xy != null) light.put("xy", value.xy)
|
||||
if (value.hue != -1 && value.sat != -1) {
|
||||
light.put("hue", value.hue)
|
||||
light.put("sat", value.sat)
|
||||
} else if (value.ct != -1) {
|
||||
light.put("ct", value.ct)
|
||||
}
|
||||
json.put(key, light)
|
||||
}
|
||||
return json.toString()
|
||||
}
|
||||
|
||||
class Light(
|
||||
var on: Boolean = false,
|
||||
var bri: Int = -1,
|
||||
var xy: JSONArray? = null,
|
||||
var hue: Int = -1,
|
||||
var sat: Int = -1,
|
||||
var ct: Int = -1,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package io.github.domi04151309.home.data
|
||||
|
||||
class ListViewItem(
|
||||
title: String = "",
|
||||
summary: String = "",
|
||||
hidden: String = "",
|
||||
icon: Int = 0,
|
||||
var state: Boolean? = null,
|
||||
var percentage: Int? = null,
|
||||
) : SimpleListItem(title, summary, hidden, icon) {
|
||||
override fun toString(): String =
|
||||
"""
|
||||
title: $title,
|
||||
summary: $summary,
|
||||
hidden: $hidden,
|
||||
state: $state,
|
||||
percentage: $percentage
|
||||
""".trimIndent()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package io.github.domi04151309.home.data
|
||||
|
||||
data class SceneGridItem(
|
||||
val name: String,
|
||||
val hidden: String = "",
|
||||
val color: Int? = null,
|
||||
)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package io.github.domi04151309.home.data
|
||||
|
||||
class SceneListItem(
|
||||
val title: String = "",
|
||||
val hidden: String = "",
|
||||
var state: Boolean = false,
|
||||
var brightness: String = "",
|
||||
var color: Int = 0,
|
||||
)
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package io.github.domi04151309.home.data
|
||||
|
||||
open class SimpleListItem(
|
||||
var title: String = "",
|
||||
var summary: String = "",
|
||||
var hidden: String = "",
|
||||
var icon: Int = 0,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package io.github.domi04151309.home.data
|
||||
|
||||
data class UnifiedRequestCallback(
|
||||
val response: List<ListViewItem>?,
|
||||
val deviceId: String,
|
||||
val errorMessage: String = "",
|
||||
)
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package io.github.domi04151309.home.discovery
|
||||
|
||||
import android.content.Context
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class NetworkServiceDiscoveryListener(
|
||||
context: Context,
|
||||
private val resolveListener: NetworkServiceResolveListener,
|
||||
) : NsdManager.DiscoveryListener {
|
||||
private val nsdManager = context.getSystemService(AppCompatActivity.NSD_SERVICE) as NsdManager
|
||||
|
||||
override fun onStartDiscoveryFailed(
|
||||
p0: String?,
|
||||
p1: Int,
|
||||
) {
|
||||
nsdManager.stopServiceDiscovery(this)
|
||||
}
|
||||
|
||||
override fun onStopDiscoveryFailed(
|
||||
p0: String?,
|
||||
p1: Int,
|
||||
) {
|
||||
nsdManager.stopServiceDiscovery(this)
|
||||
}
|
||||
|
||||
override fun onDiscoveryStarted(p0: String?) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onDiscoveryStopped(p0: String?) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onServiceFound(service: NsdServiceInfo) {
|
||||
val serviceName = service.serviceName.lowercase()
|
||||
if ((serviceName.startsWith("shelly") && !serviceName.startsWith("shellybutton1")) ||
|
||||
service.serviceType.equals("_simplehome._tcp.")
|
||||
) {
|
||||
if (resolveListener.isBusy.compareAndSet(false, true)) {
|
||||
nsdManager.resolveService(service, resolveListener)
|
||||
} else {
|
||||
resolveListener.pendingServices.add(service)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceLost(service: NsdServiceInfo) {
|
||||
val iterator = resolveListener.pendingServices.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().serviceName == service.serviceName) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package io.github.domi04151309.home.discovery
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.android.volley.toolbox.Volley
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.DeviceDiscoveryListAdapter
|
||||
import io.github.domi04151309.home.api.ShellyAPI
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
class NetworkServiceResolveListener(
|
||||
private val activity: Activity,
|
||||
private val adapter: DeviceDiscoveryListAdapter,
|
||||
) : NsdManager.ResolveListener {
|
||||
private val devices = Devices(activity)
|
||||
private val queue = Volley.newRequestQueue(activity)
|
||||
private val nsdManager = activity.getSystemService(AppCompatActivity.NSD_SERVICE) as NsdManager
|
||||
|
||||
var isBusy: AtomicBoolean = AtomicBoolean(false)
|
||||
var pendingServices: ConcurrentLinkedQueue<NsdServiceInfo> = ConcurrentLinkedQueue<NsdServiceInfo>()
|
||||
|
||||
override fun onResolveFailed(
|
||||
service: NsdServiceInfo,
|
||||
p1: Int,
|
||||
) {
|
||||
resolveNextInQueue()
|
||||
}
|
||||
|
||||
override fun onServiceResolved(service: NsdServiceInfo) {
|
||||
activity.runOnUiThread {
|
||||
if (service.serviceType.equals("._simplehome._tcp")) {
|
||||
val url =
|
||||
service.attributes["url"]?.decodeToString()
|
||||
?: service.host.hostAddress
|
||||
adapter.add(
|
||||
ListViewItem(
|
||||
title = service.serviceName,
|
||||
summary = url,
|
||||
hidden = "SimpleHome API#Raspberry Pi",
|
||||
icon = R.drawable.ic_device_raspberry_pi,
|
||||
state = devices.addressExists(url),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
val pos =
|
||||
adapter.add(
|
||||
ListViewItem(
|
||||
title = service.serviceName,
|
||||
summary = service.host.hostAddress ?: "",
|
||||
hidden = "Shelly Gen ${
|
||||
service.attributes["gen"]?.decodeToString() ?: "1"
|
||||
}#Lamp",
|
||||
icon = R.drawable.ic_device_lamp,
|
||||
state = devices.addressExists(service.host.hostAddress ?: ""),
|
||||
),
|
||||
)
|
||||
|
||||
queue.add(
|
||||
ShellyAPI.loadName(
|
||||
"http://" + service.host.hostAddress + "/",
|
||||
service.attributes["gen"]?.decodeToString()?.toInt() ?: 1,
|
||||
) { name ->
|
||||
if (name.isNotEmpty()) adapter.changeTitle(pos, name)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
resolveNextInQueue()
|
||||
}
|
||||
|
||||
private fun resolveNextInQueue() {
|
||||
val nextNsdService = pendingServices.poll()
|
||||
if (nextNsdService != null) {
|
||||
nsdManager.resolveService(nextNsdService, this)
|
||||
} else {
|
||||
isBusy.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package io.github.domi04151309.home.discovery
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.rine.upnpdiscovery.UPnPDevice
|
||||
import com.rine.upnpdiscovery.UPnPDiscovery
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.DeviceDiscoveryListAdapter
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
|
||||
class UPnPListener(
|
||||
context: Context,
|
||||
private val adapter: DeviceDiscoveryListAdapter,
|
||||
) : UPnPDiscovery.OnDiscoveryListener {
|
||||
private val devices = Devices(context)
|
||||
private val addresses = mutableListOf<String>()
|
||||
|
||||
override fun onStart() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onFoundNewDevice(device: UPnPDevice) {
|
||||
if (device.server.contains("IpBridge") && !addresses.contains(device.hostAddress)) {
|
||||
adapter.add(
|
||||
ListViewItem(
|
||||
title = device.friendlyName,
|
||||
summary = device.hostAddress,
|
||||
hidden = "Hue API#Lamp",
|
||||
icon = R.drawable.ic_device_lamp,
|
||||
state = devices.addressExists(device.hostAddress),
|
||||
),
|
||||
)
|
||||
addresses += device.hostAddress
|
||||
}
|
||||
if (device.friendlyName.startsWith("FRITZ!") && !addresses.contains(device.hostAddress)) {
|
||||
adapter.add(
|
||||
ListViewItem(
|
||||
title = device.friendlyName,
|
||||
summary = device.hostAddress,
|
||||
hidden = "Website#Router",
|
||||
icon = R.drawable.ic_device_router,
|
||||
state = devices.addressExists(device.hostAddress),
|
||||
),
|
||||
)
|
||||
addresses += device.hostAddress
|
||||
}
|
||||
if (device.server.contains("SimpleHome") && !addresses.contains(device.hostAddress)) {
|
||||
adapter.add(
|
||||
ListViewItem(
|
||||
title = device.friendlyName,
|
||||
summary = device.hostAddress,
|
||||
hidden = "SimpleHome API#Raspberry Pi",
|
||||
icon = R.drawable.ic_device_raspberry_pi,
|
||||
state = devices.addressExists(device.hostAddress),
|
||||
),
|
||||
)
|
||||
addresses += device.hostAddress
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish(devices: HashSet<UPnPDevice>) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onError(e: Exception) {
|
||||
Log.e(this::class.simpleName, e.toString())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package io.github.domi04151309.home.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
|
||||
class ControlInfoFragment(
|
||||
private val device: DeviceItem,
|
||||
private val title: String,
|
||||
) : Fragment(R.layout.fragment_control_info) {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
val view =
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
?: error("View does not exist yet.")
|
||||
|
||||
view.findViewById<ImageView>(R.id.deviceIcon).setImageResource(Global.getIcon(device.iconName))
|
||||
view.findViewById<TextView>(R.id.titleText).text = title
|
||||
view.findViewById<TextView>(R.id.subTitleText).text = device.name
|
||||
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
package io.github.domi04151309.home.fragments
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.skydoves.colorpickerview.ColorPickerView
|
||||
import com.skydoves.colorpickerview.listeners.ColorListener
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.data.LightStates
|
||||
import io.github.domi04151309.home.helpers.HueUtils
|
||||
import io.github.domi04151309.home.helpers.HueUtils.MIN_COLOR_TEMPERATURE
|
||||
import io.github.domi04151309.home.helpers.SliderUtils
|
||||
import io.github.domi04151309.home.interfaces.HueRoomInterface
|
||||
|
||||
class HueColorFragment(private var lampInterface: HueRoomInterface) : Fragment(R.layout.fragment_hue_color) {
|
||||
private lateinit var hueAPI: HueAPI
|
||||
private lateinit var colorPickerView: ColorPickerView
|
||||
private lateinit var ctText: TextView
|
||||
private lateinit var ctBar: Slider
|
||||
private lateinit var hueSatText: TextView
|
||||
private lateinit var hueBar: Slider
|
||||
private lateinit var satBar: Slider
|
||||
|
||||
private class OnSliderTouchListener(
|
||||
private val fragment: HueColorFragment,
|
||||
private val action: (slider: Slider) -> Unit,
|
||||
) : Slider.OnSliderTouchListener {
|
||||
override fun onStartTrackingTouch(slider: Slider) {
|
||||
fragment.pauseUpdates()
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
action(slider)
|
||||
fragment.resumeUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
hueAPI = HueAPI(requireContext(), lampInterface.device.id)
|
||||
|
||||
val view =
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
?: error("View does not exist yet.")
|
||||
colorPickerView = view.findViewById(R.id.colorPickerView)
|
||||
ctText = view.findViewById(R.id.ctTxt)
|
||||
ctBar = view.findViewById(R.id.ctBar)
|
||||
hueSatText = view.findViewById(R.id.hueSatTxt)
|
||||
hueBar = view.findViewById(R.id.hueBar)
|
||||
satBar = view.findViewById(R.id.satBar)
|
||||
|
||||
val availableInputs = arrayOf(colorPickerView, ctBar, hueBar, satBar)
|
||||
val ctViews = arrayOf(ctText, ctBar)
|
||||
val hueSatViews = arrayOf(colorPickerView, hueSatText, hueBar, satBar)
|
||||
|
||||
setupColorControls(hueAPI)
|
||||
setupTemperatureControls(hueAPI)
|
||||
|
||||
fun updateFunction(data: LightStates.Light) {
|
||||
if (lampInterface.canReceiveRequest) {
|
||||
if (data.ct == -1) {
|
||||
ctViews.forEach {
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
ctViews.forEach {
|
||||
it.visibility = View.VISIBLE
|
||||
}
|
||||
SliderUtils.setProgress(ctBar, data.ct)
|
||||
}
|
||||
if (data.hue == -1 || data.sat == -1) {
|
||||
hueSatViews.forEach {
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
hueSatViews.forEach {
|
||||
it.visibility = View.VISIBLE
|
||||
}
|
||||
colorPickerView.selectByHsvColor(HueUtils.hueSatToRGB(data.hue, data.sat))
|
||||
SliderUtils.setProgress(hueBar, data.hue)
|
||||
SliderUtils.setProgress(satBar, data.sat)
|
||||
}
|
||||
availableInputs.forEach {
|
||||
it.isEnabled = data.on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view.post {
|
||||
view.postDelayed({
|
||||
colorPickerView.selectByHsvColor(
|
||||
HueUtils.hueSatToRGB(
|
||||
lampInterface.lampData.state.hue,
|
||||
lampInterface.lampData.state.sat,
|
||||
),
|
||||
)
|
||||
}, LOADING_DELAY)
|
||||
updateFunction(lampInterface.lampData.state)
|
||||
lampInterface.lampData.addOnDataChangedListener(::updateFunction)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
internal fun pauseUpdates() {
|
||||
lampInterface.canReceiveRequest = false
|
||||
}
|
||||
|
||||
internal fun resumeUpdates() {
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
lampInterface.canReceiveRequest = true
|
||||
}, UPDATE_DELAY)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setupColorPicker(hueAPI: HueAPI) {
|
||||
colorPickerView.setColorListener(
|
||||
ColorListener { color, fromUser ->
|
||||
if (fromUser) {
|
||||
val hueSat = HueUtils.rgbToHueSat(color)
|
||||
hueBar.value = hueSat[0].toFloat()
|
||||
satBar.value = hueSat[1].toFloat()
|
||||
lampInterface.onColorChanged(color)
|
||||
}
|
||||
},
|
||||
)
|
||||
colorPickerView.setOnTouchListener { innerView, event ->
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
pauseUpdates()
|
||||
} else if (event.action == MotionEvent.ACTION_UP) {
|
||||
val hueSat = HueUtils.rgbToHueSat(colorPickerView.color)
|
||||
hueAPI.changeHueSatOfGroup(lampInterface.id, hueSat[0], hueSat[1])
|
||||
resumeUpdates()
|
||||
}
|
||||
innerView.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupColorControls(hueAPI: HueAPI) {
|
||||
hueBar.setLabelFormatter { value: Float ->
|
||||
HueUtils.hueToDegree(value.toInt())
|
||||
}
|
||||
satBar.setLabelFormatter { value: Float ->
|
||||
HueUtils.satToPercent(value.toInt())
|
||||
}
|
||||
|
||||
SliderUtils.setSliderGradient(
|
||||
hueBar,
|
||||
HueUtils.defaultColors(),
|
||||
)
|
||||
SliderUtils.setSliderGradient(
|
||||
satBar,
|
||||
intArrayOf(
|
||||
Color.WHITE,
|
||||
Color.RED,
|
||||
),
|
||||
)
|
||||
|
||||
hueBar.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
val color = HueUtils.hueSatToRGB(value.toInt(), satBar.value.toInt())
|
||||
colorPickerView.selectByHsvColor(color)
|
||||
lampInterface.onColorChanged(color)
|
||||
}
|
||||
SliderUtils.setSliderGradientNow(
|
||||
satBar,
|
||||
intArrayOf(
|
||||
Color.WHITE,
|
||||
HueUtils.hueToRGB(value.toInt()),
|
||||
),
|
||||
)
|
||||
}
|
||||
hueBar.addOnSliderTouchListener(
|
||||
OnSliderTouchListener(this) { slider ->
|
||||
hueAPI.changeHueOfGroup(lampInterface.id, slider.value.toInt())
|
||||
},
|
||||
)
|
||||
|
||||
satBar.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
val color = HueUtils.hueSatToRGB(hueBar.value.toInt(), value.toInt())
|
||||
colorPickerView.selectByHsvColor(color)
|
||||
lampInterface.onColorChanged(color)
|
||||
}
|
||||
}
|
||||
satBar.addOnSliderTouchListener(
|
||||
OnSliderTouchListener(this) { slider ->
|
||||
hueAPI.changeSaturationOfGroup(lampInterface.id, slider.value.toInt())
|
||||
},
|
||||
)
|
||||
|
||||
setupColorPicker(hueAPI)
|
||||
}
|
||||
|
||||
private fun setupTemperatureControls(hueAPI: HueAPI) {
|
||||
ctBar.setLabelFormatter { value: Float ->
|
||||
HueUtils.ctToKelvin(value.toInt() + MIN_COLOR_TEMPERATURE)
|
||||
}
|
||||
|
||||
SliderUtils.setSliderGradient(
|
||||
ctBar,
|
||||
intArrayOf(
|
||||
Color.WHITE,
|
||||
"#FF8B16".toColorInt(),
|
||||
),
|
||||
)
|
||||
|
||||
ctBar.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
lampInterface.onColorChanged(HueUtils.ctToRGB(value.toInt() + MIN_COLOR_TEMPERATURE))
|
||||
}
|
||||
}
|
||||
ctBar.addOnSliderTouchListener(
|
||||
OnSliderTouchListener(this) { slider ->
|
||||
hueAPI.changeColorTemperatureOfGroup(lampInterface.id, slider.value.toInt() + MIN_COLOR_TEMPERATURE)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LOADING_DELAY = 200L
|
||||
private const val UPDATE_DELAY = 5000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
package io.github.domi04151309.home.fragments
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.graphics.toColorInt
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.Response
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.skydoves.colorpickerview.ColorPickerView
|
||||
import com.skydoves.colorpickerview.listeners.ColorListener
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.HueUtils
|
||||
import io.github.domi04151309.home.helpers.HueUtils.MIN_COLOR_TEMPERATURE
|
||||
import io.github.domi04151309.home.helpers.SliderUtils
|
||||
import io.github.domi04151309.home.interfaces.HueAdvancedLampInterface
|
||||
import org.json.JSONObject
|
||||
|
||||
class HueColorSheet(private val lampInterface: HueAdvancedLampInterface) :
|
||||
BottomSheetDialogFragment(),
|
||||
Response.Listener<JSONObject> {
|
||||
private lateinit var colorPickerView: ColorPickerView
|
||||
private lateinit var ctText: TextView
|
||||
private lateinit var ctBar: Slider
|
||||
private lateinit var hueSatText: TextView
|
||||
private lateinit var hueBar: Slider
|
||||
private lateinit var satBar: Slider
|
||||
private lateinit var briText: TextView
|
||||
private lateinit var briBar: Slider
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
val hueAPI = HueAPI(requireContext(), lampInterface.device.id)
|
||||
|
||||
val view = inflater.inflate(R.layout.fragment_hue_bri_color, container, false)
|
||||
colorPickerView = view.findViewById(R.id.colorPickerView)
|
||||
ctText = view.findViewById(R.id.ctTxt)
|
||||
ctBar = view.findViewById(R.id.ctBar)
|
||||
hueSatText = view.findViewById(R.id.hueSatTxt)
|
||||
hueBar = view.findViewById(R.id.hueBar)
|
||||
satBar = view.findViewById(R.id.satBar)
|
||||
briText = view.findViewById(R.id.briTxt)
|
||||
briBar = view.findViewById(R.id.briBar)
|
||||
|
||||
Volley.newRequestQueue(requireContext())
|
||||
.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
"${lampInterface.addressPrefix}/lights/${lampInterface.id}",
|
||||
null,
|
||||
this,
|
||||
) { error ->
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
Global.volleyError(requireContext(), error),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
},
|
||||
)
|
||||
|
||||
setupColorControls(hueAPI)
|
||||
setupTemperatureControls(hueAPI)
|
||||
setupBrightnessControls(hueAPI)
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onResponse(response: JSONObject) {
|
||||
val availableInputs = arrayOf(colorPickerView, hueBar, satBar, ctBar, briBar)
|
||||
val ctViews = arrayOf(ctText, ctBar)
|
||||
val hueSatViews = arrayOf(colorPickerView, hueSatText, hueBar, satBar)
|
||||
val briViews = arrayOf(briText, briBar)
|
||||
val state = response.getJSONObject("state")
|
||||
|
||||
if (!state.has("ct")) {
|
||||
ctViews.forEach {
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
ctViews.forEach {
|
||||
it.visibility = View.VISIBLE
|
||||
}
|
||||
SliderUtils.setProgress(ctBar, state.getInt("ct") - MIN_COLOR_TEMPERATURE)
|
||||
}
|
||||
if (!state.has("hue") && !state.has("sat")) {
|
||||
hueSatViews.forEach {
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
hueSatViews.forEach {
|
||||
it.visibility = View.VISIBLE
|
||||
}
|
||||
colorPickerView.selectByHsvColor(
|
||||
HueUtils.hueSatToRGB(
|
||||
state.getInt("hue"),
|
||||
state.getInt("sat"),
|
||||
),
|
||||
)
|
||||
SliderUtils.setProgress(hueBar, state.getInt("hue"))
|
||||
SliderUtils.setProgress(satBar, state.getInt("sat"))
|
||||
}
|
||||
if (!state.has("bri")) {
|
||||
briViews.forEach {
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
briViews.forEach {
|
||||
it.visibility = View.VISIBLE
|
||||
}
|
||||
SliderUtils.setProgress(briBar, state.getInt("bri"))
|
||||
}
|
||||
availableInputs.forEach {
|
||||
it.isEnabled = state.optBoolean("on")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupColorControls(hueAPI: HueAPI) {
|
||||
hueBar.setLabelFormatter { value: Float ->
|
||||
HueUtils.hueToDegree(value.toInt())
|
||||
}
|
||||
satBar.setLabelFormatter { value: Float ->
|
||||
HueUtils.satToPercent(value.toInt())
|
||||
}
|
||||
|
||||
SliderUtils.setSliderGradient(
|
||||
hueBar,
|
||||
HueUtils.defaultColors(),
|
||||
)
|
||||
SliderUtils.setSliderGradient(
|
||||
satBar,
|
||||
intArrayOf(
|
||||
Color.WHITE,
|
||||
Color.RED,
|
||||
),
|
||||
)
|
||||
|
||||
hueBar.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
val color = HueUtils.hueSatToRGB(value.toInt(), satBar.value.toInt())
|
||||
hueAPI.changeHue(lampInterface.id, value.toInt())
|
||||
colorPickerView.selectByHsvColor(color)
|
||||
lampInterface.onColorChanged(color)
|
||||
lampInterface.onHueSatChanged(value.toInt(), satBar.value.toInt())
|
||||
}
|
||||
SliderUtils.setSliderGradientNow(
|
||||
satBar,
|
||||
intArrayOf(
|
||||
Color.WHITE,
|
||||
HueUtils.hueToRGB(
|
||||
value.toInt(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
satBar.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
val color = HueUtils.hueSatToRGB(hueBar.value.toInt(), value.toInt())
|
||||
hueAPI.changeSaturation(lampInterface.id, value.toInt())
|
||||
colorPickerView.selectByHsvColor(color)
|
||||
lampInterface.onColorChanged(color)
|
||||
lampInterface.onHueSatChanged(hueBar.value.toInt(), value.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
colorPickerView.setColorListener(
|
||||
ColorListener { color, fromUser ->
|
||||
if (fromUser) {
|
||||
val hueSat = HueUtils.rgbToHueSat(color)
|
||||
hueAPI.changeHueSat(lampInterface.id, hueSat[0], hueSat[1])
|
||||
hueBar.value = hueSat[0].toFloat()
|
||||
satBar.value = hueSat[1].toFloat()
|
||||
lampInterface.onColorChanged(color)
|
||||
lampInterface.onColorChanged(color)
|
||||
lampInterface.onHueSatChanged(hueSat[0], hueSat[1])
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupTemperatureControls(hueAPI: HueAPI) {
|
||||
ctBar.setLabelFormatter { value: Float ->
|
||||
HueUtils.ctToKelvin(value.toInt() + MIN_COLOR_TEMPERATURE)
|
||||
}
|
||||
|
||||
SliderUtils.setSliderGradient(
|
||||
ctBar,
|
||||
intArrayOf(
|
||||
Color.WHITE,
|
||||
"#FF8B16".toColorInt(),
|
||||
),
|
||||
)
|
||||
|
||||
ctBar.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) {
|
||||
hueAPI.changeColorTemperature(lampInterface.id, value.toInt() + MIN_COLOR_TEMPERATURE)
|
||||
lampInterface.onColorChanged(HueUtils.ctToRGB(value.toInt() + MIN_COLOR_TEMPERATURE))
|
||||
lampInterface.onCtChanged(value.toInt() + MIN_COLOR_TEMPERATURE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBrightnessControls(hueAPI: HueAPI) {
|
||||
briBar.setLabelFormatter { value: Float ->
|
||||
HueUtils.briToPercent(value.toInt())
|
||||
}
|
||||
|
||||
briBar.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser) hueAPI.changeBrightness(lampInterface.id, value.toInt())
|
||||
lampInterface.onBrightnessChanged(value.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
package io.github.domi04151309.home.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.TextView
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.volley.RequestQueue
|
||||
import com.android.volley.toolbox.Volley
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.adapters.HueLampListAdapter
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.helpers.HueUtils
|
||||
import io.github.domi04151309.home.helpers.UpdateHandler
|
||||
import io.github.domi04151309.home.interfaces.HueAdvancedLampInterface
|
||||
import io.github.domi04151309.home.interfaces.HueRoomInterface
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
class HueLampsFragment(private var lampInterface: HueRoomInterface) :
|
||||
Fragment(R.layout.fragment_hue_lamps),
|
||||
RecyclerViewHelperInterface,
|
||||
HueAdvancedLampInterface,
|
||||
HueAPI.RequestCallback,
|
||||
CompoundButton.OnCheckedChangeListener {
|
||||
private lateinit var hueAPI: HueAPI
|
||||
private lateinit var queue: RequestQueue
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var adapter: HueLampListAdapter
|
||||
private val updateHandler: UpdateHandler = UpdateHandler()
|
||||
|
||||
override var id: String = ""
|
||||
override var canReceiveRequest: Boolean = true
|
||||
override lateinit var device: DeviceItem
|
||||
override lateinit var addressPrefix: String
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
hueAPI = HueAPI(requireContext(), lampInterface.device.id)
|
||||
queue = Volley.newRequestQueue(context)
|
||||
|
||||
device = lampInterface.device
|
||||
addressPrefix = lampInterface.addressPrefix
|
||||
|
||||
recyclerView = super.onCreateView(inflater, container, savedInstanceState) as RecyclerView
|
||||
|
||||
adapter = HueLampListAdapter(this, this)
|
||||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
return recyclerView
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
updateHandler.setUpdateFunction {
|
||||
if (lampInterface.canReceiveRequest && hueAPI.readyForRequest) {
|
||||
hueAPI.loadLightsByIds(lampInterface.lights ?: JSONArray(), this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
updateHandler.stop()
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
) {
|
||||
id = view.findViewById<TextView>(R.id.hidden).text.toString()
|
||||
HueColorSheet(this).show(
|
||||
requireActivity().supportFragmentManager,
|
||||
HueColorSheet::class.simpleName,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("CognitiveComplexMethod")
|
||||
override fun onLightsLoaded(response: JSONObject?) {
|
||||
if (response != null) {
|
||||
var currentObject: JSONObject
|
||||
var currentState: JSONObject
|
||||
var state: Boolean?
|
||||
val listItems: MutableList<ListViewItem> = mutableListOf()
|
||||
val colorArray: MutableList<Int> = mutableListOf()
|
||||
for (i in response.keys()) {
|
||||
currentObject = response.optJSONObject(i) ?: JSONObject()
|
||||
currentState = currentObject.optJSONObject("state") ?: JSONObject()
|
||||
state = currentState.optBoolean("on")
|
||||
colorArray +=
|
||||
if (currentState.has("hue") && currentState.has("sat")) {
|
||||
HueUtils.hueSatToRGB(
|
||||
currentState.getInt("hue"),
|
||||
currentState.getInt("sat"),
|
||||
)
|
||||
} else if (currentState.has("ct")) {
|
||||
HueUtils.ctToRGB(currentState.getInt("ct"))
|
||||
} else {
|
||||
"#FFFFFF".toColorInt()
|
||||
}
|
||||
listItems +=
|
||||
ListViewItem(
|
||||
title = currentObject.optString("name"),
|
||||
summary =
|
||||
if (currentState.optBoolean("reachable")) {
|
||||
resources.getString(R.string.hue_brightness) +
|
||||
": " +
|
||||
if (state) {
|
||||
HueUtils.briToPercent(
|
||||
currentState.optInt(
|
||||
"bri",
|
||||
HueUtils.MAX_BRIGHTNESS,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
"0 %"
|
||||
}
|
||||
} else {
|
||||
resources.getString(R.string.str_unreachable)
|
||||
},
|
||||
hidden = i,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
adapter.updateData(recyclerView, listItems, colorArray)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onColorChanged(color: Int) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onBrightnessChanged(brightness: Int) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onHueSatChanged(
|
||||
hue: Int,
|
||||
sat: Int,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onCtChanged(ct: Int) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onCheckedChanged(
|
||||
compoundButton: CompoundButton,
|
||||
state: Boolean,
|
||||
) {
|
||||
if (compoundButton.isPressed) {
|
||||
hueAPI.switchLightById(
|
||||
(compoundButton.parent as ViewGroup).findViewById<TextView>(R.id.hidden).text.toString(),
|
||||
state,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
package io.github.domi04151309.home.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.ContextMenu
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.android.volley.Request
|
||||
import com.android.volley.RequestQueue
|
||||
import com.android.volley.Response
|
||||
import com.android.volley.toolbox.JsonObjectRequest
|
||||
import com.android.volley.toolbox.Volley
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.activities.HueSceneActivity
|
||||
import io.github.domi04151309.home.adapters.HueSceneGridAdapter
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.custom.CustomJsonArrayRequest
|
||||
import io.github.domi04151309.home.data.SceneGridItem
|
||||
import io.github.domi04151309.home.helpers.ColorUtils
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.HueUtils
|
||||
import io.github.domi04151309.home.interfaces.HueLampInterface
|
||||
import io.github.domi04151309.home.interfaces.RecyclerViewHelperInterface
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class HueScenesFragment(private var lampInterface: HueLampInterface) :
|
||||
Fragment(R.layout.fragment_hue_scenes),
|
||||
RecyclerViewHelperInterface,
|
||||
Response.Listener<JSONObject> {
|
||||
private var scenesRequest: JsonObjectRequest? = null
|
||||
private var selectedScene: CharSequence = ""
|
||||
private var selectedSceneName: CharSequence = ""
|
||||
private lateinit var hueAPI: HueAPI
|
||||
private lateinit var queue: RequestQueue
|
||||
private lateinit var adapter: HueSceneGridAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
hueAPI = HueAPI(requireContext(), lampInterface.device.id)
|
||||
queue = Volley.newRequestQueue(context)
|
||||
|
||||
val recyclerView = super.onCreateView(inflater, container, savedInstanceState) as RecyclerView
|
||||
adapter = HueSceneGridAdapter(this, this)
|
||||
recyclerView.layoutManager = GridLayoutManager(requireContext(), COLUMNS)
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
scenesRequest =
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET, lampInterface.addressPrefix + SCENES_PATH, null,
|
||||
this,
|
||||
) { error ->
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
Global.volleyError(requireContext(), error),
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
queue.add(scenesRequest)
|
||||
return recyclerView
|
||||
}
|
||||
|
||||
override fun onResponse(response: JSONObject) {
|
||||
try {
|
||||
val gridItems: ArrayList<SceneGridItem> = ArrayList(response.length())
|
||||
val scenes: List<Pair<String, String>> = getScenes(response)
|
||||
if (scenes.isNotEmpty()) {
|
||||
var completedRequests = 0
|
||||
for (i in scenes.indices) {
|
||||
queue.add(
|
||||
JsonObjectRequest(
|
||||
Request.Method.GET,
|
||||
lampInterface.addressPrefix + SCENES_PATH + scenes[i].first,
|
||||
null,
|
||||
{ sceneResponse ->
|
||||
gridItems +=
|
||||
SceneGridItem(
|
||||
name = scenes[i].second,
|
||||
hidden = scenes[i].first,
|
||||
color = getSceneColor(sceneResponse),
|
||||
)
|
||||
completedRequests++
|
||||
if (completedRequests == scenes.size) {
|
||||
val sortedItems =
|
||||
gridItems.sortedWith(compareBy { it.color })
|
||||
.toMutableList()
|
||||
sortedItems +=
|
||||
SceneGridItem(
|
||||
name = resources.getString(R.string.hue_add_scene),
|
||||
hidden = "add",
|
||||
)
|
||||
adapter.updateData(sortedItems)
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
Log.e(Global.LOG_TAG, error.toString())
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
adapter.updateData(
|
||||
mutableListOf(
|
||||
SceneGridItem(
|
||||
name = resources.getString(R.string.hue_add_scene),
|
||||
hidden = "add",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
Log.e(Global.LOG_TAG, e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun getScenes(response: JSONObject): List<Pair<String, String>> {
|
||||
val scenes: ArrayList<Pair<String, String>> =
|
||||
ArrayList(
|
||||
response.length() / SCENE_FRACTION_ESTIMATE,
|
||||
)
|
||||
var currentObject: JSONObject
|
||||
for (i in response.keys()) {
|
||||
currentObject = response.getJSONObject(i)
|
||||
if (currentObject.optString("group") == lampInterface.id) {
|
||||
scenes.add(Pair(i, currentObject.getString("name")))
|
||||
}
|
||||
}
|
||||
return scenes
|
||||
}
|
||||
|
||||
private fun getSceneColor(response: JSONObject): Int {
|
||||
val states = response.getJSONObject("lightstates")
|
||||
val currentSceneValues = ArrayList<Int>(states.length())
|
||||
var lampObject: JSONObject
|
||||
for (j in states.keys()) {
|
||||
lampObject = states.getJSONObject(j)
|
||||
if (lampObject.getBoolean("on")) {
|
||||
if (lampObject.has("hue") && lampObject.has("sat")) {
|
||||
currentSceneValues.clear()
|
||||
currentSceneValues.add(
|
||||
HueUtils.hueSatToRGB(
|
||||
lampObject.getInt("hue"),
|
||||
lampObject.getInt("sat"),
|
||||
),
|
||||
)
|
||||
} else if (lampObject.has("xy")) {
|
||||
val xyArray = lampObject.getJSONArray("xy")
|
||||
currentSceneValues.clear()
|
||||
currentSceneValues.add(
|
||||
ColorUtils.xyToRGB(
|
||||
xyArray.getDouble(0),
|
||||
xyArray.getDouble(1),
|
||||
),
|
||||
)
|
||||
} else if (lampObject.has("ct")) {
|
||||
currentSceneValues.add(
|
||||
HueUtils.ctToRGB(lampObject.getInt("ct")),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (currentSceneValues.isNotEmpty()) {
|
||||
currentSceneValues[0]
|
||||
} else {
|
||||
Color.WHITE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
) {
|
||||
val hiddenText = view.findViewById<TextView>(R.id.hidden).text.toString()
|
||||
if (hiddenText == "add") {
|
||||
startActivity(
|
||||
Intent(requireContext(), HueSceneActivity::class.java).putExtra(
|
||||
"deviceId",
|
||||
lampInterface.device.id,
|
||||
).putExtra("room", lampInterface.id),
|
||||
)
|
||||
} else {
|
||||
hueAPI.activateSceneOfGroup(lampInterface.id, hiddenText)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(
|
||||
menu: ContextMenu,
|
||||
v: View,
|
||||
menuInfo: ContextMenu.ContextMenuInfo?,
|
||||
) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo)
|
||||
selectedScene = v.findViewById<TextView>(R.id.hidden).text
|
||||
selectedSceneName = v.findViewById<TextView>(R.id.title).text
|
||||
if (selectedScene != "add") MenuInflater(requireContext()).inflate(R.menu.activity_hue_lamp_context, menu)
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean =
|
||||
when (item.title) {
|
||||
resources.getString(R.string.str_edit) -> {
|
||||
startActivity(
|
||||
Intent(requireContext(), HueSceneActivity::class.java)
|
||||
.putExtra("deviceId", lampInterface.device.id)
|
||||
.putExtra("room", lampInterface.id)
|
||||
.putExtra("scene", selectedScene),
|
||||
)
|
||||
true
|
||||
}
|
||||
resources.getString(R.string.str_delete) -> {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.str_delete)
|
||||
.setMessage(R.string.hue_delete_scene)
|
||||
.setPositiveButton(R.string.str_delete) { _, _ ->
|
||||
val deleteSceneRequest =
|
||||
CustomJsonArrayRequest(
|
||||
Request.Method.DELETE,
|
||||
lampInterface.addressPrefix + SCENES_PATH + selectedScene,
|
||||
null,
|
||||
{ queue.add(scenesRequest) },
|
||||
{ e -> Log.e(Global.LOG_TAG, e.toString()) },
|
||||
)
|
||||
queue.add(deleteSceneRequest)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
true
|
||||
}
|
||||
else -> {
|
||||
super.onContextItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
if (scenesChanged) {
|
||||
scenesChanged = false
|
||||
queue.add(scenesRequest)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SCENE_FRACTION_ESTIMATE = 4
|
||||
private const val COLUMNS = 3
|
||||
private const val SCENES_PATH = "/scenes/"
|
||||
|
||||
var scenesChanged: Boolean = false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package io.github.domi04151309.home.helpers
|
||||
|
||||
import android.graphics.Color
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
object ColorUtils {
|
||||
private const val MAX: Double = 255.0
|
||||
private const val MIN: Double = 0.0
|
||||
|
||||
fun temperatureToRGB(kelvin: Int): Int {
|
||||
val temp = kelvin / 100.0
|
||||
val red: Double
|
||||
val green: Double
|
||||
val blue: Double
|
||||
|
||||
if (temp <= 66) {
|
||||
red = MAX
|
||||
green = 99.4708025861 * ln(temp) - 161.1195681661
|
||||
blue =
|
||||
if (temp <= 19) {
|
||||
MIN
|
||||
} else {
|
||||
138.5177312231 * ln(temp - 10) - 305.0447927307
|
||||
}
|
||||
} else {
|
||||
red = 329.698727446 * (temp - 60).pow(-0.1332047592)
|
||||
green = 288.1221695283 * (temp - 60).pow(-0.0755148492)
|
||||
blue = MAX
|
||||
}
|
||||
|
||||
return Color.rgb(clamp(red), clamp(green), clamp(blue))
|
||||
}
|
||||
|
||||
fun xyToRGB(
|
||||
x: Double,
|
||||
y: Double,
|
||||
): Int {
|
||||
val cieY = 1.0
|
||||
val cieX = cieY * x / y
|
||||
val cieZ = (1 - x - y) * cieY / y
|
||||
|
||||
val r = +3.2404542 * cieX - 1.5371385 * cieY - 0.4985314 * cieZ
|
||||
val g = -0.9692660 * cieX + 1.8760108 * cieY + 0.0415560 * cieZ
|
||||
val b = +0.0556434 * cieX - 0.2040259 * cieY + 1.0572252 * cieZ
|
||||
|
||||
return Color.rgb(formatXyzValue(r), formatXyzValue(g), formatXyzValue(b))
|
||||
}
|
||||
|
||||
private fun formatXyzValue(v: Double): Int =
|
||||
clamp(
|
||||
(if (v <= 0.0031308) 12.92 * v else 1.055 * v.pow(1.0 / 2.4) - 0.055) * MAX,
|
||||
)
|
||||
|
||||
private fun clamp(value: Double): Int =
|
||||
when {
|
||||
value < MIN -> MIN.toInt()
|
||||
value > MAX -> MAX.toInt()
|
||||
else -> value.toInt()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package io.github.domi04151309.home.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import org.json.JSONObject
|
||||
|
||||
class DeviceSecrets(context: Context, private val id: String) {
|
||||
private val masterKeyAlias =
|
||||
MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
|
||||
private val preferences: SharedPreferences =
|
||||
EncryptedSharedPreferences.create(
|
||||
context,
|
||||
"device_secrets",
|
||||
masterKeyAlias,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||
)
|
||||
|
||||
private val secrets =
|
||||
JSONObject(
|
||||
preferences.getString(id, DEFAULT_JSON)
|
||||
?: DEFAULT_JSON,
|
||||
)
|
||||
|
||||
var username: String
|
||||
get() = secrets.optString("username")
|
||||
set(value) {
|
||||
secrets.put("username", value)
|
||||
}
|
||||
|
||||
var password: String
|
||||
get() = secrets.optString("password")
|
||||
set(value) {
|
||||
secrets.put("password", value)
|
||||
}
|
||||
|
||||
fun updateDeviceSecrets() {
|
||||
preferences.edit { putString(id, secrets.toString()) }
|
||||
}
|
||||
|
||||
fun deleteDeviceSecrets() {
|
||||
preferences.edit { remove(id) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_JSON = """{ "username": "", "password": "" }"""
|
||||
}
|
||||
}
|
||||
146
app/src/main/java/io/github/domi04151309/home/helpers/Devices.kt
Normal file
146
app/src/main/java/io/github/domi04151309/home/helpers/Devices.kt
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
package io.github.domi04151309.home.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.util.Random
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class Devices(private val context: Context) {
|
||||
private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
val length: Int get() = deviceOrder.length()
|
||||
|
||||
private val data: JSONObject get() {
|
||||
if (storedData == null) {
|
||||
storedData =
|
||||
try {
|
||||
JSONObject(
|
||||
preferences.getString("devices_json", Global.DEFAULT_JSON)
|
||||
?: Global.DEFAULT_JSON,
|
||||
)
|
||||
} catch (e: JSONException) {
|
||||
Log.w(Devices::class.simpleName, e)
|
||||
JSONObject(Global.DEFAULT_JSON)
|
||||
}
|
||||
}
|
||||
return storedData!!
|
||||
}
|
||||
|
||||
private val devicesObject: JSONObject get() = data.optJSONObject("devices") ?: JSONObject()
|
||||
|
||||
private val deviceOrder: JSONArray get() {
|
||||
if (!data.has(ORDER)) {
|
||||
data.put(ORDER, devicesObject.names() ?: JSONArray())
|
||||
}
|
||||
return data.getJSONArray(ORDER)
|
||||
}
|
||||
|
||||
private fun generateRandomId(): String {
|
||||
val random = Random()
|
||||
val builder = StringBuilder(ID_LENGTH)
|
||||
for (index in 0 until ID_LENGTH) {
|
||||
builder.append(ALLOWED_CHARACTERS[random.nextInt(ALLOWED_CHARACTERS.length)])
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun convertToDeviceItem(id: String): DeviceItem {
|
||||
val json = devicesObject.optJSONObject(id) ?: JSONObject()
|
||||
val device =
|
||||
DeviceItem(
|
||||
id,
|
||||
json.optString("name"),
|
||||
json.optString("mode"),
|
||||
json.optString("icon"),
|
||||
json.optBoolean("hide", false),
|
||||
json.optBoolean("direct_view", false),
|
||||
)
|
||||
device.address = json.optString(ADDRESS)
|
||||
return device
|
||||
}
|
||||
|
||||
fun getDeviceById(id: String): DeviceItem = convertToDeviceItem(id)
|
||||
|
||||
fun getDeviceByIndex(index: Int): DeviceItem = convertToDeviceItem(deviceOrder.getString(index))
|
||||
|
||||
fun idExists(id: String): Boolean = devicesObject.has(id)
|
||||
|
||||
fun addressExists(address: String): Boolean {
|
||||
val formattedAddress = DeviceItem.formatAddress(address)
|
||||
for (i in devicesObject.keys()) {
|
||||
if (devicesObject.getJSONObject(i).optString(ADDRESS) == formattedAddress) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun generateNewId(): String {
|
||||
var id = generateRandomId()
|
||||
while (devicesObject.has(id)) id = generateRandomId()
|
||||
return id
|
||||
}
|
||||
|
||||
fun addDevice(device: DeviceItem) {
|
||||
if (!idExists(device.id)) deviceOrder.put(device.id)
|
||||
val deviceObject =
|
||||
JSONObject()
|
||||
.put("name", device.name)
|
||||
.put(ADDRESS, device.address)
|
||||
.put("mode", device.mode)
|
||||
.put("icon", device.iconName)
|
||||
.put("hide", device.hide)
|
||||
.put("direct_view", device.directView)
|
||||
devicesObject.put(device.id, deviceObject)
|
||||
saveChanges()
|
||||
}
|
||||
|
||||
fun deleteDevice(id: String) {
|
||||
for (i in 0 until deviceOrder.length()) {
|
||||
if (deviceOrder[i] == id) {
|
||||
deviceOrder.remove(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
devicesObject.remove(id)
|
||||
saveChanges()
|
||||
DeviceSecrets(context, id).deleteDeviceSecrets()
|
||||
}
|
||||
|
||||
fun moveDevice(
|
||||
from: Int,
|
||||
to: Int,
|
||||
) {
|
||||
val list =
|
||||
MutableList(deviceOrder.length()) {
|
||||
deviceOrder.getString(it)
|
||||
}
|
||||
list.add(to, list.removeAt(from))
|
||||
data.put(ORDER, JSONArray(list))
|
||||
}
|
||||
|
||||
fun saveChanges() {
|
||||
preferences.edit { putString("devices_json", data.toString()) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INTENT_EXTRA_DEVICE: String = "device"
|
||||
|
||||
private const val ALLOWED_CHARACTERS = "0123456789abcdefghijklmnobqrstuvw"
|
||||
private const val ID_LENGTH = 8
|
||||
private const val ORDER = "order"
|
||||
private const val ADDRESS = "address"
|
||||
private var storedData: JSONObject? = null
|
||||
|
||||
fun reloadFromPreferences() {
|
||||
storedData = null
|
||||
}
|
||||
}
|
||||
}
|
||||
157
app/src/main/java/io/github/domi04151309/home/helpers/Global.kt
Normal file
157
app/src/main/java/io/github/domi04151309/home/helpers/Global.kt
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
package io.github.domi04151309.home.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import android.service.controls.DeviceTypes
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.android.volley.ClientError
|
||||
import com.android.volley.NoConnectionError
|
||||
import com.android.volley.ParseError
|
||||
import com.android.volley.TimeoutError
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.api.EspEasyAPI
|
||||
import io.github.domi04151309.home.api.HueAPI
|
||||
import io.github.domi04151309.home.api.ShellyAPI
|
||||
import io.github.domi04151309.home.api.SimpleHomeAPI
|
||||
import io.github.domi04151309.home.api.Tasmota
|
||||
import io.github.domi04151309.home.api.UnifiedAPI
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
|
||||
internal object Global {
|
||||
const val LOG_TAG: String = "HomeApp"
|
||||
|
||||
const val DEFAULT_JSON: String = "{\"devices\":{}}"
|
||||
const val ESP_EASY = "ESP Easy"
|
||||
const val HUE_API = "Hue API"
|
||||
const val SHELLY_GEN_1 = "Shelly Gen 1"
|
||||
const val SHELLY_GEN_2 = "Shelly Gen 2"
|
||||
const val SHELLY_GEN_3 = "Shelly Gen 3"
|
||||
const val SIMPLE_HOME_API = "SimpleHome API"
|
||||
const val TASMOTA = "Tasmota"
|
||||
const val NODE_RED = "Node-RED"
|
||||
const val WEBSITE = "Website"
|
||||
const val FRITZ_AUTO_LOGIN = "Fritz! Auto-Login"
|
||||
const val GRAFANA_AUTO_LOGIN = "Grafana Auto-Login"
|
||||
const val PI_HOLE_AUTO_LOGIN = "Pi-hole Auto-Login"
|
||||
val UNIFIED_MODES =
|
||||
arrayOf(
|
||||
ESP_EASY,
|
||||
HUE_API,
|
||||
SHELLY_GEN_1,
|
||||
SHELLY_GEN_2,
|
||||
SHELLY_GEN_3,
|
||||
SIMPLE_HOME_API,
|
||||
TASMOTA,
|
||||
)
|
||||
val POWER_MENU_MODES =
|
||||
arrayOf(
|
||||
ESP_EASY,
|
||||
HUE_API,
|
||||
SHELLY_GEN_1,
|
||||
SHELLY_GEN_2,
|
||||
SHELLY_GEN_3,
|
||||
SIMPLE_HOME_API,
|
||||
TASMOTA,
|
||||
)
|
||||
|
||||
fun getCorrectAPI(
|
||||
context: Context,
|
||||
identifier: String,
|
||||
deviceId: String,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface? = null,
|
||||
tasmotaHelperInterface: HomeRecyclerViewHelperInterface? = null,
|
||||
): UnifiedAPI =
|
||||
when (identifier) {
|
||||
ESP_EASY -> EspEasyAPI(context, deviceId, recyclerViewInterface)
|
||||
HUE_API -> HueAPI(context, deviceId, recyclerViewInterface)
|
||||
SIMPLE_HOME_API -> SimpleHomeAPI(context, deviceId, recyclerViewInterface)
|
||||
TASMOTA -> Tasmota(context, deviceId, tasmotaHelperInterface ?: recyclerViewInterface)
|
||||
SHELLY_GEN_1 -> ShellyAPI(context, deviceId, recyclerViewInterface, 1)
|
||||
SHELLY_GEN_2 -> ShellyAPI(context, deviceId, recyclerViewInterface, 2)
|
||||
SHELLY_GEN_3 -> ShellyAPI(context, deviceId, recyclerViewInterface, 2)
|
||||
else -> UnifiedAPI(context, deviceId, recyclerViewInterface)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
fun getIcon(
|
||||
icon: String,
|
||||
default: Int = R.drawable.ic_warning,
|
||||
): Int =
|
||||
when (icon.lowercase()) {
|
||||
"christmas tree" -> R.drawable.ic_device_christmas_tree
|
||||
"clock" -> R.drawable.ic_device_clock
|
||||
"display" -> R.drawable.ic_device_display
|
||||
"display alt" -> R.drawable.ic_device_display_alt
|
||||
"docker" -> R.drawable.ic_device_docker
|
||||
"electricity" -> R.drawable.ic_device_electricity
|
||||
"entertainment" -> R.drawable.ic_device_speaker
|
||||
"gauge" -> R.drawable.ic_device_gauge
|
||||
"grafana" -> R.drawable.ic_device_grafana
|
||||
"heating" -> R.drawable.ic_device_thermometer
|
||||
"hygrometer" -> R.drawable.ic_device_hygrometer
|
||||
"lamp" -> R.drawable.ic_device_lamp
|
||||
"lights" -> R.drawable.ic_device_lamp
|
||||
"raspberry pi" -> R.drawable.ic_device_raspberry_pi
|
||||
"raspberry pi alt" -> R.drawable.ic_device_raspberry_pi_alt
|
||||
"router" -> R.drawable.ic_device_router
|
||||
"speaker" -> R.drawable.ic_device_speaker
|
||||
"schwibbogen" -> R.drawable.ic_device_schwibbogen
|
||||
"stack" -> R.drawable.ic_device_stack
|
||||
"socket" -> R.drawable.ic_device_socket
|
||||
"thermometer" -> R.drawable.ic_device_thermometer
|
||||
"webcam" -> R.drawable.ic_device_webcam
|
||||
else -> default
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
fun getDeviceType(icon: String): Int =
|
||||
when (icon.lowercase()) {
|
||||
"christmas tree", "electricity", "schwibbogen", "socket" -> DeviceTypes.TYPE_OUTLET
|
||||
"display", "display alt" -> DeviceTypes.TYPE_DISPLAY
|
||||
"gauge", "heating", "thermometer" -> DeviceTypes.TYPE_AC_HEATER
|
||||
"hygrometer" -> DeviceTypes.TYPE_HUMIDIFIER
|
||||
"lamp", "lights" -> DeviceTypes.TYPE_LIGHT
|
||||
"webcam" -> DeviceTypes.TYPE_CAMERA
|
||||
else -> DeviceTypes.TYPE_UNKNOWN
|
||||
}
|
||||
|
||||
fun checkNetwork(context: Context): Boolean {
|
||||
if (
|
||||
!PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean("safety_checks", true)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
val connectivityManager =
|
||||
context.getSystemService(
|
||||
AppCompatActivity.CONNECTIVITY_SERVICE,
|
||||
) as ConnectivityManager
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
return if (capabilities != null) {
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fun volleyError(
|
||||
c: Context,
|
||||
error: java.lang.Exception,
|
||||
): String {
|
||||
Log.w(LOG_TAG, error)
|
||||
return when (error) {
|
||||
is TimeoutError, is NoConnectionError -> c.resources.getString(R.string.main_device_unavailable)
|
||||
is ParseError -> c.resources.getString(R.string.main_parse_error)
|
||||
is ClientError -> c.resources.getString(R.string.main_client_error)
|
||||
else -> c.resources.getString(R.string.main_device_unavailable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package io.github.domi04151309.home.helpers
|
||||
|
||||
import io.github.domi04151309.home.data.LightStates
|
||||
|
||||
class HueLightListener {
|
||||
private var listeners = mutableListOf<(data: LightStates.Light) -> Unit>()
|
||||
var state: LightStates.Light = LightStates.Light()
|
||||
set(value) {
|
||||
if (value != field) {
|
||||
field = value
|
||||
listeners.forEach {
|
||||
it(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addOnDataChangedListener(listener: (data: LightStates.Light) -> Unit) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package io.github.domi04151309.home.helpers
|
||||
|
||||
import android.graphics.Color
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
object HueUtils {
|
||||
const val MIN_COLOR_TEMPERATURE: Int = 153
|
||||
const val MAX_BRIGHTNESS: Int = 255
|
||||
|
||||
private const val ARGUMENT_OUT_OF_RANGE = "Argument out of range."
|
||||
|
||||
fun defaultColors(): IntArray =
|
||||
IntArray(7) {
|
||||
index ->
|
||||
Color.HSVToColor(floatArrayOf(index * 60f, 1f, 1f))
|
||||
}
|
||||
|
||||
fun ctToRGB(ct: Int): Int {
|
||||
require(!(ct < MIN_COLOR_TEMPERATURE || ct > 500)) { ARGUMENT_OUT_OF_RANGE }
|
||||
return ColorUtils.temperatureToRGB((6500 - 12.968299711 * (ct - MIN_COLOR_TEMPERATURE)).toInt())
|
||||
}
|
||||
|
||||
fun ctToKelvin(ct: Int): String {
|
||||
require(!(ct < MIN_COLOR_TEMPERATURE || ct > 500)) { ARGUMENT_OUT_OF_RANGE }
|
||||
return "${(6500 - 12.968299711 * (ct - MIN_COLOR_TEMPERATURE)).toInt()} K"
|
||||
}
|
||||
|
||||
fun hueSatToRGB(
|
||||
hue: Int,
|
||||
sat: Int,
|
||||
): Int {
|
||||
require(!(hue > 65_535 || sat > 254)) { ARGUMENT_OUT_OF_RANGE }
|
||||
return Color.HSVToColor(floatArrayOf(hue * 0.005493248F, sat / 254F, 1F))
|
||||
}
|
||||
|
||||
fun hueToRGB(hue: Int): Int {
|
||||
require(hue <= 65_535) { ARGUMENT_OUT_OF_RANGE }
|
||||
return Color.HSVToColor(floatArrayOf(hue * 0.005493248F, 1F, 1F))
|
||||
}
|
||||
|
||||
fun hueToDegree(hue: Int): String {
|
||||
require(hue <= 65_535) { ARGUMENT_OUT_OF_RANGE }
|
||||
return "${(hue * 0.005493248F).toInt()}°"
|
||||
}
|
||||
|
||||
fun satToPercent(sat: Int): String {
|
||||
require(sat <= 254) { ARGUMENT_OUT_OF_RANGE }
|
||||
return "${(sat / 254F * 100).toInt()} %"
|
||||
}
|
||||
|
||||
fun briToPercent(bri: Int): String =
|
||||
when {
|
||||
bri < 1 -> "0 %"
|
||||
bri > 254 -> "100 %"
|
||||
else -> "${(bri / 254F * 100).toInt()} %"
|
||||
}
|
||||
|
||||
fun rgbToHueSat(color: Int): IntArray {
|
||||
val hsv = FloatArray(3)
|
||||
Color.colorToHSV(color, hsv)
|
||||
return intArrayOf(
|
||||
(hsv[0] / 0.0054932478).toInt(),
|
||||
(hsv[1] * 254).toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
10
app/src/main/java/io/github/domi04151309/home/helpers/P.kt
Normal file
10
app/src/main/java/io/github/domi04151309/home/helpers/P.kt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
@file:Suppress("HardCodedStringLiteral")
|
||||
|
||||
package io.github.domi04151309.home.helpers
|
||||
|
||||
internal object P {
|
||||
const val PREF_COLUMNS = "columns"
|
||||
const val PREF_COLUMNS_DEFAULT = "auto"
|
||||
const val PREF_CONTROLS_AUTH = "controls_auth"
|
||||
const val PREF_CONTROLS_AUTH_DEFAULT = false
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package io.github.domi04151309.home.helpers
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.res.Resources
|
||||
import android.graphics.LinearGradient
|
||||
import android.graphics.Shader
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.graphics.drawable.PaintDrawable
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import com.google.android.material.slider.Slider
|
||||
|
||||
object SliderUtils {
|
||||
private const val CORNER_RADIUS = 16
|
||||
private const val MARGIN_VERTICAL = 16
|
||||
private const val MARGIN_HORIZONTAL = 14
|
||||
private const val ANIMATION_DURATION = 300L
|
||||
|
||||
private fun dpToPx(
|
||||
resources: Resources,
|
||||
dp: Int,
|
||||
): Int = (dp * resources.displayMetrics.density).toInt()
|
||||
|
||||
fun setSliderGradientNow(
|
||||
view: View,
|
||||
colors: IntArray,
|
||||
) {
|
||||
val gradient = PaintDrawable()
|
||||
gradient.setCornerRadius(dpToPx(view.resources, CORNER_RADIUS).toFloat())
|
||||
gradient.paint.shader =
|
||||
LinearGradient(
|
||||
0f, 0f,
|
||||
view.width.toFloat(), 0f,
|
||||
colors,
|
||||
null,
|
||||
Shader.TileMode.CLAMP,
|
||||
)
|
||||
|
||||
val layers = LayerDrawable(arrayOf(gradient))
|
||||
layers.setLayerInset(
|
||||
0,
|
||||
dpToPx(view.resources, MARGIN_HORIZONTAL),
|
||||
dpToPx(view.resources, MARGIN_VERTICAL),
|
||||
dpToPx(view.resources, MARGIN_HORIZONTAL),
|
||||
dpToPx(view.resources, MARGIN_VERTICAL),
|
||||
)
|
||||
view.background = layers
|
||||
}
|
||||
|
||||
fun setSliderGradient(
|
||||
view: View,
|
||||
colors: IntArray,
|
||||
) {
|
||||
view.post {
|
||||
setSliderGradientNow(view, colors)
|
||||
}
|
||||
}
|
||||
|
||||
fun setProgress(
|
||||
slider: Slider,
|
||||
value: Int,
|
||||
) {
|
||||
val animation = ObjectAnimator.ofFloat(slider, "value", value.toFloat())
|
||||
animation.duration = ANIMATION_DURATION
|
||||
animation.interpolator = DecelerateInterpolator()
|
||||
animation.start()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
package io.github.domi04151309.home.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.api.UnifiedAPI
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
class TasmotaHelper(private val c: Context, private val tasmota: UnifiedAPI) {
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(c)
|
||||
private val nullParent: ViewGroup? = null
|
||||
|
||||
fun updateItem(
|
||||
callback: UnifiedAPI.CallbackInterface,
|
||||
index: Int,
|
||||
) {
|
||||
val array = JSONArray(prefs.getString(tasmota.deviceId, EMPTY_ARRAY))
|
||||
val arrayItem = array.optJSONObject(index) ?: JSONObject()
|
||||
val view = LayoutInflater.from(c).inflate(R.layout.dialog_tasmota_add, nullParent, false)
|
||||
val titleTxt = view.findViewById<EditText>(R.id.title)
|
||||
val commandTxt = view.findViewById<EditText>(R.id.command)
|
||||
titleTxt.setText(arrayItem.optString(TITLE))
|
||||
commandTxt.setText(arrayItem.optString(COMMAND))
|
||||
MaterialAlertDialogBuilder(c)
|
||||
.setTitle(R.string.tasmota_add_command)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val newTitle = titleTxt.text.toString()
|
||||
val newCommand = commandTxt.text.toString()
|
||||
array.remove(index)
|
||||
prefs.edit {
|
||||
putString(
|
||||
tasmota.deviceId,
|
||||
array.put(
|
||||
JSONObject()
|
||||
.put(
|
||||
TITLE,
|
||||
if (newTitle == "") {
|
||||
c.resources.getString(R.string.tasmota_add_command_dialog_title_empty)
|
||||
} else {
|
||||
newTitle
|
||||
},
|
||||
)
|
||||
.put(
|
||||
COMMAND,
|
||||
if (newCommand == "") {
|
||||
c.resources.getString(
|
||||
R.string.tasmota_add_command_dialog_command_empty,
|
||||
)
|
||||
} else {
|
||||
newCommand
|
||||
},
|
||||
),
|
||||
).toString(),
|
||||
)
|
||||
}
|
||||
tasmota.loadList(callback)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
fun addToList(
|
||||
callback: UnifiedAPI.CallbackInterface,
|
||||
title: String = "",
|
||||
command: String = "",
|
||||
) {
|
||||
val view = LayoutInflater.from(c).inflate(R.layout.dialog_tasmota_add, nullParent, false)
|
||||
val titleTxt = view.findViewById<EditText>(R.id.title)
|
||||
val commandTxt = view.findViewById<EditText>(R.id.command)
|
||||
titleTxt.setText(title)
|
||||
commandTxt.setText(command)
|
||||
MaterialAlertDialogBuilder(c)
|
||||
.setTitle(R.string.tasmota_add_command)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
val newTitle = titleTxt.text.toString()
|
||||
val newCommand = commandTxt.text.toString()
|
||||
prefs.edit {
|
||||
putString(
|
||||
tasmota.deviceId,
|
||||
JSONArray(
|
||||
prefs.getString(tasmota.deviceId, EMPTY_ARRAY),
|
||||
).put(
|
||||
JSONObject()
|
||||
.put(
|
||||
TITLE,
|
||||
if (newTitle == "") {
|
||||
c.resources.getString(
|
||||
R.string.tasmota_add_command_dialog_title_empty,
|
||||
)
|
||||
} else {
|
||||
newTitle
|
||||
},
|
||||
)
|
||||
.put(
|
||||
COMMAND,
|
||||
if (newCommand == "") {
|
||||
c.resources.getString(
|
||||
R.string.tasmota_add_command_dialog_command_empty,
|
||||
)
|
||||
} else {
|
||||
newCommand
|
||||
},
|
||||
),
|
||||
).toString(),
|
||||
)
|
||||
}
|
||||
tasmota.loadList(callback)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
fun removeFromList(
|
||||
callback: UnifiedAPI.CallbackInterface,
|
||||
index: Int,
|
||||
) {
|
||||
val array = JSONArray(prefs.getString(tasmota.deviceId, EMPTY_ARRAY))
|
||||
array.remove(index)
|
||||
prefs.edit { putString(tasmota.deviceId, array.toString()) }
|
||||
tasmota.loadList(callback)
|
||||
}
|
||||
|
||||
fun executeOnce(callback: UnifiedAPI.CallbackInterface) {
|
||||
val view = LayoutInflater.from(c).inflate(R.layout.dialog_tasmota_execute_once, nullParent, false)
|
||||
val command = view.findViewById<EditText>(R.id.command)
|
||||
MaterialAlertDialogBuilder(c)
|
||||
.setTitle(R.string.tasmota_execute_once)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
tasmota.execute(command.text.toString(), callback)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EMPTY_ARRAY: String = "[]"
|
||||
private const val TITLE = "title"
|
||||
private const val COMMAND = "command"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package io.github.domi04151309.home.helpers
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
class UpdateHandler : Handler(Looper.getMainLooper()) {
|
||||
var running: Boolean = false
|
||||
private set
|
||||
|
||||
fun setUpdateFunction(function: () -> Unit) {
|
||||
removeCallbacksAndMessages(null)
|
||||
postDelayed(
|
||||
object : Runnable {
|
||||
override fun run() {
|
||||
function()
|
||||
postDelayed(this, UPDATE_DELAY)
|
||||
}
|
||||
},
|
||||
0,
|
||||
)
|
||||
running = true
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
running = false
|
||||
removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UPDATE_DELAY = 1000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package io.github.domi04151309.home.interfaces
|
||||
|
||||
import android.view.View
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
|
||||
interface HomeRecyclerViewHelperInterface {
|
||||
fun onItemClicked(
|
||||
view: View,
|
||||
data: ListViewItem,
|
||||
)
|
||||
|
||||
fun onStateChanged(
|
||||
view: View,
|
||||
data: ListViewItem,
|
||||
state: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package io.github.domi04151309.home.interfaces
|
||||
|
||||
interface HueAdvancedLampInterface : HueLampInterface {
|
||||
fun onBrightnessChanged(brightness: Int)
|
||||
|
||||
fun onHueSatChanged(
|
||||
hue: Int,
|
||||
sat: Int,
|
||||
)
|
||||
|
||||
fun onCtChanged(ct: Int)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package io.github.domi04151309.home.interfaces
|
||||
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
|
||||
interface HueLampInterface {
|
||||
var id: String
|
||||
var device: DeviceItem
|
||||
var addressPrefix: String
|
||||
var canReceiveRequest: Boolean
|
||||
|
||||
fun onColorChanged(color: Int)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package io.github.domi04151309.home.interfaces
|
||||
|
||||
import io.github.domi04151309.home.helpers.HueLightListener
|
||||
import org.json.JSONArray
|
||||
|
||||
interface HueRoomInterface : HueLampInterface {
|
||||
var lights: JSONArray?
|
||||
var lampData: HueLightListener
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package io.github.domi04151309.home.interfaces
|
||||
|
||||
import android.view.View
|
||||
|
||||
interface RecyclerViewHelperInterface {
|
||||
fun onItemClicked(
|
||||
view: View,
|
||||
position: Int,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package io.github.domi04151309.home.interfaces
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
interface RecyclerViewHelperInterfaceAdvanced : RecyclerViewHelperInterface {
|
||||
fun onItemHandleTouched(viewHolder: RecyclerView.ViewHolder)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package io.github.domi04151309.home.interfaces
|
||||
|
||||
import android.view.View
|
||||
import io.github.domi04151309.home.data.SceneListItem
|
||||
|
||||
interface SceneRecyclerViewHelperInterface {
|
||||
fun onItemClicked(
|
||||
view: View,
|
||||
data: SceneListItem,
|
||||
)
|
||||
|
||||
fun onStateChanged(
|
||||
view: View,
|
||||
data: SceneListItem,
|
||||
state: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
package io.github.domi04151309.home.services
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.service.controls.Control
|
||||
import android.service.controls.templates.ControlButton
|
||||
import android.service.controls.templates.RangeTemplate
|
||||
import android.service.controls.templates.StatelessTemplate
|
||||
import android.service.controls.templates.ToggleRangeTemplate
|
||||
import android.service.controls.templates.ToggleTemplate
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.preference.PreferenceManager
|
||||
import io.github.domi04151309.home.R
|
||||
import io.github.domi04151309.home.activities.ControlInfoActivity
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.ListViewItem
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.helpers.P
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
object ControlBuilders {
|
||||
private const val RANGE_MIN = 0f
|
||||
private const val RANGE_MAX = 100f
|
||||
private const val RANGE_STEP = 1f
|
||||
|
||||
private var requestCode = 0
|
||||
|
||||
private fun getPendingIntent(
|
||||
context: Context,
|
||||
id: String,
|
||||
title: String,
|
||||
): PendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
requestCode++,
|
||||
Intent(context, ControlInfoActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
putExtra(ControlInfoActivity.EXTRA_ID, id)
|
||||
putExtra(ControlInfoActivity.EXTRA_TITLE, title)
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
|
||||
private fun getControlButton(item: ListViewItem): ControlButton =
|
||||
ControlButton(
|
||||
item.state == true,
|
||||
item.state.toString(),
|
||||
)
|
||||
|
||||
private fun getRangeTemplate(
|
||||
id: String,
|
||||
item: ListViewItem,
|
||||
): RangeTemplate =
|
||||
RangeTemplate(
|
||||
id,
|
||||
RANGE_MIN,
|
||||
RANGE_MAX,
|
||||
item.percentage?.toFloat() ?: 0f,
|
||||
RANGE_STEP,
|
||||
"%.0f %%",
|
||||
)
|
||||
|
||||
fun buildUnreachableControl(
|
||||
context: Context,
|
||||
id: String,
|
||||
device: DeviceItem,
|
||||
): Control =
|
||||
Control.StatefulBuilder(id, getPendingIntent(context, id, device.name))
|
||||
.setTitle(device.name)
|
||||
.setZone(device.name)
|
||||
.setStructure(context.resources.getString(R.string.app_name))
|
||||
.setDeviceType(Global.getDeviceType(device.iconName))
|
||||
.setStatus(Control.STATUS_DISABLED)
|
||||
.setStatusText(context.resources.getString(R.string.str_unreachable))
|
||||
.build()
|
||||
|
||||
fun buildGenericControl(
|
||||
context: Context,
|
||||
listItem: ListViewItem,
|
||||
device: DeviceItem,
|
||||
): Control {
|
||||
val id = device.id + '@' + listItem.hidden
|
||||
return Control.StatelessBuilder(
|
||||
id,
|
||||
getPendingIntent(context, id, listItem.title),
|
||||
)
|
||||
.setTitle(listItem.title)
|
||||
.setSubtitle(device.name)
|
||||
.setZone(device.name)
|
||||
.setStructure(context.resources.getString(R.string.app_name))
|
||||
.setDeviceType(Global.getDeviceType(device.iconName))
|
||||
.build()
|
||||
}
|
||||
|
||||
fun buildStatefulControl(
|
||||
context: Context,
|
||||
id: String,
|
||||
listItem: ListViewItem,
|
||||
device: DeviceItem,
|
||||
): Control {
|
||||
val controlBuilder =
|
||||
Control.StatefulBuilder(id, getPendingIntent(context, id, listItem.title))
|
||||
.setTitle(listItem.title)
|
||||
.setSubtitle(device.name)
|
||||
.setZone(device.name)
|
||||
.setStructure(context.resources.getString(R.string.app_name))
|
||||
.setDeviceType(Global.getDeviceType(device.iconName))
|
||||
.setStatus(Control.STATUS_OK)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
controlBuilder.setAuthRequired(
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(
|
||||
P.PREF_CONTROLS_AUTH,
|
||||
P.PREF_CONTROLS_AUTH_DEFAULT,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (listItem.state != null) {
|
||||
controlBuilder.setStatusText(
|
||||
context.resources.getString(
|
||||
if (listItem.state == true) {
|
||||
R.string.str_on
|
||||
} else {
|
||||
R.string.str_off
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (listItem.state != null && listItem.percentage != null) {
|
||||
controlBuilder.setControlTemplate(
|
||||
ToggleRangeTemplate(
|
||||
id,
|
||||
getControlButton(listItem),
|
||||
getRangeTemplate(id, listItem),
|
||||
),
|
||||
)
|
||||
} else if (listItem.state != null) {
|
||||
controlBuilder.setControlTemplate(
|
||||
ToggleTemplate(
|
||||
id,
|
||||
getControlButton(listItem),
|
||||
),
|
||||
)
|
||||
} else if (listItem.percentage != null) {
|
||||
controlBuilder.setControlTemplate(
|
||||
getRangeTemplate(id, listItem),
|
||||
)
|
||||
}
|
||||
|
||||
if (device.mode == Global.TASMOTA) {
|
||||
controlBuilder.setControlTemplate(
|
||||
StatelessTemplate(id),
|
||||
)
|
||||
}
|
||||
|
||||
return controlBuilder.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
package io.github.domi04151309.home.services
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.service.controls.Control
|
||||
import android.service.controls.ControlsProviderService
|
||||
import android.service.controls.actions.BooleanAction
|
||||
import android.service.controls.actions.CommandAction
|
||||
import android.service.controls.actions.ControlAction
|
||||
import android.service.controls.actions.FloatAction
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import io.github.domi04151309.home.api.UnifiedAPI
|
||||
import io.github.domi04151309.home.data.DeviceItem
|
||||
import io.github.domi04151309.home.data.UnifiedRequestCallback
|
||||
import io.github.domi04151309.home.helpers.Devices
|
||||
import io.github.domi04151309.home.helpers.Global
|
||||
import io.github.domi04151309.home.interfaces.HomeRecyclerViewHelperInterface
|
||||
import java.util.concurrent.Flow
|
||||
import java.util.function.Consumer
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
class ControlService : ControlsProviderService() {
|
||||
private var updateSubscriber: Flow.Subscriber<in Control>? = null
|
||||
private var finishedRequests = 0
|
||||
|
||||
override fun createPublisherForAllAvailable(): Flow.Publisher<Control> =
|
||||
Flow.Publisher { subscriber ->
|
||||
updateSubscriber = subscriber
|
||||
if (!Global.checkNetwork(this)) {
|
||||
subscriber.onComplete()
|
||||
@Suppress("LabeledExpression")
|
||||
return@Publisher
|
||||
}
|
||||
val devices = Devices(this)
|
||||
val relevantDevices = mutableListOf<DeviceItem>()
|
||||
for (i in 0 until devices.length) {
|
||||
val currentDevice = devices.getDeviceByIndex(i)
|
||||
if (
|
||||
!currentDevice.hide &&
|
||||
Global.POWER_MENU_MODES.contains(currentDevice.mode)
|
||||
) {
|
||||
relevantDevices.add(currentDevice)
|
||||
}
|
||||
}
|
||||
finishedRequests = 0
|
||||
for (index in 0 until relevantDevices.size) {
|
||||
Global.getCorrectAPI(this, relevantDevices[index].mode, relevantDevices[index].id)
|
||||
.loadList(
|
||||
getAllAvailableCallback(
|
||||
subscriber,
|
||||
relevantDevices,
|
||||
index,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAllAvailableCallback(
|
||||
subscriber: Flow.Subscriber<in Control>,
|
||||
relevantDevices: MutableList<DeviceItem>,
|
||||
index: Int,
|
||||
): UnifiedAPI.CallbackInterface =
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
for (it in holder.response ?: emptyList()) {
|
||||
subscriber.onNext(
|
||||
ControlBuilders.buildGenericControl(this@ControlService, it, relevantDevices[index]),
|
||||
)
|
||||
}
|
||||
finishedRequests++
|
||||
if (finishedRequests == relevantDevices.size) subscriber.onComplete()
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadStatefulControl(
|
||||
subscriber: Flow.Subscriber<in Control>?,
|
||||
id: String,
|
||||
) {
|
||||
val device = Devices(this).getDeviceById(id.substring(0, id.indexOf('@')))
|
||||
if (Global.checkNetwork(this)) {
|
||||
Global
|
||||
.getCorrectAPI(this, device.mode, device.id)
|
||||
.loadList(getStatefulControlsCallback(device, id, subscriber))
|
||||
} else {
|
||||
subscriber?.onNext(ControlBuilders.buildUnreachableControl(this, id, device))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatefulControlsCallback(
|
||||
device: DeviceItem,
|
||||
id: String,
|
||||
subscriber: Flow.Subscriber<in Control>?,
|
||||
) = object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
if (holder.response == null) {
|
||||
subscriber?.onNext(ControlBuilders.buildUnreachableControl(this@ControlService, id, device))
|
||||
return
|
||||
}
|
||||
for (it in holder.response) {
|
||||
if (device.id + '@' + it.hidden != id) continue
|
||||
subscriber?.onNext(ControlBuilders.buildStatefulControl(this@ControlService, id, it, device))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
override fun createPublisherFor(controlIds: MutableList<String>): Flow.Publisher<Control> =
|
||||
Flow.Publisher { subscriber ->
|
||||
updateSubscriber = subscriber
|
||||
subscriber.onSubscribe(
|
||||
object : Flow.Subscription {
|
||||
override fun request(n: Long) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun cancel() {
|
||||
// Do nothing.
|
||||
}
|
||||
},
|
||||
)
|
||||
for (id in controlIds) {
|
||||
loadStatefulControl(subscriber, id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun performControlAction(
|
||||
controlId: String,
|
||||
action: ControlAction,
|
||||
consumer: Consumer<Int>,
|
||||
) {
|
||||
if (Global.checkNetwork(this)) {
|
||||
val device =
|
||||
Devices(this)
|
||||
.getDeviceById(controlId.substring(0, controlId.indexOf('@')))
|
||||
val api = Global.getCorrectAPI(this, device.mode, device.id)
|
||||
val relevantId = controlId.substring(device.id.length + 1)
|
||||
if (action is BooleanAction) {
|
||||
api.changeSwitchState(relevantId, action.newState)
|
||||
} else if (action is FloatAction) {
|
||||
api.changePercentage(relevantId, action.newValue)
|
||||
} else if (action is CommandAction) {
|
||||
api.execute(
|
||||
relevantId,
|
||||
object : UnifiedAPI.CallbackInterface {
|
||||
override fun onItemsLoaded(
|
||||
holder: UnifiedRequestCallback,
|
||||
recyclerViewInterface: HomeRecyclerViewHelperInterface?,
|
||||
) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
override fun onExecuted(
|
||||
result: String,
|
||||
shouldRefresh: Boolean,
|
||||
) {
|
||||
Toast.makeText(this@ControlService, result, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
consumer.accept(ControlAction.RESPONSE_OK)
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
loadStatefulControl(updateSubscriber, controlId)
|
||||
}, UPDATE_DELAY)
|
||||
} else {
|
||||
consumer.accept(ControlAction.RESPONSE_FAIL)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val UPDATE_DELAY = 100L
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-nodpi/header_bg.webp
Normal file
BIN
app/src/main/res/drawable-nodpi/header_bg.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
9
app/src/main/res/drawable/ic_about_contributor.xml
Normal file
9
app/src/main/res/drawable/ic_about_contributor.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorSecondary"
|
||||
android:pathData="M480,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM160,720v-32q0,-34 17.5,-62.5T224,582q62,-31 126,-46.5T480,520q66,0 130,15.5T736,582q29,15 46.5,43.5T800,688v32q0,33 -23.5,56.5T720,800L240,800q-33,0 -56.5,-23.5T160,720ZM240,720h480v-32q0,-11 -5.5,-20T700,654q-54,-27 -109,-40.5T480,600q-56,0 -111,13.5T260,654q-9,5 -14.5,14t-5.5,20v32ZM480,400q33,0 56.5,-23.5T560,320q0,-33 -23.5,-56.5T480,240q-33,0 -56.5,23.5T400,320q0,33 23.5,56.5T480,400ZM480,320ZM480,720Z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_about_github.xml
Normal file
9
app/src/main/res/drawable/ic_about_github.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorSecondary"
|
||||
android:pathData="M10.9,2.1c-4.6,0.5 -8.3,4.2 -8.8,8.7c-0.5,4.7 2.2,8.9 6.3,10.5C8.7,21.4 9,21.2 9,20.8v-1.6c0,0 -0.4,0.1 -0.9,0.1c-1.4,0 -2,-1.2 -2.1,-1.9c-0.1,-0.4 -0.3,-0.7 -0.6,-1C5.1,16.3 5,16.3 5,16.2C5,16 5.3,16 5.4,16c0.6,0 1.1,0.7 1.3,1c0.5,0.8 1.1,1 1.4,1c0.4,0 0.7,-0.1 0.9,-0.2c0.1,-0.7 0.4,-1.4 1,-1.8c-2.3,-0.5 -4,-1.8 -4,-4c0,-1.1 0.5,-2.2 1.2,-3C7.1,8.8 7,8.3 7,7.6C7,7.2 7,6.6 7.3,6c0,0 1.4,0 2.8,1.3C10.6,7.1 11.3,7 12,7s1.4,0.1 2,0.3C15.3,6 16.8,6 16.8,6C17,6.6 17,7.2 17,7.6c0,0.8 -0.1,1.2 -0.2,1.4c0.7,0.8 1.2,1.8 1.2,3c0,2.2 -1.7,3.5 -4,4c0.6,0.5 1,1.4 1,2.3v2.6c0,0.3 0.3,0.6 0.7,0.5c3.7,-1.5 6.3,-5.1 6.3,-9.3C22,6.1 16.9,1.4 10.9,2.1z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_about_info.xml
Normal file
9
app/src/main/res/drawable/ic_about_info.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorSecondary"
|
||||
android:pathData="M480,680q17,0 28.5,-11.5T520,640v-160q0,-17 -11.5,-28.5T480,440q-17,0 -28.5,11.5T440,480v160q0,17 11.5,28.5T480,680ZM480,360q17,0 28.5,-11.5T520,320q0,-17 -11.5,-28.5T480,280q-17,0 -28.5,11.5T440,320q0,17 11.5,28.5T480,360ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_about_library.xml
Normal file
9
app/src/main/res/drawable/ic_about_library.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorSecondary"
|
||||
android:pathData="M352,840L200,840q-33,0 -56.5,-23.5T120,760v-152q48,0 84,-30.5t36,-77.5q0,-47 -36,-77.5T120,392v-152q0,-33 23.5,-56.5T200,160h160q0,-42 29,-71t71,-29q42,0 71,29t29,71h160q33,0 56.5,23.5T800,240v160q42,0 71,29t29,71q0,42 -29,71t-71,29v160q0,33 -23.5,56.5T720,840L568,840q0,-50 -31.5,-85T460,720q-45,0 -76.5,35T352,840ZM200,760h85q24,-66 77,-93t98,-27q45,0 98,27t77,93h85v-240h80q8,0 14,-6t6,-14q0,-8 -6,-14t-14,-6h-80v-240L480,240v-80q0,-8 -6,-14t-14,-6q-8,0 -14,6t-6,14v80L200,240v88q54,20 87,67t33,105q0,57 -33,104t-87,68v88ZM460,500Z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_about_palette.xml
Normal file
9
app/src/main/res/drawable/ic_about_palette.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorSecondary"
|
||||
android:pathData="M480,880q-82,0 -155,-31.5t-127.5,-86Q143,708 111.5,635T80,480q0,-83 32.5,-156t88,-127Q256,143 330,111.5T488,80q80,0 151,27.5t124.5,76q53.5,48.5 85,115T880,442q0,115 -70,176.5T640,680h-74q-9,0 -12.5,5t-3.5,11q0,12 15,34.5t15,51.5q0,50 -27.5,74T480,880ZM480,480ZM260,520q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM380,360q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM580,360q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM700,520q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM480,800q9,0 14.5,-5t5.5,-13q0,-14 -15,-33t-15,-57q0,-42 29,-67t71,-25h70q66,0 113,-38.5T800,442q0,-121 -92.5,-201.5T488,160q-136,0 -232,93t-96,227q0,133 93.5,226.5T480,800Z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_add.xml
Normal file
9
app/src/main/res/drawable/ic_add.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorSecondary"
|
||||
android:pathData="M440,520L240,520q-17,0 -28.5,-11.5T200,480q0,-17 11.5,-28.5T240,440h200v-200q0,-17 11.5,-28.5T480,200q17,0 28.5,11.5T520,240v200h200q17,0 28.5,11.5T760,480q0,17 -11.5,28.5T720,520L520,520v200q0,17 -11.5,28.5T480,760q-17,0 -28.5,-11.5T440,720v-200Z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_arrow_back.xml
Normal file
9
app/src/main/res/drawable/ic_arrow_back.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:pathData="m313,520 l196,196q12,12 11.5,28T508,772q-12,11 -28,11.5T452,772L188,508q-6,-6 -8.5,-13t-2.5,-15q0,-8 2.5,-15t8.5,-13l264,-264q11,-11 27.5,-11t28.5,11q12,12 12,28.5T508,245L313,440h447q17,0 28.5,11.5T800,480q0,17 -11.5,28.5T760,520L313,520Z"
|
||||
android:fillColor="?android:attr/textColorSecondary"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_buttonpress.xml
Normal file
11
app/src/main/res/drawable/ic_buttonpress.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<vector android:height="24dp" android:viewportHeight="48"
|
||||
android:viewportWidth="48" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?colorAccent" android:pathData="M18,12C18,15.8672 14.8672,19 11,19C7.1328,19 4,15.8672 4,12C4,8.1328 7.1328,5 11,5C14.8672,5 18,8.1328 18,12Z"/>
|
||||
<path android:fillColor="#D8B094" android:pathData="M23.082,28.8594C23.7969,30.2734 25.5664,30.8281 27.0156,30.0938C28.4688,29.3594 29.0742,27.6055 28.3633,26.1953L25.1758,19.875C24.4648,18.4648 22.6953,17.9102 21.2422,18.6445C19.7891,19.3789 19.1836,21.1328 19.8945,22.543Z"/>
|
||||
<path android:fillColor="#D8B094" android:pathData="M28.3906,26.2344C29.1016,27.6445 30.8711,28.1992 32.3281,27.4648C33.7813,26.7344 34.3828,24.9766 33.6719,23.5664L30.4844,17.2461C29.7734,15.8398 28.0039,15.2813 26.5508,16.0156C25.0977,16.75 24.4922,18.5039 25.2031,19.9141Z"/>
|
||||
<path android:fillColor="#D8B094" android:pathData="M33.6992,23.6055C34.4102,25.0195 36.1797,25.5742 37.6367,24.8398C39.0859,24.1055 39.6914,22.3516 38.9805,20.9414L35.793,14.6211C35.0781,13.2109 33.3125,12.6563 31.8594,13.3906C30.4063,14.1211 29.8008,15.8789 30.5117,17.2891Z"/>
|
||||
<path android:fillColor="#D8B094" android:pathData="M40.2148,23.4219C39.9063,22.8125 40.2617,23.5156 40.5977,24.1758C40.7539,24.4883 40.4961,23.9766 40.2148,23.4219Z"/>
|
||||
<path android:fillColor="#D8B094" android:pathData="M20.8164,22.1758C20.3281,22.4219 19.7344,22.2266 19.4922,21.7383L13.7109,10.2773C13.2188,9.3047 11.5039,8.0898 9.7422,8.9766C7.9805,9.8672 7.9844,12.0625 8.4297,12.9453C8.6719,13.4258 11.8359,19.6992 14.6563,25.2852C16.9922,29.9219 19.0938,34.0898 19.0938,34.0898L13.5703,32.4648C11.7383,31.8633 9.8828,32.6641 9.4219,34.7656C8.9531,36.5352 10.1836,38.2891 11.8125,38.8242L19.3125,41.2852C23.1758,42.5547 27.1914,42.0938 30.8359,40.2461L31.3789,39.9688L32.1406,39.6641L36.9492,37.2344L38.2188,36.4492C41.7656,33.875 43.0742,29.0898 41.0391,25.0547C41.0391,25.0547 40.8203,24.6211 40.5977,24.1758C40.4219,23.832 39.7422,22.4805 37.9961,19.0234Z"/>
|
||||
<path android:fillColor="#F7D7C4" android:pathData="M9.3594,12.6016C8.8477,11.5625 9.2227,10.3281 10.2031,9.8477C11.1797,9.3711 12.3906,9.8242 12.8984,10.8594C13.4102,11.8984 9.8672,13.6367 9.3594,12.6016"/>
|
||||
<path android:fillColor="#F7D7C4" android:pathData="M11.8828,37.6055C10.7031,37.2891 10.0078,36.0781 10.3203,34.8984C10.6406,33.7188 11.8516,33.0234 13.0273,33.3398C14.207,33.6563 13.0625,37.9258 11.8828,37.6055"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_circle.xml
Normal file
11
app/src/main/res/drawable/ic_circle.xml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid
|
||||
android:color="?android:attr/textColorSecondary"/>
|
||||
|
||||
<size
|
||||
android:width="24dp"
|
||||
android:height="24dp"/>
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/ic_color_palette.xml
Normal file
9
app/src/main/res/drawable/ic_color_palette.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24"
|
||||
android:width="24dp">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5L16,16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,8C8.67,8 8,7.33 8,6.5S8.67,5 9.5,5s1.5,0.67 1.5,1.5S10.33,8 9.5,8zM14.5,8c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,5 14.5,5s1.5,0.67 1.5,1.5S15.33,8 14.5,8zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_delete.xml
Normal file
9
app/src/main/res/drawable/ic_delete.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorSecondary"
|
||||
android:pathData="M280,840q-33,0 -56.5,-23.5T200,760v-520q-17,0 -28.5,-11.5T160,200q0,-17 11.5,-28.5T200,160h160q0,-17 11.5,-28.5T400,120h160q17,0 28.5,11.5T600,160h160q17,0 28.5,11.5T800,200q0,17 -11.5,28.5T760,240v520q0,33 -23.5,56.5T680,840L280,840ZM680,240L280,240v520h400v-520ZM400,680q17,0 28.5,-11.5T440,640v-280q0,-17 -11.5,-28.5T400,320q-17,0 -28.5,11.5T360,360v280q0,17 11.5,28.5T400,680ZM560,680q17,0 28.5,-11.5T600,640v-280q0,-17 -11.5,-28.5T560,320q-17,0 -28.5,11.5T520,360v280q0,17 11.5,28.5T560,680ZM280,240v520,-520Z"/>
|
||||
</vector>
|
||||
BIN
app/src/main/res/drawable/ic_device_christmas_tree.webp
Normal file
BIN
app/src/main/res/drawable/ic_device_christmas_tree.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 870 B |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue