Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in / Register
Toggle navigation
M
Magisk
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
Magisk
Commits
f5aa6a3c
Commit
f5aa6a3c
authored
Aug 10, 2020
by
topjohnwu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update module fragment
Update UI and logic for loading modules
parent
c944277e
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
134 additions
and
153 deletions
+134
-153
RepoUpdater.kt
.../main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt
+1
-1
ModuleRvItem.kt
...om/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt
+5
-10
CompatHelpers.kt
...c/main/java/com/topjohnwu/magisk/ui/base/CompatHelpers.kt
+1
-1
ModuleViewModel.kt
...in/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt
+109
-117
fragment_module_md2.xml
app/src/main/res/layout/fragment_module_md2.xml
+1
-1
item_section_md2.xml
app/src/main/res/layout/item_section_md2.xml
+10
-16
item_settings_section.xml
app/src/main/res/layout/item_settings_section.xml
+2
-2
styles_md2_appearance.xml
app/src/main/res/values/styles_md2_appearance.xml
+5
-5
No files found.
app/src/main/java/com/topjohnwu/magisk/core/tasks/RepoUpdater.kt
View file @
f5aa6a3c
...
...
@@ -92,7 +92,7 @@ class RepoUpdater(
}
}
suspend
operator
fun
invoke
(
forced
:
Boolean
)
=
withContext
(
Dispatchers
.
IO
)
{
suspend
fun
run
(
forced
:
Boolean
)
=
withContext
(
Dispatchers
.
IO
)
{
val
cached
=
HashSet
(
repoDB
.
repoIDList
).
synchronized
()
when
(
loadPage
(
cached
,
etag
=
repoDB
.
etagKey
))
{
PageResult
.
CACHED
->
if
(
forced
)
forcedReload
(
cached
)
...
...
app/src/main/java/com/topjohnwu/magisk/model/entity/recycler/ModuleRvItem.kt
View file @
f5aa6a3c
package
com.topjohnwu.magisk.model.entity.recycler
import
androidx.databinding.Bindable
import
androidx.databinding.Observable
import
com.topjohnwu.magisk.BR
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.core.model.module.Module
...
...
@@ -62,7 +61,7 @@ sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() {
}
}
class
ModuleItem
(
val
item
:
Module
)
:
ObservableItem
<
ModuleItem
>()
,
Observable
{
class
ModuleItem
(
val
item
:
Module
)
:
ObservableItem
<
ModuleItem
>()
{
override
val
layoutRes
=
R
.
layout
.
item_module_md2
...
...
@@ -71,19 +70,15 @@ class ModuleItem(val item: Module) : ObservableItem<ModuleItem>(), Observable {
set
(
value
)
=
set
(
value
,
field
,
{
field
=
it
},
BR
.
repo
)
@get
:
Bindable
var
isEnabled
get
()
=
item
.
enable
set
(
value
)
{
var
isEnabled
=
item
.
enable
set
(
value
)
=
set
(
value
,
field
,
{
field
=
it
},
BR
.
enabled
)
{
item
.
enable
=
value
notifyPropertyChanged
(
BR
.
enabled
)
}
@get
:
Bindable
var
isRemoved
get
()
=
item
.
remove
set
(
value
)
{
var
isRemoved
=
item
.
remove
set
(
value
)
=
set
(
value
,
field
,
{
field
=
it
},
BR
.
removed
)
{
item
.
remove
=
value
notifyPropertyChanged
(
BR
.
removed
)
}
val
isUpdated
get
()
=
item
.
updated
...
...
app/src/main/java/com/topjohnwu/magisk/ui/base/CompatHelpers.kt
View file @
f5aa6a3c
...
...
@@ -27,7 +27,7 @@ inline fun <T : ComparableRvItem<*>> filterableListOf(
override
fun
areContentsTheSame
(
oldItem
:
T
,
newItem
:
T
)
=
oldItem
.
genericContentSameAs
(
newItem
)
}).
also
{
it
.
update
(
newItems
.
toList
())
}
fun
<
T
:
ComparableRvItem
<*
>
>
adapterOf
()
=
object
:
BindingRecyclerViewAdapter
<
T
>()
{
fun
<
T
:
RvItem
>
adapterOf
()
=
object
:
BindingRecyclerViewAdapter
<
T
>()
{
override
fun
onBindBinding
(
binding
:
ViewDataBinding
,
variableId
:
Int
,
...
...
app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt
View file @
f5aa6a3c
...
...
@@ -2,28 +2,34 @@ package com.topjohnwu.magisk.ui.module
import
androidx.databinding.Bindable
import
androidx.databinding.ObservableArrayList
import
androidx.lifecycle.Observer
import
androidx.lifecycle.viewModelScope
import
com.topjohnwu.magisk.BR
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.core.Config
import
com.topjohnwu.magisk.core.download.RemoteFileService
import
com.topjohnwu.magisk.core.model.module.Module
import
com.topjohnwu.magisk.core.model.module.Repo
import
com.topjohnwu.magisk.core.tasks.RepoUpdater
import
com.topjohnwu.magisk.data.database.RepoByNameDao
import
com.topjohnwu.magisk.data.database.RepoByUpdatedDao
import
com.topjohnwu.magisk.databinding.
Comparable
RvItem
import
com.topjohnwu.magisk.databinding.RvItem
import
com.topjohnwu.magisk.ktx.addOnListChangedCallback
import
com.topjohnwu.magisk.ktx.reboot
import
com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import
com.topjohnwu.magisk.model.entity.recycler.*
import
com.topjohnwu.magisk.model.entity.recycler.InstallModule
import
com.topjohnwu.magisk.model.entity.recycler.ModuleItem
import
com.topjohnwu.magisk.model.entity.recycler.RepoItem
import
com.topjohnwu.magisk.model.entity.recycler.SectionTitle
import
com.topjohnwu.magisk.model.events.InstallExternalModuleEvent
import
com.topjohnwu.magisk.model.events.OpenChangelogEvent
import
com.topjohnwu.magisk.model.events.dialog.ModuleInstallDialog
import
com.topjohnwu.magisk.ui.base.*
import
com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import
com.topjohnwu.magisk.utils.set
import
kotlinx.coroutines.*
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.Job
import
kotlinx.coroutines.launch
import
kotlinx.coroutines.withContext
import
me.tatarka.bindingcollectionadapter2.collections.MergeObservableList
import
kotlin.math.roundToInt
...
...
@@ -45,7 +51,7 @@ class ModuleViewModel(
private
val
repoName
:
RepoByNameDao
,
private
val
repoUpdated
:
RepoByUpdatedDao
,
private
val
repoUpdater
:
RepoUpdater
)
:
BaseViewModel
(),
Queryable
{
)
:
BaseViewModel
(),
Queryable
,
Observer
<
Pair
<
Float
,
DownloadSubject
>>
{
override
val
queryDelay
=
1000L
private
var
queryJob
:
Job
?
=
null
...
...
@@ -72,37 +78,13 @@ class ModuleViewModel(
it
.
bindExtra
(
BR
.
viewModel
,
this
)
}
private
val
itemNoneInstalled
=
TextItem
(
R
.
string
.
no_modules_found
)
private
val
itemNoneUpdatable
=
TextItem
(
R
.
string
.
module_update_none
)
private
val
itemsInstalledHelpers
=
ObservableArrayList
<
TextItem
>()
private
val
itemsUpdatableHelpers
=
ObservableArrayList
<
TextItem
>()
private
val
installSectionList
=
ObservableArrayList
<
RvItem
>()
private
val
updatableSectionList
=
ObservableArrayList
<
RvItem
>()
private
val
itemsInstalled
=
diffListOf
<
ModuleItem
>()
private
val
itemsUpdatable
=
diffListOf
<
RepoItem
.
Update
>()
private
val
itemsRemote
=
diffListOf
<
RepoItem
.
Remote
>()
val
adapter
=
adapterOf
<
ComparableRvItem
<*>>()
val
items
=
MergeObservableList
<
ComparableRvItem
<*>>()
.
insertItem
(
InstallModule
)
.
insertItem
(
sectionUpdate
)
.
insertList
(
itemsUpdatableHelpers
)
.
insertList
(
itemsUpdatable
)
.
insertItem
(
sectionActive
)
.
insertList
(
itemsInstalledHelpers
)
.
insertList
(
itemsInstalled
)
.
insertItem
(
sectionRemote
)
.
insertList
(
itemsRemote
)
!!
val
itemBinding
=
itemBindingOf
<
ComparableRvItem
<*>>
{
it
.
bindExtra
(
BR
.
viewModel
,
this
)
}
companion
object
{
private
val
sectionRemote
=
SectionTitle
(
R
.
string
.
module_section_remote
,
R
.
string
.
sorting_order
)
private
val
sectionUpdate
=
SectionTitle
(
R
.
string
.
module_section_pending
,
R
.
string
.
module_section_pending_action
,
...
...
@@ -110,23 +92,28 @@ class ModuleViewModel(
// enable with implementation of https://github.com/topjohnwu/Magisk/issues/2036
).
also
{
it
.
hasButton
=
false
}
private
val
sectionActive
=
SectionTitle
(
private
val
sectionInstalled
=
SectionTitle
(
R
.
string
.
module_installed
,
R
.
string
.
reboot
,
R
.
drawable
.
ic_restart
).
also
{
it
.
hasButton
=
false
}
init
{
updateOrderIcon
()
}
private
val
sectionRemote
=
SectionTitle
(
R
.
string
.
module_section_remote
,
R
.
string
.
sorting_order
).
apply
{
updateOrderIcon
()
}
private
fun
updateOrderIcon
()
{
sectionRemote
.
icon
=
when
(
Config
.
repoOrder
)
{
Config
.
Value
.
ORDER_NAME
->
R
.
drawable
.
ic_order_name
Config
.
Value
.
ORDER_DATE
->
R
.
drawable
.
ic_order_date
else
->
return
}
}
val
adapter
=
adapterOf
<
RvItem
>()
val
items
=
MergeObservableList
<
RvItem
>()
.
insertItem
(
InstallModule
)
.
insertList
(
updatableSectionList
)
.
insertList
(
itemsUpdatable
)
.
insertList
(
installSectionList
)
.
insertList
(
itemsInstalled
)
.
insertItem
(
sectionRemote
)
.
insertList
(
itemsRemote
)
!!
val
itemBinding
=
itemBindingOf
<
RvItem
>
{
it
.
bindExtra
(
BR
.
viewModel
,
this
)
}
// ---
...
...
@@ -143,63 +130,92 @@ class ModuleViewModel(
init
{
RemoteFileService
.
reset
()
RemoteFileService
.
progressBroadcast
.
observeForever
{
val
(
progress
,
subject
)
=
it
?:
return
@observeForever
if
(
subject
!
is
DownloadSubject
.
Module
)
{
return
@observeForever
}
update
(
subject
.
module
,
progress
.
times
(
100
).
roundToInt
())
}
RemoteFileService
.
progressBroadcast
.
observeForever
(
this
)
itemsInstalled
.
addOnListChangedCallback
(
onItemRangeInserted
=
{
_
,
_
,
_
->
itemsInstalledHelpers
.
clear
()
},
onItemRangeRemoved
=
{
_
,
_
,
_
->
addInstalledEmptyMessage
()
}
onItemRangeInserted
=
{
_
,
_
,
_
->
if
(
installSectionList
.
isEmpty
())
installSectionList
.
add
(
sectionInstalled
)
},
onItemRangeRemoved
=
{
list
,
_
,
_
->
if
(
list
.
isEmpty
())
installSectionList
.
clear
()
}
)
itemsUpdatable
.
addOnListChangedCallback
(
onItemRangeInserted
=
{
_
,
_
,
_
->
itemsUpdatableHelpers
.
clear
()
},
onItemRangeRemoved
=
{
_
,
_
,
_
->
addUpdatableEmptyMessage
()
}
onItemRangeInserted
=
{
_
,
_
,
_
->
if
(
updatableSectionList
.
isEmpty
())
updatableSectionList
.
add
(
sectionUpdate
)
},
onItemRangeRemoved
=
{
list
,
_
,
_
->
if
(
list
.
isEmpty
())
updatableSectionList
.
clear
()
}
)
}
// ---
override
fun
onCleared
()
{
super
.
onCleared
()
RemoteFileService
.
progressBroadcast
.
removeObserver
(
this
)
}
override
fun
onChanged
(
it
:
Pair
<
Float
,
DownloadSubject
>?)
{
val
(
progress
,
subject
)
=
it
?:
return
if
(
subject
!
is
DownloadSubject
.
Module
)
return
viewModelScope
.
launch
{
val
items
=
withContext
(
Dispatchers
.
Default
)
{
val
predicate
=
{
it
:
RepoItem
->
it
.
item
.
id
==
subject
.
module
.
id
}
itemsUpdatable
.
filter
(
predicate
)
+
itemsRemote
.
filter
(
predicate
)
+
itemsSearch
.
filter
(
predicate
)
}
items
.
forEach
{
it
.
progress
=
progress
.
times
(
100
).
roundToInt
()
}
}
}
override
fun
refresh
():
Job
{
return
viewModelScope
.
launch
{
loadInstalled
()
if
(
itemsRemote
.
isEmpty
())
loadRemote
()
return
loadInstalled
()
}
private
suspend
fun
loadUpdates
(
installed
:
List
<
ModuleItem
>)
=
withContext
(
Dispatchers
.
IO
)
{
installed
.
mapNotNull
{
dao
.
getUpdatableRepoById
(
it
.
item
.
id
,
it
.
item
.
versionCode
)
}
.
map
{
RepoItem
.
Update
(
it
)
}
}
private
suspend
fun
List
<
ModuleItem
>.
loadDetails
()
=
withContext
(
Dispatchers
.
IO
)
{
onEach
{
launch
{
it
.
repo
=
dao
.
getRepoById
(
it
.
item
.
id
)
}
private
fun
SectionTitle
.
updateOrderIcon
()
{
hasButton
=
true
icon
=
when
(
Config
.
repoOrder
)
{
Config
.
Value
.
ORDER_NAME
->
R
.
drawable
.
ic_order_name
Config
.
Value
.
ORDER_DATE
->
R
.
drawable
.
ic_order_date
else
->
return
}
}
private
fun
loadInstalled
()
=
viewModelScope
.
launch
{
state
=
State
.
LOADING
private
suspend
fun
loadInstalled
()
{
val
installed
=
Module
.
installed
().
map
{
ModuleItem
(
it
)
}
val
detailLoad
=
async
{
installed
.
loadDetails
()
}
val
updates
=
loadUpdates
(
installed
)
val
diff
=
withContext
(
Dispatchers
.
Default
)
{
val
i
=
async
{
itemsInstalled
.
calculateDiff
(
installed
)
}
val
u
=
async
{
itemsUpdatable
.
calculateDiff
(
updates
)
}
awaitAll
(
i
,
u
)
itemsInstalled
.
calculateDiff
(
installed
)
}
itemsInstalled
.
update
(
installed
,
diff
)
}
private
suspend
fun
loadUpdatable
()
{
val
(
updates
,
diff
)
=
withContext
(
Dispatchers
.
IO
)
{
itemsInstalled
.
forEach
{
launch
{
it
.
repo
=
dao
.
getRepoById
(
it
.
item
.
id
)
}
}
detailLoad
.
await
()
itemsInstalled
.
update
(
installed
,
diff
[
0
])
itemsUpdatable
.
update
(
updates
,
diff
[
1
])
addInstalledEmptyMessage
(
)
addUpdatableEmptyMessage
()
updateActiveState
()
state
=
State
.
LOADED
val
updates
=
itemsInstalled
.
mapNotNull
{
dao
.
getUpdatableRepoById
(
it
.
item
.
id
,
it
.
item
.
versionCode
)
}
.
map
{
RepoItem
.
Update
(
it
)
}
val
diff
=
itemsUpdatable
.
calculateDiff
(
updates
)
return
@withContext
updates
to
diff
}
itemsUpdatable
.
update
(
updates
,
diff
)
}
fun
loadRemote
()
{
...
...
@@ -217,7 +233,8 @@ class ModuleViewModel(
isRemoteLoading
=
true
val
repos
=
if
(
itemsRemote
.
isEmpty
())
{
repoUpdater
(
refetch
)
repoUpdater
.
run
(
refetch
)
loadUpdatable
()
loadRemoteDB
(
0
)
}
else
{
loadRemoteDB
(
itemsRemote
.
size
)
...
...
@@ -230,6 +247,7 @@ class ModuleViewModel(
fun
forceRefresh
()
{
itemsRemote
.
clear
()
itemsUpdatable
.
clear
()
itemsSearch
.
clear
()
refetch
=
true
refresh
()
...
...
@@ -271,47 +289,21 @@ class ModuleViewModel(
// ---
private
fun
update
(
repo
:
Repo
,
progress
:
Int
)
=
viewModelScope
.
launch
{
val
items
=
withContext
(
Dispatchers
.
Default
)
{
val
predicate
=
{
it
:
RepoItem
->
it
.
item
.
id
==
repo
.
id
}
itemsUpdatable
.
filter
(
predicate
)
+
itemsRemote
.
filter
(
predicate
)
+
itemsSearch
.
filter
(
predicate
)
}
items
.
forEach
{
it
.
progress
=
progress
}
}
// ---
private
fun
addInstalledEmptyMessage
()
{
if
(
itemsInstalled
.
isEmpty
()
&&
itemsInstalledHelpers
.
isEmpty
())
{
itemsInstalledHelpers
.
add
(
itemNoneInstalled
)
}
}
private
fun
addUpdatableEmptyMessage
()
{
if
(
itemsUpdatable
.
isEmpty
()
&&
itemsUpdatableHelpers
.
isEmpty
())
{
itemsUpdatableHelpers
.
add
(
itemNoneUpdatable
)
}
}
// ---
fun
updateActiveState
()
=
viewModelScope
.
launch
{
section
Active
.
hasButton
=
withContext
(
Dispatchers
.
Default
)
{
section
Installed
.
hasButton
=
withContext
(
Dispatchers
.
Default
)
{
itemsInstalled
.
any
{
it
.
isModified
}
}
}
fun
sectionPressed
(
item
:
SectionTitle
)
=
when
(
item
)
{
section
Active
->
reboot
()
// TODO add reboot picker, regular reboot is not always preferred
section
Installed
->
reboot
()
// TODO add reboot picker, regular reboot is not always preferred
sectionRemote
->
{
Config
.
repoOrder
=
when
(
Config
.
repoOrder
)
{
Config
.
Value
.
ORDER_NAME
->
Config
.
Value
.
ORDER_DATE
Config
.
Value
.
ORDER_DATE
->
Config
.
Value
.
ORDER_NAME
else
->
Config
.
Value
.
ORDER_NAME
}
updateOrderIcon
()
sectionRemote
.
updateOrderIcon
()
queryHandler
.
post
{
itemsRemote
.
clear
()
loadRemote
()
...
...
app/src/main/res/layout/fragment_module_md2.xml
View file @
f5aa6a3c
...
...
@@ -32,7 +32,7 @@
android:id=
"@+id/module_list"
adapter=
"@{viewModel.adapter}"
dividerHorizontal=
"@{@drawable/divider_l1}"
dividerVertical=
"@{@drawable/divider_l
1
}"
dividerVertical=
"@{@drawable/divider_l
_50
}"
gone=
"@{viewModel.loading && viewModel.items.empty}"
itemBinding=
"@{viewModel.itemBinding}"
items=
"@{viewModel.items}"
...
...
app/src/main/res/layout/item_section_md2.xml
View file @
f5aa6a3c
...
...
@@ -15,42 +15,36 @@
</data>
<
androidx.constraintlayout.widget.Constraint
Layout
<
Linear
Layout
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:animateLayoutChanges=
"true"
android:
layout_height=
"wrap_content
"
>
android:
gravity=
"center_vertical
"
>
<
androidx.appcompat.widget.AppCompat
TextView
<TextView
android:id=
"@+id/module_title"
android:layout_weight=
"1"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:text=
"@{item.title}"
android:textAppearance=
"@style/AppearanceFoundation.
Body
"
android:textAppearance=
"@style/AppearanceFoundation.
Large
"
android:textColor=
"@color/color_primary_transient"
android:textStyle=
"bold"
tools:text=
"@tools:sample/lorem/random"
app:layout_constraintBottom_toBottomOf=
"parent"
app:layout_constraintEnd_toStartOf=
"@+id/module_button"
app:layout_constraintStart_toStartOf=
"parent"
app:layout_constraintTop_toTopOf=
"parent"
/>
tools:text=
"Installed"
/>
<com.google.android.material.button.MaterialButton
android:id=
"@+id/module_button"
style=
"@style/WidgetFoundation.Button.Text.Secondary"
gon
e=
"@{!item.hasButton}"
invisibl
e=
"@{!item.hasButton}"
android:onClick=
"@{() -> viewModel.sectionPressed(item)}"
android:layout_width=
"wrap_content"
android:layout_height=
"wrap_content"
android:layout_marginStart=
"@dimen/l1"
android:text=
"@{item.button}"
tools:text=
"@tools:sample/lorem"
android:textAllCaps=
"false"
app:icon=
"@{item.icon}"
app:layout_constraintBottom_toBottomOf=
"parent"
app:layout_constraintEnd_toEndOf=
"parent"
app:layout_constraintStart_toEndOf=
"@+id/module_title"
app:layout_constraintTop_toTopOf=
"parent"
/>
tools:text=
"Reboot"
/>
</
androidx.constraintlayout.widget.Constraint
Layout>
</
Linear
Layout>
</layout>
app/src/main/res/layout/item_settings_section.xml
View file @
f5aa6a3c
...
...
@@ -22,7 +22,7 @@
android:layout_width=
"match_parent"
android:layout_height=
"wrap_content"
android:text=
"@{item.title}"
android:textAppearance=
"@style/AppearanceFoundation.
Display
.Secondary"
android:textAppearance=
"@style/AppearanceFoundation.
Large
.Secondary"
android:textStyle=
"bold"
tools:text=
"@tools:sample/lorem"
/>
...
...
app/src/main/res/values/styles_md2_appearance.xml
View file @
f5aa6a3c
...
...
@@ -2,19 +2,19 @@
<resources>
<!--region Display-->
<style
name=
"AppearanceFoundation.
Display"
parent=
"TextAppearance.AppCompat.Display1
"
>
<style
name=
"AppearanceFoundation.
Large"
parent=
"TextAppearance.AppCompat.Large
"
>
<item
name=
"android:textColor"
>
?attr/colorOnSurface
</item>
</style>
<style
name=
"AppearanceFoundation.
Display
.Secondary"
>
<style
name=
"AppearanceFoundation.
Large
.Secondary"
>
<item
name=
"android:textColor"
>
?colorSecondary
</item>
</style>
<style
name=
"AppearanceFoundation.
Display
.Variant"
>
<style
name=
"AppearanceFoundation.
Large
.Variant"
>
<item
name=
"android:textColor"
>
?attr/colorOnSurfaceVariant
</item>
</style>
<style
name=
"AppearanceFoundation.Display.OnPrimary"
>
<style
name=
"AppearanceFoundation.Display.OnPrimary"
parent=
"TextAppearance.AppCompat.Display1"
>
<item
name=
"android:textColor"
>
?attr/colorOnPrimary
</item>
</style>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment