Initial stable version
This commit is contained in:
Родитель
11efcaa703
Коммит
3f54c41ede
|
@ -1,23 +1,54 @@
|
|||
# Compiled class file
|
||||
# Windows thumbnail db
|
||||
Thumbs.db
|
||||
|
||||
# OSX files
|
||||
.DS_Store
|
||||
|
||||
# built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# files for the dex VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
# generated files
|
||||
bin/
|
||||
gen/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# Android Studio
|
||||
.gradle
|
||||
/*/local.properties
|
||||
/*/out
|
||||
/*/*/build
|
||||
build
|
||||
/*/*/production
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
*~
|
||||
*.swp
|
||||
/.idea/libraries
|
||||
/.idea/caches
|
||||
/.idea/gradle.xml
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/tasks.xml
|
||||
/.idea/vcs.xml
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
*.orig
|
||||
.idea/.name
|
||||
.idea/jarRepositories.xml
|
||||
.idea/navEditor.xml
|
||||
|
|
|
@ -0,0 +1,298 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="WizardSettings">
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="imageWizard">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="imageAssetPanel">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="actionbar">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="clipArt">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
<entry key="imagePath" value="C:\Users\dunajczykb\AppData\Local\Temp\ic_android_black_24dp.xml" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="text">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="textAsset">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="launcher">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="foregroundClipArt">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="imagePath" value="C:\Users\dunajczykb\AppData\Local\Temp\ic_android_black_24dp.xml" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="foregroundImage">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
<entry key="imagePath" value="D:\Projects\Microsoft\Dynamics 365\Graphics\ic_icon.png" />
|
||||
<entry key="scalingPercent" value="46" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="foregroundText">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="foregroundTextAsset">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="backgroundAssetType" value="COLOR" />
|
||||
<entry key="backgroundColor" value="ffffff" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="launcherLegacy">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="clipArt">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
<entry key="imagePath" value="C:\Users\dunajczykb\AppData\Local\Temp\ic_android_black_24dp.xml" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="text">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="textAsset">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="notification">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="clipArt">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
<entry key="imagePath" value="C:\Users\dunajczykb\AppData\Local\Temp\ic_android_black_24dp.xml" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="text">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="textAsset">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="tvBanner">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="foregroundText">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="tvChannel">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="children">
|
||||
<map>
|
||||
<entry key="foregroundClipArt">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="imagePath" value="C:\Users\dunajczykb\AppData\Local\Temp\ic_android_black_24dp.xml" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="foregroundImage">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="foregroundText">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="foregroundTextAsset">
|
||||
<value>
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="color" value="000000" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,122 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
||||
</project>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="JDK" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,110 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'androidx.navigation.safeargs'
|
||||
|
||||
android {
|
||||
signingConfigs {
|
||||
ttpsctemprelease {
|
||||
storeFile file('..\\ttpsctemprelease.jks')
|
||||
storePassword 'Password'
|
||||
keyPassword 'Password'
|
||||
keyAlias 'ttpsctemprelease'
|
||||
}
|
||||
}
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.2"
|
||||
|
||||
kapt {
|
||||
generateStubs = true
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.ttpsc.com.ttpsc.dynamics365fieldService"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 29
|
||||
versionCode 10
|
||||
versionName "1.0 b10"
|
||||
|
||||
renderscriptTargetApi 29
|
||||
renderscriptSupportModeEnabled true
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.ttpsctemprelease
|
||||
}
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
// To inline the bytecode built with JVM target 1.8 into
|
||||
// bytecode that is being built with JVM target 1.6. (e.g. navArgs)
|
||||
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.71"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.0.0'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
|
||||
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.0'
|
||||
implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
|
||||
implementation "com.github.akarnokd:rxjava3-retrofit-adapter:3.0.0"
|
||||
|
||||
implementation 'com.google.dagger:dagger-android:2.27'
|
||||
implementation 'com.google.dagger:dagger-android-support:2.27'
|
||||
annotationProcessor 'com.google.dagger:dagger-android-processor:2.27'
|
||||
annotationProcessor 'com.google.dagger:dagger-compiler:2.27'
|
||||
|
||||
kapt 'com.google.dagger:dagger-compiler:2.27'
|
||||
kapt 'com.google.dagger:dagger-android-processor:2.27'
|
||||
|
||||
compileOnly 'javax.annotation:jsr250-api:1.0'
|
||||
implementation 'javax.inject:javax.inject:1'
|
||||
|
||||
implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion"
|
||||
implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion"
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
|
||||
implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.8.1'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.5.0'
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
||||
def appCenterSdkVersion = '3.2.2'
|
||||
implementation "com.microsoft.appcenter:appcenter-analytics:${appCenterSdkVersion}"
|
||||
implementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||
implementation "androidx.security:security-crypto:1.1.0-alpha01"
|
||||
implementation 'org.conscrypt:conscrypt-android:2.2.1'
|
||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||
implementation 'com.microsoft.identity.client:msal:1.4.+'
|
||||
implementation 'com.googlecode.mp4parser:isoparser:1.1.22'
|
||||
implementation 'com.squareup.retrofit2:converter-scalars:2.5.0'
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# 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
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": 1,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.ttpsc.com.ttpsc.dynamics365fieldService",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"properties": [],
|
||||
"versionCode": 2,
|
||||
"versionName": "2",
|
||||
"enabled": true,
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.ttpsc.dynamics365fieldService
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.ttpsc.com.ttpsc.dynamics365fieldService", appContext.packageName)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.ttpsc.dynamics365fieldService">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.sensor.gyroscope"
|
||||
android:required="true" />
|
||||
<uses-feature android:name="android.hardware.camera.any"
|
||||
android:required="true" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"
|
||||
android:required="true" />
|
||||
|
||||
<!--Permissions-->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
|
||||
<application
|
||||
android:name=".AndroidApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/appcenter_backup_rule"
|
||||
android:icon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".views.activities.MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="dynamicsfcs" />
|
||||
<data android:host="workorders" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="com.microsoft.identity.client.BrowserTabActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:host="com.ttpsc.dynamics365fieldService"
|
||||
android:path="/Bmce+9aHdOoVtE7fS3B07tfj7Bc="
|
||||
android:scheme="msauth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name="com.ttpsc.dynamics365fieldService.views.activities.RecordingActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 40 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 175 KiB |
|
@ -0,0 +1,28 @@
|
|||
package com.ttpsc.dynamics365fieldService
|
||||
|
||||
import android.app.Application
|
||||
import com.ttpsc.dynamics365fieldService.core.di.ApplicationComponent
|
||||
import com.ttpsc.dynamics365fieldService.core.di.ApplicationModule
|
||||
import com.ttpsc.dynamics365fieldService.core.di.DaggerApplicationComponent
|
||||
|
||||
class AndroidApplication : Application() {
|
||||
|
||||
companion object {
|
||||
var companionAppComponent: ApplicationComponent? = null
|
||||
}
|
||||
|
||||
private val appComponent: ApplicationComponent by lazy(mode = LazyThreadSafetyMode.NONE) {
|
||||
DaggerApplicationComponent
|
||||
.builder()
|
||||
.applicationModule(ApplicationModule(this))
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
this.injectMembers()
|
||||
companionAppComponent = appComponent
|
||||
}
|
||||
|
||||
private fun injectMembers() = appComponent.inject(this)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.ttpsc.dynamics365fieldService
|
||||
|
||||
class AppConfiguration {
|
||||
companion object {
|
||||
val ENDPOINT_SUFFIX = "/api/data/v9.0/"
|
||||
private var _endpoint = ""
|
||||
var endpoint: String
|
||||
get() = _endpoint
|
||||
set(value) {
|
||||
_endpoint = value
|
||||
}
|
||||
}
|
||||
|
||||
class StorageKeys {
|
||||
companion object {
|
||||
val environmentKey = "ENVIRONMENT_KEY"
|
||||
val userAccountKey = "USER_ACCOUNT_KEY"
|
||||
val accessTokenKey = "TOKEN_STORAGE_KEY"
|
||||
val userResourceIdKey = "USER_RESOURCE_ID_KEY"
|
||||
val accessTokenExpirationDateKey = "ACCESS_TOKEN_EXPIRATION_DATE_KEY"
|
||||
val userFirstNameKey = "USER_FIRST_NAME_KEY"
|
||||
val userLastNameKey = "USER_LAST_NAME_KEY"
|
||||
}
|
||||
}
|
||||
|
||||
class RequestTags{
|
||||
companion object {
|
||||
val leaveUrl = "LEAVE_URL"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
|
||||
interface Operation<TResult> {
|
||||
|
||||
fun execute(): Observable<TResult>
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
|
||||
interface ChangeBookableResourceBookingStatusOperation: Operation<Pair<Boolean, String?>> {
|
||||
var bookingStatusId: String
|
||||
var bookableResourceBookingId: String
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
|
||||
interface CreateNoteOperation: Operation<Pair<Boolean, String?>> {
|
||||
var filename: String?
|
||||
var subject: String?
|
||||
var isDocument: Boolean
|
||||
var objecttypecode: String
|
||||
var workOrderId: String?
|
||||
var documentBody: Array<Byte>?
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
|
||||
interface CreateProcedureOperation : Operation<Procedure> {
|
||||
var title: String
|
||||
var subject: String?
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.DescriptionLine
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
import java.io.File
|
||||
|
||||
interface CreateStepOperation : Operation<Procedure> {
|
||||
var guideId: String
|
||||
var stepNumber: Int
|
||||
var descriptionLines: List<DescriptionLine>
|
||||
var title: String
|
||||
var images: List<File>?
|
||||
var video: File?
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
|
||||
interface DownloadFileOperation : Operation<ByteArray> {
|
||||
var fileUrl: String
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Alert
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
|
||||
interface GetAlertsOperation : Operation<List<Alert>>{
|
||||
var queryMap: Map<String, String>?
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResourceBooking
|
||||
|
||||
interface GetBookableResourceBookingDetailsOperation: Operation<BookableResourceBooking> {
|
||||
var bookableResourceBookingId: String
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
|
||||
interface GetBookableResourceBookingsCountOperation : Operation<Int> {
|
||||
var bookableResource: BookableResource
|
||||
var query: Map<String, String>?
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResourceBooking
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.PageableModel
|
||||
|
||||
interface GetBookableResourcesBookingsOperation :
|
||||
Operation<PageableModel<List<BookableResourceBooking>>> {
|
||||
var bookableResource: BookableResource
|
||||
var query: Map<String, String>?
|
||||
var itemsPerPage: Int?
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookingStatus
|
||||
|
||||
interface GetBookableResourcesOperation : Operation<BookableResource>
|
|
@ -0,0 +1,6 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookingStatus
|
||||
|
||||
interface GetBookingStatusesOperation : Operation<List<BookingStatus>>
|
|
@ -0,0 +1,6 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Category
|
||||
|
||||
interface GetCategoriesOperation : Operation<MutableList<Category>>
|
|
@ -0,0 +1,8 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.CustomField
|
||||
|
||||
interface GetFormOperation: Operation<List<CustomField>> {
|
||||
var query: Map<String, String>?
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Note
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.WorkOrder
|
||||
|
||||
interface GetNotesOperation : Operation<List<Note>> {
|
||||
var queryMap: Map<String, String>?
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
|
||||
interface GetProcedureOperation : Operation<Procedure> {
|
||||
var procedureId: String
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
|
||||
interface GetProceduresOperation : Operation<List<Procedure>>{
|
||||
var bookableResource: BookableResource
|
||||
var query: Map<String, String>?
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Note
|
||||
|
||||
interface GetRawWorkOrdersOperation : Operation<Pair<Map<String, String>, String?>> {
|
||||
var queryMap: Map<String, String>?
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.UserInfo
|
||||
|
||||
interface GetUserInfoOperation : Operation<List<UserInfo>>{
|
||||
var email: String
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.WorkOrder
|
||||
|
||||
interface GetWorkOrdersOperation : Operation<List<WorkOrder>>{
|
||||
var queryMap: Map<String, String>?
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
|
||||
|
||||
interface LogInOperation : Operation<Boolean> {
|
||||
var userName: String?
|
||||
var environmentUrl: String?
|
||||
var activity: Activity?
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
|
||||
interface SaveEnvironmentDataOperation: Operation<Unit> {
|
||||
var environmentUrl: String?
|
||||
var userEmail: String?
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.abstraction.operations
|
||||
|
||||
import android.app.Activity
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.Operation
|
||||
|
||||
interface UpdateUserSessionOperation: Operation<Unit> {
|
||||
var activity: Activity?
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
class Alert (
|
||||
val alertId: String?,
|
||||
val building: String?,
|
||||
val caseOwner: String?,
|
||||
val caseOwnerId: String?,
|
||||
val description: String?,
|
||||
val deviceName: String?,
|
||||
val equipment: String?,
|
||||
val faultSavings: Float?,
|
||||
val floor: String?,
|
||||
val fullAssetPath: String?,
|
||||
val stateCode: Int?,
|
||||
val ioTHubDeviceId: String?,
|
||||
val latitude: Float?,
|
||||
val likelihood: Float?,
|
||||
val longitude: Float?,
|
||||
val subject: String?,
|
||||
val unit: String?,
|
||||
val equipmentClass: String?,
|
||||
val workOrderId: String?
|
||||
): Serializable
|
|
@ -0,0 +1,24 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class Attachment(
|
||||
val fileUrl: String?,
|
||||
val filePath: String?,
|
||||
val customFileName: String?
|
||||
) : Serializable {
|
||||
var thumbnailUrl: String? = ""
|
||||
var mediumUrl: String? = ""
|
||||
var attachmentType: AttachmentType = AttachmentType.NONE
|
||||
|
||||
@Suppress("unused")
|
||||
val fileName: String?
|
||||
get() {
|
||||
val fileUri = filePath ?: fileUrl
|
||||
return fileUri?.split('/')?.last() ?: customFileName
|
||||
}
|
||||
}
|
||||
|
||||
enum class AttachmentType : Serializable {
|
||||
NONE, IMAGE, VIDEO
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
class BookableResource(
|
||||
val userId: String,
|
||||
val bookableResourceId: String
|
||||
)
|
|
@ -0,0 +1,55 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import java.util.*
|
||||
|
||||
class BookableResourceBooking(
|
||||
var id: String?,
|
||||
var workOrderId: String?,
|
||||
var workOrder: WorkOrder?,
|
||||
var startTime: Date?,
|
||||
var endTime: Date?,
|
||||
var resource: String?,
|
||||
var bookingStatus: String?,
|
||||
var name: String?,
|
||||
var stateCode: Int?,
|
||||
var statusCode: Int?,
|
||||
var latitude: String?,
|
||||
var longitude: String?
|
||||
)
|
||||
{
|
||||
var nextPageLink: String? = null
|
||||
|
||||
fun toProcedure(): Procedure{
|
||||
val procedure = Procedure(
|
||||
id = this.id!!,
|
||||
key = this.id!!,
|
||||
guid = "",
|
||||
title = workOrder?.summary,
|
||||
categoryName = "",
|
||||
descriptionLines = mutableListOf(DescriptionLine(
|
||||
textRaw = workOrder?.iotAlerts?.firstOrNull()?.description ?: "",
|
||||
bullet = TextBullet.None,
|
||||
level = 0,
|
||||
lineId = 0
|
||||
)),
|
||||
labels = listOf(),
|
||||
template = null,
|
||||
parent = null,
|
||||
children = null,
|
||||
timeSpentMillis = 0,
|
||||
timeEstimateMillis = 0,
|
||||
attachments = mutableListOf(),
|
||||
isTemplate = false,
|
||||
isSynchronizable = false,
|
||||
level = "",
|
||||
alerts = workOrder?.iotAlerts ?: mutableListOf(),
|
||||
startTime = this.startTime,
|
||||
endTime = this.endTime,
|
||||
status = bookingStatus,
|
||||
workOrderId = workOrder?.id,
|
||||
workOrder = workOrder
|
||||
)
|
||||
|
||||
return procedure
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
@Parcelize
|
||||
class BookingStatus(
|
||||
val name: String,
|
||||
var bookingStatusId: String? = null
|
||||
) : Parcelable, Serializable
|
|
@ -0,0 +1,17 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class Category(
|
||||
val id: String,
|
||||
val key: String,
|
||||
val guid: String?,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val labels: List<String>?,
|
||||
var procedures: List<Procedure>?,
|
||||
val timeSpentMillis: Int = 0,
|
||||
val timeEstimateMillis: Int = 0,
|
||||
val attachments: List<Attachment>?,
|
||||
val isSynchronizable: Boolean = false
|
||||
) : Serializable
|
|
@ -0,0 +1,10 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
class CustomField(
|
||||
var fieldName: String,
|
||||
var fieldDescription: String,
|
||||
var fieldValue: String
|
||||
) : Serializable {
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class DescriptionLine(
|
||||
val textRaw: String,
|
||||
val bullet: TextBullet?,
|
||||
val level: Int?,
|
||||
val lineId: Any?
|
||||
) : Serializable {
|
||||
override fun toString() = textRaw
|
||||
}
|
||||
|
||||
enum class TextBullet : Serializable {
|
||||
None,
|
||||
Black,
|
||||
Red,
|
||||
Orange,
|
||||
Yellow,
|
||||
LightBlue,
|
||||
Blue,
|
||||
Green,
|
||||
Violet,
|
||||
IconNote,
|
||||
IconCaution,
|
||||
IconReminder
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
@Parcelize
|
||||
class Note (
|
||||
val id: String?,
|
||||
val fileName: String?,
|
||||
val subject: String?,
|
||||
val isDocument:Boolean,
|
||||
val associatedTo: String?,
|
||||
val date: Date?
|
||||
): Parcelable, Serializable {
|
||||
lateinit var documentBody: Array<Byte>
|
||||
var userInfo: UserInfo? = null
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class OauthResult(
|
||||
@SerializedName("token_type")
|
||||
val tokenType:String,
|
||||
@SerializedName("scope")
|
||||
val scope: String,
|
||||
@SerializedName("expires_in")
|
||||
val expirationSeconds: Int,
|
||||
@SerializedName("access_token")
|
||||
val access_token: String,
|
||||
@SerializedName("refresh_token")
|
||||
val refresh_token: String,
|
||||
@SerializedName("id_token")
|
||||
val id_token: String
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
class PageableModel<ModelType> (
|
||||
val model: ModelType,
|
||||
val nextPageLink: String?
|
||||
)
|
|
@ -0,0 +1,32 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
data class Procedure(
|
||||
val id: String,
|
||||
val key: String,
|
||||
val guid: String?,
|
||||
var title: String?,
|
||||
val categoryName: String,
|
||||
val descriptionLines: List<DescriptionLine>,
|
||||
val labels: List<String>?,
|
||||
val template: Procedure?,
|
||||
val parent: Procedure?,
|
||||
var children: List<Procedure>?,
|
||||
val timeSpentMillis: Int = 0,
|
||||
val timeEstimateMillis: Int = 0,
|
||||
val attachments: MutableList<Attachment>?,
|
||||
val isTemplate: Boolean = true,
|
||||
val isSynchronizable: Boolean = false,
|
||||
val level: String,
|
||||
val alerts: MutableList<Alert> = mutableListOf(),
|
||||
val startTime: Date?,
|
||||
val endTime: Date?,
|
||||
val status: String?,
|
||||
val workOrderId: String?,
|
||||
var workOrder: WorkOrder?
|
||||
) : Serializable {
|
||||
var customFields: MutableList<CustomField> = mutableListOf()
|
||||
override fun toString() = "$id | $title"
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.io.Serializable
|
||||
|
||||
data class QrCodeScan (
|
||||
@SerializedName("ResourceId")
|
||||
var environmentUrl: String,
|
||||
@SerializedName("UserId")
|
||||
val login: String
|
||||
): Serializable {}
|
|
@ -0,0 +1,30 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
class ServiceTerritory(
|
||||
val odata_etag: String? = null,
|
||||
val entityimageid: String? = null,
|
||||
val timezoneruleversionnumber: String? = null,
|
||||
val modifiedon: String? = null,
|
||||
val importsequencenumber: String? = null,
|
||||
val _parentterritoryid_value: String? = null,
|
||||
val _managerid_value: String? = null,
|
||||
val _createdonbehalfby_value: String? = null,
|
||||
val _createdby_value: String? = null,
|
||||
val _modifiedonbehalfby_value: String? = null,
|
||||
val entityimage_timestamp: String? = null,
|
||||
val exchangerate: String? = null,
|
||||
val description: String? = null,
|
||||
val utcconversiontimezonecode: String? = null,
|
||||
val versionnumber: Int? = null,
|
||||
val _modifiedby_value: String? = null,
|
||||
val _transactioncurrencyid_value: String? = null,
|
||||
val entityimage: String? = null,
|
||||
val entityimage_url: String? = null,
|
||||
val createdon: String? = null,
|
||||
val overriddencreatedon: String? = null,
|
||||
val territoryid: String? = null,
|
||||
val _organizationid_value: String? = null,
|
||||
val name: String?
|
||||
) : Serializable
|
|
@ -0,0 +1,13 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
class UserInfo(
|
||||
val resourceId: String,
|
||||
val email: String,
|
||||
val fullName: String?,
|
||||
val firstName: String?,
|
||||
val lastName: String?
|
||||
) : Serializable {
|
||||
override fun toString() = "$resourceId | $email"
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.models
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import java.io.Serializable
|
||||
|
||||
class WorkOrder(
|
||||
var id: String?,
|
||||
var name: String? = null,
|
||||
var summary: String? = null,
|
||||
var description: String? = null,
|
||||
var address1: String? = null,
|
||||
var address2: String? = null,
|
||||
var address3: String? = null,
|
||||
var city: String? = null,
|
||||
var stateOrProvince: String? = null,
|
||||
var postalCode: String? = null,
|
||||
var country: String? = null,
|
||||
var serviceTerritoryId: String? = null,
|
||||
var iotAlertId: String? = null,
|
||||
var iotAlerts: MutableList<Alert> = mutableListOf(),
|
||||
var notes: List<Note> = listOf(),
|
||||
var videoAttachmentUrl: String? = null,
|
||||
var imageAttachmentUrl: String? = null,
|
||||
var documentAttachmentUrl: String? = null,
|
||||
var serviceTerritory: ServiceTerritory? = null,
|
||||
var iconicsLink: String? = null
|
||||
) : Serializable
|
|
@ -0,0 +1,40 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.ttpsc.dynamics365fieldService.AppConfiguration
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.ChangeBookableResourceBookingStatusOperation
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.models.dynamics.DalChangeStatusError
|
||||
import com.ttpsc.dynamics365fieldService.dal.models.dynamics.DalChangeStatusErrorBody
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class DynamicsChangeBookableResourceBookingStatus @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) :
|
||||
ChangeBookableResourceBookingStatusOperation {
|
||||
|
||||
private val _repository = repository
|
||||
|
||||
override lateinit var bookingStatusId: String
|
||||
override lateinit var bookableResourceBookingId: String
|
||||
|
||||
override fun execute(): Observable<Pair<Boolean, String?>> {
|
||||
return _repository.changeBookableResourceBookingStatus(
|
||||
bookableResourceBookingId,
|
||||
mapOf("@odata.id" to "${AppConfiguration.endpoint}${AppConfiguration.ENDPOINT_SUFFIX}bookingstatuses($bookingStatusId)")
|
||||
).executeOnBackground().map { response ->
|
||||
if (response.isSuccessful == false) {
|
||||
val error = Gson().fromJson(
|
||||
response.errorBody()?.string(),
|
||||
DalChangeStatusErrorBody::class.java
|
||||
)
|
||||
return@map Pair<Boolean, String?>(true, error.error?.message)
|
||||
}
|
||||
return@map Pair<Boolean, String?>(false, null)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.CreateNoteOperation
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.models.dynamics.DalChangeStatusErrorBody
|
||||
import com.ttpsc.dynamics365fieldService.dal.models.dynamics.DalCreateNoteRequestBody
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsCreateNoteOperation @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) :
|
||||
CreateNoteOperation {
|
||||
override var filename: String? = null
|
||||
override var subject: String? = null
|
||||
override var isDocument: Boolean = false
|
||||
override lateinit var objecttypecode: String
|
||||
override var workOrderId: String? = null
|
||||
override var documentBody: Array<Byte>? = null
|
||||
|
||||
private val _repository = repository
|
||||
|
||||
override fun execute(): Observable<Pair<Boolean, String?>> {
|
||||
|
||||
val note = DalCreateNoteRequestBody()
|
||||
|
||||
if (filename != null) {
|
||||
note.filename = this.filename!!
|
||||
}
|
||||
|
||||
if (subject != null) {
|
||||
note.subject = this.subject!!
|
||||
}
|
||||
|
||||
note.isdocument = this.isDocument
|
||||
|
||||
if (workOrderId != null) {
|
||||
note.objecttypecode = "msdyn_workorder"
|
||||
note.workOrderBindPath = "/msdyn_workorders($workOrderId)"
|
||||
}
|
||||
if(documentBody != null) {
|
||||
note.base64documentbody = Base64.getEncoder().encodeToString(documentBody!!.toByteArray())
|
||||
}
|
||||
|
||||
return _repository.createNote(note)
|
||||
.executeOnBackground()
|
||||
.map {response ->
|
||||
if (response.isSuccessful == false) {
|
||||
val error = Gson().fromJson(
|
||||
response.errorBody()?.string(),
|
||||
DalChangeStatusErrorBody::class.java
|
||||
)
|
||||
return@map Pair<Boolean, String?>(true, error.error?.message)
|
||||
}
|
||||
return@map Pair<Boolean, String?>(false, null)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.DownloadFileOperation
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsDownloadFile @Inject constructor(dynamicsRepository: DynamicsApiRepository) :
|
||||
DownloadFileOperation {
|
||||
private val _dynamicsRepository = dynamicsRepository
|
||||
override lateinit var fileUrl: String
|
||||
|
||||
override fun execute(): Observable<ByteArray> {
|
||||
return _dynamicsRepository
|
||||
.downloadFile(fileUrl)
|
||||
.executeOnBackground()
|
||||
.map { it?.bytes() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetAlertsOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetProceduresOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Alert
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.AuthorizationManager
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetAlerts @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) :
|
||||
GetAlertsOperation {
|
||||
|
||||
private val _repository = repository
|
||||
|
||||
override var queryMap: Map<String, String>? = null
|
||||
|
||||
override fun execute(): Observable<List<Alert>> {
|
||||
return _repository.getAlerts(queryMap)
|
||||
.executeOnBackground()
|
||||
.map { it ->
|
||||
it.value.map { dalAlert -> dalAlert.toAlert() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetBookableResourcesOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetProceduresOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.AuthorizationManager
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetBookableResource @Inject constructor(
|
||||
repository: DynamicsApiRepository,
|
||||
authorizationManager: AuthorizationManager
|
||||
) :
|
||||
GetBookableResourcesOperation {
|
||||
|
||||
private val _repository = repository
|
||||
private val _authorizationManager = authorizationManager
|
||||
|
||||
override fun execute(): Observable<BookableResource> {
|
||||
val queryMap =
|
||||
mapOf("\$filter" to "_userid_value eq ${_authorizationManager.getUserResourceId()}")
|
||||
|
||||
return _repository.getBookableResources(queryMap)
|
||||
.executeOnBackground()
|
||||
.map { it ->
|
||||
it.value.first().toBookableResource()
|
||||
//TODO remove and throw exception if there is no resource
|
||||
/*if(it.value.count() > 0) {
|
||||
it.value.first().toBookableResource()
|
||||
}else{
|
||||
|
||||
BookableResource("", "6ff1d0b1-32a0-ea11-a812-000d3a33fb78")
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetBookableResourceBookingDetailsOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResourceBooking
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetBookableResourceBookingDetails @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
): GetBookableResourceBookingDetailsOperation {
|
||||
private val _repository = repository
|
||||
|
||||
override var bookableResourceBookingId: String = ""
|
||||
|
||||
override fun execute(): Observable<BookableResourceBooking> {
|
||||
val queryMap =
|
||||
mutableMapOf("\$filter" to "bookableresourcebookingid eq $bookableResourceBookingId")
|
||||
|
||||
return _repository.getBookableResourceBookingDetails(queryMap)
|
||||
.executeOnBackground()
|
||||
.map { it ->
|
||||
it.value.first().toBookableResourceBooking()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetBookableResourceBookingsCountOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetBookableResourceBookingsCount @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) :
|
||||
GetBookableResourceBookingsCountOperation {
|
||||
|
||||
private val _repository = repository
|
||||
override lateinit var bookableResource: BookableResource
|
||||
override var query: Map<String, String>? = null
|
||||
|
||||
override fun execute(): Observable<Int> {
|
||||
val queryMap =
|
||||
mutableMapOf(
|
||||
"\$filter" to "_resource_value eq ${bookableResource.bookableResourceId}",
|
||||
"\$select" to "bookableresourcebookingid",
|
||||
"\$count" to "true"
|
||||
)
|
||||
query?.let {
|
||||
for (map in it){
|
||||
if(queryMap[map.key] != null) {
|
||||
queryMap[map.key] += " and " + map.value
|
||||
}else {
|
||||
queryMap[map.key] = map.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _repository.getBookableResourceBookings(queryMap, null)
|
||||
.executeOnBackground()
|
||||
.map { it ->
|
||||
it.count
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetBookableResourcesBookingsOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResourceBooking
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.PageableModel
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.AuthorizationManager
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetBookableResourceBookingsOperation @Inject constructor(
|
||||
repository: DynamicsApiRepository) :
|
||||
GetBookableResourcesBookingsOperation {
|
||||
|
||||
private val _repository = repository
|
||||
override lateinit var bookableResource: BookableResource
|
||||
override var query: Map<String, String>? = null
|
||||
override var itemsPerPage: Int? = null
|
||||
|
||||
override fun execute(): Observable<PageableModel<List<BookableResourceBooking>>> {
|
||||
val queryMap =
|
||||
mutableMapOf("\$filter" to "_resource_value eq ${bookableResource.bookableResourceId}")
|
||||
query?.let {
|
||||
for (map in it){
|
||||
if(queryMap[map.key] != null) {
|
||||
queryMap[map.key] += " and " + map.value
|
||||
}else {
|
||||
queryMap[map.key] = map.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var header: String? = null
|
||||
|
||||
if (itemsPerPage != null) {
|
||||
header = "odata.maxpagesize=$itemsPerPage"
|
||||
}
|
||||
|
||||
return _repository.getBookableResourceBookings(queryMap, header)
|
||||
.executeOnBackground()
|
||||
.map {
|
||||
PageableModel(
|
||||
it.value.map { dalBookableResourceBooking -> dalBookableResourceBooking.toBookableResourceBooking() },
|
||||
it.nextPageLink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetBookingStatusesOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetProceduresOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookingStatus
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetBookingStatusesOperation @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) :
|
||||
GetBookingStatusesOperation {
|
||||
private val _repository = repository
|
||||
|
||||
override fun execute(): Observable<List<BookingStatus>> =
|
||||
_repository.getBookingStatues()
|
||||
.executeOnBackground()
|
||||
.map { it ->
|
||||
it.value.map { dalBookingStatus -> dalBookingStatus.toBookingStatus() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetFormOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.CustomField
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetForm @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) : GetFormOperation {
|
||||
|
||||
private val _repository = repository
|
||||
override var query: Map<String, String>? = null
|
||||
override fun execute(): Observable<List<CustomField>> {
|
||||
return _repository.downloadForm(query)
|
||||
.executeOnBackground()
|
||||
.map { it ->
|
||||
if (it.value.count() > 0) {
|
||||
it.value.first().parseForm().map { it.toCustomField() }
|
||||
} else {
|
||||
mutableListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetNotesOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Note
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetNotes @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) :
|
||||
GetNotesOperation {
|
||||
private val _repository = repository
|
||||
override var queryMap: Map<String, String>? = null
|
||||
|
||||
override fun execute(): Observable<List<Note>> =
|
||||
_repository.getNotes(queryMap)
|
||||
.executeOnBackground()
|
||||
.map { it ->
|
||||
it.value.map { dalNote -> dalNote.toNote() }
|
||||
}.map {
|
||||
it.sortedByDescending { note -> note.date }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetRawWorkOrdersOperation
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.models.dynamics.DalChangeStatusErrorBody
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetRawWorkOrder @Inject
|
||||
constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) :
|
||||
GetRawWorkOrdersOperation {
|
||||
private val _repository = repository
|
||||
override var queryMap: Map<String, String>? = null
|
||||
|
||||
override fun execute(): Observable<Pair<Map<String, String>, String?>> =
|
||||
_repository.getRawWorkOrders(queryMap)
|
||||
.executeOnBackground()
|
||||
.map {responseBody ->
|
||||
|
||||
val bodyString = responseBody.body()
|
||||
|
||||
val fieldValueMap: MutableMap<String, String> = mutableMapOf()
|
||||
if(responseBody.isSuccessful && bodyString != null) {
|
||||
val responseString = bodyString.substringAfter("value\":")
|
||||
.replace("{", "")
|
||||
.replace("}", "")
|
||||
.replace("]", "")
|
||||
.replace("[", "")
|
||||
.replace("\"", "")
|
||||
|
||||
val fieldValues = responseString.split(",")
|
||||
|
||||
for (fieldValue in fieldValues) {
|
||||
val splitted = fieldValue.split(
|
||||
delimiters = *arrayOf(":"),
|
||||
ignoreCase = true,
|
||||
limit = 2
|
||||
)
|
||||
if (splitted.count() > 1 && splitted[1].contains("null") == false) {
|
||||
fieldValueMap.put(splitted[0], splitted[1])
|
||||
}
|
||||
}
|
||||
}else
|
||||
{
|
||||
if (responseBody.isSuccessful == false) {
|
||||
val error = Gson().fromJson(
|
||||
responseBody.errorBody()?.string(),
|
||||
DalChangeStatusErrorBody::class.java
|
||||
)
|
||||
return@map Pair<Map<String, String>, String?>(fieldValueMap, error.error?.message)
|
||||
}
|
||||
}
|
||||
return@map Pair<Map<String, String>, String?>(fieldValueMap, null)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetUserInfoOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetWorkOrdersOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.UserInfo
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetUserInfo @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) :
|
||||
GetUserInfoOperation {
|
||||
override lateinit var email: String
|
||||
|
||||
private val _repository = repository
|
||||
|
||||
override fun execute(): Observable<List<UserInfo>> {
|
||||
val query = mapOf<String, String>(
|
||||
"\$select" to "systemuserid, fullname, windowsliveid, domainname, firstname, lastname",
|
||||
"\$filter" to "domainname eq $email"
|
||||
)
|
||||
|
||||
return _repository.getUserInfo(query)
|
||||
.executeOnBackground()
|
||||
.map { it ->
|
||||
it.value.map { dalUserInfo -> dalUserInfo.toUserInfo() }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetBookingStatusesOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.GetWorkOrdersOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookingStatus
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.WorkOrder
|
||||
import com.ttpsc.dynamics365fieldService.core.extensions.executeOnBackground
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsGetWorkOrdersOperation @Inject constructor(
|
||||
repository: DynamicsApiRepository
|
||||
) :
|
||||
GetWorkOrdersOperation {
|
||||
private val _repository = repository
|
||||
override var queryMap: Map<String, String>? = null
|
||||
|
||||
override fun execute(): Observable<List<WorkOrder>> =
|
||||
_repository.getWorkOrders(queryMap)
|
||||
.executeOnBackground()
|
||||
.map { it ->
|
||||
it.value.map { dalWorkOrder -> dalWorkOrder.toWorkOrder() }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import com.microsoft.identity.client.*
|
||||
import com.microsoft.identity.client.exception.MsalException
|
||||
import com.ttpsc.dynamics365fieldService.AndroidApplication
|
||||
import com.ttpsc.dynamics365fieldService.R
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.LogInOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.SaveEnvironmentDataOperation
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.AuthorizationManager
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import kotlin.math.log
|
||||
|
||||
class DynamicsLogIn @Inject constructor(
|
||||
private val authorizationManager: AuthorizationManager,
|
||||
private val clientApplication: ISingleAccountPublicClientApplication,
|
||||
private val saveEnvironmentDataOperation: SaveEnvironmentDataOperation
|
||||
) : LogInOperation {
|
||||
|
||||
private val subject: PublishSubject<Boolean> = PublishSubject.create()
|
||||
override var userName: String? = null
|
||||
override var environmentUrl: String? = null
|
||||
override var activity: Activity? = null
|
||||
|
||||
override fun execute(): Observable<Boolean> {
|
||||
if (activity == null) {
|
||||
return Observable.empty()
|
||||
}
|
||||
val login = if (userName != null) userName!! else ""
|
||||
val environment = if (environmentUrl != null) environmentUrl!! else ""
|
||||
|
||||
clientApplication.signIn(activity!!, login, arrayOf("$environment//user_impersonation"), getAuthInteractiveCallback(userName, environmentUrl))
|
||||
return subject
|
||||
}
|
||||
|
||||
private fun getAuthInteractiveCallback(email:String?, environment: String?): AuthenticationCallback {
|
||||
return object : AuthenticationCallback {
|
||||
override fun onSuccess(authenticationResult: IAuthenticationResult) {
|
||||
println("Successfully authenticated")
|
||||
saveEnvironmentDataOperation.apply {
|
||||
environmentUrl = environment
|
||||
userEmail = email
|
||||
}.execute().subscribe()
|
||||
|
||||
authorizationManager.setAuthorizationToken(authenticationResult.accessToken)
|
||||
authorizationManager.saveTokenExpirationDate(authenticationResult.expiresOn)
|
||||
subject.onNext(true)
|
||||
}
|
||||
|
||||
override fun onError(exception: MsalException) {
|
||||
println("Authentication failed: $exception")
|
||||
subject.onError(exception)
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
subject.onNext(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.ttpsc.dynamics365fieldService.bll.operations.dynamics
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.ttpsc.dynamics365fieldService.AppConfiguration
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.SaveEnvironmentDataOperation
|
||||
import com.ttpsc.dynamics365fieldService.core.managers.DynamicsAuthorizationManager
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import javax.inject.Inject
|
||||
|
||||
class DynamicsSaveEnvironmentDataOperation @Inject constructor(val sharedPreferences: SharedPreferences) :
|
||||
SaveEnvironmentDataOperation {
|
||||
override var environmentUrl: String? = null
|
||||
override var userEmail: String? = null
|
||||
override fun execute(): Observable<Unit> {
|
||||
return Observable.create { observer ->
|
||||
|
||||
if (environmentUrl == null || userEmail == null) {
|
||||
observer.onNext(Unit)
|
||||
}
|
||||
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(AppConfiguration.StorageKeys.environmentKey, environmentUrl)
|
||||
.putString(AppConfiguration.StorageKeys.userAccountKey, userEmail)
|
||||
.apply()
|
||||
|
||||
AppConfiguration.endpoint = environmentUrl!!
|
||||
observer.onNext(Unit)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.abstraction
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.UserInfo
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Interceptor
|
||||
import java.util.*
|
||||
|
||||
interface AuthorizationManager: Interceptor, Authenticator {
|
||||
fun setAuthorizationToken(authToken: String)
|
||||
fun getAuthorizationHeader(): String?
|
||||
fun signOut()
|
||||
fun setUserData(userInfo: UserInfo?)
|
||||
fun getUserEmail(): String?
|
||||
fun getUserResourceId(): String?
|
||||
val userNeedsLogin: Observable<Unit>
|
||||
fun saveTokenExpirationDate(date: Date)
|
||||
fun getUserFirstName(): String?
|
||||
fun getUserLastName(): String?
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.abstraction
|
||||
|
||||
import android.app.Activity
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
|
||||
interface LanguageManager {
|
||||
val languageChanged: Observable<String>
|
||||
fun getLanguage(): String
|
||||
fun registerForLanguageChange(activity: Activity)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.abstraction
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
|
||||
interface LifecycleProvidingActivity {
|
||||
val onCreate: Observable<Unit>
|
||||
val onStart: Observable<Unit>
|
||||
val onResume: Observable<Unit>
|
||||
val onPause: Observable<Unit>
|
||||
val onStop: Observable<Unit>
|
||||
val onDestroy: Observable<Unit>
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.di
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.AndroidApplication
|
||||
import com.ttpsc.dynamics365fieldService.core.di.api.AuthenticationModule
|
||||
import com.ttpsc.dynamics365fieldService.core.di.api.DynamicsApiModule
|
||||
import com.ttpsc.dynamics365fieldService.core.di.viewmodel.ViewModelModule
|
||||
import com.ttpsc.dynamics365fieldService.views.*
|
||||
import com.ttpsc.dynamics365fieldService.views.activities.MainActivity
|
||||
import dagger.Component
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Component(modules = [ApplicationModule::class, DynamicsApiModule::class, ViewModelModule::class, AuthenticationModule::class])
|
||||
interface ApplicationComponent {
|
||||
fun inject(application: AndroidApplication)
|
||||
fun inject(loginFragment: LoginFragment)
|
||||
fun inject(guidesListFragment: GuidesListFragment)
|
||||
fun inject(guideSummaryFragment: GuideSummaryFragment)
|
||||
|
||||
fun inject(guideDetailsFragment: GuideDetailsFragment)
|
||||
fun inject(startFragment: StartFragment)
|
||||
fun inject(mainActivity: MainActivity)
|
||||
fun inject(notesListFragment: NotesListFragment)
|
||||
fun inject(guideDetailsAttachmentsFragment: GuideDetailsAttachmentsFragment)
|
||||
fun inject(mainMenuFragment: MainMenuFragment)
|
||||
fun inject(noteDetailsFragment: NoteDetailsFragment)
|
||||
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.microsoft.identity.client.*
|
||||
import com.ttpsc.dynamics365fieldService.AndroidApplication
|
||||
import com.ttpsc.dynamics365fieldService.AppConfiguration
|
||||
import com.ttpsc.dynamics365fieldService.BuildConfig
|
||||
import com.ttpsc.dynamics365fieldService.R
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.AuthorizationManager
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.LanguageManager
|
||||
import com.ttpsc.dynamics365fieldService.core.managers.ApiLogger
|
||||
import com.ttpsc.dynamics365fieldService.core.managers.SystemLanguageManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import hu.akarnokd.rxjava3.retrofit.RxJava3CallAdapterFactory
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.*
|
||||
import okhttp3.internal.immutableListOf
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.reactivestreams.Subscriber
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
||||
@Module
|
||||
class ApplicationModule(private val application: AndroidApplication) {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApplicationContext(): Context = application
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesLanguageManager(): LanguageManager = SystemLanguageManager()
|
||||
|
||||
@Inject
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(
|
||||
@Named("BASE_ENDPOINT") baseEndpoint: String,
|
||||
authorizationManager: AuthorizationManager
|
||||
): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseEndpoint)
|
||||
.client(createClient(authorizationManager))
|
||||
.addConverterFactory(ScalarsConverterFactory.create())
|
||||
.addConverterFactory(GsonConverterFactory.create(createGsonConverter()))
|
||||
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePublicClientApplication(context: Context): ISingleAccountPublicClientApplication {
|
||||
return HackySingleAccountPublicClientApplication(
|
||||
PublicClientApplicationConfigurationFactory.initializeConfiguration(
|
||||
context.applicationContext,
|
||||
R.raw.auth_config_maksym
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class HackySingleAccountPublicClientApplication(c: PublicClientApplicationConfiguration) :
|
||||
SingleAccountPublicClientApplication(c)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSharedPreferences(): SharedPreferences {
|
||||
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
|
||||
return EncryptedSharedPreferences
|
||||
.create(
|
||||
"AppPreferences",
|
||||
masterKeyAlias,
|
||||
application,
|
||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||
)
|
||||
}
|
||||
|
||||
class EndpointInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val desiredEndpoint = AppConfiguration.endpoint + AppConfiguration.ENDPOINT_SUFFIX
|
||||
val tag = chain.request().tag(String::class.java)
|
||||
|
||||
if (tag.isNullOrEmpty() || tag != AppConfiguration.RequestTags.leaveUrl) {
|
||||
val currentUrl = chain.request().url.toString()
|
||||
if (currentUrl.startsWith(desiredEndpoint))
|
||||
return chain.proceed(chain.request())
|
||||
|
||||
val urlPath = currentUrl.substring(
|
||||
currentUrl.indexOf(AppConfiguration.ENDPOINT_SUFFIX)
|
||||
+ AppConfiguration.ENDPOINT_SUFFIX.length
|
||||
)
|
||||
val newRequest = chain.request().newBuilder()
|
||||
.url(desiredEndpoint + urlPath)
|
||||
.build()
|
||||
return chain.proceed(newRequest)
|
||||
} else {
|
||||
return chain.proceed(chain.request())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createClient(authorizationManager: AuthorizationManager): OkHttpClient {
|
||||
val okHttpClientBuilder = OkHttpClient.Builder()
|
||||
if (BuildConfig.DEBUG) {
|
||||
val loggingInterceptor =
|
||||
HttpLoggingInterceptor(ApiLogger()).setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
okHttpClientBuilder.addInterceptor(loggingInterceptor)
|
||||
}
|
||||
|
||||
okHttpClientBuilder.addInterceptor(EndpointInterceptor())
|
||||
okHttpClientBuilder.addInterceptor(authorizationManager)
|
||||
okHttpClientBuilder.authenticator(authorizationManager)
|
||||
return okHttpClientBuilder.build()
|
||||
}
|
||||
|
||||
private fun createGsonConverter() =
|
||||
GsonBuilder()
|
||||
.create()
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.di.api
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.microsoft.identity.client.ISingleAccountPublicClientApplication
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.LogInOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.SaveEnvironmentDataOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.UpdateUserSessionOperation
|
||||
import com.ttpsc.dynamics365fieldService.bll.operations.dynamics.DynamicsLogIn
|
||||
import com.ttpsc.dynamics365fieldService.bll.operations.dynamics.DynamicsSaveEnvironmentDataOperation
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.AuthorizationManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
@Module
|
||||
class AuthenticationModule {
|
||||
|
||||
@Provides
|
||||
fun provideLogInOperation(
|
||||
authorizationManager: AuthorizationManager,
|
||||
clientApplication: ISingleAccountPublicClientApplication,
|
||||
saveEnvironmentOperation: SaveEnvironmentDataOperation
|
||||
): LogInOperation = DynamicsLogIn(authorizationManager, clientApplication, saveEnvironmentOperation)
|
||||
|
||||
@Provides
|
||||
fun provideSaveEnvironmentOperation(sharedPreferences: SharedPreferences): SaveEnvironmentDataOperation =
|
||||
DynamicsSaveEnvironmentDataOperation(sharedPreferences)
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.di.api
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.microsoft.identity.client.ISingleAccountPublicClientApplication
|
||||
import com.ttpsc.dynamics365fieldService.AppConfiguration
|
||||
import com.ttpsc.dynamics365fieldService.bll.abstraction.operations.*
|
||||
import com.ttpsc.dynamics365fieldService.bll.operations.dynamics.*
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.AuthorizationManager
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.LanguageManager
|
||||
import com.ttpsc.dynamics365fieldService.core.managers.DynamicsAuthorizationManager
|
||||
import com.ttpsc.dynamics365fieldService.dal.repository.DynamicsApiRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import retrofit2.Retrofit
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class DynamicsApiModule {
|
||||
@Provides
|
||||
@Named("BASE_ENDPOINT")
|
||||
fun getBaseEndpoint(): String {
|
||||
return AppConfiguration.endpoint + AppConfiguration.ENDPOINT_SUFFIX
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthorizationManager(
|
||||
sharedPreferences: SharedPreferences,
|
||||
publicClientApplication: ISingleAccountPublicClientApplication
|
||||
): AuthorizationManager =
|
||||
DynamicsAuthorizationManager(sharedPreferences, publicClientApplication)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApiService(retrofit: Retrofit) =
|
||||
DynamicsApiRepository(retrofit)
|
||||
|
||||
@Provides
|
||||
fun providesGetUserInfoOperation(
|
||||
repository: DynamicsApiRepository
|
||||
): GetUserInfoOperation = DynamicsGetUserInfo(repository)
|
||||
|
||||
@Provides
|
||||
fun provideGetProcedureDetailsOperation(
|
||||
repository: DynamicsApiRepository
|
||||
): GetBookableResourceBookingDetailsOperation =
|
||||
DynamicsGetBookableResourceBookingDetails(repository)
|
||||
|
||||
@Provides
|
||||
fun provideGetProceduresOperation(
|
||||
repository: DynamicsApiRepository,
|
||||
languageManager: LanguageManager
|
||||
): GetBookableResourcesBookingsOperation =
|
||||
DynamicsGetBookableResourceBookingsOperation(repository)
|
||||
|
||||
@Provides
|
||||
fun provideGetBookableResourceBookingsOperationCount(
|
||||
repository: DynamicsApiRepository
|
||||
): GetBookableResourceBookingsCountOperation =
|
||||
DynamicsGetBookableResourceBookingsCount(repository)
|
||||
|
||||
@Provides
|
||||
fun provideGetAlertsOperation(
|
||||
repository: DynamicsApiRepository
|
||||
): GetAlertsOperation = DynamicsGetAlerts(repository)
|
||||
|
||||
@Provides
|
||||
fun providesGetBookingStatusesOperation(
|
||||
repository: DynamicsApiRepository
|
||||
): GetBookingStatusesOperation = DynamicsGetBookingStatusesOperation(repository)
|
||||
|
||||
@Provides
|
||||
fun provideWorkOrdersOperation(repository: DynamicsApiRepository): GetWorkOrdersOperation =
|
||||
DynamicsGetWorkOrdersOperation(repository)
|
||||
|
||||
@Provides
|
||||
fun provideGetBookableResourcesOperation(
|
||||
repository: DynamicsApiRepository,
|
||||
authorizationManager: AuthorizationManager
|
||||
): GetBookableResourcesOperation =
|
||||
DynamicsGetBookableResource(repository, authorizationManager)
|
||||
|
||||
@Provides
|
||||
fun providesChangeBookableResourceBookingStatus(
|
||||
repository: DynamicsApiRepository
|
||||
): ChangeBookableResourceBookingStatusOperation =
|
||||
DynamicsChangeBookableResourceBookingStatus(repository)
|
||||
|
||||
@Provides
|
||||
fun provideGetNotesOperation(
|
||||
repository: DynamicsApiRepository
|
||||
): GetNotesOperation = DynamicsGetNotes(repository)
|
||||
|
||||
@Provides
|
||||
fun provideCreateNoteOperation(
|
||||
repository: DynamicsApiRepository
|
||||
): CreateNoteOperation = DynamicsCreateNoteOperation(repository)
|
||||
|
||||
@Provides
|
||||
fun providesDownloadFileOperation(repository: DynamicsApiRepository): DownloadFileOperation =
|
||||
DynamicsDownloadFile(repository)
|
||||
|
||||
@Provides
|
||||
fun providesGetFormOperation(repository: DynamicsApiRepository): GetFormOperation =
|
||||
DynamicsGetForm(repository)
|
||||
|
||||
@Provides
|
||||
fun providesGetRawWorkOrdersOperation(repository: DynamicsApiRepository): GetRawWorkOrdersOperation =
|
||||
DynamicsGetRawWorkOrder(repository)
|
||||
|
||||
@Provides
|
||||
fun provideGetCategoriesOperation(repository: DynamicsApiRepository): GetCategoriesOperation {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetGuideDetailsOperation(repository: DynamicsApiRepository): GetProcedureOperation {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
fun providesCreateProcedureOperation(repository: DynamicsApiRepository): CreateProcedureOperation {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun providesStepProcedureOperation(repository: DynamicsApiRepository): CreateStepOperation {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.di.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
class ViewModelFactory
|
||||
@Inject constructor(
|
||||
private val creators: Map<Class<out ViewModel>,
|
||||
@JvmSuppressWildcards Provider<ViewModel>>
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
val creator = creators[modelClass] ?: creators.asIterable()
|
||||
.firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
|
||||
?: throw IllegalArgumentException("Unknown ViewModel class $modelClass")
|
||||
|
||||
return try {
|
||||
creator.get() as T
|
||||
} catch (e: Exception) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.di.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.MapKey
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
@MapKey
|
||||
@Target(
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER
|
||||
)
|
||||
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
|
|
@ -0,0 +1,65 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.di.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.ttpsc.dynamics365fieldService.viewmodels.*
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Suppress("unused")
|
||||
@Module
|
||||
abstract class ViewModelModule {
|
||||
@Binds
|
||||
internal abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(LoginViewModel::class)
|
||||
abstract fun bindsLoginViewModel(viewModel: LoginViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(StartViewModel::class)
|
||||
abstract fun bindsStartViewModel(viewModel: StartViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ProcedureDetailsViewModel::class)
|
||||
abstract fun bindsProcedureDetailsViewModel(viewModel: ProcedureDetailsViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ProcedureStepViewModel::class)
|
||||
abstract fun bindsProcedureStepViewModel(viewModel: ProcedureStepViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ProcedureStepsViewModel::class)
|
||||
abstract fun bindsProcedureStepsViewModel(viewModel: ProcedureStepsViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(GalleryViewModel::class)
|
||||
abstract fun bindsGalleryViewModel(viewModel: GalleryViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(SettingsViewModel::class)
|
||||
abstract fun bindsSettingsViewModel(viewModel: SettingsViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ProcedureListViewModel::class)
|
||||
abstract fun bindsProcedureListViewModel(viewModel: ProcedureListViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(MainViewModel::class)
|
||||
abstract fun bindsMainViewModel(viewModel: MainViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(NoteDetailsViewModel::class)
|
||||
abstract fun bindsNoteDetailsViewModel(viewModel: NoteDetailsViewModel): ViewModel
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.extensions
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.annotations.NonNull
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
|
||||
fun <T> Observable<@NonNull T>.executeOnBackground(): @NonNull Observable<@NonNull T> {
|
||||
return this.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.unsubscribeOn(Schedulers.io())
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.extensions
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import java.lang.reflect.Field
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private const val BASE64_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private val RX_BASE64_CLEANR = "[^=" + BASE64_SET + "]".toRegex()
|
||||
|
||||
@Suppress("unused", "unused")
|
||||
val String.base64encoded: String
|
||||
get() {
|
||||
val pad = when (this.length % 3) {
|
||||
1 -> "=="
|
||||
2 -> "="
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val raw = this + 0.toChar().toString().repeat(minOf(0, pad.lastIndex))
|
||||
|
||||
return raw.chunkedSequence(3) {
|
||||
Triple(
|
||||
it[0].toInt(),
|
||||
it[1].toInt(),
|
||||
it[2].toInt()
|
||||
)
|
||||
}.map { (first, second, third) ->
|
||||
(0xFF.and(first) shl 16) +
|
||||
(0xFF.and(second) shl 8) +
|
||||
0xFF.and(third)
|
||||
}.map { n ->
|
||||
sequenceOf(
|
||||
(n shr 18) and 0x3F,
|
||||
(n shr 12) and 0x3F,
|
||||
(n shr 6) and 0x3F,
|
||||
n and 0x3F
|
||||
)
|
||||
}.flatten()
|
||||
.map { BASE64_SET[it] }
|
||||
.joinToString("")
|
||||
.dropLast(pad.length) + pad
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
val String.base64decoded: String
|
||||
get() {
|
||||
require(this.length % 4 != 0) { "The string \"$this\" does not comply with BASE64 length requirement." }
|
||||
val clean = this.replace(RX_BASE64_CLEANR, "").replace("=", "A")
|
||||
val padLen: Int = this.count { it == '=' }
|
||||
|
||||
return clean.chunkedSequence(4) {
|
||||
(BASE64_SET.indexOf(clean[0]) shl 18) +
|
||||
(BASE64_SET.indexOf(clean[1]) shl 12) +
|
||||
(BASE64_SET.indexOf(clean[2]) shl 6) +
|
||||
BASE64_SET.indexOf(clean[3])
|
||||
}.map { n ->
|
||||
sequenceOf(
|
||||
0xFF.and(n shr 16),
|
||||
0xFF.and(n shr 8),
|
||||
0xFF.and(n)
|
||||
)
|
||||
}.flatten()
|
||||
.map { it.toChar() }
|
||||
.joinToString("")
|
||||
.dropLast(padLen)
|
||||
}
|
||||
|
||||
fun Enum<*>.getSerializedName(): String? {
|
||||
return try {
|
||||
val f: Field = this.javaClass.getField(this.name)
|
||||
val a: SerializedName = f.getAnnotation(SerializedName::class.java)
|
||||
a.value
|
||||
} catch (ignored: NoSuchFieldException) {
|
||||
null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.gson.JsonSyntaxException
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
|
||||
class ApiLogger : HttpLoggingInterceptor.Logger {
|
||||
override fun log(message: String) {
|
||||
val logName = "ApiLogger"
|
||||
if (message.startsWith("{") || message.startsWith("[")) {
|
||||
try {
|
||||
val prettyPrintJson = GsonBuilder().setPrettyPrinting()
|
||||
.create().toJson(JsonParser().parse(message))
|
||||
Log.d(logName, prettyPrintJson)
|
||||
} catch (m: JsonSyntaxException) {
|
||||
Log.e(logName, message, m)
|
||||
}
|
||||
} else {
|
||||
Log.d(logName, message)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.CookieSyncManager
|
||||
import com.google.gson.Gson
|
||||
import com.microsoft.identity.client.ISingleAccountPublicClientApplication
|
||||
import com.ttpsc.dynamics365fieldService.AppConfiguration
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.UserInfo
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.AuthorizationManager
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import java.io.IOException
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.chrono.ChronoLocalDateTime
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
class DynamicsAuthorizationManager @Inject constructor(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
private val clientApplication: ISingleAccountPublicClientApplication
|
||||
) :
|
||||
AuthorizationManager {
|
||||
|
||||
private var _token: String? = null
|
||||
private var _expirationDate: Date? = null
|
||||
private var _userResourceId: String? = null
|
||||
private var _userFirstName: String? = null
|
||||
private var _userLastName: String? = null
|
||||
private var _environment: String? = null
|
||||
private var userNeedsLoginSubject: PublishSubject<Unit> = PublishSubject.create()
|
||||
|
||||
override var userNeedsLogin: Observable<Unit> =
|
||||
userNeedsLoginSubject.throttleFirst(30, TimeUnit.SECONDS)
|
||||
|
||||
init {
|
||||
_token = sharedPreferences.getString(AppConfiguration.StorageKeys.accessTokenKey, null)
|
||||
_userResourceId =
|
||||
sharedPreferences.getString(AppConfiguration.StorageKeys.userResourceIdKey, null)
|
||||
_userFirstName =
|
||||
sharedPreferences.getString(AppConfiguration.StorageKeys.userFirstNameKey, null)
|
||||
_userLastName =
|
||||
sharedPreferences.getString(AppConfiguration.StorageKeys.userLastNameKey, null)
|
||||
|
||||
val serializedDate = sharedPreferences.getString(
|
||||
AppConfiguration.StorageKeys.accessTokenExpirationDateKey,
|
||||
null
|
||||
)
|
||||
if (serializedDate != null) {
|
||||
_expirationDate = Gson().fromJson(serializedDate, Date::class.java)
|
||||
}
|
||||
|
||||
_environment = sharedPreferences.getString(
|
||||
AppConfiguration.StorageKeys.environmentKey,
|
||||
null
|
||||
)
|
||||
|
||||
if (_token == null || _environment == null) {
|
||||
try {
|
||||
signOut()
|
||||
} catch (ex: Exception) {
|
||||
Log.w("AUTOMATIC SIGN OUT EXCEPTION", ex.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAuthorizationToken(authToken: String) {
|
||||
_token = authToken
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(AppConfiguration.StorageKeys.accessTokenKey, _token)
|
||||
.apply()
|
||||
}
|
||||
|
||||
override fun getAuthorizationHeader(): String? {
|
||||
if (_token == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return "Bearer $_token"
|
||||
}
|
||||
|
||||
override fun saveTokenExpirationDate(date: Date) {
|
||||
_expirationDate = date
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(
|
||||
AppConfiguration.StorageKeys.accessTokenExpirationDateKey,
|
||||
Gson().toJson(date)
|
||||
)
|
||||
.apply()
|
||||
}
|
||||
|
||||
override fun signOut() {
|
||||
_token = null
|
||||
_userResourceId = null
|
||||
AppConfiguration.endpoint = ""
|
||||
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.remove(AppConfiguration.StorageKeys.accessTokenKey)
|
||||
.remove(AppConfiguration.StorageKeys.environmentKey)
|
||||
.remove(AppConfiguration.StorageKeys.userResourceIdKey)
|
||||
.remove(AppConfiguration.StorageKeys.userFirstNameKey)
|
||||
.remove(AppConfiguration.StorageKeys.userLastNameKey)
|
||||
.apply()
|
||||
GlobalScope.async {
|
||||
clientApplication.signOut()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setUserData(userInfo: UserInfo?) {
|
||||
if (userInfo != null) {
|
||||
_userResourceId = userInfo.resourceId
|
||||
_userFirstName = userInfo.firstName
|
||||
_userLastName = userInfo.lastName
|
||||
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(AppConfiguration.StorageKeys.userResourceIdKey, userInfo.resourceId)
|
||||
.apply()
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(AppConfiguration.StorageKeys.userFirstNameKey, userInfo.firstName)
|
||||
.apply()
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString(AppConfiguration.StorageKeys.userLastNameKey, userInfo.lastName)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getUserEmail(): String? =
|
||||
sharedPreferences.getString(AppConfiguration.StorageKeys.userAccountKey, null)
|
||||
|
||||
override fun getUserFirstName(): String? =
|
||||
sharedPreferences.getString(AppConfiguration.StorageKeys.userFirstNameKey, null)
|
||||
|
||||
override fun getUserLastName(): String? =
|
||||
sharedPreferences.getString(AppConfiguration.StorageKeys.userLastNameKey, null)
|
||||
|
||||
override fun getUserResourceId(): String? = _userResourceId
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
var request = chain.request()
|
||||
request = request.newBuilder()
|
||||
.build()
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
var requestAvailable: Request? = null
|
||||
try {
|
||||
if (_environment.isNullOrEmpty()) {
|
||||
_environment =
|
||||
sharedPreferences.getString(AppConfiguration.StorageKeys.environmentKey, null)
|
||||
}
|
||||
|
||||
val now = LocalDateTime.now()
|
||||
val expirationDate =
|
||||
_expirationDate?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDateTime()
|
||||
|
||||
if (_token == null || (_expirationDate != null && now.isAfter(expirationDate))) {
|
||||
println("TRYING TO GET ANOTHER TOKEN")
|
||||
|
||||
val authority =
|
||||
clientApplication.getConfiguration().getDefaultAuthority().getAuthorityURL()
|
||||
.toString()
|
||||
val result = clientApplication.acquireTokenSilent(
|
||||
arrayOf("$_environment//user_impersonation"),
|
||||
authority
|
||||
)
|
||||
saveTokenExpirationDate(result.expiresOn)
|
||||
if (result.accessToken != _token) {
|
||||
setAuthorizationToken(result.accessToken)
|
||||
}
|
||||
}
|
||||
|
||||
println("Access token: $_token")
|
||||
requestAvailable = response.request.newBuilder().addHeader(
|
||||
"Authorization",
|
||||
getAuthorizationHeader()!!
|
||||
).build()
|
||||
return requestAvailable
|
||||
} catch (ex: Exception) {
|
||||
signOut()
|
||||
userNeedsLoginSubject.onNext(Unit)
|
||||
}
|
||||
return requestAvailable
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.LanguageManager
|
||||
import com.ttpsc.dynamics365fieldService.core.abstraction.LifecycleProvidingActivity
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import java.util.*
|
||||
|
||||
class SystemLanguageManager : LanguageManager {
|
||||
|
||||
private val _languageChangeSubject: BehaviorSubject<String> = BehaviorSubject.create()
|
||||
private var lifecycleActivity: LifecycleProvidingActivity? = null
|
||||
private var _languageReceiverRegistered = false
|
||||
override val languageChanged: Observable<String>
|
||||
get() = _languageChangeSubject
|
||||
|
||||
override fun getLanguage(): String =
|
||||
Locale.getDefault().language.toLowerCase(Locale.ROOT)
|
||||
|
||||
override fun registerForLanguageChange(activity: Activity) {
|
||||
lifecycleActivity = activity as LifecycleProvidingActivity
|
||||
val filter = IntentFilter(Intent.ACTION_LOCALE_CHANGED)
|
||||
|
||||
val languageReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
_languageChangeSubject.onNext(getLanguage())
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleActivity?.onStop?.subscribe {
|
||||
unregisterReceiver(activity, languageReceiver)
|
||||
}
|
||||
|
||||
lifecycleActivity?.onDestroy?.subscribe {
|
||||
unregisterReceiver(activity, languageReceiver)
|
||||
}
|
||||
|
||||
lifecycleActivity?.onResume?.subscribe {
|
||||
registerReceiver(activity, languageReceiver, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerReceiver(
|
||||
activity: Activity,
|
||||
languageReceiver: BroadcastReceiver,
|
||||
filter: IntentFilter
|
||||
) {
|
||||
if (_languageReceiverRegistered == false) {
|
||||
activity.registerReceiver(languageReceiver, filter)
|
||||
_languageReceiverRegistered = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun unregisterReceiver(
|
||||
activity: Activity,
|
||||
languageReceiver: BroadcastReceiver
|
||||
) {
|
||||
if (_languageReceiverRegistered) {
|
||||
activity.unregisterReceiver(languageReceiver)
|
||||
_languageReceiverRegistered = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers.VideoCompressor;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.opengl.EGL14;
|
||||
import android.opengl.EGLConfig;
|
||||
import android.opengl.EGLContext;
|
||||
import android.opengl.EGLDisplay;
|
||||
import android.opengl.EGLExt;
|
||||
import android.opengl.EGLSurface;
|
||||
import android.os.Build;
|
||||
import android.view.Surface;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||
public class InputSurface {
|
||||
private static final boolean VERBOSE = false;
|
||||
private static final int EGL_RECORDABLE_ANDROID = 0x3142;
|
||||
private static final int EGL_OPENGL_ES2_BIT = 4;
|
||||
private EGLDisplay mEGLDisplay;
|
||||
private EGLContext mEGLContext;
|
||||
private EGLSurface mEGLSurface;
|
||||
private Surface mSurface;
|
||||
|
||||
public InputSurface(Surface surface) {
|
||||
if (surface == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
mSurface = surface;
|
||||
eglSetup();
|
||||
}
|
||||
|
||||
private void eglSetup() {
|
||||
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
|
||||
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
|
||||
throw new RuntimeException("unable to get EGL14 display");
|
||||
}
|
||||
int[] version = new int[2];
|
||||
if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
|
||||
mEGLDisplay = null;
|
||||
throw new RuntimeException("unable to initialize EGL14");
|
||||
}
|
||||
|
||||
int[] attribList = {
|
||||
EGL14.EGL_RED_SIZE, 8,
|
||||
EGL14.EGL_GREEN_SIZE, 8,
|
||||
EGL14.EGL_BLUE_SIZE, 8,
|
||||
EGL14.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL_RECORDABLE_ANDROID, 1,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
EGLConfig[] configs = new EGLConfig[1];
|
||||
int[] numConfigs = new int[1];
|
||||
if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length,
|
||||
numConfigs, 0)) {
|
||||
throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config");
|
||||
}
|
||||
|
||||
int[] attrib_list = {
|
||||
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
|
||||
mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, attrib_list, 0);
|
||||
checkEglError("eglCreateContext");
|
||||
if (mEGLContext == null) {
|
||||
throw new RuntimeException("null context");
|
||||
}
|
||||
|
||||
int[] surfaceAttribs = {
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
mEGLSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, configs[0], mSurface,
|
||||
surfaceAttribs, 0);
|
||||
checkEglError("eglCreateWindowSurface");
|
||||
if (mEGLSurface == null) {
|
||||
throw new RuntimeException("surface was null");
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (EGL14.eglGetCurrentContext().equals(mEGLContext)) {
|
||||
EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
|
||||
}
|
||||
EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface);
|
||||
EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
|
||||
mSurface.release();
|
||||
mEGLDisplay = null;
|
||||
mEGLContext = null;
|
||||
mEGLSurface = null;
|
||||
mSurface = null;
|
||||
}
|
||||
|
||||
public void makeCurrent() {
|
||||
if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
|
||||
throw new RuntimeException("eglMakeCurrent failed");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean swapBuffers() {
|
||||
return EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface);
|
||||
}
|
||||
|
||||
public Surface getSurface() {
|
||||
return mSurface;
|
||||
}
|
||||
|
||||
public void setPresentationTime(long nsecs) {
|
||||
EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs);
|
||||
}
|
||||
|
||||
private void checkEglError(String msg) {
|
||||
boolean failed = false;
|
||||
int error;
|
||||
while ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) {
|
||||
failed = true;
|
||||
}
|
||||
if (failed) {
|
||||
throw new RuntimeException("EGL error encountered (see log)");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,436 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers.VideoCompressor;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
|
||||
import com.coremedia.iso.BoxParser;
|
||||
import com.coremedia.iso.IsoFile;
|
||||
import com.coremedia.iso.IsoTypeWriter;
|
||||
import com.coremedia.iso.boxes.Box;
|
||||
import com.coremedia.iso.boxes.Container;
|
||||
import com.coremedia.iso.boxes.DataEntryUrlBox;
|
||||
import com.coremedia.iso.boxes.DataInformationBox;
|
||||
import com.coremedia.iso.boxes.DataReferenceBox;
|
||||
import com.coremedia.iso.boxes.FileTypeBox;
|
||||
import com.coremedia.iso.boxes.HandlerBox;
|
||||
import com.coremedia.iso.boxes.MediaBox;
|
||||
import com.coremedia.iso.boxes.MediaHeaderBox;
|
||||
import com.coremedia.iso.boxes.MediaInformationBox;
|
||||
import com.coremedia.iso.boxes.MovieBox;
|
||||
import com.coremedia.iso.boxes.MovieHeaderBox;
|
||||
import com.coremedia.iso.boxes.SampleSizeBox;
|
||||
import com.coremedia.iso.boxes.SampleTableBox;
|
||||
import com.coremedia.iso.boxes.SampleToChunkBox;
|
||||
import com.coremedia.iso.boxes.StaticChunkOffsetBox;
|
||||
import com.coremedia.iso.boxes.SyncSampleBox;
|
||||
import com.coremedia.iso.boxes.TimeToSampleBox;
|
||||
import com.coremedia.iso.boxes.TrackBox;
|
||||
import com.coremedia.iso.boxes.TrackHeaderBox;
|
||||
import com.googlecode.mp4parser.DataSource;
|
||||
import com.googlecode.mp4parser.util.Matrix;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@TargetApi(16)
|
||||
public class MP4Builder {
|
||||
private InterleaveChunkMdat mdat = null;
|
||||
private Mp4Movie currentMp4Movie = null;
|
||||
private FileOutputStream fos = null;
|
||||
private FileChannel fc = null;
|
||||
private long dataOffset = 0;
|
||||
private long writedSinceLastMdat = 0;
|
||||
private boolean writeNewMdat = true;
|
||||
private HashMap<Track, long[]> track2SampleSizes = new HashMap<>();
|
||||
private ByteBuffer sizeBuffer = null;
|
||||
|
||||
public MP4Builder createMovie(Mp4Movie mp4Movie) throws Exception {
|
||||
currentMp4Movie = mp4Movie;
|
||||
|
||||
fos = new FileOutputStream(mp4Movie.getCacheFile());
|
||||
fc = fos.getChannel();
|
||||
|
||||
FileTypeBox fileTypeBox = createFileTypeBox();
|
||||
fileTypeBox.getBox(fc);
|
||||
dataOffset += fileTypeBox.getSize();
|
||||
writedSinceLastMdat += dataOffset;
|
||||
|
||||
mdat = new InterleaveChunkMdat();
|
||||
|
||||
sizeBuffer = ByteBuffer.allocateDirect(4);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void flushCurrentMdat() throws Exception {
|
||||
long oldPosition = fc.position();
|
||||
fc.position(mdat.getOffset());
|
||||
mdat.getBox(fc);
|
||||
fc.position(oldPosition);
|
||||
mdat.setDataOffset(0);
|
||||
mdat.setContentSize(0);
|
||||
fos.flush();
|
||||
}
|
||||
|
||||
public boolean writeSampleData(int trackIndex, ByteBuffer byteBuf, MediaCodec.BufferInfo bufferInfo, boolean isAudio) throws Exception {
|
||||
if (writeNewMdat) {
|
||||
mdat.setContentSize(0);
|
||||
mdat.getBox(fc);
|
||||
mdat.setDataOffset(dataOffset);
|
||||
dataOffset += 16;
|
||||
writedSinceLastMdat += 16;
|
||||
writeNewMdat = false;
|
||||
}
|
||||
|
||||
mdat.setContentSize(mdat.getContentSize() + bufferInfo.size);
|
||||
writedSinceLastMdat += bufferInfo.size;
|
||||
|
||||
boolean flush = false;
|
||||
if (writedSinceLastMdat >= 32 * 1024) {
|
||||
flushCurrentMdat();
|
||||
writeNewMdat = true;
|
||||
flush = true;
|
||||
writedSinceLastMdat -= 32 * 1024;
|
||||
}
|
||||
|
||||
currentMp4Movie.addSample(trackIndex, dataOffset, bufferInfo);
|
||||
byteBuf.position(bufferInfo.offset + (isAudio ? 0 : 4));
|
||||
byteBuf.limit(bufferInfo.offset + bufferInfo.size);
|
||||
|
||||
if (!isAudio) {
|
||||
sizeBuffer.position(0);
|
||||
sizeBuffer.putInt(bufferInfo.size - 4);
|
||||
sizeBuffer.position(0);
|
||||
fc.write(sizeBuffer);
|
||||
}
|
||||
|
||||
fc.write(byteBuf);
|
||||
dataOffset += bufferInfo.size;
|
||||
|
||||
if (flush) {
|
||||
fos.flush();
|
||||
}
|
||||
return flush;
|
||||
}
|
||||
|
||||
public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception {
|
||||
return currentMp4Movie.addTrack(mediaFormat, isAudio);
|
||||
}
|
||||
|
||||
public void finishMovie(boolean error) throws Exception {
|
||||
if (mdat.getContentSize() != 0) {
|
||||
flushCurrentMdat();
|
||||
}
|
||||
|
||||
for (Track track : currentMp4Movie.getTracks()) {
|
||||
List<Sample> samples = track.getSamples();
|
||||
long[] sizes = new long[samples.size()];
|
||||
for (int i = 0; i < sizes.length; i++) {
|
||||
sizes[i] = samples.get(i).getSize();
|
||||
}
|
||||
track2SampleSizes.put(track, sizes);
|
||||
}
|
||||
|
||||
Box moov = createMovieBox(currentMp4Movie);
|
||||
moov.getBox(fc);
|
||||
fos.flush();
|
||||
|
||||
fc.close();
|
||||
fos.close();
|
||||
}
|
||||
|
||||
protected FileTypeBox createFileTypeBox() {
|
||||
LinkedList<String> minorBrands = new LinkedList<>();
|
||||
minorBrands.add("isom");
|
||||
minorBrands.add("3gp4");
|
||||
return new FileTypeBox("isom", 0, minorBrands);
|
||||
}
|
||||
|
||||
private class InterleaveChunkMdat implements Box {
|
||||
private Container parent;
|
||||
private long contentSize = 1024 * 1024 * 1024;
|
||||
private long dataOffset = 0;
|
||||
|
||||
public Container getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return dataOffset;
|
||||
}
|
||||
|
||||
public void setDataOffset(long offset) {
|
||||
dataOffset = offset;
|
||||
}
|
||||
|
||||
public void setParent(Container parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public void setContentSize(long contentSize) {
|
||||
this.contentSize = contentSize;
|
||||
}
|
||||
|
||||
public long getContentSize() {
|
||||
return contentSize;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return "mdat";
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return 16 + contentSize;
|
||||
}
|
||||
|
||||
private boolean isSmallBox(long contentSize) {
|
||||
return (contentSize + 8) < 4294967296L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parse(DataSource dataSource, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException {
|
||||
|
||||
}
|
||||
|
||||
public void getBox(WritableByteChannel writableByteChannel) throws IOException {
|
||||
ByteBuffer bb = ByteBuffer.allocate(16);
|
||||
long size = getSize();
|
||||
if (isSmallBox(size)) {
|
||||
IsoTypeWriter.writeUInt32(bb, size);
|
||||
} else {
|
||||
IsoTypeWriter.writeUInt32(bb, 1);
|
||||
}
|
||||
bb.put(IsoFile.fourCCtoBytes("mdat"));
|
||||
if (isSmallBox(size)) {
|
||||
bb.put(new byte[8]);
|
||||
} else {
|
||||
IsoTypeWriter.writeUInt64(bb, size);
|
||||
}
|
||||
bb.rewind();
|
||||
writableByteChannel.write(bb);
|
||||
}
|
||||
}
|
||||
|
||||
public static long gcd(long a, long b) {
|
||||
if (b == 0) {
|
||||
return a;
|
||||
}
|
||||
return gcd(b, a % b);
|
||||
}
|
||||
|
||||
public long getTimescale(Mp4Movie mp4Movie) {
|
||||
long timescale = 0;
|
||||
if (!mp4Movie.getTracks().isEmpty()) {
|
||||
timescale = mp4Movie.getTracks().iterator().next().getTimeScale();
|
||||
}
|
||||
for (Track track : mp4Movie.getTracks()) {
|
||||
timescale = gcd(track.getTimeScale(), timescale);
|
||||
}
|
||||
return timescale;
|
||||
}
|
||||
|
||||
protected MovieBox createMovieBox(Mp4Movie movie) {
|
||||
MovieBox movieBox = new MovieBox();
|
||||
MovieHeaderBox mvhd = new MovieHeaderBox();
|
||||
|
||||
mvhd.setCreationTime(new Date());
|
||||
mvhd.setModificationTime(new Date());
|
||||
mvhd.setMatrix(Matrix.ROTATE_0);
|
||||
long movieTimeScale = getTimescale(movie);
|
||||
long duration = 0;
|
||||
|
||||
for (Track track : movie.getTracks()) {
|
||||
long tracksDuration = track.getDuration() * movieTimeScale / track.getTimeScale();
|
||||
if (tracksDuration > duration) {
|
||||
duration = tracksDuration;
|
||||
}
|
||||
}
|
||||
|
||||
mvhd.setDuration(duration);
|
||||
mvhd.setTimescale(movieTimeScale);
|
||||
mvhd.setNextTrackId(movie.getTracks().size() + 1);
|
||||
|
||||
movieBox.addBox(mvhd);
|
||||
for (Track track : movie.getTracks()) {
|
||||
movieBox.addBox(createTrackBox(track, movie));
|
||||
}
|
||||
return movieBox;
|
||||
}
|
||||
|
||||
protected TrackBox createTrackBox(Track track, Mp4Movie movie) {
|
||||
TrackBox trackBox = new TrackBox();
|
||||
TrackHeaderBox tkhd = new TrackHeaderBox();
|
||||
|
||||
tkhd.setEnabled(true);
|
||||
tkhd.setInMovie(true);
|
||||
tkhd.setInPreview(true);
|
||||
if (track.isAudio()) {
|
||||
tkhd.setMatrix(Matrix.ROTATE_0);
|
||||
} else {
|
||||
tkhd.setMatrix(movie.getMatrix());
|
||||
}
|
||||
tkhd.setAlternateGroup(0);
|
||||
tkhd.setCreationTime(track.getCreationTime());
|
||||
tkhd.setDuration(track.getDuration() * getTimescale(movie) / track.getTimeScale());
|
||||
tkhd.setHeight(track.getHeight());
|
||||
tkhd.setWidth(track.getWidth());
|
||||
tkhd.setLayer(0);
|
||||
tkhd.setModificationTime(new Date());
|
||||
tkhd.setTrackId(track.getTrackId() + 1);
|
||||
tkhd.setVolume(track.getVolume());
|
||||
|
||||
trackBox.addBox(tkhd);
|
||||
|
||||
MediaBox mdia = new MediaBox();
|
||||
trackBox.addBox(mdia);
|
||||
MediaHeaderBox mdhd = new MediaHeaderBox();
|
||||
mdhd.setCreationTime(track.getCreationTime());
|
||||
mdhd.setDuration(track.getDuration());
|
||||
mdhd.setTimescale(track.getTimeScale());
|
||||
mdhd.setLanguage("eng");
|
||||
mdia.addBox(mdhd);
|
||||
HandlerBox hdlr = new HandlerBox();
|
||||
hdlr.setName(track.isAudio() ? "SoundHandle" : "VideoHandle");
|
||||
hdlr.setHandlerType(track.getHandler());
|
||||
|
||||
mdia.addBox(hdlr);
|
||||
|
||||
MediaInformationBox minf = new MediaInformationBox();
|
||||
minf.addBox(track.getMediaHeaderBox());
|
||||
|
||||
DataInformationBox dinf = new DataInformationBox();
|
||||
DataReferenceBox dref = new DataReferenceBox();
|
||||
dinf.addBox(dref);
|
||||
DataEntryUrlBox url = new DataEntryUrlBox();
|
||||
url.setFlags(1);
|
||||
dref.addBox(url);
|
||||
minf.addBox(dinf);
|
||||
|
||||
Box stbl = createStbl(track);
|
||||
minf.addBox(stbl);
|
||||
mdia.addBox(minf);
|
||||
|
||||
return trackBox;
|
||||
}
|
||||
|
||||
protected Box createStbl(Track track) {
|
||||
SampleTableBox stbl = new SampleTableBox();
|
||||
|
||||
createStsd(track, stbl);
|
||||
createStts(track, stbl);
|
||||
createStss(track, stbl);
|
||||
createStsc(track, stbl);
|
||||
createStsz(track, stbl);
|
||||
createStco(track, stbl);
|
||||
|
||||
return stbl;
|
||||
}
|
||||
|
||||
protected void createStsd(Track track, SampleTableBox stbl) {
|
||||
stbl.addBox(track.getSampleDescriptionBox());
|
||||
}
|
||||
|
||||
protected void createStts(Track track, SampleTableBox stbl) {
|
||||
TimeToSampleBox.Entry lastEntry = null;
|
||||
List<TimeToSampleBox.Entry> entries = new ArrayList<>();
|
||||
|
||||
for (long delta : track.getSampleDurations()) {
|
||||
if (lastEntry != null && lastEntry.getDelta() == delta) {
|
||||
lastEntry.setCount(lastEntry.getCount() + 1);
|
||||
} else {
|
||||
lastEntry = new TimeToSampleBox.Entry(1, delta);
|
||||
entries.add(lastEntry);
|
||||
}
|
||||
}
|
||||
TimeToSampleBox stts = new TimeToSampleBox();
|
||||
stts.setEntries(entries);
|
||||
stbl.addBox(stts);
|
||||
}
|
||||
|
||||
protected void createStss(Track track, SampleTableBox stbl) {
|
||||
long[] syncSamples = track.getSyncSamples();
|
||||
if (syncSamples != null && syncSamples.length > 0) {
|
||||
SyncSampleBox stss = new SyncSampleBox();
|
||||
stss.setSampleNumber(syncSamples);
|
||||
stbl.addBox(stss);
|
||||
}
|
||||
}
|
||||
|
||||
protected void createStsc(Track track, SampleTableBox stbl) {
|
||||
SampleToChunkBox stsc = new SampleToChunkBox();
|
||||
stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>());
|
||||
|
||||
long lastOffset = -1;
|
||||
int lastChunkNumber = 1;
|
||||
int lastSampleCount = 0;
|
||||
|
||||
int previousWritedChunkCount = -1;
|
||||
|
||||
int samplesCount = track.getSamples().size();
|
||||
for (int a = 0; a < samplesCount; a++) {
|
||||
Sample sample = track.getSamples().get(a);
|
||||
long offset = sample.getOffset();
|
||||
long size = sample.getSize();
|
||||
|
||||
lastOffset = offset + size;
|
||||
lastSampleCount++;
|
||||
|
||||
boolean write = false;
|
||||
if (a != samplesCount - 1) {
|
||||
Sample nextSample = track.getSamples().get(a + 1);
|
||||
if (lastOffset != nextSample.getOffset()) {
|
||||
write = true;
|
||||
}
|
||||
} else {
|
||||
write = true;
|
||||
}
|
||||
if (write) {
|
||||
if (previousWritedChunkCount != lastSampleCount) {
|
||||
stsc.getEntries().add(new SampleToChunkBox.Entry(lastChunkNumber, lastSampleCount, 1));
|
||||
previousWritedChunkCount = lastSampleCount;
|
||||
}
|
||||
lastSampleCount = 0;
|
||||
lastChunkNumber++;
|
||||
}
|
||||
}
|
||||
stbl.addBox(stsc);
|
||||
}
|
||||
|
||||
protected void createStsz(Track track, SampleTableBox stbl) {
|
||||
SampleSizeBox stsz = new SampleSizeBox();
|
||||
stsz.setSampleSizes(track2SampleSizes.get(track));
|
||||
stbl.addBox(stsz);
|
||||
}
|
||||
|
||||
protected void createStco(Track track, SampleTableBox stbl) {
|
||||
ArrayList<Long> chunksOffsets = new ArrayList<>();
|
||||
long lastOffset = -1;
|
||||
for (Sample sample : track.getSamples()) {
|
||||
long offset = sample.getOffset();
|
||||
if (lastOffset != -1 && lastOffset != offset) {
|
||||
lastOffset = -1;
|
||||
}
|
||||
if (lastOffset == -1) {
|
||||
chunksOffsets.add(offset);
|
||||
}
|
||||
lastOffset = offset + sample.getSize();
|
||||
}
|
||||
long[] chunkOffsetsLong = new long[chunksOffsets.size()];
|
||||
for (int a = 0; a < chunksOffsets.size(); a++) {
|
||||
chunkOffsetsLong[a] = chunksOffsets.get(a);
|
||||
}
|
||||
|
||||
StaticChunkOffsetBox stco = new StaticChunkOffsetBox();
|
||||
stco.setChunkOffsets(chunkOffsetsLong);
|
||||
stbl.addBox(stco);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers.VideoCompressor;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
|
||||
import com.googlecode.mp4parser.util.Matrix;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@TargetApi(16)
|
||||
public class Mp4Movie {
|
||||
private Matrix matrix = Matrix.ROTATE_0;
|
||||
private ArrayList<Track> tracks = new ArrayList<Track>();
|
||||
private File cacheFile;
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
public Matrix getMatrix() {
|
||||
return matrix;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setCacheFile(File file) {
|
||||
cacheFile = file;
|
||||
}
|
||||
|
||||
public void setRotation(int angle) {
|
||||
if (angle == 0) {
|
||||
matrix = Matrix.ROTATE_0;
|
||||
} else if (angle == 90) {
|
||||
matrix = Matrix.ROTATE_90;
|
||||
} else if (angle == 180) {
|
||||
matrix = Matrix.ROTATE_180;
|
||||
} else if (angle == 270) {
|
||||
matrix = Matrix.ROTATE_270;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSize(int w, int h) {
|
||||
width = w;
|
||||
height = h;
|
||||
}
|
||||
|
||||
public ArrayList<Track> getTracks() {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public File getCacheFile() {
|
||||
return cacheFile;
|
||||
}
|
||||
|
||||
public void addSample(int trackIndex, long offset, MediaCodec.BufferInfo bufferInfo) throws Exception {
|
||||
if (trackIndex < 0 || trackIndex >= tracks.size()) {
|
||||
return;
|
||||
}
|
||||
Track track = tracks.get(trackIndex);
|
||||
track.addSample(offset, bufferInfo);
|
||||
}
|
||||
|
||||
public int addTrack(MediaFormat mediaFormat, boolean isAudio) throws Exception {
|
||||
tracks.add(new Track(tracks.size(), mediaFormat, isAudio));
|
||||
return tracks.size() - 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,191 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers.VideoCompressor;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.opengl.GLES20;
|
||||
import android.view.Surface;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.egl.EGLContext;
|
||||
import javax.microedition.khronos.egl.EGLDisplay;
|
||||
import javax.microedition.khronos.egl.EGLSurface;
|
||||
|
||||
@TargetApi(16)
|
||||
public class OutputSurface implements SurfaceTexture.OnFrameAvailableListener {
|
||||
|
||||
private static final int EGL_OPENGL_ES2_BIT = 4;
|
||||
private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
|
||||
private EGL10 mEGL;
|
||||
private EGLDisplay mEGLDisplay = null;
|
||||
private EGLContext mEGLContext = null;
|
||||
private EGLSurface mEGLSurface = null;
|
||||
private SurfaceTexture mSurfaceTexture;
|
||||
private Surface mSurface;
|
||||
private final Object mFrameSyncObject = new Object();
|
||||
private boolean mFrameAvailable;
|
||||
private TextureRenderer mTextureRender;
|
||||
private int mWidth;
|
||||
private int mHeight;
|
||||
private int rotateRender = 0;
|
||||
private ByteBuffer mPixelBuf;
|
||||
|
||||
public OutputSurface(int width, int height, int rotate) {
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
rotateRender = rotate;
|
||||
mPixelBuf = ByteBuffer.allocateDirect(mWidth * mHeight * 4);
|
||||
mPixelBuf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
eglSetup(width, height);
|
||||
makeCurrent();
|
||||
setup();
|
||||
}
|
||||
|
||||
public OutputSurface() {
|
||||
setup();
|
||||
}
|
||||
|
||||
private void setup() {
|
||||
mTextureRender = new TextureRenderer(rotateRender);
|
||||
mTextureRender.surfaceCreated();
|
||||
mSurfaceTexture = new SurfaceTexture(mTextureRender.getTextureId());
|
||||
mSurfaceTexture.setOnFrameAvailableListener(this);
|
||||
mSurface = new Surface(mSurfaceTexture);
|
||||
}
|
||||
|
||||
private void eglSetup(int width, int height) {
|
||||
mEGL = (EGL10) EGLContext.getEGL();
|
||||
mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
|
||||
|
||||
if (mEGLDisplay == EGL10.EGL_NO_DISPLAY) {
|
||||
throw new RuntimeException("unable to get EGL10 display");
|
||||
}
|
||||
|
||||
if (!mEGL.eglInitialize(mEGLDisplay, null)) {
|
||||
mEGLDisplay = null;
|
||||
throw new RuntimeException("unable to initialize EGL10");
|
||||
}
|
||||
|
||||
int[] attribList = {
|
||||
EGL10.EGL_RED_SIZE, 8,
|
||||
EGL10.EGL_GREEN_SIZE, 8,
|
||||
EGL10.EGL_BLUE_SIZE, 8,
|
||||
EGL10.EGL_ALPHA_SIZE, 8,
|
||||
EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT,
|
||||
EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL10.EGL_NONE
|
||||
};
|
||||
EGLConfig[] configs = new EGLConfig[1];
|
||||
int[] numConfigs = new int[1];
|
||||
if (!mEGL.eglChooseConfig(mEGLDisplay, attribList, configs, configs.length, numConfigs)) {
|
||||
throw new RuntimeException("unable to find RGB888+pbuffer EGL config");
|
||||
}
|
||||
int[] attrib_list = {
|
||||
EGL_CONTEXT_CLIENT_VERSION, 2,
|
||||
EGL10.EGL_NONE
|
||||
};
|
||||
mEGLContext = mEGL.eglCreateContext(mEGLDisplay, configs[0], EGL10.EGL_NO_CONTEXT, attrib_list);
|
||||
checkEglError("eglCreateContext");
|
||||
if (mEGLContext == null) {
|
||||
throw new RuntimeException("null context");
|
||||
}
|
||||
int[] surfaceAttribs = {
|
||||
EGL10.EGL_WIDTH, width,
|
||||
EGL10.EGL_HEIGHT, height,
|
||||
EGL10.EGL_NONE
|
||||
};
|
||||
mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, configs[0], surfaceAttribs);
|
||||
checkEglError("eglCreatePbufferSurface");
|
||||
if (mEGLSurface == null) {
|
||||
throw new RuntimeException("surface was null");
|
||||
}
|
||||
}
|
||||
|
||||
public void release() {
|
||||
if (mEGL != null) {
|
||||
if (mEGL.eglGetCurrentContext().equals(mEGLContext)) {
|
||||
mEGL.eglMakeCurrent(mEGLDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
|
||||
}
|
||||
mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface);
|
||||
mEGL.eglDestroyContext(mEGLDisplay, mEGLContext);
|
||||
}
|
||||
mSurface.release();
|
||||
mEGLDisplay = null;
|
||||
mEGLContext = null;
|
||||
mEGLSurface = null;
|
||||
mEGL = null;
|
||||
mTextureRender = null;
|
||||
mSurface = null;
|
||||
mSurfaceTexture = null;
|
||||
}
|
||||
|
||||
public void makeCurrent() {
|
||||
if (mEGL == null) {
|
||||
throw new RuntimeException("not configured for makeCurrent");
|
||||
}
|
||||
checkEglError("before makeCurrent");
|
||||
if (!mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {
|
||||
throw new RuntimeException("eglMakeCurrent failed");
|
||||
}
|
||||
}
|
||||
|
||||
public Surface getSurface() {
|
||||
return mSurface;
|
||||
}
|
||||
|
||||
public void changeFragmentShader(String fragmentShader) {
|
||||
mTextureRender.changeFragmentShader(fragmentShader);
|
||||
}
|
||||
|
||||
public void awaitNewImage() {
|
||||
final int TIMEOUT_MS = 5000;
|
||||
synchronized (mFrameSyncObject) {
|
||||
while (!mFrameAvailable) {
|
||||
try {
|
||||
mFrameSyncObject.wait(TIMEOUT_MS);
|
||||
if (!mFrameAvailable) {
|
||||
throw new RuntimeException("Surface frame wait timed out");
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
throw new RuntimeException(ie);
|
||||
}
|
||||
}
|
||||
mFrameAvailable = false;
|
||||
}
|
||||
mTextureRender.checkGlError("before updateTexImage");
|
||||
mSurfaceTexture.updateTexImage();
|
||||
}
|
||||
|
||||
public void drawImage(boolean invert) {
|
||||
mTextureRender.drawFrame(mSurfaceTexture, invert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFrameAvailable(SurfaceTexture st) {
|
||||
synchronized (mFrameSyncObject) {
|
||||
if (mFrameAvailable) {
|
||||
throw new RuntimeException("mFrameAvailable already set, frame could be dropped");
|
||||
}
|
||||
mFrameAvailable = true;
|
||||
mFrameSyncObject.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public ByteBuffer getFrame() {
|
||||
mPixelBuf.rewind();
|
||||
GLES20.glReadPixels(0, 0, mWidth, mHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mPixelBuf);
|
||||
return mPixelBuf;
|
||||
}
|
||||
|
||||
private void checkEglError(String msg) {
|
||||
if (mEGL.eglGetError() != EGL10.EGL_SUCCESS) {
|
||||
throw new RuntimeException("EGL error encountered (see log)");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers.VideoCompressor;
|
||||
|
||||
public class Sample {
|
||||
private long offset = 0;
|
||||
private long size = 0;
|
||||
|
||||
public Sample(long offset, long size) {
|
||||
this.offset = offset;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers.VideoCompressor;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.opengl.GLES11Ext;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.Matrix;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
@TargetApi(16)
|
||||
public class TextureRenderer {
|
||||
|
||||
private static final int FLOAT_SIZE_BYTES = 4;
|
||||
private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES;
|
||||
private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0;
|
||||
private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3;
|
||||
private static final float[] mTriangleVerticesData = {
|
||||
-1.0f, -1.0f, 0, 0.f, 0.f,
|
||||
1.0f, -1.0f, 0, 1.f, 0.f,
|
||||
-1.0f, 1.0f, 0, 0.f, 1.f,
|
||||
1.0f, 1.0f, 0, 1.f, 1.f,
|
||||
};
|
||||
private FloatBuffer mTriangleVertices;
|
||||
|
||||
private static final String VERTEX_SHADER =
|
||||
"uniform mat4 uMVPMatrix;\n" +
|
||||
"uniform mat4 uSTMatrix;\n" +
|
||||
"attribute vec4 aPosition;\n" +
|
||||
"attribute vec4 aTextureCoord;\n" +
|
||||
"varying vec2 vTextureCoord;\n" +
|
||||
"void main() {\n" +
|
||||
" gl_Position = uMVPMatrix * aPosition;\n" +
|
||||
" vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" +
|
||||
"}\n";
|
||||
|
||||
private static final String FRAGMENT_SHADER =
|
||||
"#extension GL_OES_EGL_image_external : require\n" +
|
||||
"precision mediump float;\n" +
|
||||
"varying vec2 vTextureCoord;\n" +
|
||||
"uniform samplerExternalOES sTexture;\n" +
|
||||
"void main() {\n" +
|
||||
" gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
|
||||
"}\n";
|
||||
|
||||
private float[] mMVPMatrix = new float[16];
|
||||
private float[] mSTMatrix = new float[16];
|
||||
private int mProgram;
|
||||
private int mTextureID = -12345;
|
||||
private int muMVPMatrixHandle;
|
||||
private int muSTMatrixHandle;
|
||||
private int maPositionHandle;
|
||||
private int maTextureHandle;
|
||||
private int rotationAngle = 0;
|
||||
|
||||
public TextureRenderer(int rotation) {
|
||||
rotationAngle = rotation;
|
||||
mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer();
|
||||
mTriangleVertices.put(mTriangleVerticesData).position(0);
|
||||
Matrix.setIdentityM(mSTMatrix, 0);
|
||||
}
|
||||
|
||||
public int getTextureId() {
|
||||
return mTextureID;
|
||||
}
|
||||
|
||||
public void drawFrame(SurfaceTexture st, boolean invert) {
|
||||
checkGlError("onDrawFrame start");
|
||||
st.getTransformMatrix(mSTMatrix);
|
||||
|
||||
if (invert) {
|
||||
mSTMatrix[5] = -mSTMatrix[5];
|
||||
mSTMatrix[13] = 1.0f - mSTMatrix[13];
|
||||
}
|
||||
|
||||
GLES20.glUseProgram(mProgram);
|
||||
checkGlError("glUseProgram");
|
||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);
|
||||
mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET);
|
||||
GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);
|
||||
checkGlError("glVertexAttribPointer maPosition");
|
||||
GLES20.glEnableVertexAttribArray(maPositionHandle);
|
||||
checkGlError("glEnableVertexAttribArray maPositionHandle");
|
||||
mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET);
|
||||
GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);
|
||||
checkGlError("glVertexAttribPointer maTextureHandle");
|
||||
GLES20.glEnableVertexAttribArray(maTextureHandle);
|
||||
checkGlError("glEnableVertexAttribArray maTextureHandle");
|
||||
GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, mSTMatrix, 0);
|
||||
GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0);
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
|
||||
checkGlError("glDrawArrays");
|
||||
GLES20.glFinish();
|
||||
}
|
||||
|
||||
public void surfaceCreated() {
|
||||
mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
|
||||
if (mProgram == 0) {
|
||||
throw new RuntimeException("failed creating program");
|
||||
}
|
||||
maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
|
||||
checkGlError("glGetAttribLocation aPosition");
|
||||
if (maPositionHandle == -1) {
|
||||
throw new RuntimeException("Could not get attrib location for aPosition");
|
||||
}
|
||||
maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord");
|
||||
checkGlError("glGetAttribLocation aTextureCoord");
|
||||
if (maTextureHandle == -1) {
|
||||
throw new RuntimeException("Could not get attrib location for aTextureCoord");
|
||||
}
|
||||
muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
|
||||
checkGlError("glGetUniformLocation uMVPMatrix");
|
||||
if (muMVPMatrixHandle == -1) {
|
||||
throw new RuntimeException("Could not get attrib location for uMVPMatrix");
|
||||
}
|
||||
muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix");
|
||||
checkGlError("glGetUniformLocation uSTMatrix");
|
||||
if (muSTMatrixHandle == -1) {
|
||||
throw new RuntimeException("Could not get attrib location for uSTMatrix");
|
||||
}
|
||||
int[] textures = new int[1];
|
||||
GLES20.glGenTextures(1, textures, 0);
|
||||
mTextureID = textures[0];
|
||||
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID);
|
||||
checkGlError("glBindTexture mTextureID");
|
||||
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
|
||||
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
|
||||
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
|
||||
checkGlError("glTexParameter");
|
||||
|
||||
Matrix.setIdentityM(mMVPMatrix, 0);
|
||||
if (rotationAngle != 0) {
|
||||
Matrix.rotateM(mMVPMatrix, 0, rotationAngle, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void changeFragmentShader(String fragmentShader) {
|
||||
GLES20.glDeleteProgram(mProgram);
|
||||
mProgram = createProgram(VERTEX_SHADER, fragmentShader);
|
||||
if (mProgram == 0) {
|
||||
throw new RuntimeException("failed creating program");
|
||||
}
|
||||
}
|
||||
|
||||
private int loadShader(int shaderType, String source) {
|
||||
int shader = GLES20.glCreateShader(shaderType);
|
||||
checkGlError("glCreateShader type=" + shaderType);
|
||||
GLES20.glShaderSource(shader, source);
|
||||
GLES20.glCompileShader(shader);
|
||||
int[] compiled = new int[1];
|
||||
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
|
||||
if (compiled[0] == 0) {
|
||||
GLES20.glDeleteShader(shader);
|
||||
shader = 0;
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
private int createProgram(String vertexSource, String fragmentSource) {
|
||||
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
|
||||
if (vertexShader == 0) {
|
||||
return 0;
|
||||
}
|
||||
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
|
||||
if (pixelShader == 0) {
|
||||
return 0;
|
||||
}
|
||||
int program = GLES20.glCreateProgram();
|
||||
checkGlError("glCreateProgram");
|
||||
if (program == 0) {
|
||||
return 0;
|
||||
}
|
||||
GLES20.glAttachShader(program, vertexShader);
|
||||
checkGlError("glAttachShader");
|
||||
GLES20.glAttachShader(program, pixelShader);
|
||||
checkGlError("glAttachShader");
|
||||
GLES20.glLinkProgram(program);
|
||||
int[] linkStatus = new int[1];
|
||||
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
|
||||
if (linkStatus[0] != GLES20.GL_TRUE) {
|
||||
GLES20.glDeleteProgram(program);
|
||||
program = 0;
|
||||
}
|
||||
return program;
|
||||
}
|
||||
|
||||
public void checkGlError(String op) {
|
||||
int error;
|
||||
if ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
|
||||
throw new RuntimeException(op + ": glError " + error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,255 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers.VideoCompressor;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaFormat;
|
||||
|
||||
import com.coremedia.iso.boxes.AbstractMediaHeaderBox;
|
||||
import com.coremedia.iso.boxes.SampleDescriptionBox;
|
||||
import com.coremedia.iso.boxes.SoundMediaHeaderBox;
|
||||
import com.coremedia.iso.boxes.VideoMediaHeaderBox;
|
||||
import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry;
|
||||
import com.coremedia.iso.boxes.sampleentry.VisualSampleEntry;
|
||||
import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox;
|
||||
import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.AudioSpecificConfig;
|
||||
import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.DecoderConfigDescriptor;
|
||||
import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.ESDescriptor;
|
||||
import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.SLConfigDescriptor;
|
||||
import com.mp4parser.iso14496.part15.AvcConfigurationBox;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
|
||||
@TargetApi(16)
|
||||
public class Track {
|
||||
private long trackId = 0;
|
||||
private ArrayList<Sample> samples = new ArrayList<Sample>();
|
||||
private long duration = 0;
|
||||
private String handler;
|
||||
private AbstractMediaHeaderBox headerBox = null;
|
||||
private SampleDescriptionBox sampleDescriptionBox = null;
|
||||
private LinkedList<Integer> syncSamples = null;
|
||||
private int timeScale;
|
||||
private Date creationTime = new Date();
|
||||
private int height;
|
||||
private int width;
|
||||
private float volume = 0;
|
||||
private ArrayList<Long> sampleDurations = new ArrayList<Long>();
|
||||
private boolean isAudio = false;
|
||||
private static Map<Integer, Integer> samplingFrequencyIndexMap = new HashMap<Integer, Integer>();
|
||||
private long lastPresentationTimeUs = 0;
|
||||
private boolean first = true;
|
||||
|
||||
static {
|
||||
samplingFrequencyIndexMap.put(96000, 0x0);
|
||||
samplingFrequencyIndexMap.put(88200, 0x1);
|
||||
samplingFrequencyIndexMap.put(64000, 0x2);
|
||||
samplingFrequencyIndexMap.put(48000, 0x3);
|
||||
samplingFrequencyIndexMap.put(44100, 0x4);
|
||||
samplingFrequencyIndexMap.put(32000, 0x5);
|
||||
samplingFrequencyIndexMap.put(24000, 0x6);
|
||||
samplingFrequencyIndexMap.put(22050, 0x7);
|
||||
samplingFrequencyIndexMap.put(16000, 0x8);
|
||||
samplingFrequencyIndexMap.put(12000, 0x9);
|
||||
samplingFrequencyIndexMap.put(11025, 0xa);
|
||||
samplingFrequencyIndexMap.put(8000, 0xb);
|
||||
}
|
||||
|
||||
public Track(int id, MediaFormat format, boolean isAudio) throws Exception {
|
||||
trackId = id;
|
||||
if (!isAudio) {
|
||||
sampleDurations.add((long)3015);
|
||||
duration = 3015;
|
||||
width = format.getInteger(MediaFormat.KEY_WIDTH);
|
||||
height = format.getInteger(MediaFormat.KEY_HEIGHT);
|
||||
timeScale = 90000;
|
||||
syncSamples = new LinkedList<Integer>();
|
||||
handler = "vide";
|
||||
headerBox = new VideoMediaHeaderBox();
|
||||
sampleDescriptionBox = new SampleDescriptionBox();
|
||||
String mime = format.getString(MediaFormat.KEY_MIME);
|
||||
if (mime.equals("video/avc")) {
|
||||
VisualSampleEntry visualSampleEntry = new VisualSampleEntry("avc1");
|
||||
visualSampleEntry.setDataReferenceIndex(1);
|
||||
visualSampleEntry.setDepth(24);
|
||||
visualSampleEntry.setFrameCount(1);
|
||||
visualSampleEntry.setHorizresolution(72);
|
||||
visualSampleEntry.setVertresolution(72);
|
||||
visualSampleEntry.setWidth(width);
|
||||
visualSampleEntry.setHeight(height);
|
||||
|
||||
AvcConfigurationBox avcConfigurationBox = new AvcConfigurationBox();
|
||||
|
||||
if (format.getByteBuffer("csd-0") != null) {
|
||||
ArrayList<byte[]> spsArray = new ArrayList<byte[]>();
|
||||
ByteBuffer spsBuff = format.getByteBuffer("csd-0");
|
||||
spsBuff.position(4);
|
||||
byte[] spsBytes = new byte[spsBuff.remaining()];
|
||||
spsBuff.get(spsBytes);
|
||||
spsArray.add(spsBytes);
|
||||
|
||||
ArrayList<byte[]> ppsArray = new ArrayList<byte[]>();
|
||||
ByteBuffer ppsBuff = format.getByteBuffer("csd-1");
|
||||
ppsBuff.position(4);
|
||||
byte[] ppsBytes = new byte[ppsBuff.remaining()];
|
||||
ppsBuff.get(ppsBytes);
|
||||
ppsArray.add(ppsBytes);
|
||||
avcConfigurationBox.setSequenceParameterSets(spsArray);
|
||||
avcConfigurationBox.setPictureParameterSets(ppsArray);
|
||||
}
|
||||
//ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(spsBytes);
|
||||
//SeqParameterSet seqParameterSet = SeqParameterSet.read(byteArrayInputStream);
|
||||
|
||||
avcConfigurationBox.setAvcLevelIndication(13);
|
||||
avcConfigurationBox.setAvcProfileIndication(100);
|
||||
avcConfigurationBox.setBitDepthLumaMinus8(-1);
|
||||
avcConfigurationBox.setBitDepthChromaMinus8(-1);
|
||||
avcConfigurationBox.setChromaFormat(-1);
|
||||
avcConfigurationBox.setConfigurationVersion(1);
|
||||
avcConfigurationBox.setLengthSizeMinusOne(3);
|
||||
avcConfigurationBox.setProfileCompatibility(0);
|
||||
|
||||
visualSampleEntry.addBox(avcConfigurationBox);
|
||||
sampleDescriptionBox.addBox(visualSampleEntry);
|
||||
} else if (mime.equals("video/mp4v")) {
|
||||
VisualSampleEntry visualSampleEntry = new VisualSampleEntry("mp4v");
|
||||
visualSampleEntry.setDataReferenceIndex(1);
|
||||
visualSampleEntry.setDepth(24);
|
||||
visualSampleEntry.setFrameCount(1);
|
||||
visualSampleEntry.setHorizresolution(72);
|
||||
visualSampleEntry.setVertresolution(72);
|
||||
visualSampleEntry.setWidth(width);
|
||||
visualSampleEntry.setHeight(height);
|
||||
|
||||
sampleDescriptionBox.addBox(visualSampleEntry);
|
||||
}
|
||||
} else {
|
||||
sampleDurations.add((long)1024);
|
||||
duration = 1024;
|
||||
isAudio = true;
|
||||
volume = 1;
|
||||
timeScale = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
||||
handler = "soun";
|
||||
headerBox = new SoundMediaHeaderBox();
|
||||
sampleDescriptionBox = new SampleDescriptionBox();
|
||||
AudioSampleEntry audioSampleEntry = new AudioSampleEntry("mp4a");
|
||||
audioSampleEntry.setChannelCount(format.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
|
||||
audioSampleEntry.setSampleRate(format.getInteger(MediaFormat.KEY_SAMPLE_RATE));
|
||||
audioSampleEntry.setDataReferenceIndex(1);
|
||||
audioSampleEntry.setSampleSize(16);
|
||||
|
||||
ESDescriptorBox esds = new ESDescriptorBox();
|
||||
ESDescriptor descriptor = new ESDescriptor();
|
||||
descriptor.setEsId(0);
|
||||
|
||||
SLConfigDescriptor slConfigDescriptor = new SLConfigDescriptor();
|
||||
slConfigDescriptor.setPredefined(2);
|
||||
descriptor.setSlConfigDescriptor(slConfigDescriptor);
|
||||
|
||||
DecoderConfigDescriptor decoderConfigDescriptor = new DecoderConfigDescriptor();
|
||||
decoderConfigDescriptor.setObjectTypeIndication(0x40);
|
||||
decoderConfigDescriptor.setStreamType(5);
|
||||
decoderConfigDescriptor.setBufferSizeDB(1536);
|
||||
decoderConfigDescriptor.setMaxBitRate(96000);
|
||||
decoderConfigDescriptor.setAvgBitRate(96000);
|
||||
|
||||
AudioSpecificConfig audioSpecificConfig = new AudioSpecificConfig();
|
||||
audioSpecificConfig.setAudioObjectType(2);
|
||||
audioSpecificConfig.setSamplingFrequencyIndex(samplingFrequencyIndexMap.get((int)audioSampleEntry.getSampleRate()));
|
||||
audioSpecificConfig.setChannelConfiguration(audioSampleEntry.getChannelCount());
|
||||
decoderConfigDescriptor.setAudioSpecificInfo(audioSpecificConfig);
|
||||
|
||||
descriptor.setDecoderConfigDescriptor(decoderConfigDescriptor);
|
||||
|
||||
ByteBuffer data = descriptor.serialize();
|
||||
esds.setEsDescriptor(descriptor);
|
||||
esds.setData(data);
|
||||
audioSampleEntry.addBox(esds);
|
||||
sampleDescriptionBox.addBox(audioSampleEntry);
|
||||
}
|
||||
}
|
||||
|
||||
public long getTrackId() {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
public void addSample(long offset, MediaCodec.BufferInfo bufferInfo) {
|
||||
boolean isSyncFrame = !isAudio && (bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) != 0;
|
||||
samples.add(new Sample(offset, bufferInfo.size));
|
||||
if (syncSamples != null && isSyncFrame) {
|
||||
syncSamples.add(samples.size());
|
||||
}
|
||||
|
||||
long delta = bufferInfo.presentationTimeUs - lastPresentationTimeUs;
|
||||
lastPresentationTimeUs = bufferInfo.presentationTimeUs;
|
||||
delta = (delta * timeScale + 500000L) / 1000000L;
|
||||
if (!first) {
|
||||
sampleDurations.add(sampleDurations.size() - 1, delta);
|
||||
duration += delta;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
|
||||
public ArrayList<Sample> getSamples() {
|
||||
return samples;
|
||||
}
|
||||
|
||||
public long getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public String getHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
public AbstractMediaHeaderBox getMediaHeaderBox() {
|
||||
return headerBox;
|
||||
}
|
||||
|
||||
public SampleDescriptionBox getSampleDescriptionBox() {
|
||||
return sampleDescriptionBox;
|
||||
}
|
||||
|
||||
public long[] getSyncSamples() {
|
||||
if (syncSamples == null || syncSamples.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
long[] returns = new long[syncSamples.size()];
|
||||
for (int i = 0; i < syncSamples.size(); i++) {
|
||||
returns[i] = syncSamples.get(i);
|
||||
}
|
||||
return returns;
|
||||
}
|
||||
|
||||
public int getTimeScale() {
|
||||
return timeScale;
|
||||
}
|
||||
|
||||
public Date getCreationTime() {
|
||||
return creationTime;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public float getVolume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
public ArrayList<Long> getSampleDurations() {
|
||||
return sampleDurations;
|
||||
}
|
||||
|
||||
public boolean isAudio() {
|
||||
return isAudio;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers.VideoCompressor;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.ObservableEmitter;
|
||||
import kotlin.Unit;
|
||||
|
||||
public class VideoCompress {
|
||||
|
||||
public static Observable<Boolean> compressVideoHigh(String srcPath, String destPath) {
|
||||
return Observable.create(emitter -> {
|
||||
VideoCompressTask task = new VideoCompressTask(emitter, VideoController.COMPRESS_QUALITY_HIGH);
|
||||
task.execute(srcPath, destPath);
|
||||
});
|
||||
}
|
||||
|
||||
public static Observable<Boolean> compressVideoMedium(String srcPath, String destPath) {
|
||||
return Observable.create(emitter -> {
|
||||
VideoCompressTask task = new VideoCompressTask(emitter, VideoController.COMPRESS_QUALITY_MEDIUM);
|
||||
task.execute(srcPath, destPath);
|
||||
});
|
||||
}
|
||||
|
||||
public static Observable<Boolean> compressVideoLow(String srcPath, String destPath) {
|
||||
return Observable.create(emitter -> {
|
||||
VideoCompressTask task = new VideoCompressTask(emitter, VideoController.COMPRESS_QUALITY_LOW);
|
||||
task.execute(srcPath, destPath);
|
||||
});
|
||||
}
|
||||
|
||||
private static class VideoCompressTask extends AsyncTask<String, Float, Boolean> {
|
||||
private ObservableEmitter<Boolean> emitter;
|
||||
private int mQuality;
|
||||
|
||||
public VideoCompressTask(ObservableEmitter<Boolean> listener, int quality) {
|
||||
emitter = listener;
|
||||
mQuality = quality;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(String... paths) {
|
||||
Log.i("VideoController", "Entered doInBackground");
|
||||
return VideoController.getInstance().convertVideo(paths[0], paths[1], mQuality, new VideoController.CompressProgressListener() {
|
||||
@Override
|
||||
public void onProgress(float percent) {
|
||||
publishProgress(percent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Float... percent) {
|
||||
super.onProgressUpdate(percent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
super.onPostExecute(result);
|
||||
if (emitter != null) {
|
||||
emitter.onNext(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,781 @@
|
|||
package com.ttpsc.dynamics365fieldService.core.managers.VideoCompressor;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaExtractor;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public class VideoController {
|
||||
static final int COMPRESS_QUALITY_HIGH = 1;
|
||||
static final int COMPRESS_QUALITY_MEDIUM = 2;
|
||||
static final int COMPRESS_QUALITY_LOW = 3;
|
||||
|
||||
public static File cachedFile;
|
||||
public String path;
|
||||
|
||||
public final static String MIME_TYPE = "video/avc";
|
||||
private final static int PROCESSOR_TYPE_OTHER = 0;
|
||||
private final static int PROCESSOR_TYPE_QCOM = 1;
|
||||
private final static int PROCESSOR_TYPE_INTEL = 2;
|
||||
private final static int PROCESSOR_TYPE_MTK = 3;
|
||||
private final static int PROCESSOR_TYPE_SEC = 4;
|
||||
private final static int PROCESSOR_TYPE_TI = 5;
|
||||
private static volatile VideoController Instance = null;
|
||||
private boolean videoConvertFirstWrite = true;
|
||||
|
||||
interface CompressProgressListener {
|
||||
void onProgress(float percent);
|
||||
}
|
||||
|
||||
public static VideoController getInstance() {
|
||||
VideoController localInstance = Instance;
|
||||
if (localInstance == null) {
|
||||
synchronized (VideoController.class) {
|
||||
localInstance = Instance;
|
||||
if (localInstance == null) {
|
||||
Instance = localInstance = new VideoController();
|
||||
}
|
||||
}
|
||||
}
|
||||
return localInstance;
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public static int selectColorFormat(MediaCodecInfo codecInfo, String mimeType) {
|
||||
MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType);
|
||||
int lastColorFormat = 0;
|
||||
for (int i = 0; i < capabilities.colorFormats.length; i++) {
|
||||
int colorFormat = capabilities.colorFormats[i];
|
||||
if (isRecognizedFormat(colorFormat)) {
|
||||
lastColorFormat = colorFormat;
|
||||
if (!(codecInfo.getName().equals("OMX.SEC.AVC.Encoder") && colorFormat == 19)) {
|
||||
return colorFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lastColorFormat;
|
||||
}
|
||||
|
||||
private static boolean isRecognizedFormat(int colorFormat) {
|
||||
switch (colorFormat) {
|
||||
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
|
||||
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
|
||||
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
|
||||
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
|
||||
case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public native static int convertVideoFrame(ByteBuffer src, ByteBuffer dest, int destFormat, int width, int height, int padding, int swap);
|
||||
|
||||
private void didWriteData(final boolean last, final boolean error) {
|
||||
final boolean firstWrite = videoConvertFirstWrite;
|
||||
if (firstWrite) {
|
||||
videoConvertFirstWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class VideoConvertRunnable implements Runnable {
|
||||
|
||||
private String videoPath;
|
||||
private String destPath;
|
||||
|
||||
private VideoConvertRunnable(String videoPath, String destPath) {
|
||||
this.videoPath = videoPath;
|
||||
this.destPath = destPath;
|
||||
}
|
||||
|
||||
public static void runConversion(final String videoPath, final String destPath) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
VideoConvertRunnable wrapper = new VideoConvertRunnable(videoPath, destPath);
|
||||
Thread th = new Thread(wrapper, "VideoConvertRunnable");
|
||||
th.start();
|
||||
th.join();
|
||||
} catch (Exception e) {
|
||||
Log.e("tmessages", e.getMessage());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
VideoController.getInstance().convertVideo(videoPath, destPath, 0, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static MediaCodecInfo selectCodec(String mimeType) {
|
||||
int numCodecs = MediaCodecList.getCodecCount();
|
||||
MediaCodecInfo lastCodecInfo = null;
|
||||
for (int i = 0; i < numCodecs; i++) {
|
||||
MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
|
||||
if (!codecInfo.isEncoder()) {
|
||||
continue;
|
||||
}
|
||||
String[] types = codecInfo.getSupportedTypes();
|
||||
for (String type : types) {
|
||||
if (type.equalsIgnoreCase(mimeType)) {
|
||||
lastCodecInfo = codecInfo;
|
||||
if (!lastCodecInfo.getName().equals("OMX.SEC.avc.enc")) {
|
||||
return lastCodecInfo;
|
||||
} else if (lastCodecInfo.getName().equals("OMX.SEC.AVC.Encoder")) {
|
||||
return lastCodecInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lastCodecInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Background conversion for queueing tasks
|
||||
* @param path source file to compress
|
||||
* @param dest destination directory to put result
|
||||
*/
|
||||
|
||||
public void scheduleVideoConvert(String path, String dest) {
|
||||
startVideoConvertFromQueue(path, dest);
|
||||
}
|
||||
|
||||
private void startVideoConvertFromQueue(String path, String dest) {
|
||||
VideoConvertRunnable.runConversion(path, dest);
|
||||
}
|
||||
|
||||
@TargetApi(16)
|
||||
private long readAndWriteTrack(MediaExtractor extractor, MP4Builder mediaMuxer, MediaCodec.BufferInfo info, long start, long end, File file, boolean isAudio) throws Exception {
|
||||
int trackIndex = selectTrack(extractor, isAudio);
|
||||
if (trackIndex >= 0) {
|
||||
extractor.selectTrack(trackIndex);
|
||||
MediaFormat trackFormat = extractor.getTrackFormat(trackIndex);
|
||||
int muxerTrackIndex = mediaMuxer.addTrack(trackFormat, isAudio);
|
||||
int maxBufferSize = trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
|
||||
boolean inputDone = false;
|
||||
if (start > 0) {
|
||||
extractor.seekTo(start, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
|
||||
} else {
|
||||
extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocateDirect(maxBufferSize);
|
||||
long startTime = -1;
|
||||
|
||||
while (!inputDone) {
|
||||
|
||||
boolean eof = false;
|
||||
int index = extractor.getSampleTrackIndex();
|
||||
if (index == trackIndex) {
|
||||
info.size = extractor.readSampleData(buffer, 0);
|
||||
|
||||
if (info.size < 0) {
|
||||
info.size = 0;
|
||||
eof = true;
|
||||
} else {
|
||||
info.presentationTimeUs = extractor.getSampleTime();
|
||||
if (start > 0 && startTime == -1) {
|
||||
startTime = info.presentationTimeUs;
|
||||
}
|
||||
if (end < 0 || info.presentationTimeUs < end) {
|
||||
info.offset = 0;
|
||||
info.flags = extractor.getSampleFlags();
|
||||
if (mediaMuxer.writeSampleData(muxerTrackIndex, buffer, info, isAudio)) {
|
||||
// didWriteData(messageObject, file, false, false);
|
||||
}
|
||||
extractor.advance();
|
||||
} else {
|
||||
eof = true;
|
||||
}
|
||||
}
|
||||
} else if (index == -1) {
|
||||
eof = true;
|
||||
}
|
||||
if (eof) {
|
||||
inputDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
extractor.unselectTrack(trackIndex);
|
||||
return startTime;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@TargetApi(16)
|
||||
private int selectTrack(MediaExtractor extractor, boolean audio) {
|
||||
int numTracks = extractor.getTrackCount();
|
||||
for (int i = 0; i < numTracks; i++) {
|
||||
MediaFormat format = extractor.getTrackFormat(i);
|
||||
String mime = format.getString(MediaFormat.KEY_MIME);
|
||||
if (audio) {
|
||||
if (mime.startsWith("audio/")) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
if (mime.startsWith("video/")) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the actual video compression. Processes the frames and does the magic
|
||||
* @param sourcePath the source uri for the file as per
|
||||
* @param destinationPath the destination directory where compressed video is eventually saved
|
||||
* @return
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public boolean convertVideo(final String sourcePath, String destinationPath, int quality, CompressProgressListener listener) {
|
||||
this.path=sourcePath;
|
||||
Log.i("VideoController", "Entered convertVideo");
|
||||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
retriever.setDataSource(path);
|
||||
String width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
|
||||
String height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
|
||||
String rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
|
||||
long duration = Long.valueOf(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000;
|
||||
|
||||
long startTime = -1;
|
||||
long endTime = -1;
|
||||
|
||||
int rotationValue = Integer.valueOf(rotation);
|
||||
int originalWidth = Integer.valueOf(width);
|
||||
int originalHeight = Integer.valueOf(height);
|
||||
|
||||
int resultWidth;
|
||||
int resultHeight;
|
||||
int bitrate;
|
||||
switch (quality) {
|
||||
default:
|
||||
case COMPRESS_QUALITY_HIGH:
|
||||
resultWidth = originalWidth * 2 / 3;
|
||||
resultHeight = originalHeight * 2 / 3;
|
||||
bitrate = resultWidth * resultHeight * 30;
|
||||
break;
|
||||
case COMPRESS_QUALITY_MEDIUM:
|
||||
resultWidth = originalWidth / 2;
|
||||
resultHeight = originalHeight / 2;
|
||||
bitrate = resultWidth * resultHeight * 10;
|
||||
break;
|
||||
case COMPRESS_QUALITY_LOW:
|
||||
resultWidth = originalWidth / 2;
|
||||
resultHeight = originalHeight / 2;
|
||||
bitrate = (resultWidth/2) * (resultHeight/2) * 10;
|
||||
break;
|
||||
}
|
||||
|
||||
int rotateRender = 0;
|
||||
|
||||
File cacheFile = new File(destinationPath);
|
||||
|
||||
if (Build.VERSION.SDK_INT < 18 && resultHeight > resultWidth && resultWidth != originalWidth && resultHeight != originalHeight) {
|
||||
int temp = resultHeight;
|
||||
resultHeight = resultWidth;
|
||||
resultWidth = temp;
|
||||
rotationValue = 90;
|
||||
rotateRender = 270;
|
||||
} else if (Build.VERSION.SDK_INT > 20) {
|
||||
if (rotationValue == 90) {
|
||||
int temp = resultHeight;
|
||||
resultHeight = resultWidth;
|
||||
resultWidth = temp;
|
||||
rotationValue = 0;
|
||||
rotateRender = 270;
|
||||
} else if (rotationValue == 180) {
|
||||
rotateRender = 180;
|
||||
rotationValue = 0;
|
||||
} else if (rotationValue == 270) {
|
||||
int temp = resultHeight;
|
||||
resultHeight = resultWidth;
|
||||
resultWidth = temp;
|
||||
rotationValue = 0;
|
||||
rotateRender = 90;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File inputFile = new File(path);
|
||||
if (!inputFile.canRead()) {
|
||||
didWriteData(true, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
videoConvertFirstWrite = true;
|
||||
boolean error = false;
|
||||
long videoStartTime = startTime;
|
||||
|
||||
long time = System.currentTimeMillis();
|
||||
|
||||
if (resultWidth != 0 && resultHeight != 0) {
|
||||
MP4Builder mediaMuxer = null;
|
||||
MediaExtractor extractor = null;
|
||||
|
||||
try {
|
||||
Log.i("VideoController", "Entered try");
|
||||
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
|
||||
Mp4Movie movie = new Mp4Movie();
|
||||
movie.setCacheFile(cacheFile);
|
||||
movie.setRotation(rotationValue);
|
||||
movie.setSize(resultWidth, resultHeight);
|
||||
mediaMuxer = new MP4Builder().createMovie(movie);
|
||||
extractor = new MediaExtractor();
|
||||
extractor.setDataSource(inputFile.toString());
|
||||
|
||||
|
||||
if (resultWidth != originalWidth || resultHeight != originalHeight) {
|
||||
int videoIndex;
|
||||
videoIndex = selectTrack(extractor, false);
|
||||
|
||||
if (videoIndex >= 0) {
|
||||
MediaCodec decoder = null;
|
||||
MediaCodec encoder = null;
|
||||
InputSurface inputSurface = null;
|
||||
OutputSurface outputSurface = null;
|
||||
|
||||
try {
|
||||
long videoTime = -1;
|
||||
boolean outputDone = false;
|
||||
boolean inputDone = false;
|
||||
boolean decoderDone = false;
|
||||
int swapUV = 0;
|
||||
int videoTrackIndex = -5;
|
||||
|
||||
int colorFormat;
|
||||
int processorType = PROCESSOR_TYPE_OTHER;
|
||||
String manufacturer = Build.MANUFACTURER.toLowerCase();
|
||||
if (Build.VERSION.SDK_INT < 18) {
|
||||
MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);
|
||||
colorFormat = selectColorFormat(codecInfo, MIME_TYPE);
|
||||
if (colorFormat == 0) {
|
||||
throw new RuntimeException("no supported color format");
|
||||
}
|
||||
String codecName = codecInfo.getName();
|
||||
if (codecName.contains("OMX.qcom.")) {
|
||||
processorType = PROCESSOR_TYPE_QCOM;
|
||||
if (Build.VERSION.SDK_INT == 16) {
|
||||
if (manufacturer.equals("lge") || manufacturer.equals("nokia")) {
|
||||
swapUV = 1;
|
||||
}
|
||||
}
|
||||
} else if (codecName.contains("OMX.Intel.")) {
|
||||
processorType = PROCESSOR_TYPE_INTEL;
|
||||
} else if (codecName.equals("OMX.MTK.VIDEO.ENCODER.AVC")) {
|
||||
processorType = PROCESSOR_TYPE_MTK;
|
||||
} else if (codecName.equals("OMX.SEC.AVC.Encoder")) {
|
||||
processorType = PROCESSOR_TYPE_SEC;
|
||||
swapUV = 1;
|
||||
} else if (codecName.equals("OMX.TI.DUCATI1.VIDEO.H264E")) {
|
||||
processorType = PROCESSOR_TYPE_TI;
|
||||
}
|
||||
Log.e("tmessages", "codec = " + codecInfo.getName() + " manufacturer = " + manufacturer + "device = " + Build.MODEL);
|
||||
} else {
|
||||
colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface;
|
||||
}
|
||||
Log.e("tmessages", "colorFormat = " + colorFormat);
|
||||
|
||||
int resultHeightAligned = resultHeight;
|
||||
int padding = 0;
|
||||
int bufferSize = resultWidth * resultHeight * 3 / 2;
|
||||
if (processorType == PROCESSOR_TYPE_OTHER) {
|
||||
if (resultHeight % 16 != 0) {
|
||||
resultHeightAligned += (16 - (resultHeight % 16));
|
||||
padding = resultWidth * (resultHeightAligned - resultHeight);
|
||||
bufferSize += padding * 5 / 4;
|
||||
}
|
||||
} else if (processorType == PROCESSOR_TYPE_QCOM) {
|
||||
if (!manufacturer.toLowerCase().equals("lge")) {
|
||||
int uvoffset = (resultWidth * resultHeight + 2047) & ~2047;
|
||||
padding = uvoffset - (resultWidth * resultHeight);
|
||||
bufferSize += padding;
|
||||
}
|
||||
} else if (processorType == PROCESSOR_TYPE_TI) {
|
||||
//resultHeightAligned = 368;
|
||||
//bufferSize = resultWidth * resultHeightAligned * 3 / 2;
|
||||
//resultHeightAligned += (16 - (resultHeight % 16));
|
||||
//padding = resultWidth * (resultHeightAligned - resultHeight);
|
||||
//bufferSize += padding * 5 / 4;
|
||||
} else if (processorType == PROCESSOR_TYPE_MTK) {
|
||||
if (manufacturer.equals("baidu")) {
|
||||
resultHeightAligned += (16 - (resultHeight % 16));
|
||||
padding = resultWidth * (resultHeightAligned - resultHeight);
|
||||
bufferSize += padding * 5 / 4;
|
||||
}
|
||||
}
|
||||
|
||||
extractor.selectTrack(videoIndex);
|
||||
if (startTime > 0) {
|
||||
extractor.seekTo(startTime, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
|
||||
} else {
|
||||
extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
|
||||
}
|
||||
MediaFormat inputFormat = extractor.getTrackFormat(videoIndex);
|
||||
|
||||
MediaFormat outputFormat = MediaFormat.createVideoFormat(MIME_TYPE, resultWidth, resultHeight);
|
||||
outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
|
||||
outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
|
||||
outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 25);
|
||||
outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
|
||||
if (Build.VERSION.SDK_INT < 18) {
|
||||
outputFormat.setInteger("stride", resultWidth + 32);
|
||||
outputFormat.setInteger("slice-height", resultHeight);
|
||||
}
|
||||
|
||||
encoder = MediaCodec.createEncoderByType(MIME_TYPE);
|
||||
encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
if (Build.VERSION.SDK_INT >= 18) {
|
||||
inputSurface = new InputSurface(encoder.createInputSurface());
|
||||
inputSurface.makeCurrent();
|
||||
}
|
||||
encoder.start();
|
||||
|
||||
decoder = MediaCodec.createDecoderByType(inputFormat.getString(MediaFormat.KEY_MIME));
|
||||
if (Build.VERSION.SDK_INT >= 18) {
|
||||
outputSurface = new OutputSurface();
|
||||
} else {
|
||||
outputSurface = new OutputSurface(resultWidth, resultHeight, rotateRender);
|
||||
}
|
||||
decoder.configure(inputFormat, outputSurface.getSurface(), null, 0);
|
||||
decoder.start();
|
||||
|
||||
final int TIMEOUT_USEC = 2500;
|
||||
ByteBuffer[] decoderInputBuffers = null;
|
||||
ByteBuffer[] encoderOutputBuffers = null;
|
||||
ByteBuffer[] encoderInputBuffers = null;
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
decoderInputBuffers = decoder.getInputBuffers();
|
||||
encoderOutputBuffers = encoder.getOutputBuffers();
|
||||
if (Build.VERSION.SDK_INT < 18) {
|
||||
encoderInputBuffers = encoder.getInputBuffers();
|
||||
}
|
||||
}
|
||||
|
||||
while (!outputDone) {
|
||||
if (!inputDone) {
|
||||
boolean eof = false;
|
||||
int index = extractor.getSampleTrackIndex();
|
||||
if (index == videoIndex) {
|
||||
int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
|
||||
if (inputBufIndex >= 0) {
|
||||
ByteBuffer inputBuf;
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
inputBuf = decoderInputBuffers[inputBufIndex];
|
||||
} else {
|
||||
inputBuf = decoder.getInputBuffer(inputBufIndex);
|
||||
}
|
||||
int chunkSize = extractor.readSampleData(inputBuf, 0);
|
||||
if (chunkSize < 0) {
|
||||
decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||
inputDone = true;
|
||||
} else {
|
||||
decoder.queueInputBuffer(inputBufIndex, 0, chunkSize, extractor.getSampleTime(), 0);
|
||||
extractor.advance();
|
||||
}
|
||||
}
|
||||
} else if (index == -1) {
|
||||
eof = true;
|
||||
}
|
||||
if (eof) {
|
||||
int inputBufIndex = decoder.dequeueInputBuffer(TIMEOUT_USEC);
|
||||
if (inputBufIndex >= 0) {
|
||||
decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||
inputDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean decoderOutputAvailable = !decoderDone;
|
||||
boolean encoderOutputAvailable = true;
|
||||
while (decoderOutputAvailable || encoderOutputAvailable) {
|
||||
int encoderStatus = encoder.dequeueOutputBuffer(info, TIMEOUT_USEC);
|
||||
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||
encoderOutputAvailable = false;
|
||||
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
encoderOutputBuffers = encoder.getOutputBuffers();
|
||||
}
|
||||
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
MediaFormat newFormat = encoder.getOutputFormat();
|
||||
if (videoTrackIndex == -5) {
|
||||
videoTrackIndex = mediaMuxer.addTrack(newFormat, false);
|
||||
}
|
||||
} else if (encoderStatus < 0) {
|
||||
throw new RuntimeException("unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
|
||||
} else {
|
||||
ByteBuffer encodedData;
|
||||
if (Build.VERSION.SDK_INT < 21) {
|
||||
encodedData = encoderOutputBuffers[encoderStatus];
|
||||
} else {
|
||||
encodedData = encoder.getOutputBuffer(encoderStatus);
|
||||
}
|
||||
if (encodedData == null) {
|
||||
throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
|
||||
}
|
||||
if (info.size > 1) {
|
||||
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
||||
if (mediaMuxer.writeSampleData(videoTrackIndex, encodedData, info, false)) {
|
||||
didWriteData(false, false);
|
||||
}
|
||||
} else if (videoTrackIndex == -5) {
|
||||
byte[] csd = new byte[info.size];
|
||||
encodedData.limit(info.offset + info.size);
|
||||
encodedData.position(info.offset);
|
||||
encodedData.get(csd);
|
||||
ByteBuffer sps = null;
|
||||
ByteBuffer pps = null;
|
||||
for (int a = info.size - 1; a >= 0; a--) {
|
||||
if (a > 3) {
|
||||
if (csd[a] == 1 && csd[a - 1] == 0 && csd[a - 2] == 0 && csd[a - 3] == 0) {
|
||||
sps = ByteBuffer.allocate(a - 3);
|
||||
pps = ByteBuffer.allocate(info.size - (a - 3));
|
||||
sps.put(csd, 0, a - 3).position(0);
|
||||
pps.put(csd, a - 3, info.size - (a - 3)).position(0);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MediaFormat newFormat = MediaFormat.createVideoFormat(MIME_TYPE, resultWidth, resultHeight);
|
||||
if (sps != null && pps != null) {
|
||||
newFormat.setByteBuffer("csd-0", sps);
|
||||
newFormat.setByteBuffer("csd-1", pps);
|
||||
}
|
||||
videoTrackIndex = mediaMuxer.addTrack(newFormat, false);
|
||||
}
|
||||
}
|
||||
outputDone = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
encoder.releaseOutputBuffer(encoderStatus, false);
|
||||
}
|
||||
if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!decoderDone) {
|
||||
int decoderStatus = decoder.dequeueOutputBuffer(info, TIMEOUT_USEC);
|
||||
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
|
||||
decoderOutputAvailable = false;
|
||||
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||
|
||||
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
MediaFormat newFormat = decoder.getOutputFormat();
|
||||
Log.e("tmessages", "newFormat = " + newFormat);
|
||||
} else if (decoderStatus < 0) {
|
||||
throw new RuntimeException("unexpected result from decoder.dequeueOutputBuffer: " + decoderStatus);
|
||||
} else {
|
||||
boolean doRender;
|
||||
if (Build.VERSION.SDK_INT >= 18) {
|
||||
doRender = info.size != 0;
|
||||
} else {
|
||||
doRender = info.size != 0 || info.presentationTimeUs != 0;
|
||||
}
|
||||
if (endTime > 0 && info.presentationTimeUs >= endTime) {
|
||||
inputDone = true;
|
||||
decoderDone = true;
|
||||
doRender = false;
|
||||
info.flags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM;
|
||||
}
|
||||
if (startTime > 0 && videoTime == -1) {
|
||||
if (info.presentationTimeUs < startTime) {
|
||||
doRender = false;
|
||||
Log.e("tmessages", "drop frame startTime = " + startTime + " present time = " + info.presentationTimeUs);
|
||||
} else {
|
||||
videoTime = info.presentationTimeUs;
|
||||
}
|
||||
}
|
||||
decoder.releaseOutputBuffer(decoderStatus, doRender);
|
||||
if (doRender) {
|
||||
boolean errorWait = false;
|
||||
try {
|
||||
outputSurface.awaitNewImage();
|
||||
} catch (Exception e) {
|
||||
errorWait = true;
|
||||
Log.e("tmessages", e.getMessage());
|
||||
}
|
||||
if (!errorWait) {
|
||||
if (Build.VERSION.SDK_INT >= 18) {
|
||||
outputSurface.drawImage(false);
|
||||
inputSurface.setPresentationTime(info.presentationTimeUs * 1000);
|
||||
|
||||
if (listener != null) {
|
||||
listener.onProgress((float) info.presentationTimeUs / (float) duration * 100);
|
||||
}
|
||||
|
||||
inputSurface.swapBuffers();
|
||||
} else {
|
||||
int inputBufIndex = encoder.dequeueInputBuffer(TIMEOUT_USEC);
|
||||
if (inputBufIndex >= 0) {
|
||||
outputSurface.drawImage(true);
|
||||
ByteBuffer rgbBuf = outputSurface.getFrame();
|
||||
ByteBuffer yuvBuf = encoderInputBuffers[inputBufIndex];
|
||||
yuvBuf.clear();
|
||||
convertVideoFrame(rgbBuf, yuvBuf, colorFormat, resultWidth, resultHeight, padding, swapUV);
|
||||
encoder.queueInputBuffer(inputBufIndex, 0, bufferSize, info.presentationTimeUs, 0);
|
||||
} else {
|
||||
Log.e("tmessages", "input buffer not available");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
||||
decoderOutputAvailable = false;
|
||||
Log.e("tmessages", "decoder stream end");
|
||||
if (Build.VERSION.SDK_INT >= 18) {
|
||||
encoder.signalEndOfInputStream();
|
||||
} else {
|
||||
int inputBufIndex = encoder.dequeueInputBuffer(TIMEOUT_USEC);
|
||||
if (inputBufIndex >= 0) {
|
||||
encoder.queueInputBuffer(inputBufIndex, 0, 1, info.presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (videoTime != -1) {
|
||||
videoStartTime = videoTime;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("tmessages", e.getMessage());
|
||||
error = true;
|
||||
}
|
||||
|
||||
extractor.unselectTrack(videoIndex);
|
||||
|
||||
if (outputSurface != null) {
|
||||
outputSurface.release();
|
||||
}
|
||||
if (inputSurface != null) {
|
||||
inputSurface.release();
|
||||
}
|
||||
if (decoder != null) {
|
||||
decoder.stop();
|
||||
decoder.release();
|
||||
}
|
||||
if (encoder != null) {
|
||||
encoder.stop();
|
||||
encoder.release();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
long videoTime = readAndWriteTrack(extractor, mediaMuxer, info, startTime, endTime, cacheFile, false);
|
||||
if (videoTime != -1) {
|
||||
videoStartTime = videoTime;
|
||||
}
|
||||
}
|
||||
if (!error) {
|
||||
readAndWriteTrack(extractor, mediaMuxer, info, videoStartTime, endTime, cacheFile, true);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
error = true;
|
||||
Log.e("tmessages", e.getMessage());
|
||||
} finally {
|
||||
if (extractor != null) {
|
||||
extractor.release();
|
||||
}
|
||||
if (mediaMuxer != null) {
|
||||
try {
|
||||
mediaMuxer.finishMovie(false);
|
||||
} catch (Exception e) {
|
||||
Log.e("tmessages", e.getMessage());
|
||||
}
|
||||
}
|
||||
Log.e("tmessages", "time = " + (System.currentTimeMillis() - time));
|
||||
}
|
||||
} else {
|
||||
didWriteData(true, true);
|
||||
return false;
|
||||
}
|
||||
didWriteData(true, error);
|
||||
|
||||
cachedFile=cacheFile;
|
||||
|
||||
/* File fdelete = inputFile;
|
||||
if (fdelete.exists()) {
|
||||
if (fdelete.delete()) {
|
||||
Log.e("file Deleted :" ,inputFile.getPath());
|
||||
} else {
|
||||
Log.e("file not Deleted :" , inputFile.getPath());
|
||||
}
|
||||
}*/
|
||||
|
||||
//inputFile.delete();
|
||||
Log.e("ViratPath",path+"");
|
||||
Log.e("ViratPath",cacheFile.getPath()+"");
|
||||
Log.e("ViratPath",inputFile.getPath()+"");
|
||||
|
||||
|
||||
/* Log.e("ViratPath",path+"");
|
||||
File replacedFile = new File(path);
|
||||
|
||||
FileOutputStream fos = null;
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
fos = new FileOutputStream(replacedFile);
|
||||
inputStream = new FileInputStream(cacheFile);
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
while ((len = inputStream.read(buf)) > 0) {
|
||||
fos.write(buf, 0, len);
|
||||
}
|
||||
inputStream.close();
|
||||
fos.close();
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
*/
|
||||
|
||||
// cacheFile.delete();
|
||||
|
||||
/* try {
|
||||
// copyFile(cacheFile,inputFile);
|
||||
//inputFile.delete();
|
||||
FileUtils.copyFile(cacheFile,inputFile);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}*/
|
||||
// cacheFile.delete();
|
||||
// inputFile.delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void copyFile(File src, File dst) throws IOException
|
||||
{
|
||||
FileChannel inChannel = new FileInputStream(src).getChannel();
|
||||
FileChannel outChannel = new FileOutputStream(dst).getChannel();
|
||||
try
|
||||
{
|
||||
inChannel.transferTo(1, inChannel.size(), outChannel);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (inChannel != null)
|
||||
inChannel.close();
|
||||
if (outChannel != null)
|
||||
outChannel.close();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.ttpsc.dynamics365fieldService.dal.abstraction
|
||||
|
||||
import com.ttpsc.dynamics365fieldService.AppConfiguration
|
||||
import com.ttpsc.dynamics365fieldService.dal.models.dynamics.*
|
||||
import com.ttpsc.dynamics365fieldService.dal.models.dynamics.customFields.FormWrapper
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.*
|
||||
|
||||
interface DynamicsFieldServiceApi {
|
||||
@GET("bookableresources")
|
||||
fun getBookableResources(@QueryMap options: Map<String, String>?): Observable<DalBaseDynamicsModel<DalBookableResource>>
|
||||
|
||||
@GET("bookableresourcebookings")
|
||||
fun getBookableResourceBookings(@Header("Prefer") itemsPerPageCountHeader: String?, @QueryMap options: Map<String, String>?): Observable<DalBaseDynamicsModel<DalBookableResourceBooking>>
|
||||
|
||||
@GET("bookingstatuses")
|
||||
fun getBookingStatuses(): Observable<DalBaseDynamicsModel<DalDynamicsBookingStatus>>
|
||||
|
||||
@GET("msdyn_workorders")
|
||||
fun getWorkOrders(@QueryMap options: Map<String, String>?): Observable<DalBaseDynamicsModel<DalDynamicsWorkOrder>>
|
||||
|
||||
@GET("systemusers")
|
||||
fun getUserInfo(@QueryMap options: Map<String, String>?): Observable<DalBaseDynamicsModel<DalDynamicsUserInfo>>
|
||||
|
||||
@GET("msdyn_iotalerts")
|
||||
fun getAlerts(@QueryMap options: Map<String, String>?): Observable<DalBaseDynamicsModel<DalDynamicsAlert>>
|
||||
|
||||
@PUT("bookableresourcebookings({bookableResourceBookingId})/BookingStatus/\$ref")
|
||||
fun changeBookableResourceBookingStatus(@Path("bookableResourceBookingId") bookableResourceBookingId: String, @Body bookingStatusBody: Map<String, String>): Observable<Response<Void>>
|
||||
|
||||
@GET("annotations")
|
||||
fun getNotes(@QueryMap options: Map<String, String>?): Observable<DalBaseDynamicsModel<DalNote>>
|
||||
|
||||
@POST("annotations")
|
||||
fun createNote(@Body note: DalCreateNoteRequestBody): Observable<Response<Void>>
|
||||
|
||||
@GET("systemforms")
|
||||
fun downloadForm(@QueryMap options: Map<String, String>?): Observable<DalBaseDynamicsModel<FormWrapper>>
|
||||
|
||||
@GET("msdyn_workorders")
|
||||
fun getRawWorkOrders(@QueryMap options: Map<String, String>?): Observable<Response<String>>
|
||||
|
||||
@GET
|
||||
fun downloadFile(
|
||||
@Url fileUrl: String?, @Tag leaveUrl: String = AppConfiguration.RequestTags.leaveUrl
|
||||
): Observable<ResponseBody?>
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package com.ttpsc.dynamics365fieldService.dal.models.dynamics
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class DalBaseDynamicsModel <DataType>(
|
||||
@SerializedName("@odata.context")
|
||||
val dataContext: String,
|
||||
val value : List<DataType>,
|
||||
@SerializedName("@odata.count")
|
||||
val count: Int?,
|
||||
@SerializedName("@odata.nextLink")
|
||||
val nextPageLink: String?
|
||||
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
package com.ttpsc.dynamics365fieldService.dal.models.dynamics
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.BookableResource
|
||||
import com.ttpsc.dynamics365fieldService.bll.models.Procedure
|
||||
|
||||
class DalBookableResource(
|
||||
|
||||
@SerializedName("@odata.etag") val etag: String,
|
||||
@SerializedName("_userid_value") val _userid_value: String,
|
||||
@SerializedName("bookableresourceid") val bookableresourceid: String
|
||||
){
|
||||
fun toBookableResource(): BookableResource {
|
||||
|
||||
val bookableResource = BookableResource(
|
||||
userId = _userid_value,
|
||||
bookableResourceId = bookableresourceid
|
||||
)
|
||||
|
||||
return bookableResource
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче