Merge pull request #1152 from nextcloud/qaBuild

QA Build & GHActions for Quality Checks
This commit is contained in:
Andy Scherzinger 2021-04-27 18:56:47 +02:00 committed by GitHub
commit 4d7edef0ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 2913 additions and 1554 deletions

16
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,16 @@
FROM ubuntu:focal
ARG DEBIAN_FRONTEND=noninteractive
ENV ANDROID_HOME=/usr/lib/android-sdk
RUN apt-get update -y
RUN apt-get install -y unzip wget openjdk-8-jdk vim
RUN wget https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip -O /tmp/commandlinetools.zip
RUN cd /tmp && unzip commandlinetools.zip
RUN mkdir -p /usr/lib/android-sdk/cmdline-tools/
RUN cd /tmp/ && mv cmdline-tools/ latest/ && mv latest/ /usr/lib/android-sdk/cmdline-tools/
RUN mkdir /usr/lib/android-sdk/licenses/
RUN chmod -R 755 /usr/lib/android-sdk/
RUN mkdir -p $HOME/.gradle
RUN echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > $HOME/.gradle/gradle.properties

5
.devcontainer/README.md Normal file
View file

@ -0,0 +1,5 @@
# Instructions
1. Start a DevContainer either on GitHub Codespaces or locally in VSCode
2. Accept all licenses by running `yes | /usr/lib/android-sdk/cmdline-tools/latest/bin/sdkmanager --licenses`
3. You can now build the app using `./gradlew clean build`

View file

@ -0,0 +1,3 @@
ANDROID_HOME=/usr/lib/android-sdk
JAVA_OPTS="-Xmx8192M"
GRADLE_OPTS="-Dorg.gradle.daemon=true"

View file

@ -0,0 +1,4 @@
{
"name": "NextcloudTalkAndroid",
"dockerFile": "Dockerfile",
}

2
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,2 @@
# You can add one username per supported platform and one custom link
custom: https://nextcloud.com/include/

View file

@ -1,11 +1,16 @@
version: 2
updates:
- package-ecosystem: gradle
directory: "/"
schedule:
interval: daily
time: "03:00"
timezone: Europe/Paris
open-pull-requests-limit: 10
labels:
- 3. to review
- package-ecosystem: "github-actions"
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
- package-ecosystem: gradle
directory: "/"
schedule:
interval: daily
time: "03:00"
timezone: Europe/Paris
open-pull-requests-limit: 10
labels:
- 3. to review
- dependencies

30
.github/workflows/assembleFlavors.yml vendored Normal file
View file

@ -0,0 +1,30 @@
name: "Assemble"
on:
pull_request:
branches: [ master, stable-* ]
jobs:
flavor:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
flavor: [ Generic, Gplay ]
steps:
- uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build ${{ matrix.flavor }}
run: |
echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" >> gradle.properties
./gradlew assemble${{ matrix.flavor }}
- name: Archive apk
uses: actions/upload-artifact@v2
if: ${{ always() }}
with:
name: Nextcloud-APK
path: app/build/outputs/apk/**/**/*.apk
retention-days: 5

View file

@ -0,0 +1,13 @@
name: Auto approve
on:
pull_request_target:
branches: [ master, stable-* ]
jobs:
auto-approve:
runs-on: ubuntu-latest
steps:
- uses: hmarr/auto-approve-action@v2.0.0
if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]'
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"

21
.github/workflows/check.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: Check
on:
pull_request:
branches: [ master, stable-* ]
jobs:
check:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
task: [ detekt, ktlint ]
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Check ${{ matrix.task }}
run: ./gradlew ${{ matrix.task }}

View file

@ -0,0 +1,13 @@
name: "Validate Gradle Wrapper"
on:
pull_request:
branches: [ master, stable-* ]
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1

29
.github/workflows/qa.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: "QA"
on:
pull_request:
branches: [ master, stable-* ]
jobs:
qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Build QA
env:
KS_PASS: ${{ secrets.KS_PASS }}
KEY_PASS: ${{ secrets.KEY_PASS }}
LOG_USERNAME: ${{ secrets.LOG_USERNAME }}
LOG_PASSWORD: ${{ secrets.LOG_PASSWORD }}
run: |
mkdir -p $HOME/.gradle
echo "org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError" > $HOME/.gradle/gradle.properties
sed -i "/qa/,/\}/ s/versionCode .*/versionCode ${{github.event.number}} /" app/build.gradle
sed -i "/qa/,/\}/ s/versionName .*/versionName \"${{github.event.number}}\"/" app/build.gradle
./gradlew assembleQaDebug
$(find /usr/local/lib/android/sdk/build-tools/*/apksigner | sort | tail -n1) sign --ks-pass pass:$KS_PASS --key-pass pass:$KEY_PASS --ks-key-alias key0 --ks scripts/QA_keystore.jks app/build/outputs/apk/qa/debug/app-qa-*.apk
sudo scripts/uploadArtifact.sh $LOG_USERNAME $LOG_PASSWORD ${{github.event.number}} ${{github.event.number}} ${{ secrets.GITHUB_TOKEN }}

19
.github/workflows/stale.yml vendored Normal file
View file

@ -0,0 +1,19 @@
name: 'Close stale issues'
on:
schedule:
- cron: '* */2 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
days-before-stale: 28
days-before-close: 14
days-before-pr-close: -1
only-labels: 'bug,needs info/discussion'
stale-issue-message: 'This bug report did not receive an update in the last 4 weeks.
Please take a look again and update the issue with new details,
otherwise the issue will be automatically closed in 2 weeks. Thank you!'
exempt-all-pr-milestones: true

View file

@ -0,0 +1,219 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="99" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
<option name="RIGHT_MARGIN" value="120" />
<option name="WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN" value="true" />
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<MarkdownNavigatorCodeStyleSettings>
<option name="RIGHT_MARGIN" value="120" />
</MarkdownNavigatorCodeStyleSettings>
<XML>
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="JAVA">
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="true" />
<option name="ALIGN_MULTILINE_METHOD_BRACKETS" value="true" />
<option name="WRAP_COMMENTS" value="true" />
<option name="IF_BRACE_FORCE" value="3" />
<option name="DOWHILE_BRACE_FORCE" value="3" />
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<option name="FIELD_ANNOTATION_WRAP" value="0" />
</codeStyleSettings>
<codeStyleSettings language="Markdown">
<option name="RIGHT_MARGIN" value="120" />
</codeStyleSettings>
<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" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View file

@ -0,0 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="ktlint" />
<inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
</profile>
</component>

View file

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="ktlint" />
<version value="1.0" />
</settings>
</component>

View file

@ -23,6 +23,11 @@ apply plugin: 'kotlin-android'
apply plugin: 'findbugs'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'io.gitlab.arturbosch.detekt'
configurations {
ktlint
}
def taskRequest = getGradle().getStartParameter().getTaskRequests().toString()
if (taskRequest.contains("Gplay") || taskRequest.contains("findbugs") || taskRequest.contains("lint")) {
@ -33,7 +38,6 @@ android {
compileSdkVersion 29
buildToolsVersion '28.0.3'
defaultConfig {
applicationId "com.nextcloud.talk2"
minSdkVersion 21
targetSdkVersion 29
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -49,9 +53,20 @@ android {
productFlavors {
// used for f-droid
generic
gplay
generic {
applicationId 'com.nextcloud.talk2'
dimension "default"
}
gplay {
applicationId 'com.nextcloud.talk2'
dimension "default"
}
qa {
applicationId "com.nextcloud.talk2.qa"
dimension "default"
versionCode 1
versionName "1"
}
}
// Enabling multidex support.
@ -86,6 +101,7 @@ android {
}
packagingOptions {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/rxjava.properties'
@ -161,6 +177,7 @@ dependencies {
implementation ('com.gitlab.bitfireAT:dav4jvm:f2078bc846', {
exclude group: 'org.ogce', module: 'xpp3' // Android comes with its own XmlPullParser
})
ktlint "com.pinterest:ktlint:0.41.0"
implementation 'org.conscrypt:conscrypt-android:2.5.1'
@ -272,6 +289,30 @@ dependencies {
findbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:7.4.6'
}
task ktlint(type: JavaExec, group: "verification") {
description = "Check Kotlin code style."
main = "com.pinterest.ktlint.Main"
classpath = configurations.ktlint
args "--reporter=plain", "--reporter=plain,output=${buildDir}/ktlint.txt,src/**/*.kt"
}
task ktlintFormat(type: JavaExec, group: "formatting") {
description = "Fix Kotlin code style deviations."
main = "com.pinterest.ktlint.Main"
classpath = configurations.ktlint
args "-F", "src/**/*.kt"
}
detekt {
reports {
xml {
enabled = false
}
}
config = files("../detekt.yml")
input = files("src/")
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"

View file

@ -104,7 +104,6 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
@Inject
var eventBus: EventBus? = null
override fun onCreate() {
super.onCreate()
sharedApplication!!.componentApplication.inject(this)
@ -148,18 +147,26 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
val pushUtils = PushUtils()
val privateKey = pushUtils.readKeyFromFile(false) as PrivateKey
try {
signatureVerification = pushUtils.verifySignature(base64DecodedSignature,
base64DecodedSubject)
signatureVerification = pushUtils.verifySignature(
base64DecodedSignature,
base64DecodedSubject
)
if (signatureVerification!!.signatureValid) {
val cipher = Cipher.getInstance("RSA/None/PKCS1Padding")
cipher.init(Cipher.DECRYPT_MODE, privateKey)
val decryptedSubject = cipher.doFinal(base64DecodedSubject)
decryptedPushMessage = LoganSquare.parse(String(decryptedSubject),
DecryptedPushMessage::class.java)
decryptedPushMessage = LoganSquare.parse(
String(decryptedSubject),
DecryptedPushMessage::class.java
)
decryptedPushMessage?.apply {
timestamp = System.currentTimeMillis()
if (delete) {
cancelExistingNotificationWithId(applicationContext, signatureVerification!!.userEntity, notificationId)
cancelExistingNotificationWithId(
applicationContext,
signatureVerification!!.userEntity,
notificationId
)
} else if (deleteAll) {
cancelAllNotificationsForAccount(applicationContext, signatureVerification!!.userEntity)
} else if (type == "call") {
@ -171,39 +178,66 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
fullScreenIntent.putExtras(bundle)
fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val fullScreenPendingIntent = PendingIntent.getActivity(this@MagicFirebaseMessagingService, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)
val fullScreenPendingIntent = PendingIntent.getActivity(
this@MagicFirebaseMessagingService,
0,
fullScreenIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
val audioAttributesBuilder = AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
val audioAttributesBuilder =
AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
audioAttributesBuilder.setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST)
val ringtonePreferencesString: String? = appPreferences!!.callRingtoneUri
val soundUri = if (TextUtils.isEmpty(ringtonePreferencesString)) {
Uri.parse("android.resource://" + applicationContext.packageName +
"/raw/librem_by_feandesign_call")
Uri.parse(
"android.resource://" + applicationContext.packageName +
"/raw/librem_by_feandesign_call"
)
} else {
try {
val ringtoneSettings = LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java)
val ringtoneSettings =
LoganSquare.parse(ringtonePreferencesString, RingtoneSettings::class.java)
ringtoneSettings.ringtoneUri
} catch (exception: IOException) {
Uri.parse("android.resource://" + applicationContext.packageName + "/raw/librem_by_feandesign_call")
}
}
val notificationChannelId = NotificationUtils.getNotificationChannelId(applicationContext.resources
.getString(R.string.nc_notification_channel_calls), applicationContext.resources
.getString(R.string.nc_notification_channel_calls_description), true,
NotificationManagerCompat.IMPORTANCE_HIGH, soundUri!!, audioAttributesBuilder.build(), null, false)
val notificationChannelId = NotificationUtils.getNotificationChannelId(
applicationContext.resources
.getString(R.string.nc_notification_channel_calls),
applicationContext.resources
.getString(R.string.nc_notification_channel_calls_description),
true,
NotificationManagerCompat.IMPORTANCE_HIGH,
soundUri!!,
audioAttributesBuilder.build(),
null,
false
)
createNotificationChannel(applicationContext!!,
notificationChannelId, applicationContext.resources
.getString(R.string.nc_notification_channel_calls), applicationContext.resources
.getString(R.string.nc_notification_channel_calls_description), true,
NotificationManagerCompat.IMPORTANCE_HIGH, soundUri, audioAttributesBuilder.build(), null, false)
createNotificationChannel(
applicationContext!!,
notificationChannelId,
applicationContext.resources
.getString(R.string.nc_notification_channel_calls),
applicationContext.resources
.getString(R.string.nc_notification_channel_calls_description),
true,
NotificationManagerCompat.IMPORTANCE_HIGH,
soundUri,
audioAttributesBuilder.build(),
null,
false
)
val uri = Uri.parse(signatureVerification!!.userEntity.baseUrl)
val baseUrl = uri.host
val notification = NotificationCompat.Builder(this@MagicFirebaseMessagingService, notificationChannelId)
val notification =
NotificationCompat.Builder(this@MagicFirebaseMessagingService, notificationChannelId)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setSmallIcon(R.drawable.ic_call_black_24dp)
@ -213,7 +247,7 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
.setContentTitle(EmojiCompat.get().process(decryptedPushMessage!!.subject))
.setAutoCancel(true)
.setOngoing(true)
//.setTimeoutAfter(45000L)
// .setTimeoutAfter(45000L)
.setContentIntent(fullScreenPendingIntent)
.setFullScreenIntent(fullScreenPendingIntent, true)
.setSound(soundUri)
@ -224,10 +258,12 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
startForeground(decryptedPushMessage!!.timestamp.toInt(), notification)
} else {
val messageData = Data.Builder()
.putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject)
.putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature)
.putString(BundleKeys.KEY_NOTIFICATION_SUBJECT, subject)
.putString(BundleKeys.KEY_NOTIFICATION_SIGNATURE, signature)
.build()
val pushNotificationWork =
OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData)
.build()
val pushNotificationWork = OneTimeWorkRequest.Builder(NotificationWorker::class.java).setInputData(messageData).build()
WorkManager.getInstance().enqueue(pushNotificationWork)
}
}
@ -244,47 +280,59 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
}
}
private fun checkIfCallIsActive(signatureVerification: SignatureVerification, decryptedPushMessage: DecryptedPushMessage) {
val ncApi = retrofit!!.newBuilder().client(okHttpClient!!.newBuilder().cookieJar(JavaNetCookieJar(CookieManager())).build()).build().create(NcApi::class.java)
private fun checkIfCallIsActive(
signatureVerification: SignatureVerification,
decryptedPushMessage: DecryptedPushMessage
) {
val ncApi = retrofit!!.newBuilder()
.client(okHttpClient!!.newBuilder().cookieJar(JavaNetCookieJar(CookieManager())).build()).build()
.create(NcApi::class.java)
var hasParticipantsInCall = false
var inCallOnDifferentDevice = false
ncApi.getPeersForCall(ApiUtils.getCredentials(signatureVerification.userEntity.username, signatureVerification.userEntity.token),
ApiUtils.getUrlForCall(signatureVerification.userEntity.baseUrl,
decryptedPushMessage.id))
.takeWhile {
isServiceInForeground
ncApi.getPeersForCall(
ApiUtils.getCredentials(signatureVerification.userEntity.username, signatureVerification.userEntity.token),
ApiUtils.getUrlForCall(
signatureVerification.userEntity.baseUrl,
decryptedPushMessage.id
)
)
.takeWhile {
isServiceInForeground
}
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<ParticipantsOverall> {
override fun onSubscribe(d: Disposable) {
}
.subscribeOn(Schedulers.io())
.subscribe(object : Observer<ParticipantsOverall> {
override fun onSubscribe(d: Disposable) {
}
override fun onNext(participantsOverall: ParticipantsOverall) {
val participantList: List<Participant> = participantsOverall.ocs.data
hasParticipantsInCall = participantList.isNotEmpty()
if (!hasParticipantsInCall) {
for (participant in participantList) {
if (participant.userId == signatureVerification.userEntity.userId) {
inCallOnDifferentDevice = true
break
}
override fun onNext(participantsOverall: ParticipantsOverall) {
val participantList: List<Participant> = participantsOverall.ocs.data
hasParticipantsInCall = participantList.isNotEmpty()
if (!hasParticipantsInCall) {
for (participant in participantList) {
if (participant.userId == signatureVerification.userEntity.userId) {
inCallOnDifferentDevice = true
break
}
}
}
if (!hasParticipantsInCall || inCallOnDifferentDevice) {
stopForeground(true)
handler.removeCallbacksAndMessages(null)
} else if (isServiceInForeground) {
handler.postDelayed({
if (!hasParticipantsInCall || inCallOnDifferentDevice) {
stopForeground(true)
handler.removeCallbacksAndMessages(null)
} else if (isServiceInForeground) {
handler.postDelayed(
{
checkIfCallIsActive(signatureVerification, decryptedPushMessage)
}, 5000)
}
},
5000
)
}
}
override fun onError(e: Throwable) {}
override fun onComplete() {
}
})
override fun onError(e: Throwable) {}
override fun onComplete() {
}
})
}
}
}

View file

@ -76,8 +76,11 @@ open class BaseActivity : AppCompatActivity() {
}
}
fun showCertificateDialog(cert: X509Certificate, magicTrustManager: MagicTrustManager,
sslErrorHandler: SslErrorHandler?) {
fun showCertificateDialog(
cert: X509Certificate,
magicTrustManager: MagicTrustManager,
sslErrorHandler: SslErrorHandler?
) {
val formatter = DateFormat.getDateInstance(DateFormat.LONG)
val validFrom = formatter.format(cert.notBefore)
val validUntil = formatter.format(cert.notAfter)
@ -101,30 +104,30 @@ open class BaseActivity : AppCompatActivity() {
issuedFor = cert.subjectDN.name
}
@SuppressLint("StringFormatMatches") val dialogText = String.format(resources
@SuppressLint("StringFormatMatches") val dialogText = String.format(
resources
.getString(R.string.nc_certificate_dialog_text),
issuedBy, issuedFor, validFrom, validUntil)
issuedBy, issuedFor, validFrom, validUntil
)
LovelyStandardDialog(this)
.setTopColorRes(R.color.nc_darkRed)
.setNegativeButtonColorRes(R.color.nc_darkRed)
.setPositiveButtonColorRes(R.color.colorPrimary)
.setIcon(R.drawable.ic_security_white_24dp)
.setTitle(R.string.nc_certificate_dialog_title)
.setMessage(dialogText)
.setPositiveButton(R.string.nc_yes) { v ->
magicTrustManager.addCertInTrustStore(cert)
sslErrorHandler?.proceed()
}
.setNegativeButton(R.string.nc_no) { view1 ->
sslErrorHandler?.cancel()
}
.show()
.setTopColorRes(R.color.nc_darkRed)
.setNegativeButtonColorRes(R.color.nc_darkRed)
.setPositiveButtonColorRes(R.color.colorPrimary)
.setIcon(R.drawable.ic_security_white_24dp)
.setTitle(R.string.nc_certificate_dialog_title)
.setMessage(dialogText)
.setPositiveButton(R.string.nc_yes) { v ->
magicTrustManager.addCertInTrustStore(cert)
sslErrorHandler?.proceed()
}
.setNegativeButton(R.string.nc_no) { view1 ->
sslErrorHandler?.cancel()
}
.show()
} catch (e: CertificateParsingException) {
Log.d(TAG, "Failed to parse the certificate")
}
}
@Subscribe(threadMode = ThreadMode.MAIN)

View file

@ -38,7 +38,6 @@ import pl.droidsonroids.gif.GifDrawable
import pl.droidsonroids.gif.GifImageView
import java.io.File
class FullScreenImageActivity : AppCompatActivity() {
private lateinit var path: String
@ -55,9 +54,11 @@ class FullScreenImageActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.share) {
val shareUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID,
File(path))
val shareUri = FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID,
File(path)
)
val shareIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
@ -78,19 +79,19 @@ class FullScreenImageActivity : AppCompatActivity() {
setContentView(R.layout.activity_full_screen_image)
setSupportActionBar(findViewById(R.id.imageview_toolbar))
supportActionBar?.setDisplayShowTitleEnabled(false);
supportActionBar?.setDisplayShowTitleEnabled(false)
imageWrapperView = findViewById(R.id.image_wrapper_view)
photoView = findViewById(R.id.photo_view)
gifView = findViewById(R.id.gif_view)
photoView.setOnPhotoTapListener{ view, x, y ->
photoView.setOnPhotoTapListener { view, x, y ->
toggleFullscreen()
}
photoView.setOnOutsidePhotoTapListener{
photoView.setOnOutsidePhotoTapListener {
toggleFullscreen()
}
gifView.setOnClickListener{
gifView.setOnClickListener {
toggleFullscreen()
}
@ -115,29 +116,33 @@ class FullScreenImageActivity : AppCompatActivity() {
}
}
private fun toggleFullscreen(){
showFullscreen = !showFullscreen;
if (showFullscreen){
private fun toggleFullscreen() {
showFullscreen = !showFullscreen
if (showFullscreen) {
hideSystemUI()
supportActionBar?.hide()
} else{
} else {
showSystemUI()
supportActionBar?.show()
}
}
private fun hideSystemUI() {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
or View.SYSTEM_UI_FLAG_FULLSCREEN
)
}
private fun showSystemUI() {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
}
}
}

View file

@ -22,7 +22,6 @@ package com.nextcloud.talk.activities
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
@ -33,7 +32,6 @@ import autodagger.AutoInjector
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.nextcloud.talk.BuildConfig
import com.nextcloud.talk.R
@ -54,9 +52,11 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.share) {
val shareUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID,
File(path))
val shareUri = FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID,
File(path)
)
val shareIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
@ -82,11 +82,11 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener {
setContentView(R.layout.activity_full_screen_media)
setSupportActionBar(findViewById(R.id.mediaview_toolbar))
supportActionBar?.setDisplayShowTitleEnabled(false);
supportActionBar?.setDisplayShowTitleEnabled(false)
playerView = findViewById(R.id.player_view)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
playerView.showController()
if (isAudioOnly) {
@ -121,7 +121,7 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener {
private fun initializePlayer() {
player = SimpleExoPlayer.Builder(applicationContext).build()
playerView.player = player;
playerView.player = player
player.playWhenReady = true
player.addListener(this)
}
@ -131,17 +131,21 @@ class FullScreenMediaActivity : AppCompatActivity(), Player.EventListener {
}
private fun hideSystemUI() {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN)
or View.SYSTEM_UI_FLAG_FULLSCREEN
)
}
private fun showSystemUI() {
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
}
}

View file

@ -34,7 +34,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import io.noties.markwon.Markwon
import java.io.File
@AutoInjector(NextcloudTalkApplication::class)
class FullScreenTextViewerActivity : AppCompatActivity() {
@ -48,9 +47,11 @@ class FullScreenTextViewerActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.share) {
val shareUri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID,
File(path))
val shareUri = FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID,
File(path)
)
val shareIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
@ -71,8 +72,7 @@ class FullScreenTextViewerActivity : AppCompatActivity() {
setContentView(R.layout.activity_full_screen_text)
setSupportActionBar(findViewById(R.id.textview_toolbar))
supportActionBar?.setDisplayShowTitleEnabled(false);
supportActionBar?.setDisplayShowTitleEnabled(false)
textView = findViewById(R.id.text_view)
val fileName = intent.getStringExtra("FILE_NAME")
@ -81,13 +81,12 @@ class FullScreenTextViewerActivity : AppCompatActivity() {
var text = readFile(path)
if (isMarkdown) {
val markwon = Markwon.create(applicationContext);
markwon.setMarkdown(textView, text);
val markwon = Markwon.create(applicationContext)
markwon.setMarkdown(textView, text)
} else {
textView.text = text
}
}
private fun readFile(fileName: String) = File(fileName).inputStream().readBytes().toString(Charsets.UTF_8)
}

View file

@ -48,7 +48,7 @@ class MagicCallActivity : BaseActivity() {
@BindView(R.id.controller_container)
lateinit var container: ViewGroup
@BindView(R.id.chatControllerView)
lateinit var chatContainer: ViewGroup
@ -60,10 +60,12 @@ class MagicCallActivity : BaseActivity() {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
requestWindowFeature(Window.FEATURE_NO_TITLE)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN or
window.addFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN or
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
)
window.decorView.systemUiVisibility = systemUiVisibility
setContentView(R.layout.activity_magic_call)
@ -74,26 +76,32 @@ class MagicCallActivity : BaseActivity() {
if (!router!!.hasRootController()) {
if (intent.getBooleanExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)) {
router!!.setRoot(RouterTransaction.with(CallNotificationController(intent.extras))
router!!.setRoot(
RouterTransaction.with(CallNotificationController(intent.extras))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
} else {
router!!.setRoot(RouterTransaction.with(CallController(intent.extras))
router!!.setRoot(
RouterTransaction.with(CallController(intent.extras))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
}
}
val extras = intent.extras ?: Bundle()
extras.putBoolean("showToggleChat", true)
chatController = ChatController(extras)
chatRouter = Conductor.attachRouter(this, chatContainer, savedInstanceState)
chatRouter!!.setRoot(RouterTransaction.with(chatController)
chatRouter!!.setRoot(
RouterTransaction.with(chatController)
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
}
fun showChat() {
chatContainer.visibility = View.VISIBLE
container.visibility = View.GONE

View file

@ -79,23 +79,31 @@ class MainActivity : BaseActivity(), ActionBarProvider {
@BindView(R.id.appBar)
lateinit var appBar: AppBarLayout
@BindView(R.id.toolbar)
lateinit var toolbar: MaterialToolbar
@BindView(R.id.home_toolbar)
lateinit var searchCardView: MaterialCardView
@BindView(R.id.search_text)
lateinit var searchInputText: MaterialTextView
@BindView(R.id.switch_account_button)
lateinit var settingsButton: MaterialButton
@BindView(R.id.controller_container)
lateinit var container: ViewGroup
@Inject
lateinit var userUtils: UserUtils
@Inject
lateinit var dataStore: ReactiveEntityStore<Persistable>
@Inject
lateinit var sqlCipherDatabaseSource: SqlCipherDatabaseSource
@Inject
lateinit var ncApi: NcApi
@ -124,39 +132,53 @@ class MainActivity : BaseActivity(), ActionBarProvider {
if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
if (!router!!.hasRootController()) {
router!!.setRoot(RouterTransaction.with(ConversationsListController())
router!!.setRoot(
RouterTransaction.with(ConversationsListController())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
}
onNewIntent(intent)
} else if (!router!!.hasRootController()) {
if (hasDb) {
if (userUtils.anyUserExists()) {
router!!.setRoot(RouterTransaction.with(ConversationsListController())
router!!.setRoot(
RouterTransaction.with(ConversationsListController())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
} else {
if (!TextUtils.isEmpty(resources.getString(R.string.weblogin_url))) {
router!!.pushController(RouterTransaction.with(
WebViewLoginController(resources.getString(R.string.weblogin_url), false))
router!!.pushController(
RouterTransaction.with(
WebViewLoginController(resources.getString(R.string.weblogin_url), false)
)
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
} else {
router!!.setRoot(RouterTransaction.with(ServerSelectionController())
router!!.setRoot(
RouterTransaction.with(ServerSelectionController())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
}
}
} else {
if (!TextUtils.isEmpty(resources.getString(R.string.weblogin_url))) {
router!!.pushController(RouterTransaction.with(
WebViewLoginController(resources.getString(R.string.weblogin_url), false))
router!!.pushController(
RouterTransaction.with(
WebViewLoginController(resources.getString(R.string.weblogin_url), false)
)
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
} else {
router!!.setRoot(RouterTransaction.with(ServerSelectionController())
router!!.setRoot(
RouterTransaction.with(ServerSelectionController())
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
}
}
}
@ -167,7 +189,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
checkIfWeAreSecure()
}
handleActionFromContact(intent)
}
@ -182,7 +204,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
// userId @ server
userId = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DATA1))
}
cursor.close()
}
@ -193,65 +215,82 @@ class MainActivity : BaseActivity(), ActionBarProvider {
if (userUtils.currentUser?.baseUrl?.endsWith(baseUrl) == true) {
startConversation(user)
} else {
Snackbar.make(container, R.string.nc_phone_book_integration_account_not_found, Snackbar
.LENGTH_LONG).show()
Snackbar.make(
container, R.string.nc_phone_book_integration_account_not_found,
Snackbar.LENGTH_LONG
).show()
}
}
}
}
}
private fun startConversation(userId: String) {
val roomType = "1"
val currentUser = userUtils.currentUser ?: return
val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(currentUser.baseUrl, roomType,
userId, null)
ncApi.createRoom(credentials,
retrofitBucket.url, retrofitBucket.queryMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(roomOverall: RoomOverall) {
val conversationIntent = Intent(context, MagicCallActivity::class.java)
val bundle = Bundle()
bundle.putParcelable(KEY_USER_ENTITY, currentUser)
bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs.data.token)
bundle.putString(KEY_ROOM_ID, roomOverall.ocs.data.roomId)
if (currentUser.hasSpreedFeatureCapability("chat-v2")) {
ncApi.getRoom(credentials,
ApiUtils.getRoom(currentUser.baseUrl,
roomOverall.ocs.data.token))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(roomOverall: RoomOverall) {
bundle.putParcelable(KEY_ACTIVE_CONVERSATION,
Parcels.wrap(roomOverall.ocs.data))
remapChatController(router!!, currentUser.id,
roomOverall.ocs.data.token, bundle, true)
}
val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
currentUser.baseUrl, roomType,
userId, null
)
ncApi.createRoom(
credentials,
retrofitBucket.url, retrofitBucket.queryMap
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(roomOverall: RoomOverall) {
val conversationIntent = Intent(context, MagicCallActivity::class.java)
val bundle = Bundle()
bundle.putParcelable(KEY_USER_ENTITY, currentUser)
bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs.data.token)
bundle.putString(KEY_ROOM_ID, roomOverall.ocs.data.roomId)
if (currentUser.hasSpreedFeatureCapability("chat-v2")) {
ncApi.getRoom(
credentials,
ApiUtils.getRoom(
currentUser.baseUrl,
roomOverall.ocs.data.token
)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {}
override fun onNext(roomOverall: RoomOverall) {
bundle.putParcelable(
KEY_ACTIVE_CONVERSATION,
Parcels.wrap(roomOverall.ocs.data)
)
remapChatController(
router!!, currentUser.id,
roomOverall.ocs.data.token, bundle, true
)
}
override fun onError(e: Throwable) {}
override fun onComplete() {}
})
} else {
conversationIntent.putExtras(bundle)
startActivity(conversationIntent)
Handler().postDelayed({
override fun onError(e: Throwable) {}
override fun onComplete() {}
})
} else {
conversationIntent.putExtras(bundle)
startActivity(conversationIntent)
Handler().postDelayed(
{
if (!isDestroyed) {
router!!.popCurrentController()
}
}, 100)
}
},
100
)
}
}
override fun onError(e: Throwable) {}
override fun onComplete() {}
})
override fun onError(e: Throwable) {}
override fun onComplete() {}
})
}
@RequiresApi(api = Build.VERSION_CODES.M)
@ -260,16 +299,17 @@ class MainActivity : BaseActivity(), ActionBarProvider {
if (keyguardManager.isKeyguardSecure && appPreferences.isScreenLocked) {
if (!SecurityUtils.checkIfWeAreAuthenticated(appPreferences.screenLockTimeout)) {
if (router != null && router!!.getControllerWithTag(LockedController.TAG) == null) {
router!!.pushController(RouterTransaction.with(LockedController())
router!!.pushController(
RouterTransaction.with(LockedController())
.pushChangeHandler(VerticalChangeHandler())
.popChangeHandler(VerticalChangeHandler())
.tag(LockedController.TAG))
.tag(LockedController.TAG)
)
}
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
@ -277,12 +317,16 @@ class MainActivity : BaseActivity(), ActionBarProvider {
if (intent.hasExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
if (intent.getBooleanExtra(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false)) {
router!!.pushController(RouterTransaction.with(CallNotificationController(intent.extras))
router!!.pushController(
RouterTransaction.with(CallNotificationController(intent.extras))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()))
.popChangeHandler(HorizontalChangeHandler())
)
} else {
ConductorRemapping.remapChatController(router!!, intent.getLongExtra(BundleKeys.KEY_INTERNAL_USER_ID, -1),
intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN), intent.extras!!, false)
ConductorRemapping.remapChatController(
router!!, intent.getLongExtra(BundleKeys.KEY_INTERNAL_USER_ID, -1),
intent.getStringExtra(BundleKeys.KEY_ROOM_TOKEN), intent.extras!!, false
)
}
}
}

View file

@ -39,7 +39,6 @@ import autodagger.AutoInjector
import butterknife.BindView
import butterknife.ButterKnife
import coil.load
import coil.transform.CircleCropTransformation
import com.amulyakhare.textdrawable.TextDrawable
import com.facebook.drawee.view.SimpleDraweeView
import com.nextcloud.talk.R
@ -51,7 +50,6 @@ import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.TextMatchers
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import com.stfalcon.chatkit.utils.DateFormatter
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -104,8 +102,8 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
init {
ButterKnife.bind(
this,
itemView
this,
itemView
)
}
@ -131,13 +129,13 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
messageUserAvatarView?.setImageDrawable(DisplayUtils.getRoundedDrawable(layerDrawable))
} else if (message.actorType == "bots") {
val drawable = TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRound(
">",
context!!.resources.getColor(R.color.black)
)
.beginConfig()
.bold()
.endConfig()
.buildRound(
">",
context!!.resources.getColor(R.color.black)
)
messageUserAvatarView!!.visibility = View.VISIBLE
messageUserAvatarView?.setImageDrawable(drawable)
}
@ -165,9 +163,9 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
}
val bubbleDrawable = DisplayUtils.getMessageSelector(
bgBubbleColor,
resources.getColor(R.color.transparent),
bgBubbleColor, bubbleResource
bgBubbleColor,
resources.getColor(R.color.transparent),
bgBubbleColor, bubbleResource
)
ViewCompat.setBackground(bubble, bubbleDrawable)
@ -187,23 +185,23 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
if (individualHashMap["type"] == "user" || individualHashMap["type"] == "guest" || individualHashMap["type"] == "call") {
if (individualHashMap["id"] == message.activeUser!!.userId) {
messageString = DisplayUtils.searchAndReplaceWithMentionSpan(
messageText!!.context,
messageString,
individualHashMap["id"]!!,
individualHashMap["name"]!!,
individualHashMap["type"]!!,
message.activeUser!!,
R.xml.chip_you
messageText!!.context,
messageString,
individualHashMap["id"]!!,
individualHashMap["name"]!!,
individualHashMap["type"]!!,
message.activeUser!!,
R.xml.chip_you
)
} else {
messageString = DisplayUtils.searchAndReplaceWithMentionSpan(
messageText!!.context,
messageString,
individualHashMap["id"]!!,
individualHashMap["name"]!!,
individualHashMap["type"]!!,
message.activeUser!!,
R.xml.chip_others
messageText!!.context,
messageString,
individualHashMap["id"]!!,
individualHashMap["name"]!!,
individualHashMap["type"]!!,
message.activeUser!!,
R.xml.chip_others
)
}
} else if (individualHashMap["type"] == "file") {
@ -231,18 +229,21 @@ class MagicIncomingTextMessageViewHolder(incomingView: View) : MessageHolders
parentChatMessage.imageUrl?.let {
quotedMessagePreview?.visibility = View.VISIBLE
quotedMessagePreview?.load(it) {
addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
)
}
} ?: run {
quotedMessagePreview?.visibility = View.GONE
}
quotedUserName?.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
?: context!!.getText(R.string.nc_nick_guest)
quotedMessage?.text = parentChatMessage.text
quotedUserName?.setTextColor(context!!.resources.getColor(R.color.textColorMaxContrast))
if(parentChatMessage.actorId?.equals(message.activeUser.userId) == true) {
if (parentChatMessage.actorId?.equals(message.activeUser.userId) == true) {
quoteColoredView?.setBackgroundResource(R.color.colorPrimary)
} else {
quoteColoredView?.setBackgroundResource(R.color.textColorMaxContrast)

View file

@ -36,7 +36,6 @@ import autodagger.AutoInjector
import butterknife.BindView
import butterknife.ButterKnife
import coil.load
import coil.transform.CircleCropTransformation
import com.google.android.flexbox.FlexboxLayout
import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
@ -48,8 +47,7 @@ import com.nextcloud.talk.utils.DisplayUtils.getMessageSelector
import com.nextcloud.talk.utils.DisplayUtils.searchAndReplaceWithMentionSpan
import com.nextcloud.talk.utils.TextMatchers
import com.stfalcon.chatkit.messages.MessageHolders.OutcomingTextMessageViewHolder
import com.stfalcon.chatkit.utils.DateFormatter
import java.util.*
import java.util.HashMap
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -57,6 +55,7 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
@JvmField
@BindView(R.id.messageText)
var messageText: EmojiTextView? = null
@JvmField
@BindView(R.id.messageTime)
var messageTimeView: TextView? = null
@ -104,20 +103,26 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
for (key in messageParameters.keys) {
val individualHashMap: HashMap<String, String>? = message.messageParameters[key]
if (individualHashMap != null) {
if (individualHashMap["type"] == "user" || (individualHashMap["type"]
== "guest") || individualHashMap["type"] == "call") {
messageString = searchAndReplaceWithMentionSpan(messageText!!.context,
messageString,
individualHashMap["id"]!!,
individualHashMap["name"]!!,
individualHashMap["type"]!!,
message.activeUser,
R.xml.chip_others)
if (individualHashMap["type"] == "user" || (
individualHashMap["type"] == "guest"
) || individualHashMap["type"] == "call"
) {
messageString = searchAndReplaceWithMentionSpan(
messageText!!.context,
messageString,
individualHashMap["id"]!!,
individualHashMap["name"]!!,
individualHashMap["type"]!!,
message.activeUser,
R.xml.chip_others
)
} else if (individualHashMap["type"] == "file") {
realView.setOnClickListener(View.OnClickListener { v: View? ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"]))
context!!.startActivity(browserIntent)
})
realView.setOnClickListener(
View.OnClickListener { v: View? ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"]))
context!!.startActivity(browserIntent)
}
)
}
}
}
@ -135,17 +140,19 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
}
if (message.isGrouped) {
val bubbleDrawable = getMessageSelector(
bgBubbleColor,
resources.getColor(R.color.transparent),
bgBubbleColor,
R.drawable.shape_grouped_outcoming_message)
bgBubbleColor,
resources.getColor(R.color.transparent),
bgBubbleColor,
R.drawable.shape_grouped_outcoming_message
)
ViewCompat.setBackground(bubble, bubbleDrawable)
} else {
val bubbleDrawable = getMessageSelector(
bgBubbleColor,
resources.getColor(R.color.transparent),
bgBubbleColor,
R.drawable.shape_outcoming_message)
bgBubbleColor,
resources.getColor(R.color.transparent),
bgBubbleColor,
R.drawable.shape_outcoming_message
)
ViewCompat.setBackground(bubble, bubbleDrawable)
}
messageText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
@ -160,13 +167,16 @@ class MagicOutcomingTextMessageViewHolder(itemView: View) : OutcomingTextMessage
parentChatMessage.imageUrl?.let {
quotedMessagePreview?.visibility = View.VISIBLE
quotedMessagePreview?.load(it) {
addHeader("Authorization", ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token))
addHeader(
"Authorization",
ApiUtils.getCredentials(message.activeUser.username, message.activeUser.token)
)
}
} ?: run {
quotedMessagePreview?.visibility = View.GONE
}
quotedUserName?.text = parentChatMessage.actorDisplayName
?: context!!.getText(R.string.nc_nick_guest)
?: context!!.getText(R.string.nc_nick_guest)
quotedMessage?.text = parentChatMessage.text
quotedMessage?.setTextColor(context!!.resources.getColor(R.color.nc_outcoming_text_default))
quotedUserName?.setTextColor(context!!.resources.getColor(R.color.nc_grey))

View file

@ -90,6 +90,7 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
@Inject
lateinit var appPreferences: AppPreferences
@Inject
lateinit var okHttpClient: OkHttpClient
//endregion
@ -105,8 +106,10 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true)
}
PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(this)
.createInitializationOptions())
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(this)
.createInitializationOptions()
)
} catch (e: UnsatisfiedLinkError) {
Log.w(TAG, e)
}
@ -120,8 +123,8 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
val securityKeyManager = SecurityKeyManager.getInstance()
val securityKeyConfig = SecurityKeyManagerConfig.Builder()
.setEnableDebugLogging(BuildConfig.DEBUG)
.build()
.setEnableDebugLogging(BuildConfig.DEBUG)
.build()
securityKeyManager.init(this, securityKeyConfig)
initializeWebRtc()
@ -136,13 +139,15 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
super.onCreate()
val imagePipelineConfig = ImagePipelineConfig.newBuilder(this)
.setNetworkFetcher(OkHttpNetworkFetcherWithCache(okHttpClient))
.setMainDiskCacheConfig(DiskCacheConfig.newBuilder(this)
.setMaxCacheSize(0)
.setMaxCacheSizeOnLowDiskSpace(0)
.setMaxCacheSizeOnVeryLowDiskSpace(0)
.build())
.build()
.setNetworkFetcher(OkHttpNetworkFetcherWithCache(okHttpClient))
.setMainDiskCacheConfig(
DiskCacheConfig.newBuilder(this)
.setMaxCacheSize(0)
.setMaxCacheSizeOnLowDiskSpace(0)
.setMaxCacheSizeOnVeryLowDiskSpace(0)
.build()
)
.build()
Fresco.initialize(this, imagePipelineConfig)
Security.insertProviderAt(Conscrypt.newProvider(), 1)
@ -152,8 +157,10 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
val pushRegistrationWork = OneTimeWorkRequest.Builder(PushRegistrationWorker::class.java).build()
val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build()
val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder(CapabilitiesWorker::class.java,
12, TimeUnit.HOURS).build()
val periodicCapabilitiesUpdateWork = PeriodicWorkRequest.Builder(
CapabilitiesWorker::class.java,
12, TimeUnit.HOURS
).build()
val capabilitiesUpdateWork = OneTimeWorkRequest.Builder(CapabilitiesWorker::class.java).build()
val signalingSettingsWork = OneTimeWorkRequest.Builder(SignalingSettingsWorker::class.java).build()
@ -161,7 +168,11 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
WorkManager.getInstance().enqueue(accountRemovalWork)
WorkManager.getInstance().enqueue(capabilitiesUpdateWork)
WorkManager.getInstance().enqueue(signalingSettingsWork)
WorkManager.getInstance().enqueueUniquePeriodicWork("DailyCapabilitiesUpdateWork", ExistingPeriodicWorkPolicy.REPLACE, periodicCapabilitiesUpdateWork)
WorkManager.getInstance().enqueueUniquePeriodicWork(
"DailyCapabilitiesUpdateWork",
ExistingPeriodicWorkPolicy.REPLACE,
periodicCapabilitiesUpdateWork
)
val config = BundledEmojiCompatConfig(this)
config.setReplaceAll(true)
@ -176,17 +187,16 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
}
//endregion
//region Protected methods
protected fun buildComponent() {
componentApplication = DaggerNextcloudTalkApplicationComponent.builder()
.busModule(BusModule())
.contextModule(ContextModule(applicationContext))
.databaseModule(DatabaseModule())
.restModule(RestModule(applicationContext))
.userModule(UserModule())
.arbitraryStorageModule(ArbitraryStorageModule())
.build()
.busModule(BusModule())
.contextModule(ContextModule(applicationContext))
.databaseModule(DatabaseModule())
.restModule(RestModule(applicationContext))
.userModule(UserModule())
.arbitraryStorageModule(ArbitraryStorageModule())
.build()
}
override fun attachBaseContext(base: Context) {
@ -196,19 +206,20 @@ class NextcloudTalkApplication : MultiDexApplication(), LifecycleObserver {
private fun buildDefaultImageLoader(): ImageLoader {
return ImageLoader.Builder(applicationContext)
.availableMemoryPercentage(0.5) // Use 50% of the application's available memory.
.crossfade(true) // Show a short crossfade when loading images from network or disk into an ImageView.
.componentRegistry {
if (SDK_INT >= P) {
add(ImageDecoderDecoder(applicationContext))
} else {
add(GifDecoder())
}
add(SvgDecoder(applicationContext))
.availableMemoryPercentage(0.5) // Use 50% of the application's available memory.
.crossfade(true) // Show a short crossfade when loading images from network or disk into an ImageView.
.componentRegistry {
if (SDK_INT >= P) {
add(ImageDecoderDecoder(applicationContext))
} else {
add(GifDecoder())
}
.okHttpClient(okHttpClient)
.build()
add(SvgDecoder(applicationContext))
}
.okHttpClient(okHttpClient)
.build()
}
companion object {
private val TAG = NextcloudTalkApplication::class.java.simpleName
//region Singleton

View file

@ -44,10 +44,6 @@ import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.bluelinelabs.conductor.RouterTransaction;
import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
import com.bluelinelabs.logansquare.LoganSquare;
@ -71,7 +67,6 @@ import com.nextcloud.talk.models.RingtoneSettings;
import com.nextcloud.talk.models.database.UserEntity;
import com.nextcloud.talk.models.json.conversations.Conversation;
import com.nextcloud.talk.models.json.conversations.RoomOverall;
import com.nextcloud.talk.models.json.conversations.RoomsOverall;
import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
import com.nextcloud.talk.utils.ApiUtils;
@ -94,6 +89,9 @@ import java.util.List;
import javax.inject.Inject;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import autodagger.AutoInjector;
import butterknife.BindView;
import butterknife.OnClick;
@ -206,13 +204,13 @@ public class CallNotificationController extends BaseController {
originalBundle.putString(BundleKeys.INSTANCE.getKEY_CONVERSATION_NAME(), currentConversation.getDisplayName());
getRouter().replaceTopController(RouterTransaction.with(new CallController(originalBundle))
.popChangeHandler(new HorizontalChangeHandler())
.pushChangeHandler(new HorizontalChangeHandler()));
.popChangeHandler(new HorizontalChangeHandler())
.pushChangeHandler(new HorizontalChangeHandler()));
}
private void checkIfAnyParticipantsRemainInRoom() {
ncApi.getPeersForCall(credentials, ApiUtils.getUrlForCall(userBeingCalled.getBaseUrl(),
currentConversation.getToken()))
currentConversation.getToken()))
.subscribeOn(Schedulers.io())
.takeWhile(observable -> !leavingScreen)
.subscribe(new Observer<ParticipantsOverall>() {
@ -261,7 +259,7 @@ public class CallNotificationController extends BaseController {
private void handleFromNotification() {
boolean isConversationApiV3 = userBeingCalled.hasSpreedFeatureCapability("conversation-v3");
if(isConversationApiV3) {
if (isConversationApiV3) {
ncApi.getRoom(credentials, ApiUtils.getRoomV3(userBeingCalled.getBaseUrl(), roomId))
.subscribeOn(Schedulers.io())
.retry(3)
@ -279,12 +277,12 @@ public class CallNotificationController extends BaseController {
boolean hasCallFlags = userBeingCalled.hasSpreedFeatureCapability("conversation-call-flags");
if (hasCallFlags) {
if (isInCallWithVideo(currentConversation.callFlag)){
if (isInCallWithVideo(currentConversation.callFlag)) {
incomingCallVoiceOrVideoTextView.setText(String.format(getResources().getString(R.string.nc_call_video),
getResources().getString(R.string.nc_app_name)));
getResources().getString(R.string.nc_app_name)));
} else {
incomingCallVoiceOrVideoTextView.setText(String.format(getResources().getString(R.string.nc_call_voice),
getResources().getString(R.string.nc_app_name)));
getResources().getString(R.string.nc_app_name)));
}
}
}
@ -412,7 +410,7 @@ public class CallNotificationController extends BaseController {
ImageRequest imageRequest =
DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(userBeingCalled.getBaseUrl(),
currentConversation.getName(), R.dimen.avatar_size_very_big), null);
currentConversation.getName(), R.dimen.avatar_size_very_big), null);
ImagePipeline imagePipeline = Fresco.getImagePipeline();
DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(imageRequest, null);
@ -422,11 +420,11 @@ public class CallNotificationController extends BaseController {
protected void onNewResultImpl(@Nullable Bitmap bitmap) {
if (avatarImageView != null) {
avatarImageView.getHierarchy().setImage(new BitmapDrawable(bitmap), 100,
true);
true);
if (getResources() != null) {
incomingTextRelativeLayout.setBackground(getResources().getDrawable(R.drawable
.incoming_gradient));
.incoming_gradient));
}
if ((AvatarStatusCodeHolder.getInstance().getStatusCode() == 200 || AvatarStatusCodeHolder.getInstance().getStatusCode() == 0) &&
@ -512,7 +510,7 @@ public class CallNotificationController extends BaseController {
if (TextUtils.isEmpty(callRingtonePreferenceString)) {
// play default sound
ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() +
"/raw/librem_by_feandesign_call");
"/raw/librem_by_feandesign_call");
} else {
try {
RingtoneSettings ringtoneSettings = LoganSquare.parse(callRingtonePreferenceString, RingtoneSettings.class);
@ -520,7 +518,7 @@ public class CallNotificationController extends BaseController {
} catch (IOException e) {
Log.e(TAG, "Failed to parse ringtone settings");
ringtoneUri = Uri.parse("android.resource://" + getApplicationContext().getPackageName() +
"/raw/librem_by_feandesign_call");
"/raw/librem_by_feandesign_call");
}
}
@ -531,7 +529,7 @@ public class CallNotificationController extends BaseController {
mediaPlayer.setLooping(true);
AudioAttributes audioAttributes = new AudioAttributes.Builder().setContentType(AudioAttributes
.CONTENT_TYPE_SONIFICATION).setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build();
.CONTENT_TYPE_SONIFICATION).setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build();
mediaPlayer.setAudioAttributes(audioAttributes);
mediaPlayer.setOnPreparedListener(mp -> mediaPlayer.start());

View file

@ -85,50 +85,69 @@ import io.reactivex.schedulers.Schedulers
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.*
import java.util.Calendar
import java.util.Collections
import java.util.Comparator
import java.util.Locale
import javax.inject.Inject
import kotlin.collections.ArrayList
@AutoInjector(NextcloudTalkApplication::class)
class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleAdapter.OnItemClickListener {
@BindView(R.id.notification_settings)
lateinit var notificationsPreferenceScreen: MaterialPreferenceScreen
@BindView(R.id.progressBar)
lateinit var progressBar: ProgressBar
@BindView(R.id.conversation_info_message_notifications)
lateinit var messageNotificationLevel: MaterialChoicePreference
@BindView(R.id.webinar_settings)
lateinit var conversationInfoWebinar: MaterialPreferenceScreen
@BindView(R.id.conversation_info_lobby)
lateinit var conversationInfoLobby: MaterialSwitchPreference
@BindView(R.id.conversation_info_name)
lateinit var nameCategoryView: MaterialPreferenceCategory
@BindView(R.id.start_time_preferences)
lateinit var startTimeView: MaterialStandardPreference
@BindView(R.id.avatar_image)
lateinit var conversationAvatarImageView: SimpleDraweeView
@BindView(R.id.display_name_text)
lateinit var conversationDisplayName: EmojiTextView
@BindView(R.id.participants_list_category)
lateinit var participantsListCategory: MaterialPreferenceCategory
@BindView(R.id.addParticipantsAction)
lateinit var addParticipantsAction: MaterialStandardPreference;
lateinit var addParticipantsAction: MaterialStandardPreference
@BindView(R.id.recycler_view)
lateinit var recyclerView: RecyclerView
@BindView(R.id.deleteConversationAction)
lateinit var deleteConversationAction: MaterialStandardPreference
@BindView(R.id.leaveConversationAction)
lateinit var leaveConversationAction: MaterialStandardPreference
@BindView(R.id.ownOptions)
lateinit var ownOptionsCategory: MaterialPreferenceCategory
@BindView(R.id.muteCalls)
lateinit var muteCalls: MaterialSwitchPreference
@set:Inject
lateinit var ncApi: NcApi
@set:Inject
lateinit var context: Context
@set:Inject
lateinit var eventBus: EventBus
@ -164,7 +183,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
NextcloudTalkApplication.sharedApplication?.componentApplication?.inject(this)
conversationUser = args.getParcelable(BundleKeys.KEY_USER_ENTITY)
conversationToken = args.getString(BundleKeys.KEY_ROOM_TOKEN)
hasAvatarSpacing = args.getBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, false);
hasAvatarSpacing = args.getBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, false)
credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token)
}
@ -207,14 +226,19 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
}
private fun setupWebinaryView() {
if (conversationUser!!.hasSpreedFeatureCapability("webinary-lobby") && (conversation!!.type
== Conversation.ConversationType.ROOM_GROUP_CALL || conversation!!.type ==
Conversation.ConversationType.ROOM_PUBLIC_CALL) && conversation!!.canModerate(conversationUser)) {
if (conversationUser!!.hasSpreedFeatureCapability("webinary-lobby") &&
(
conversation!!.type == Conversation.ConversationType.ROOM_GROUP_CALL ||
conversation!!.type == Conversation.ConversationType.ROOM_PUBLIC_CALL
) &&
conversation!!.canModerate(conversationUser)
) {
conversationInfoWebinar.visibility = View.VISIBLE
val isLobbyOpenToModeratorsOnly = conversation!!.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY
val isLobbyOpenToModeratorsOnly =
conversation!!.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY
(conversationInfoLobby.findViewById<View>(R.id.mp_checkable) as SwitchCompat)
.isChecked = isLobbyOpenToModeratorsOnly
.isChecked = isLobbyOpenToModeratorsOnly
reconfigureLobbyTimerView()
@ -225,12 +249,17 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
currentTimeCalendar.timeInMillis = conversation!!.lobbyTimer * 1000
}
dateTimePicker(minDateTime = Calendar.getInstance(), requireFutureDateTime =
true, currentDateTime = currentTimeCalendar, dateTimeCallback = { _,
dateTime ->
reconfigureLobbyTimerView(dateTime)
submitLobbyChanges()
})
dateTimePicker(
minDateTime = Calendar.getInstance(),
requireFutureDateTime =
true,
currentDateTime = currentTimeCalendar,
dateTimeCallback = { _,
dateTime ->
reconfigureLobbyTimerView(dateTime)
submitLobbyChanges()
}
)
}
}
@ -253,7 +282,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
}
conversation!!.lobbyState = if (isChecked) Conversation.LobbyState
.LOBBY_STATE_MODERATORS_ONLY else Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS
.LOBBY_STATE_MODERATORS_ONLY else Conversation.LobbyState.LOBBY_STATE_ALL_PARTICIPANTS
if (conversation!!.lobbyTimer != null && conversation!!.lobbyTimer != java.lang.Long.MIN_VALUE && conversation!!.lobbyTimer != 0L) {
startTimeView.setSummary(DateUtils.getLocalDateStringFromTimestampForLobby(conversation!!.lobbyTimer))
@ -269,27 +298,34 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
}
fun submitLobbyChanges() {
val state = if ((conversationInfoLobby.findViewById<View>(R.id
.mp_checkable) as SwitchCompat).isChecked) 1 else 0
ncApi.setLobbyForConversation(ApiUtils.getCredentials(conversationUser!!.username,
conversationUser.token), ApiUtils.getUrlForLobbyForConversation
(conversationUser.baseUrl, conversation!!.token), state, conversation!!.lobbyTimer)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<GenericOverall> {
override fun onComplete() {
}
val state = if (
(
conversationInfoLobby.findViewById<View>(
R.id.mp_checkable
) as SwitchCompat
).isChecked
) 1 else 0
ncApi.setLobbyForConversation(
ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token),
ApiUtils.getUrlForLobbyForConversation(conversationUser.baseUrl, conversation!!.token),
state,
conversation!!.lobbyTimer
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<GenericOverall> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: GenericOverall) {
}
override fun onNext(t: GenericOverall) {
}
override fun onError(e: Throwable) {
}
})
override fun onError(e: Throwable) {
}
})
}
private fun showLovelyDialog(dialogId: Int, savedInstanceState: Bundle) {
@ -313,17 +349,21 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
private fun showDeleteConversationDialog(savedInstanceState: Bundle?) {
if (activity != null) {
LovelyStandardDialog(activity, LovelyStandardDialog.ButtonLayout.HORIZONTAL)
.setTopColorRes(R.color.nc_darkRed)
.setIcon(DisplayUtils.getTintedDrawable(context!!.resources,
R.drawable.ic_delete_black_24dp, R.color.bg_default))
.setPositiveButtonColor(context!!.resources.getColor(R.color.nc_darkRed))
.setTitle(R.string.nc_delete_call)
.setMessage(conversation!!.deleteWarningMessage)
.setPositiveButton(R.string.nc_delete) { deleteConversation() }
.setNegativeButton(R.string.nc_cancel, null)
.setInstanceStateHandler(ID_DELETE_CONVERSATION_DIALOG, saveStateHandler!!)
.setSavedInstanceState(savedInstanceState)
.show()
.setTopColorRes(R.color.nc_darkRed)
.setIcon(
DisplayUtils.getTintedDrawable(
context!!.resources,
R.drawable.ic_delete_black_24dp, R.color.bg_default
)
)
.setPositiveButtonColor(context!!.resources.getColor(R.color.nc_darkRed))
.setTitle(R.string.nc_delete_call)
.setMessage(conversation!!.deleteWarningMessage)
.setPositiveButton(R.string.nc_delete) { deleteConversation() }
.setNegativeButton(R.string.nc_cancel, null)
.setInstanceStateHandler(ID_DELETE_CONVERSATION_DIALOG, saveStateHandler!!)
.setSavedInstanceState(savedInstanceState)
.show()
}
}
@ -335,8 +375,8 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
super.onRestoreViewState(view, savedViewState)
if (LovelySaveStateHandler.wasDialogOnScreen(savedViewState)) {
//Dialog won't be restarted automatically, so we need to call this method.
//Each dialog knows how to restore its state
// Dialog won't be restarted automatically, so we need to call this method.
// Each dialog knows how to restore its state
showLovelyDialog(LovelySaveStateHandler.getSavedDialogId(savedViewState), savedViewState)
}
}
@ -397,27 +437,28 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
}
private fun getListOfParticipants() {
ncApi.getPeersForCall(credentials, ApiUtils.getUrlForParticipants(conversationUser!!.baseUrl, conversationToken))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<ParticipantsOverall> {
override fun onSubscribe(d: Disposable) {
participantsDisposable = d
}
ncApi.getPeersForCall(
credentials,
ApiUtils.getUrlForParticipants(conversationUser!!.baseUrl, conversationToken)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<ParticipantsOverall> {
override fun onSubscribe(d: Disposable) {
participantsDisposable = d
}
override fun onNext(participantsOverall: ParticipantsOverall) {
handleParticipants(participantsOverall.ocs.data)
}
override fun onNext(participantsOverall: ParticipantsOverall) {
handleParticipants(participantsOverall.ocs.data)
}
override fun onError(e: Throwable) {
}
override fun onComplete() {
participantsDisposable!!.dispose()
}
})
override fun onError(e: Throwable) {
}
override fun onComplete() {
participantsDisposable!!.dispose()
}
})
}
@OnClick(R.id.addParticipantsAction)
@ -430,21 +471,33 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
existingParticipantsId.add(userItem.model.userId)
}
bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true);
bundle.putBoolean(BundleKeys.KEY_ADD_PARTICIPANTS, true)
bundle.putStringArrayList(BundleKeys.KEY_EXISTING_PARTICIPANTS, existingParticipantsId)
bundle.putString(BundleKeys.KEY_TOKEN, conversation!!.token)
getRouter().pushController((RouterTransaction.with(ContactsController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler())))
getRouter().pushController(
(
RouterTransaction.with(
ContactsController(bundle)
)
.pushChangeHandler(
HorizontalChangeHandler()
)
.popChangeHandler(
HorizontalChangeHandler()
)
)
)
}
@OnClick(R.id.leaveConversationAction)
internal fun leaveConversation() {
workerData?.let {
WorkManager.getInstance().enqueue(OneTimeWorkRequest.Builder
(LeaveConversationWorker::class
.java).setInputData(it).build()
WorkManager.getInstance().enqueue(
OneTimeWorkRequest.Builder(
LeaveConversationWorker::class
.java
).setInputData(it).build()
)
popTwoLastControllers()
}
@ -452,8 +505,11 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
private fun deleteConversation() {
workerData?.let {
WorkManager.getInstance().enqueue(OneTimeWorkRequest.Builder
(DeleteConversationWorker::class.java).setInputData(it).build())
WorkManager.getInstance().enqueue(
OneTimeWorkRequest.Builder(
DeleteConversationWorker::class.java
).setInputData(it).build()
)
popTwoLastControllers()
}
}
@ -471,69 +527,67 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
private fun fetchRoomInfo() {
ncApi.getRoom(credentials, ApiUtils.getRoom(conversationUser!!.baseUrl, conversationToken))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
roomDisposable = d
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<RoomOverall> {
override fun onSubscribe(d: Disposable) {
roomDisposable = d
}
override fun onNext(roomOverall: RoomOverall) {
conversation = roomOverall.ocs.data
val conversationCopy = conversation
if (conversationCopy!!.canModerate(conversationUser)) {
addParticipantsAction.visibility = View.VISIBLE
} else {
addParticipantsAction.visibility = View.GONE
}
override fun onNext(roomOverall: RoomOverall) {
conversation = roomOverall.ocs.data
if (isAttached && (!isBeingDestroyed || !isDestroyed)) {
ownOptionsCategory.visibility = View.VISIBLE
val conversationCopy = conversation
setupWebinaryView()
if (conversationCopy!!.canModerate(conversationUser)) {
addParticipantsAction.visibility = View.VISIBLE
if (!conversation!!.canLeave(conversationUser)) {
leaveConversationAction.visibility = View.GONE
} else {
addParticipantsAction.visibility = View.GONE
leaveConversationAction.visibility = View.VISIBLE
}
if (isAttached && (!isBeingDestroyed || !isDestroyed)) {
ownOptionsCategory.visibility = View.VISIBLE
setupWebinaryView()
if (!conversation!!.canLeave(conversationUser)) {
leaveConversationAction.visibility = View.GONE
} else {
leaveConversationAction.visibility = View.VISIBLE
}
if (!conversation!!.canModerate(conversationUser)) {
deleteConversationAction.visibility = View.GONE
} else {
deleteConversationAction.visibility = View.VISIBLE
}
if (Conversation.ConversationType.ROOM_SYSTEM == conversation!!.type) {
muteCalls.visibility = View.GONE
}
getListOfParticipants()
progressBar.visibility = View.GONE
nameCategoryView.visibility = View.VISIBLE
conversationDisplayName.text = conversation!!.displayName
loadConversationAvatar()
adjustNotificationLevelUI()
notificationsPreferenceScreen.visibility = View.VISIBLE
if (!conversation!!.canModerate(conversationUser)) {
deleteConversationAction.visibility = View.GONE
} else {
deleteConversationAction.visibility = View.VISIBLE
}
}
override fun onError(e: Throwable) {
if (Conversation.ConversationType.ROOM_SYSTEM == conversation!!.type) {
muteCalls.visibility = View.GONE
}
}
getListOfParticipants()
override fun onComplete() {
roomDisposable!!.dispose()
progressBar.visibility = View.GONE
nameCategoryView.visibility = View.VISIBLE
conversationDisplayName.text = conversation!!.displayName
loadConversationAvatar()
adjustNotificationLevelUI()
notificationsPreferenceScreen.visibility = View.VISIBLE
}
})
}
override fun onError(e: Throwable) {
}
override fun onComplete() {
roomDisposable!!.dispose()
}
})
}
private fun adjustNotificationLevelUI() {
@ -543,12 +597,13 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
messageNotificationLevel.alpha = 1.0f
if (conversation!!.notificationLevel != Conversation.NotificationLevel.DEFAULT) {
val stringValue: String = when (EnumNotificationLevelConverter().convertToInt(conversation!!.notificationLevel)) {
1 -> "always"
2 -> "mention"
3 -> "never"
else -> "mention"
}
val stringValue: String =
when (EnumNotificationLevelConverter().convertToInt(conversation!!.notificationLevel)) {
1 -> "always"
2 -> "mention"
3 -> "never"
else -> "mention"
}
messageNotificationLevel.value = stringValue
} else {
@ -577,22 +632,38 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
private fun loadConversationAvatar() {
when (conversation!!.type) {
Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (!TextUtils.isEmpty
(conversation!!.name)) {
Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL -> if (
!TextUtils.isEmpty(conversation!!.name)
) {
val draweeController = Fresco.newDraweeControllerBuilder()
.setOldController(conversationAvatarImageView.controller)
.setAutoPlayAnimations(true)
.setImageRequest(DisplayUtils.getImageRequestForUrl(ApiUtils.getUrlForAvatarWithName(conversationUser!!.baseUrl,
conversation!!.name, R.dimen.avatar_size_big), conversationUser))
.build()
.setOldController(conversationAvatarImageView.controller)
.setAutoPlayAnimations(true)
.setImageRequest(
DisplayUtils.getImageRequestForUrl(
ApiUtils.getUrlForAvatarWithName(
conversationUser!!.baseUrl,
conversation!!.name, R.dimen.avatar_size_big
),
conversationUser
)
)
.build()
conversationAvatarImageView.controller = draweeController
}
Conversation.ConversationType.ROOM_GROUP_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage(DisplayUtils
.getRoundedBitmapDrawableFromVectorDrawableResource(resources,
R.drawable.ic_people_group_white_24px))
Conversation.ConversationType.ROOM_PUBLIC_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage(DisplayUtils
.getRoundedBitmapDrawableFromVectorDrawableResource(resources,
R.drawable.ic_link_white_24px))
Conversation.ConversationType.ROOM_GROUP_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage(
DisplayUtils
.getRoundedBitmapDrawableFromVectorDrawableResource(
resources,
R.drawable.ic_people_group_white_24px
)
)
Conversation.ConversationType.ROOM_PUBLIC_CALL -> conversationAvatarImageView.hierarchy.setPlaceholderImage(
DisplayUtils
.getRoundedBitmapDrawableFromVectorDrawableResource(
resources,
R.drawable.ic_link_white_24px
)
)
Conversation.ConversationType.ROOM_SYSTEM -> {
val layers = arrayOfNulls<Drawable>(2)
layers[0] = context.getDrawable(R.drawable.ic_launcher_background)
@ -610,13 +681,14 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
val userItem = adapter?.getItem(position) as UserItem
val participant = userItem.model
if (participant.userId != conversationUser!!.userId) {
var items = mutableListOf(
BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_promote)),
BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_demote)),
BasicListItemWithImage(R.drawable.ic_delete_grey600_24dp,
context.getString(R.string.nc_remove_participant))
BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_promote)),
BasicListItemWithImage(R.drawable.ic_pencil_grey600_24dp, context.getString(R.string.nc_demote)),
BasicListItemWithImage(
R.drawable.ic_delete_grey600_24dp,
context.getString(R.string.nc_remove_participant)
)
)
if (!conversation!!.canModerate(conversationUser)) {
@ -629,7 +701,6 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
}
}
if (items.isNotEmpty()) {
MaterialDialog(activity!!, BottomSheet(WRAP_CONTENT)).show {
cornerRadius(res = R.dimen.corner_radius)
@ -639,38 +710,62 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
if (index == 0) {
if (participant.type == Participant.ParticipantType.MODERATOR) {
ncApi.demoteModeratorToUser(credentials, ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token), participant.userId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
getListOfParticipants()
}
ncApi.demoteModeratorToUser(
credentials,
ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token),
participant.userId
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
getListOfParticipants()
}
} else if (participant.type == Participant.ParticipantType.USER) {
ncApi.promoteUserToModerator(credentials, ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token), participant.userId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
getListOfParticipants()
}
ncApi.promoteUserToModerator(
credentials,
ApiUtils.getUrlForModerators(conversationUser.baseUrl, conversation!!.token),
participant.userId
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
getListOfParticipants()
}
}
} else if (index == 1) {
if (participant.type == Participant.ParticipantType.GUEST ||
participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK) {
ncApi.removeParticipantFromConversation(credentials, ApiUtils.getUrlForRemovingParticipantFromConversation(conversationUser.baseUrl, conversation!!.token, true), participant.sessionId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
getListOfParticipants()
}
participant.type == Participant.ParticipantType.USER_FOLLOWING_LINK
) {
ncApi.removeParticipantFromConversation(
credentials,
ApiUtils.getUrlForRemovingParticipantFromConversation(
conversationUser.baseUrl,
conversation!!.token,
true
),
participant.sessionId
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
getListOfParticipants()
}
} else {
ncApi.removeParticipantFromConversation(credentials, ApiUtils.getUrlForRemovingParticipantFromConversation(conversationUser.baseUrl, conversation!!.token, false), participant.userId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
getListOfParticipants()
// get participants again
}
ncApi.removeParticipantFromConversation(
credentials,
ApiUtils.getUrlForRemovingParticipantFromConversation(
conversationUser.baseUrl,
conversation!!.token,
false
),
participant.userId
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
getListOfParticipants()
// get participants again
}
}
}
}
@ -678,7 +773,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
}
}
return true;
return true
}
companion object {
@ -709,7 +804,7 @@ class ConversationInfoController(args: Bundle) : BaseController(args), FlexibleA
}
return left.model.displayName.toLowerCase(Locale.ROOT).compareTo(
right.model.displayName.toLowerCase(Locale.ROOT)
right.model.displayName.toLowerCase(Locale.ROOT)
)
}
}

View file

@ -53,5 +53,4 @@ abstract class ButterKnifeController : Controller {
unbinder!!.unbind()
unbinder = null
}
}

View file

@ -29,10 +29,11 @@ interface ListItemWithImage {
}
data class BasicListItemWithImage(
@DrawableRes val iconRes: Int,
override val title: String) : ListItemWithImage {
@DrawableRes val iconRes: Int,
override val title: String
) : ListItemWithImage {
override fun populateIcon(imageView: ImageView) {
imageView.setImageResource(iconRes)
}
}
}

View file

@ -23,7 +23,6 @@ package com.nextcloud.talk.controllers.bottomsheet.items
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.WhichButton
@ -34,14 +33,14 @@ import com.afollestad.materialdialogs.internal.rtl.RtlTextView
import com.afollestad.materialdialogs.list.getItemSelector
import com.afollestad.materialdialogs.utils.MDUtil.inflate
import com.afollestad.materialdialogs.utils.MDUtil.maybeSetTextColor
import com.google.android.material.textview.MaterialTextView
import com.nextcloud.talk.R
private const val KEY_ACTIVATED_INDEX = "activated_index"
internal class ListItemViewHolder(
itemView: View,
private val adapter: ListIconDialogAdapter<*>) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
itemView: View,
private val adapter: ListIconDialogAdapter<*>
) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
init {
itemView.setOnClickListener(this)
}
@ -53,11 +52,12 @@ internal class ListItemViewHolder(
}
internal class ListIconDialogAdapter<IT : ListItemWithImage>(
private var dialog: MaterialDialog,
private var items: List<IT>,
disabledItems: IntArray?,
private var waitForPositiveButton: Boolean,
private var selection: ListItemListener<IT>) : RecyclerView.Adapter<ListItemViewHolder>(), DialogAdapter<IT, ListItemListener<IT>> {
private var dialog: MaterialDialog,
private var items: List<IT>,
disabledItems: IntArray?,
private var waitForPositiveButton: Boolean,
private var selection: ListItemListener<IT>
) : RecyclerView.Adapter<ListItemViewHolder>(), DialogAdapter<IT, ListItemListener<IT>> {
private var disabledIndices: IntArray = disabledItems ?: IntArray(0)
@ -81,12 +81,13 @@ internal class ListIconDialogAdapter<IT : ListItemWithImage>(
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int): ListItemViewHolder {
parent: ViewGroup,
viewType: Int
): ListItemViewHolder {
val listItemView: View = parent.inflate(dialog.windowContext, R.layout.menu_item_sheet)
val viewHolder = ListItemViewHolder(
itemView = listItemView,
adapter = this
itemView = listItemView,
adapter = this
)
viewHolder.titleView.maybeSetTextColor(dialog.windowContext, R.attr.md_color_content)
return viewHolder
@ -95,8 +96,9 @@ internal class ListIconDialogAdapter<IT : ListItemWithImage>(
override fun getItemCount() = items.size
override fun onBindViewHolder(
holder: ListItemViewHolder,
position: Int) {
holder: ListItemViewHolder,
position: Int
) {
holder.itemView.isEnabled = !disabledIndices.contains(position)
val currentItem = items[position]
@ -121,8 +123,9 @@ internal class ListIconDialogAdapter<IT : ListItemWithImage>(
}
override fun replaceItems(
items: List<IT>,
listener: ListItemListener<IT>) {
items: List<IT>,
listener: ListItemListener<IT>
) {
this.items = items
if (listener != null) {
this.selection = listener

View file

@ -28,37 +28,40 @@ import com.afollestad.materialdialogs.list.customListAdapter
import com.afollestad.materialdialogs.list.getListAdapter
typealias ListItemListener<IT> =
((dialog: MaterialDialog, index: Int, item: IT) -> Unit)?
((dialog: MaterialDialog, index: Int, item: IT) -> Unit)?
@CheckResult fun <IT : ListItemWithImage> MaterialDialog.listItemsWithImage(
items: List<IT>,
disabledIndices: IntArray? = null,
waitForPositiveButton: Boolean = true,
selection: ListItemListener<IT> = null): MaterialDialog {
@CheckResult
fun <IT : ListItemWithImage> MaterialDialog.listItemsWithImage(
items: List<IT>,
disabledIndices: IntArray? = null,
waitForPositiveButton: Boolean = true,
selection: ListItemListener<IT> = null
): MaterialDialog {
if (getListAdapter() != null) {
return updateListItemsWithImage(
items = items,
disabledIndices = disabledIndices
items = items,
disabledIndices = disabledIndices
)
}
val layoutManager = LinearLayoutManager(windowContext)
return customListAdapter(
adapter = ListIconDialogAdapter(
dialog = this,
items = items,
disabledItems = disabledIndices,
waitForPositiveButton = waitForPositiveButton,
selection = selection
),
layoutManager = layoutManager
adapter = ListIconDialogAdapter(
dialog = this,
items = items,
disabledItems = disabledIndices,
waitForPositiveButton = waitForPositiveButton,
selection = selection
),
layoutManager = layoutManager
)
}
fun MaterialDialog.updateListItemsWithImage(
items: List<ListItemWithImage>,
disabledIndices: IntArray? = null): MaterialDialog {
items: List<ListItemWithImage>,
disabledIndices: IntArray? = null
): MaterialDialog {
val adapter = getListAdapter()
check(adapter != null) {
"updateGridItems(...) can't be used before you've created a bottom sheet grid dialog."

View file

@ -20,6 +20,4 @@
package com.nextcloud.talk.events
class CallNotificationClick {
}
class CallNotificationClick

View file

@ -33,7 +33,11 @@ import android.provider.ContactsContract
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.core.os.ConfigurationCompat
import androidx.work.*
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import autodagger.AutoInjector
import com.bluelinelabs.conductor.Controller
import com.google.gson.Gson
@ -54,10 +58,9 @@ import okhttp3.MediaType
import okhttp3.RequestBody
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class ContactAddressBookWorker(val context: Context, workerParameters: WorkerParameters) :
Worker(context, workerParameters) {
Worker(context, workerParameters) {
@Inject
lateinit var ncApi: NcApi
@ -85,7 +88,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
}
val deleteAll = inputData.getBoolean(DELETE_ALL, false)
if(deleteAll){
if (deleteAll) {
deleteAllLinkedAccounts()
return Result.success()
}
@ -99,7 +102,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
}
}
if(AccountManager.get(context).getAccountsByType(accountType).isEmpty()){
if (AccountManager.get(context).getAccountsByType(accountType).isEmpty()) {
AccountManager.get(context).addAccountExplicitly(Account(accountName, accountType), "", null)
} else {
Log.d(TAG, "Account already exists")
@ -107,7 +110,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
val deviceContactsWithNumbers = collectContactsWithPhoneNumbersFromDevice()
if(deviceContactsWithNumbers.isNotEmpty()){
if (deviceContactsWithNumbers.isNotEmpty()) {
val currentLocale = ConfigurationCompat.getLocales(context.resources.configuration)[0].country
val map = mutableMapOf<String, Any>()
@ -117,28 +120,29 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
val json = Gson().toJson(map)
ncApi.searchContactsByPhoneNumber(
ApiUtils.getCredentials(currentUser.username, currentUser.token),
ApiUtils.getUrlForSearchByNumber(currentUser.baseUrl),
RequestBody.create(MediaType.parse("application/json"), json))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<ContactsByNumberOverall> {
override fun onComplete() {
}
ApiUtils.getCredentials(currentUser.username, currentUser.token),
ApiUtils.getUrlForSearchByNumber(currentUser.baseUrl),
RequestBody.create(MediaType.parse("application/json"), json)
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<ContactsByNumberOverall> {
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
}
override fun onSubscribe(d: Disposable) {
}
override fun onNext(foundContacts: ContactsByNumberOverall) {
val contactsWithAssociatedPhoneNumbers = foundContacts.ocs.map
deleteLinkedAccounts(contactsWithAssociatedPhoneNumbers)
createLinkedAccounts(contactsWithAssociatedPhoneNumbers)
}
override fun onNext(foundContacts: ContactsByNumberOverall) {
val contactsWithAssociatedPhoneNumbers = foundContacts.ocs.map
deleteLinkedAccounts(contactsWithAssociatedPhoneNumbers)
createLinkedAccounts(contactsWithAssociatedPhoneNumbers)
}
override fun onError(e: Throwable) {
Log.e(javaClass.simpleName, "Failed to searchContactsByPhoneNumber", e)
}
})
override fun onError(e: Throwable) {
Log.e(javaClass.simpleName, "Failed to searchContactsByPhoneNumber", e)
}
})
}
// store timestamp
@ -151,11 +155,11 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
val deviceContactsWithNumbers: MutableMap<String, List<String>> = mutableMapOf()
val contactCursor = context.contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
null
ContactsContract.Contacts.CONTENT_URI,
null,
null,
null,
null
)
if (contactCursor != null) {
@ -163,7 +167,8 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
contactCursor.moveToFirst()
for (i in 0 until contactCursor.count) {
val id = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts._ID))
val lookup = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY))
val lookup =
contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY))
deviceContactsWithNumbers[lookup] = getPhoneNumbersFromDeviceContact(id)
contactCursor.moveToNext()
}
@ -178,39 +183,50 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
Log.d(TAG, "deleteLinkedAccount")
fun deleteLinkedAccount(id: String) {
val rawContactUri = ContactsContract.RawContacts.CONTENT_URI
.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
.build()
val count = context.contentResolver.delete(rawContactUri, ContactsContract.RawContacts.CONTACT_ID + " " +
"LIKE \"" + id + "\"", null)
Log.d(TAG, "deleted $count linked accounts for id $id")
}
val rawContactUri = ContactsContract.Data.CONTENT_URI
.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
.appendQueryParameter(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat")
.build()
val count = context.contentResolver.delete(
rawContactUri,
ContactsContract.RawContacts.CONTACT_ID + " " + "LIKE \"" + id + "\"",
null
)
Log.d(TAG, "deleted $count linked accounts for id $id")
}
val rawContactUri = ContactsContract.Data.CONTENT_URI
.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
.appendQueryParameter(
ContactsContract.Data.MIMETYPE,
"vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat"
)
.build()
val rawContactsCursor = context.contentResolver.query(
rawContactUri,
null,
null,
null,
null
rawContactUri,
null,
null,
null,
null
)
if (rawContactsCursor != null) {
if (rawContactsCursor.count > 0) {
while (rawContactsCursor.moveToNext()) {
val lookupKey = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY))
val contactId = rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.CONTACT_ID))
val lookupKey =
rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.LOOKUP_KEY))
val contactId =
rawContactsCursor.getString(rawContactsCursor.getColumnIndex(ContactsContract.Data.CONTACT_ID))
if (contactsWithAssociatedPhoneNumbers == null || !contactsWithAssociatedPhoneNumbers.containsKey(lookupKey)) {
if (contactsWithAssociatedPhoneNumbers == null || !contactsWithAssociatedPhoneNumbers.containsKey(
lookupKey
)
) {
deleteLinkedAccount(contactId)
}
}
@ -222,25 +238,29 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
}
private fun createLinkedAccounts(contactsWithAssociatedPhoneNumbers: Map<String, String>?) {
fun hasLinkedAccount(id: String) : Boolean {
fun hasLinkedAccount(id: String): Boolean {
var hasLinkedAccount = false
val where = ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?"
val where =
ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?"
val params = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id)
val rawContactUri = ContactsContract.Data.CONTENT_URI
.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
.appendQueryParameter(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat")
.build()
.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
.appendQueryParameter(
ContactsContract.Data.MIMETYPE,
"vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat"
)
.build()
val rawContactsCursor = context.contentResolver.query(
rawContactUri,
null,
where,
params,
null
rawContactUri,
null,
where,
params,
null
)
if (rawContactsCursor != null) {
@ -259,18 +279,19 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
val lookupUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey)
val lookupContactUri = ContactsContract.Contacts.lookupContact(context.contentResolver, lookupUri)
val contactCursor = context.contentResolver.query(
lookupContactUri,
null,
null,
null,
null)
lookupContactUri,
null,
null,
null,
null
)
if (contactCursor != null) {
if (contactCursor.count > 0) {
contactCursor.moveToFirst()
val id = contactCursor.getString(contactCursor.getColumnIndex(ContactsContract.Contacts._ID))
if(hasLinkedAccount(id)){
if (hasLinkedAccount(id)) {
return
}
@ -285,34 +306,60 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
val rawContactsUri = ContactsContract.RawContacts.CONTENT_URI.buildUpon().build()
val dataUri = ContactsContract.Data.CONTENT_URI.buildUpon().build()
ops.add(ContentProviderOperation
ops.add(
ContentProviderOperation
.newInsert(rawContactsUri)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
.withValue(ContactsContract.RawContacts.AGGREGATION_MODE,
ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT)
.withValue(
ContactsContract.RawContacts.AGGREGATION_MODE,
ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT
)
.withValue(ContactsContract.RawContacts.SYNC2, cloudId)
.build())
ops.add(ContentProviderOperation
.build()
)
ops.add(
ContentProviderOperation
.newInsert(dataUri)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, numbers[0])
.build())
ops.add(ContentProviderOperation
.build()
)
ops.add(
ContentProviderOperation
.newInsert(dataUri)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(
ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE
)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName)
.build())
ops.add(ContentProviderOperation
.build()
)
ops.add(
ContentProviderOperation
.newInsert(dataUri)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat")
.withValue(
ContactsContract.Data.MIMETYPE,
"vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat"
)
.withValue(ContactsContract.Data.DATA1, cloudId)
.withValue(ContactsContract.Data.DATA2, String.format(context.resources.getString(R
.string.nc_phone_book_integration_chat_via), accountName))
.build())
.withValue(
ContactsContract.Data.DATA2,
String.format(
context.resources.getString(
R.string.nc_phone_book_integration_chat_via
),
accountName
)
)
.build()
)
try {
context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
@ -322,8 +369,11 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
Log.e(javaClass.simpleName, "", e)
}
Log.d(TAG, "added new entry for contact $displayName (cloudId: $cloudId | lookupKey: $lookupKey" +
" | id: $id)")
Log.d(
TAG,
"added new entry for contact $displayName (cloudId: $cloudId | lookupKey: $lookupKey" +
" | id: $id)"
)
}
contactCursor.close()
}
@ -341,18 +391,21 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
}
private fun getDisplayNameFromDeviceContact(id: String?): String? {
var displayName:String? = null
val whereName = ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?"
var displayName: String? = null
val whereName =
ContactsContract.Data.MIMETYPE + " = ? AND " + ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?"
val whereNameParams = arrayOf(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE, id)
val nameCursor = context.contentResolver.query(
ContactsContract.Data.CONTENT_URI,
null,
whereName,
whereNameParams,
ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME)
ContactsContract.Data.CONTENT_URI,
null,
whereName,
whereNameParams,
ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME
)
if (nameCursor != null) {
while (nameCursor.moveToNext()) {
displayName = nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME))
displayName =
nameCursor.getString(nameCursor.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME))
}
nameCursor.close()
}
@ -362,11 +415,12 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
private fun getPhoneNumbersFromDeviceContact(id: String?): MutableList<String> {
val numbers = mutableListOf<String>()
val phonesNumbersCursor = context.contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
ContactsContract.Data.CONTACT_ID + " = " + id,
null,
null)
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
null,
ContactsContract.Data.CONTACT_ID + " = " + id,
null,
null
)
if (phonesNumbersCursor != null) {
while (phonesNumbersCursor.moveToNext()) {
@ -374,7 +428,7 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
}
phonesNumbersCursor.close()
}
if(numbers.size > 0){
if (numbers.size > 0) {
Log.d(TAG, "Found ${numbers.size} phone numbers for contact with id $id")
}
return numbers
@ -382,11 +436,11 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
fun deleteAllLinkedAccounts() {
val rawContactUri = ContactsContract.RawContacts.CONTENT_URI
.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
.build()
.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_NAME, accountName)
.appendQueryParameter(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType)
.build()
context.contentResolver.delete(rawContactUri, null, null)
Log.d(TAG, "deleted all linked accounts")
}
@ -398,42 +452,63 @@ class ContactAddressBookWorker(val context: Context, workerParameters: WorkerPar
const val DELETE_ALL = "DELETE_ALL"
fun run(context: Context) {
if (ContextCompat.checkSelfPermission(context,
Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context,
Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_CONTACTS
) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_CONTACTS
) == PackageManager.PERMISSION_GRANTED
) {
WorkManager
.getInstance()
.enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java)
.setInputData(Data.Builder().putBoolean(KEY_FORCE, false).build())
.build())
.getInstance()
.enqueue(
OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java)
.setInputData(Data.Builder().putBoolean(KEY_FORCE, false).build())
.build()
)
}
}
fun checkPermission(controller: Controller, context: Context): Boolean {
if (ContextCompat.checkSelfPermission(context,
Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context,
Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
controller.requestPermissions(arrayOf(Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS), REQUEST_PERMISSION)
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_CONTACTS
) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_CONTACTS
) != PackageManager.PERMISSION_GRANTED
) {
controller.requestPermissions(
arrayOf(
Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS
),
REQUEST_PERMISSION
)
return false
} else {
WorkManager
.getInstance()
.enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java)
.setInputData(Data.Builder().putBoolean(KEY_FORCE, true).build())
.build())
.getInstance()
.enqueue(
OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java)
.setInputData(Data.Builder().putBoolean(KEY_FORCE, true).build())
.build()
)
return true
}
}
fun deleteAll() {
WorkManager
.getInstance()
.enqueue(OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java)
.setInputData(Data.Builder().putBoolean(DELETE_ALL, true).build())
.build())
WorkManager
.getInstance()
.enqueue(
OneTimeWorkRequest.Builder(ContactAddressBookWorker::class.java)
.setInputData(Data.Builder().putBoolean(DELETE_ALL, true).build())
.build()
)
}
}
}

View file

@ -33,13 +33,16 @@ import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.database.user.UserUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import okhttp3.ResponseBody
import java.io.*
import java.io.BufferedInputStream
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerParameters) :
Worker(context, workerParameters) {
Worker(context, workerParameters) {
private var totalFileSize: Int = -1
@ -86,8 +89,9 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa
private fun downloadFile(currentUser: UserEntity, url: String, fileName: String): Result {
val downloadCall = ncApi.downloadFile(
ApiUtils.getCredentials(currentUser.username, currentUser.token),
url)
ApiUtils.getCredentials(currentUser.username, currentUser.token),
url
)
return executeDownload(downloadCall.execute().body(), fileName)
}
@ -152,6 +156,5 @@ class DownloadFileToCacheWorker(val context: Context, workerParameters: WorkerPa
const val KEY_FILE_SIZE = "KEY_FILE_SIZE"
const val PROGRESS = "PROGRESS"
const val SUCCESS = "SUCCESS"
}
}

View file

@ -23,7 +23,11 @@ package com.nextcloud.talk.jobs
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.work.*
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import autodagger.AutoInjector
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
@ -46,13 +50,12 @@ import retrofit2.Response
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.util.*
import java.util.ArrayList
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerParameters) :
Worker(context, workerParameters) {
Worker(context, workerParameters) {
@Inject
lateinit var ncApi: NcApi
@ -107,31 +110,37 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
return requestBody
}
private fun uploadFile(currentUser: UserEntity, ncTargetpath: String?, filename: String, roomToken: String?,
requestBody: RequestBody?, sourcefileUri: Uri) {
private fun uploadFile(
currentUser: UserEntity,
ncTargetpath: String?,
filename: String,
roomToken: String?,
requestBody: RequestBody?,
sourcefileUri: Uri
) {
ncApi.uploadFile(
ApiUtils.getCredentials(currentUser.username, currentUser.token),
ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, ncTargetpath, filename),
requestBody
ApiUtils.getCredentials(currentUser.username, currentUser.token),
ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, ncTargetpath, filename),
requestBody
)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<Response<GenericOverall>> {
override fun onSubscribe(d: Disposable) {
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<Response<GenericOverall>> {
override fun onSubscribe(d: Disposable) {
}
override fun onNext(t: Response<GenericOverall>) {
}
override fun onNext(t: Response<GenericOverall>) {
}
override fun onError(e: Throwable) {
Log.e(TAG, "failed to upload file $filename")
}
override fun onError(e: Throwable) {
Log.e(TAG, "failed to upload file $filename")
}
override fun onComplete() {
shareFile(roomToken, currentUser, ncTargetpath, filename)
copyFileToCache(sourcefileUri, filename)
}
})
override fun onComplete() {
shareFile(roomToken, currentUser, ncTargetpath, filename)
copyFileToCache(sourcefileUri, filename)
}
})
}
private fun copyFileToCache(sourceFileUri: Uri, filename: String) {
@ -151,13 +160,13 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
paths.add("$ncTargetpath/$filename")
val data = Data.Builder()
.putLong(KEY_INTERNAL_USER_ID, currentUser.id)
.putString(KEY_ROOM_TOKEN, roomToken)
.putStringArray(KEY_FILE_PATHS, paths.toTypedArray())
.build()
.putLong(KEY_INTERNAL_USER_ID, currentUser.id)
.putString(KEY_ROOM_TOKEN, roomToken)
.putStringArray(KEY_FILE_PATHS, paths.toTypedArray())
.build()
val shareWorker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java)
.setInputData(data)
.build()
.setInputData(data)
.build()
WorkManager.getInstance().enqueue(shareWorker)
}
@ -167,4 +176,4 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
const val NC_TARGETPATH = "NC_TARGETPATH"
const val ROOM_TOKEN = "ROOM_TOKEN"
}
}
}

View file

@ -25,7 +25,7 @@ package com.nextcloud.talk.models.json.chat
class ChatUtils {
companion object {
fun getParsedMessage(message: String?, messageParameters: HashMap<String?, HashMap<String?, String?>>?):
String? {
String? {
var resultMessage = message
if (messageParameters != null && messageParameters.size > 0) {
for (key in messageParameters.keys) {
@ -46,4 +46,3 @@ class ChatUtils {
}
}
}

View file

@ -22,11 +22,28 @@ package com.nextcloud.talk.models.json.converters
import com.bluelinelabs.logansquare.typeconverters.StringBasedTypeConverter
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.*
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_ENDED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_JOINED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_LEFT
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CALL_STARTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CONVERSATION_CREATED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.CONVERSATION_RENAMED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.DUMMY
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.FILE_SHARED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUESTS_ALLOWED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.GUESTS_DISALLOWED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_NONE
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_NON_MODERATORS
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.LOBBY_OPEN_TO_EVERYONE
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERATOR_DEMOTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.MODERATOR_PROMOTED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PARENT_MESSAGE_DELETED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_REMOVED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.PASSWORD_SET
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_ADDED
import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.USER_REMOVED
/*
conversation_created - {actor} created the conversation
conversation_renamed - {actor} renamed the conversation from "foo" to "bar"
call_joined - {actor} joined the call
@ -40,7 +57,6 @@ import com.nextcloud.talk.models.json.chat.ChatMessage.SystemMessageType.*
user_removed - {actor} removed {user} from the conversation
moderator_promoted - {actor} promoted {user} to moderator
moderator_demoted - {actor} demoted {user} from moderator
*/
class EnumSystemMessageTypeConverter : StringBasedTypeConverter<ChatMessage.SystemMessageType>() {
override fun getFromString(string: String): ChatMessage.SystemMessageType {

View file

@ -20,12 +20,10 @@
package com.nextcloud.talk.receivers
import android.app.NotificationChannelGroup
import android.app.NotificationManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
@ -34,7 +32,6 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.utils.NotificationUtils
import com.nextcloud.talk.utils.database.user.UserUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -50,16 +47,20 @@ class PackageReplacedReceiver : BroadcastReceiver() {
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
if (intent != null && intent.action != null &&
intent.action == "android.intent.action.MY_PACKAGE_REPLACED") {
intent.action == "android.intent.action.MY_PACKAGE_REPLACED"
) {
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
if (packageInfo.versionCode > 43 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = context.getSystemService(Context
.NOTIFICATION_SERVICE) as NotificationManager
val notificationManager = context.getSystemService(
Context
.NOTIFICATION_SERVICE
) as NotificationManager
if (!appPreferences.isNotificationChannelUpgradedToV2) {
for (notificationChannelGroup in notificationManager
.notificationChannelGroups) {
for (
notificationChannelGroup in notificationManager.notificationChannelGroups
) {
notificationManager.deleteNotificationChannelGroup(notificationChannelGroup.id)
}
@ -80,7 +81,6 @@ class PackageReplacedReceiver : BroadcastReceiver() {
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, "Failed to fetch package info")
}
}
}

View file

@ -32,7 +32,6 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.components.filebrowser.controllers.BrowserController
import com.nextcloud.talk.controllers.ChatController
class AttachmentDialog(val activity: Activity, var chatController: ChatController) : BottomSheetDialog(activity) {
@BindView(R.id.txt_attach_file_from_local)
@ -54,7 +53,7 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
var serverName = chatController.conversationUser?.serverName
attachFromCloud?.text = chatController.resources?.let {
if(serverName.isNullOrEmpty()){
if (serverName.isNullOrEmpty()) {
serverName = it.getString(R.string.nc_server_product_name)
}
String.format(it.getString(R.string.nc_upload_from_cloud), serverName)

View file

@ -30,12 +30,13 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.controllers.ProfileController
import com.nextcloud.talk.models.json.userprofile.Scope
class ScopeDialog(con: Context,
private val userInfoAdapter: ProfileController.UserInfoAdapter,
private val field: ProfileController.Field,
private val position: Int) :
BottomSheetDialog(con) {
class ScopeDialog(
con: Context,
private val userInfoAdapter: ProfileController.UserInfoAdapter,
private val field: ProfileController.Field,
private val position: Int
) :
BottomSheetDialog(con) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = layoutInflater.inflate(R.layout.dialog_scope, null)

View file

@ -32,7 +32,8 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.models.ImportAccount
import com.nextcloud.talk.models.database.UserEntity
import java.util.*
import java.util.ArrayList
import java.util.Arrays
object AccountUtils {
@ -60,14 +61,15 @@ object AccountUtils {
break
}
} else {
if (internalUserEntity.username == importAccount.username && (internalUserEntity
.baseUrl == "http://" + importAccount.baseUrl ||
internalUserEntity.baseUrl == "https://" + importAccount
.baseUrl)) {
if (internalUserEntity.username == importAccount.username &&
(
internalUserEntity.baseUrl == "http://" + importAccount.baseUrl ||
internalUserEntity.baseUrl == "https://" + importAccount.baseUrl
)
) {
accountFound = true
break
}
}
} else {
accountFound = true
@ -88,8 +90,12 @@ object AccountUtils {
val packageManager = context.packageManager
var appName = ""
try {
appName = packageManager.getApplicationLabel(packageManager.getApplicationInfo(packageName,
PackageManager.GET_META_DATA)) as String
appName = packageManager.getApplicationLabel(
packageManager.getApplicationInfo(
packageName,
PackageManager.GET_META_DATA
)
) as String
} catch (e: PackageManager.NameNotFoundException) {
Log.e(TAG, "Failed to get app name based on package")
}
@ -103,7 +109,10 @@ object AccountUtils {
val packageInfo = pm.getPackageInfo(context.getString(R.string.nc_import_accounts_from), 0)
if (packageInfo.versionCode >= 30060151) {
val ownSignatures = pm.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES).signatures
val filesAppSignatures = pm.getPackageInfo(context.getString(R.string.nc_import_accounts_from), PackageManager.GET_SIGNATURES).signatures
val filesAppSignatures = pm.getPackageInfo(
context.getString(R.string.nc_import_accounts_from),
PackageManager.GET_SIGNATURES
).signatures
if (Arrays.equals(ownSignatures, filesAppSignatures)) {
val accMgr = AccountManager.get(context)
@ -118,7 +127,7 @@ object AccountUtils {
}
}
} catch (appNotFoundException: PackageManager.NameNotFoundException) {
// ignore
}
return false
@ -146,4 +155,3 @@ object AccountUtils {
return ImportAccount(username, password, urlString)
}
}

View file

@ -27,7 +27,13 @@ import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
import com.nextcloud.talk.controllers.ChatController
object ConductorRemapping {
fun remapChatController(router: Router, internalUserId: Long, roomTokenOrId: String, bundle: Bundle, replaceTop: Boolean) {
fun remapChatController(
router: Router,
internalUserId: Long,
roomTokenOrId: String,
bundle: Bundle,
replaceTop: Boolean
) {
val tag = "$internalUserId@$roomTokenOrId"
if (router.getControllerWithTag(tag) != null) {
val backstack = router.backstack
@ -44,13 +50,17 @@ object ConductorRemapping {
router.setBackstack(backstack, HorizontalChangeHandler())
} else {
if (!replaceTop) {
router.pushController(RouterTransaction.with(ChatController(bundle))
router.pushController(
RouterTransaction.with(ChatController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()).tag(tag))
.popChangeHandler(HorizontalChangeHandler()).tag(tag)
)
} else {
router.replaceTopController(RouterTransaction.with(ChatController(bundle))
router.replaceTopController(
RouterTransaction.with(ChatController(bundle))
.pushChangeHandler(HorizontalChangeHandler())
.popChangeHandler(HorizontalChangeHandler()).tag(tag))
.popChangeHandler(HorizontalChangeHandler()).tag(tag)
)
}
}
}

View file

@ -21,8 +21,9 @@
package com.nextcloud.talk.utils
import java.text.DateFormat
import java.util.*
import java.util.Calendar
import java.util.Date
import java.util.Locale
object DateUtils {
fun getLocalDateTimeStringFromTimestamp(timestamp: Long): String {
@ -30,14 +31,16 @@ object DateUtils {
val tz = cal.timeZone
/* date formatter in local timezone */
val format = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT, Locale
.getDefault())
val format = DateFormat.getDateTimeInstance(
DateFormat.DEFAULT, DateFormat.SHORT,
Locale.getDefault()
)
format.timeZone = tz
return format.format(Date(timestamp))
}
fun getLocalDateStringFromTimestampForLobby(timestamp: Long): String {
return getLocalDateTimeStringFromTimestamp(timestamp * 1000);
return getLocalDateTimeStringFromTimestamp(timestamp * 1000)
}
}

View file

@ -21,12 +21,10 @@
package com.nextcloud.talk.utils
import com.nextcloud.talk.R
import java.util.HashMap
object DrawableUtils {
fun getDrawableResourceIdForMimeType(mimetype: String): Int {
var localMimetype = mimetype
val drawableMap = HashMap<String, Int>()
@ -55,15 +53,20 @@ object DrawableUtils {
drawableMap["application/vnd.google-earth.kmz"] = R.drawable.ic_mimetype_location
drawableMap["application/vnd.ms-excel"] = R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.ms-excel.addin.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.ms-excel.sheet.binary.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.ms-excel.sheet.binary.macroEnabled.12"] =
R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.ms-excel.sheet.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.ms-excel.template.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.ms-fontobject"] = R.drawable.ic_mimetype_image
drawableMap["application/vnd.ms-powerpoint"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.ms-powerpoint.addin.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.ms-powerpoint.presentation.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.ms-powerpoint.slideshow.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.ms-powerpoint.template.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.ms-powerpoint.addin.macroEnabled.12"] =
R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.ms-powerpoint.presentation.macroEnabled.12"] =
R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.ms-powerpoint.slideshow.macroEnabled.12"] =
R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.ms-powerpoint.template.macroEnabled.12"] =
R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.ms-visio.drawing.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.ms-visio.drawing"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.ms-visio.stencil.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document
@ -72,20 +75,29 @@ object DrawableUtils {
drawableMap["application/vnd.ms-visio.template"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.ms-word.template.macroEnabled.12"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.oasis.opendocument.presentation"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.oasis.opendocument.presentation-template"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.oasis.opendocument.presentation-template"] =
R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.oasis.opendocument.spreadsheet"] = R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.oasis.opendocument.spreadsheet-template"] = R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.oasis.opendocument.spreadsheet-template"] =
R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.oasis.opendocument.text"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.oasis.opendocument.text-master"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.oasis.opendocument.text-template"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.oasis.opendocument.text-web"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.openxmlformats-officedocument.presentationml.presentation"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.openxmlformats-officedocument.presentationml.slideshow"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.openxmlformats-officedocument.presentationml.template"] = R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] = R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.template"] = R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.template"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.openxmlformats-officedocument.presentationml.presentation"] =
R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.openxmlformats-officedocument.presentationml.slideshow"] =
R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.openxmlformats-officedocument.presentationml.template"] =
R.drawable.ic_mimetype_x_office_presentation
drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"] =
R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.openxmlformats-officedocument.spreadsheetml.template"] =
R.drawable.ic_mimetype_x_office_spreadsheet
drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.document"] =
R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.openxmlformats-officedocument.wordprocessingml.template"] =
R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.visio"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/vnd.wordperfect"] = R.drawable.ic_mimetype_x_office_document
drawableMap["application/x-7z-compressed"] = R.drawable.ic_mimetype_package_x_generic

View file

@ -29,7 +29,7 @@ import com.nextcloud.talk.BuildConfig
import java.io.FileNotFoundException
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
object LoggingUtils {
fun writeLogEntryToFile(context: Context, logEntry: String) {
@ -38,8 +38,10 @@ object LoggingUtils {
val logEntryWithDateTime = dateFormat.format(date) + ": " + logEntry + "\n"
try {
val outputStream = context.openFileOutput("nc_log.txt",
Context.MODE_PRIVATE or Context.MODE_APPEND)
val outputStream = context.openFileOutput(
"nc_log.txt",
Context.MODE_PRIVATE or Context.MODE_APPEND
)
outputStream.write(logEntryWithDateTime.toByteArray())
outputStream.flush()
outputStream.close()
@ -48,13 +50,12 @@ object LoggingUtils {
} catch (e: IOException) {
e.printStackTrace()
}
}
fun sendMailWithAttachment(context: Context) {
val logFile = context.getFileStreamPath("nc_log.txt")
val emailIntent = Intent(Intent.ACTION_SEND)
val mailto = "mario@nextcloud.com"
val mailto = "android@nextcloud.com"
emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(mailto))
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Talk logs")
emailIntent.type = "text/plain"

View file

@ -33,7 +33,7 @@ import android.service.notification.StatusBarNotification
import com.nextcloud.talk.R
import com.nextcloud.talk.models.database.UserEntity
import com.nextcloud.talk.utils.bundle.BundleKeys
import java.util.*
import java.util.Objects
object NotificationUtils {
val NOTIFICATION_CHANNEL_CALLS = "NOTIFICATION_CHANNEL_CALLS"
@ -47,25 +47,50 @@ object NotificationUtils {
return longArrayOf(0L, 400L, 800L, 600L, 800L, 800L, 800L, 1000L)
}
fun getNotificationChannelId(channelName: String,
channelDescription: String, enableLights: Boolean,
importance: Int, sound: Uri, audioAttributes: AudioAttributes, vibrationPattern: LongArray?, bypassDnd: Boolean): String {
return Objects.hash(channelName, channelDescription, enableLights, importance, sound, audioAttributes, vibrationPattern, bypassDnd).toString()
fun getNotificationChannelId(
channelName: String,
channelDescription: String,
enableLights: Boolean,
importance: Int,
sound: Uri,
audioAttributes: AudioAttributes,
vibrationPattern: LongArray?,
bypassDnd: Boolean
): String {
return Objects.hash(
channelName,
channelDescription,
enableLights,
importance,
sound,
audioAttributes,
vibrationPattern,
bypassDnd
).toString()
}
@TargetApi(Build.VERSION_CODES.O)
fun createNotificationChannel(context: Context,
channelId: String, channelName: String,
channelDescription: String, enableLights: Boolean,
importance: Int, sound: Uri, audioAttributes: AudioAttributes,
vibrationPattern: LongArray?, bypassDnd: Boolean = false) {
fun createNotificationChannel(
context: Context,
channelId: String,
channelName: String,
channelDescription: String,
enableLights: Boolean,
importance: Int,
sound: Uri,
audioAttributes: AudioAttributes,
vibrationPattern: LongArray?,
bypassDnd: Boolean = false
) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && notificationManager.getNotificationChannel(channelId) == null) {
val channel = NotificationChannel(channelId, channelName,
importance)
val channel = NotificationChannel(
channelId, channelName,
importance
)
channel.description = channelDescription
channel.enableLights(enableLights)
@ -84,8 +109,11 @@ object NotificationUtils {
}
@TargetApi(Build.VERSION_CODES.O)
fun createNotificationChannelGroup(context: Context,
groupId: String, groupName: CharSequence) {
fun createNotificationChannelGroup(
context: Context,
groupId: String,
groupName: CharSequence
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -113,12 +141,12 @@ object NotificationUtils {
}
}
}
}
fun cancelExistingNotificationWithId(context: Context?, conversationUser: UserEntity, notificationId: Long) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L &&
context != null) {
context != null
) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -128,7 +156,10 @@ object NotificationUtils {
notification = statusBarNotification.notification
if (notification != null && !notification.extras.isEmpty) {
if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && notificationId == notification.extras.getLong(BundleKeys.KEY_NOTIFICATION_ID)) {
if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && notificationId == notification.extras.getLong(
BundleKeys.KEY_NOTIFICATION_ID
)
) {
notificationManager.cancel(statusBarNotification.id)
}
}
@ -136,11 +167,14 @@ object NotificationUtils {
}
}
fun findNotificationForRoom(context: Context?,
conversationUser: UserEntity,
roomTokenOrId: String): StatusBarNotification? {
fun findNotificationForRoom(
context: Context?,
conversationUser: UserEntity,
roomTokenOrId: String
): StatusBarNotification? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L &&
context != null) {
context != null
) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -150,7 +184,10 @@ object NotificationUtils {
notification = statusBarNotification.notification
if (notification != null && !notification.extras.isEmpty) {
if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && roomTokenOrId == statusBarNotification.notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)) {
if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && roomTokenOrId == statusBarNotification.notification.extras.getString(
BundleKeys.KEY_ROOM_TOKEN
)
) {
return statusBarNotification
}
}
@ -160,10 +197,14 @@ object NotificationUtils {
return null
}
fun cancelExistingNotificationsForRoom(context: Context?, conversationUser: UserEntity,
roomTokenOrId: String) {
fun cancelExistingNotificationsForRoom(
context: Context?,
conversationUser: UserEntity,
roomTokenOrId: String
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && conversationUser.id != -1L &&
context != null) {
context != null
) {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@ -173,7 +214,11 @@ object NotificationUtils {
notification = statusBarNotification.notification
if (notification != null && !notification.extras.isEmpty) {
if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) && roomTokenOrId == statusBarNotification.notification.extras.getString(BundleKeys.KEY_ROOM_TOKEN)) {
if (conversationUser.id == notification.extras.getLong(BundleKeys.KEY_INTERNAL_USER_ID) &&
roomTokenOrId == statusBarNotification.notification.extras.getString(
BundleKeys.KEY_ROOM_TOKEN
)
) {
notificationManager.cancel(statusBarNotification.id)
}
}

View file

@ -25,7 +25,6 @@ import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
object UriUtils {
@ -51,5 +50,4 @@ object UriUtils {
}
return filename
}
}

View file

@ -21,8 +21,8 @@
package com.nextcloud.talk.utils.bundle
object BundleKeys {
val KEY_SELECTED_USERS = "KEY_SELECTED_USERS";
val KEY_SELECTED_GROUPS = "KEY_SELECTED_GROUPS";
val KEY_SELECTED_USERS = "KEY_SELECTED_USERS"
val KEY_SELECTED_GROUPS = "KEY_SELECTED_GROUPS"
val KEY_USERNAME = "KEY_USERNAME"
val KEY_TOKEN = "KEY_TOKEN"
val KEY_BASE_URL = "KEY_BASE_URL"

View file

@ -13,11 +13,17 @@ import java.io.IOException
import java.net.InetAddress
import java.net.Socket
import java.security.GeneralSecurityException
import java.util.*
import javax.net.ssl.*
import java.util.LinkedList
import javax.net.ssl.KeyManager
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
class SSLSocketFactoryCompat(keyManager: KeyManager?,
trustManager: X509TrustManager) : SSLSocketFactory() {
class SSLSocketFactoryCompat(
keyManager: KeyManager?,
trustManager: X509TrustManager
) : SSLSocketFactory() {
private var delegate: SSLSocketFactory
@ -50,24 +56,24 @@ class SSLSocketFactoryCompat(keyManager: KeyManager?,
/* set up reasonable cipher suites */
val knownCiphers = arrayOf<String>(
// TLS 1.2
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
// maximum interoperability
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
"SSL_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_RSA_WITH_AES_128_CBC_SHA",
// additionally
"TLS_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
// TLS 1.2
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
// maximum interoperability
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
"SSL_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_RSA_WITH_AES_128_CBC_SHA",
// additionally
"TLS_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
)
val availableCiphers = socket.supportedCipherSuites
@ -89,31 +95,31 @@ class SSLSocketFactoryCompat(keyManager: KeyManager?,
} catch (e: IOException) {
// Exception is to be ignored
} finally {
socket?.close() // doesn't implement Closeable on all supported Android versions
socket?.close() // doesn't implement Closeable on all supported Android versions
}
}
}
}
init {
try {
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(
if (keyManager != null) arrayOf(keyManager) else null,
arrayOf(trustManager),
null)
if (keyManager != null) arrayOf(keyManager) else null,
arrayOf(trustManager),
null
)
delegate = sslContext.socketFactory
} catch (e: GeneralSecurityException) {
throw IllegalStateException() // system has no TLS
throw IllegalStateException() // system has no TLS
}
}
override fun getDefaultCipherSuites(): Array<String>? = cipherSuites
?: delegate.defaultCipherSuites
?: delegate.defaultCipherSuites
override fun getSupportedCipherSuites(): Array<String>? = cipherSuites
?: delegate.supportedCipherSuites
?: delegate.supportedCipherSuites
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
val ssl = delegate.createSocket(s, host, port, autoClose)
@ -150,10 +156,8 @@ class SSLSocketFactoryCompat(keyManager: KeyManager?,
return ssl
}
private fun upgradeTLS(ssl: SSLSocket) {
protocols?.let { ssl.enabledProtocols = it }
cipherSuites?.let { ssl.enabledCipherSuites = it }
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,36 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.utils;
import com.nextcloud.talk.interfaces.ClosedInterface;
public class ClosedInterfaceImpl implements ClosedInterface {
@Override
public void providerInstallerInstallIfNeededAsync() {
// does absolutely nothing :)
}
@Override
public boolean isGooglePlayServicesAvailable() {
return false;
}
}

View file

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.08035714"
android:scaleY="0.08035714">
<path
android:pathData="M0,0h1344v1344h-1344z"
android:strokeLineJoin="round"
android:fillType="evenOdd">
<aapt:attr name="android:fillColor">
<gradient
android:startY="1344.0002"
android:startX="163.34073"
android:endY="1.2959057E-4"
android:endX="1343.9999"
android:type="linear">
<item android:offset="0" android:color="#FF0082C9"/>
<item android:offset="1" android:color="#FF1CAFFF"/>
</gradient>
</aapt:attr>
</path>
</group>
</vector>

View file

@ -0,0 +1,37 @@
<!--
~ /*
~ * Nextcloud Talk application
~ *
~ * @author Mario Danic
~ * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
~ *
~ * This program is free software: you can redistribute it and/or modify
~ * it under the terms of the GNU General Public License as published by
~ * the Free Software Foundation, either version 3 of the License, or
~ * at your option) any later version.
~ *
~ * This program is distributed in the hope that it will be useful,
~ * but WITHOUT ANY WARRANTY; without even the implied warranty of
~ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ * GNU General Public License for more details.
~ *
~ * You should have received a copy of the GNU General Public License
~ * along with this program. If not, see <http://www.gnu.org/licenses/>.
~ */
-->
<vector android:autoMirrored="true" android:height="108dp"
android:viewportHeight="1344" android:viewportWidth="1344"
android:width="108dp" xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillType="evenOdd"
android:pathData="M0,0h1344v1344h-1344z" android:strokeLineJoin="round">
<aapt:attr name="android:fillColor">
<gradient android:endX="1343.9999"
android:endY="1.2959057E-4" android:startX="163.34073"
android:startY="1344.0002" android:type="linear">
<item android:color="#FF0082C9" android:offset="0"/>
<item android:color="#FF1CAFFF" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
</vector>

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="0.06428572"
android:scaleY="0.06428572"
android:translateX="10.8"
android:translateY="10.8">
<path
android:pathData="M671.96,347C493.7,347 347.02,493.69 347.02,671.95L347.02,671.96C347.02,850.22 493.7,996.91 671.96,996.91C731.43,996.79 789.75,980.34 840.52,949.37C880.46,965.24 969.91,1012.33 991.21,991.99C1013.45,970.74 965.09,870.74 953.49,833.58C981.82,784.43 996.79,728.7 996.9,671.96C996.9,493.7 850.22,347.02 671.96,347.01L671.96,347ZM672,470.54C782.53,470.54 873.48,561.5 873.48,672.03C873.48,782.57 782.52,873.51 672,873.51C561.47,873.51 470.52,782.57 470.51,672.03C470.51,561.5 561.46,470.54 672,470.54L672,470.54ZM670.27,655.83C670.27,674.12 666.59,689.32 659.24,701.43C651.88,713.54 641.5,721.74 628.09,726.04L662.07,761.39L637.95,761.39L610.12,729.17L604.75,729.36C583.72,729.36 567.49,722.93 556.06,710.08C544.64,697.22 538.93,679.07 538.93,655.63C538.93,632.39 544.66,614.37 556.11,601.58C567.57,588.79 583.85,582.39 604.94,582.39C625.45,582.39 641.47,588.9 652.99,601.92C664.51,614.94 670.27,632.91 670.27,655.83ZM787.85,727.41L770.08,682L712.85,682L695.27,727.41L678.48,727.41L734.92,584.05L748.89,584.05L805.04,727.41L787.85,727.41ZM556.5,655.83C556.5,675.16 560.62,689.83 568.86,699.82C577.09,709.82 589.06,714.81 604.75,714.81C620.57,714.81 632.51,709.83 640.59,699.87C648.66,689.91 652.7,675.23 652.7,655.83C652.7,636.62 648.67,622.05 640.64,612.13C632.59,602.2 620.7,597.23 604.94,597.23C589.12,597.23 577.09,602.23 568.86,612.22C560.62,622.22 556.5,636.75 556.5,655.83ZM764.9,667.06L748.3,622.82C746.15,617.22 743.94,610.35 741.66,602.21C740.23,608.46 738.18,615.33 735.51,622.82L718.71,667.06L764.9,667.06Z"
android:fillColor="#ffffff"
android:fillType="nonZero"/>
</group>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Andy Scherzinger
~ Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<resources>
<string name="nc_app_name">Nextcloud Talk QA</string>
<string name="nc_server_product_name">Nextcloud</string>
</resources>

View file

@ -34,6 +34,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.13.1"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

472
detekt.yml Normal file
View file

@ -0,0 +1,472 @@
build:
maxIssues: 346
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
processors:
active: true
exclude:
# - 'FunctionCountProcessor'
# - 'PropertyCountProcessor'
# - 'ClassCountProcessor'
# - 'PackageCountProcessor'
# - 'KtFileCountProcessor'
console-reports:
active: true
exclude:
# - 'ProjectStatisticsReport'
# - 'ComplexityReport'
# - 'NotificationReport'
# - 'FindingsReport'
# - 'BuildFailureReport'
comments:
active: true
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$)
UndocumentedPublicClass:
active: false
searchInNestedClass: true
searchInInnerClass: true
searchInInnerObject: true
searchInInnerInterface: true
UndocumentedPublicFunction:
active: false
complexity:
active: true
ComplexCondition:
active: true
threshold: 4
ComplexInterface:
active: false
threshold: 10
includeStaticDeclarations: false
ComplexMethod:
active: true
threshold: 10
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
excludes: ['**/androidTest/**']
LabeledExpression:
active: false
ignoredLabels: ""
LargeClass:
active: true
threshold: 600
LongMethod:
active: true
threshold: 60
excludes: ['**/androidTest/**']
LongParameterList:
active: true
threshold: 6
ignoreDefaultParameters: false
MethodOverloading:
active: false
threshold: 6
NestedBlockDepth:
active: true
threshold: 4
StringLiteralDuplication:
active: false
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: true
thresholdInFiles: 15
thresholdInClasses: 15
thresholdInInterfaces: 15
thresholdInObjects: 15
thresholdInEnums: 11
ignoreDeprecated: true
ignorePrivate: false
ignoreOverridden: true
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: true
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: false
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: false
methodNames: 'toString,hashCode,equals,finalize'
InstanceOfCheckForException:
active: false
NotImplementedDeclaration:
active: false
PrintStackTrace:
active: false
RethrowCaughtException:
active: false
ReturnFromFinally:
active: false
SwallowedException:
active: false
ignoredExceptionTypes: 'InterruptedException,NumberFormatException,ParseException,MalformedURLException'
ThrowingExceptionFromFinally:
active: false
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: false
exceptions: 'IllegalArgumentException,IllegalStateException,IOException'
ThrowingNewInstanceOfSameException:
active: false
TooGenericExceptionCaught:
active: true
exceptionNames:
- ArrayIndexOutOfBoundsException
- Error
- Exception
- IllegalMonitorStateException
- NullPointerException
- IndexOutOfBoundsException
- RuntimeException
- Throwable
allowedExceptionNameRegex: "^(_|(ignore|expected).*)"
TooGenericExceptionThrown:
active: true
exceptionNames:
- Error
- Exception
- Throwable
- RuntimeException
formatting:
active: true
android: false
ChainWrapping:
active: true
CommentSpacing:
active: true
Filename:
active: true
FinalNewline:
active: true
ImportOrdering:
active: false
Indentation:
active: true
indentSize: 4
continuationIndentSize: 4
MaximumLineLength:
active: true
maxLineLength: 120
ModifierOrdering:
active: true
NoBlankLineBeforeRbrace:
active: true
NoConsecutiveBlankLines:
active: true
NoEmptyClassBody:
active: true
NoLineBreakAfterElse:
active: true
NoLineBreakBeforeAssignment:
active: true
NoMultipleSpaces:
active: true
NoSemicolons:
active: true
NoTrailingSpaces:
active: true
NoUnitReturn:
active: true
NoUnusedImports:
active: true
NoWildcardImports:
active: true
PackageName:
active: true
ParameterListWrapping:
active: true
indentSize: 4
SpacingAroundColon:
active: true
SpacingAroundComma:
active: true
SpacingAroundCurly:
active: true
SpacingAroundKeyword:
active: true
SpacingAroundOperators:
active: true
SpacingAroundParens:
active: true
SpacingAroundRangeOperator:
active: true
StringTemplate:
active: true
naming:
active: true
ClassNaming:
active: true
classPattern: '[A-Z$][a-zA-Z0-9$]*'
ConstructorParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
EnumNaming:
active: true
enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
forbiddenName: ''
FunctionMaxLength:
active: false
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
minimumFunctionNameLength: 3
FunctionNaming:
active: true
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
excludeClassPattern: '$^'
ignoreOverridden: true
excludes: "**/*Test.kt"
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
MatchingDeclarationName:
active: true
MemberNameEqualsClassName:
active: false
ignoreOverridden: true
ObjectPropertyNaming:
active: true
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
packagePattern: '^[a-z]+(\.[a-z][A-Za-z0-9]*)*$'
TopLevelPropertyNaming:
active: true
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*'
VariableMaxLength:
active: false
maximumVariableNameLength: 64
VariableMinLength:
active: false
minimumVariableNameLength: 1
VariableNaming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
performance:
active: true
ArrayPrimitive:
active: false
ForEachOnRange:
active: true
SpreadOperator:
active: true
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
DuplicateCaseInWhenExpression:
active: true
EqualsAlwaysReturnsTrueOrFalse:
active: false
EqualsWithHashCodeExist:
active: true
ExplicitGarbageCollectionCall:
active: true
InvalidRange:
active: false
IteratorHasNextCallsNextMethod:
active: false
IteratorNotThrowingNoSuchElementException:
active: false
LateinitUsage:
active: false
excludeAnnotatedProperties: ""
ignoreOnClassesPattern: ""
UnconditionalJumpStatementInLoop:
active: false
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: false
UnsafeCast:
active: false
UselessPostfixExpression:
active: false
WrongEqualsTypeParameter:
active: false
style:
active: true
CollapsibleIfStatements:
active: false
DataClassContainsFunctions:
active: false
conversionFunctionPrefix: 'to'
EqualsNullCall:
active: false
EqualsOnSignatureLine:
active: false
ExplicitItLambdaParameter:
active: false
ExpressionBodySyntax:
active: false
includeLineWrapping: false
ForbiddenComment:
active: true
values: 'TODO:,FIXME:,STOPSHIP:'
ForbiddenImport:
active: false
imports: ''
ForbiddenVoid:
active: false
FunctionOnlyReturningConstant:
active: false
ignoreOverridableFunction: true
excludedFunctions: 'describeContents'
LoopWithTooManyJumpStatements:
active: false
maxJumpCount: 1
MagicNumber:
active: true
ignoreNumbers: '-1,0,1,2'
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
excludes: "**/*Test.kt"
MandatoryBracesIfStatements:
active: false
MaxLineLength:
active: true
maxLineLength: 120
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
MayBeConst:
active: false
ModifierOrder:
active: true
NestedClassesVisibility:
active: false
NewLineAtEndOfFile:
active: true
NoTabs:
active: false
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: false
OptionalWhenBraces:
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:
active: false
RedundantVisibilityModifierRule:
active: false
ReturnCount:
active: true
max: 2
excludedFunctions: "equals"
excludeLabeled: false
excludeReturnFromLambda: true
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: false
SpacingBetweenPackageAndImports:
active: false
ThrowsCount:
active: true
max: 2
TrailingWhitespace:
active: false
UnderscoresInNumericLiterals:
active: false
acceptableDecimalLength: 5
UnnecessaryAbstractClass:
active: false
excludeAnnotatedClasses: "dagger.Module"
UnnecessaryApply:
active: false
UnnecessaryInheritance:
active: false
UnnecessaryLet:
active: false
UnnecessaryParentheses:
active: false
UntilInsteadOfRangeTo:
active: false
UnusedImports:
active: false
UnusedPrivateClass:
active: false
UnusedPrivateMember:
active: false
allowedNames: "(_|ignored|expected|serialVersionUID)"
UseDataClass:
active: false
excludeAnnotatedClasses: ""
UtilityClassWithPublicConstructor:
active: false
VarCouldBeVal:
active: false
WildcardImport:
active: true
excludeImports: 'java.util.*,kotlinx.android.synthetic.*'

View file

@ -1,309 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1344 1344" version="1.1" xmlns="http://www.w3.org/2000/svg" xml:space="preserve"
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
<rect x="-30.496" y="-19.169" width="1488.2" height="1362.73" style="fill:rgb(0,130,201);"/>
<g id="g13338" transform="matrix(5.13054,0,0,5.13054,-5362.99,82.5409)">
<path id="path256" d="M1298,54.1C1299.6,54.1 1300.9,52.8 1300.9,51.2C1300.9,49.6 1299.6,48.3 1298,48.3C1296.4,48.3 1295.1,49.6 1295.1,51.2C1295.1,52.8 1296.4,54.1 1298,54.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path258" d="M1307.5,54.1C1309.1,54.1 1310.4,52.8 1310.4,51.2C1310.4,49.6 1309.1,48.3 1307.5,48.3C1305.9,48.3 1304.6,49.6 1304.6,51.2C1304.6,52.8 1305.9,54.1 1307.5,54.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path310" d="M1298,63.5C1299.6,63.5 1300.9,62.2 1300.9,60.6C1300.9,59 1299.6,57.7 1298,57.7C1296.4,57.7 1295.1,59 1295.1,60.6C1295.1,62.2 1296.4,63.5 1298,63.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path312" d="M1307.5,63.5C1309.1,63.5 1310.4,62.2 1310.4,60.6C1310.4,59 1309.1,57.7 1307.5,57.7C1305.9,57.7 1304.6,59 1304.6,60.6C1304.6,62.2 1305.9,63.5 1307.5,63.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path366" d="M1260.3,73C1262,73 1263.3,71.7 1263.3,70C1263.3,68.4 1262,67 1260.3,67C1258.7,67 1257.3,68.3 1257.3,70C1257.3,71.7 1258.7,73 1260.3,73Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path368" d="M1279.2,72.9C1280.8,72.9 1282.1,71.6 1282.1,70C1282.1,68.4 1280.8,67.1 1279.2,67.1C1277.6,67.1 1276.3,68.4 1276.3,70C1276.3,71.6 1277.6,72.9 1279.2,72.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path370" d="M1288.6,73.5C1290.5,73.5 1292,72 1292,70.1C1292,68.2 1290.5,66.7 1288.6,66.7C1286.7,66.7 1285.2,68.2 1285.2,70.1C1285.2,71.9 1286.7,73.5 1288.6,73.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path372" d="M1298,73C1299.6,73 1301,71.7 1301,70.1C1301,68.5 1299.7,67.2 1298,67.2C1296.4,67.2 1295.1,68.5 1295.1,70.1C1295.1,71.7 1296.4,73 1298,73Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path374" d="M1307.5,73C1309.2,73 1310.5,71.7 1310.5,70C1310.5,68.4 1309.2,67 1307.5,67C1305.8,67 1304.5,68.3 1304.5,70C1304.5,71.7 1305.8,73 1307.5,73Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path438" d="M1250.8,82.4C1252.4,82.4 1253.7,81.1 1253.7,79.5C1253.7,77.9 1252.4,76.6 1250.8,76.6C1249.2,76.6 1247.9,77.9 1247.9,79.5C1248,81.1 1249.3,82.4 1250.8,82.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path440" d="M1260.3,82.4C1262,82.4 1263.3,81.1 1263.3,79.4C1263.3,77.8 1262,76.4 1260.3,76.4C1258.7,76.4 1257.3,77.7 1257.3,79.4C1257.3,81.1 1258.7,82.4 1260.3,82.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path442" d="M1279.2,82.4C1280.8,82.4 1282.1,81.1 1282.1,79.5C1282.1,77.9 1280.8,76.6 1279.2,76.6C1277.6,76.6 1276.3,77.9 1276.3,79.5C1276.3,81.1 1277.6,82.4 1279.2,82.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path444" d="M1288.6,82.4C1290.2,82.4 1291.5,81.1 1291.5,79.5C1291.5,77.9 1290.2,76.6 1288.6,76.6C1287,76.6 1285.7,77.9 1285.7,79.5C1285.7,81.1 1287,82.4 1288.6,82.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path446" d="M1307.5,82.4C1309.2,82.4 1310.5,81.1 1310.5,79.4C1310.5,77.8 1309.2,76.4 1307.5,76.4C1305.8,76.4 1304.5,77.7 1304.5,79.4C1304.5,81.1 1305.8,82.4 1307.5,82.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path504" d="M1279.2,91.8C1280.8,91.8 1282.1,90.5 1282.1,88.9C1282.1,87.3 1280.8,86 1279.2,86C1277.6,86 1276.3,87.3 1276.3,88.9C1276.3,90.5 1277.6,91.8 1279.2,91.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path564" d="M1260.3,101.2C1261.9,101.2 1263.2,99.9 1263.2,98.3C1263.2,96.7 1261.9,95.4 1260.3,95.4C1258.7,95.4 1257.4,96.7 1257.4,98.3C1257.4,99.9 1258.7,101.2 1260.3,101.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path566" d="M1269.7,101.3C1271.3,101.3 1272.6,100 1272.6,98.4C1272.6,96.8 1271.3,95.5 1269.7,95.5C1268.1,95.5 1266.7,96.8 1266.7,98.4C1266.8,100 1268.1,101.3 1269.7,101.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path568" d="M1279.2,101.4C1280.9,101.4 1282.3,100 1282.3,98.3C1282.3,96.6 1280.9,95.2 1279.2,95.2C1277.5,95.2 1276.1,96.6 1276.1,98.3C1276.1,100.1 1277.4,101.4 1279.2,101.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path570" d="M1288.6,101.7C1290.5,101.7 1292,100.2 1292,98.3C1292,96.5 1290.5,94.9 1288.6,94.9C1286.8,94.9 1285.3,96.4 1285.3,98.3C1285.2,100.2 1286.7,101.7 1288.6,101.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path572" d="M1298,102.1C1300.1,102.1 1301.7,100.4 1301.7,98.4C1301.7,96.3 1300,94.7 1298,94.7C1296,94.7 1294.3,96.4 1294.3,98.4C1294.3,100.4 1296,102.1 1298,102.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path574" d="M1307.5,101.2C1309.1,101.2 1310.4,99.9 1310.4,98.3C1310.4,96.7 1309.1,95.4 1307.5,95.4C1305.9,95.4 1304.6,96.7 1304.6,98.3C1304.6,99.9 1305.9,101.2 1307.5,101.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path642" d="M1250.8,110.7C1252.4,110.7 1253.7,109.4 1253.7,107.8C1253.7,106.2 1252.4,104.9 1250.8,104.9C1249.2,104.9 1247.9,106.2 1247.9,107.8C1248,109.4 1249.3,110.7 1250.8,110.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path644" d="M1260.3,110.8C1262,110.8 1263.3,109.5 1263.3,107.8C1263.3,106.2 1262,104.8 1260.3,104.8C1258.7,104.8 1257.3,106.1 1257.3,107.8C1257.3,109.4 1258.7,110.8 1260.3,110.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path646" d="M1269.7,110.8C1271.3,110.8 1272.7,109.5 1272.7,107.8C1272.7,106.2 1271.4,104.8 1269.7,104.8C1268,104.8 1266.7,106.1 1266.7,107.8C1266.8,109.4 1268.1,110.8 1269.7,110.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path648" d="M1279.2,111.5C1281.2,111.5 1282.9,109.8 1282.9,107.8C1282.9,105.8 1281.2,104.1 1279.2,104.1C1277.2,104.1 1275.5,105.8 1275.5,107.8C1275.4,109.8 1277.1,111.5 1279.2,111.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path650" d="M1288.6,110.7C1290.2,110.7 1291.5,109.4 1291.5,107.8C1291.5,106.2 1290.2,104.9 1288.6,104.9C1287,104.9 1285.7,106.2 1285.7,107.8C1285.7,109.4 1287,110.7 1288.6,110.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path652" d="M1298,111.2C1299.9,111.2 1301.4,109.7 1301.4,107.8C1301.4,105.9 1299.9,104.4 1298,104.4C1296.1,104.4 1294.6,105.9 1294.6,107.8C1294.6,109.7 1296.1,111.2 1298,111.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path654" d="M1307.5,111.8C1309.7,111.8 1311.5,110 1311.5,107.8C1311.5,105.6 1309.7,103.8 1307.5,103.8C1305.3,103.8 1303.5,105.6 1303.5,107.8C1303.5,110 1305.2,111.8 1307.5,111.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path722" d="M1250.8,120.8C1252.8,120.8 1254.4,119.2 1254.4,117.2C1254.4,115.2 1252.8,113.6 1250.8,113.6C1248.8,113.6 1247.2,115.2 1247.2,117.2C1247.3,119.2 1248.9,120.8 1250.8,120.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path724" d="M1260.3,120.2C1262,120.2 1263.3,118.9 1263.3,117.2C1263.3,115.6 1262,114.2 1260.3,114.2C1258.7,114.2 1257.3,115.5 1257.3,117.2C1257.3,118.9 1258.7,120.2 1260.3,120.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path726" d="M1269.7,120.2C1271.3,120.2 1272.7,118.9 1272.7,117.2C1272.7,115.6 1271.4,114.2 1269.7,114.2C1268,114.2 1266.7,115.5 1266.7,117.2C1266.8,118.9 1268.1,120.2 1269.7,120.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path728" d="M1279.2,120.9C1281.2,120.9 1282.9,119.2 1282.9,117.2C1282.9,115.2 1281.2,113.5 1279.2,113.5C1277.2,113.5 1275.5,115.2 1275.5,117.2C1275.4,119.3 1277.1,120.9 1279.2,120.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path730" d="M1288.6,120.1C1290.2,120.1 1291.5,118.8 1291.5,117.2C1291.5,115.6 1290.2,114.3 1288.6,114.3C1287,114.3 1285.7,115.6 1285.7,117.2C1285.7,118.8 1287,120.1 1288.6,120.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path732" d="M1298,120.6C1299.9,120.6 1301.4,119.1 1301.4,117.2C1301.4,115.3 1299.9,113.8 1298,113.8C1296.1,113.8 1294.6,115.3 1294.6,117.2C1294.6,119.1 1296.1,120.6 1298,120.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path734" d="M1307.5,120.1C1309.1,120.1 1310.4,118.8 1310.4,117.2C1310.4,115.6 1309.1,114.3 1307.5,114.3C1305.9,114.3 1304.6,115.6 1304.6,117.2C1304.6,118.8 1305.9,120.1 1307.5,120.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path798" d="M1250.8,129.5C1252.4,129.5 1253.7,128.2 1253.7,126.6C1253.7,125 1252.4,123.7 1250.8,123.7C1249.2,123.7 1247.9,125 1247.9,126.6C1248,128.3 1249.3,129.5 1250.8,129.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path800" d="M1260.3,129.5C1261.9,129.5 1263.2,128.2 1263.2,126.6C1263.2,125 1261.9,123.7 1260.3,123.7C1258.7,123.7 1257.4,125 1257.4,126.6C1257.4,128.3 1258.7,129.5 1260.3,129.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path802" d="M1269.7,129.5C1271.3,129.5 1272.6,128.2 1272.6,126.6C1272.6,125 1271.3,123.7 1269.7,123.7C1268.1,123.7 1266.8,125 1266.8,126.6C1266.8,128.3 1268.1,129.5 1269.7,129.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path804" d="M1279.2,129.6C1280.8,129.6 1282.2,128.3 1282.2,126.6C1282.2,124.9 1280.9,123.6 1279.2,123.6C1277.5,123.6 1276.2,124.9 1276.2,126.6C1276.2,128.3 1277.5,129.6 1279.2,129.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path806" d="M1288.6,130C1290.5,130 1292,128.5 1292,126.7C1292,124.8 1290.5,123.3 1288.6,123.3C1286.8,123.3 1285.3,124.8 1285.3,126.7C1285.2,128.5 1286.7,130 1288.6,130Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path808" d="M1298,129.5C1299.6,129.5 1300.9,128.2 1300.9,126.6C1300.9,125 1299.6,123.7 1298,123.7C1296.4,123.7 1295.1,125 1295.1,126.6C1295.1,128.3 1296.4,129.5 1298,129.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path810" d="M1307.5,129.5C1309.1,129.5 1310.4,128.2 1310.4,126.6C1310.4,125 1309.1,123.7 1307.5,123.7C1305.9,123.7 1304.6,125 1304.6,126.6C1304.6,128.3 1305.9,129.5 1307.5,129.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path874" d="M1203.7,139C1205.3,139 1206.6,137.7 1206.6,136.1C1206.6,134.5 1205.3,133.2 1203.7,133.2C1202.1,133.2 1200.8,134.5 1200.8,136.1C1200.8,137.7 1202.1,139 1203.7,139Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path876" d="M1213.1,139.8C1215.2,139.8 1216.8,138.1 1216.8,136.1C1216.8,134 1215.1,132.4 1213.1,132.4C1211.1,132.4 1209.4,134.1 1209.4,136.1C1209.4,138.1 1211.1,139.8 1213.1,139.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path878" d="M1232,139C1233.6,139 1234.9,137.7 1234.9,136.1C1234.9,134.5 1233.6,133.2 1232,133.2C1230.4,133.2 1229.1,134.5 1229.1,136.1C1229.1,137.7 1230.4,139 1232,139Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path880" d="M1241.4,139.7C1243.4,139.7 1245,138.1 1245,136.1C1245,134.1 1243.4,132.5 1241.4,132.5C1239.4,132.5 1237.8,134.1 1237.8,136.1C1237.8,138.1 1239.4,139.7 1241.4,139.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path882" d="M1260.3,139C1261.9,139 1263.2,137.7 1263.2,136.1C1263.2,134.5 1261.9,133.2 1260.3,133.2C1258.7,133.2 1257.4,134.5 1257.4,136.1C1257.4,137.7 1258.7,139 1260.3,139Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path884" d="M1269.7,139C1271.3,139 1272.6,137.7 1272.6,136.1C1272.6,134.5 1271.3,133.2 1269.7,133.2C1268.1,133.2 1266.8,134.5 1266.8,136.1C1266.8,137.7 1268.1,139 1269.7,139Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path886" d="M1288.6,139C1290.2,139 1291.5,137.7 1291.5,136.1C1291.5,134.5 1290.2,133.2 1288.6,133.2C1287,133.2 1285.7,134.5 1285.7,136.1C1285.7,137.7 1287,139 1288.6,139Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path888" d="M1298,139C1299.6,139 1300.9,137.7 1300.9,136.1C1300.9,134.5 1299.6,133.2 1298,133.2C1296.4,133.2 1295.1,134.5 1295.1,136.1C1295.1,137.7 1296.4,139 1298,139Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path890" d="M1307.5,140C1309.7,140 1311.4,138.3 1311.4,136.1C1311.4,134 1309.7,132.2 1307.5,132.2C1305.3,132.2 1303.6,133.9 1303.6,136.1C1303.6,138.2 1305.3,140 1307.5,140Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path954" d="M1203.7,148.4C1205.3,148.4 1206.6,147.1 1206.6,145.5C1206.6,143.9 1205.3,142.6 1203.7,142.6C1202.1,142.6 1200.8,143.9 1200.8,145.5C1200.8,147.1 1202.1,148.4 1203.7,148.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path956" d="M1213.1,149.3C1215.2,149.3 1216.8,147.6 1216.8,145.6C1216.8,143.5 1215.1,141.9 1213.1,141.9C1211.1,141.9 1209.4,143.6 1209.4,145.6C1209.4,147.6 1211.1,149.3 1213.1,149.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path958" d="M1241.4,148.4C1243,148.4 1244.3,147.1 1244.3,145.5C1244.3,143.9 1243,142.6 1241.4,142.6C1239.8,142.6 1238.5,143.9 1238.5,145.5C1238.5,147.1 1239.8,148.4 1241.4,148.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path960" d="M1260.3,148.4C1261.9,148.4 1263.2,147.1 1263.2,145.5C1263.2,143.9 1261.9,142.6 1260.3,142.6C1258.7,142.6 1257.4,143.9 1257.4,145.5C1257.4,147.1 1258.7,148.4 1260.3,148.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path962" d="M1269.7,148.9C1271.6,148.9 1273.1,147.4 1273.1,145.5C1273.1,143.6 1271.6,142.1 1269.7,142.1C1267.8,142.1 1266.3,143.6 1266.3,145.5C1266.3,147.4 1267.8,148.9 1269.7,148.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path964" d="M1279.2,148.4C1280.8,148.4 1282.1,147.1 1282.1,145.5C1282.1,143.9 1280.8,142.6 1279.2,142.6C1277.6,142.6 1276.3,143.9 1276.3,145.5C1276.3,147.1 1277.6,148.4 1279.2,148.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path966" d="M1288.6,148.4C1290.2,148.4 1291.5,147.1 1291.5,145.5C1291.5,143.9 1290.2,142.6 1288.6,142.6C1287,142.6 1285.7,143.9 1285.7,145.5C1285.7,147.1 1287,148.4 1288.6,148.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path968" d="M1298,148.5C1299.6,148.5 1301,147.2 1301,145.6C1301,144 1299.7,142.7 1298,142.7C1296.4,142.7 1295.1,144 1295.1,145.6C1295.1,147.1 1296.4,148.5 1298,148.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path970" d="M1307.5,148.4C1309.1,148.4 1310.4,147.1 1310.4,145.5C1310.4,143.9 1309.1,142.6 1307.5,142.6C1305.9,142.6 1304.6,143.9 1304.6,145.5C1304.6,147.1 1305.9,148.4 1307.5,148.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1034" d="M1203.7,157.8C1205.3,157.8 1206.6,156.5 1206.6,154.9C1206.6,153.3 1205.3,152 1203.7,152C1202.1,152 1200.8,153.3 1200.8,154.9C1200.8,156.5 1202.1,157.8 1203.7,157.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1036" d="M1222.5,157.8C1224.1,157.8 1225.4,156.5 1225.4,154.9C1225.4,153.3 1224.1,152 1222.5,152C1220.9,152 1219.6,153.3 1219.6,154.9C1219.7,156.5 1221,157.8 1222.5,157.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1038" d="M1232,158.5C1233.9,158.5 1235.5,156.9 1235.5,155C1235.5,153.1 1233.9,151.5 1232,151.5C1230,151.5 1228.5,153.1 1228.5,155C1228.5,156.9 1230,158.5 1232,158.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1040" d="M1241.4,157.8C1243,157.8 1244.3,156.5 1244.3,154.9C1244.3,153.3 1243,152 1241.4,152C1239.8,152 1238.5,153.3 1238.5,154.9C1238.6,156.5 1239.8,157.8 1241.4,157.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1042" d="M1250.8,157.8C1252.4,157.8 1253.7,156.5 1253.7,154.9C1253.7,153.3 1252.4,152 1250.8,152C1249.2,152 1247.9,153.3 1247.9,154.9C1248,156.5 1249.3,157.8 1250.8,157.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1044" d="M1260.3,158.3C1262.1,158.3 1263.6,156.8 1263.6,155C1263.6,153.2 1262.1,151.7 1260.3,151.7C1258.5,151.7 1257,153.2 1257,155C1257,156.8 1258.4,158.3 1260.3,158.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1046" d="M1269.7,158.3C1271.5,158.3 1273,156.8 1273,155C1273,153.2 1271.5,151.7 1269.7,151.7C1267.9,151.7 1266.4,153.2 1266.4,155C1266.4,156.8 1267.9,158.3 1269.7,158.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1048" d="M1279.2,158.3C1281,158.3 1282.5,156.8 1282.5,154.9C1282.5,153.1 1281,151.6 1279.2,151.6C1277.3,151.6 1275.8,153.1 1275.8,154.9C1275.8,156.8 1277.3,158.3 1279.2,158.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1050" d="M1288.6,158.3C1290.5,158.3 1292,156.8 1292,154.9C1292,153.1 1290.5,151.6 1288.6,151.6C1286.8,151.6 1285.3,153.1 1285.3,154.9C1285.2,156.8 1286.7,158.3 1288.6,158.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1052" d="M1298,158.4C1299.9,158.4 1301.4,156.9 1301.4,155C1301.4,153.1 1299.9,151.6 1298,151.6C1296.1,151.6 1294.6,153.1 1294.6,155C1294.6,156.8 1296.1,158.4 1298,158.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1054" d="M1307.5,157.8C1309.1,157.8 1310.4,156.5 1310.4,154.9C1310.4,153.3 1309.1,152 1307.5,152C1305.9,152 1304.6,153.3 1304.6,154.9C1304.6,156.5 1305.9,157.8 1307.5,157.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1118" d="M1147.1,167.3C1148.7,167.3 1150,166 1150,164.4C1150,162.8 1148.7,161.5 1147.1,161.5C1145.5,161.5 1144.2,162.8 1144.2,164.4C1144.2,166 1145.5,167.3 1147.1,167.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1120" d="M1156.5,167.3C1158.1,167.3 1159.4,166 1159.4,164.4C1159.4,162.8 1158.1,161.5 1156.5,161.5C1154.9,161.5 1153.6,162.8 1153.6,164.4C1153.6,166 1154.9,167.3 1156.5,167.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1122" d="M1166,167.3C1167.6,167.3 1168.9,166 1168.9,164.4C1168.9,162.8 1167.6,161.5 1166,161.5C1164.4,161.5 1163.1,162.8 1163.1,164.4C1163.1,166 1164.4,167.3 1166,167.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1124" d="M1175.4,167.6C1177.2,167.6 1178.6,166.2 1178.6,164.4C1178.6,162.6 1177.1,161.2 1175.4,161.2C1173.6,161.2 1172.2,162.6 1172.2,164.4C1172.2,166.2 1173.6,167.6 1175.4,167.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1126" d="M1213.1,167.3C1214.7,167.3 1216,166 1216,164.4C1216,162.8 1214.7,161.5 1213.1,161.5C1211.5,161.5 1210.2,162.8 1210.2,164.4C1210.2,166 1211.5,167.3 1213.1,167.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1128" d="M1222.5,168.4C1224.7,168.4 1226.5,166.6 1226.5,164.4C1226.5,162.2 1224.7,160.4 1222.5,160.4C1220.3,160.4 1218.5,162.2 1218.5,164.4C1218.5,166.6 1220.4,168.4 1222.5,168.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1130" d="M1232,167.3C1233.6,167.3 1234.9,166 1234.9,164.4C1234.9,162.8 1233.6,161.5 1232,161.5C1230.4,161.5 1229.1,162.8 1229.1,164.4C1229.1,166 1230.4,167.3 1232,167.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1132" d="M1241.4,168.4C1243.6,168.4 1245.4,166.6 1245.4,164.4C1245.4,162.2 1243.6,160.4 1241.4,160.4C1239.2,160.4 1237.4,162.2 1237.4,164.4C1237.4,166.6 1239.2,168.4 1241.4,168.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1134" d="M1250.8,167.7C1252.6,167.7 1254.1,166.2 1254.1,164.4C1254.1,162.6 1252.6,161.1 1250.8,161.1C1249,161.1 1247.5,162.6 1247.5,164.4C1247.6,166.2 1249,167.7 1250.8,167.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1136" d="M1260.3,167.7C1262.1,167.7 1263.6,166.2 1263.6,164.4C1263.6,162.6 1262.1,161.1 1260.3,161.1C1258.5,161.1 1257,162.6 1257,164.4C1257,166.2 1258.4,167.7 1260.3,167.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1138" d="M1269.7,167.7C1271.5,167.7 1273,166.2 1273,164.4C1273,162.6 1271.5,161.1 1269.7,161.1C1267.9,161.1 1266.4,162.6 1266.4,164.4C1266.4,166.2 1267.9,167.7 1269.7,167.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1140" d="M1279.2,167.7C1281,167.7 1282.5,166.2 1282.5,164.4C1282.5,162.5 1281,161 1279.2,161C1277.3,161 1275.8,162.5 1275.8,164.4C1275.8,166.2 1277.3,167.7 1279.2,167.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1142" d="M1288.6,167.3C1290.2,167.3 1291.5,166 1291.5,164.4C1291.5,162.8 1290.2,161.5 1288.6,161.5C1287,161.5 1285.7,162.8 1285.7,164.4C1285.7,166 1287,167.3 1288.6,167.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1144" d="M1298,167.9C1300,167.9 1301.5,166.3 1301.5,164.4C1301.5,162.4 1299.9,160.9 1298,160.9C1296.1,160.9 1294.5,162.5 1294.5,164.4C1294.5,166.3 1296.1,167.9 1298,167.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1146" d="M1307.5,167.8C1309.4,167.8 1310.9,166.3 1310.9,164.4C1310.9,162.5 1309.4,161 1307.5,161C1305.6,161 1304.1,162.5 1304.1,164.4C1304.1,166.3 1305.6,167.8 1307.5,167.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1210" d="M1156.5,176.7C1158.1,176.7 1159.4,175.4 1159.4,173.8C1159.4,172.2 1158.1,170.9 1156.5,170.9C1154.9,170.9 1153.6,172.2 1153.6,173.8C1153.6,175.4 1154.9,176.7 1156.5,176.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1212" d="M1166,176.7C1167.6,176.7 1168.9,175.4 1168.9,173.8C1168.9,172.2 1167.6,170.9 1166,170.9C1164.4,170.9 1163.1,172.2 1163.1,173.8C1163.1,175.4 1164.4,176.7 1166,176.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1214" d="M1175.4,176.7C1177,176.7 1178.3,175.4 1178.3,173.8C1178.3,172.2 1177,170.9 1175.4,170.9C1173.8,170.9 1172.5,172.2 1172.5,173.8C1172.5,175.4 1173.8,176.7 1175.4,176.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1216" d="M1184.8,176.7C1186.4,176.7 1187.7,175.4 1187.7,173.8C1187.7,172.2 1186.4,170.9 1184.8,170.9C1183.2,170.9 1181.9,172.2 1181.9,173.8C1181.9,175.4 1183.2,176.7 1184.8,176.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1218" d="M1213.1,176.7C1214.7,176.7 1216,175.4 1216,173.8C1216,172.2 1214.7,170.9 1213.1,170.9C1211.5,170.9 1210.2,172.2 1210.2,173.8C1210.2,175.4 1211.5,176.7 1213.1,176.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1220" d="M1222.5,176.7C1224.1,176.7 1225.4,175.4 1225.4,173.8C1225.4,172.2 1224.1,170.9 1222.5,170.9C1220.9,170.9 1219.6,172.2 1219.6,173.8C1219.7,175.4 1221,176.7 1222.5,176.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1222" d="M1232,177.5C1234,177.5 1235.7,175.8 1235.7,173.8C1235.7,171.7 1234,170.1 1232,170.1C1229.9,170.1 1228.3,171.8 1228.3,173.8C1228.3,175.9 1229.9,177.5 1232,177.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1224" d="M1241.4,177.8C1243.6,177.8 1245.4,176 1245.4,173.8C1245.4,171.6 1243.6,169.8 1241.4,169.8C1239.2,169.8 1237.4,171.6 1237.4,173.8C1237.4,176 1239.2,177.8 1241.4,177.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1226" d="M1250.8,177.8C1253,177.8 1254.8,176 1254.8,173.8C1254.8,171.6 1253,169.8 1250.8,169.8C1248.6,169.8 1246.8,171.6 1246.8,173.8C1246.8,176 1248.6,177.8 1250.8,177.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1228" d="M1260.3,177.2C1262.1,177.2 1263.6,175.7 1263.6,173.9C1263.6,172.1 1262.1,170.6 1260.3,170.6C1258.5,170.6 1257,172.1 1257,173.9C1257,175.7 1258.4,177.2 1260.3,177.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1230" d="M1269.7,176.7C1271.3,176.7 1272.6,175.4 1272.6,173.8C1272.6,172.2 1271.3,170.9 1269.7,170.9C1268.1,170.9 1266.8,172.2 1266.8,173.8C1266.8,175.4 1268.1,176.7 1269.7,176.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1232" d="M1279.2,176.7C1280.8,176.7 1282.1,175.4 1282.1,173.8C1282.1,172.2 1280.8,170.9 1279.2,170.9C1277.6,170.9 1276.3,172.2 1276.3,173.8C1276.3,175.4 1277.6,176.7 1279.2,176.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1234" d="M1288.6,176.7C1290.2,176.7 1291.5,175.4 1291.5,173.8C1291.5,172.2 1290.2,170.9 1288.6,170.9C1287,170.9 1285.7,172.2 1285.7,173.8C1285.7,175.4 1287,176.7 1288.6,176.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1236" d="M1298,177.4C1300,177.4 1301.5,175.8 1301.5,173.8C1301.5,171.9 1299.9,170.3 1298,170.3C1296.1,170.3 1294.5,171.9 1294.5,173.8C1294.5,175.8 1296.1,177.4 1298,177.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1238" d="M1307.5,177.2C1309.4,177.2 1310.9,175.7 1310.9,173.8C1310.9,171.9 1309.4,170.4 1307.5,170.4C1305.6,170.4 1304.1,171.9 1304.1,173.8C1304,175.7 1305.6,177.2 1307.5,177.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1302" d="M1156.5,186.1C1158.1,186.1 1159.4,184.8 1159.4,183.2C1159.4,181.6 1158.1,180.3 1156.5,180.3C1154.9,180.3 1153.6,181.6 1153.6,183.2C1153.6,184.9 1154.9,186.1 1156.5,186.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1304" d="M1166,186.4C1167.7,186.4 1169.2,185 1169.2,183.2C1169.2,181.4 1167.7,180 1166,180C1164.2,180 1162.8,181.4 1162.8,183.2C1162.8,185 1164.2,186.4 1166,186.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1306" d="M1175.4,186.1C1177,186.1 1178.3,184.8 1178.3,183.2C1178.3,181.6 1177,180.3 1175.4,180.3C1173.8,180.3 1172.5,181.6 1172.5,183.2C1172.5,184.9 1173.8,186.1 1175.4,186.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1308" d="M1184.8,186.1C1186.4,186.1 1187.7,184.8 1187.7,183.2C1187.7,181.6 1186.4,180.3 1184.8,180.3C1183.2,180.3 1181.9,181.6 1181.9,183.2C1181.9,184.9 1183.2,186.1 1184.8,186.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1310" d="M1194.3,187C1196.3,187 1198,185.3 1198,183.3C1198,181.2 1196.3,179.6 1194.3,179.6C1192.2,179.6 1190.6,181.3 1190.6,183.3C1190.5,185.3 1192.2,187 1194.3,187Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1312" d="M1203.7,186.2C1205.3,186.2 1206.7,184.9 1206.7,183.2C1206.7,181.6 1205.4,180.2 1203.7,180.2C1202.1,180.2 1200.7,181.5 1200.7,183.2C1200.7,184.9 1202.1,186.2 1203.7,186.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1314" d="M1213.1,186.1C1214.7,186.1 1216,184.8 1216,183.2C1216,181.6 1214.7,180.3 1213.1,180.3C1211.5,180.3 1210.2,181.6 1210.2,183.2C1210.2,184.9 1211.5,186.1 1213.1,186.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1316" d="M1222.5,186.1C1224.1,186.1 1225.4,184.8 1225.4,183.2C1225.4,181.6 1224.1,180.3 1222.5,180.3C1220.9,180.3 1219.6,181.6 1219.6,183.2C1219.7,184.9 1221,186.1 1222.5,186.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1318" d="M1232,187C1234,187 1235.7,185.3 1235.7,183.3C1235.7,181.2 1234,179.6 1232,179.6C1229.9,179.6 1228.3,181.3 1228.3,183.3C1228.3,185.3 1229.9,187 1232,187Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1320" d="M1241.4,186.6C1243.2,186.6 1244.7,185.1 1244.7,183.2C1244.7,181.3 1243.2,179.8 1241.4,179.8C1239.5,179.8 1238,181.3 1238,183.2C1238.1,185.1 1239.6,186.6 1241.4,186.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1322" d="M1250.8,186.5C1252.6,186.5 1254.1,185 1254.1,183.2C1254.1,181.4 1252.6,179.9 1250.8,179.9C1249,179.9 1247.5,181.4 1247.5,183.2C1247.6,185.1 1249,186.5 1250.8,186.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1324" d="M1260.3,186.6C1262.1,186.6 1263.6,185.1 1263.6,183.3C1263.6,181.5 1262.1,180 1260.3,180C1258.5,180 1257,181.5 1257,183.3C1257,185.1 1258.4,186.6 1260.3,186.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1326" d="M1269.7,186.6C1271.5,186.6 1273,185.1 1273,183.3C1273,181.5 1271.5,180 1269.7,180C1267.9,180 1266.4,181.5 1266.4,183.3C1266.4,185.1 1267.9,186.6 1269.7,186.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1328" d="M1279.2,186.1C1280.8,186.1 1282.1,184.8 1282.1,183.2C1282.1,181.6 1280.8,180.3 1279.2,180.3C1277.6,180.3 1276.3,181.6 1276.3,183.2C1276.3,184.9 1277.6,186.1 1279.2,186.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1330" d="M1288.6,186.6C1290.5,186.6 1292,185.1 1292,183.2C1292,181.3 1290.5,179.8 1288.6,179.8C1286.8,179.8 1285.3,181.3 1285.3,183.2C1285.2,185.1 1286.7,186.6 1288.6,186.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1332" d="M1298,186.7C1299.9,186.7 1301.4,185.2 1301.4,183.3C1301.4,181.4 1299.9,179.9 1298,179.9C1296.1,179.9 1294.6,181.4 1294.6,183.3C1294.6,185.1 1296.1,186.7 1298,186.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1334" d="M1307.5,186.7C1309.4,186.7 1310.9,185.2 1310.9,183.3C1310.9,181.4 1309.4,179.9 1307.5,179.9C1305.6,179.9 1304.1,181.4 1304.1,183.3C1304,185.1 1305.6,186.7 1307.5,186.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1398" d="M1166,195.6C1167.6,195.6 1168.9,194.3 1168.9,192.7C1168.9,191.1 1167.6,189.8 1166,189.8C1164.4,189.8 1163.1,191.1 1163.1,192.7C1163.1,194.3 1164.4,195.6 1166,195.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1400" d="M1175.4,195.6C1177,195.6 1178.3,194.3 1178.3,192.7C1178.3,191.1 1177,189.8 1175.4,189.8C1173.8,189.8 1172.5,191.1 1172.5,192.7C1172.5,194.3 1173.8,195.6 1175.4,195.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1402" d="M1194.3,195.6C1195.9,195.6 1197.2,194.3 1197.2,192.7C1197.2,191.1 1195.9,189.8 1194.3,189.8C1192.7,189.8 1191.4,191.1 1191.4,192.7C1191.4,194.3 1192.6,195.6 1194.3,195.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1404" d="M1203.7,195.6C1205.3,195.6 1206.6,194.3 1206.6,192.7C1206.6,191.1 1205.3,189.8 1203.7,189.8C1202.1,189.8 1200.8,191.1 1200.8,192.7C1200.8,194.3 1202.1,195.6 1203.7,195.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1406" d="M1213.1,195.6C1214.7,195.6 1216,194.3 1216,192.7C1216,191.1 1214.7,189.8 1213.1,189.8C1211.5,189.8 1210.2,191.1 1210.2,192.7C1210.2,194.3 1211.5,195.6 1213.1,195.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1408" d="M1222.5,195.6C1224.1,195.6 1225.4,194.3 1225.4,192.7C1225.4,191.1 1224.1,189.8 1222.5,189.8C1220.9,189.8 1219.6,191.1 1219.6,192.7C1219.7,194.3 1221,195.6 1222.5,195.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1410" d="M1232,195.6C1233.6,195.6 1234.9,194.3 1234.9,192.7C1234.9,191.1 1233.6,189.8 1232,189.8C1230.4,189.8 1229.1,191.1 1229.1,192.7C1229.1,194.3 1230.4,195.6 1232,195.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1412" d="M1241.4,196C1243.2,196 1244.7,194.5 1244.7,192.6C1244.7,190.8 1243.2,189.3 1241.4,189.3C1239.5,189.3 1238,190.8 1238,192.6C1238.1,194.5 1239.6,196 1241.4,196Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1414" d="M1250.8,196C1252.6,196 1254.1,194.5 1254.1,192.7C1254.1,190.9 1252.6,189.4 1250.8,189.4C1249,189.4 1247.5,190.9 1247.5,192.7C1247.6,194.5 1249,196 1250.8,196Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1416" d="M1260.3,196C1262.1,196 1263.6,194.5 1263.6,192.7C1263.6,190.9 1262.1,189.4 1260.3,189.4C1258.5,189.4 1257,190.9 1257,192.7C1257,194.5 1258.4,196 1260.3,196Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1418" d="M1269.7,196C1271.5,196 1273,194.5 1273,192.7C1273,190.9 1271.5,189.4 1269.7,189.4C1267.9,189.4 1266.4,190.9 1266.4,192.7C1266.4,194.5 1267.9,196 1269.7,196Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1420" d="M1279.2,196C1281,196 1282.5,194.5 1282.5,192.6C1282.5,190.8 1281,189.3 1279.2,189.3C1277.3,189.3 1275.8,190.8 1275.8,192.6C1275.8,194.5 1277.3,196 1279.2,196Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1422" d="M1288.6,196C1290.5,196 1292,194.5 1292,192.6C1292,190.8 1290.5,189.3 1288.6,189.3C1286.8,189.3 1285.3,190.8 1285.3,192.6C1285.2,194.5 1286.7,196 1288.6,196Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1424" d="M1298,196.1C1299.9,196.1 1301.4,194.6 1301.4,192.7C1301.4,190.8 1299.9,189.3 1298,189.3C1296.1,189.3 1294.6,190.8 1294.6,192.7C1294.6,194.6 1296.1,196.1 1298,196.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1426" d="M1307.5,196.1C1309.4,196.1 1310.9,194.6 1310.9,192.7C1310.9,190.8 1309.4,189.3 1307.5,189.3C1305.6,189.3 1304.1,190.8 1304.1,192.7C1304.1,194.6 1305.6,196.1 1307.5,196.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1490" d="M1118.8,205C1120.4,205 1121.7,203.7 1121.7,202.1C1121.7,200.5 1120.4,199.2 1118.8,199.2C1117.2,199.2 1115.9,200.5 1115.9,202.1C1115.9,203.7 1117.2,205 1118.8,205Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1492" d="M1128.2,205C1129.8,205 1131.1,203.7 1131.1,202.1C1131.1,200.5 1129.8,199.2 1128.2,199.2C1126.6,199.2 1125.3,200.5 1125.3,202.1C1125.3,203.7 1126.6,205 1128.2,205Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1494" d="M1137.6,205C1139.2,205 1140.5,203.7 1140.5,202.1C1140.5,200.5 1139.2,199.2 1137.6,199.2C1136,199.2 1134.7,200.5 1134.7,202.1C1134.8,203.7 1136.1,205 1137.6,205Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1496" d="M1166,205C1167.6,205 1168.9,203.7 1168.9,202.1C1168.9,200.5 1167.6,199.2 1166,199.2C1164.4,199.2 1163.1,200.5 1163.1,202.1C1163.1,203.7 1164.4,205 1166,205Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1498" d="M1175.4,205.1C1177,205.1 1178.4,203.8 1178.4,202.2C1178.4,200.6 1177.1,199.3 1175.4,199.3C1173.8,199.3 1172.5,200.6 1172.5,202.2C1172.5,203.7 1173.8,205.1 1175.4,205.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1500" d="M1184.8,205.1C1186.5,205.1 1187.8,203.7 1187.8,202.1C1187.8,200.4 1186.4,199.1 1184.8,199.1C1183.1,199.1 1181.8,200.4 1181.8,202.1C1181.8,203.8 1183.2,205.1 1184.8,205.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1502" d="M1194.3,205.4C1196.1,205.4 1197.5,203.9 1197.5,202.2C1197.5,200.4 1196,199 1194.3,199C1192.6,199 1191.1,200.4 1191.1,202.2C1191,203.9 1192.5,205.4 1194.3,205.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1504" d="M1203.7,205C1205.3,205 1206.6,203.7 1206.6,202.1C1206.6,200.5 1205.3,199.2 1203.7,199.2C1202.1,199.2 1200.8,200.5 1200.8,202.1C1200.8,203.7 1202.1,205 1203.7,205Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1506" d="M1213.1,205C1214.7,205 1216,203.7 1216,202.1C1216,200.5 1214.7,199.2 1213.1,199.2C1211.5,199.2 1210.2,200.5 1210.2,202.1C1210.2,203.7 1211.5,205 1213.1,205Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1508" d="M1222.5,206.1C1224.7,206.1 1226.4,204.3 1226.4,202.2C1226.4,200 1224.7,198.3 1222.5,198.3C1220.4,198.3 1218.6,200.1 1218.6,202.2C1218.6,204.3 1220.4,206.1 1222.5,206.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1510" d="M1232,205.4C1233.8,205.4 1235.2,203.9 1235.2,202.2C1235.2,200.4 1233.7,199 1232,199C1230.3,199 1228.8,200.4 1228.8,202.2C1228.8,203.9 1230.2,205.4 1232,205.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1512" d="M1241.4,205.4C1243.2,205.4 1244.6,203.9 1244.6,202.2C1244.6,200.4 1243.1,199 1241.4,199C1239.6,199 1238.2,200.4 1238.2,202.2C1238.2,203.9 1239.6,205.4 1241.4,205.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1514" d="M1250.8,205.4C1252.6,205.4 1254.1,203.9 1254.1,202.1C1254.1,200.3 1252.6,198.8 1250.8,198.8C1249,198.8 1247.5,200.3 1247.5,202.1C1247.6,203.9 1249,205.4 1250.8,205.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1516" d="M1260.3,205.4C1262.1,205.4 1263.6,203.9 1263.6,202.1C1263.6,200.3 1262.1,198.8 1260.3,198.8C1258.5,198.8 1257,200.3 1257,202.1C1257,204 1258.4,205.4 1260.3,205.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1518" d="M1269.7,205.4C1271.5,205.4 1273,203.9 1273,202.1C1273,200.3 1271.5,198.8 1269.7,198.8C1267.9,198.8 1266.4,200.3 1266.4,202.1C1266.4,204 1267.9,205.4 1269.7,205.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1520" d="M1279.2,205.5C1281,205.5 1282.5,204 1282.5,202.1C1282.5,200.3 1281,198.7 1279.2,198.7C1277.3,198.7 1275.8,200.2 1275.8,202.1C1275.8,204 1277.3,205.5 1279.2,205.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1522" d="M1288.6,205.5C1290.5,205.5 1292,204 1292,202.1C1292,200.3 1290.5,198.7 1288.6,198.7C1286.8,198.7 1285.3,200.2 1285.3,202.1C1285.2,204 1286.7,205.5 1288.6,205.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1524" d="M1298,205.5C1299.9,205.5 1301.4,204 1301.4,202.1C1301.4,200.2 1299.9,198.7 1298,198.7C1296.1,198.7 1294.6,200.2 1294.6,202.1C1294.6,204 1296.1,205.5 1298,205.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1526" d="M1307.5,205.5C1309.4,205.5 1310.9,204 1310.9,202.1C1310.9,200.2 1309.4,198.7 1307.5,198.7C1305.6,198.7 1304.1,200.2 1304.1,202.1C1304,204 1305.6,205.5 1307.5,205.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1590" d="M1081,214.5C1082.6,214.5 1083.9,213.2 1083.9,211.6C1083.9,210 1082.6,208.7 1081,208.7C1079.4,208.7 1078.1,210 1078.1,211.6C1078.2,213.1 1079.5,214.5 1081,214.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1592" d="M1090.5,215.6C1092.7,215.6 1094.5,213.8 1094.5,211.6C1094.5,209.4 1092.7,207.6 1090.5,207.6C1088.3,207.6 1086.5,209.4 1086.5,211.6C1086.5,213.8 1088.3,215.6 1090.5,215.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1594" d="M1156.5,214.5C1158.1,214.5 1159.4,213.2 1159.4,211.6C1159.4,210 1158.1,208.7 1156.5,208.7C1154.9,208.7 1153.6,210 1153.6,211.6C1153.6,213.1 1154.9,214.5 1156.5,214.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1596" d="M1166,214.5C1167.6,214.5 1168.9,213.2 1168.9,211.6C1168.9,210 1167.6,208.7 1166,208.7C1164.4,208.7 1163.1,210 1163.1,211.6C1163.1,213.1 1164.4,214.5 1166,214.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1598" d="M1184.8,214.5C1186.4,214.5 1187.7,213.2 1187.7,211.6C1187.7,210 1186.4,208.7 1184.8,208.7C1183.2,208.7 1181.9,210 1181.9,211.6C1181.9,213.1 1183.2,214.5 1184.8,214.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1600" d="M1194.3,214.5C1195.9,214.5 1197.2,213.2 1197.2,211.6C1197.2,210 1195.9,208.7 1194.3,208.7C1192.7,208.7 1191.4,210 1191.4,211.6C1191.4,213.1 1192.6,214.5 1194.3,214.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1602" d="M1203.7,214.6C1205.4,214.6 1206.8,213.2 1206.8,211.5C1206.8,209.8 1205.4,208.4 1203.7,208.4C1202,208.4 1200.6,209.8 1200.6,211.5C1200.6,213.3 1202,214.6 1203.7,214.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1604" d="M1213.1,214.8C1214.9,214.8 1216.3,213.4 1216.3,211.6C1216.3,209.8 1214.9,208.4 1213.1,208.4C1211.3,208.4 1209.9,209.8 1209.9,211.6C1209.9,213.3 1211.3,214.8 1213.1,214.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1606" d="M1222.5,214.8C1224.3,214.8 1225.7,213.4 1225.7,211.6C1225.7,209.8 1224.2,208.4 1222.5,208.4C1220.7,208.4 1219.3,209.8 1219.3,211.6C1219.3,213.3 1220.8,214.8 1222.5,214.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1608" d="M1232,214.5C1233.6,214.5 1234.9,213.2 1234.9,211.6C1234.9,210 1233.6,208.7 1232,208.7C1230.4,208.7 1229.1,210 1229.1,211.6C1229.1,213.1 1230.4,214.5 1232,214.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1610" d="M1241.4,214.8C1243.2,214.8 1244.6,213.4 1244.6,211.6C1244.6,209.8 1243.1,208.4 1241.4,208.4C1239.6,208.4 1238.2,209.8 1238.2,211.6C1238.2,213.3 1239.6,214.8 1241.4,214.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1612" d="M1250.8,214.8C1252.6,214.8 1254.1,213.3 1254.1,211.5C1254.1,209.7 1252.6,208.2 1250.8,208.2C1249,208.2 1247.5,209.7 1247.5,211.5C1247.6,213.4 1249,214.8 1250.8,214.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1614" d="M1260.3,214.9C1262.1,214.9 1263.6,213.4 1263.6,211.6C1263.6,209.8 1262.1,208.3 1260.3,208.3C1258.5,208.3 1257,209.8 1257,211.6C1257,213.4 1258.4,214.9 1260.3,214.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1616" d="M1269.7,214.9C1271.5,214.9 1273,213.4 1273,211.6C1273,209.8 1271.5,208.3 1269.7,208.3C1267.9,208.3 1266.4,209.8 1266.4,211.6C1266.4,213.4 1267.9,214.9 1269.7,214.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1618" d="M1279.2,214.9C1281,214.9 1282.5,213.4 1282.5,211.5C1282.5,209.7 1281,208.2 1279.2,208.2C1277.3,208.2 1275.8,209.7 1275.8,211.5C1275.8,213.4 1277.3,214.9 1279.2,214.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1620" d="M1288.6,214.9C1290.5,214.9 1292,213.4 1292,211.5C1292,209.7 1290.5,208.2 1288.6,208.2C1286.8,208.2 1285.3,209.7 1285.3,211.5C1285.2,213.4 1286.7,214.9 1288.6,214.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1622" d="M1298,215C1299.9,215 1301.4,213.5 1301.4,211.6C1301.4,209.7 1299.9,208.2 1298,208.2C1296.1,208.2 1294.6,209.7 1294.6,211.6C1294.6,213.4 1296.1,215 1298,215Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1624" d="M1307.5,215C1309.4,215 1310.9,213.5 1310.9,211.6C1310.9,209.7 1309.4,208.2 1307.5,208.2C1305.6,208.2 1304.1,209.7 1304.1,211.6C1304,213.4 1305.6,215 1307.5,215Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1688" d="M1194.3,223.9C1195.9,223.9 1197.2,222.6 1197.2,221C1197.2,219.4 1195.9,218.1 1194.3,218.1C1192.7,218.1 1191.4,219.4 1191.4,221C1191.4,222.6 1192.6,223.9 1194.3,223.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1690" d="M1213.1,223.9C1214.7,223.9 1216,222.6 1216,221C1216,219.4 1214.7,218.1 1213.1,218.1C1211.5,218.1 1210.2,219.4 1210.2,221C1210.2,222.6 1211.5,223.9 1213.1,223.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1692" d="M1222.5,223.9C1224.1,223.9 1225.4,222.6 1225.4,221C1225.4,219.4 1224.1,218.1 1222.5,218.1C1220.9,218.1 1219.6,219.4 1219.6,221C1219.6,222.6 1221,223.9 1222.5,223.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1694" d="M1232,223.9C1233.6,223.9 1234.9,222.6 1234.9,221C1234.9,219.4 1233.6,218.1 1232,218.1C1230.4,218.1 1229.1,219.4 1229.1,221C1229.1,222.6 1230.4,223.9 1232,223.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1696" d="M1241.4,223.9C1243,223.9 1244.3,222.6 1244.3,221C1244.3,219.4 1243,218.1 1241.4,218.1C1239.8,218.1 1238.5,219.4 1238.5,221C1238.5,222.6 1239.8,223.9 1241.4,223.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1698" d="M1250.8,224.8C1252.9,224.8 1254.6,223.1 1254.6,221C1254.6,218.9 1252.9,217.2 1250.8,217.2C1248.7,217.2 1247,218.9 1247,221C1247.1,223.1 1248.8,224.8 1250.8,224.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1700" d="M1260.3,223.9C1261.9,223.9 1263.2,222.6 1263.2,221C1263.2,219.4 1261.9,218.1 1260.3,218.1C1258.7,218.1 1257.4,219.4 1257.4,221C1257.4,222.6 1258.7,223.9 1260.3,223.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1702" d="M1269.7,223.9C1271.3,223.9 1272.6,222.6 1272.6,221C1272.6,219.4 1271.3,218.1 1269.7,218.1C1268.1,218.1 1266.8,219.4 1266.8,221C1266.8,222.6 1268.1,223.9 1269.7,223.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1704" d="M1279.2,223.9C1280.8,223.9 1282.1,222.6 1282.1,221C1282.1,219.4 1280.8,218.1 1279.2,218.1C1277.6,218.1 1276.3,219.4 1276.3,221C1276.3,222.6 1277.6,223.9 1279.2,223.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1706" d="M1288.6,224.4C1290.5,224.4 1292,222.9 1292,221C1292,219.2 1290.5,217.6 1288.6,217.6C1286.8,217.6 1285.3,219.1 1285.3,221C1285.2,222.8 1286.7,224.4 1288.6,224.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1708" d="M1298,224.4C1299.9,224.4 1301.4,222.9 1301.4,221C1301.4,219.1 1299.9,217.6 1298,217.6C1296.1,217.6 1294.6,219.1 1294.6,221C1294.6,222.9 1296.1,224.4 1298,224.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1710" d="M1307.5,224.4C1309.4,224.4 1310.9,222.9 1310.9,221C1310.9,219.1 1309.4,217.6 1307.5,217.6C1305.6,217.6 1304.1,219.1 1304.1,221C1304.1,222.9 1305.6,224.4 1307.5,224.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1774" d="M1175.4,233.3C1177,233.3 1178.3,232 1178.3,230.4C1178.3,228.8 1177,227.5 1175.4,227.5C1173.8,227.5 1172.5,228.8 1172.5,230.4C1172.5,232 1173.8,233.3 1175.4,233.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1776" d="M1184.8,233.9C1186.7,233.9 1188.3,232.3 1188.3,230.4C1188.3,228.5 1186.8,226.9 1184.8,226.9C1182.9,226.9 1181.3,228.5 1181.3,230.4C1181.4,232.3 1182.9,233.9 1184.8,233.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1778" d="M1194.3,233.3C1195.9,233.3 1197.2,232 1197.2,230.4C1197.2,228.8 1195.9,227.5 1194.3,227.5C1192.7,227.5 1191.4,228.8 1191.4,230.4C1191.4,232 1192.6,233.3 1194.3,233.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1780" d="M1203.7,233.3C1205.3,233.3 1206.6,232 1206.6,230.4C1206.6,228.8 1205.3,227.5 1203.7,227.5C1202.1,227.5 1200.8,228.8 1200.8,230.4C1200.8,232 1202.1,233.3 1203.7,233.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1782" d="M1213.1,233.3C1214.7,233.3 1216,232 1216,230.4C1216,228.8 1214.7,227.5 1213.1,227.5C1211.5,227.5 1210.2,228.8 1210.2,230.4C1210.2,232 1211.5,233.3 1213.1,233.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1784" d="M1222.5,233.3C1224.1,233.3 1225.4,232 1225.4,230.4C1225.4,228.8 1224.1,227.5 1222.5,227.5C1220.9,227.5 1219.6,228.8 1219.6,230.4C1219.7,232 1221,233.3 1222.5,233.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1786" d="M1232,233.5C1233.7,233.5 1235.1,232.1 1235.1,230.4C1235.1,228.7 1233.7,227.3 1232,227.3C1230.3,227.3 1228.9,228.7 1228.9,230.4C1228.9,232.1 1230.3,233.5 1232,233.5Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1788" d="M1241.4,233.3C1243,233.3 1244.3,232 1244.3,230.4C1244.3,228.8 1243,227.5 1241.4,227.5C1239.8,227.5 1238.5,228.8 1238.5,230.4C1238.6,232 1239.8,233.3 1241.4,233.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1790" d="M1250.8,233.3C1252.4,233.3 1253.7,232 1253.7,230.4C1253.7,228.8 1252.4,227.5 1250.8,227.5C1249.2,227.5 1247.9,228.8 1247.9,230.4C1248,232 1249.3,233.3 1250.8,233.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1792" d="M1260.3,233.3C1261.9,233.3 1263.2,232 1263.2,230.4C1263.2,228.8 1261.9,227.5 1260.3,227.5C1258.7,227.5 1257.4,228.8 1257.4,230.4C1257.4,232 1258.7,233.3 1260.3,233.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1794" d="M1269.7,233.3C1271.3,233.3 1272.6,232 1272.6,230.4C1272.6,228.8 1271.3,227.5 1269.7,227.5C1268.1,227.5 1266.8,228.8 1266.8,230.4C1266.8,232 1268.1,233.3 1269.7,233.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1796" d="M1279.2,234.4C1281.4,234.4 1283.2,232.6 1283.2,230.4C1283.2,228.2 1281.4,226.4 1279.2,226.4C1277,226.4 1275.2,228.2 1275.2,230.4C1275.1,232.6 1276.9,234.4 1279.2,234.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1798" d="M1288.6,233.8C1290.5,233.8 1292,232.3 1292,230.4C1292,228.6 1290.5,227.1 1288.6,227.1C1286.8,227.1 1285.3,228.6 1285.3,230.4C1285.2,232.3 1286.7,233.8 1288.6,233.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1800" d="M1298,233.8C1299.9,233.8 1301.4,232.3 1301.4,230.4C1301.4,228.5 1299.9,227 1298,227C1296.1,227 1294.6,228.5 1294.6,230.4C1294.6,232.3 1296.1,233.8 1298,233.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1802" d="M1307.5,233.8C1309.4,233.8 1310.9,232.3 1310.9,230.4C1310.9,228.5 1309.4,227 1307.5,227C1305.6,227 1304.1,228.5 1304.1,230.4C1304,232.3 1305.6,233.8 1307.5,233.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1866" d="M1175.4,242.7C1177,242.7 1178.3,241.4 1178.3,239.8C1178.3,238.2 1177,236.9 1175.4,236.9C1173.8,236.9 1172.5,238.2 1172.5,239.8C1172.5,241.5 1173.8,242.7 1175.4,242.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1868" d="M1184.8,242.8C1186.4,242.8 1187.8,241.5 1187.8,239.9C1187.8,238.3 1186.5,237 1184.8,237C1183.2,237 1181.9,238.3 1181.9,239.9C1181.9,241.5 1183.2,242.8 1184.8,242.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1870" d="M1194.3,242.7C1195.9,242.7 1197.2,241.4 1197.2,239.8C1197.2,238.2 1195.9,236.9 1194.3,236.9C1192.7,236.9 1191.4,238.2 1191.4,239.8C1191.4,241.5 1192.6,242.7 1194.3,242.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1872" d="M1203.7,242.7C1205.3,242.7 1206.6,241.4 1206.6,239.8C1206.6,238.2 1205.3,236.9 1203.7,236.9C1202.1,236.9 1200.8,238.2 1200.8,239.8C1200.8,241.5 1202.1,242.7 1203.7,242.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1874" d="M1213.1,242.7C1214.7,242.7 1216,241.4 1216,239.8C1216,238.2 1214.7,236.9 1213.1,236.9C1211.5,236.9 1210.2,238.2 1210.2,239.8C1210.2,241.5 1211.5,242.7 1213.1,242.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1876" d="M1222.5,242.7C1224.1,242.7 1225.4,241.4 1225.4,239.8C1225.4,238.2 1224.1,236.9 1222.5,236.9C1220.9,236.9 1219.6,238.2 1219.6,239.8C1219.7,241.5 1221,242.7 1222.5,242.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1878" d="M1232,242.7C1233.6,242.7 1234.9,241.4 1234.9,239.8C1234.9,238.2 1233.6,236.9 1232,236.9C1230.4,236.9 1229.1,238.2 1229.1,239.8C1229.1,241.5 1230.4,242.7 1232,242.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1880" d="M1241.4,243.1C1243.2,243.1 1244.6,241.7 1244.6,239.9C1244.6,238.1 1243.1,236.7 1241.4,236.7C1239.6,236.7 1238.2,238.1 1238.2,239.9C1238.2,241.7 1239.6,243.1 1241.4,243.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1882" d="M1250.8,243.1C1252.6,243.1 1254.1,241.6 1254.1,239.8C1254.1,238 1252.6,236.5 1250.8,236.5C1249,236.5 1247.5,238 1247.5,239.8C1247.5,241.6 1249,243.1 1250.8,243.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1884" d="M1260.3,242.7C1261.9,242.7 1263.2,241.4 1263.2,239.8C1263.2,238.2 1261.9,236.9 1260.3,236.9C1258.7,236.9 1257.4,238.2 1257.4,239.8C1257.4,241.5 1258.7,242.7 1260.3,242.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1886" d="M1269.7,242.8C1271.3,242.8 1272.7,241.5 1272.7,239.8C1272.7,238.2 1271.4,236.8 1269.7,236.8C1268,236.8 1266.7,238.1 1266.7,239.8C1266.8,241.5 1268.1,242.8 1269.7,242.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1888" d="M1279.2,243.6C1281.2,243.6 1282.9,241.9 1282.9,239.9C1282.9,237.8 1281.2,236.2 1279.2,236.2C1277.1,236.2 1275.5,237.9 1275.5,239.9C1275.4,241.9 1277.1,243.6 1279.2,243.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1890" d="M1288.6,243.2C1290.5,243.2 1292,241.7 1292,239.8C1292,238 1290.5,236.4 1288.6,236.4C1286.8,236.4 1285.3,237.9 1285.3,239.8C1285.2,241.7 1286.7,243.2 1288.6,243.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1892" d="M1298,243.3C1299.9,243.3 1301.4,241.8 1301.4,239.9C1301.4,238 1299.9,236.5 1298,236.5C1296.1,236.5 1294.6,238 1294.6,239.9C1294.6,241.8 1296.1,243.3 1298,243.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1894" d="M1307.5,243.3C1309.4,243.3 1310.9,241.8 1310.9,239.9C1310.9,238 1309.4,236.5 1307.5,236.5C1305.6,236.5 1304.1,238 1304.1,239.9C1304.1,241.8 1305.6,243.3 1307.5,243.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1972" d="M1203.7,253.3C1205.9,253.3 1207.7,251.5 1207.7,249.3C1207.7,247.1 1205.9,245.3 1203.7,245.3C1201.5,245.3 1199.7,247.1 1199.7,249.3C1199.7,251.5 1201.5,253.3 1203.7,253.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1976" d="M1222.5,253.3C1224.7,253.3 1226.5,251.5 1226.5,249.3C1226.5,247.1 1224.7,245.3 1222.5,245.3C1220.3,245.3 1218.5,247.1 1218.5,249.3C1218.6,251.5 1220.4,253.3 1222.5,253.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1982" d="M1250.8,252.6C1252.6,252.6 1254.1,251.1 1254.1,249.3C1254.1,247.5 1252.6,246 1250.8,246C1249,246 1247.5,247.5 1247.5,249.3C1247.6,251.1 1249,252.6 1250.8,252.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1984" d="M1260.3,252.6C1262.1,252.6 1263.6,251.1 1263.6,249.3C1263.6,247.5 1262.1,246 1260.3,246C1258.5,246 1257,247.5 1257,249.3C1257,251.1 1258.4,252.6 1260.3,252.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1988" d="M1279.2,253C1281.2,253 1282.9,251.3 1282.9,249.3C1282.9,247.3 1281.2,245.6 1279.2,245.6C1277.1,245.6 1275.5,247.3 1275.5,249.3C1275.4,251.3 1277.1,253 1279.2,253Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1990" d="M1288.6,252.6C1290.5,252.6 1292,251.1 1292,249.2C1292,247.4 1290.5,245.9 1288.6,245.9C1286.8,245.9 1285.3,247.4 1285.3,249.2C1285.2,251.1 1286.7,252.6 1288.6,252.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1992" d="M1298,252.7C1299.9,252.7 1301.4,251.2 1301.4,249.3C1301.4,247.4 1299.9,245.9 1298,245.9C1296.1,245.9 1294.6,247.4 1294.6,249.3C1294.6,251.2 1296.1,252.7 1298,252.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path1994" d="M1307.5,252.7C1309.4,252.7 1310.9,251.2 1310.9,249.3C1310.9,247.4 1309.4,245.9 1307.5,245.9C1305.6,245.9 1304.1,247.4 1304.1,249.3C1304,251.2 1305.6,252.7 1307.5,252.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
</g>
<g id="g26674" transform="matrix(5.83508,0,0,5.79928,-2923.37,-686.141)">
<path id="path15294" d="M586,223.7C584.4,223.7 583.1,225 583.1,226.6C583.1,228.2 584.4,229.5 586,229.5C587.6,229.5 588.9,228.2 588.9,226.6C588.8,225 587.6,223.7 586,223.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15296" d="M576.5,223.1C574.6,223.1 573,224.7 573,226.6C573,228.5 574.6,230.1 576.5,230.1C578.4,230.1 580,228.5 580,226.6C580,224.7 578.5,223.1 576.5,223.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15506" d="M586,204.9C584.4,204.9 583.1,206.2 583.1,207.8C583.1,209.4 584.4,210.7 586,210.7C587.6,210.7 588.9,209.4 588.9,207.8C588.8,206.2 587.6,204.9 586,204.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15508" d="M576.5,204.9C574.9,204.9 573.6,206.2 573.6,207.8C573.6,209.4 574.9,210.7 576.5,210.7C578.1,210.7 579.4,209.4 579.4,207.8C579.4,206.2 578.1,204.9 576.5,204.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15510" d="M519.9,204.9C518.3,204.9 517,206.2 517,207.8C517,209.4 518.3,210.7 519.9,210.7C521.5,210.7 522.8,209.4 522.8,207.8C522.8,206.2 521.5,204.9 519.9,204.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15512" d="M510.5,204.9C508.9,204.9 507.6,206.2 507.6,207.8C507.6,209.4 508.9,210.7 510.5,210.7C512.1,210.7 513.4,209.4 513.4,207.8C513.4,206.2 512.1,204.9 510.5,204.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15634" d="M567.1,195.4C565.5,195.4 564.2,196.7 564.2,198.3C564.2,199.9 565.5,201.2 567.1,201.2C568.7,201.2 570,199.9 570,198.3C570,196.7 568.7,195.4 567.1,195.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15636" d="M538.8,195.4C537.2,195.4 535.9,196.7 535.9,198.3C535.9,199.9 537.2,201.2 538.8,201.2C540.4,201.2 541.7,199.9 541.7,198.3C541.7,196.7 540.4,195.4 538.8,195.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15638" d="M519.9,195.4C518.3,195.4 517,196.7 517,198.3C517,199.9 518.3,201.2 519.9,201.2C521.5,201.2 522.8,199.9 522.8,198.3C522.8,196.7 521.5,195.4 519.9,195.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<circle id="circle15640" cx="510.5" cy="198.3" r="3" style="fill:rgb(76,167,217);"/>
<path id="path15766" d="M595.4,186C593.8,186 592.5,187.3 592.5,188.9C592.5,190.5 593.8,191.8 595.4,191.8C597,191.8 598.3,190.5 598.3,188.9C598.3,187.3 597,186 595.4,186Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15768" d="M567.1,186C565.5,186 564.2,187.3 564.2,188.9C564.2,190.5 565.5,191.8 567.1,191.8C568.7,191.8 570,190.5 570,188.9C570,187.3 568.7,186 567.1,186Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15770" d="M548.2,186C546.6,186 545.3,187.3 545.3,188.9C545.3,190.5 546.6,191.8 548.2,191.8C549.8,191.8 551.1,190.5 551.1,188.9C551.1,187.3 549.8,186 548.2,186Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15772" d="M538.8,186C537.2,186 535.9,187.3 535.9,188.9C535.9,190.5 537.2,191.8 538.8,191.8C540.4,191.8 541.7,190.5 541.7,188.9C541.7,187.3 540.4,186 538.8,186Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15774" d="M529.4,185C527.3,185 525.5,186.7 525.5,188.9C525.5,191 527.2,192.8 529.4,192.8C531.5,192.8 533.3,191.1 533.3,188.9C533.2,186.7 531.5,185 529.4,185Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15776" d="M519.9,185.7C518.1,185.7 516.7,187.1 516.7,188.9C516.7,190.7 518.1,192.1 519.9,192.1C521.7,192.1 523.1,190.7 523.1,188.9C523.1,187.1 521.7,185.7 519.9,185.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15778" d="M510.5,184.9C508.3,184.9 506.5,186.7 506.5,188.9C506.5,191.1 508.3,192.9 510.5,192.9C512.7,192.9 514.5,191.1 514.5,188.9C514.5,186.7 512.7,184.9 510.5,184.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15780" d="M501.1,186C499.5,186 498.2,187.3 498.2,188.9C498.2,190.5 499.5,191.8 501.1,191.8C502.7,191.8 504,190.5 504,188.9C503.9,187.3 502.6,186 501.1,186Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15904" d="M595.4,176.6C593.8,176.6 592.5,177.9 592.5,179.5C592.5,181.1 593.8,182.4 595.4,182.4C597,182.4 598.3,181.1 598.3,179.5C598.3,177.9 597,176.6 595.4,176.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15906" d="M576.5,176.6C574.9,176.6 573.6,177.9 573.6,179.5C573.6,181.1 574.9,182.4 576.5,182.4C578.1,182.4 579.4,181.1 579.4,179.5C579.4,177.9 578.1,176.6 576.5,176.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15908" d="M567.1,176.6C565.5,176.6 564.2,177.9 564.2,179.5C564.2,181.1 565.5,182.4 567.1,182.4C568.7,182.4 570,181.1 570,179.5C570,177.9 568.7,176.6 567.1,176.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15910" d="M557.7,176.4C556,176.4 554.6,177.8 554.6,179.5C554.6,181.2 556,182.6 557.7,182.6C559.4,182.6 560.8,181.2 560.8,179.5C560.7,177.7 559.4,176.4 557.7,176.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15912" d="M548.2,175.4C546,175.4 544.2,177.2 544.2,179.4C544.2,181.6 546,183.4 548.2,183.4C550.4,183.4 552.2,181.6 552.2,179.4C552.2,177.2 550.4,175.4 548.2,175.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15914" d="M538.8,176.6C537.2,176.6 535.9,177.9 535.9,179.5C535.9,181.1 537.2,182.4 538.8,182.4C540.4,182.4 541.7,181.1 541.7,179.5C541.7,177.9 540.4,176.6 538.8,176.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15916" d="M529.4,176.6C527.8,176.6 526.5,177.9 526.5,179.5C526.5,181.1 527.8,182.4 529.4,182.4C531,182.4 532.3,181.1 532.3,179.5C532.2,177.9 531,176.6 529.4,176.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15918" d="M519.9,176.4C518.2,176.4 516.9,177.7 516.9,179.4C516.9,181.1 518.3,182.4 519.9,182.4C521.6,182.4 522.9,181.1 522.9,179.4C522.9,177.8 521.6,176.4 519.9,176.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15920" d="M510.5,176.2C508.7,176.2 507.3,177.6 507.3,179.4C507.3,181.2 508.7,182.6 510.5,182.6C512.3,182.6 513.7,181.2 513.7,179.4C513.7,177.7 512.3,176.2 510.5,176.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path15922" d="M501.1,176.6C499.5,176.6 498.2,177.9 498.2,179.5C498.2,181.1 499.5,182.4 501.1,182.4C502.7,182.4 504,181.1 504,179.5C503.9,177.9 502.6,176.6 501.1,176.6Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16048" d="M652,167.1C650.4,167.1 649.1,168.4 649.1,170C649.1,171.6 650.4,172.9 652,172.9C653.6,172.9 654.9,171.6 654.9,170C654.9,168.4 653.6,167.1 652,167.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16050" d="M623.7,167.1C622.1,167.1 620.8,168.4 620.8,170C620.8,171.6 622.1,172.9 623.7,172.9C625.3,172.9 626.6,171.6 626.6,170C626.6,168.4 625.3,167.1 623.7,167.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16052" d="M614.3,167.1C612.7,167.1 611.4,168.4 611.4,170C611.4,171.6 612.7,172.9 614.3,172.9C615.9,172.9 617.2,171.6 617.2,170C617.2,168.4 615.8,167.1 614.3,167.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16054" d="M604.8,167.1C603.2,167.1 601.9,168.4 601.9,170C601.9,171.6 603.2,172.9 604.8,172.9C606.4,172.9 607.7,171.6 607.7,170C607.7,168.4 606.4,167.1 604.8,167.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16056" d="M595.4,167.1C593.8,167.1 592.5,168.4 592.5,170C592.5,171.6 593.8,172.9 595.4,172.9C597,172.9 598.3,171.6 598.3,170C598.3,168.4 597,167.1 595.4,167.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16058" d="M586,166.3C584,166.3 582.3,168 582.3,170C582.3,172.1 584,173.7 586,173.7C588,173.7 589.7,172 589.7,170C589.6,168 588,166.3 586,166.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16060" d="M576.5,167.1C574.9,167.1 573.6,168.4 573.6,170C573.6,171.6 574.9,172.9 576.5,172.9C578.1,172.9 579.4,171.6 579.4,170C579.4,168.4 578.1,167.1 576.5,167.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16062" d="M567.1,167C565.4,167 564,168.4 564,170.1C564,171.8 565.4,173.2 567.1,173.2C568.8,173.2 570.2,171.8 570.2,170.1C570.2,168.3 568.8,167 567.1,167Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16064" d="M557.7,166.7C555.9,166.7 554.4,168.2 554.4,170C554.4,171.9 555.9,173.4 557.7,173.4C559.6,173.4 561.1,171.9 561.1,170C561,168.2 559.5,166.7 557.7,166.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16066" d="M548.2,166.2C546.1,166.2 544.4,167.9 544.4,170C544.4,172.1 546.1,173.8 548.2,173.8C550.3,173.8 552,172.1 552,170C552,167.9 550.3,166.2 548.2,166.2Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16068" d="M538.8,166C536.6,166 534.8,167.8 534.8,170C534.8,172.2 536.6,174 538.8,174C541,174 542.8,172.2 542.8,170C542.8,167.8 541,166 538.8,166Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16070" d="M529.4,167.1C527.8,167.1 526.5,168.4 526.5,170C526.5,171.6 527.8,172.9 529.4,172.9C531,172.9 532.3,171.6 532.3,170C532.2,168.4 531,167.1 529.4,167.1Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16072" d="M519.9,167C518.2,167 516.9,168.4 516.9,170C516.9,171.7 518.3,173 519.9,173C521.6,173 522.9,171.7 522.9,170C522.9,168.4 521.6,167 519.9,167Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16074" d="M510.5,166.8C508.7,166.8 507.3,168.2 507.3,170C507.3,171.8 508.7,173.2 510.5,173.2C512.3,173.2 513.7,171.8 513.7,170C513.7,168.3 512.3,166.8 510.5,166.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16076" d="M501.1,166.7C499.3,166.7 497.8,168.2 497.8,170C497.8,171.9 499.3,173.4 501.1,173.4C503,173.4 504.5,171.9 504.5,170C504.4,168.2 502.9,166.7 501.1,166.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16158" d="M614.3,157.7C612.7,157.7 611.4,159 611.4,160.6C611.4,162.2 612.7,163.5 614.3,163.5C615.9,163.5 617.2,162.2 617.2,160.6C617.2,159 615.8,157.7 614.3,157.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16160" d="M604.8,157.7C603.2,157.7 601.9,159 601.9,160.6C601.9,162.2 603.2,163.5 604.8,163.5C606.4,163.5 607.7,162.2 607.7,160.6C607.8,159 606.4,157.7 604.8,157.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16162" d="M595.4,157.7C593.8,157.7 592.5,159 592.5,160.6C592.5,162.2 593.8,163.5 595.4,163.5C597,163.5 598.3,162.2 598.3,160.6C598.3,159 597,157.7 595.4,157.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16244" d="M548.2,148.3C546.6,148.3 545.3,149.6 545.3,151.2C545.3,152.8 546.6,154.1 548.2,154.1C549.8,154.1 551.1,152.8 551.1,151.2C551.1,149.6 549.8,148.3 548.2,148.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16246" d="M538.8,148.3C537.2,148.3 535.9,149.6 535.9,151.2C535.9,152.8 537.2,154.1 538.8,154.1C540.4,154.1 541.7,152.8 541.7,151.2C541.7,149.6 540.4,148.3 538.8,148.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16248" d="M529.4,147.7C527.5,147.7 525.9,149.3 525.9,151.2C525.9,153.1 527.4,154.7 529.4,154.7C531.3,154.7 532.9,153.1 532.9,151.2C532.8,149.2 531.3,147.7 529.4,147.7Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16370" d="M576.5,138.8C574.9,138.8 573.6,140.1 573.6,141.7C573.6,143.3 574.9,144.6 576.5,144.6C578.1,144.6 579.4,143.3 579.4,141.7C579.4,140.1 578.1,138.8 576.5,138.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16372" d="M557.7,138.8C556.1,138.8 554.8,140.1 554.8,141.7C554.8,143.3 556.1,144.6 557.7,144.6C559.3,144.6 560.6,143.3 560.6,141.7C560.5,140.1 559.2,138.8 557.7,138.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16374" d="M548.2,138.8C546.6,138.8 545.3,140.1 545.3,141.7C545.3,143.3 546.6,144.6 548.2,144.6C549.8,144.6 551.1,143.3 551.1,141.7C551.1,140.1 549.8,138.8 548.2,138.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16376" d="M538.8,138.8C537.2,138.8 535.8,140.1 535.8,141.8C535.8,143.4 537.1,144.8 538.8,144.8C540.4,144.8 541.8,143.5 541.8,141.8C541.8,140.1 540.4,138.8 538.8,138.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16378" d="M529.4,138.3C527.5,138.3 525.9,139.8 525.9,141.8C525.9,143.7 527.4,145.3 529.4,145.3C531.3,145.3 532.9,143.7 532.9,141.8C532.8,139.8 531.3,138.3 529.4,138.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16380" d="M519.9,138.8C518.3,138.8 517,140.1 517,141.7C517,143.3 518.3,144.6 519.9,144.6C521.5,144.6 522.8,143.3 522.8,141.7C522.8,140.1 521.5,138.8 519.9,138.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<circle id="circle16382" cx="510.5" cy="141.7" r="3.1" style="fill:rgb(76,167,217);"/>
<path id="path16384" d="M501.1,138.8C499.5,138.8 498.2,140.1 498.2,141.7C498.2,143.3 499.5,144.6 501.1,144.6C502.7,144.6 504,143.3 504,141.7C503.9,140.1 502.6,138.8 501.1,138.8Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16514" d="M689.7,129.4C688.1,129.4 686.8,130.7 686.8,132.3C686.8,133.9 688.1,135.2 689.7,135.2C691.3,135.2 692.6,133.9 692.6,132.3C692.6,130.7 691.3,129.4 689.7,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16516" d="M680.3,129C678.5,129 677,130.5 677,132.3C677,134.1 678.5,135.6 680.3,135.6C682.1,135.6 683.6,134.1 683.6,132.3C683.6,130.5 682.1,129 680.3,129Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16518" d="M652,129.4C650.4,129.4 649.1,130.7 649.1,132.3C649.1,133.9 650.4,135.2 652,135.2C653.6,135.2 654.9,133.9 654.9,132.3C654.9,130.7 653.6,129.4 652,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16520" d="M642.6,128.9C640.8,128.9 639.2,130.4 639.2,132.2C639.2,134.1 640.7,135.6 642.6,135.6C644.4,135.6 646,134.1 646,132.2C645.9,130.4 644.4,128.9 642.6,128.9Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16522" d="M623.7,129.4C622.1,129.4 620.8,130.7 620.8,132.3C620.8,133.9 622.1,135.2 623.7,135.2C625.3,135.2 626.6,133.9 626.6,132.3C626.6,130.7 625.3,129.4 623.7,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16524" d="M614.3,129.4C612.7,129.4 611.4,130.7 611.4,132.3C611.4,133.9 612.7,135.2 614.3,135.2C615.9,135.2 617.2,133.9 617.2,132.3C617.2,130.7 615.8,129.4 614.3,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16526" d="M604.8,129.4C603.2,129.4 601.9,130.7 601.9,132.3C601.9,133.9 603.2,135.2 604.8,135.2C606.4,135.2 607.7,133.9 607.7,132.3C607.7,130.7 606.4,129.4 604.8,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16528" d="M595.4,129.4C593.8,129.4 592.5,130.7 592.5,132.3C592.5,133.9 593.8,135.2 595.4,135.2C597,135.2 598.3,133.9 598.3,132.3C598.3,130.7 597,129.4 595.4,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16530" d="M586,129.4C584.4,129.4 583.1,130.7 583.1,132.3C583.1,133.9 584.4,135.2 586,135.2C587.6,135.2 588.9,133.9 588.9,132.3C588.9,130.7 587.6,129.4 586,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16532" d="M576.5,129.4C574.9,129.4 573.6,130.7 573.6,132.3C573.6,133.9 574.9,135.2 576.5,135.2C578.1,135.2 579.4,133.9 579.4,132.3C579.4,130.7 578.1,129.4 576.5,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16534" d="M567.1,129.3C565.5,129.3 564.2,130.6 564.2,132.2C564.2,133.8 565.5,135.1 567.1,135.1C568.7,135.1 570,133.8 570,132.2C570,130.7 568.7,129.3 567.1,129.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16536" d="M557.7,129.4C556.1,129.4 554.8,130.7 554.8,132.3C554.8,133.9 556.1,135.2 557.7,135.2C559.3,135.2 560.6,133.9 560.6,132.3C560.6,130.7 559.2,129.4 557.7,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16538" d="M548.2,129.4C546.6,129.4 545.3,130.7 545.3,132.3C545.3,133.9 546.6,135.2 548.2,135.2C549.8,135.2 551.1,133.9 551.1,132.3C551.1,130.7 549.8,129.4 548.2,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16540" d="M538.8,129.3C537.2,129.3 535.8,130.6 535.8,132.3C535.8,134 537.1,135.3 538.8,135.3C540.4,135.3 541.8,134 541.8,132.3C541.8,130.6 540.4,129.3 538.8,129.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16542" d="M529.4,129.3C527.8,129.3 526.4,130.6 526.4,132.3C526.4,134 527.7,135.3 529.4,135.3C531,135.3 532.4,134 532.4,132.3C532.4,130.6 531,129.3 529.4,129.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16544" d="M519.9,129.3C518.3,129.3 516.9,130.6 516.9,132.3C516.9,134 518.2,135.3 519.9,135.3C521.5,135.3 522.9,134 522.9,132.3C522.9,130.6 521.6,129.3 519.9,129.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16546" d="M510.5,129.3C508.9,129.3 507.5,130.6 507.5,132.3C507.5,134 508.8,135.3 510.5,135.3C512.1,135.3 513.5,134 513.5,132.3C513.5,130.6 512.1,129.3 510.5,129.3Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16548" d="M501.1,129.4C499.5,129.4 498.2,130.7 498.2,132.3C498.2,133.9 499.5,135.2 501.1,135.2C502.7,135.2 504,133.9 504,132.3C504,130.7 502.6,129.4 501.1,129.4Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16670" d="M708.6,120C707,120 705.7,121.3 705.7,122.9C705.7,124.5 707,125.8 708.6,125.8C710.2,125.8 711.5,124.5 711.5,122.9C711.5,121.3 710.2,120 708.6,120Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16672" d="M699.2,120C697.6,120 696.3,121.3 696.3,122.9C696.3,124.5 697.6,125.8 699.2,125.8C700.8,125.8 702.1,124.5 702.1,122.9C702,121.3 700.8,120 699.2,120Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16674" d="M595.4,120C593.8,120 592.5,121.3 592.5,122.9C592.5,124.5 593.8,125.8 595.4,125.8C597,125.8 598.3,124.5 598.3,122.9C598.3,121.3 597,120 595.4,120Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
<path id="path16676" d="M586,120C584.4,120 583.1,121.3 583.1,122.9C583.1,124.5 584.4,125.8 586,125.8C587.6,125.8 588.9,124.5 588.9,122.9C588.8,121.3 587.6,120 586,120Z" style="fill:rgb(76,167,217);fill-rule:nonzero;"/>
</g>
</svg>
<svg clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="1.4142" version="1.1"
viewBox="0 0 1344 1344" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="a" x1="18.23" x2="150" y1="150" y2="-7.6294e-6" gradientTransform="matrix(8.96 0 0 8.96 -7.8457e-5 .00019795)" gradientUnits="userSpaceOnUse"><stop stop-color="#0082c9" offset="0"/><stop
stop-color="#1cafff" offset="1" /></linearGradient></defs>
<rect y=".00012207" width="1344" height="1344" fill="url(#a)" fill-rule="evenodd" /></svg>

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 565 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1344 1344" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path id="path2" d="M671.959,347C493.701,347.003 347.017,493.69 347.017,671.951L347.017,671.96C347.017,850.222 493.701,996.909 671.959,996.912C731.433,996.791 789.748,980.344 840.521,949.372C880.458,965.242 969.914,1012.33 991.205,991.988C1013.45,970.741 965.085,870.743 953.49,833.584C981.82,784.426 996.787,728.695 996.898,671.958C996.895,493.7 850.215,347.016 671.96,347.011L671.959,347ZM671.997,470.543C782.529,470.543 873.483,561.5 873.483,672.035C873.476,782.565 782.524,873.512 671.996,873.512C561.469,873.512 470.517,782.565 470.509,672.035C470.509,561.5 561.464,470.543 671.996,470.543L671.997,470.543ZM670.273,655.827C670.273,674.121 666.595,689.323 659.238,701.433C651.882,713.542 641.497,721.745 628.086,726.042L662.07,761.394L637.949,761.394L610.117,729.167L604.746,729.362C583.717,729.362 567.49,722.933 556.064,710.075C544.639,697.217 538.926,679.069 538.926,655.632C538.926,632.39 544.655,614.372 556.113,601.579C567.572,588.786 583.848,582.39 604.941,582.39C625.449,582.39 641.465,588.9 652.988,601.921C664.512,614.942 670.273,632.91 670.273,655.827ZM787.852,727.409L770.078,681.999L712.852,681.999L695.273,727.409L678.477,727.409L734.922,584.05L748.887,584.05L805.039,727.409L787.852,727.409ZM556.504,655.827C556.504,675.163 560.622,689.828 568.857,699.821C577.093,709.815 589.056,714.811 604.746,714.811C620.566,714.811 632.513,709.831 640.586,699.87C648.659,689.909 652.695,675.228 652.695,655.827C652.695,636.621 648.675,622.054 640.635,612.126C632.594,602.198 620.697,597.233 604.941,597.233C589.121,597.233 577.093,602.23 568.857,612.224C560.622,622.217 556.504,636.752 556.504,655.827ZM764.902,667.058L748.301,622.819C746.152,617.22 743.939,610.352 741.66,602.214C740.228,608.464 738.177,615.332 735.508,622.819L718.711,667.058L764.902,667.058Z" style="fill:white;fill-rule:nonzero;"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
scripts/QA_keystore.jks Normal file

Binary file not shown.

35
scripts/uploadArtifact.sh Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env bash
#1: LOG_USERNAME
#2: LOG_PASSWORD
#3: DRONE_BUILD_NUMBER
#4: DRONE_PULL_REQUEST
#5: GITHUB_TOKEN
DAV_URL=https://nextcloud.kaminsky.me/remote.php/webdav/android-artifacts/
PUBLIC_URL=https://www.kaminsky.me/nc-dev/android-artifacts
USER=$1
PASS=$2
BUILD=$3
PR=$4
GITHUB_TOKEN=$5
if ! test -e app/build/outputs/apk/qa/debug/app-qa-*.apk ; then
exit 1
fi
echo "Uploaded artifact to $DAV_URL/$BUILD-talk.apk"
# delete all old comments, starting with "APK file:"
oldComments=$(curl 2>/dev/null --header "authorization: Bearer $GITHUB_TOKEN" -X GET https://api.github.com/repos/nextcloud/talk-android/issues/$PR/comments | jq '.[] | (.id |tostring) + "|" + (.user.login | test("github-actions") | tostring) + "|" + (.body | test("APK file:.*") | tostring)' | grep "true|true" | tr -d "\"" | cut -f1 -d"|")
echo $oldComments | while read comment ; do
curl 2>/dev/null --header "authorization: Bearer $GITHUB_TOKEN" -X DELETE https://api.github.com/repos/nextcloud/talk-android/issues/comments/$comment
done
apt-get -y install qrencode
qrencode -o $PR.png "$PUBLIC_URL/$BUILD-talk.apk"
curl -u $USER:$PASS -X PUT $DAV_URL/$BUILD-talk.apk --upload-file app/build/outputs/apk/qa/debug/app-qa-*.apk
curl -u $USER:$PASS -X PUT $DAV_URL/$BUILD-talk.png --upload-file $PR.png
curl --header "authorization: Bearer $GITHUB_TOKEN" -X POST https://api.github.com/repos/nextcloud/talk-android/issues/$PR/comments -d "{ \"body\" : \"APK file: $PUBLIC_URL/$BUILD-talk.apk <br/><br/> ![qrcode]($PUBLIC_URL/$BUILD-talk.png) <br/><br/>To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud Talk app. \" }"