Merge pull request #97 from vector-im/feature/settings

Feature/settings
This commit is contained in:
Benoit Marty 2019-04-04 11:39:56 +02:00 committed by GitHub
commit e929019247
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
460 changed files with 16171 additions and 947 deletions

View File

@ -48,7 +48,9 @@ script:
# Build app (assembleGplayRelease assembleFdroidRelease)
# Build Android test (assembleAndroidTest) (disabled for now)
# Code quality (lintGplayRelease lintFdroidRelease)
- ./gradlew clean assembleGplayRelease assembleFdroidRelease lintGplayRelease lintFdroidRelease --stacktrace
# Split into two steps because if a task contain Fdroid, PlayService will be disabled
- ./gradlew clean assembleGplayRelease lintGplayRelease --stacktrace
- ./gradlew clean assembleFdroidRelease lintFdroidRelease --stacktrace
# Run unitary test (Disable for now, see https://travis-ci.org/vector-im/riot-android/builds/502504370)
# - ./gradlew testGplayReleaseUnitTest --stacktrace
# Other code quality check

View File

@ -11,6 +11,7 @@ buildscript {
} }
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.google.gms:google-services:4.2.0'
classpath "com.airbnb.okreplay:gradle-plugin:1.4.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2'

View File

@ -58,6 +58,10 @@ android {
adbOptions {
installOptions "-g"
}

lintOptions {
lintConfig file("lint.xml")
}
}

static def gitRevision() {

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Modify some severity -->

<!-- Resource -->
<issue id="MissingTranslation" severity="warning" />
<issue id="TypographyEllipsis" severity="error" />
<issue id="ImpliedQuantity" severity="warning" />

<!-- UX -->
<issue id="ButtonOrder" severity="error" />

<!-- Layout -->
<issue id="UnknownIdInLayout" severity="error" />
<issue id="StringFormatCount" severity="error" />
<issue id="HardcodedText" severity="error" />
<issue id="SpUsage" severity="error" />
<issue id="ObsoleteLayoutParam" severity="error" />
<issue id="InefficientWeight" severity="error" />
<issue id="DisableBaselineAlignment" severity="error" />
<issue id="ScrollViewSize" severity="error" />

<!-- RTL -->
<issue id="RtlEnabled" severity="error" />
<issue id="RtlHardcoded" severity="error" />
<issue id="RtlSymmetry" severity="error" />

<!-- Code -->
<issue id="SetTextI18n" severity="error" />
<issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" />

</lint>

View File

@ -24,7 +24,7 @@ interface MatrixCallback<in T> {

/**
* On success method, default to no-op
* @param data the data successfuly returned from the async function
* @param data the data successfully returned from the async function
*/
fun onSuccess(data: T) {
//no-op

View File

@ -19,15 +19,22 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.user.UserService

/**
* This interface defines interactions with a session.
* An instance of a session will be provided by the SDK.
*/
interface Session : RoomService, GroupService, UserService {
interface Session :
RoomService,
GroupService,
UserService,
CryptoService,
SignOutService {

/**
* The params associated to the session
@ -69,5 +76,4 @@ interface Session : RoomService, GroupService, UserService {
// Not used at the moment
interface Listener


}

View File

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

package im.vector.matrix.android.api.session.crypto

interface CryptoService {

// Not supported for the moment
fun isCryptoEnabled() = false
}

View File

@ -26,16 +26,16 @@ interface ReadService {
/**
* Force the read marker to be set on the latest event.
*/
fun markAllAsRead(callback: MatrixCallback<Void>)
fun markAllAsRead(callback: MatrixCallback<Unit>)

/**
* Set the read receipt on the event with provided eventId.
*/
fun setReadReceipt(eventId: String, callback: MatrixCallback<Void>)
fun setReadReceipt(eventId: String, callback: MatrixCallback<Unit>)

/**
* Set the read marker on the event with provided eventId.
*/
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Void>)
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)

}

View File

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

package im.vector.matrix.android.api.session.signout

import im.vector.matrix.android.api.MatrixCallback

/**
* This interface defines a method to sign out. It's implemented at the session level.
*/
interface SignOutService {

/**
* Sign out
*/
fun signOut(callback: MatrixCallback<Unit>)

}

View File

@ -25,4 +25,5 @@ internal interface SessionParamsStore {

fun save(sessionParams: SessionParams): Try<SessionParams>

fun delete(): Try<Unit>
}

View File

@ -17,13 +17,13 @@
package im.vector.matrix.android.internal.auth.db

import arrow.core.Try
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.SessionParamsStore
import io.realm.Realm
import io.realm.RealmConfiguration

internal class RealmSessionParamsStore(private val mapper: SessionParamsMapper,
private val realmConfiguration: RealmConfiguration) : SessionParamsStore {
private val realmConfiguration: RealmConfiguration) : SessionParamsStore {

override fun save(sessionParams: SessionParams): Try<SessionParams> {
return Try {
@ -50,4 +50,16 @@ internal class RealmSessionParamsStore(private val mapper: SessionParamsMapper,
return sessionParams
}

override fun delete(): Try<Unit> {
return Try {
val realm = Realm.getInstance(realmConfiguration)
realm.executeTransaction {
it.where(SessionParamsEntity::class.java)
.findAll()
.deleteAllFromRealm()
}
realm.close()
}
}

}

View File

@ -20,6 +20,7 @@ import android.os.Looper
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentUrlResolver
@ -29,13 +30,16 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.di.MatrixKoinHolder
import im.vector.matrix.android.internal.session.group.GroupModule
import im.vector.matrix.android.internal.session.room.RoomModule
import im.vector.matrix.android.internal.session.signout.SignOutModule
import im.vector.matrix.android.internal.session.sync.SyncModule
import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.user.UserModule
@ -57,6 +61,7 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
private val roomService by inject<RoomService>()
private val groupService by inject<GroupService>()
private val userService by inject<UserService>()
private val signOutService by inject<SignOutService>()
private val syncThread by inject<SyncThread>()
private val contentUrlResolver by inject<ContentUrlResolver>()
private var isOpen = false
@ -70,8 +75,9 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
val syncModule = SyncModule().definition
val roomModule = RoomModule().definition
val groupModule = GroupModule().definition
val signOutModule = SignOutModule().definition
val userModule = UserModule().definition
MatrixKoinHolder.instance.loadModules(listOf(sessionModule, syncModule, roomModule, groupModule, userModule))
MatrixKoinHolder.instance.loadModules(listOf(sessionModule, syncModule, roomModule, groupModule, signOutModule, userModule))
scope = getKoin().getOrCreateScope(SCOPE)
if (!monarchy.isMonarchyThreadOpen) {
monarchy.openManually()
@ -94,6 +100,23 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
isOpen = false
}

@MainThread
override fun signOut(callback: MatrixCallback<Unit>) {
assert(isOpen)
return signOutService.signOut(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// Close the session
close()

callback.onSuccess(data)
}

override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
}

override fun contentUrlResolver(): ContentUrlResolver {
return contentUrlResolver
}

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.content.DefaultContentUrlResolver
@ -33,6 +34,7 @@ import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.signout.DefaultSignOutService
import im.vector.matrix.android.internal.session.user.DefaultUserService
import im.vector.matrix.android.internal.session.user.UserEntityUpdater
import im.vector.matrix.android.internal.util.md5
@ -102,6 +104,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
DefaultGroupService(get()) as GroupService
}

scope(DefaultSession.SCOPE) {
DefaultSignOutService(get(), get()) as SignOutService
}

scope(DefaultSession.SCOPE) {
DefaultUserService(get()) as UserService
}

View File

@ -30,20 +30,20 @@ internal class DefaultReadService(private val roomId: String,
private val setReadMarkersTask: SetReadMarkersTask,
private val taskExecutor: TaskExecutor) : ReadService {

override fun markAllAsRead(callback: MatrixCallback<Void>) {
override fun markAllAsRead(callback: MatrixCallback<Unit>) {
val latestEvent = getLatestEvent()
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId)
setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor)
}

override fun setReadReceipt(eventId: String, callback: MatrixCallback<Void>) {
override fun setReadReceipt(eventId: String, callback: MatrixCallback<Unit>) {
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId)
setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor)
}

override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Void>) {
override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>) {
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = null)
setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
setReadMarkersTask.configureWith(params).dispatchTo(callback).executeBy(taskExecutor)
}

private fun getLatestEvent(): EventEntity? {

View File

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

package im.vector.matrix.android.internal.session.signout

import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith

internal class DefaultSignOutService(private val signOutTask: SignOutTask,
private val taskExecutor: TaskExecutor) : SignOutService {

override fun signOut(callback: MatrixCallback<Unit>) {
signOutTask
.configureWith(Unit)
.dispatchTo(callback)
.executeBy(taskExecutor)
}

}

View File

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

package im.vector.matrix.android.internal.session.signout

import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.POST

internal interface SignOutAPI {

/**
* Invalidate the access token, so that it can no longer be used for authorization.
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "logout")
fun signOut(): Call<Unit>

}

View File

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

package im.vector.matrix.android.internal.session.signout

import im.vector.matrix.android.internal.session.DefaultSession
import org.koin.dsl.module.module
import retrofit2.Retrofit

class SignOutModule {

val definition = module(override = true) {

scope(DefaultSession.SCOPE) {
val retrofit: Retrofit = get()
retrofit.create(SignOutAPI::class.java)
}

scope(DefaultSession.SCOPE) {
DefaultSignOutTask(get(), get()) as SignOutTask
}

}
}

View File

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

package im.vector.matrix.android.internal.session.signout

import arrow.core.Try
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task

internal interface SignOutTask : Task<Unit, Unit>


internal class DefaultSignOutTask(private val signOutAPI: SignOutAPI,
private val sessionParamsStore: SessionParamsStore) : SignOutTask {

override fun execute(params: Unit): Try<Unit> {
return executeRequest<Unit> {
apiCall = signOutAPI.signOut()
}.flatMap {
// TODO Clear DB, media cache, etc.
sessionParamsStore.delete()
}
}
}

View File

@ -1,20 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:orientation="horizontal" >
android:paddingBottom="10dp">

<ImageView
android:id="@+id/imageView_icon_and_text"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_gravity="center_vertical"
android:src="@drawable/matrix_user"/>
android:src="@drawable/matrix_user" />

<TextView
android:id="@+id/textView_icon_and_text"
@ -24,7 +25,7 @@
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:singleLine="true"
android:text = "A text here"
android:textColor="@android:color/white"/>
android:textColor="@android:color/white"
tools:text="A text here" />

</LinearLayout>

View File

@ -43,8 +43,8 @@
<string name="notice_room_name_removed">%1$s ha eliminat el nom de la sala</string>
<string name="notice_room_topic_removed">%1$s ha eliminat el tema de la sala</string>
<string name="notice_event_redacted">"ha redactat %1$s "</string>
<string name="notice_event_redacted_by">" per %1$s"</string>
<string name="notice_event_redacted_reason">" [raó: %1$s]"</string>
<string name="notice_event_redacted_by">per %1$s</string>
<string name="notice_event_redacted_reason">[raó: %1$s]</string>
<string name="notice_profile_change_redacted">%1$s ha actualitzat el seu perfil %2$s</string>
<string name="notice_room_third_party_invite">%1$s ha enviat una invitació a l\'usuari %2$s per a entrar a la sala</string>
<string name="notice_room_third_party_registered_invite">%1$s ha acceptat la invitació per a %2$s</string>

View File

@ -39,7 +39,7 @@
<string name="notice_avatar_changed_too">(Profilbild wurde ebenfalls geändert)</string>
<string name="notice_room_name_removed">%1$s hat den Raumnamen entfernt</string>
<string name="notice_room_topic_removed">%1$s hat das Raum-Thema entfernt</string>
<string name="notice_event_redacted">"Verborgen %1$s "</string>
<string name="notice_event_redacted">%1$s verborgen</string>
<string name="notice_event_redacted_by"> durch %1$s</string>
<string name="notice_event_redacted_reason"> [Grund: %1$s]</string>
<string name="notice_profile_change_redacted">%1$s hat das Benutzerprofil aktualisiert %2$s</string>

View File

@ -2,69 +2,69 @@
<resources>

<string name="summary_message">%1$s: %2$s</string>
<string name="summary_user_sent_image">%1$s stuurde een afbeelding.</string>
<string name="summary_user_sent_image">%1$s heeft een afbeelding gestuurd.</string>

<string name="notice_room_invite_no_invitee">%s\'s uitnodiging</string>
<string name="notice_room_invite">%1$s nodigde %2$s uit</string>
<string name="notice_room_invite_you">%1$s heeft jou uitgenodigd</string>
<string name="notice_room_join">%1$s is tot de ruimte toegetreden</string>
<string name="notice_room_leave">%1$s heeft de ruimte verlaten</string>
<string name="notice_room_reject">%1$s heeft de uitnodiging niet geaccepteerd</string>
<string name="notice_room_kick">%1$s verwijderde %2$s</string>
<string name="notice_room_invite_no_invitee">Uitnodiging van %s</string>
<string name="notice_room_invite">%1$s heeft %2$s uitgenodigd</string>
<string name="notice_room_invite_you">%1$s heeft u uitgenodigd</string>
<string name="notice_room_join">%1$s neemt nu deel aan het gesprek</string>
<string name="notice_room_leave">%1$s heeft het gesprek verlaten</string>
<string name="notice_room_reject">%1$s heeft de uitnodiging geweigerd</string>
<string name="notice_room_kick">%1$s heeft %2$s uit het gesprek verwijderd</string>
<string name="notice_room_unban">%1$s heeft %2$s ontbannen</string>
<string name="notice_room_ban">%1$s heeft %2$s verbannen</string>
<string name="notice_room_withdraw">%1$s heeft de uitnodiging van %2$s teruggetrokken</string>
<string name="notice_avatar_url_changed">%1$s heeft zijn of haar avatar aangepast</string>
<string name="notice_display_name_set">%1$s heeft zijn of haar naam aangepast naar %2$s</string>
<string name="notice_display_name_changed_from">%1$s heeft zijn of haar naam aangepast van %2$s naar %3$s</string>
<string name="notice_display_name_removed">%1$s heeft zijn of haar naam verwijderd (%2$s)</string>
<string name="notice_room_withdraw">%1$s heeft de uitnodiging van %2$s ingetrokken</string>
<string name="notice_avatar_url_changed">%1$s heeft zijn/haar avatar aangepast</string>
<string name="notice_display_name_set">%1$s heeft zijn/haar naam aangepast naar %2$s</string>
<string name="notice_display_name_changed_from">%1$s heeft zijn/haar naam aangepast van %2$s naar %3$s</string>
<string name="notice_display_name_removed">%1$s heeft zijn/haar naam verwijderd (%2$s)</string>
<string name="notice_room_topic_changed">%1$s heeft het onderwerp veranderd naar: %2$s</string>
<string name="notice_room_name_changed">%1$s heeft de ruimtenaam veranderd naar: %2$s</string>
<string name="notice_placed_video_call">%s heeft een video-oproep geplaatst.</string>
<string name="notice_placed_voice_call">%s heeft een spraak-oproep geplaatst.</string>
<string name="notice_room_name_changed">%1$s heeft de gespreksnaam veranderd naar: %2$s</string>
<string name="notice_placed_video_call">%s heeft een video-oproep gemaakt.</string>
<string name="notice_placed_voice_call">%s heeft een spraakoproep gemaakt.</string>
<string name="notice_answered_call">%s heeft de oproep beantwoord.</string>
<string name="notice_ended_call">%s heeft de oproep beëindigd.</string>
<string name="notice_made_future_room_visibility">%1$s heeft de toekomstige geschiedenis beschikbaar gemaakt voor %2$s</string>
<string name="notice_room_visibility_invited">alle ruimte deelnemers, vanaf het punt dat ze zijn uitgenodigd.</string>
<string name="notice_room_visibility_joined">alle ruimte deelnemers, vanaf het punt dat ze zijn toegetreden.</string>
<string name="notice_room_visibility_shared">alle ruimte deelnemers.</string>
<string name="notice_room_visibility_world_readable">Iedereen.</string>
<string name="notice_ended_call">%s heeft opgehangen.</string>
<string name="notice_made_future_room_visibility">%1$s heeft de toekomstige gespreksgeschiedenis zichtbaar gemaakt voor %2$s</string>
<string name="notice_room_visibility_invited">alle deelnemers aan het gesprek, vanaf het punt dat ze zijn uitgenodigd.</string>
<string name="notice_room_visibility_joined">alle deelnemers aan het gesprek, vanaf het punt dat ze zijn toegetreden.</string>
<string name="notice_room_visibility_shared">alle deelnemers aan het gesprek.</string>
<string name="notice_room_visibility_world_readable">iedereen.</string>
<string name="notice_room_visibility_unknown">onbekend (%s).</string>
<string name="notice_end_to_end">%1$s heeft eind-tot-eind encryptie aangezet (%2$s)</string>
<string name="notice_end_to_end">%1$s heeft eind-tot-eind-versleuteling aangezet (%2$s)</string>

<string name="notice_requested_voip_conference">%1$s heeft een VoIP vergadering aangevraagd</string>
<string name="notice_voip_started">VoIP vergadering gestart</string>
<string name="notice_voip_finished">VoIP vergadering gestopt</string>
<string name="notice_requested_voip_conference">%1$s heeft een VoIP-vergadering aangevraagd</string>
<string name="notice_voip_started">VoIP-vergadering gestart</string>
<string name="notice_voip_finished">VoIP-vergadering gestopt</string>

<string name="notice_avatar_changed_too">(avatar was veranderd naar)</string>
<string name="notice_room_name_removed">%1$s heeft de ruimtenaam verwijderd</string>
<string name="notice_room_topic_removed">%1$s heeft het ruimteonderwerp verwijderd</string>
<string name="notice_event_redacted">verdwijderd %1$s </string>
<string name="notice_avatar_changed_too">(avatar is ook veranderd)</string>
<string name="notice_room_name_removed">%1$s heeft de gespreksnaam verwijderd</string>
<string name="notice_room_topic_removed">%1$s heeft het gespreksonderwerp verwijderd</string>
<string name="notice_event_redacted">heeft %1$s verwijderd</string>
<string name="notice_event_redacted_by"> door %1$s</string>
<string name="notice_event_redacted_reason"> [reden: %1$s]</string>
<string name="notice_profile_change_redacted">%1$s heeft zijn of haar profiel %2$s geüpdatet</string>
<string name="notice_room_third_party_invite">%1$s stuurde een uitnodiging naar %2$s om de ruimte toe te treden</string>
<string name="notice_room_third_party_registered_invite">%1$s accepteerde de uitnodiging voor %2$s</string>
<string name="notice_profile_change_redacted">%1$s heeft zijn/haar profiel %2$s bijgewerkt</string>
<string name="notice_room_third_party_invite">%1$s heeft een uitnodiging naar %2$s gestuurd om het gesprek toe te treden</string>
<string name="notice_room_third_party_registered_invite">%1$s heeft de uitnodiging voor %2$s aanvaard</string>

<string name="notice_crypto_unable_to_decrypt">** Niet in staat tot het decoderen van: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">De afzender\'s apparaat heeft geen sleutels voor dit bericht gestuurd.</string>
<string name="notice_crypto_unable_to_decrypt">** Kan niet ontsleutelen: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">Het apparaat van de afzender heeft geen sleutels voor dit bericht gestuurd.</string>

<!-- Room Screen -->
<string name="could_not_redact">Kon niet verwijderd worden</string>
<string name="unable_to_send_message">Niet in staat om het bericht te sturen</string>
<string name="unable_to_send_message">Kan bericht niet verzenden</string>

<string name="message_failed_to_upload">Uploaden van de afbeelding mislukt</string>

<!-- general errors -->
<string name="network_error">Netwerkfout</string>
<string name="matrix_error">Matrix fout</string>
<string name="matrix_error">Matrix-fout</string>

<!-- Home Screen -->

<!-- Last seen time -->

<!-- room error messages -->
<string name="room_error_join_failed_empty_room">Het is momenteel niet mogelijk om een lege ruimte opnieuw toe te treden.</string>
<string name="room_error_join_failed_empty_room">Het is momenteel niet mogelijk om een leeg gesprek opnieuw toe te treden.</string>

<string name="encrypted_message">Versleuteld bericht</string>

@ -76,16 +76,16 @@

<string name="message_reply_to_prefix">Als antwoord op</string>

<string name="reply_to_an_image">verstuurde een plaatje.</string>
<string name="reply_to_a_video">verstuurde een video.</string>
<string name="reply_to_an_audio_file">verstuurde een audiobestand.</string>
<string name="reply_to_a_file">verstuurde een bestand.</string>
<string name="reply_to_an_image">heeft een afbeelding gestuurd.</string>
<string name="reply_to_a_video">heeft een video gestuurd.</string>
<string name="reply_to_an_audio_file">heeft een audiobestand gestuurd.</string>
<string name="reply_to_a_file">heeft een bestand gestuurd.</string>

<!-- Room display name -->
<string name="room_displayname_invite_from">Uitnodiging van %s</string>
<string name="room_displayname_room_invite">Ruimte uitnodiging</string>
<string name="room_displayname_room_invite">Gespreksuitnodiging</string>
<string name="room_displayname_two_members">%1$s en %2$s</string>
<string name="room_displayname_empty_room">Lege ruimte</string>
<string name="room_displayname_empty_room">Leeg gesprek</string>

<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s en 1 andere</item>

View File

@ -35,7 +35,7 @@
<string name="notice_ended_call">%s zakończył(a) rozmowę.</string>
<string name="notice_room_name_removed">%1$s usunął(-ęła) nazwę pokoju</string>
<string name="notice_room_topic_removed">%1$s usunął(-ęła) temat pokoju</string>
<string name="notice_event_redacted_reason">" [powód: %1$s]"</string>
<string name="notice_event_redacted_reason">[powód: %1$s]</string>
<string name="summary_user_sent_sticker">%1$s wysłał(a) naklejkę.</string>

<string name="notice_end_to_end">%1$s włączył(a) szyfrowanie end-to-end (%2$s)</string>
@ -54,7 +54,7 @@
<item quantity="one">%1$s i jeden inny</item>
<item quantity="few">%1$s i kilku innych</item>
<item quantity="many">%1$s i %2$d innych</item>
<item quantity="other" />
<item quantity="other"/>
</plurals>

<string name="notice_crypto_unable_to_decrypt">** Nie można odszyfrować: %s **</string>
@ -68,8 +68,8 @@
<string name="notice_voip_started">Rozpoczęto grupowe połączenie głosowe VoIP</string>
<string name="notice_voip_finished">Zakończono grupowe połączenie głosowe VoIP</string>

<string name="notice_event_redacted">"zredagowane %1$s… "</string>
<string name="notice_event_redacted_by">" przez %1$s"</string>
<string name="notice_event_redacted">zredagowane %1$s</string>
<string name="notice_event_redacted_by">przez %1$s</string>
<string name="notice_profile_change_redacted">%1$s zaktualizował swój profil %2$s</string>
<string name="notice_room_third_party_invite">%1$s wysłał(a) zaproszenie do %2$s aby dołączył(a) do tego pokoju</string>
<string name="notice_room_third_party_registered_invite">%1$s zaakceptował(a) zaproszenie dla %2$s</string>

View File

@ -38,9 +38,9 @@
<string name="notice_avatar_changed_too">(a tiež obrázok v profile)</string>
<string name="notice_room_name_removed">%1$s odstránil názov miestnosti</string>
<string name="notice_room_topic_removed">%1$s odstránil tému miestnosti</string>
<string name="notice_event_redacted">"zmazaná udalosť %1$s "</string>
<string name="notice_event_redacted_by">" používateľom %1$s"</string>
<string name="notice_event_redacted_reason">" [dôvod: %1$s]"</string>
<string name="notice_event_redacted">"vymazané %1$s "</string>
<string name="notice_event_redacted_by">používateľom %1$s</string>
<string name="notice_event_redacted_reason">[dôvod: %1$s]</string>
<string name="notice_profile_change_redacted">%1$s aktualizoval svoj profil %2$s</string>
<string name="notice_room_third_party_invite">%1$s pozval %2$s vstúpiť do miestnosti</string>
<string name="notice_room_third_party_registered_invite">%1$s prijal pozvanie do %2$s</string>
@ -81,7 +81,7 @@
<item quantity="one">%1$s a 1 ďalší</item>
<item quantity="few">%1$s a %2$d ďalší</item>
<item quantity="many">%1$s a %2$d ďalších</item>
<item quantity="other" />
<item quantity="other"/>
</plurals>



View File

@ -28,8 +28,8 @@

<string name="notice_avatar_changed_too">(u ndryshua edhe avatari)</string>
<string name="notice_room_name_removed">%1$s hoqi emrin e dhomës</string>
<string name="notice_event_redacted_by">" nga %1$s"</string>
<string name="notice_event_redacted_reason">" [arsye: %1$s]"</string>
<string name="notice_event_redacted_by">nga %1$s</string>
<string name="notice_event_redacted_reason">[arsye: %1$s]</string>
<string name="notice_profile_change_redacted">%1$s përditësoi profilin e tij %2$s</string>
<string name="notice_room_third_party_registered_invite">%1$s pranoi ftesën tuaj për %2$s</string>

@ -41,7 +41,7 @@
<string name="could_not_redact">Su redaktua dot</string>
<string name="unable_to_send_message">Sarrihet të dërgohet mesazh</string>

<string name="message_failed_to_upload">Su arrit të ngarkohej figurë</string>
<string name="message_failed_to_upload">Ngarkimi i figurës dështoi</string>

<string name="network_error">Gabim rrjeti</string>
<string name="matrix_error">Gabim Matrix</string>
@ -76,7 +76,7 @@
<string name="notice_end_to_end">%1$s aktivizoi fshehtëzim skaj-më-skaj (%2$s)</string>

<string name="notice_room_topic_removed">%1$s hoqi temën e dhomës</string>
<string name="notice_event_redacted">"redaktoi %1$s "</string>
<string name="notice_event_redacted">redaktoi %1$s</string>
<string name="notice_room_third_party_invite">%1$s dërgoi një ftesë për %2$s që të marrë pjesë në dhomë</string>
<plurals name="room_displayname_three_and_more_members">
<item quantity="one">%1$s dhe 1 tjetër</item>

View File

@ -105,7 +105,8 @@ fi
echo
echo "Search for long files..."

${checkLongFilesScript} 2000 \
# TODO Reduce this once VectorSettingsPreferencesFragment.kt has been reworked
${checkLongFilesScript} 3500 \
./vector/src/main/java \
./vector/src/main/res/layout \
./vector/src/main/res/values \

View File

@ -19,8 +19,8 @@
# Exit on any error
set -e

echo "Copy strings to SDK"
echo
echo "Copy strings to SDK"

cp ../matrix-android-sdk/matrix-sdk/src/main/res/values/strings.xml ./matrix-sdk-android/src/main/res/values/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-ar/strings.xml ./matrix-sdk-android/src/main/res/values-ar/strings.xml
@ -61,8 +61,7 @@ cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-zh-rCN/strings.xml ./mat
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-zh-rTW/strings.xml ./matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml

echo
echo "Copy strings to Riot"
echo
echo "Copy strings to RiotX"

cp ../riot-android/vector/src/main/res/values/strings.xml ./vector/src/main/res/values/strings.xml
cp ../riot-android/vector/src/main/res/values-ar/strings.xml ./vector/src/main/res/values-ar/strings.xml
@ -107,6 +106,39 @@ cp ../riot-android/vector/src/main/res/values-uk/strings.xml ./vector/src/ma
cp ../riot-android/vector/src/main/res/values-zh-rCN/strings.xml ./vector/src/main/res/values-zh-rCN/strings.xml
cp ../riot-android/vector/src/main/res/values-zh-rTW/strings.xml ./vector/src/main/res/values-zh-rTW/strings.xml

echo
echo "Copy drawables to RiotX"

cp ../riot-android/vector/src/main/res/drawable/* ./vector/src/main/res/drawable/
cp ../riot-android/vector/src/main/res/drawable-hdpi/* ./vector/src/main/res/drawable-hdpi/
cp ../riot-android/vector/src/main/res/drawable-mdpi/* ./vector/src/main/res/drawable-mdpi/
cp ../riot-android/vector/src/main/res/drawable-xhdpi/* ./vector/src/main/res/drawable-xhdpi/
cp ../riot-android/vector/src/main/res/drawable-xxhdpi/* ./vector/src/main/res/drawable-xxhdpi/
cp ../riot-android/vector/src/main/res/drawable-xxxhdpi/* ./vector/src/main/res/drawable-xxxhdpi/

echo
echo "Copy icon to RiotX"

cp ../riot-android/vector/src/main/res/mipmap-anydpi-v26/* ./vector/src/main/res/mipmap-anydpi-v26/
cp ../riot-android/vector/src/main/res/mipmap-hdpi/* ./vector/src/main/res/mipmap-hdpi/
cp ../riot-android/vector/src/main/res/mipmap-mdpi/* ./vector/src/main/res/mipmap-mdpi/
cp ../riot-android/vector/src/main/res/mipmap-xhdpi/* ./vector/src/main/res/mipmap-xhdpi/
cp ../riot-android/vector/src/main/res/mipmap-xxhdpi/* ./vector/src/main/res/mipmap-xxhdpi/
cp ../riot-android/vector/src/main/res/mipmap-xxxhdpi/* ./vector/src/main/res/mipmap-xxxhdpi/


echo
echo "Copy other elements to RiotX"

cp ../riot-android/vector/src/main/res/anim/* ./vector/src/main/res/anim/
cp ../riot-android/vector/src/main/res/color/* ./vector/src/main/res/color/
cp ../riot-android/vector/src/main/res/menu/* ./vector/src/main/res/menu/
cp ../riot-android/vector/src/main/res/menu/* ./vector/src/main/res/menu/
cp ../riot-android/vector/src/main/res/values-v17/* ./vector/src/main/res/values-v17/
cp ../riot-android/vector/src/main/res/values-v21/* ./vector/src/main/res/values-v21/
cp ../riot-android/vector/src/main/res/values-w480dp/* ./vector/src/main/res/values-w480dp/
cp ../riot-android/vector/src/main/res/values-w820dp/* ./vector/src/main/res/values-w820dp/
cp ../riot-android/vector/src/main/res/raw/* ./vector/src/main/res/raw/

echo
echo "Success!"

View File

@ -74,10 +74,12 @@ android {
buildTypes {
debug {
resValue "bool", "debug_mode", "true"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
}

release {
resValue "bool", "debug_mode", "false"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"

minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
@ -105,35 +107,7 @@ android {
}

lintOptions {
warning 'MissingTranslation'

// Treat some warnings as errors
// Resources
error 'TypographyEllipsis'
warning 'ImpliedQuantity'

// UX
error 'ButtonOrder'

// Layout
error 'UnknownIdInLayout'
error 'StringFormatCount'
error 'HardcodedText'
error 'SpUsage'
error 'ObsoleteLayoutParam'
error 'InefficientWeight'
error 'DisableBaselineAlignment'
error 'ScrollViewSize'

// RTL
error 'RtlEnabled'
error 'RtlHardcoded'
error 'RtlSymmetry'

// Code
error 'SetTextI18n'
error 'ViewConstructor'
error 'UseValueOf'
lintConfig file("lint.xml")
}

compileOptions {
@ -181,6 +155,9 @@ dependencies {
kapt "com.airbnb.android:epoxy-processor:$epoxy_version"
implementation 'com.airbnb.android:mvrx:0.7.0'

// Work
implementation "android.arch.work:work-runtime-ktx:1.0.0"

// FP
implementation "io.arrow-kt:arrow-core:$arrow_version"

@ -209,14 +186,23 @@ dependencies {
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"

// Badge for compatibility
implementation 'me.leolin:ShortcutBadger:1.1.2@aar'

// DI
implementation "org.koin:koin-android:$koin_version"
implementation "org.koin:koin-android-scope:$koin_version"

// gplay flavor only
gplayImplementation 'com.google.firebase:firebase-core:16.0.8'
gplayImplementation 'com.google.firebase:firebase-messaging:17.5.0'

// TESTS
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}


if (!getGradle().getStartParameter().getTaskRequests().toString().contains("Fdroid")) {
apply plugin: 'com.google.gms.google-services'
}

33
vector/lint.xml Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- Modify some severity -->

<!-- Resource -->
<issue id="MissingTranslation" severity="warning" />
<issue id="TypographyEllipsis" severity="error" />
<issue id="ImpliedQuantity" severity="warning" />

<!-- UX -->
<issue id="ButtonOrder" severity="error" />

<!-- Layout -->
<issue id="UnknownIdInLayout" severity="error" />
<issue id="StringFormatCount" severity="error" />
<issue id="HardcodedText" severity="error" />
<issue id="SpUsage" severity="error" />
<issue id="ObsoleteLayoutParam" severity="error" />
<issue id="InefficientWeight" severity="error" />
<issue id="DisableBaselineAlignment" severity="error" />
<issue id="ScrollViewSize" severity="error" />

<!-- RTL -->
<issue id="RtlEnabled" severity="error" />
<issue id="RtlHardcoded" severity="error" />
<issue id="RtlSymmetry" severity="error" />

<!-- Code -->
<issue id="SetTextI18n" severity="error" />
<issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" />

</lint>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.riotredesign">

<application>

<receiver android:name=".receiver.OnApplicationUpgradeReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>

</application>

</manifest>

View File

@ -0,0 +1,56 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm;

import android.app.Activity;
import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class FcmHelper {

/**
* Retrieves the FCM registration token.
*
* @return the FCM token or null if not received from FCM
*/
@Nullable
public static String getFcmToken(Context context) {
return null;
}

/**
* Store FCM token to the SharedPrefs
*
* @param context android context
* @param token the token to store
*/
public static void storeFcmToken(@NonNull Context context,
@Nullable String token) {
// No op
}

/**
* onNewToken may not be called on application upgrade, so ensure my shared pref is set
*
* @param activity the first launch Activity
*/
public static void ensureFcmTokenIsRetrieved(final Activity activity) {
// No op
}
}

View File

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

import androidx.fragment.app.Fragment
import im.vector.fragments.troubleshoot.TestAccountSettings
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings
import im.vector.riotredesign.features.settings.troubleshoot.TestDeviceSettings
import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings
import im.vector.riotredesign.push.fcm.troubleshoot.TestAutoStartBoot
import im.vector.riotredesign.push.fcm.troubleshoot.TestBackgroundRestrictions

class NotificationTroubleshootTestManagerFactory {

companion object {
fun createTestManager(fragment: Fragment, session: Session?): NotificationTroubleshootTestManager {
val mgr = NotificationTroubleshootTestManager(fragment)
mgr.addTest(TestSystemSettings(fragment))
if (session != null) {
mgr.addTest(TestAccountSettings(fragment, session))
}
mgr.addTest(TestDeviceSettings(fragment))
if (session != null) {
mgr.addTest(TestBingRulesSettings(fragment, session))
}
// mgr.addTest(TestNotificationServiceRunning(fragment))
// mgr.addTest(TestServiceRestart(fragment))
mgr.addTest(TestAutoStartBoot(fragment))
mgr.addTest(TestBackgroundRestrictions(fragment))
// mgr.addTest(TestBatteryOptimization(fragment))
return mgr
}
}

}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm.troubleshoot

import androidx.fragment.app.Fragment
import im.vector.riotredesign.R
import im.vector.riotredesign.features.settings.PreferencesManager
import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest

/**
* Test that the application is started on boot
*/
class TestAutoStartBoot(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_service_boot_title) {

override fun perform() {
if (PreferencesManager.autoStartOnBoot(fragment.context)) {
description = fragment.getString(R.string.settings_troubleshoot_test_service_boot_success)
status = TestStatus.SUCCESS
quickFix = null
} else {
description = fragment.getString(R.string.settings_troubleshoot_test_service_boot_failed)
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_service_boot_quickfix) {
override fun doFix() {
PreferencesManager.setAutoStartOnBoot(fragment.context, true)
manager?.retry()
}
}
status = TestStatus.FAILED
}
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm.troubleshoot

import android.content.Context
import android.net.ConnectivityManager
import androidx.core.net.ConnectivityManagerCompat
import androidx.fragment.app.Fragment
import im.vector.riotredesign.R
import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest

class TestBackgroundRestrictions(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_bg_restricted_title) {

override fun perform() {
(fragment.context!!.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).apply {
// Checks if the device is on a metered network
if (isActiveNetworkMetered) {
// Checks users Data Saver settings.
val restrictBackgroundStatus = ConnectivityManagerCompat.getRestrictBackgroundStatus(this)
when (restrictBackgroundStatus) {
ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED -> {
// Background data usage is blocked for this app. Wherever possible,
// the app should also use less data in the foreground.
description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_failed,
"RESTRICT_BACKGROUND_STATUS_ENABLED")
status = TestStatus.FAILED
quickFix = null
}
ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED -> {
// The app is whitelisted. Wherever possible,
// the app should use less data in the foreground and background.
description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_success,
"RESTRICT_BACKGROUND_STATUS_WHITELISTED")
status = TestStatus.SUCCESS
quickFix = null
}
ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED -> {
// Data Saver is disabled. Since the device is connected to a
// metered network, the app should use less data wherever possible.
description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_success,
"RESTRICT_BACKGROUND_STATUS_DISABLED")
status = TestStatus.SUCCESS
quickFix = null
}

}

} else {
// The device is not on a metered network.
// Use data as required to perform syncs, downloads, and updates.
description = fragment.getString(R.string.settings_troubleshoot_test_bg_restricted_success, "")
status = TestStatus.SUCCESS
quickFix = null
}
}
}

}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm.troubleshoot

import androidx.fragment.app.Fragment
import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.isIgnoringBatteryOptimizations
import im.vector.riotredesign.core.utils.requestDisablingBatteryOptimization
import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest

// Not used anymore
class TestBatteryOptimization(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_battery_title) {

override fun perform() {

if (fragment.context != null && isIgnoringBatteryOptimizations(fragment.context!!)) {
description = fragment.getString(R.string.settings_troubleshoot_test_battery_success)
status = TestStatus.SUCCESS
quickFix = null
} else {
description = fragment.getString(R.string.settings_troubleshoot_test_battery_failed)
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_battery_quickfix) {
override fun doFix() {
fragment.activity?.let {
requestDisablingBatteryOptimization(it, fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX)
}
}
}
status = TestStatus.FAILED
}
}

}

View File

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

package im.vector.riotredesign.receiver;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

import timber.log.Timber;

public class OnApplicationUpgradeReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
Timber.d("## onReceive() : Application has been upgraded, restart event stream service.");

// Start Event stream
// TODO EventStreamServiceX.Companion.onApplicationUpgrade(context);
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.riotredesign">

<application>

<!-- Firebase components -->
<meta-data
android:name="firebase_analytics_collection_deactivated"
android:value="true" />

<service android:name=".push.fcm.VectorFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

</application>

</manifest>

View File

@ -0,0 +1,46 @@
{
"project_info": {
"project_number": "912726360885",
"firebase_url": "https://vector-alpha.firebaseio.com",
"project_id": "vector-alpha",
"storage_bucket": "vector-alpha.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:912726360885:android:448c9b63161abc9c",
"android_client_info": {
"package_name": "im.vector.riotredesign"
}
},
"oauth_client": [
{
"client_id": "912726360885-rsae0i66rgqt6ivnudu1pv4tksg9i8b2.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}

View File

@ -0,0 +1,120 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm;

import android.app.Activity;
import android.content.Context;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import im.vector.riotredesign.R;
import timber.log.Timber;

/**
* This class store the FCM token in SharedPrefs and ensure this token is retrieved.
* It has an alter ego in the fdroid variant.
*/
public class FcmHelper {
private static final String LOG_TAG = FcmHelper.class.getSimpleName();

private static final String PREFS_KEY_FCM_TOKEN = "FCM_TOKEN";

/**
* Retrieves the FCM registration token.
*
* @return the FCM token or null if not received from FCM
*/
@Nullable
public static String getFcmToken(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getString(PREFS_KEY_FCM_TOKEN, null);
}

/**
* Store FCM token to the SharedPrefs
*
* @param context android context
* @param token the token to store
*/
public static void storeFcmToken(@NonNull Context context,
@Nullable String token) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putString(PREFS_KEY_FCM_TOKEN, token)
.apply();
}

/**
* onNewToken may not be called on application upgrade, so ensure my shared pref is set
*
* @param activity the first launch Activity
*/
public static void ensureFcmTokenIsRetrieved(final Activity activity) {
if (TextUtils.isEmpty(getFcmToken(activity))) {


//vfe: according to firebase doc
//'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) {
try {
FirebaseInstanceId.getInstance().getInstanceId()
.addOnSuccessListener(activity, new OnSuccessListener<InstanceIdResult>() {
@Override
public void onSuccess(InstanceIdResult instanceIdResult) {
storeFcmToken(activity, instanceIdResult.getToken());
}
})
.addOnFailureListener(activity, new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
}
});
} catch (Throwable e) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed " + e.getMessage());
}
} else {
Toast.makeText(activity, R.string.no_valid_google_play_services_apk, Toast.LENGTH_SHORT).show();
Timber.e("No valid Google Play Services found. Cannot use FCM.");
}
}
}

/**
* Check the device to make sure it has the Google Play Services APK. If
* it doesn't, display a dialog that allows users to download the APK from
* the Google Play Store or enable it in the device's system settings.
*/
private static boolean checkPlayServices(Activity activity) {
GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
int resultCode = apiAvailability.isGooglePlayServicesAvailable(activity);
if (resultCode != ConnectionResult.SUCCESS) {
return false;
}
return true;
}
}

View File

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

import androidx.fragment.app.Fragment
import im.vector.fragments.troubleshoot.TestAccountSettings
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.push.fcm.troubleshoot.TestFirebaseToken
import im.vector.riotredesign.push.fcm.troubleshoot.TestPlayServices
import im.vector.riotredesign.push.fcm.troubleshoot.TestTokenRegistration
import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.riotredesign.features.settings.troubleshoot.TestBingRulesSettings
import im.vector.riotredesign.features.settings.troubleshoot.TestDeviceSettings
import im.vector.riotredesign.features.settings.troubleshoot.TestSystemSettings

class NotificationTroubleshootTestManagerFactory {

companion object {
fun createTestManager(fragment: Fragment, session: Session?): NotificationTroubleshootTestManager {
val mgr = NotificationTroubleshootTestManager(fragment)
mgr.addTest(TestSystemSettings(fragment))
if (session != null) {
mgr.addTest(TestAccountSettings(fragment, session))
}
mgr.addTest(TestDeviceSettings(fragment))
if (session != null) {
mgr.addTest(TestBingRulesSettings(fragment, session))
}
mgr.addTest(TestPlayServices(fragment))
mgr.addTest(TestFirebaseToken(fragment))
mgr.addTest(TestTokenRegistration(fragment))
return mgr
}
}

}

View File

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

package im.vector.riotredesign.push.fcm

import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R
import im.vector.riotredesign.core.preference.BingRule
import im.vector.riotredesign.features.badge.BadgeProxy
import im.vector.riotredesign.features.notifications.NotifiableEventResolver
import im.vector.riotredesign.features.notifications.NotifiableMessageEvent
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import im.vector.riotredesign.features.notifications.SimpleNotifiableEvent
import org.koin.android.ext.android.inject
import timber.log.Timber

/**
* Class extending FirebaseMessagingService.
*/
class VectorFirebaseMessagingService : FirebaseMessagingService() {

val notificationDrawerManager by inject<NotificationDrawerManager>()

private val notifiableEventResolver by lazy {
NotifiableEventResolver(this)
}

// UI handler
private val mUIHandler by lazy {
Handler(Looper.getMainLooper())
}

/**
* Called when message is received.
*
* @param message the message
*/
override fun onMessageReceived(message: RemoteMessage?) {
if (message == null || message.data == null) {
Timber.e("## onMessageReceived() : received a null message or message with no data")
return
}
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceived()" + message.data.toString())
Timber.i("## onMessageReceived() from FCM with priority " + message.priority)
}

//safe guard
/* TODO
val pushManager = Matrix.getInstance(applicationContext).pushManager
if (!pushManager.areDeviceNotificationsAllowed()) {
Timber.i("## onMessageReceived() : the notifications are disabled")
return
}
*/

//TODO if the app is in foreground, we could just ignore this. The sync loop is already going?
// TODO mUIHandler.post { onMessageReceivedInternal(message.data, pushManager) }
}

/**
* Called if InstanceID token is updated. This may occur if the security of
* the previous token had been compromised. Note that this is also called
* when the InstanceID token is initially generated, so this is where
* you retrieve the token.
*/
override fun onNewToken(refreshedToken: String?) {
Timber.i("onNewToken: FCM Token has been updated")
FcmHelper.storeFcmToken(this, refreshedToken)
// TODO Matrix.getInstance(this)?.pushManager?.resetFCMRegistration(refreshedToken)
}

override fun onDeletedMessages() {
Timber.d("## onDeletedMessages()")
}

/**
* Internal receive method
*
* @param data Data map containing message data as key/value pairs.
* For Set of keys use data.keySet().
*/
private fun onMessageReceivedInternal(data: Map<String, String> /*, pushManager: PushManager*/) {
try {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("## onMessageReceivedInternal() : $data")
}
// update the badge counter
val unreadCount = data.get("unread")?.let { Integer.parseInt(it) } ?: 0
BadgeProxy.updateBadgeCount(applicationContext, unreadCount)

/* TODO
val session = Matrix.getInstance(applicationContext)?.defaultSession

if (VectorApp.isAppInBackground() && !pushManager.isBackgroundSyncAllowed) {
//Notification contains metadata and maybe data information
handleNotificationWithoutSyncingMode(data, session)
} else {
// Safe guard... (race?)
if (isEventAlreadyKnown(data["event_id"], data["room_id"])) return
//Catch up!!
EventStreamServiceX.onPushReceived(this)
}
*/
} catch (e: Exception) {
Timber.e(e, "## onMessageReceivedInternal() failed : " + e.message)
}
}

// check if the event was not yet received
// a previous catchup might have already retrieved the notified event
private fun isEventAlreadyKnown(eventId: String?, roomId: String?): Boolean {
if (null != eventId && null != roomId) {
try {
/* TODO
val sessions = Matrix.getInstance(applicationContext).sessions

if (null != sessions && !sessions.isEmpty()) {
for (session in sessions) {
if (session.dataHandler?.store?.isReady == true) {
session.dataHandler.store?.getEvent(eventId, roomId)?.let {
Timber.e("## isEventAlreadyKnown() : ignore the event " + eventId
+ " in room " + roomId + " because it is already known")
return true
}
}
}
}
*/
} catch (e: Exception) {
Timber.e(e, "## isEventAlreadyKnown() : failed to check if the event was already defined " + e.message)
}

}
return false
}

private fun handleNotificationWithoutSyncingMode(data: Map<String, String>, session: Session?) {

if (session == null) {
Timber.e("## handleNotificationWithoutSyncingMode cannot find session")
return
}

// The Matrix event ID of the event being notified about.
// This is required if the notification is about a particular Matrix event.
// It may be omitted for notifications that only contain updated badge counts.
// This ID can and should be used to detect duplicate notification requests.
val eventId = data["event_id"] ?: return //Just ignore


val eventType = data["type"]
if (eventType == null) {
//Just add a generic unknown event
val simpleNotifiableEvent = SimpleNotifiableEvent(
session.sessionParams.credentials.userId,
eventId,
true, //It's an issue in this case, all event will bing even if expected to be silent.
title = getString(R.string.notification_unknown_new_event),
description = "",
type = null,
timestamp = System.currentTimeMillis(),
soundName = BingRule.ACTION_VALUE_DEFAULT,
isPushGatewayEvent = true
)
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)
notificationDrawerManager.refreshNotificationDrawer(null)

return
} else {

val event = parseEvent(data)
if (event?.roomId == null) {
//unsupported event
Timber.e("Received an event with no room id")
return
} else {

var notifiableEvent = notifiableEventResolver.resolveEvent(event, null, null /* TODO session.fulfillRule(event) */, session)

if (notifiableEvent == null) {
Timber.e("Unsupported notifiable event ${eventId}")
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.e("--> ${event}")
}
} else {


if (notifiableEvent is NotifiableMessageEvent) {
if (TextUtils.isEmpty(notifiableEvent.senderName)) {
notifiableEvent.senderName = data["sender_display_name"] ?: data["sender"] ?: ""
}
if (TextUtils.isEmpty(notifiableEvent.roomName)) {
notifiableEvent.roomName = findRoomNameBestEffort(data, session) ?: ""
}
}

notifiableEvent.isPushGatewayEvent = true
notifiableEvent.matrixID = session.sessionParams.credentials.userId
notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
notificationDrawerManager.refreshNotificationDrawer(null)
}
}
}
}

private fun findRoomNameBestEffort(data: Map<String, String>, session: Session?): String? {
var roomName: String? = data["room_name"]
val roomId = data["room_id"]
if (null == roomName && null != roomId) {
// Try to get the room name from our store
/*
TODO
if (session?.dataHandler?.store?.isReady == true) {
val room = session.getRoom(roomId)
roomName = room?.getRoomDisplayName(this)
}
*/
}
return roomName
}

/**
* Try to create an event from the FCM data
*
* @param data the FCM data
* @return the event
*/
private fun parseEvent(data: Map<String, String>?): Event? {
// accept only event with room id.
if (null == data || !data.containsKey("room_id") || !data.containsKey("event_id")) {
return null
}

try {
return Event(eventId = data["event_id"],
sender = data["sender"],
roomId = data["room_id"],
type = data.getValue("type"),
// TODO content = data.getValue("content"),
originServerTs = System.currentTimeMillis())
} catch (e: Exception) {
Timber.e(e, "buildEvent fails " + e.localizedMessage)
}

return null
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm.troubleshoot

import androidx.fragment.app.Fragment
import com.google.firebase.iid.FirebaseInstanceId
import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.startAddGoogleAccountIntent
import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
import timber.log.Timber

/*
* Test that app can successfully retrieve a token via firebase
*/
class TestFirebaseToken(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_fcm_title) {

override fun perform() {
status = TestStatus.RUNNING
val activity = fragment.activity
if (activity != null) {
try {
FirebaseInstanceId.getInstance().instanceId
.addOnCompleteListener(activity) { task ->
if (!task.isSuccessful) {
val errorMsg = if (task.exception == null) "Unknown" else task.exception!!.localizedMessage
//Can't find where this constant is (not documented -or deprecated in docs- and all obfuscated)
if ("SERVICE_NOT_AVAILABLE".equals(errorMsg)) {
description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed_service_not_available, errorMsg)
} else if ("TOO_MANY_REGISTRATIONS".equals(errorMsg)) {
description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed_too_many_registration, errorMsg)
} else if ("ACCOUNT_MISSING".equals(errorMsg)) {
description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed_account_missing, errorMsg)
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_fcm_failed_account_missing_quick_fix) {
override fun doFix() {
startAddGoogleAccountIntent(fragment, NotificationTroubleshootTestManager.REQ_CODE_FIX)
}
}
} else {
description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed, errorMsg)
}
status = TestStatus.FAILED
} else {
task.result?.token?.let {
val tok = it.substring(0, Math.min(8, it.length)) + "********************"
description = fragment.getString(R.string.settings_troubleshoot_test_fcm_success, tok)
Timber.e("Retrieved FCM token success [$it].")
}
status = TestStatus.SUCCESS
}
}
} catch (e: Throwable) {
description = fragment.getString(R.string.settings_troubleshoot_test_fcm_failed, e.localizedMessage)
status = TestStatus.FAILED
}
} else {
status = TestStatus.FAILED
}
}

}

View File

@ -0,0 +1,55 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm.troubleshoot

import androidx.fragment.app.Fragment
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import im.vector.riotredesign.R
import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
import timber.log.Timber

/*
* Check that the play services APK is available an up-to-date. If needed provide quick fix to install it.
*/
class TestPlayServices(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_play_services_title) {

override fun perform() {
val apiAvailability = GoogleApiAvailability.getInstance()
val resultCode = apiAvailability.isGooglePlayServicesAvailable(fragment.context)
if (resultCode == ConnectionResult.SUCCESS) {
quickFix = null
description = fragment.getString(R.string.settings_troubleshoot_test_play_services_success)
status = TestStatus.SUCCESS
} else {
if (apiAvailability.isUserResolvableError(resultCode)) {
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_play_services_quickfix) {
override fun doFix() {
fragment.activity?.let {
apiAvailability.getErrorDialog(it, resultCode, 9000 /*hey does the magic number*/).show()
}
}
}
Timber.e("Play Services apk error $resultCode -> ${apiAvailability.getErrorString(resultCode)}.")
}

description = fragment.getString(R.string.settings_troubleshoot_test_play_services_failed, apiAvailability.getErrorString(resultCode))
status = TestStatus.FAILED
}
}

}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.push.fcm.troubleshoot

import androidx.fragment.app.Fragment
import im.vector.riotredesign.R
import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest

/**
* Force registration of the token to HomeServer
*/
class TestTokenRegistration(val fragment: Fragment) : TroubleshootTest(R.string.settings_troubleshoot_test_token_registration_title) {

override fun perform() {
/*
TODO
Matrix.getInstance(VectorApp.getInstance().baseContext).pushManager.forceSessionsRegistration(object : ApiCallback<Void> {
override fun onSuccess(info: Void?) {
description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_success)
status = TestStatus.SUCCESS
}

override fun onNetworkError(e: Exception?) {
description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_failed, e?.localizedMessage)
status = TestStatus.FAILED
}

override fun onMatrixError(e: MatrixError?) {
description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_failed, e?.localizedMessage)
status = TestStatus.FAILED
}

override fun onUnexpectedError(e: Exception?) {
description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_failed, e?.localizedMessage)
status = TestStatus.FAILED
}
})
*/

status = TestStatus.FAILED

}

}

View File

@ -18,7 +18,7 @@

<activity
android:name=".features.MainActivity"
android:theme="@style/Theme.Riot.Splash">
android:theme="@style/AppTheme.Launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

@ -32,6 +32,15 @@
<activity
android:name=".features.rageshake.BugReportActivity"
android:label="@string/title_activity_bug_report" />
<activity
android:name=".features.settings.VectorSettingsActivity"
android:label="@string/title_activity_settings"
android:windowSoftInputMode="adjustResize" />

<service
android:name=".core.services.CallService"
android:exported="false" />

</application>

</manifest>

View File

@ -26,6 +26,7 @@ import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.list.RoomSelectionRepository
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import im.vector.riotredesign.features.notifications.NotificationDrawerManager
import org.koin.dsl.module.module

class AppModule(private val context: Context) {
@ -64,6 +65,10 @@ class AppModule(private val context: Context) {
RoomSummaryComparator()
}

single {
NotificationDrawerManager(context)
}

factory {
Matrix.getInstance().currentSession!!
}

View File

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

package im.vector.riotredesign.core.extensions

import java.net.URLEncoder

/**
* Append param and value to a Url, using "?" or "&". Value parameter will be encoded
* Return this for chaining purpose
*/
fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder {
if (contains("?")) {
append("&")
} else {
append("?")
}

append(param)
append("=")
append(URLEncoder.encode(value, "utf-8"))

return this
}

View File

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

package im.vector.riotredesign.core.extensions

import android.text.InputType
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.widget.SearchView
import im.vector.riotredesign.R

/**
* Remove left margin of a SearchView
*/
fun SearchView.withoutLeftMargin() {
(findViewById<View>(R.id.search_edit_frame))?.let {
val searchEditFrameParams = it.layoutParams as ViewGroup.MarginLayoutParams
searchEditFrameParams.leftMargin = 0
it.layoutParams = searchEditFrameParams
}

(findViewById<View>(R.id.search_mag_icon))?.let {
val searchIconParams = it.layoutParams as ViewGroup.MarginLayoutParams
searchIconParams.leftMargin = 0
it.layoutParams = searchIconParams
}
}

fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) {
if (visible) {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
} else {
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
}
if (updateCursor) setSelection(text?.length ?: 0)
}

View File

@ -23,11 +23,13 @@ import android.view.MenuItem
import android.view.View
import androidx.annotation.*
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import butterknife.BindView
import butterknife.ButterKnife
import butterknife.Unbinder
import com.airbnb.mvrx.BaseMvRxActivity
import com.bumptech.glide.util.Util
import com.google.android.material.snackbar.Snackbar
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R
import im.vector.riotredesign.features.rageshake.BugReportActivity
@ -47,8 +49,14 @@ abstract class RiotActivity : BaseMvRxActivity() {
* ========================================================================================== */

@Nullable
@JvmField
@BindView(R.id.toolbar)
protected lateinit var toolbar: Toolbar
var toolbar: Toolbar? = null

@Nullable
@JvmField
@BindView(R.id.vector_coordinator_layout)
var coordinatorLayout: CoordinatorLayout? = null

/* ==========================================================================================
* DATA
@ -269,4 +277,23 @@ abstract class RiotActivity : BaseMvRxActivity() {
* Return a object containing other themes for this activity
*/
open fun getOtherThemes(): ActivityOtherThemes = ActivityOtherThemes.Default

/* ==========================================================================================
* PUBLIC METHODS
* ========================================================================================== */

protected fun showSnackbar(message: String) {
coordinatorLayout?.let {
Snackbar.make(it, message, Snackbar.LENGTH_SHORT)
}
}

/* ==========================================================================================
* Temporary method
* ========================================================================================== */

protected fun notImplemented() {
showSnackbar(getString(R.string.not_implemented))
}

}

View File

@ -18,17 +18,70 @@ package im.vector.riotredesign.core.platform

import android.os.Bundle
import android.os.Parcelable
import android.view.*
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.annotation.MainThread
import butterknife.ButterKnife
import butterknife.Unbinder
import com.airbnb.mvrx.BaseMvRxFragment
import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util.assertMainThread
import timber.log.Timber

abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {

// Butterknife unbinder
private var mUnBinder: Unbinder? = null

val riotActivity: RiotActivity by lazy {
activity as RiotActivity
}

/* ==========================================================================================
* Life cycle
* ========================================================================================== */

@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

if (getMenuRes() != -1) {
setHasOptionsMenu(true)
}
}

final override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(getLayoutResId(), container, false)
}

@LayoutRes
abstract fun getLayoutResId(): Int

@CallSuper
override fun onResume() {
super.onResume()

Timber.d("onResume Fragment ${this.javaClass.simpleName}")
}

@CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mUnBinder = ButterKnife.bind(this, view)
}

@CallSuper
override fun onDestroyView() {
super.onDestroyView()
mUnBinder?.unbind()
mUnBinder = null
}

/* ==========================================================================================
* Restorable
* ========================================================================================== */

private val restorables = ArrayList<Restorable>()

override fun onSaveInstanceState(outState: Bundle) {
@ -60,4 +113,19 @@ abstract class RiotFragment : BaseMvRxFragment(), OnBackPressed {
return this
}


/* ==========================================================================================
* MENU MANAGEMENT
* ========================================================================================== */

open fun getMenuRes() = -1

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
val menuRes = getMenuRes()

if (menuRes != -1) {
inflater.inflate(menuRes, menu)
}
}

}

View File

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

package im.vector.riotredesign.core.platform

import android.text.Editable
import android.text.TextWatcher

/**
* TextWatcher with default no op implementation
*/
open class SimpleTextWatcher : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
// No op
}

override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
// No op
}

override fun afterTextChanged(s: Editable) {
// No op
}
}

View File

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

package im.vector.riotredesign.core.platform

import androidx.annotation.CallSuper
import androidx.preference.PreferenceFragmentCompat
import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.toast
import timber.log.Timber

abstract class VectorPreferenceFragment : PreferenceFragmentCompat() {

val vectorActivity: RiotActivity by lazy {
activity as RiotActivity
}

/* ==========================================================================================
* Life cycle
* ========================================================================================== */

@CallSuper
override fun onResume() {
super.onResume()

Timber.d("onResume Fragment ${this.javaClass.simpleName}")
}

/* ==========================================================================================
* Protected
* ========================================================================================== */

protected fun notImplemented() {
// Snackbar cannot be display on PreferenceFragment
// Snackbar.make(view!!, R.string.not_implemented, Snackbar.LENGTH_SHORT)
activity?.toast(R.string.not_implemented)
}

}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import androidx.preference.PreferenceViewHolder
import im.vector.riotredesign.R

/**
* Preference used in Room setting for Room aliases
*/
class AddressPreference : VectorPreference {

// members
private var mMainAddressIconView: ImageView? = null
private var mIsMainIconVisible = false

/**
* @return the main icon view.
*/
val mainIconView: View?
get() = mMainAddressIconView

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

init {
widgetLayoutResource = R.layout.vector_settings_address_preference
}

override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)

val view = holder.itemView
mMainAddressIconView = view.findViewById(R.id.main_address_icon_view)
mMainAddressIconView!!.visibility = if (mIsMainIconVisible) View.VISIBLE else View.GONE
}

/**
* Set the main address icon visibility.
*
* @param isVisible true to display the main icon
*/
fun setMainIconVisible(isVisible: Boolean) {
mIsMainIconVisible = isVisible

mMainAddressIconView?.visibility = if (mIsMainIconVisible) View.VISIBLE else View.GONE
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.text.TextUtils
import android.util.AttributeSet
import android.view.View
import android.widget.RadioGroup
import android.widget.TextView
import androidx.preference.PreferenceViewHolder
import im.vector.riotredesign.R

// TODO Replace by real Bingrule class
class BingRule(rule: BingRule) {
fun shouldNotNotify() = false
fun shouldNotify() = false
fun setNotify(b: Boolean) {

}

fun setHighlight(b: Boolean) {

}

fun removeNotificationSound() {

}

val ruleId: CharSequence? = null
var isEnabled = false
var notificationSound: String? = null
val kind: CharSequence? = null

companion object {
const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = "TODO"
const val ACTION_VALUE_DEFAULT = "TODO"
const val KIND_UNDERRIDE = "TODO"
const val RULE_ID_INVITE_ME = "TODO"
const val RULE_ID_CALL = "TODO"
const val ACTION_VALUE_RING = "TODO"
const val RULE_ID_DISABLE_ALL = "TODO"
const val ACTION_DONT_NOTIFY = "TODO"
const val RULE_ID_CONTAIN_DISPLAY_NAME = "TODO"
const val RULE_ID_CONTAIN_USER_NAME = "TODO"
const val RULE_ID_ONE_TO_ONE_ROOM = "TODO"
const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = "TODO"
}

}

class BingRulePreference : VectorPreference {

/**
* @return the selected bing rule
*/
var rule: BingRule? = null
private set

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

init {
layoutResource = R.layout.vector_preference_bing_rule
}

/**
* @return the bing rule status index
*/
val ruleStatusIndex: Int
get() {
if (null != rule) {
if (TextUtils.equals(rule!!.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
if (rule!!.shouldNotNotify()) {
return if (rule!!.isEnabled) {
NOTIFICATION_OFF_INDEX
} else {
NOTIFICATION_SILENT_INDEX
}
} else if (rule!!.shouldNotify()) {
return NOTIFICATION_NOISY_INDEX
}
}

if (rule!!.isEnabled) {
return if (rule!!.shouldNotNotify()) {
NOTIFICATION_OFF_INDEX
} else if (null != rule!!.notificationSound) {
NOTIFICATION_NOISY_INDEX
} else {
NOTIFICATION_SILENT_INDEX
}
}
}

return NOTIFICATION_OFF_INDEX
}

/**
* Update the bing rule.
*
* @param aBingRule
*/
fun setBingRule(aBingRule: BingRule) {
rule = aBingRule
refreshSummary()
}

/**
* Refresh the summary
*/
private fun refreshSummary() {
summary = context.getString(when (ruleStatusIndex) {
NOTIFICATION_OFF_INDEX -> R.string.notification_off
NOTIFICATION_SILENT_INDEX -> R.string.notification_silent
else -> R.string.notification_noisy
})
}

/**
* Create a bing rule with the updated required at index.
*
* @param index index
* @return a bing rule with the updated flags / null if there is no update
*/
fun createRule(index: Int): BingRule? {
var rule: BingRule? = null

if (null != this.rule && index != ruleStatusIndex) {
rule = BingRule(this.rule!!)

if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
when (index) {
NOTIFICATION_OFF_INDEX -> {
rule.isEnabled = true
rule.setNotify(false)
}
NOTIFICATION_SILENT_INDEX -> {
rule.isEnabled = false
rule.setNotify(false)
}
NOTIFICATION_NOISY_INDEX -> {
rule.isEnabled = true
rule.setNotify(true)
rule.notificationSound = BingRule.ACTION_VALUE_DEFAULT
}
}

return rule
}


if (NOTIFICATION_OFF_INDEX == index) {
if (TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE)
|| TextUtils.equals(rule.ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
rule.setNotify(false)
} else {
rule.isEnabled = false
}
} else {
rule.isEnabled = true
rule.setNotify(true)
rule.setHighlight(!TextUtils.equals(this.rule!!.kind, BingRule.KIND_UNDERRIDE)
&& !TextUtils.equals(rule.ruleId, BingRule.RULE_ID_INVITE_ME)
&& NOTIFICATION_NOISY_INDEX == index)
if (NOTIFICATION_NOISY_INDEX == index) {
rule.notificationSound = if (TextUtils.equals(rule.ruleId, BingRule.RULE_ID_CALL))
BingRule.ACTION_VALUE_RING
else
BingRule.ACTION_VALUE_DEFAULT
} else {
rule.removeNotificationSound()
}
}
}

return rule
}

override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)

holder.itemView.findViewById<TextView>(android.R.id.summary)?.visibility = View.GONE
holder.itemView.setOnClickListener(null)
holder.itemView.setOnLongClickListener(null)

val radioGroup = holder.findViewById(R.id.bingPreferenceRadioGroup) as? RadioGroup
radioGroup?.setOnCheckedChangeListener(null)

when (ruleStatusIndex) {
NOTIFICATION_OFF_INDEX -> {
radioGroup?.check(R.id.bingPreferenceRadioBingRuleOff)
}
NOTIFICATION_SILENT_INDEX -> {
radioGroup?.check(R.id.bingPreferenceRadioBingRuleSilent)
}
else -> {
radioGroup?.check(R.id.bingPreferenceRadioBingRuleNoisy)
}
}

radioGroup?.setOnCheckedChangeListener { group, checkedId ->
when (checkedId) {
R.id.bingPreferenceRadioBingRuleOff -> {
onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_OFF_INDEX)
}
R.id.bingPreferenceRadioBingRuleSilent -> {
onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_SILENT_INDEX)
}
R.id.bingPreferenceRadioBingRuleNoisy -> {
onPreferenceChangeListener?.onPreferenceChange(this, NOTIFICATION_NOISY_INDEX)
}
}
}

}


companion object {

// index in mRuleStatuses
private const val NOTIFICATION_OFF_INDEX = 0
private const val NOTIFICATION_SILENT_INDEX = 1
private const val NOTIFICATION_NOISY_INDEX = 2
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import im.vector.riotredesign.R

class ProgressBarPreference : Preference {

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

init {
layoutResource = R.layout.vector_settings_spinner_preference
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.util.AttributeSet
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.Room

/**
* Specialized class to target a Room avatar preference.
* Based don the avatar preference class it redefines refreshAvatar() and
* add the new method setConfiguration().
*/
class RoomAvatarPreference : UserAvatarPreference {

private var mRoom: Room? = null

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

override fun refreshAvatar() {
if (null != mAvatarView && null != mRoom) {
// TODO
// VectorUtils.loadRoomAvatar(context, mSession, mAvatarView, mRoom)
}
}

fun setConfiguration(aSession: Session, aRoom: Room) {
mSession = aSession
mRoom = aRoom
refreshAvatar()
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.ProgressBar
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R

open class UserAvatarPreference : Preference {

internal var mAvatarView: ImageView? = null
internal var mSession: Session? = null
private var mLoadingProgressBar: ProgressBar? = null

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

init {
widgetLayoutResource = R.layout.vector_settings_round_avatar
isIconSpaceReserved = false
}

override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)

mAvatarView = holder.itemView.findViewById(R.id.settings_avatar)
mLoadingProgressBar = holder.itemView.findViewById(R.id.avatar_update_progress_bar)
refreshAvatar()
}

open fun refreshAvatar() {
if (null != mAvatarView && null != mSession) {
// TODO
// val myUser = mSession!!.myUser
// VectorUtils.loadUserAvatar(context, mSession, mAvatarView, myUser.avatarUrl, myUser.user_id, myUser.displayname)
}
}

fun setSession(session: Session) {
mSession = session
refreshAvatar()
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceViewHolder
import im.vector.riotredesign.R
import timber.log.Timber

/**
* Use this class to create an EditTextPreference form code and avoid a crash (see https://code.google.com/p/android/issues/detail?id=231576)
*/
class VectorEditTextPreference : EditTextPreference {

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

init {
dialogLayoutResource = R.layout.dialog_preference_edit_text
isIconSpaceReserved = false
}

// No single line for title
override fun onBindViewHolder(holder: PreferenceViewHolder) {
// display the title in multi-line to avoid ellipsis.
try {
holder.itemView.findViewById<TextView>(android.R.id.title)?.setSingleLine(false)
} catch (e: Exception) {
Timber.e(e, "onBindView " + e.message)
}

super.onBindViewHolder(holder)
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreference
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.group.Group
import im.vector.riotredesign.R

class VectorGroupPreference : SwitchPreference {

private var mAvatarView: ImageView? = null

private var mGroup: Group? = null
private var mSession: Session? = null

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)

override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)

val createdView = holder.itemView

if (mAvatarView == null) {
try {
// insert the group avatar to the left
val iconView = createdView.findViewById<ImageView>(android.R.id.icon)

var iconViewParent = iconView.parent

while (null != iconViewParent.parent) {
iconViewParent = iconViewParent.parent
}

val inflater = LayoutInflater.from(context)
val layout = inflater.inflate(R.layout.vector_settings_round_group_avatar, (iconViewParent as LinearLayout), false) as FrameLayout
mAvatarView = layout.findViewById(R.id.settings_round_group_avatar)

val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.gravity = Gravity.CENTER
layout.layoutParams = params
iconViewParent.addView(layout, 0)

} catch (e: Exception) {
mAvatarView = null
}

}

refreshAvatar()
}

/**
* Init the group information
*
* @param group the group
* @param session the session
*/
fun setGroup(group: Group, session: Session) {
mGroup = group
mSession = session

refreshAvatar()
}

/**
* Refresh the avatar
*/
private fun refreshAvatar() {
if (null != mAvatarView && null != mSession && null != mGroup) {
// TODO
// VectorUtils.loadGroupAvatar(context, mSession, mAvatarView, mGroup)
}
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import im.vector.riotredesign.R

/**
* Customize ListPreference class to add a warning icon to the right side of the list.
*/
class VectorListPreference : ListPreference {

//
private var mWarningIconView: View? = null
private var mIsWarningIconVisible = false
private var mWarningIconClickListener: OnPreferenceWarningIconClickListener? = null

/**
* Interface definition for a callback to be invoked when the warning icon is clicked.
*/
interface OnPreferenceWarningIconClickListener {
/**
* Called when a warning icon has been clicked.
*
* @param preference The Preference that was clicked.
*/
fun onWarningIconClick(preference: Preference)
}

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

init {
widgetLayoutResource = R.layout.vector_settings_list_preference_with_warning
isIconSpaceReserved = false
}

override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)

val view = holder.itemView

mWarningIconView = view.findViewById(R.id.list_preference_warning_icon)
mWarningIconView!!.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE

mWarningIconView!!.setOnClickListener {
if (null != mWarningIconClickListener) {
mWarningIconClickListener!!.onWarningIconClick(this@VectorListPreference)
}
}
}

/**
* Sets the callback to be invoked when this warning icon is clicked.
*
* @param onPreferenceWarningIconClickListener The callback to be invoked.
*/
fun setOnPreferenceWarningIconClickListener(onPreferenceWarningIconClickListener: OnPreferenceWarningIconClickListener) {
mWarningIconClickListener = onPreferenceWarningIconClickListener
}

/**
* Set the warning icon visibility.
*
* @param isVisible to display the icon
*/
fun setWarningIconVisible(isVisible: Boolean) {
mIsWarningIconVisible = isVisible

if (null != mWarningIconView) {
mWarningIconView!!.visibility = if (mIsWarningIconVisible) View.VISIBLE else View.GONE
}
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.animation.Animator
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.View
import android.widget.TextView
import androidx.core.animation.doOnEnd
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import im.vector.riotredesign.R
import im.vector.riotredesign.features.themes.ThemeUtils
import timber.log.Timber


/**
* create a Preference with a dedicated click/long click methods.
* It also allow the title to be displayed on several lines
*/
open class VectorPreference : Preference {

var mTypeface = Typeface.NORMAL

// long press listener
/**
* Returns the callback to be invoked when this Preference is long clicked.
*
* @return The callback to be invoked.
*/
/**
* Sets the callback to be invoked when this Preference is long clicked.
*
* @param onPreferenceLongClickListener The callback to be invoked.
*/
var onPreferenceLongClickListener: OnPreferenceLongClickListener? = null

/**
* Interface definition for a callback to be invoked when a preference is
* long clicked.
*/
interface OnPreferenceLongClickListener {
/**
* Called when a Preference has been clicked.
*
* @param preference The Preference that was clicked.
* @return True if the click was handled.
*/
fun onPreferenceLongClick(preference: Preference): Boolean
}

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle)

init {
isIconSpaceReserved = false
}

var isHighlighted = false
set(value) {
field = value
notifyChanged()
}

var currentHighlightAnimator: Animator? = null

override fun onBindViewHolder(holder: PreferenceViewHolder) {
val itemView = holder.itemView
addClickListeners(itemView)

// display the title in multi-line to avoid ellipsis.
try {
val title = itemView.findViewById<TextView>(android.R.id.title)
val summary = itemView.findViewById<TextView>(android.R.id.summary)
if (title != null) {
title.setSingleLine(false)
title.setTypeface(null, mTypeface)
}

if (title !== summary) {
summary.setTypeface(null, mTypeface)
}

//cancel existing animation (find a way to resume if happens during anim?)
currentHighlightAnimator?.cancel()
if (isHighlighted) {
val colorFrom = Color.TRANSPARENT
val colorTo = ThemeUtils.getColor(itemView.context, R.attr.colorAccent)
currentHighlightAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo).apply {
duration = 250 // milliseconds
addUpdateListener { animator ->
itemView?.setBackgroundColor(animator.animatedValue as Int)
}
doOnEnd {
currentHighlightAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorTo, colorFrom).apply {
duration = 250 // milliseconds
addUpdateListener { animator ->
itemView?.setBackgroundColor(animator.animatedValue as Int)
}
doOnEnd {
isHighlighted = false
}
start()
}
}
startDelay = 200
start()
}
} else {
itemView.setBackgroundColor(Color.TRANSPARENT)
}

} catch (e: Exception) {
Timber.e(LOG_TAG, "onBindView " + e.message, e)
}

super.onBindViewHolder(holder)
}

/**
* @param view
*/
private fun addClickListeners(view: View) {
view.setOnLongClickListener {
if (null != onPreferenceLongClickListener) {
onPreferenceLongClickListener!!.onPreferenceLongClick(this@VectorPreference)
} else false
}

view.setOnClickListener {
// call only the click listener
if (onPreferenceClickListener != null) {
onPreferenceClickListener.onPreferenceClick(this@VectorPreference)
}
}
}

companion object {
private val LOG_TAG = VectorPreference::class.java.simpleName
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.graphics.Typeface
import android.util.AttributeSet
import android.view.ViewGroup
import android.widget.TextView
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceViewHolder

/**
* Customize PreferenceCategory class to redefine some attributes.
*/
class VectorPreferenceCategory : PreferenceCategory {

constructor(context: Context) : super(context)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

init {
isIconSpaceReserved = false
}

override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)

val titleTextView = holder.itemView.findViewById<TextView>(android.R.id.title)

titleTextView?.setTypeface(null, Typeface.BOLD)

// "isIconSpaceReserved = false" does not work for preference category, so remove the padding
(titleTextView?.parent as? ViewGroup)?.setPadding(0, 0, 0, 0)
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.util.AttributeSet
import androidx.preference.Preference
import im.vector.riotredesign.R

/**
* Divider for Preference screen
*/
class VectorPreferenceDivider @JvmOverloads constructor(context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : Preference(context, attrs, defStyleAttr, defStyleRes) {

init {
layoutResource = R.layout.vector_preference_divider
}
}

View File

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

package im.vector.riotredesign.core.preference

import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.preference.PreferenceViewHolder
import androidx.preference.SwitchPreference

/**
* Switch preference with title on multiline (only used in XML)
*/
class VectorSwitchPreference : SwitchPreference {

// Note: @JvmOverload does not work here...
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)

constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

constructor(context: Context, attrs: AttributeSet) : super(context, attrs)

constructor(context: Context) : super(context)

init {
isIconSpaceReserved = false
}

override fun onBindViewHolder(holder: PreferenceViewHolder) {
// display the title in multi-line to avoid ellipsis.
holder.itemView.findViewById<TextView>(android.R.id.title)?.setSingleLine(false)

super.onBindViewHolder(holder)
}
}

View File

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

package im.vector.riotredesign.core.services

import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import im.vector.riotredesign.features.notifications.NotificationUtils
import timber.log.Timber

/**
* Foreground service to manage calls
*/
class CallService : VectorService() {

/**
* call in progress (foreground notification)
*/
private var mCallIdInProgress: String? = null

/**
* incoming (foreground notification)
*/
private var mIncomingCallId: String? = null

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) {
// Service started again by the system.
// TODO What do we do here?
return START_STICKY
}

when (intent.action) {
ACTION_INCOMING_CALL -> displayIncomingCallNotification(intent)
ACTION_PENDING_CALL -> displayCallInProgressNotification(intent)
ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
else ->
// Should not happen
myStopSelf()
}

// We want the system to restore the service if killed
return START_STICKY
}

//================================================================================
// Call notification management
//================================================================================

/**
* Display a permanent notification when there is an incoming call.
*
* @param session the session
* @param isVideo true if this is a video call, false for voice call
* @param room the room
* @param callId the callId
*/
private fun displayIncomingCallNotification(intent: Intent) {
Timber.d("displayIncomingCallNotification")

// TODO
/*

// the incoming call in progress is already displayed
if (!TextUtils.isEmpty(mIncomingCallId)) {
Timber.d("displayIncomingCallNotification : the incoming call in progress is already displayed")
} else if (!TextUtils.isEmpty(mCallIdInProgress)) {
Timber.d("displayIncomingCallNotification : a 'call in progress' notification is displayed")
} else if (null == CallsManager.getSharedInstance().activeCall) {
val callId = intent.getStringExtra(EXTRA_CALL_ID)

Timber.d("displayIncomingCallNotification : display the dedicated notification")
val notification = NotificationUtils.buildIncomingCallNotification(
this,
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
intent.getStringExtra(EXTRA_ROOM_NAME),
intent.getStringExtra(EXTRA_MATRIX_ID),
callId)
startForeground(NOTIFICATION_ID, notification)

mIncomingCallId = callId

// turn the screen on for 3 seconds
if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
try {
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
val wl = pm.newWakeLock(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
CallService::class.java.simpleName)
wl.acquire(3000)
wl.release()
} catch (re: RuntimeException) {
Timber.e(re, "displayIncomingCallNotification : failed to turn screen on ")
}

}
} else {
Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call")
}// test if there is no active call
*/
}

/**
* Display a call in progress notification.
*/
private fun displayCallInProgressNotification(intent: Intent) {
val callId = intent.getStringExtra(EXTRA_CALL_ID)

val notification = NotificationUtils.buildPendingCallNotification(applicationContext,
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
intent.getStringExtra(EXTRA_ROOM_NAME),
intent.getStringExtra(EXTRA_ROOM_ID),
intent.getStringExtra(EXTRA_MATRIX_ID),
callId)

startForeground(NOTIFICATION_ID, notification)

mCallIdInProgress = callId
}

/**
* Hide the permanent call notifications
*/
private fun hideCallNotifications() {
val notification = NotificationUtils.buildCallEndedNotification(applicationContext)

// It's mandatory to startForeground to avoid crash
startForeground(NOTIFICATION_ID, notification)

myStopSelf()
}

companion object {
private const val NOTIFICATION_ID = 6480

private const val ACTION_INCOMING_CALL = "im.vector.riotredesign.core.services.CallService.INCOMING_CALL"
private const val ACTION_PENDING_CALL = "im.vector.riotredesign.core.services.CallService.PENDING_CALL"
private const val ACTION_NO_ACTIVE_CALL = "im.vector.riotredesign.core.services.CallService.NO_ACTIVE_CALL"

private const val EXTRA_IS_VIDEO = "EXTRA_IS_VIDEO"
private const val EXTRA_ROOM_NAME = "EXTRA_ROOM_NAME"
private const val EXTRA_ROOM_ID = "EXTRA_ROOM_ID"
private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"
private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"

fun onIncomingCall(context: Context,
isVideo: Boolean,
roomName: String,
roomId: String,
matrixId: String,
callId: String) {
val intent = Intent(context, CallService::class.java)
.apply {
action = ACTION_INCOMING_CALL
putExtra(EXTRA_IS_VIDEO, isVideo)
putExtra(EXTRA_ROOM_NAME, roomName)
putExtra(EXTRA_ROOM_ID, roomId)
putExtra(EXTRA_MATRIX_ID, matrixId)
putExtra(EXTRA_CALL_ID, callId)
}

ContextCompat.startForegroundService(context, intent)
}

fun onPendingCall(context: Context,
isVideo: Boolean,
roomName: String,
roomId: String,
matrixId: String,
callId: String) {
val intent = Intent(context, CallService::class.java)
.apply {
action = ACTION_PENDING_CALL
putExtra(EXTRA_IS_VIDEO, isVideo)
putExtra(EXTRA_ROOM_NAME, roomName)
putExtra(EXTRA_ROOM_ID, roomId)
putExtra(EXTRA_MATRIX_ID, matrixId)
putExtra(EXTRA_CALL_ID, callId)
}

ContextCompat.startForegroundService(context, intent)
}

fun onNoActiveCall(context: Context) {
val intent = Intent(context, CallService::class.java)
.apply {
action = ACTION_NO_ACTIVE_CALL
}

ContextCompat.startForegroundService(context, intent)
}
}
}

View File

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

package im.vector.riotredesign.core.services

import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import androidx.work.Constraints
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotredesign.R
import im.vector.riotredesign.features.notifications.NotifiableEventResolver
import im.vector.riotredesign.features.notifications.NotificationUtils
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.util.concurrent.TimeUnit

/**
* A service in charge of controlling whether the event stream is running or not.
*
* It manages messages notifications displayed to the end user.
*/
class EventStreamServiceX : VectorService() {

/**
* Managed session (no multi session for Riot)
*/
private val mSession by inject<Session>()

/**
* Set to true to simulate a push immediately when service is destroyed
*/
private var mSimulatePushImmediate = false

/**
* The current state.
*/
private var serviceState = ServiceState.INIT
set(newServiceState) {
Timber.i("setServiceState from $field to $newServiceState")
field = newServiceState
}

/**
* Push manager
*/
// TODO private var mPushManager: PushManager? = null

private var mNotifiableEventResolver: NotifiableEventResolver? = null

/**
* Live events listener
*/
/* TODO
private val mEventsListener = object : MXEventListener() {
override fun onBingEvent(event: Event, roomState: RoomState, bingRule: BingRule) {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.i("%%%%%%%% MXEventListener: the event $event")
}

Timber.i("prepareNotification : " + event.eventId + " in " + roomState.roomId)
val session = Matrix.getMXSession(applicationContext, event.matrixId)

// invalid session ?
// should never happen.
// But it could be triggered because of multi accounts management.
// The dedicated account is removing but some pushes are still received.
if (null == session || !session.isAlive) {
Timber.i("prepareNotification : don't bing - no session")
return
}

if (EventType.CALL_INVITE == event.type) {
handleCallInviteEvent(event)
return
}


val notifiableEvent = mNotifiableEventResolver!!.resolveEvent(event, roomState, bingRule, session)
if (notifiableEvent != null) {
VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
}
}

override fun onLiveEventsChunkProcessed(fromToken: String, toToken: String) {
Timber.i("%%%%%%%% MXEventListener: onLiveEventsChunkProcessed[$fromToken->$toToken]")

VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(OutdatedEventDetector(this@EventStreamServiceX))

// do not suspend the application if there is some active calls
if (ServiceState.CATCHUP == serviceState) {
val hasActiveCalls = mSession?.mCallsManager?.hasActiveCalls() == true

// if there are some active calls, the catchup should not be stopped.
// because an user could answer to a call from another device.
// there will no push because it is his own message.
// so, the client has no choice to catchup until the ring is shutdown
if (hasActiveCalls) {
Timber.i("onLiveEventsChunkProcessed : Catchup again because there are active calls")
catchup(false)
} else if (ServiceState.CATCHUP == serviceState) {
Timber.i("onLiveEventsChunkProcessed : no Active call")
CallsManager.getSharedInstance().checkDeadCalls()
stop()
}
}
}
} */

/**
* Service internal state
*/
private enum class ServiceState {
// Initial state
INIT,
// Service is started for a Catchup. Once the catchup is finished the service will be stopped
CATCHUP,
// Service is started, and session is monitored
STARTED
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Cancel any previous worker
cancelAnySimulatedPushSchedule()

// no intent : restarted by Android
if (null == intent) {
// Cannot happen anymore
Timber.e("onStartCommand : null intent")
myStopSelf()
return START_NOT_STICKY
}

val action = intent.action

Timber.i("onStartCommand with action : $action (current state $serviceState)")

// Manage foreground notification
when (action) {
ACTION_BOOT_COMPLETE,
ACTION_APPLICATION_UPGRADE,
ACTION_SIMULATED_PUSH_RECEIVED -> {
// Display foreground notification
Timber.i("startForeground")
val notification = NotificationUtils.buildForegroundServiceNotification(this, R.string.notification_sync_in_progress)
startForeground(NotificationUtils.NOTIFICATION_ID_FOREGROUND_SERVICE, notification)
}
ACTION_GO_TO_FOREGROUND -> {
// Stop foreground notification display
Timber.i("stopForeground")
stopForeground(true)
}
}

if (null == mSession) {
Timber.e("onStartCommand : no sessions")
myStopSelf()
return START_NOT_STICKY
}

when (action) {
ACTION_START,
ACTION_GO_TO_FOREGROUND ->
when (serviceState) {
ServiceState.INIT ->
start(false)
ServiceState.CATCHUP ->
// A push has been received before, just change state, to avoid stopping the service when catchup is over
serviceState = ServiceState.STARTED
ServiceState.STARTED -> {
// Nothing to do
}
}
ACTION_STOP,
ACTION_GO_TO_BACKGROUND,
ACTION_LOGOUT ->
stop()
ACTION_PUSH_RECEIVED,
ACTION_SIMULATED_PUSH_RECEIVED ->
when (serviceState) {
ServiceState.INIT ->
start(true)
ServiceState.CATCHUP ->
catchup(true)
ServiceState.STARTED ->
// Nothing to do
Unit
}
ACTION_PUSH_UPDATE -> pushStatusUpdate()
ACTION_BOOT_COMPLETE -> {
// No FCM only
mSimulatePushImmediate = true
stop()
}
ACTION_APPLICATION_UPGRADE -> {
// FDroid only
catchup(true)
}
else -> {
// Should not happen
}
}

// We don't want the service to be restarted automatically by the System
return START_NOT_STICKY
}

override fun onDestroy() {
super.onDestroy()

// Schedule worker?
scheduleSimulatedPushIfNeeded()
}

/**
* Tell the WorkManager to cancel any schedule of push simulation
*/
private fun cancelAnySimulatedPushSchedule() {
WorkManager.getInstance().cancelAllWorkByTag(PUSH_SIMULATOR_REQUEST_TAG)
}

/**
* Configure the WorkManager to schedule a simulated push, if necessary
*/
private fun scheduleSimulatedPushIfNeeded() {
if (shouldISimulatePush()) {
val delay = if (mSimulatePushImmediate) 0 else 60_000 // TODO mPushManager?.backgroundSyncDelay ?: let { 60_000 }
Timber.i("## service is schedule to restart in $delay millis, if network is connected")

val pushSimulatorRequest = OneTimeWorkRequestBuilder<PushSimulatorWorker>()
.setInitialDelay(delay.toLong(), TimeUnit.MILLISECONDS)
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.addTag(PUSH_SIMULATOR_REQUEST_TAG)
.build()

WorkManager.getInstance().let {
// Cancel any previous worker
it.cancelAllWorkByTag(PUSH_SIMULATOR_REQUEST_TAG)
it.enqueue(pushSimulatorRequest)
}
}
}

/**
* Start the even stream.
*
* @param session the session
*/
private fun startEventStream(session: Session) {
/* TODO
// resume if it was only suspended
if (null != session.currentSyncToken) {
session.resumeEventStream()
} else {
session.startEventStream(store?.eventStreamToken)
}
*/
}

/**
* Monitor the provided session.
*
* @param session the session
*/
private fun monitorSession(session: Session) {
/* TODO
session.dataHandler.addListener(mEventsListener)
CallsManager.getSharedInstance().addSession(session)

val store = session.dataHandler.store

// the store is ready (no data loading in progress...)
if (store!!.isReady) {
startEventStream(session, store)
} else {
// wait that the store is ready before starting the events stream
store.addMXStoreListener(object : MXStoreListener() {
override fun onStoreReady(accountId: String) {
startEventStream(session, store)

store.removeMXStoreListener(this)
}

override fun onStoreCorrupted(accountId: String, description: String) {
// start a new initial sync
if (null == store.eventStreamToken) {
startEventStream(session, store)
} else {
// the data are out of sync
Matrix.getInstance(applicationContext)!!.reloadSessions(applicationContext)
}

store.removeMXStoreListener(this)
}

override fun onStoreOOM(accountId: String, description: String) {
val uiHandler = Handler(mainLooper)

uiHandler.post {
Toast.makeText(applicationContext, "$accountId : $description", Toast.LENGTH_LONG).show()
Matrix.getInstance(applicationContext)!!.reloadSessions(applicationContext)
}
}
})

store.open()
}
*/
}

/**
* internal start.
*/
private fun start(forPush: Boolean) {
val applicationContext = applicationContext
// TODO mPushManager = Matrix.getInstance(applicationContext)!!.pushManager
mNotifiableEventResolver = NotifiableEventResolver(applicationContext)

monitorSession(mSession!!)

serviceState = if (forPush) {
ServiceState.CATCHUP
} else {
ServiceState.STARTED
}
}

/**
* internal stop.
*/
private fun stop() {
Timber.i("## stop(): the service is stopped")

/* TODO
if (null != mSession && mSession!!.isAlive) {
mSession!!.stopEventStream()
mSession!!.dataHandler.removeListener(mEventsListener)
CallsManager.getSharedInstance().removeSession(mSession)
}
mSession = null
*/

// Stop the service
myStopSelf()
}

/**
* internal catchup method.
*
* @param checkState true to check if the current state allow to perform a catchup
*/
private fun catchup(checkState: Boolean) {
var canCatchup = true

if (!checkState) {
Timber.i("catchup without checking serviceState ")
} else {
Timber.i("catchup with serviceState " + serviceState + " CurrentActivity ") // TODO + VectorApp.getCurrentActivity())

/* TODO
// the catchup should only be done
// 1- the serviceState is in catchup : the event stream might have gone to sleep between two catchups
// 2- the thread is suspended
// 3- the application has been launched by a push so there is no displayed activity
canCatchup = (serviceState == ServiceState.CATCHUP
//|| (serviceState == ServiceState.PAUSE)
|| ServiceState.STARTED == serviceState && null == VectorApp.getCurrentActivity())
*/
}

if (canCatchup) {
if (mSession != null) {
// TODO mSession!!.catchupEventStream()
} else {
Timber.i("catchup no session")
}

serviceState = ServiceState.CATCHUP
} else {
Timber.i("No catchup is triggered because there is already a running event thread")
}
}

/**
* The push status has been updated (i.e disabled or enabled).
* TODO Useless now?
*/
private fun pushStatusUpdate() {
Timber.i("## pushStatusUpdate")
}

/* ==========================================================================================
* Push simulator
* ========================================================================================== */

/**
* @return true if the FCM is disable or not setup, user allowed background sync, user wants notification
*/
private fun shouldISimulatePush(): Boolean {
return false

/* TODO

if (Matrix.getInstance(applicationContext)?.defaultSession == null) {
Timber.i("## shouldISimulatePush: NO: no session")

return false
}

mPushManager?.let { pushManager ->
if (pushManager.useFcm()
&& !TextUtils.isEmpty(pushManager.currentRegistrationToken)
&& pushManager.isServerRegistered) {
// FCM is ok
Timber.i("## shouldISimulatePush: NO: FCM is up")
return false
}

if (!pushManager.isBackgroundSyncAllowed) {
// User has disabled background sync
Timber.i("## shouldISimulatePush: NO: background sync not allowed")
return false
}

if (!pushManager.areDeviceNotificationsAllowed()) {
// User does not want notifications
Timber.i("## shouldISimulatePush: NO: user does not want notification")
return false
}
}

// Lets simulate push
Timber.i("## shouldISimulatePush: YES")
return true
*/
}


//================================================================================
// Call management
//================================================================================

private fun handleCallInviteEvent(event: Event) {
/*
TODO
val session = Matrix.getMXSession(applicationContext, event.matrixId)

// invalid session ?
// should never happen.
// But it could be triggered because of multi accounts management.
// The dedicated account is removing but some pushes are still received.
if (null == session || !session.isAlive) {
Timber.d("prepareCallNotification : don't bing - no session")
return
}

val room: Room? = session.dataHandler.getRoom(event.roomId)

// invalid room ?
if (null == room) {
Timber.i("prepareCallNotification : don't bing - the room does not exist")
return
}

var callId: String? = null
var isVideo = false

try {
callId = event.contentAsJsonObject?.get("call_id")?.asString

// Check if it is a video call
val offer = event.contentAsJsonObject?.get("offer")?.asJsonObject
val sdp = offer?.get("sdp")
val sdpValue = sdp?.asString

isVideo = sdpValue?.contains("m=video") == true
} catch (e: Exception) {
Timber.e("prepareNotification : getContentAsJsonObject " + e.message, e)
}

if (!TextUtils.isEmpty(callId)) {
CallService.onIncomingCall(this,
isVideo,
room.getRoomDisplayName(this),
room.roomId,
session.myUserId!!,
callId!!)
}
*/
}

companion object {
private const val PUSH_SIMULATOR_REQUEST_TAG = "PUSH_SIMULATOR_REQUEST_TAG"

private const val ACTION_START = "im.vector.riotredesign.core.services.EventStreamServiceX.START"
private const val ACTION_LOGOUT = "im.vector.riotredesign.core.services.EventStreamServiceX.LOGOUT"
private const val ACTION_GO_TO_FOREGROUND = "im.vector.riotredesign.core.services.EventStreamServiceX.GO_TO_FOREGROUND"
private const val ACTION_GO_TO_BACKGROUND = "im.vector.riotredesign.core.services.EventStreamServiceX.GO_TO_BACKGROUND"
private const val ACTION_PUSH_UPDATE = "im.vector.riotredesign.core.services.EventStreamServiceX.PUSH_UPDATE"
private const val ACTION_PUSH_RECEIVED = "im.vector.riotredesign.core.services.EventStreamServiceX.PUSH_RECEIVED"
private const val ACTION_SIMULATED_PUSH_RECEIVED = "im.vector.riotredesign.core.services.EventStreamServiceX.SIMULATED_PUSH_RECEIVED"
private const val ACTION_STOP = "im.vector.riotredesign.core.services.EventStreamServiceX.STOP"
private const val ACTION_BOOT_COMPLETE = "im.vector.riotredesign.core.services.EventStreamServiceX.BOOT_COMPLETE"
private const val ACTION_APPLICATION_UPGRADE = "im.vector.riotredesign.core.services.EventStreamServiceX.APPLICATION_UPGRADE"

/* ==========================================================================================
* Events sent to the service
* ========================================================================================== */

fun onApplicationStarted(context: Context) {
sendAction(context, ACTION_START)
}

fun onLogout(context: Context) {
sendAction(context, ACTION_LOGOUT)
}

fun onAppGoingToForeground(context: Context) {
sendAction(context, ACTION_GO_TO_FOREGROUND)
}

fun onAppGoingToBackground(context: Context) {
sendAction(context, ACTION_GO_TO_BACKGROUND)
}

fun onPushUpdate(context: Context) {
sendAction(context, ACTION_PUSH_UPDATE)
}

fun onPushReceived(context: Context) {
sendAction(context, ACTION_PUSH_RECEIVED)
}

fun onSimulatedPushReceived(context: Context) {
sendAction(context, ACTION_SIMULATED_PUSH_RECEIVED, true)
}

fun onApplicationStopped(context: Context) {
sendAction(context, ACTION_STOP)
}

fun onBootComplete(context: Context) {
sendAction(context, ACTION_BOOT_COMPLETE, true)
}

fun onApplicationUpgrade(context: Context) {
sendAction(context, ACTION_APPLICATION_UPGRADE, true)
}

private fun sendAction(context: Context, action: String, foreground: Boolean = false) {
Timber.i("sendAction $action")

val intent = Intent(context, EventStreamServiceX::class.java)
intent.action = action

if (foreground) {
ContextCompat.startForegroundService(context, intent)
} else {
context.startService(intent)
}
}
}
}

View File

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

package im.vector.riotredesign.core.services

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters

/**
* This class simulate push event when FCM is not working/disabled
*/
class PushSimulatorWorker(val context: Context,
workerParams: WorkerParameters) : Worker(context, workerParams) {

override fun doWork(): Result {
// Simulate a Push
EventStreamServiceX.onSimulatedPushReceived(context)

// Indicate whether the task finished successfully with the Result
return Result.success()
}
}

View File

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

package im.vector.riotredesign.core.services

import android.app.Service
import android.content.Intent
import android.os.IBinder
import timber.log.Timber

/**
* Parent class for all services
*/
abstract class VectorService : Service() {

/**
* Tells if the service self destroyed.
*/
private var mIsSelfDestroyed = false

override fun onCreate() {
super.onCreate()

Timber.i("## onCreate() : $this")
}

override fun onDestroy() {
Timber.i("## onDestroy() : $this")

if (!mIsSelfDestroyed) {
Timber.w("## Destroy by the system : $this")
}

super.onDestroy()
}

protected fun myStopSelf() {
mIsSelfDestroyed = true
stopSelf()
}

override fun onBind(intent: Intent?): IBinder? {
return null
}
}

View File

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

package im.vector.riotredesign.core.utils

import android.content.Context
import android.media.Ringtone
import android.media.RingtoneManager
import android.net.Uri
import android.preference.PreferenceManager
import androidx.core.content.edit
import im.vector.riotredesign.features.settings.PreferencesManager

/**
* This file manages the sound ringtone for calls.
* It allows you to use the default Riot Ringtone, or the standard ringtone or set a different one from the available choices
* in Android.
*/

/**
* Returns a Uri object that points to a specific Ringtone.
*
* If no Ringtone was explicitly set using Riot, it will return the Uri for the current system
* ringtone for calls.
*
* @return the [Uri] of the currently set [Ringtone]
* @see Ringtone
*/
fun getCallRingtoneUri(context: Context): Uri? {
val callRingtone: String? = PreferenceManager.getDefaultSharedPreferences(context)
.getString(PreferencesManager.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, null)

callRingtone?.let {
return Uri.parse(it)
}

return try {
// Use current system notification sound for incoming calls per default (note that it can return null)
RingtoneManager.getActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE)
} catch (e: SecurityException) {
// Ignore for now
null
}
}

/**
* Returns a Ringtone object that can then be played.
*
* If no Ringtone was explicitly set using Riot, it will return the current system ringtone
* for calls.
*
* @return the currently set [Ringtone]
* @see Ringtone
*/
fun getCallRingtone(context: Context): Ringtone? {
getCallRingtoneUri(context)?.let {
// Note that it can also return null
return RingtoneManager.getRingtone(context, it)
}

return null
}

/**
* Returns a String with the name of the current Ringtone.
*
* If no Ringtone was explicitly set using Riot, it will return the name of the current system
* ringtone for calls.
*
* @return the name of the currently set [Ringtone], or null
* @see Ringtone
*/
fun getCallRingtoneName(context: Context): String? {
return getCallRingtone(context)?.getTitle(context)
}

/**
* Sets the selected ringtone for riot calls.
*
* @param ringtoneUri
* @see Ringtone
*/
fun setCallRingtoneUri(context: Context, ringtoneUri: Uri) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit {
putString(PreferencesManager.SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY, ringtoneUri.toString())
}
}

/**
* Set using Riot default ringtone
*/
fun useRiotDefaultRingtone(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PreferencesManager.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, true)
}

/**
* Ask if default Riot ringtone has to be used
*/
fun setUseRiotDefaultRingtone(context: Context, useRiotDefault: Boolean) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit {
putBoolean(PreferencesManager.SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY, useRiotDefault)
}
}

View File

@ -0,0 +1,576 @@
package im.vector.riotredesign.core.utils

import android.content.Context
import android.os.Build
import android.security.KeyPairGeneratorSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.annotation.RequiresApi
import java.io.*
import java.math.BigInteger
import java.security.KeyPairGenerator
import java.security.KeyStore
import java.security.SecureRandom
import java.util.*
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import javax.security.auth.x500.X500Principal


/**
* Offers simple methods to securely store secrets in an Android Application.
* The encryption keys are randomly generated and securely managed by the key store, thus your secrets
* are safe. You only need to remember a key alias to perform encrypt/decrypt operations.
*
* <b>Android M++</b>
* On android M+, the keystore can generates and store AES keys via API. But below API M this functionality
* is not available.
*
* <b>Android [K-M[</b>
* For android >=KITKAT and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a
* random secret key in generated to perform encryption.
* This secret key is encrypted with the public RSA key and stored with the encrypted secret.
* In order to decrypt the encrypted secret key will be retrieved then decrypted with the RSA private key.
*
* <b>Older androids</b>
* For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt.
* The salt and iv are stored with encrypted data.
*
* Sample usage:
* <code>
* val secret = "The answer is 42"
* val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context)
* //This can be stored anywhere e.g. encoded in b64 and stored in preference for example
*
* //to get back the secret, just call
* val kDecripted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context)
* </code>
*
* You can also just use this utility to store a secret key, and use any encryption algorthim that you want.
*
* Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
* add a pin or change the schema); So you might and with a useless pile of bytes.
*/
object SecretStoringUtils {

private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val AES_MODE = "AES/GCM/NoPadding";
private const val RSA_MODE = "RSA/ECB/PKCS1Padding"

const val FORMAT_API_M: Byte = 0
const val FORMAT_1: Byte = 1
const val FORMAT_2: Byte = 2

val keyStore: KeyStore by lazy {
KeyStore.getInstance(ANDROID_KEY_STORE).apply {
load(null)
}
}

private val secureRandom = SecureRandom()

/**
* Encrypt the given secret using the android Keystore.
* On android >= M, will directly use the keystore to generate a symetric key
* On KitKat >= KitKat and <M, as symetric key gen is not available, will use an asymetric key generated
* in the keystore to encrypted a random symetric key. The encrypted symetric key is returned
* in the bytearray (in can be stored anywhere, it is encrypted)
* On older version a key in generated from alias with random salt.
*
* The secret is encrypted using the following method: AES/GCM/NoPadding
*/
@Throws(Exception::class)
fun securelyStoreString(secret: String, keyAlias: String, context: Context): ByteArray? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return encryptStringM(secret, keyAlias)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return encryptStringJ(secret, keyAlias, context)
} else {
return encryptForOldDevicesNotGood(secret, keyAlias)
}
}

/**
* Decrypt a secret that was encrypted by #securelyStoreString()
*/
@Throws(Exception::class)
fun loadSecureSecret(encrypted: ByteArray, keyAlias: String, context: Context): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return decryptStringM(encrypted, keyAlias)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return decryptStringJ(encrypted, keyAlias, context)
} else {
return decryptForOldDevicesNotGood(encrypted, keyAlias)
}
}

fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream, context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
saveSecureObjectM(keyAlias, output, any)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return saveSecureObjectK(keyAlias, output, any, context)
} else {
return saveSecureObjectOldNotGood(keyAlias, output, any)
}
}

fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String, context: Context): T? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return loadSecureObjectM(keyAlias, inputStream)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return loadSecureObjectK(keyAlias, inputStream, context)
} else {
return loadSecureObjectOldNotGood(keyAlias, inputStream)
}
}


@RequiresApi(Build.VERSION_CODES.M)
fun getOrGenerateSymmetricKeyForAlias(alias: String): SecretKey {
val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
?.secretKey
if (secretKeyEntry == null) {
//we generate it
val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val keyGenSpec = KeyGenParameterSpec.Builder(alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(128)
.build()
generator.init(keyGenSpec)
return generator.generateKey()
}
return secretKeyEntry
}


/*
Symetric Key Generation is only available in M, so before M the idea is to:
- Generate a pair of RSA keys;
- Generate a random AES key;
- Encrypt the AES key using the RSA public key;
- Store the encrypted AES
Generate a key pair for encryption
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun getOrGenerateKeyPairForAlias(alias: String, context: Context): KeyStore.PrivateKeyEntry {
val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry)

if (privateKeyEntry != null) return privateKeyEntry

val start = Calendar.getInstance()
val end = Calendar.getInstance()
end.add(Calendar.YEAR, 30)

val spec = KeyPairGeneratorSpec.Builder(context)
.setAlias(alias)
.setSubject(X500Principal("CN=$alias"))
.setSerialNumber(BigInteger.TEN)
//.setEncryptionRequired() requires that the phone as a pin/schema
.setStartDate(start.time)
.setEndDate(end.time)
.build()
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE).run {
initialize(spec)
generateKeyPair()
}
return (keyStore.getEntry(alias, null) as KeyStore.PrivateKeyEntry)

}


@RequiresApi(Build.VERSION_CODES.M)
fun encryptStringM(text: String, keyAlias: String): ByteArray? {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)

val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
//we happen the iv to the final result
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
return formatMMake(iv, encryptedBytes)
}

@RequiresApi(Build.VERSION_CODES.M)
fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk))

val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)

val cipher = Cipher.getInstance(AES_MODE)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)

return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
}

@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
fun encryptStringJ(text: String, keyAlias: String, context: Context): ByteArray? {
//we generate a random symetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
val sKey = SecretKeySpec(key, "AES")

//we encrypt this key thanks to the key store
val encryptedKey = rsaEncrypt(keyAlias, key, context)

val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
val iv = cipher.iv
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))

return format1Make(encryptedKey, iv, encryptedBytes)
}

fun encryptForOldDevicesNotGood(text: String, keyAlias: String): ByteArray {
val salt = ByteArray(8)
secureRandom.nextBytes(salt)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
val tmp = factory.generateSecret(spec)
val sKey = SecretKeySpec(tmp.encoded, "AES")

val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
val iv = cipher.iv
val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))

return format2Make(salt, iv, encryptedBytes)
}

fun decryptForOldDevicesNotGood(data: ByteArray, keyAlias: String): String? {

val (salt, iv, encrypted) = format2Extract(ByteArrayInputStream(data))
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128)
val tmp = factory.generateSecret(spec)
val sKey = SecretKeySpec(tmp.encoded, "AES")

val cipher = Cipher.getInstance(AES_MODE)
// cipher.init(Cipher.ENCRYPT_MODE, sKey)
// val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))

val specIV = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, sKey, specIV)

return String(cipher.doFinal(encrypted), Charsets.UTF_8)
}

@RequiresApi(Build.VERSION_CODES.KITKAT)
fun decryptStringJ(data: ByteArray, keyAlias: String, context: Context): String? {

val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data))

//we need to decrypt the key
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)

return String(cipher.doFinal(encrypted), Charsets.UTF_8)

}


@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)

val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
val iv = cipher.iv

val bos1 = ByteArrayOutputStream()
ObjectOutputStream(bos1).use {
it.writeObject(writeObject)
}
//Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV
val doFinal = cipher.doFinal(bos1.toByteArray())
output.write(FORMAT_API_M.toInt())
output.write(iv.size)
output.write(iv)
output.write(doFinal)
}

@RequiresApi(Build.VERSION_CODES.KITKAT)
fun saveSecureObjectK(keyAlias: String, output: OutputStream, writeObject: Any, context: Context) {
//we generate a random symetric key
val key = ByteArray(16)
secureRandom.nextBytes(key)
val sKey = SecretKeySpec(key, "AES")

//we encrypt this key thanks to the key store
val encryptedKey = rsaEncrypt(keyAlias, key, context)

val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, sKey)
val iv = cipher.iv

val bos1 = ByteArrayOutputStream()
val cos = CipherOutputStream(bos1, cipher)
ObjectOutputStream(cos).use {
it.writeObject(writeObject)
}

output.write(FORMAT_1.toInt())
output.write((encryptedKey.size and 0xFF00).shr(8))
output.write(encryptedKey.size and 0x00FF)
output.write(encryptedKey)
output.write(iv.size)
output.write(iv)
output.write(bos1.toByteArray())
}

fun saveSecureObjectOldNotGood(keyAlias: String, output: OutputStream, writeObject: Any) {
val salt = ByteArray(8)
secureRandom.nextBytes(salt)
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128))
val secretKey = SecretKeySpec(tmp.encoded, "AES")


val cipher = Cipher.getInstance(AES_MODE)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv

val bos1 = ByteArrayOutputStream()
ObjectOutputStream(bos1).use {
it.writeObject(writeObject)
}
//Have to do it like that if i encapsulate the outputstream, the cipher could fail saying reuse IV
val doFinal = cipher.doFinal(bos1.toByteArray())

output.write(FORMAT_2.toInt())
output.write(salt.size)
output.write(salt)
output.write(iv.size)
output.write(iv)
output.write(doFinal)
}

// @RequiresApi(Build.VERSION_CODES.M)
// @Throws(IOException::class)
// fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) {
// FileOutputStream(file).use {
// saveSecureObjectM(keyAlias, it, writeObject)
// }
// }
//
// @RequiresApi(Build.VERSION_CODES.M)
// @Throws(IOException::class)
// fun <T> loadSecureObjectM(keyAlias: String, file: File): T? {
// FileInputStream(file).use {
// return loadSecureObjectM<T>(keyAlias, it)
// }
// }

@RequiresApi(Build.VERSION_CODES.M)
@Throws(IOException::class)
fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
val secretKey = getOrGenerateSymmetricKeyForAlias(keyAlias)

val format = inputStream.read()
assert(format.toByte() == FORMAT_API_M)

val ivSize = inputStream.read()
val iv = ByteArray(ivSize)
inputStream.read(iv, 0, ivSize)
val cipher = Cipher.getInstance(AES_MODE)
val spec = GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)

CipherInputStream(inputStream, cipher).use { cipherInputStream ->
ObjectInputStream(cipherInputStream).use {
val readObject = it.readObject()
return readObject as? T
}
}

}

@RequiresApi(Build.VERSION_CODES.KITKAT)
@Throws(IOException::class)
fun <T> loadSecureObjectK(keyAlias: String, inputStream: InputStream, context: Context): T? {

val (encryptedKey, iv, encrypted) = format1Extract(inputStream)

//we need to decrypt the key
val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey), context)
val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)

val encIS = ByteArrayInputStream(encrypted)

CipherInputStream(encIS, cipher).use { cipherInputStream ->
ObjectInputStream(cipherInputStream).use {
val readObject = it.readObject()
return readObject as? T
}
}
}

@Throws(Exception::class)
fun <T> loadSecureObjectOldNotGood(keyAlias: String, inputStream: InputStream): T? {

val (salt, iv, encrypted) = format2Extract(inputStream)

val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val tmp = factory.generateSecret(PBEKeySpec(keyAlias.toCharArray(), salt, 10000, 128))
val sKey = SecretKeySpec(tmp.encoded, "AES")
//we need to decrypt the key

val cipher = Cipher.getInstance(AES_MODE)
val spec = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) IvParameterSpec(iv) else GCMParameterSpec(128, iv)
cipher.init(Cipher.DECRYPT_MODE, sKey, spec)

val encIS = ByteArrayInputStream(encrypted)

CipherInputStream(encIS, cipher).use {
ObjectInputStream(it).use {
val readObject = it.readObject()
return readObject as? T
}
}
}


@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@Throws(Exception::class)
private fun rsaEncrypt(alias: String, secret: ByteArray, context: Context): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
// Encrypt the text
val inputCipher = Cipher.getInstance(RSA_MODE)
inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)

val outputStream = ByteArrayOutputStream()
val cipherOutputStream = CipherOutputStream(outputStream, inputCipher)
cipherOutputStream.write(secret)
cipherOutputStream.close()

return outputStream.toByteArray()
}

@RequiresApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@Throws(Exception::class)
private fun rsaDecrypt(alias: String, encrypted: InputStream, context: Context): ByteArray {
val privateKeyEntry = getOrGenerateKeyPairForAlias(alias, context)
val output = Cipher.getInstance(RSA_MODE)
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)

val bos = ByteArrayOutputStream()
CipherInputStream(encrypted, output).use {
it.copyTo(bos)
}

return bos.toByteArray()
}

private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
val format = bis.read().toByte()
assert(format == FORMAT_API_M)

val ivSize = bis.read()
val iv = ByteArray(ivSize)
bis.read(iv, 0, ivSize)


val bos = ByteArrayOutputStream()
var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Pair(iv, encrypted)
}

private fun formatMMake(iv: ByteArray, data: ByteArray): ByteArray {
val bos = ByteArrayOutputStream(2 + iv.size + data.size)
bos.write(FORMAT_API_M.toInt())
bos.write(iv.size)
bos.write(iv)
bos.write(data)
return bos.toByteArray()
}

private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {

val format = bis.read()
assert(format.toByte() == FORMAT_1)

val keySizeBig = bis.read()
val keySizeLow = bis.read()
val encryptedKeySize = keySizeBig.shl(8) + keySizeLow
val encryptedKey = ByteArray(encryptedKeySize)
bis.read(encryptedKey)

val ivSize = bis.read()
val iv = ByteArray(ivSize)
bis.read(iv)

val bos = ByteArrayOutputStream()

var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Triple(encryptedKey, iv, encrypted)
}

private fun format1Make(encryptedKey: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
val bos = ByteArrayOutputStream(4 + encryptedKey.size + iv.size + encryptedBytes.size)
bos.write(FORMAT_1.toInt())
bos.write((encryptedKey.size and 0xFF00).shr(8))
bos.write(encryptedKey.size and 0x00FF)
bos.write(encryptedKey)
bos.write(iv.size)
bos.write(iv)
bos.write(encryptedBytes)

return bos.toByteArray()
}

private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size)
bos.write(FORMAT_2.toInt())
bos.write(salt.size)
bos.write(salt)
bos.write(iv.size)
bos.write(iv)
bos.write(encryptedBytes)

return bos.toByteArray()
}

private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {

val format = bis.read()
assert(format.toByte() == FORMAT_2)

val saltSize = bis.read()
val salt = ByteArray(saltSize)
bis.read(salt)

val ivSize = bis.read()
val iv = ByteArray(ivSize)
bis.read(iv)

val bos = ByteArrayOutputStream()

var next = bis.read()
while (next != -1) {
bos.write(next)
next = bis.read()
}
val encrypted = bos.toByteArray()
return Triple(salt, iv, encrypted)
}
}

View File

@ -26,6 +26,7 @@ import android.provider.Settings
import android.widget.Toast
import androidx.fragment.app.Fragment
import im.vector.riotredesign.R
import im.vector.riotredesign.features.notifications.supportNotificationChannels
import im.vector.riotredesign.features.settings.VectorLocale
import timber.log.Timber
import java.util.*
@ -124,10 +125,6 @@ fun startNotificationSettingsIntent(fragment: Fragment, requestCode: Int) {
fragment.startActivityForResult(intent, requestCode)
}

// TODO This comes from NotificationUtils
fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)


/**
* Shows notification system settings for the given channel id.
*/
@ -184,3 +181,8 @@ fun startImportTextFromFileIntent(fragment: Fragment, requestCode: Int) {
fun Context.toast(resId: Int) {
Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()
}

// Not in KTX anymore
fun Context.toast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

View File

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

package im.vector.riotredesign.features.badge

import android.content.Context
import android.os.Build
import im.vector.matrix.android.api.session.Session
import me.leolin.shortcutbadger.ShortcutBadger
import timber.log.Timber

/**
* Manage application badge (displayed in the launcher)
*/
object BadgeProxy {

/**
* Badge is now managed by notification channel, so no need to use compatibility library in recent versions
*
* @return true if library ShortcutBadger can be used
*/
private fun useShortcutBadger() = Build.VERSION.SDK_INT < Build.VERSION_CODES.O

/**
* Update the application badge value.
*
* @param context the context
* @param badgeValue the new badge value
*/
fun updateBadgeCount(context: Context, badgeValue: Int) {
if (!useShortcutBadger()) {
return
}

try {
ShortcutBadger.setBadge(context, badgeValue)
} catch (e: Exception) {
Timber.e(e, "## updateBadgeCount(): Exception Msg=" + e.message)
}

}

/**
* Refresh the badge count for specific configurations.<br></br>
* The refresh is only effective if the device is:
* * offline * does not support FCM
* * FCM registration failed
* <br></br>Notifications rooms are parsed to track the notification count value.
*
* @param aSession session value
* @param aContext App context
*/
fun specificUpdateBadgeUnreadCount(aSession: Session?, aContext: Context?) {
if (!useShortcutBadger()) {
return
}

/* TODO
val dataHandler: MXDataHandler

// sanity check
if (null == aContext || null == aSession) {
Timber.w("## specificUpdateBadgeUnreadCount(): invalid input null values")
} else {
dataHandler = aSession.dataHandler

if (dataHandler == null) {
Timber.w("## specificUpdateBadgeUnreadCount(): invalid DataHandler instance")
} else {
if (aSession.isAlive) {
var isRefreshRequired: Boolean
val pushManager = Matrix.getInstance(aContext)!!.pushManager

// update the badge count if the device is offline, FCM is not supported or FCM registration failed
isRefreshRequired = !Matrix.getInstance(aContext)!!.isConnected
isRefreshRequired = isRefreshRequired or (null != pushManager && (!pushManager.useFcm() || !pushManager.hasRegistrationToken()))

if (isRefreshRequired) {
updateBadgeCount(aContext, dataHandler)
}
}
}
}
*/
}

/**
* Update the badge count value according to the rooms content.
*
* @param aContext App context
* @param aDataHandler data handler instance
*/
private fun updateBadgeCount(aSession: Session?, aContext: Context?) {
if (!useShortcutBadger()) {
return
}

/* TODO
//sanity check
if (null == aContext || null == aDataHandler) {
Timber.w("## updateBadgeCount(): invalid input null values")
} else if (null == aDataHandler.store) {
Timber.w("## updateBadgeCount(): invalid store instance")
} else {
val roomCompleteList = ArrayList(aDataHandler.store.rooms)
var unreadRoomsCount = 0

for (room in roomCompleteList) {
if (room.notificationCount > 0) {
unreadRoomsCount++
}
}

// update the badge counter
Timber.d("## updateBadgeCount(): badge update count=$unreadRoomsCount")
updateBadgeCount(aContext, unreadRoomsCount)
}
*/
}
}

View File

@ -27,6 +27,7 @@ import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.hideKeyboard
import im.vector.riotredesign.core.extensions.observeEvent
@ -37,6 +38,8 @@ import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragment
import im.vector.riotredesign.features.rageshake.BugReporter
import im.vector.riotredesign.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotredesign.features.settings.VectorSettingsActivity
import im.vector.riotredesign.features.workers.signout.SignOutUiWorker
import kotlinx.android.synthetic.main.activity_home.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
@ -101,12 +104,22 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
drawerToggle.syncState()
}

override fun getMenuRes() = R.menu.home

override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
drawerLayout.openDrawer(GravityCompat.START)
return true
}
R.id.sliding_menu_settings -> {
startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
return true
}
R.id.sliding_menu_sign_out -> {
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
return true
}
}

return true

View File

@ -17,9 +17,6 @@
package im.vector.riotredesign.features.home

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.replaceChildFragment
import im.vector.riotredesign.core.platform.RiotFragment
@ -35,9 +32,7 @@ class HomeDrawerFragment : RiotFragment() {
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home_drawer, container, false)
}
override fun getLayoutResId() = R.layout.fragment_home_drawer

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

View File

@ -17,9 +17,6 @@
package im.vector.riotredesign.features.home.group

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.fragmentViewModel
@ -44,9 +41,7 @@ class GroupListFragment : RiotFragment(), GroupSummaryController.Callback {
private val viewModel: GroupListViewModel by fragmentViewModel()
private val groupController by inject<GroupSummaryController>()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_group_list, container, false)
}
override fun getLayoutResId() = R.layout.fragment_group_list

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

View File

@ -16,11 +16,9 @@

package im.vector.riotredesign.features.home.room.detail

import android.graphics.drawable.AnimationDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.bumptech.glide.Glide
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import kotlinx.android.synthetic.main.fragment_loading_room_detail.*
@ -34,15 +32,15 @@ class LoadingRoomDetailFragment : RiotFragment() {
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_loading_room_detail, container, false)
}
override fun getLayoutResId() = R.layout.fragment_loading_room_detail

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Glide.with(this)
.load(R.drawable.riot_splash)
.into(animatedLogoImageView)

val background = animatedLogoImageView.background
if (background is AnimationDrawable) {
background.start()
}
}



View File

@ -18,9 +18,7 @@ package im.vector.riotredesign.features.home.room.detail

import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
@ -69,9 +67,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {

private lateinit var scrollOnNewMessageCallback: ScrollOnNewMessageCallback

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_detail, container, false)
}
override fun getLayoutResId() = R.layout.fragment_room_detail

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

View File

@ -97,7 +97,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
.subscribeBy(onNext = { actions ->
val mostRecentEvent = actions.maxBy { it.event.displayIndex }
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
room.setReadReceipt(eventId, callback = object : MatrixCallback<Unit> {})
}
})
.disposeOnClear()

View File

@ -19,9 +19,6 @@ package im.vector.riotredesign.features.home.room.list
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete
@ -54,9 +51,7 @@ class RoomListFragment : RiotFragment(), RoomSummaryController.Callback {
private val homeNavigator by inject<HomeNavigator>()
private val roomListViewModel: RoomListViewModel by fragmentViewModel()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_room_list, container, false)
}
override fun getLayoutResId() = R.layout.fragment_room_list

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

View File

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

package im.vector.riotredesign.features.homeserver

import android.content.Context
import android.text.TextUtils
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import im.vector.riotredesign.R

/**
* Object to store and retrieve home and identity server urls
*/
object ServerUrlsRepository {

// Keys used to store default servers urls from the referrer
private const val DEFAULT_REFERRER_HOME_SERVER_URL_PREF = "default_referrer_home_server_url"
private const val DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF = "default_referrer_identity_server_url"

// Keys used to store current home server url and identity url
const val HOME_SERVER_URL_PREF = "home_server_url"
const val IDENTITY_SERVER_URL_PREF = "identity_server_url"

/**
* Save home and identity sever urls received by the Referrer receiver
*/
fun setDefaultUrlsFromReferrer(context: Context, homeServerUrl: String, identityServerUrl: String) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit {
if (!TextUtils.isEmpty(homeServerUrl)) {
putString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF, homeServerUrl)
}

if (!TextUtils.isEmpty(identityServerUrl)) {
putString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF, identityServerUrl)
}
}
}

/**
* Save home and identity sever urls entered by the user. May be custom or default value
*/
fun saveServerUrls(context: Context, homeServerUrl: String, identityServerUrl: String) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit {
putString(HOME_SERVER_URL_PREF, homeServerUrl)
putString(IDENTITY_SERVER_URL_PREF, identityServerUrl)
}
}

/**
* Return last used home server url, or the default one from referrer or the default one from resources
*/
fun getLastHomeServerUrl(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)

return prefs.getString(HOME_SERVER_URL_PREF,
prefs.getString(DEFAULT_REFERRER_HOME_SERVER_URL_PREF,
getDefaultHomeServerUrl(context)))
}


/**
* Return last used identity server url, or the default one from referrer or the default one from resources
*/
fun getLastIdentityServerUrl(context: Context): String {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)

return prefs.getString(IDENTITY_SERVER_URL_PREF,
prefs.getString(DEFAULT_REFERRER_IDENTITY_SERVER_URL_PREF,
getDefaultIdentityServerUrl(context)))
}

/**
* Return true if url is the default home server url form resources
*/
fun isDefaultHomeServerUrl(context: Context, url: String) = url == getDefaultHomeServerUrl(context)

/**
* Return true if url is the default identity server url form resources
*/
fun isDefaultIdentityServerUrl(context: Context, url: String) = url == getDefaultIdentityServerUrl(context)

/**
* Return default home server url from resources
*/
fun getDefaultHomeServerUrl(context: Context): String = context.getString(R.string.default_hs_server_url)

/**
* Return default identity server url from resources
*/
fun getDefaultIdentityServerUrl(context: Context): String = context.getString(R.string.default_identity_server_url)
}

View File

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

package im.vector.riotredesign.features.notifications

import android.content.Context
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.WorkerThread
import androidx.core.graphics.drawable.IconCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import timber.log.Timber

/**
* FIXME It works, but it does not refresh the notification, when it's already displayed
*/
class IconLoader(val context: Context,
val listener: IconLoaderListener) {

/**
* Avatar Url -> Icon
*/
private val cache = HashMap<String, IconCompat>()

// URLs to load
private val toLoad = HashSet<String>()

// Black list of URLs (broken URL, etc.)
private val blacklist = HashSet<String>()

private var uiHandler = Handler()

private val handlerThread: HandlerThread = HandlerThread("IconLoader", Thread.MIN_PRIORITY)
private var backgroundHandler: Handler

init {
handlerThread.start()
backgroundHandler = Handler(handlerThread.looper)
}

/**
* Get icon of a user.
* If already in cache, use it, else load it and call IconLoaderListener.onIconsLoaded() when ready
*/
fun getUserIcon(path: String?): IconCompat? {
if (path == null) {
return null
}

synchronized(cache) {
if (cache[path] != null) {
return cache[path]
}

// Add to the queue, if not blacklisted
if (!blacklist.contains(path)) {
if (toLoad.contains(path)) {
// Wait
} else {
toLoad.add(path)

backgroundHandler.post {
loadUserIcon(path)
}
}
}
}

return null
}

@WorkerThread
private fun loadUserIcon(path: String) {
val iconCompat = path.let {
try {
Glide.with(context)
.asBitmap()
.load(path)
.apply(RequestOptions.circleCropTransform()
.format(DecodeFormat.PREFER_ARGB_8888))
.submit()
.get()
} catch (e: Exception) {
Timber.e(e, "decodeFile failed")
null
}?.let { bitmap ->
IconCompat.createWithBitmap(bitmap)
}
}

synchronized(cache) {
if (iconCompat == null) {
// Add to the blacklist
blacklist.add(path)
} else {
cache[path] = iconCompat
}

toLoad.remove(path)

if (toLoad.isEmpty()) {
uiHandler.post {
listener.onIconsLoaded()
}
}
}
}


interface IconLoaderListener {
fun onIconsLoaded()
}
}

View File

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

import androidx.core.app.NotificationCompat


data class InviteNotifiableEvent(
override var matrixID: String?,
override val eventId: String,
var roomId: String,
override var noisy: Boolean,
override val title: String,
override val description: String,
override val type: String?,
override val timestamp: Long,
override var soundName: String?,
override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {

override var hasBeenDisplayed: Boolean = false
override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC

}

View File

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

import java.io.Serializable

interface NotifiableEvent : Serializable {
var matrixID: String?
val eventId: String
var noisy: Boolean
val title: String
val description: String?
val type: String?
val timestamp: Long
//NotificationCompat.VISIBILITY_PUBLIC , VISIBILITY_PRIVATE , VISIBILITY_SECRET
var lockScreenVisibility: Int
// Compat: Only for android <7, for newer version the sound is defined in the channel
var soundName: String?
var hasBeenDisplayed: Boolean
//Used to know if event should be replaced with the one coming from eventstream
var isPushGatewayEvent: Boolean
}

View File

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

import android.content.Context
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotredesign.core.preference.BingRule

// TODO Remove
class RoomState {

}


/**
* The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event.
* It is used as a bridge between the Event Thread and the NotificationDrawerManager.
* The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that,
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
*/
class NotifiableEventResolver(val context: Context) {

//private val eventDisplay = RiotEventDisplay(context)

fun resolveEvent(event: Event, roomState: RoomState?, bingRule: BingRule?, session: Session): NotifiableEvent? {
// TODO
return null
/*
val store = session.dataHandler.store
if (store == null) {
Log.e("## NotifiableEventResolver, unable to get store")
//TODO notify somehow that something did fail?
return null
}

when (event.type) {
EventType.MESSAGE -> {
return resolveMessageEvent(event, bingRule, session, store)
}
EventType.ENCRYPTED -> {
val messageEvent = resolveMessageEvent(event, bingRule, session, store)
messageEvent?.lockScreenVisibility = NotificationCompat.VISIBILITY_PRIVATE
return messageEvent
}
EventType.STATE_ROOM_MEMBER -> {
return resolveStateRoomEvent(event, bingRule, session, store)
}
else -> {

//If the event can be displayed, display it as is
eventDisplay.getTextualDisplay(event, roomState)?.toString()?.let { body ->
return SimpleNotifiableEvent(
session.myUserId,
eventId = event.eventId,
noisy = bingRule?.notificationSound != null,
timestamp = event.originServerTs,
description = body,
soundName = bingRule?.notificationSound,
title = context.getString(R.string.notification_unknown_new_event),
type = event.type)
}

//Unsupported event
Timber.w("NotifiableEventResolver Received an unsupported event matching a bing rule")
return null
}
}
*/
}

/*
private fun resolveMessageEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? {
//If we are here, that means that the event should be notified to the user, we check now how it should be presented (sound)
val soundName = bingRule?.notificationSound
val noisy = bingRule?.notificationSound != null

//The event only contains an eventId, and roomId (type is m.room.*) , we need to get the displayable content (names, avatar, text, etc...)
val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/)

if (room == null) {
Timber.e("## Unable to resolve room for eventId [${event.eventId}] and roomID [${event.roomId}]")
// Ok room is not known in store, but we can still display something
val body = eventDisplay.getTextualDisplay(event, null)?.toString()
?: context.getString(R.string.notification_unknown_new_event)
val roomName = context.getString(R.string.notification_unknown_room_name)
val senderDisplayName = event.sender ?: ""

val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId,
timestamp = event.originServerTs,
noisy = noisy,
senderName = senderDisplayName,
senderId = event.sender,
body = body,
roomId = event.roomId,
roomName = roomName)

notifiableEvent.matrixID = session.myUserId
notifiableEvent.soundName = soundName

return notifiableEvent
} else {

val body = eventDisplay.getTextualDisplay(event, room.state)?.toString()
?: context.getString(R.string.notification_unknown_new_event)
val roomName = room.getRoomDisplayName(context)
val senderDisplayName = room.state.getMemberName(event.sender) ?: event.sender ?: ""

val notifiableEvent = NotifiableMessageEvent(
eventId = event.eventId,
timestamp = event.originServerTs,
noisy = noisy,
senderName = senderDisplayName,
senderId = event.sender,
body = body,
roomId = event.roomId,
roomName = roomName,
roomIsDirect = room.isDirect)

notifiableEvent.matrixID = session.myUserId
notifiableEvent.soundName = soundName


val roomAvatarPath = session.mediaCache?.thumbnailCacheFile(room.avatarUrl, 50)
if (roomAvatarPath != null) {
notifiableEvent.roomAvatarPath = roomAvatarPath.path
} else {
// prepare for the next time
session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), room.avatarUrl, 50)
}

room.state.getMember(event.sender)?.avatarUrl?.let {
val size = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)
val userAvatarUrlPath = session.mediaCache?.thumbnailCacheFile(it, size)
if (userAvatarUrlPath != null) {
notifiableEvent.senderAvatarPath = userAvatarUrlPath.path
} else {
// prepare for the next time
session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), it, size)
}
}

return notifiableEvent
}
}

private fun resolveStateRoomEvent(event: Event, bingRule: BingRule?, session: MXSession, store: IMXStore): NotifiableEvent? {
if (RoomMember.MEMBERSHIP_INVITE == event.contentAsJsonObject?.getAsJsonPrimitive("membership")?.asString) {
val room = store.getRoom(event.roomId /*roomID cannot be null (see Matrix SDK code)*/)
val body = eventDisplay.getTextualDisplay(event, room.state)?.toString()
?: context.getString(R.string.notification_new_invitation)
return InviteNotifiableEvent(
session.myUserId,
eventId = event.eventId,
roomId = event.roomId,
timestamp = event.originServerTs,
noisy = bingRule?.notificationSound != null,
title = context.getString(R.string.notification_new_invitation),
description = body,
soundName = bingRule?.notificationSound,
type = event.type,
isPushGatewayEvent = false)
} else {
Timber.e("## unsupported notifiable event for eventId [${event.eventId}]")
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.e("## unsupported notifiable event for event [${event}]")
}
//TODO generic handling?
}
return null
} */
}

View File

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

import androidx.core.app.NotificationCompat
import im.vector.matrix.android.api.session.events.model.EventType

data class NotifiableMessageEvent(
override val eventId: String,
override var noisy: Boolean,
override val timestamp: Long,
var senderName: String?,
var senderId: String?,
var body: String?,
var roomId: String,
var roomName: String?,
var roomIsDirect: Boolean = false
) : NotifiableEvent {


override var matrixID: String? = null
override var soundName: String? = null
override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
override var hasBeenDisplayed: Boolean = false

var roomAvatarPath: String? = null
var senderAvatarPath: String? = null

override var isPushGatewayEvent: Boolean = false

override val type: String
get() = EventType.MESSAGE

override val description: String?
get() = body ?: ""

override val title: String
get() = senderName ?: ""

//This is used for >N notification, as the result of a smart reply
var outGoingMessage = false
var outGoingMessageFailed = false

}

View File

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

package im.vector.riotredesign.features.notifications

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.app.RemoteInput
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.Room
import org.koin.standalone.KoinComponent
import org.koin.standalone.inject
import timber.log.Timber

/**
* Receives actions broadcast by notification (on click, on dismiss, inline replies, etc.)
*/
class NotificationBroadcastReceiver : BroadcastReceiver(), KoinComponent {

private val notificationDrawerManager by inject<NotificationDrawerManager>()

override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null || context == null) return

Timber.d("ReplyNotificationBroadcastReceiver received : $intent")

when (intent.action) {
NotificationUtils.SMART_REPLY_ACTION ->
handleSmartReply(intent, context)
NotificationUtils.DISMISS_ROOM_NOTIF_ACTION ->
intent.getStringExtra(KEY_ROOM_ID)?.let {
notificationDrawerManager.clearMessageEventOfRoom(it)
}
NotificationUtils.DISMISS_SUMMARY_ACTION ->
notificationDrawerManager.clearAllEvents()
NotificationUtils.MARK_ROOM_READ_ACTION ->
intent.getStringExtra(KEY_ROOM_ID)?.let {
notificationDrawerManager.clearMessageEventOfRoom(it)
handleMarkAsRead(context, it)
}
}
}

private fun handleMarkAsRead(context: Context, roomId: String) {
/*
TODO
Matrix.getInstance(context)?.defaultSession?.let { session ->
session.dataHandler
?.getRoom(roomId)
?.markAllAsRead(object : SimpleApiCallback<Void>() {
override fun onSuccess(void: Void?) {
// Ignore
}
})
}
*/
}

private fun handleSmartReply(intent: Intent, context: Context) {
/*
TODO
val message = getReplyMessage(intent)
val roomId = intent.getStringExtra(KEY_ROOM_ID)

if (TextUtils.isEmpty(message) || TextUtils.isEmpty(roomId)) {
//ignore this event
//Can this happen? should we update notification?
return
}
val matrixId = intent.getStringExtra(EXTRA_MATRIX_ID)
Matrix.getInstance(context)?.getSession(matrixId)?.let { session ->
session.dataHandler?.getRoom(roomId)?.let { room ->
sendMatrixEvent(message!!, session, roomId!!, room, context)
}
}
*/
}

private fun sendMatrixEvent(message: String, session: Session, roomId: String, room: Room, context: Context?) {
/*
TODO

val mxMessage = Message()
mxMessage.msgtype = Message.MSGTYPE_TEXT
mxMessage.body = message

val event = Event(mxMessage, session.credentials.userId, roomId)
room.storeOutgoingEvent(event)
room.sendEvent(event, object : ApiCallback<Void?> {
override fun onSuccess(info: Void?) {
Timber.d("Send message : onSuccess ")
val notifiableMessageEvent = NotifiableMessageEvent(
event.eventId,
false,
System.currentTimeMillis(),
session.myUser?.displayname
?: context?.getString(R.string.notification_sender_me),
session.myUserId,
message,
roomId,
room.getRoomDisplayName(context),
room.isDirect)
notifiableMessageEvent.outGoingMessage = true
VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null)
}

override fun onNetworkError(e: Exception) {
Timber.d("Send message : onNetworkError " + e.message, e)
onSmartReplyFailed(e.localizedMessage)
}

override fun onMatrixError(e: MatrixError) {
Timber.d("Send message : onMatrixError " + e.message)
if (e is MXCryptoError) {
Toast.makeText(context, e.detailedErrorDescription, Toast.LENGTH_SHORT).show()
onSmartReplyFailed(e.detailedErrorDescription)
} else {
Toast.makeText(context, e.localizedMessage, Toast.LENGTH_SHORT).show()
onSmartReplyFailed(e.localizedMessage)
}
}

override fun onUnexpectedError(e: Exception) {
Timber.e(e, "Send message : onUnexpectedError " + e.message)
onSmartReplyFailed(e.message)
}


fun onSmartReplyFailed(reason: String?) {
val notifiableMessageEvent = NotifiableMessageEvent(
event.eventId,
false,
System.currentTimeMillis(),
session.myUser?.displayname
?: context?.getString(R.string.notification_sender_me),
session.myUserId,
message,
roomId,
room.getRoomDisplayName(context),
room.isDirect)
notifiableMessageEvent.outGoingMessage = true
notifiableMessageEvent.outGoingMessageFailed = true

VectorApp.getInstance().notificationDrawerManager.onNotifiableEventReceived(notifiableMessageEvent)
VectorApp.getInstance().notificationDrawerManager.refreshNotificationDrawer(null)
}
})
*/
}


private fun getReplyMessage(intent: Intent?): String? {
if (intent != null) {
val remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
return remoteInput.getCharSequence(KEY_TEXT_REPLY)?.toString()
}
}
return null
}

companion object {
const val KEY_ROOM_ID = "roomID"
const val KEY_TEXT_REPLY = "key_text_reply"
const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"
}
}

View File

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

import android.app.Notification
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.text.TextUtils
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.SecretStoringUtils
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream

/**
* The NotificationDrawerManager receives notification events as they arrived (from event stream or fcm) and
* organise them in order to display them in the notification drawer.
* Events can be grouped into the same notification, old (already read) events can be removed to do some cleaning.
*/
class NotificationDrawerManager(val context: Context) {

//The first time the notification drawer is refreshed, we force re-render of all notifications
private var firstTime = true

private var eventList = loadEventInfo()
private var myUserDisplayName: String = ""
private var myUserAvatarUrl: String = ""

private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size)

private var currentRoomId: String? = null

private var iconLoader = IconLoader(context,
object : IconLoader.IconLoaderListener {
override fun onIconsLoaded() {
// Force refresh
refreshNotificationDrawer(null)
}
})

/**
* No multi session support for now
*/
private fun initWithSession(session: Session?) {
session?.let {
/*
myUserDisplayName = it.myUser?.displayname ?: it.myUserId

// User Avatar
it.myUser?.avatarUrl?.let { avatarUrl ->
val userAvatarUrlPath = it.mediaCache?.thumbnailCacheFile(avatarUrl, avatarSize)
if (userAvatarUrlPath != null) {
myUserAvatarUrl = userAvatarUrlPath.path
} else {
// prepare for the next time
session.mediaCache?.loadAvatarThumbnail(session.homeServerConfig, ImageView(context), avatarUrl, avatarSize)
}
}
*/
}
}

/**
Should be called as soon as a new event is ready to be displayed.
The notification corresponding to this event will not be displayed until
#refreshNotificationDrawer() is called.
Events might be grouped and there might not be one notification per event!
*/
fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) {
//If we support multi session, event list should be per userId
//Currently only manage single session
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
Timber.d("%%%%%%%% onNotifiableEventReceived $notifiableEvent")
}
synchronized(eventList) {
val existing = eventList.firstOrNull { it.eventId == notifiableEvent.eventId }
if (existing != null) {
if (existing.isPushGatewayEvent) {
//Use the event coming from the event stream as it may contains more info than
//the fcm one (like type/content/clear text)
// In this case the message has already been notified, and might have done some noise
// So we want the notification to be updated even if it has already been displayed
// But it should make no noise (e.g when an encrypted message from FCM should be
// update with clear text after a sync)
notifiableEvent.hasBeenDisplayed = false
notifiableEvent.noisy = false
eventList.remove(existing)
eventList.add(notifiableEvent)

} else {
//keep the existing one, do not replace
}
} else {
eventList.add(notifiableEvent)
}

}
}

/**
Clear all known events and refresh the notification drawer
*/
fun clearAllEvents() {
synchronized(eventList) {
eventList.clear()
}
refreshNotificationDrawer(null)
}

/** Clear all known message events for this room and refresh the notification drawer */
fun clearMessageEventOfRoom(roomId: String?) {
Timber.d("clearMessageEventOfRoom $roomId")

if (roomId != null) {
eventList.removeAll { e ->
if (e is NotifiableMessageEvent) {
return@removeAll e.roomId == roomId
}
return@removeAll false
}
NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID)
}
refreshNotificationDrawer(null)
}

/**
Should be called when the application is currently opened and showing timeline for the given roomId.
Used to ignore events related to that room (no need to display notification) and clean any existing notification on this room.
*/
fun setCurrentRoom(roomId: String?) {
var hasChanged: Boolean
synchronized(eventList) {
hasChanged = roomId != currentRoomId
currentRoomId = roomId
}
if (hasChanged) {
clearMessageEventOfRoom(roomId)
}
}

fun homeActivityDidResume(matrixID: String?) {
synchronized(eventList) {
eventList.removeAll { e ->
return@removeAll e !is NotifiableMessageEvent //messages are cleared when entering room
}
}
}

fun clearMemberShipNotificationForRoom(roomId: String) {
synchronized(eventList) {
eventList.removeAll { e ->
if (e is InviteNotifiableEvent) {
return@removeAll e.roomId == roomId
}
return@removeAll false
}
}
}


fun refreshNotificationDrawer(outdatedDetector: OutdatedEventDetector?) {
if (myUserDisplayName.isBlank()) {
// TODO
// initWithSession(Matrix.getInstance(context).defaultSession)
}

if (myUserDisplayName.isBlank()) {
// Should not happen, but myUserDisplayName cannot be blank if used to create a Person
return
}

synchronized(eventList) {

Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER ")
//TMP code
var hasNewEvent = false
var summaryIsNoisy = false
val summaryInboxStyle = NotificationCompat.InboxStyle()

//group events by room to create a single MessagingStyle notif
val roomIdToEventMap: MutableMap<String, ArrayList<NotifiableMessageEvent>> = HashMap()
val simpleEvents: ArrayList<NotifiableEvent> = ArrayList()
val notifications: ArrayList<Notification> = ArrayList()

val eventIterator = eventList.listIterator()
while (eventIterator.hasNext()) {
val event = eventIterator.next()
if (event is NotifiableMessageEvent) {
val roomId = event.roomId
var roomEvents = roomIdToEventMap[roomId]
if (roomEvents == null) {
roomEvents = ArrayList()
roomIdToEventMap[roomId] = roomEvents
}

if (shouldIgnoreMessageEventInRoom(roomId) || outdatedDetector?.isMessageOutdated(event) == true) {
//forget this event
eventIterator.remove()
} else {
roomEvents.add(event)
}
} else {
simpleEvents.add(event)
}
}


Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER ${roomIdToEventMap.size} room groups")

var globalLastMessageTimestamp = 0L

//events have been grouped
for ((roomId, events) in roomIdToEventMap) {

if (events.isEmpty()) {
//Just clear this notification
Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId has no more events")
NotificationUtils.cancelNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID)
continue
}

val roomGroup = RoomEventGroupInfo(roomId)
roomGroup.hasNewEvent = false
roomGroup.shouldBing = false
roomGroup.isDirect = events[0].roomIsDirect
val roomName = events[0].roomName ?: events[0].senderName ?: ""
val style = NotificationCompat.MessagingStyle(Person.Builder()
.setName(myUserDisplayName)
.setIcon(iconLoader.getUserIcon(myUserAvatarUrl))
.setKey(events[0].matrixID)
.build())
roomGroup.roomDisplayName = roomName

style.isGroupConversation = !roomGroup.isDirect

if (!roomGroup.isDirect) {
style.conversationTitle = roomName
}

val largeBitmap = getRoomBitmap(events)


for (event in events) {
//if all events in this room have already been displayed there is no need to update it
if (!event.hasBeenDisplayed) {
roomGroup.shouldBing = roomGroup.shouldBing || event.noisy
roomGroup.customSound = event.soundName
}
roomGroup.hasNewEvent = roomGroup.hasNewEvent || !event.hasBeenDisplayed

val senderPerson = Person.Builder()
.setName(event.senderName)
.setIcon(iconLoader.getUserIcon(event.senderAvatarPath))
.setKey(event.senderId)
.build()

if (event.outGoingMessage && event.outGoingMessageFailed) {
style.addMessage(context.getString(R.string.notification_inline_reply_failed), event.timestamp, senderPerson)
roomGroup.hasSmartReplyError = true
} else {
style.addMessage(event.body, event.timestamp, senderPerson)
}
event.hasBeenDisplayed = true //we can consider it as displayed

//It is possible that this event was previously shown as an 'anonymous' simple notif.
//And now it will be merged in a single MessageStyle notif, so we can clean to be sure
NotificationUtils.cancelNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID)
}

try {
val summaryLine = context.resources.getQuantityString(
R.plurals.notification_compat_summary_line_for_room, events.size, roomName, events.size)
summaryInboxStyle.addLine(summaryLine)
} catch (e: Throwable) {
//String not found or bad format
Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER failed to resolve string")
summaryInboxStyle.addLine(roomName)
}

if (firstTime || roomGroup.hasNewEvent) {
//Should update displayed notification
Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId need refresh")
val lastMessageTimestamp = events.last().timestamp

if (globalLastMessageTimestamp < lastMessageTimestamp) {
globalLastMessageTimestamp = lastMessageTimestamp
}

NotificationUtils.buildMessagesListNotification(context, style, roomGroup, largeBitmap, lastMessageTimestamp, myUserDisplayName)
?.let {
//is there an id for this room?
notifications.add(it)
NotificationUtils.showNotificationMessage(context, roomId, ROOM_MESSAGES_NOTIFICATION_ID, it)
}
hasNewEvent = true
summaryIsNoisy = summaryIsNoisy || roomGroup.shouldBing
} else {
Timber.d("%%%%%%%% REFRESH NOTIFICATION DRAWER $roomId is up to date")
}
}


//Handle simple events
for (event in simpleEvents) {
//We build a simple event
if (firstTime || !event.hasBeenDisplayed) {
NotificationUtils.buildSimpleEventNotification(context, event, null, myUserDisplayName)?.let {
notifications.add(it)
NotificationUtils.showNotificationMessage(context, event.eventId, ROOM_EVENT_NOTIFICATION_ID, it)
event.hasBeenDisplayed = true //we can consider it as displayed
hasNewEvent = true
summaryIsNoisy = summaryIsNoisy || event.noisy
summaryInboxStyle.addLine(event.description)
}
}
}


//======== Build summary notification =========
//On Android 7.0 (API level 24) and higher, the system automatically builds a summary for
// your group using snippets of text from each notification. The user can expand this
// notification to see each separate notification.
// To support older versions, which cannot show a nested group of notifications,
// you must create an extra notification that acts as the summary.
// This appears as the only notification and the system hides all the others.
// So this summary should include a snippet from all the other notifications,
// which the user can tap to open your app.
// The behavior of the group summary may vary on some device types such as wearables.
// To ensure the best experience on all devices and versions, always include a group summary when you create a group
// https://developer.android.com/training/notify-user/group

if (eventList.isEmpty()) {
NotificationUtils.cancelNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID)
} else {
val nbEvents = roomIdToEventMap.size + simpleEvents.size
val sumTitle = context.resources.getQuantityString(
R.plurals.notification_compat_summary_title, nbEvents, nbEvents)
summaryInboxStyle.setBigContentTitle(sumTitle)
NotificationUtils.buildSummaryListNotification(
context,
summaryInboxStyle,
sumTitle,
noisy = hasNewEvent && summaryIsNoisy,
lastMessageTimestamp = globalLastMessageTimestamp
)?.let {
NotificationUtils.showNotificationMessage(context, null, SUMMARY_NOTIFICATION_ID, it)
}

if (hasNewEvent && summaryIsNoisy) {
try {
// turn the screen on for 3 seconds
/*
TODO
if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
val pm = VectorApp.getInstance().getSystemService(Context.POWER_SERVICE) as PowerManager
val wl = pm.newWakeLock(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
NotificationDrawerManager::class.java.name)
wl.acquire(3000)
wl.release()
}
*/
} catch (e: Throwable) {
Timber.e(e, "## Failed to turn screen on")
}

}
}
//notice that we can get bit out of sync with actual display but not a big issue
firstTime = false
}
}

private fun getRoomBitmap(events: ArrayList<NotifiableMessageEvent>): Bitmap? {
if (events.isEmpty()) return null

//Use the last event (most recent?)
val roomAvatarPath = events[events.size - 1].roomAvatarPath
?: events[events.size - 1].senderAvatarPath
if (!TextUtils.isEmpty(roomAvatarPath)) {
val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.ARGB_8888
try {
return BitmapFactory.decodeFile(roomAvatarPath, options)
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "decodeFile failed with an oom")
}

}
return null
}

private fun shouldIgnoreMessageEventInRoom(roomId: String?): Boolean {
return currentRoomId != null && roomId == currentRoomId
}


fun persistInfo() {
if (eventList.isEmpty()) {
deleteCachedRoomNotifications(context)
return
}
try {
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (!file.exists()) file.createNewFile()
FileOutputStream(file).use {
SecretStoringUtils.securelyStoreObject(eventList, "notificationMgr", it, this.context)
}
} catch (e: Throwable) {
Timber.e(e, "## Failed to save cached notification info")
}
}

private fun loadEventInfo(): ArrayList<NotifiableEvent> {
try {
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (file.exists()) {
FileInputStream(file).use {
val events: ArrayList<NotifiableEvent>? = SecretStoringUtils.loadSecureSecret(it, "notificationMgr", this.context)
if (events != null) {
return ArrayList(events.mapNotNull { it as? NotifiableEvent })
}
}
}
} catch (e: Throwable) {
Timber.e(e, "## Failed to load cached notification info")
}
return ArrayList()
}

private fun deleteCachedRoomNotifications(context: Context) {
val file = File(context.applicationContext.cacheDir, ROOMS_NOTIFICATIONS_FILE_NAME)
if (file.exists()) {
file.delete()
}
}

companion object {
private const val SUMMARY_NOTIFICATION_ID = 0
private const val ROOM_MESSAGES_NOTIFICATION_ID = 1
private const val ROOM_EVENT_NOTIFICATION_ID = 2

private const val ROOMS_NOTIFICATIONS_FILE_NAME = "im.vector.notifications.cache"
}
}

View File

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

package im.vector.riotredesign.features.notifications

import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.RemoteInput
import androidx.core.app.TaskStackBuilder
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import im.vector.riotredesign.BuildConfig
import im.vector.riotredesign.R
import im.vector.riotredesign.core.utils.startNotificationChannelSettingsIntent
import im.vector.riotredesign.features.home.HomeActivity
import im.vector.riotredesign.features.settings.PreferencesManager
import timber.log.Timber
import java.util.*


fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)

/**
* Util class for creating notifications.
*/
object NotificationUtils {

/* ==========================================================================================
* IDs for notifications
* ========================================================================================== */

/**
* Identifier of the foreground notification used to keep the application alive
* when it runs in background.
* This notification, which is not removable by the end user, displays what
* the application is doing while in background.
*/
const val NOTIFICATION_ID_FOREGROUND_SERVICE = 61

/* ==========================================================================================
* IDs for actions
* ========================================================================================== */

private const val JOIN_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.JOIN_ACTION"
private const val REJECT_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.REJECT_ACTION"
private const val QUICK_LAUNCH_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.QUICK_LAUNCH_ACTION"
const val MARK_ROOM_READ_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.MARK_ROOM_READ_ACTION"
const val SMART_REPLY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.SMART_REPLY_ACTION"
const val DISMISS_SUMMARY_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_SUMMARY_ACTION"
const val DISMISS_ROOM_NOTIF_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION"
private const val TAP_TO_VIEW_ACTION = "${BuildConfig.APPLICATION_ID}.NotificationActions.TAP_TO_VIEW_ACTION"

/* ==========================================================================================
* IDs for channels
* ========================================================================================== */

// on devices >= android O, we need to define a channel for each notifications
private const val LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID = "LISTEN_FOR_EVENTS_NOTIFICATION_CHANNEL_ID"

private const val NOISY_NOTIFICATION_CHANNEL_ID = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID"

private const val SILENT_NOTIFICATION_CHANNEL_ID = "DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID_V2"
private const val CALL_NOTIFICATION_CHANNEL_ID = "CALL_NOTIFICATION_CHANNEL_ID_V2"

/* ==========================================================================================
* Channel names
* ========================================================================================== */

/**
* Create notification channels.
*
* @param context the context
*/
@TargetApi(Build.VERSION_CODES.O)
fun createNotificationChannels(context: Context) {
if (!supportNotificationChannels()) {
return
}

val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)

//Migration - the noisy channel was deleted and recreated when sound preference was changed (id was DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE
// + currentTimeMillis).
//Now the sound can only be change directly in system settings, so for app upgrading we are deleting this former channel
//Starting from this version the channel will not be dynamic
for (channel in notificationManager.notificationChannels) {
val channelId = channel.id
val legacyBaseName = "DEFAULT_NOISY_NOTIFICATION_CHANNEL_ID_BASE"
if (channelId.startsWith(legacyBaseName)) {
notificationManager.deleteNotificationChannel(channelId)
}
}
//Migration - Remove deprecated channels
for (channelId in listOf("DEFAULT_SILENT_NOTIFICATION_CHANNEL_ID", "CALL_NOTIFICATION_CHANNEL_ID")) {
notificationManager.getNotificationChannel(channelId)?.let {
notificationManager.deleteNotificationChannel(channelId)
}
}

/**
* Default notification importance: shows everywhere, makes noise, but does not visually
* intrude.
*/
notificationManager.createNotificationChannel(NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_noisy_notifications),
NotificationManager.IMPORTANCE_DEFAULT)
.apply {
description = context.getString(R.string.notification_noisy_notifications)
enableVibration(true)
enableLights(true)
lightColor = accentColor
})

/**
* Low notification importance: shows everywhere, but is not intrusive.
*/
notificationManager.createNotificationChannel(NotificationChannel(SILENT_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_silent_notifications),
NotificationManager.IMPORTANCE_LOW)
.apply {
description = context.getString(R.string.notification_silent_notifications)
setSound(null, null)
enableLights(true)
lightColor = accentColor
})

notificationManager.createNotificationChannel(NotificationChannel(LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.notification_listening_for_events),
NotificationManager.IMPORTANCE_MIN)
.apply {
description = context.getString(R.string.notification_listening_for_events)
setSound(null, null)
setShowBadge(false)
})

notificationManager.createNotificationChannel(NotificationChannel(CALL_NOTIFICATION_CHANNEL_ID,
context.getString(R.string.call),
NotificationManager.IMPORTANCE_HIGH)
.apply {
description = context.getString(R.string.call)
setSound(null, null)
enableLights(true)
lightColor = accentColor
})
}

/**
* Build a polling thread listener notification
*
* @param context Android context
* @param subTitleResId subtitle string resource Id of the notification
* @return the polling thread listener notification
*/
@SuppressLint("NewApi")
fun buildForegroundServiceNotification(context: Context, @StringRes subTitleResId: Int): Notification {
// build the pending intent go to the home screen if this is clicked.
val i = Intent(context, HomeActivity::class.java)
i.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
val pi = PendingIntent.getActivity(context, 0, i, 0)

val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)

val builder = NotificationCompat.Builder(context, LISTENING_FOR_EVENTS_NOTIFICATION_CHANNEL_ID)
.setContentTitle(context.getString(subTitleResId))
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.setSmallIcon(R.drawable.logo_transparent)
.setProgress(0, 0, true)
.setColor(accentColor)
.setContentIntent(pi)

// hide the notification from the status bar
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
builder.priority = NotificationCompat.PRIORITY_MIN
}

val notification = builder.build()

notification.flags = notification.flags or Notification.FLAG_NO_CLEAR

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// some devices crash if this field is not set
// even if it is deprecated

// setLatestEventInfo() is deprecated on Android M, so we try to use
// reflection at runtime, to avoid compiler error: "Cannot resolve method.."
try {
val deprecatedMethod = notification.javaClass
.getMethod("setLatestEventInfo",
Context::class.java,
CharSequence::class.java,
CharSequence::class.java,
PendingIntent::class.java)
deprecatedMethod.invoke(notification, context, context.getString(R.string.app_name), context.getString(subTitleResId), pi)
} catch (ex: Exception) {
Timber.e(ex, "## buildNotification(): Exception - setLatestEventInfo() Msg=" + ex.message)
}

}
return notification
}

/**
* Build an incoming call notification.
* This notification starts the VectorHomeActivity which is in charge of centralizing the incoming call flow.
*
* @param context the context.
* @param isVideo true if this is a video call, false for voice call
* @param roomName the room name in which the call is pending.
* @param matrixId the matrix id
* @param callId the call id.
* @return the call notification.
*/
@SuppressLint("NewApi")
fun buildIncomingCallNotification(context: Context,
isVideo: Boolean,
roomName: String,
matrixId: String,
callId: String): Notification {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)

val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
.setContentTitle(ensureTitleNotEmpty(context, roomName))
.apply {
if (isVideo) {
setContentText(context.getString(R.string.incoming_video_call))
} else {
setContentText(context.getString(R.string.incoming_voice_call))
}
}
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
.setCategory(NotificationCompat.CATEGORY_CALL)
.setLights(accentColor, 500, 500)

//Compat: Display the incoming call notification on the lock screen
builder.priority = NotificationCompat.PRIORITY_MAX

// clear the activity stack to home activity
val intent = Intent(context, HomeActivity::class.java)
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
// TODO .putExtra(VectorHomeActivity.EXTRA_CALL_SESSION_ID, matrixId)
// TODO .putExtra(VectorHomeActivity.EXTRA_CALL_ID, callId)

// Recreate the back stack
val stackBuilder = TaskStackBuilder.create(context)
.addParentStack(HomeActivity::class.java)
.addNextIntent(intent)


// android 4.3 issue
// use a generator for the private requestCode.
// When using 0, the intent is not created/launched when the user taps on the notification.
//
val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)

builder.setContentIntent(pendingIntent)

return builder.build()
}

/**
* Build a pending call notification
*
* @param context the context.
* @param isVideo true if this is a video call, false for voice call
* @param roomName the room name in which the call is pending.
* @param roomId the room Id
* @param matrixId the matrix id
* @param callId the call id.
* @return the call notification.
*/
@SuppressLint("NewApi")
fun buildPendingCallNotification(context: Context,
isVideo: Boolean,
roomName: String,
roomId: String,
matrixId: String,
callId: String): Notification {

val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
.setContentTitle(ensureTitleNotEmpty(context, roomName))
.apply {
if (isVideo) {
setContentText(context.getString(R.string.video_call_in_progress))
} else {
setContentText(context.getString(R.string.call_in_progress))
}
}
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
.setCategory(NotificationCompat.CATEGORY_CALL)

// Display the pending call notification on the lock screen
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
builder.priority = NotificationCompat.PRIORITY_MAX
}

/* TODO
// Build the pending intent for when the notification is clicked
val roomIntent = Intent(context, VectorRoomActivity::class.java)
.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId)
.putExtra(VectorRoomActivity.EXTRA_MATRIX_ID, matrixId)
.putExtra(VectorRoomActivity.EXTRA_START_CALL_ID, callId)

// Recreate the back stack
val stackBuilder = TaskStackBuilder.create(context)
.addParentStack(VectorRoomActivity::class.java)
.addNextIntent(roomIntent)

// android 4.3 issue
// use a generator for the private requestCode.
// When using 0, the intent is not created/launched when the user taps on the notification.
//
val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)

builder.setContentIntent(pendingIntent)
*/

return builder.build()
}

/**
* Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended
*/
fun buildCallEndedNotification(context: Context): Notification {
return NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
.setContentTitle(context.getString(R.string.call_ended))
.setSmallIcon(R.drawable.ic_material_call_end_grey)
.setCategory(NotificationCompat.CATEGORY_CALL)
.build()
}

/**
* Build a notification for a Room
*/
fun buildMessagesListNotification(context: Context,
messageStyle: NotificationCompat.MessagingStyle,
roomInfo: RoomEventGroupInfo,
largeIcon: Bitmap?,
lastMessageTimestamp: Long,
senderDisplayNameForReplyCompat: String?): Notification? {

val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
// Build the pending intent for when the notification is clicked
val openRoomIntent = buildOpenRoomIntent(context, roomInfo.roomId)
val smallIcon = if (roomInfo.shouldBing) R.drawable.icon_notif_important else R.drawable.logo_transparent

val channelID = if (roomInfo.shouldBing) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID
return NotificationCompat.Builder(context, channelID)
.setWhen(lastMessageTimestamp)
// MESSAGING_STYLE sets title and content for API 16 and above devices.
.setStyle(messageStyle)

// A category allows groups of notifications to be ranked and filtered per user or system settings.
// For example, alarm notifications should display before promo notifications, or message from known contact
// that can be displayed in not disturb mode if white listed (the later will need compat28.x)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)

// Title for API < 16 devices.
.setContentTitle(roomInfo.roomDisplayName)
// Content for API < 16 devices.
.setContentText(context.getString(R.string.notification_new_messages))

// Number of new notifications for API <24 (M and below) devices.
.setSubText(context
.resources
.getQuantityString(R.plurals.room_new_messages_notification, messageStyle.messages.size, messageStyle.messages.size)
)

// Auto-bundling is enabled for 4 or more notifications on API 24+ (N+)
// devices and all Wear devices. But we want a custom grouping, so we specify the groupID
// TODO Group should be current user display name
.setGroup(context.getString(R.string.app_name))

//In order to avoid notification making sound twice (due to the summary notification)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)

.setSmallIcon(smallIcon)

// Set primary color (important for Wear 2.0 Notifications).
.setColor(accentColor)

// Sets priority for 25 and below. For 26 and above, 'priority' is deprecated for
// 'importance' which is set in the NotificationChannel. The integers representing
// 'priority' are different from 'importance', so make sure you don't mix them.
.apply {
priority = NotificationCompat.PRIORITY_DEFAULT
if (roomInfo.shouldBing) {
//Compat
PreferencesManager.getNotificationRingTone(context)?.let {
setSound(it)
}
setLights(accentColor, 500, 500)
} else {
priority = NotificationCompat.PRIORITY_LOW
}

//Add actions and notification intents
// Mark room as read
val markRoomReadIntent = Intent(context, NotificationBroadcastReceiver::class.java)
markRoomReadIntent.action = MARK_ROOM_READ_ACTION
markRoomReadIntent.data = Uri.parse("foobar://${roomInfo.roomId}")
markRoomReadIntent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
val markRoomReadPendingIntent = PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), markRoomReadIntent,
PendingIntent.FLAG_UPDATE_CURRENT)

addAction(NotificationCompat.Action(
R.drawable.ic_material_done_all_white,
context.getString(R.string.action_mark_room_read),
markRoomReadPendingIntent))

// Quick reply
if (!roomInfo.hasSmartReplyError) {
buildQuickReplyIntent(context, roomInfo.roomId, senderDisplayNameForReplyCompat)?.let { replyPendingIntent ->
val remoteInput = RemoteInput.Builder(NotificationBroadcastReceiver.KEY_TEXT_REPLY)
.setLabel(context.getString(R.string.action_quick_reply))
.build()
NotificationCompat.Action.Builder(R.drawable.vector_notification_quick_reply,
context.getString(R.string.action_quick_reply), replyPendingIntent)
.addRemoteInput(remoteInput)
.build()?.let {
addAction(it)
}
}
}

if (openRoomIntent != null) {
setContentIntent(openRoomIntent)
}

if (largeIcon != null) {
setLargeIcon(largeIcon)
}

val intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomInfo.roomId)
intent.action = DISMISS_ROOM_NOTIF_ACTION
val pendingIntent = PendingIntent.getBroadcast(context.applicationContext,
System.currentTimeMillis().toInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT)
setDeleteIntent(pendingIntent)
}
.build()
}


fun buildSimpleEventNotification(context: Context, simpleNotifiableEvent: NotifiableEvent, largeIcon: Bitmap?, matrixId: String): Notification? {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
// Build the pending intent for when the notification is clicked
val smallIcon = if (simpleNotifiableEvent.noisy) R.drawable.icon_notif_important else R.drawable.logo_transparent

val channelID = if (simpleNotifiableEvent.noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID

return NotificationCompat.Builder(context, channelID)
.setContentTitle(context.getString(R.string.app_name))
.setContentText(simpleNotifiableEvent.description)
.setGroup(context.getString(R.string.app_name))
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
.setSmallIcon(smallIcon)
.setColor(accentColor)
.apply {
if (simpleNotifiableEvent is InviteNotifiableEvent) {
/*
TODO
val roomId = simpleNotifiableEvent.roomId
// offer to type a quick reject button
val rejectIntent = JoinRoomActivity.getRejectRoomIntent(context, roomId, matrixId)

// the action must be unique else the parameters are ignored
rejectIntent.action = REJECT_ACTION
rejectIntent.data = Uri.parse("foobar://$roomId&$matrixId")
addAction(
R.drawable.vector_notification_reject_invitation,
context.getString(R.string.reject),
PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), rejectIntent, 0))

// offer to type a quick accept button
val joinIntent = JoinRoomActivity.getJoinRoomIntent(context, roomId, matrixId)

// the action must be unique else the parameters are ignored
joinIntent.action = JOIN_ACTION
joinIntent.data = Uri.parse("foobar://$roomId&$matrixId")
addAction(
R.drawable.vector_notification_accept_invitation,
context.getString(R.string.join),
PendingIntent.getActivity(context, 0, joinIntent, 0))
*/
} else {
setAutoCancel(true)
}

val contentIntent = Intent(context, HomeActivity::class.java)
contentIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
//pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
contentIntent.data = Uri.parse("foobar://" + simpleNotifiableEvent.eventId)
setContentIntent(PendingIntent.getActivity(context, 0, contentIntent, 0))

if (largeIcon != null) {
setLargeIcon(largeIcon)
}

if (simpleNotifiableEvent.noisy) {
//Compat
priority = NotificationCompat.PRIORITY_DEFAULT
PreferencesManager.getNotificationRingTone(context)?.let {
setSound(it)
}
setLights(accentColor, 500, 500)
} else {
priority = NotificationCompat.PRIORITY_LOW
}
setAutoCancel(true)
}
.build()
}

private fun buildOpenRoomIntent(context: Context, roomId: String): PendingIntent? {
// TODO
return null
/*
val roomIntentTap = Intent(context, VectorRoomActivity::class.java)
roomIntentTap.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId)
roomIntentTap.action = TAP_TO_VIEW_ACTION
//pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that
roomIntentTap.data = Uri.parse("foobar://openRoom?$roomId")

// Recreate the back stack
return TaskStackBuilder.create(context)
.addNextIntentWithParentStack(Intent(context, VectorHomeActivity::class.java))
.addNextIntent(roomIntentTap)
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
*/
}

private fun buildOpenHomePendingIntentForSummary(context: Context): PendingIntent {
val intent = Intent(context, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
// TODO intent.putExtra(VectorHomeActivity.EXTRA_CLEAR_EXISTING_NOTIFICATION, true)
intent.data = Uri.parse("foobar://tapSummary")
return PendingIntent.getActivity(context, Random().nextInt(1000), intent, PendingIntent.FLAG_UPDATE_CURRENT)
}

/*
Direct reply is new in Android N, and Android already handles the UI, so the right pending intent
here will ideally be a Service/IntentService (for a long running background task) or a BroadcastReceiver,
which runs on the UI thread. It also works without unlocking, making the process really fluid for the user.
However, for Android devices running Marshmallow and below (API level 23 and below),
it will be more appropriate to use an activity. Since you have to provide your own UI.
*/
private fun buildQuickReplyIntent(context: Context, roomId: String, senderName: String?): PendingIntent? {
val intent: Intent
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = SMART_REPLY_ACTION
intent.data = Uri.parse("foobar://$roomId")
intent.putExtra(NotificationBroadcastReceiver.KEY_ROOM_ID, roomId)
return PendingIntent.getBroadcast(context, System.currentTimeMillis().toInt(), intent,
PendingIntent.FLAG_UPDATE_CURRENT)
} else {
/*
TODO
if (!LockScreenActivity.isDisplayingALockScreenActivity()) {
// start your activity for Android M and below
val quickReplyIntent = Intent(context, LockScreenActivity::class.java)
quickReplyIntent.putExtra(LockScreenActivity.EXTRA_ROOM_ID, roomId)
quickReplyIntent.putExtra(LockScreenActivity.EXTRA_SENDER_NAME, senderName ?: "")

// the action must be unique else the parameters are ignored
quickReplyIntent.action = QUICK_LAUNCH_ACTION
quickReplyIntent.data = Uri.parse("foobar://$roomId")
return PendingIntent.getActivity(context, 0, quickReplyIntent, 0)
}
*/
}
return null
}

//// Number of new notifications for API <24 (M and below) devices.
/**
* Build the summary notification
*/
fun buildSummaryListNotification(context: Context,
style: NotificationCompat.Style,
compatSummary: String,
noisy: Boolean,
lastMessageTimestamp: Long): Notification? {
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
val smallIcon = if (noisy) R.drawable.icon_notif_important else R.drawable.logo_transparent

return NotificationCompat.Builder(context, if (noisy) NOISY_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID)
// used in compat < N, after summary is built based on child notifications
.setWhen(lastMessageTimestamp)
.setStyle(style)
.setContentTitle(context.getString(R.string.app_name))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(smallIcon)
//set content text to support devices running API level < 24
.setContentText(compatSummary)
.setGroup(context.getString(R.string.app_name))
//set this notification as the summary for the group
.setGroupSummary(true)
.setColor(accentColor)
.apply {
if (noisy) {
//Compat
priority = NotificationCompat.PRIORITY_DEFAULT
PreferencesManager.getNotificationRingTone(context)?.let {
setSound(it)
}
setLights(accentColor, 500, 500)
} else {
//compat
priority = NotificationCompat.PRIORITY_LOW
}
}
.setContentIntent(buildOpenHomePendingIntentForSummary(context))
.setDeleteIntent(getDismissSummaryPendingIntent(context))
.build()

}

private fun getDismissSummaryPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, NotificationBroadcastReceiver::class.java)
intent.action = DISMISS_SUMMARY_ACTION
intent.data = Uri.parse("foobar://deleteSummary")
return PendingIntent.getBroadcast(context.applicationContext,
0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}

fun showNotificationMessage(context: Context, tag: String?, id: Int, notification: Notification) {
with(NotificationManagerCompat.from(context)) {
notify(tag, id, notification)
}
}

fun cancelNotificationMessage(context: Context, tag: String?, id: Int) {
NotificationManagerCompat.from(context)
.cancel(tag, id)
}

/**
* Cancel the foreground notification service
*/
fun cancelNotificationForegroundService(context: Context) {
NotificationManagerCompat.from(context)
.cancel(NOTIFICATION_ID_FOREGROUND_SERVICE)
}

/**
* Cancel all the notification
*/
fun cancelAllNotifications(context: Context) {
// Keep this try catch (reported by GA)
try {
NotificationManagerCompat.from(context)
.cancelAll()
} catch (e: Exception) {
Timber.e(e, "## cancelAllNotifications() failed " + e.message)
}
}

/**
* Return true it the user has enabled the do not disturb mode
*/
fun isDoNotDisturbModeOn(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return false
}

// We cannot use NotificationManagerCompat here.
val setting = (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).currentInterruptionFilter

return setting == NotificationManager.INTERRUPTION_FILTER_NONE
|| setting == NotificationManager.INTERRUPTION_FILTER_ALARMS
}

private fun ensureTitleNotEmpty(context: Context, title: String?): CharSequence {
if (TextUtils.isEmpty(title)) {
return context.getString(R.string.app_name)
}

return title!!
}

fun openSystemSettingsForSilentCategory(fragment: Fragment) {
startNotificationChannelSettingsIntent(fragment, SILENT_NOTIFICATION_CHANNEL_ID)
}

fun openSystemSettingsForNoisyCategory(fragment: Fragment) {
startNotificationChannelSettingsIntent(fragment, NOISY_NOTIFICATION_CHANNEL_ID)
}

fun openSystemSettingsForCallCategory(fragment: Fragment) {
startNotificationChannelSettingsIntent(fragment, CALL_NOTIFICATION_CHANNEL_ID)
}
}

View File

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

import android.content.Context

class OutdatedEventDetector(val context: Context) {

/**
* Returns true if the given event is outdated.
* Used to clean up notifications if a displayed message has been read on an
* other device.
*/
fun isMessageOutdated(notifiableEvent: NotifiableEvent): Boolean {
if (notifiableEvent is NotifiableMessageEvent) {
val eventID = notifiableEvent.eventId
val roomID = notifiableEvent.roomId
/*
TODO
Matrix.getMXSession(context.applicationContext, notifiableEvent.matrixID)?.let { session ->
//find the room
if (session.isAlive) {
session.dataHandler.getRoom(roomID)?.let { room ->
if (room.isEventRead(eventID)) {
Timber.d("Notifiable Event $eventID is read, and should be removed")
return true
}
}
}
}
*/
}
return false
}
}

View File

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

package im.vector.riotredesign.features.notifications

/**
* Data class to hold information about a group of notifications for a room
*/
data class RoomEventGroupInfo(
val roomId: String
) {
var roomDisplayName: String = ""
var roomAvatarPath: String? = null
//An event in the list has not yet been display
var hasNewEvent: Boolean = false
//true if at least one on the not yet displayed event is noisy
var shouldBing: Boolean = false
var customSound: String? = null
var hasSmartReplyError = false
var isDirect = false
}

View File

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

import androidx.core.app.NotificationCompat

data class SimpleNotifiableEvent(
override var matrixID: String?,
override val eventId: String,
override var noisy: Boolean,
override val title: String,
override val description: String,
override val type: String?,
override val timestamp: Long,
override var soundName: String?,
override var isPushGatewayEvent: Boolean = false) : NotifiableEvent {

override var hasBeenDisplayed: Boolean = false
override var lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC

}

View File

@ -0,0 +1,861 @@
/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.settings;

import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.media.RingtoneManager;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;

import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import androidx.annotation.Nullable;
import androidx.preference.PreferenceManager;
import im.vector.riotredesign.R;
import im.vector.riotredesign.features.homeserver.ServerUrlsRepository;
import im.vector.riotredesign.features.themes.ThemeUtils;
import timber.log.Timber;

public class PreferencesManager {

public static final String VERSION_BUILD = "VERSION_BUILD";

public static final String SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY = "SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY_2";
public static final String SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY = "SETTINGS_CHANGE_PASSWORD_PREFERENCE_KEY";
public static final String SETTINGS_VERSION_PREFERENCE_KEY = "SETTINGS_VERSION_PREFERENCE_KEY";
public static final String SETTINGS_OLM_VERSION_PREFERENCE_KEY = "SETTINGS_OLM_VERSION_PREFERENCE_KEY";
public static final String SETTINGS_LOGGED_IN_PREFERENCE_KEY = "SETTINGS_LOGGED_IN_PREFERENCE_KEY";
public static final String SETTINGS_HOME_SERVER_PREFERENCE_KEY = "SETTINGS_HOME_SERVER_PREFERENCE_KEY";
public static final String SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY = "SETTINGS_IDENTITY_SERVER_PREFERENCE_KEY";
public static final String SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY = "SETTINGS_APP_TERM_CONDITIONS_PREFERENCE_KEY";
public static final String SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY = "SETTINGS_PRIVACY_POLICY_PREFERENCE_KEY";
public static final String SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_PRIVACY_PREFERENCE_KEY";
public static final String SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY";
public static final String SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY";
public static final String SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY = "SETTINGS_THIRD_PARTY_NOTICES_PREFERENCE_KEY";
public static final String SETTINGS_COPYRIGHT_PREFERENCE_KEY = "SETTINGS_COPYRIGHT_PREFERENCE_KEY";
public static final String SETTINGS_CLEAR_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_CACHE_PREFERENCE_KEY";
public static final String SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY = "SETTINGS_CLEAR_MEDIA_CACHE_PREFERENCE_KEY";
public static final String SETTINGS_USER_SETTINGS_PREFERENCE_KEY = "SETTINGS_USER_SETTINGS_PREFERENCE_KEY";
public static final String SETTINGS_CONTACT_PREFERENCE_KEYS = "SETTINGS_CONTACT_PREFERENCE_KEYS";
public static final String SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGETS_PREFERENCE_KEY";
public static final String SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY = "SETTINGS_NOTIFICATIONS_TARGET_DIVIDER_PREFERENCE_KEY";
public static final String SETTINGS_IGNORED_USERS_PREFERENCE_KEY = "SETTINGS_IGNORED_USERS_PREFERENCE_KEY";
public static final String SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY = "SETTINGS_IGNORE_USERS_DIVIDER_PREFERENCE_KEY";
public static final String SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY";
public static final String SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_DIVIDER_PREFERENCE_KEY";
public static final String SETTINGS_LABS_PREFERENCE_KEY = "SETTINGS_LABS_PREFERENCE_KEY";
public static final String SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_PREFERENCE_KEY";
public static final String SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_DIVIDER_PREFERENCE_KEY";
public static final String SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY";
public static final String SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY = "SETTINGS_CRYPTOGRAPHY_MANAGE_DIVIDER_PREFERENCE_KEY";
public static final String SETTINGS_DEVICES_LIST_PREFERENCE_KEY = "SETTINGS_DEVICES_LIST_PREFERENCE_KEY";
public static final String SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY = "SETTINGS_DEVICES_DIVIDER_PREFERENCE_KEY";
public static final String SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY = "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY";
public static final String SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY
= "SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_IS_ACTIVE_PREFERENCE_KEY";
public static final String SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_NAME_PREFERENCE_KEY";
public static final String SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_ID_PREFERENCE_KEY";
public static final String SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_EXPORT_E2E_ROOM_KEYS_PREFERENCE_KEY";
public static final String SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_IMPORT_E2E_ROOM_KEYS_PREFERENCE_KEY";
public static final String SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_NEVER_SENT_TO_PREFERENCE_KEY";
public static final String SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY = "SETTINGS_ENCRYPTION_INFORMATION_DEVICE_KEY_PREFERENCE_KEY";

public static final String SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY";

// user
public static final String SETTINGS_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_DISPLAY_NAME_PREFERENCE_KEY";
public static final String SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY = "SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY";

// contacts
public static final String SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY = "SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY";

// interface
public static final String SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY = "SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY";
public static final String SETTINGS_INTERFACE_TEXT_SIZE_KEY = "SETTINGS_INTERFACE_TEXT_SIZE_KEY";
public static final String SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY";
private static final String SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY";
private static final String SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY";
private static final String SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY";
private static final String SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY";
private static final String SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY";
private static final String SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY = "SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY";
private static final String SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY = "SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY";
private static final String SETTINGS_VIBRATE_ON_MENTION_KEY = "SETTINGS_VIBRATE_ON_MENTION_KEY";
private static final String SETTINGS_SEND_MESSAGE_WITH_ENTER = "SETTINGS_SEND_MESSAGE_WITH_ENTER";

// home
private static final String SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY = "SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY";
private static final String SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY = "SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY";

// flair
public static final String SETTINGS_GROUPS_FLAIR_KEY = "SETTINGS_GROUPS_FLAIR_KEY";

// notifications
public static final String SETTINGS_NOTIFICATIONS_KEY = "SETTINGS_NOTIFICATIONS_KEY";
public static final String SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY";
public static final String SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY";
public static final String SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY = "SETTINGS_TURN_SCREEN_ON_PREFERENCE_KEY";
public static final String SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY";
public static final String SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY";
public static final String SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY = "SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY";
public static final String SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY";
public static final String SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY = "SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY";
public static final String SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY_2";
public static final String SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY = "SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY_2";
public static final String SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY_2";
public static final String SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY = "SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY_2";
public static final String SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY = "SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY_2";
public static final String SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY = "SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY_2";

// media
private static final String SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY = "SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY";
private static final String SETTINGS_DEFAULT_MEDIA_SOURCE_KEY = "SETTINGS_DEFAULT_MEDIA_SOURCE_KEY";
private static final String SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY = "SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY";
private static final String SETTINGS_PLAY_SHUTTER_SOUND_KEY = "SETTINGS_PLAY_SHUTTER_SOUND_KEY";

// background sync
public static final String SETTINGS_START_ON_BOOT_PREFERENCE_KEY = "SETTINGS_START_ON_BOOT_PREFERENCE_KEY";
public static final String SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY";
public static final String SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY";
public static final String SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY";

// Calls
public static final String SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_USE_RIOT_PREFERENCE_KEY";
public static final String SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY = "SETTINGS_CALL_RINGTONE_URI_PREFERENCE_KEY";

// labs
public static final String SETTINGS_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_LAZY_LOADING_PREFERENCE_KEY";
public static final String SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY = "SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY";
public static final String SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY = "SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY";
private static final String SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY = "SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY";
private static final String SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY = "SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY";
private static final String SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY = "SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY";

// analytics
public static final String SETTINGS_USE_ANALYTICS_KEY = "SETTINGS_USE_ANALYTICS_KEY";
public static final String SETTINGS_USE_RAGE_SHAKE_KEY = "SETTINGS_USE_RAGE_SHAKE_KEY";

// other
public static final String SETTINGS_MEDIA_SAVING_PERIOD_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_KEY";
private static final String SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY = "SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY";
private static final String DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY = "DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY";
private static final String DID_MIGRATE_TO_NOTIFICATION_REWORK = "DID_MIGRATE_TO_NOTIFICATION_REWORK";
private static final String DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY = "DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY";
public static final String SETTINGS_DEACTIVATE_ACCOUNT_KEY = "SETTINGS_DEACTIVATE_ACCOUNT_KEY";
private static final String SETTINGS_DISPLAY_ALL_EVENTS_KEY = "SETTINGS_DISPLAY_ALL_EVENTS_KEY";

private static final int MEDIA_SAVING_3_DAYS = 0;
private static final int MEDIA_SAVING_1_WEEK = 1;
private static final int MEDIA_SAVING_1_MONTH = 2;
private static final int MEDIA_SAVING_FOREVER = 3;

// some preferences keys must be kept after a logout
private static final List<String> mKeysToKeepAfterLogout = Arrays.asList(
SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY,
SETTINGS_DEFAULT_MEDIA_SOURCE_KEY,
SETTINGS_PLAY_SHUTTER_SOUND_KEY,

SETTINGS_SEND_TYPING_NOTIF_KEY,
SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY,
SETTINGS_12_24_TIMESTAMPS_KEY,
SETTINGS_SHOW_READ_RECEIPTS_KEY,
SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY,
SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY,
SETTINGS_MEDIA_SAVING_PERIOD_KEY,
SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY,
SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY,
SETTINGS_SEND_MESSAGE_WITH_ENTER,

SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY,
SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY,
// Do not keep SETTINGS_LAZY_LOADING_PREFERENCE_KEY because the user may log in on a server which does not support lazy loading
SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY,
SETTINGS_START_ON_BOOT_PREFERENCE_KEY,
SETTINGS_INTERFACE_TEXT_SIZE_KEY,
SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY,
SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY,
SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY,

SETTINGS_ROOM_SETTINGS_LABS_END_TO_END_PREFERENCE_KEY,
SETTINGS_CONTACTS_PHONEBOOK_COUNTRY_PREFERENCE_KEY,
SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY,
SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY,
SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY,
SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY,
SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY,

SETTINGS_USE_RAGE_SHAKE_KEY
);

/**
* Clear the preferences.
*
* @param context the context
*/
public static void clearPreferences(Context context) {
Set<String> keysToKeep = new HashSet<>(mKeysToKeepAfterLogout);

// home server urls
keysToKeep.add(ServerUrlsRepository.HOME_SERVER_URL_PREF);
keysToKeep.add(ServerUrlsRepository.IDENTITY_SERVER_URL_PREF);

// theme
keysToKeep.add(ThemeUtils.APPLICATION_THEME_KEY);

SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = preferences.edit();

// get all the existing keys
Set<String> keys = preferences.getAll().keySet();
// remove the one to keep

keys.removeAll(keysToKeep);

for (String key : keys) {
editor.remove(key);
}

editor.apply();
}

/**
* Tells if we have already asked the user to disable battery optimisations on android >= M devices.
*
* @param context the context
* @return true if it was already requested
*/
public static boolean didAskUserToIgnoreBatteryOptimizations(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, false);
}

/**
* Mark as requested the question to disable battery optimisations.
*
* @param context the context
*/
public static void setDidAskUserToIgnoreBatteryOptimizations(Context context) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(DID_ASK_TO_IGNORE_BATTERY_OPTIMIZATIONS_KEY, true)
.apply();
}

public static boolean didMigrateToNotificationRework(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, false);
}

public static void setDidMigrateToNotificationRework(Context context) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(DID_MIGRATE_TO_NOTIFICATION_REWORK, true)
.apply();
}

/**
* Tells if the timestamp must be displayed in 12h format
*
* @param context the context
* @return true if the time must be displayed in 12h format
*/
public static boolean displayTimeIn12hFormat(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_12_24_TIMESTAMPS_KEY, false);
}

/**
* Tells if the join and leave membership events should be shown in the messages list.
*
* @param context the context
* @return true if the join and leave membership events should be shown in the messages list
*/
public static boolean showJoinLeaveMessages(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, true);
}

/**
* Tells if the avatar and display name events should be shown in the messages list.
*
* @param context the context
* @return true true if the avatar and display name events should be shown in the messages list.
*/
public static boolean showAvatarDisplayNameChangeMessages(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY, true);
}

/**
* Tells the native camera to take a photo or record a video.
*
* @param context the context
* @return true to use the native camera app to record video or take photo.
*/
public static boolean useNativeCamera(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_NATIVE_CAMERA_PREFERENCE_KEY, false);
}

/**
* Tells if the send voice feature is enabled.
*
* @param context the context
* @return true if the send voice feature is enabled.
*/
public static boolean isSendVoiceFeatureEnabled(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_SEND_VOICE_FEATURE_PREFERENCE_KEY, false);
}

/**
* Tells which compression level to use by default
*
* @param context the context
* @return the selected compression level
*/
public static int getSelectedDefaultMediaCompressionLevel(Context context) {
return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_DEFAULT_MEDIA_COMPRESSION_KEY, "0"));
}

/**
* Tells which media source to use by default
*
* @param context the context
* @return the selected media source
*/
public static int getSelectedDefaultMediaSource(Context context) {
return Integer.parseInt(PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_DEFAULT_MEDIA_SOURCE_KEY, "0"));
}

/**
* Tells whether to use shutter sound.
*
* @param context the context
* @return true if shutter sound should play
*/
public static boolean useShutterSound(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PLAY_SHUTTER_SOUND_KEY, true);
}

/**
* Update the notification ringtone
*
* @param context the context
* @param uri the new notification ringtone, or null for no RingTone
*/
public static void setNotificationRingTone(Context context, @Nullable Uri uri) {
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit();

String value = "";

if (null != uri) {
value = uri.toString();

if (value.startsWith("file://")) {
// it should never happen
// else android.os.FileUriExposedException will be triggered.
// see https://github.com/vector-im/riot-android/issues/1725
return;
}
}

editor.putString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, value);
editor.apply();
}

/**
* Provides the selected notification ring tone
*
* @param context the context
* @return the selected ring tone or null for no RingTone
*/
@Nullable
public static Uri getNotificationRingTone(Context context) {
String url = PreferenceManager.getDefaultSharedPreferences(context).getString(SETTINGS_NOTIFICATION_RINGTONE_PREFERENCE_KEY, null);

// the user selects "None"
if (TextUtils.equals(url, "")) {
return null;
}

Uri uri = null;

// https://github.com/vector-im/riot-android/issues/1725
if ((null != url) && !url.startsWith("file://")) {
try {
uri = Uri.parse(url);
} catch (Exception e) {
Timber.e(e, "## getNotificationRingTone() : Uri.parse failed");
}
}

if (null == uri) {
uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
}

Timber.d("## getNotificationRingTone() returns " + uri);
return uri;
}

/**
* Provide the notification ringtone filename
*
* @param context the context
* @return the filename or null if "None" is selected
*/
@Nullable
public static String getNotificationRingToneName(Context context) {
Uri toneUri = getNotificationRingTone(context);

if (null == toneUri) {
return null;
}

String name = null;

Cursor cursor = null;

try {
String[] proj = {MediaStore.Audio.Media.DATA};
cursor = context.getContentResolver().query(toneUri, proj, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA);
cursor.moveToFirst();

File file = new File(cursor.getString(column_index));
name = file.getName();

if (name.contains(".")) {
name = name.substring(0, name.lastIndexOf("."));
}
} catch (Exception e) {
Timber.e(e, "## getNotificationRingToneName() failed() : " + e.getMessage());
} finally {
if (cursor != null) {
cursor.close();
}
}

return name;
}

/**
* Enable or disable the lazy loading
*
* @param context the context
* @param newValue true to enable lazy loading, false to disable it
*/
public static void setUseLazyLoading(Context context, boolean newValue) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, newValue)
.apply();
}

/**
* Tells if the lazy loading is enabled
*
* @param context the context
* @return true if the lazy loading of room members is enabled
*/
public static boolean useLazyLoading(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_LAZY_LOADING_PREFERENCE_KEY, false);
}

/**
* User explicitly refuses the lazy loading.
*
* @param context the context
*/
public static void setUserRefuseLazyLoading(Context context) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, true)
.apply();
}

/**
* Tells if the user has explicitly refused the lazy loading
*
* @param context the context
* @return true if the user has explicitly refuse the lazy loading of room members
*/
public static boolean hasUserRefusedLazyLoading(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USER_REFUSED_LAZY_LOADING_PREFERENCE_KEY, false);
}

/**
* Tells if the data save mode is enabled
*
* @param context the context
* @return true if the data save mode is enabled
*/
public static boolean useDataSaveMode(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DATA_SAVE_MODE_PREFERENCE_KEY, false);
}

/**
* Tells if the conf calls must be done with Jitsi.
*
* @param context the context
* @return true if the conference call must be done with jitsi.
*/
public static boolean useJitsiConfCall(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_JITSI_CONF_PREFERENCE_KEY, true);
}

/**
* Tells if the application is started on boot
*
* @param context the context
* @return true if the application must be started on boot
*/
public static boolean autoStartOnBoot(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, true);
}

/**
* Tells if the application is started on boot
*
* @param context the context
* @param value true to start the application on boot
*/
public static void setAutoStartOnBoot(Context context, boolean value) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(SETTINGS_START_ON_BOOT_PREFERENCE_KEY, value)
.apply();
}

/**
* Provides the selected saving period.
*
* @param context the context
* @return the selected period
*/
public static int getSelectedMediasSavingPeriod(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, MEDIA_SAVING_1_WEEK);
}

/**
* Updates the selected saving period.
*
* @param context the context
* @param index the selected period index
*/
public static void setSelectedMediasSavingPeriod(Context context, int index) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putInt(SETTINGS_MEDIA_SAVING_PERIOD_SELECTED_KEY, index)
.apply();
}

/**
* Provides the minimum last access time to keep a media file.
*
* @param context the context
* @return the min last access time (in seconds)
*/
public static long getMinMediasLastAccessTime(Context context) {
int selection = getSelectedMediasSavingPeriod(context);

switch (selection) {
case MEDIA_SAVING_3_DAYS:
return (System.currentTimeMillis() / 1000) - (3 * 24 * 60 * 60);
case MEDIA_SAVING_1_WEEK:
return (System.currentTimeMillis() / 1000) - (7 * 24 * 60 * 60);
case MEDIA_SAVING_1_MONTH:
return (System.currentTimeMillis() / 1000) - (30 * 24 * 60 * 60);
case MEDIA_SAVING_FOREVER:
return 0;
}

return 0;
}

/**
* Provides the selected saving period.
*
* @param context the context
* @return the selected period
*/
public static String getSelectedMediasSavingPeriodString(Context context) {
int selection = getSelectedMediasSavingPeriod(context);

switch (selection) {
case MEDIA_SAVING_3_DAYS:
return context.getString(R.string.media_saving_period_3_days);
case MEDIA_SAVING_1_WEEK:
return context.getString(R.string.media_saving_period_1_week);
case MEDIA_SAVING_1_MONTH:
return context.getString(R.string.media_saving_period_1_month);
case MEDIA_SAVING_FOREVER:
return context.getString(R.string.media_saving_period_forever);
}
return "?";
}

/**
* Fix some migration issues
*/
public static void fixMigrationIssues(Context context) {
// some key names have been updated to supported language switch
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);

if (preferences.contains(context.getString(R.string.settings_pin_missed_notifications))) {
preferences.edit()
.putBoolean(SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY,
preferences.getBoolean(context.getString(R.string.settings_pin_missed_notifications), false))
.remove(context.getString(R.string.settings_pin_missed_notifications))
.apply();
}

if (preferences.contains(context.getString(R.string.settings_pin_unread_messages))) {
preferences.edit()
.putBoolean(SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY,
preferences.getBoolean(context.getString(R.string.settings_pin_unread_messages), false))
.remove(context.getString(R.string.settings_pin_unread_messages))
.apply();
}

if (preferences.contains("MARKDOWN_PREFERENCE_KEY")) {
preferences.edit()
.putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, preferences.getBoolean("MARKDOWN_PREFERENCE_KEY", true))
.remove("MARKDOWN_PREFERENCE_KEY")
.apply();
}

if (preferences.contains("SETTINGS_DONT_SEND_TYPING_NOTIF_KEY")) {
preferences.edit()
.putBoolean(SETTINGS_SEND_TYPING_NOTIF_KEY, !preferences.getBoolean("SETTINGS_DONT_SEND_TYPING_NOTIF_KEY", true))
.remove("SETTINGS_DONT_SEND_TYPING_NOTIF_KEY")
.apply();
}

if (preferences.contains("SETTINGS_DISABLE_MARKDOWN_KEY")) {
preferences.edit()
.putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, !preferences.getBoolean("SETTINGS_DISABLE_MARKDOWN_KEY", true))
.remove("SETTINGS_DISABLE_MARKDOWN_KEY")
.apply();
}

if (preferences.contains("SETTINGS_HIDE_READ_RECEIPTS")) {
preferences.edit()
.putBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, !preferences.getBoolean("SETTINGS_HIDE_READ_RECEIPTS", true))
.remove("SETTINGS_HIDE_READ_RECEIPTS")
.apply();
}

if (preferences.contains("SETTINGS_HIDE_JOIN_LEAVE_MESSAGES_KEY")) {
preferences.edit()
.putBoolean(SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY, !preferences.getBoolean("SETTINGS_HIDE_JOIN_LEAVE_MESSAGES_KEY", true))
.remove("SETTINGS_HIDE_JOIN_LEAVE_MESSAGES_KEY")
.apply();
}

if (preferences.contains("SETTINGS_HIDE_AVATAR_DISPLAY_NAME_CHANGES")) {
preferences.edit()
.putBoolean(SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY,
!preferences.getBoolean("SETTINGS_HIDE_AVATAR_DISPLAY_NAME_CHANGES", true))
.remove("SETTINGS_HIDE_AVATAR_DISPLAY_NAME_CHANGES")
.apply();
}
}

/**
* Tells if the markdown is enabled
*
* @param context the context
* @return true if the markdown is enabled
*/
public static boolean isMarkdownEnabled(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, true);
}

/**
* Update the markdown enable status.
*
* @param context the context
* @param isEnabled true to enable the markdown
*/
public static void setMarkdownEnabled(Context context, boolean isEnabled) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(SETTINGS_ENABLE_MARKDOWN_KEY, isEnabled)
.apply();
}

/**
* Tells if the read receipts should be shown
*
* @param context the context
* @return true if the read receipts should be shown
*/
public static boolean showReadReceipts(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SHOW_READ_RECEIPTS_KEY, true);
}

/**
* Tells if the message timestamps must be always shown
*
* @param context the context
* @return true if the message timestamps must be always shown
*/
public static boolean alwaysShowTimeStamps(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY, false);
}

/**
* Tells if the typing notifications should be sent
*
* @param context the context
* @return true to send the typing notifs
*/
public static boolean sendTypingNotifs(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SEND_TYPING_NOTIF_KEY, true);
}

/**
* Tells of the missing notifications rooms must be displayed at left (home screen)
*
* @param context the context
* @return true to move the missed notifications to the left side
*/
public static boolean pinMissedNotifications(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PIN_MISSED_NOTIFICATIONS_PREFERENCE_KEY, true);
}

/**
* Tells of the unread rooms must be displayed at left (home screen)
*
* @param context the context
* @return true to move the unread room to the left side
*/
public static boolean pinUnreadMessages(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PIN_UNREAD_MESSAGES_PREFERENCE_KEY, true);
}

/**
* Tells if the phone must vibrate when mentioning
*
* @param context the context
* @return true
*/
public static boolean vibrateWhenMentioning(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_VIBRATE_ON_MENTION_KEY, false);
}

/**
* Tells if a dialog has been displayed to ask to use the analytics tracking (piwik, matomo, etc.).
*
* @param context the context
* @return true if a dialog has been displayed to ask to use the analytics tracking
*/
public static boolean didAskToUseAnalytics(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, false);
}

/**
* To call if the user has been asked for analytics tracking.
*
* @param context the context
*/
public static void setDidAskToUseAnalytics(Context context) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(DID_ASK_TO_USE_ANALYTICS_TRACKING_KEY, true)
.apply();
}

/**
* Tells if the analytics tracking is authorized (piwik, matomo, etc.).
*
* @param context the context
* @return true if the analytics tracking is authorized
*/
public static boolean useAnalytics(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_ANALYTICS_KEY, false);
}

/**
* Enable or disable the analytics tracking.
*
* @param context the context
* @param useAnalytics true to enable the analytics tracking
*/
public static void setUseAnalytics(Context context, boolean useAnalytics) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(SETTINGS_USE_ANALYTICS_KEY, useAnalytics)
.apply();
}

/**
* Tells if media should be previewed before sending
*
* @param context the context
* @return true to preview media
*/
public static boolean previewMediaWhenSending(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_PREVIEW_MEDIA_BEFORE_SENDING_KEY, false);
}

/**
* Tells if message should be send by pressing enter on the soft keyboard
*
* @param context the context
* @return true to send message with enter
*/
public static boolean sendMessageWithEnter(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_SEND_MESSAGE_WITH_ENTER, false);
}

/**
* Tells if the rage shake is used.
*
* @param context the context
* @return true if the rage shake is used
*/
public static boolean useRageshake(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, true);
}

/**
* Update the rage shake status.
*
* @param context the context
* @param isEnabled true to enable the rage shake
*/
public static void setUseRageshake(Context context, boolean isEnabled) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit()
.putBoolean(SETTINGS_USE_RAGE_SHAKE_KEY, isEnabled)
.apply();
}

/**
* Tells if all the events must be displayed ie even the redacted events.
*
* @param context the context
* @return true to display all the events even the redacted ones.
*/
public static boolean displayAllEvents(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SETTINGS_DISPLAY_ALL_EVENTS_KEY, false);
}
}

View File

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

import android.content.Context
import android.content.Intent
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotActivity
import org.koin.android.ext.android.inject

/**
* Displays the client settings.
*/
class VectorSettingsActivity : RiotActivity(),
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
FragmentManager.OnBackStackChangedListener,
VectorSettingsFragmentInteractionListener {

private lateinit var vectorSettingsPreferencesFragment: VectorSettingsPreferencesFragment

override fun getLayoutRes() = R.layout.activity_vector_settings

override fun getTitleRes() = R.string.title_activity_settings

private var keyToHighlight: String? = null

private val session by inject<Session>()

override fun initUiAndData() {
configureToolbar()

if (isFirstCreation()) {
vectorSettingsPreferencesFragment = VectorSettingsPreferencesFragment.newInstance(session.sessionParams.credentials.userId)
// display the fragment
supportFragmentManager.beginTransaction()
.replace(R.id.vector_settings_page, vectorSettingsPreferencesFragment, FRAGMENT_TAG)
.commit()
} else {
vectorSettingsPreferencesFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as VectorSettingsPreferencesFragment
}


supportFragmentManager.addOnBackStackChangedListener(this)

}

override fun onDestroy() {
supportFragmentManager.removeOnBackStackChangedListener(this)
super.onDestroy()
}

override fun onBackStackChanged() {
if (0 == supportFragmentManager.backStackEntryCount) {
supportActionBar?.title = getString(getTitleRes())
}
}

override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat?, pref: Preference?): Boolean {
var oFragment: Fragment? = null

if (PreferencesManager.SETTINGS_NOTIFICATION_TROUBLESHOOT_PREFERENCE_KEY == pref?.key) {
oFragment = VectorSettingsNotificationsTroubleshootFragment.newInstance(session.sessionParams.credentials.userId)
} else if (PreferencesManager.SETTINGS_NOTIFICATION_ADVANCED_PREFERENCE_KEY == pref?.key) {
oFragment = VectorSettingsAdvancedNotificationPreferenceFragment.newInstance(session.sessionParams.credentials.userId)
}

if (oFragment != null) {
oFragment.setTargetFragment(caller, 0)
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom,
R.anim.anim_slide_in_bottom, R.anim.anim_slide_out_bottom)
.replace(R.id.vector_settings_page, oFragment, pref?.title.toString())
.addToBackStack(null)
.commit()
return true
}
return false
}


override fun requestHighlightPreferenceKeyOnResume(key: String?) {
keyToHighlight = key
}

override fun requestedKeyToHighlight(): String? {
return keyToHighlight
}

companion object {
fun getIntent(context: Context, userId: String) = Intent(context, VectorSettingsActivity::class.java)
.apply {
//putExtra(MXCActionBarActivity.EXTRA_MATRIX_ID, userId)
}

private const val FRAGMENT_TAG = "VectorSettingsPreferencesFragment"
}
}

View File

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

import android.app.Activity
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.core.content.edit
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.withArgs
import im.vector.riotredesign.core.platform.RiotActivity
import im.vector.riotredesign.core.platform.VectorPreferenceFragment
import im.vector.riotredesign.core.preference.BingRule
import im.vector.riotredesign.core.preference.BingRulePreference
import im.vector.riotredesign.features.notifications.NotificationUtils
import im.vector.riotredesign.features.notifications.supportNotificationChannels
import org.koin.android.ext.android.inject

class VectorSettingsAdvancedNotificationPreferenceFragment : VectorPreferenceFragment() {

// members
private val mSession by inject<Session>()
private var mLoadingView: View? = null

// events listener
/* TODO
private val mEventsListener = object : MXEventListener() {
override fun onBingRulesUpdate() {
refreshPreferences()
refreshDisplay()
}
} */

override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
// define the layout
addPreferencesFromResource(R.xml.vector_settings_notification_advanced_preferences)

val callNotificationsSystemOptions = findPreference(PreferencesManager.SETTINGS_SYSTEM_CALL_NOTIFICATION_PREFERENCE_KEY)
if (supportNotificationChannels()) {
callNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
NotificationUtils.openSystemSettingsForCallCategory(this)
false
}
} else {
callNotificationsSystemOptions.isVisible = false
}

val noisyNotificationsSystemOptions = findPreference(PreferencesManager.SETTINGS_SYSTEM_NOISY_NOTIFICATION_PREFERENCE_KEY)
if (supportNotificationChannels()) {
noisyNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
NotificationUtils.openSystemSettingsForNoisyCategory(this)
false
}
} else {
noisyNotificationsSystemOptions.isVisible = false
}

val silentNotificationsSystemOptions = findPreference(PreferencesManager.SETTINGS_SYSTEM_SILENT_NOTIFICATION_PREFERENCE_KEY)
if (supportNotificationChannels()) {
silentNotificationsSystemOptions.onPreferenceClickListener = Preference.OnPreferenceClickListener {
NotificationUtils.openSystemSettingsForSilentCategory(this)
false
}
} else {
silentNotificationsSystemOptions.isVisible = false
}


// Ringtone
val ringtonePreference = findPreference(PreferencesManager.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY)

if (supportNotificationChannels()) {
ringtonePreference.isVisible = false
} else {
ringtonePreference.summary = PreferencesManager.getNotificationRingToneName(activity)
ringtonePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)

if (null != PreferencesManager.getNotificationRingTone(activity)) {
intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, PreferencesManager.getNotificationRingTone(activity))
}

startActivityForResult(intent, REQUEST_NOTIFICATION_RINGTONE)
false
}
}

for (preferenceKey in mPrefKeyToBingRuleId.keys) {
val preference = findPreference(preferenceKey)
if (null != preference) {
if (preference is BingRulePreference) {
//preference.isEnabled = null != rules && isConnected && pushManager.areDeviceNotificationsAllowed()
val rule: BingRule? = null // TODO mSession.dataHandler.pushRules()?.findDefaultRule(mPrefKeyToBingRuleId[preferenceKey])

if (rule == null) {
// The rule is not defined, hide the preference
preference.isVisible = false
} else {
preference.isVisible = true
preference.setBingRule(rule)
preference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
val rule = preference.createRule(newValue as Int)
if (null != rule) {
/*
TODO
displayLoadingView()
mSession.dataHandler.bingRulesManager.updateRule(preference.rule,
rule,
object : BingRulesManager.onBingRuleUpdateListener {
private fun onDone() {
refreshDisplay()
hideLoadingView()
}

override fun onBingRuleUpdateSuccess() {
onDone()
}

override fun onBingRuleUpdateFailure(errorMessage: String) {
activity?.toast(errorMessage)
onDone()
}
})
*/
}
false
}
}
}
}
}
}

private fun refreshDisplay() {
listView?.adapter?.notifyDataSetChanged()
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_NOTIFICATION_RINGTONE -> {
PreferencesManager.setNotificationRingTone(activity,
data?.getParcelableExtra<Parcelable>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) as Uri?)

// test if the selected ring tone can be played
val notificationRingToneName = PreferencesManager.getNotificationRingToneName(activity)
if (null != notificationRingToneName) {
PreferencesManager.setNotificationRingTone(activity, PreferencesManager.getNotificationRingTone(activity))
findPreference(PreferencesManager.SETTINGS_NOTIFICATION_RINGTONE_SELECTION_PREFERENCE_KEY).summary = notificationRingToneName
}
}
}
}
}

override fun onResume() {
super.onResume()
(activity as? RiotActivity)?.supportActionBar?.setTitle(R.string.settings_notification_advanced)
// find the view from parent activity
mLoadingView = activity!!.findViewById(R.id.vector_settings_spinner_views)

/* TODO
if (mSession.isAlive) {

mSession.dataHandler.addListener(mEventsListener)

// refresh anything else
refreshPreferences()
refreshDisplay()
}
*/
}

override fun onPause() {
super.onPause()

/* TODO
if (mSession.isAlive) {
mSession.dataHandler.removeListener(mEventsListener)
}
*/
}

/**
* Refresh the known information about the account
*/
private fun refreshPreferences() {
PreferenceManager.getDefaultSharedPreferences(activity).edit {
/* TODO
mSession.dataHandler.pushRules()?.let {
for (prefKey in mPrefKeyToBingRuleId.keys) {
val preference = findPreference(prefKey)

if (null != preference && preference is SwitchPreference) {
val ruleId = mPrefKeyToBingRuleId[prefKey]

val rule = it.findDefaultRule(ruleId)
var isEnabled = null != rule && rule.isEnabled

if (TextUtils.equals(ruleId, BingRule.RULE_ID_DISABLE_ALL) || TextUtils.equals(ruleId, BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS)) {
isEnabled = !isEnabled
} else if (isEnabled) {
val actions = rule!!.actions

// no action -> noting will be done
if (null == actions || actions.isEmpty()) {
isEnabled = false
} else if (1 == actions.size) {
try {
isEnabled = !TextUtils.equals(actions[0] as String, BingRule.ACTION_DONT_NOTIFY)
} catch (e: Exception) {
Timber.e(LOG_TAG, "## refreshPreferences failed " + e.message, e)
}

}
}// check if the rule is only defined by don't notify

putBoolean(prefKey, isEnabled)
}
}
}
*/
}
}


//==============================================================================================================
// Display methods
//==============================================================================================================

/**
* Display the loading view.
*/
private fun displayLoadingView() {
if (null != mLoadingView) {
mLoadingView!!.visibility = View.VISIBLE
}
}

/**
* Hide the loading view.
*/
private fun hideLoadingView() {
if (null != mLoadingView) {
mLoadingView!!.visibility = View.GONE
}
}


/* ==========================================================================================
* Companion
* ========================================================================================== */

companion object {
private const val REQUEST_NOTIFICATION_RINGTONE = 888

// preference name <-> rule Id
private var mPrefKeyToBingRuleId = mapOf(
PreferencesManager.SETTINGS_CONTAINING_MY_DISPLAY_NAME_PREFERENCE_KEY to BingRule.RULE_ID_CONTAIN_DISPLAY_NAME,
PreferencesManager.SETTINGS_CONTAINING_MY_USER_NAME_PREFERENCE_KEY to BingRule.RULE_ID_CONTAIN_USER_NAME,
PreferencesManager.SETTINGS_MESSAGES_IN_ONE_TO_ONE_PREFERENCE_KEY to BingRule.RULE_ID_ONE_TO_ONE_ROOM,
PreferencesManager.SETTINGS_MESSAGES_IN_GROUP_CHAT_PREFERENCE_KEY to BingRule.RULE_ID_ALL_OTHER_MESSAGES_ROOMS,
PreferencesManager.SETTINGS_INVITED_TO_ROOM_PREFERENCE_KEY to BingRule.RULE_ID_INVITE_ME,
PreferencesManager.SETTINGS_CALL_INVITATIONS_PREFERENCE_KEY to BingRule.RULE_ID_CALL,
PreferencesManager.SETTINGS_MESSAGES_SENT_BY_BOT_PREFERENCE_KEY to BingRule.RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS
)

fun newInstance(matrixId: String) = VectorSettingsAdvancedNotificationPreferenceFragment()
.withArgs {
// putString(MXCActionBarActivity.EXTRA_MATRIX_ID, matrixId)
}
}
}

View File

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

interface VectorSettingsFragmentInteractionListener {

fun requestHighlightPreferenceKeyOnResume(key: String?)

fun requestedKeyToHighlight(): String?

}

View File

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

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionManager
import butterknife.BindView
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.push.fcm.NotificationTroubleshootTestManagerFactory
import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.withArgs
import im.vector.riotredesign.core.platform.RiotActivity
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.features.rageshake.BugReporter
import im.vector.riotredesign.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest
import org.koin.android.ext.android.inject

class VectorSettingsNotificationsTroubleshootFragment : RiotFragment() {

@BindView(R.id.troubleshoot_test_recycler_view)
lateinit var mRecyclerView: RecyclerView
@BindView(R.id.troubleshoot_bottom_view)
lateinit var mBottomView: ViewGroup
@BindView(R.id.toubleshoot_summ_description)
lateinit var mSummaryDescription: TextView
@BindView(R.id.troubleshoot_summ_button)
lateinit var mSummaryButton: Button
@BindView(R.id.troubleshoot_run_button)
lateinit var mRunButton: Button

private var testManager: NotificationTroubleshootTestManager? = null
// members
private val mSession by inject<Session>()

override fun getLayoutResId() = R.layout.fragment_settings_notifications_troubleshoot

private var interactionListener: VectorSettingsFragmentInteractionListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val appContext = activity!!.applicationContext
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val layoutManager = LinearLayoutManager(context)
mRecyclerView.layoutManager = layoutManager

val dividerItemDecoration = DividerItemDecoration(mRecyclerView.context,
layoutManager.orientation)
mRecyclerView.addItemDecoration(dividerItemDecoration)


mSummaryButton.setOnClickListener {
BugReporter.openBugReportScreen(activity!!)
}

mRunButton.setOnClickListener {
testManager?.retry()
}
startUI()
}

private fun startUI() {

mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_running_status,
0, 0)

testManager = NotificationTroubleshootTestManagerFactory.createTestManager(this, mSession)

testManager?.statusListener = { troubleshootTestManager ->
if (isAdded) {
TransitionManager.beginDelayedTransition(mBottomView)
when (troubleshootTestManager.diagStatus) {
TroubleshootTest.TestStatus.NOT_STARTED -> {
mSummaryDescription.text = ""
mSummaryButton.visibility = View.GONE
mRunButton.visibility = View.VISIBLE
}
TroubleshootTest.TestStatus.RUNNING -> {
//Forces int type because it's breaking lint
val size: Int = troubleshootTestManager.testList.size
val currentTestIndex: Int = troubleshootTestManager.currentTestIndex
mSummaryDescription.text = getString(
R.string.settings_troubleshoot_diagnostic_running_status,
currentTestIndex,
size
)
mSummaryButton.visibility = View.GONE
mRunButton.visibility = View.GONE
}
TroubleshootTest.TestStatus.FAILED -> {
//check if there are quick fixes
var hasQuickFix = false
testManager?.testList?.let {
for (test in it) {
if (test.status == TroubleshootTest.TestStatus.FAILED && test.quickFix != null) {
hasQuickFix = true
break
}
}
}
if (hasQuickFix) {
mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_failure_status_with_quickfix)
} else {
mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_failure_status_no_quickfix)
}
mSummaryButton.visibility = View.VISIBLE
mRunButton.visibility = View.VISIBLE
}
TroubleshootTest.TestStatus.SUCCESS -> {
mSummaryDescription.text = getString(R.string.settings_troubleshoot_diagnostic_success_status)
mSummaryButton.visibility = View.VISIBLE
mRunButton.visibility = View.VISIBLE
}
}
}

}
mRecyclerView.adapter = testManager?.adapter
testManager?.runDiagnostic()
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == NotificationTroubleshootTestManager.REQ_CODE_FIX) {
testManager?.retry()
return
}
super.onActivityResult(requestCode, resultCode, data)
}

override fun onDetach() {
testManager?.cancel()
interactionListener = null
super.onDetach()
}

override fun onResume() {
super.onResume()
(activity as? RiotActivity)?.supportActionBar?.setTitle(R.string.settings_notification_troubleshoot)
}

override fun onAttach(context: Context) {
super.onAttach(context)
if (context is VectorSettingsFragmentInteractionListener) {
interactionListener = context
}
}

companion object {
// static constructor
fun newInstance(matrixId: String) = VectorSettingsNotificationsTroubleshootFragment()
.withArgs {
// TODO putString(MXCActionBarActivity.EXTRA_MATRIX_ID, matrixId)
}
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.settings.troubleshoot

import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotredesign.R
import im.vector.riotredesign.features.themes.ThemeUtils

class NotificationTroubleshootRecyclerViewAdapter(val tests: ArrayList<TroubleshootTest>)
: RecyclerView.Adapter<NotificationTroubleshootRecyclerViewAdapter.ViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(viewType, parent, false)
return ViewHolder(itemView)
}

override fun getItemViewType(position: Int): Int = R.layout.item_notification_troubleshoot

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val test = tests[position]
holder.bind(test)
}

override fun getItemCount(): Int = tests.size

class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

@BindView(R.id.troubleshootTestTitle)
lateinit var titleText: TextView
@BindView(R.id.troubleshootTestDescription)
lateinit var descriptionText: TextView
@BindView(R.id.troubleshootStatusIcon)
lateinit var statusIconImage: ImageView
@BindView(R.id.troubleshootProgressBar)
lateinit var progressBar: ProgressBar
@BindView(R.id.troubleshootTestButton)
lateinit var fixButton: Button

init {
ButterKnife.bind(this, itemView)
}

fun bind(test: TroubleshootTest) {

val context = itemView.context
titleText.setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
descriptionText.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_default_text_hint_color))

when (test.status) {
TroubleshootTest.TestStatus.NOT_STARTED -> {
titleText.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_default_text_hint_color))
descriptionText.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_default_text_hint_color))

progressBar.visibility = View.INVISIBLE
statusIconImage.visibility = View.VISIBLE
statusIconImage.setImageResource(R.drawable.unit_test)
}
TroubleshootTest.TestStatus.RUNNING -> {
progressBar.visibility = View.VISIBLE
statusIconImage.visibility = View.INVISIBLE

}
TroubleshootTest.TestStatus.FAILED -> {
progressBar.visibility = View.INVISIBLE
statusIconImage.visibility = View.VISIBLE
statusIconImage.setImageResource(R.drawable.unit_test_ko)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
statusIconImage.imageTintList = null
}

descriptionText.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_highlighted_message_text_color))
}
TroubleshootTest.TestStatus.SUCCESS -> {
progressBar.visibility = View.INVISIBLE
statusIconImage.visibility = View.VISIBLE
statusIconImage.setImageResource(R.drawable.unit_test_ok)
}
}

val quickFix = test.quickFix
if (quickFix != null) {
fixButton.setText(test.quickFix!!.title)
fixButton.setOnClickListener { _ ->
test.quickFix!!.doFix()
}
fixButton.visibility = View.VISIBLE
} else {
fixButton.visibility = View.GONE
}

titleText.setText(test.titleResId)
val description = test.description
if (description == null) {
descriptionText.visibility = View.GONE
} else {
descriptionText.visibility = View.VISIBLE
descriptionText.text = description
}
}

}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2018 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.settings.troubleshoot

import android.os.Handler
import android.os.Looper
import androidx.fragment.app.Fragment
import kotlin.properties.Delegates

class NotificationTroubleshootTestManager(val fragment: Fragment) {

val testList = ArrayList<TroubleshootTest>()
var isCancelled = false

var currentTestIndex by Delegates.observable(0) { _, _, _ ->
statusListener?.invoke(this)
}
val adapter = NotificationTroubleshootRecyclerViewAdapter(testList)


var statusListener: ((NotificationTroubleshootTestManager) -> Unit)? = null

var diagStatus: TroubleshootTest.TestStatus by Delegates.observable(TroubleshootTest.TestStatus.NOT_STARTED) { _, _, _ ->
statusListener?.invoke(this)
}


fun addTest(test: TroubleshootTest) {
testList.add(test)
test.manager = this
}

fun runDiagnostic() {
if (isCancelled) return
currentTestIndex = 0
val handler = Handler(Looper.getMainLooper())
diagStatus = if (testList.size > 0) TroubleshootTest.TestStatus.RUNNING else TroubleshootTest.TestStatus.SUCCESS
var isAllGood = true
for ((index, test) in testList.withIndex()) {
test.statusListener = {
if (!isCancelled) {
adapter.notifyItemChanged(index)
if (it.isFinished()) {
isAllGood = isAllGood && (it.status == TroubleshootTest.TestStatus.SUCCESS)
currentTestIndex++
if (currentTestIndex < testList.size) {
val troubleshootTest = testList[currentTestIndex]
troubleshootTest.status = TroubleshootTest.TestStatus.RUNNING
//Cosmetic: Start with a small delay for UI/UX reason (better animation effect) for non async tests
handler.postDelayed({
if (fragment.isAdded) {
troubleshootTest.perform()
}
}, 600)
} else {
//we are done, test global status?
diagStatus = if (isAllGood) TroubleshootTest.TestStatus.SUCCESS else TroubleshootTest.TestStatus.FAILED
}
}
}
}
}
if (fragment.isAdded) {
testList.firstOrNull()?.perform()
}
}

fun retry() {
for (test in testList) {
test.cancel()
test.description = null
test.quickFix = null
test.status = TroubleshootTest.TestStatus.NOT_STARTED
}
runDiagnostic()
}

fun cancel() {
isCancelled = true
for (test in testList) {
test.cancel()
}
}

companion object {
const val REQ_CODE_FIX = 9099
}
}

View File

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

import androidx.fragment.app.Fragment
import im.vector.matrix.android.api.session.Session
import im.vector.riotredesign.R
import im.vector.riotredesign.features.settings.troubleshoot.TroubleshootTest

/**
* Check that the main pushRule (RULE_ID_DISABLE_ALL) is correctly setup
*/
class TestAccountSettings(val fragment: Fragment, val session: Session) : TroubleshootTest(R.string.settings_troubleshoot_test_account_settings_title) {

override fun perform() {
/*
TODO
val defaultRule = session?.dataHandler?.bingRulesManager?.pushRules()?.findDefaultRule(BingRule.RULE_ID_DISABLE_ALL)
if (defaultRule != null) {
if (!defaultRule.isEnabled) {
description = fragment.getString(R.string.settings_troubleshoot_test_account_settings_success)
quickFix = null
status = TestStatus.SUCCESS
} else {
description = fragment.getString(R.string.settings_troubleshoot_test_account_settings_failed)
quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_account_settings_quickfix) {
override fun doFix() {
if (manager?.diagStatus == TestStatus.RUNNING) return //wait before all is finished
session?.dataHandler?.bingRulesManager?.updateEnableRuleStatus(defaultRule, !defaultRule.isEnabled,
object : BingRulesManager.onBingRuleUpdateListener {

override fun onBingRuleUpdateSuccess() {
manager?.retry()
}

override fun onBingRuleUpdateFailure(errorMessage: String) {
manager?.retry()
}
})
}
}
status = TestStatus.FAILED
}
} else {
//should not happen?
status = TestStatus.FAILED
}
*/

status = TestStatus.FAILED
}
}

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