mirror of
https://github.com/element-hq/element-android
synced 2024-11-23 09:55:40 +03:00
SignOut
This commit is contained in:
parent
3091a337c9
commit
08dacacdda
20 changed files with 883 additions and 19 deletions
|
@ -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
|
||||
|
|
|
@ -22,13 +22,19 @@ 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, CryptoService {
|
||||
interface Session :
|
||||
RoomService,
|
||||
GroupService,
|
||||
UserService,
|
||||
CryptoService,
|
||||
SignOutService {
|
||||
|
||||
/**
|
||||
* The params associated to the session
|
||||
|
|
|
@ -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>)
|
||||
|
||||
}
|
|
@ -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>)
|
||||
|
||||
}
|
|
@ -25,4 +25,5 @@ internal interface SessionParamsStore {
|
|||
|
||||
fun save(sessionParams: SessionParams): Try<SessionParams>
|
||||
|
||||
fun delete()
|
||||
}
|
|
@ -17,8 +17,8 @@
|
|||
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
|
||||
|
||||
|
@ -50,4 +50,14 @@ internal class RealmSessionParamsStore(private val mapper: SessionParamsMapper,
|
|||
return sessionParams
|
||||
}
|
||||
|
||||
override fun delete() {
|
||||
val realm = Realm.getInstance(realmConfiguration)
|
||||
realm.executeTransaction {
|
||||
it.where(SessionParamsEntity::class.java)
|
||||
.findAll()
|
||||
.deleteAllFromRealm()
|
||||
}
|
||||
realm.close()
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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? {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}.map {
|
||||
// TODO Clear DB, media cache, etc.
|
||||
sessionParamsStore.delete()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
@ -38,6 +39,7 @@ import im.vector.riotredesign.features.home.room.detail.LoadingRoomDetailFragmen
|
|||
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
|
||||
|
@ -114,6 +116,10 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
|
|||
startActivity(VectorSettingsActivity.getIntent(this, "TODO"))
|
||||
return true
|
||||
}
|
||||
R.id.sliding_menu_sign_out -> {
|
||||
SignOutUiWorker(this).perform(Matrix.getInstance().currentSession!!)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
/*
|
||||
* 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.workers.signout
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.R
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
|
||||
class SignOutBottomSheetDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
val session by inject<Session>()
|
||||
|
||||
@BindView(R.id.bottom_sheet_signout_warning_text)
|
||||
lateinit var sheetTitle: TextView
|
||||
|
||||
@BindView(R.id.bottom_sheet_signout_backingup_status_group)
|
||||
lateinit var backingUpStatusGroup: ViewGroup
|
||||
|
||||
@BindView(R.id.keys_backup_setup)
|
||||
lateinit var setupClickableView: View
|
||||
|
||||
@BindView(R.id.keys_backup_activate)
|
||||
lateinit var activateClickableView: View
|
||||
|
||||
@BindView(R.id.keys_backup_dont_want)
|
||||
lateinit var dontWantClickableView: View
|
||||
|
||||
@BindView(R.id.bottom_sheet_signout_icon_progress_bar)
|
||||
lateinit var backupProgress: ProgressBar
|
||||
|
||||
@BindView(R.id.bottom_sheet_signout_icon)
|
||||
lateinit var backupCompleteImage: ImageView
|
||||
|
||||
@BindView(R.id.bottom_sheet_backup_status_text)
|
||||
lateinit var backupStatusTex: TextView
|
||||
|
||||
@BindView(R.id.bottom_sheet_signout_button)
|
||||
lateinit var signoutClickableView: View
|
||||
|
||||
|
||||
@BindView(R.id.root_layout)
|
||||
lateinit var rootLayout: ViewGroup
|
||||
|
||||
|
||||
var onSignOut: Runnable? = null
|
||||
|
||||
companion object {
|
||||
fun newInstance(userId: String) = SignOutBottomSheetDialogFragment()
|
||||
|
||||
private const val EXPORT_REQ = 0
|
||||
}
|
||||
|
||||
init {
|
||||
isCancelable = true
|
||||
}
|
||||
|
||||
private lateinit var viewModel: SignOutViewModel
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(SignOutViewModel::class.java)
|
||||
|
||||
viewModel.init(session)
|
||||
|
||||
setupClickableView.setOnClickListener {
|
||||
context?.let { context ->
|
||||
// TODO startActivityForResult(KeysBackupSetupActivity.intent(context, getExtraMatrixID(), true), EXPORT_REQ)
|
||||
}
|
||||
}
|
||||
|
||||
activateClickableView.setOnClickListener {
|
||||
context?.let { context ->
|
||||
// TODO startActivity(KeysBackupManageActivity.intent(context, getExtraMatrixID()))
|
||||
}
|
||||
}
|
||||
|
||||
signoutClickableView.setOnClickListener {
|
||||
this.onSignOut?.run()
|
||||
}
|
||||
|
||||
dontWantClickableView.setOnClickListener { _ ->
|
||||
context?.let {
|
||||
AlertDialog.Builder(it)
|
||||
.setTitle(R.string.are_you_sure)
|
||||
.setMessage(R.string.sign_out_bottom_sheet_will_lose_secure_messages)
|
||||
.setPositiveButton(R.string.backup) { _, _ ->
|
||||
/* TODO
|
||||
when (viewModel.keysBackupState.value) {
|
||||
KeysBackupStateManager.KeysBackupState.NotTrusted -> {
|
||||
context?.let { context ->
|
||||
startActivity(KeysBackupManageActivity.intent(context, getExtraMatrixID()))
|
||||
}
|
||||
}
|
||||
KeysBackupStateManager.KeysBackupState.Disabled -> {
|
||||
context?.let { context ->
|
||||
startActivityForResult(KeysBackupSetupActivity.intent(context, getExtraMatrixID(), true), EXPORT_REQ)
|
||||
}
|
||||
}
|
||||
KeysBackupStateManager.KeysBackupState.BackingUp,
|
||||
KeysBackupStateManager.KeysBackupState.WillBackUp -> {
|
||||
//keys are already backing up please wait
|
||||
context?.toast(R.string.keys_backup_is_not_finished_please_wait)
|
||||
}
|
||||
else -> {
|
||||
//nop
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
.setNegativeButton(R.string.action_sign_out) { _, _ ->
|
||||
onSignOut?.run()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
viewModel.keysExportedToFile.observe(this, Observer {
|
||||
val hasExportedToFile = it ?: false
|
||||
if (hasExportedToFile) {
|
||||
//We can allow to sign out
|
||||
|
||||
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
|
||||
|
||||
signoutClickableView.isVisible = true
|
||||
dontWantClickableView.isVisible = false
|
||||
setupClickableView.isVisible = false
|
||||
activateClickableView.isVisible = false
|
||||
backingUpStatusGroup.isVisible = false
|
||||
}
|
||||
})
|
||||
|
||||
/* TODO
|
||||
viewModel.keysBackupState.observe(this, Observer {
|
||||
if (viewModel.keysExportedToFile.value == true) {
|
||||
//ignore this
|
||||
return@Observer
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(rootLayout)
|
||||
when (it) {
|
||||
KeysBackupStateManager.KeysBackupState.ReadyToBackUp -> {
|
||||
signoutClickableView.isVisible = true
|
||||
dontWantClickableView.isVisible = false
|
||||
setupClickableView.isVisible = false
|
||||
activateClickableView.isVisible = false
|
||||
backingUpStatusGroup.isVisible = true
|
||||
|
||||
backupProgress.isVisible = false
|
||||
backupCompleteImage.isVisible = true
|
||||
backupStatusTex.text = getString(R.string.keys_backup_info_keys_all_backup_up)
|
||||
|
||||
sheetTitle.text = getString(R.string.action_sign_out_confirmation_simple)
|
||||
}
|
||||
KeysBackupStateManager.KeysBackupState.BackingUp,
|
||||
KeysBackupStateManager.KeysBackupState.WillBackUp -> {
|
||||
backingUpStatusGroup.isVisible = true
|
||||
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backing_up)
|
||||
dontWantClickableView.isVisible = true
|
||||
setupClickableView.isVisible = false
|
||||
activateClickableView.isVisible = false
|
||||
|
||||
backupProgress.isVisible = true
|
||||
backupCompleteImage.isVisible = false
|
||||
backupStatusTex.text = getString(R.string.sign_out_bottom_sheet_backing_up_keys)
|
||||
|
||||
}
|
||||
KeysBackupStateManager.KeysBackupState.NotTrusted -> {
|
||||
backingUpStatusGroup.isVisible = false
|
||||
dontWantClickableView.isVisible = true
|
||||
setupClickableView.isVisible = false
|
||||
activateClickableView.isVisible = true
|
||||
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_backup_not_active)
|
||||
}
|
||||
else -> {
|
||||
backingUpStatusGroup.isVisible = false
|
||||
dontWantClickableView.isVisible = true
|
||||
setupClickableView.isVisible = true
|
||||
activateClickableView.isVisible = false
|
||||
sheetTitle.text = getString(R.string.sign_out_bottom_sheet_warning_no_backup)
|
||||
}
|
||||
}
|
||||
|
||||
// updateSignOutSection()
|
||||
})
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.bottom_sheet_logout_and_backup, container, false)
|
||||
ButterKnife.bind(this, view)
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
//We want to force the bottom sheet initial state to expanded
|
||||
(dialog as? BottomSheetDialog)?.let { bottomSheetDialog ->
|
||||
bottomSheetDialog.setOnShowListener { dialog ->
|
||||
val d = dialog as BottomSheetDialog
|
||||
(d.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout)?.let {
|
||||
BottomSheetBehavior.from(it).state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
/* TODO
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == EXPORT_REQ) {
|
||||
val manualExportDone = data?.getBooleanExtra(KeysBackupSetupActivity.MANUAL_EXPORT, false)
|
||||
viewModel.keysExportedToFile.value = manualExportDone
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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.workers.signout
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotredesign.R
|
||||
import im.vector.riotredesign.core.platform.RiotActivity
|
||||
import im.vector.riotredesign.features.MainActivity
|
||||
|
||||
class SignOutUiWorker(val activity: RiotActivity) {
|
||||
|
||||
fun perform(session: Session) {
|
||||
if (SignOutViewModel.doYouNeedToBeDisplayed(session)) {
|
||||
val signOutDialog = SignOutBottomSheetDialogFragment.newInstance(session.sessionParams.credentials.userId)
|
||||
signOutDialog.onSignOut = Runnable {
|
||||
doSignOut(session)
|
||||
}
|
||||
signOutDialog.show(activity.supportFragmentManager, "SO")
|
||||
} else {
|
||||
// Display a simple confirmation dialog
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.action_sign_out)
|
||||
.setMessage(R.string.action_sign_out_confirmation_simple)
|
||||
.setPositiveButton(R.string.action_sign_out) { _, _ ->
|
||||
doSignOut(session)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun doSignOut(session: Session) {
|
||||
// TODO showWaitingView()
|
||||
|
||||
session.signOut(object : MatrixCallback<Unit> {
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
// Start MainActivity in a new task
|
||||
val intent = Intent(activity, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
// TODO Notify user, or ignore?
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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.workers.signout
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
|
||||
class SignOutViewModel : ViewModel() { // TODO, KeysBackupStateManager.KeysBackupStateListener {
|
||||
// Keys exported manually
|
||||
var keysExportedToFile = MutableLiveData<Boolean>()
|
||||
|
||||
// var keysBackupState = MutableLiveData<KeysBackupStateManager.KeysBackupState>()
|
||||
|
||||
private var mxSession: Session? = null
|
||||
|
||||
fun init(session: Session) {
|
||||
if (mxSession == null) {
|
||||
mxSession = session
|
||||
|
||||
// TODO
|
||||
//mxSession?.crypto
|
||||
// ?.keysBackup
|
||||
// ?.addListener(this)
|
||||
}
|
||||
|
||||
//keysBackupState.value = mxSession?.crypto
|
||||
// ?.keysBackup
|
||||
// ?.state
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Safe way to get the current KeysBackup version
|
||||
// */
|
||||
// fun getCurrentBackupVersion(): String {
|
||||
// return mxSession
|
||||
// ?.crypto
|
||||
// ?.keysBackup
|
||||
// ?.currentBackupVersion
|
||||
// ?: ""
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Safe way to get the number of keys to backup
|
||||
// */
|
||||
// fun getNumberOfKeysToBackup(): Int {
|
||||
// return mxSession
|
||||
// ?.crypto
|
||||
// ?.cryptoStore
|
||||
// ?.inboundGroupSessionsCount(false)
|
||||
// ?: 0
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Safe way to tell if there are more keys on the server
|
||||
// */
|
||||
// fun canRestoreKeys(): Boolean {
|
||||
// return mxSession
|
||||
// ?.crypto
|
||||
// ?.keysBackup
|
||||
// ?.canRestoreKeys() == true
|
||||
// }
|
||||
//
|
||||
// override fun onCleared() {
|
||||
// super.onCleared()
|
||||
//
|
||||
// mxSession?.crypto
|
||||
// ?.keysBackup
|
||||
// ?.removeListener(this)
|
||||
// }
|
||||
//
|
||||
// override fun onStateChange(newState: KeysBackupStateManager.KeysBackupState) {
|
||||
// keysBackupState.value = newState
|
||||
// }
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The backup check on logout flow has to be displayed if there are keys in the store, and the keys backup state is not Ready
|
||||
*/
|
||||
fun doYouNeedToBeDisplayed(session: Session?): Boolean {
|
||||
return false
|
||||
|
||||
/* TODO
|
||||
return session
|
||||
?.crypto
|
||||
?.cryptoStore
|
||||
?.inboundGroupSessionsCount(false)
|
||||
?: 0 > 0
|
||||
&& session
|
||||
?.crypto
|
||||
?.keysBackup
|
||||
?.state != KeysBackupStateManager.KeysBackupState.ReadyToBackUp
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
202
vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml
Normal file
202
vector/src/main/res/layout/bottom_sheet_logout_and_backup.xml
Normal file
|
@ -0,0 +1,202 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/root_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/action_sign_out"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_signout_warning_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="@string/sign_out_bottom_sheet_warning_no_backup" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom_sheet_signout_backingup_status_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginEnd="@dimen/layout_horizontal_margin"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bottom_sheet_signout_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/unit_test_ok"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/bottom_sheet_signout_icon_progress_bar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:visibility="visible"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_backup_status_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
tools:text="@string/keys_backup_info_keys_all_backup_up" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/keys_backup_setup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:minHeight="50dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/backup_keys"
|
||||
android:tint="?android:attr/textColorTertiary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/keys_backup_setup"
|
||||
android:textSize="17sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/keys_backup_activate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:minHeight="50dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/backup_keys"
|
||||
android:tint="?android:attr/textColorTertiary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/keys_backup_activate"
|
||||
android:textSize="17sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/keys_backup_dont_want"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:minHeight="50dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_material_leave"
|
||||
android:tint="@color/vector_error_color" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/sign_out_bottom_sheet_dont_want_secure_messages"
|
||||
android:textColor="@color/vector_error_color"
|
||||
android:textSize="17sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom_sheet_signout_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:minHeight="50dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/layout_horizontal_margin"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:src="@drawable/ic_material_exit_to_app"
|
||||
android:tint="@color/vector_error_color" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/action_sign_out"
|
||||
android:textColor="@color/vector_error_color"
|
||||
android:textSize="17sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -1,11 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/sliding_menu_settings"
|
||||
android:icon="@drawable/ic_settings"
|
||||
android:title="@string/room_sliding_menu_settings"
|
||||
app:showAsAction="ifRoom" />
|
||||
android:title="@string/room_sliding_menu_settings" />
|
||||
|
||||
<item
|
||||
android:id="@+id/sliding_menu_sign_out"
|
||||
android:icon="@drawable/ic_material_exit_to_app"
|
||||
android:title="@string/action_sign_out" />
|
||||
|
||||
</menu>
|
Loading…
Reference in a new issue