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
abc54571
Commit
abc54571
authored
Aug 21, 2020
by
topjohnwu
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Cleanup DownloadService
parent
4b238a9c
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
264 additions
and
222 deletions
+264
-222
BaseDownloadService.kt
...com/topjohnwu/magisk/core/download/BaseDownloadService.kt
+201
-0
DownloadService.kt
...ava/com/topjohnwu/magisk/core/download/DownloadService.kt
+29
-84
ManagerUpgrade.kt
...java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt
+8
-6
NotificationService.kt
...com/topjohnwu/magisk/core/download/NotificationService.kt
+0
-96
ProgressInputStream.kt
...va/com/topjohnwu/magisk/core/utils/ProgressInputStream.kt
+1
-2
HomeFragment.kt
...rc/main/java/com/topjohnwu/magisk/ui/home/HomeFragment.kt
+2
-0
HomeViewModel.kt
...c/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt
+7
-9
InstallFragment.kt
...n/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt
+2
-0
InstallViewModel.kt
.../java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt
+10
-12
ModuleFragment.kt
...ain/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt
+2
-0
ModuleViewModel.kt
...in/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt
+2
-13
No files found.
app/src/main/java/com/topjohnwu/magisk/core/download/
RemoteFile
Service.kt
→
app/src/main/java/com/topjohnwu/magisk/core/download/
BaseDownload
Service.kt
View file @
abc54571
...
@@ -2,17 +2,20 @@ package com.topjohnwu.magisk.core.download
...
@@ -2,17 +2,20 @@ package com.topjohnwu.magisk.core.download
import
android.app.Notification
import
android.app.Notification
import
android.content.Intent
import
android.content.Intent
import
androidx.lifecycle.LiveData
import
android.os.IBinder
import
androidx.lifecycle.LifecycleOwner
import
androidx.lifecycle.MutableLiveData
import
androidx.lifecycle.MutableLiveData
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.core.ForegroundTracker
import
com.topjohnwu.magisk.core.ForegroundTracker
import
com.topjohnwu.magisk.core.download.DownloadSubject.Magisk
import
com.topjohnwu.magisk.core.base.BaseService
import
com.topjohnwu.magisk.core.download.DownloadSubject.Module
import
com.topjohnwu.magisk.core.utils.ProgressInputStream
import
com.topjohnwu.magisk.core.utils.ProgressInputStream
import
com.topjohnwu.magisk.data.network.GithubRawServices
import
com.topjohnwu.magisk.data.network.GithubRawServices
import
com.topjohnwu.magisk.ktx.checkSum
import
com.topjohnwu.magisk.ktx.checkSum
import
com.topjohnwu.magisk.ktx.writeTo
import
com.topjohnwu.magisk.ktx.writeTo
import
com.topjohnwu.magisk.view.Notifications
import
com.topjohnwu.magisk.view.Notifications
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.cancel
import
kotlinx.coroutines.launch
import
kotlinx.coroutines.launch
import
okhttp3.ResponseBody
import
okhttp3.ResponseBody
import
org.koin.android.ext.android.inject
import
org.koin.android.ext.android.inject
...
@@ -20,81 +23,105 @@ import org.koin.core.KoinComponent
...
@@ -20,81 +23,105 @@ import org.koin.core.KoinComponent
import
timber.log.Timber
import
timber.log.Timber
import
java.io.IOException
import
java.io.IOException
import
java.io.InputStream
import
java.io.InputStream
import
java.util.*
import
kotlin.collections.HashMap
import
kotlin.random.Random.Default.nextInt
abstract
class
RemoteFileService
:
NotificationService
()
{
abstract
class
BaseDownloadService
:
BaseService
(),
KoinComponent
{
private
val
hasNotifications
get
()
=
notifications
.
isNotEmpty
()
private
val
notifications
=
Collections
.
synchronizedMap
(
HashMap
<
Int
,
Notification
.
Builder
>())
private
val
coroutineScope
=
CoroutineScope
(
Dispatchers
.
IO
)
val
service
:
GithubRawServices
by
inject
()
val
service
:
GithubRawServices
by
inject
()
// -- Service overrides
override
fun
onBind
(
intent
:
Intent
?):
IBinder
?
=
null
override
fun
onStartCommand
(
intent
:
Intent
?,
flags
:
Int
,
startId
:
Int
):
Int
{
override
fun
onStartCommand
(
intent
:
Intent
?,
flags
:
Int
,
startId
:
Int
):
Int
{
intent
?.
getParcelableExtra
<
DownloadSubject
>(
ARG_URL
)
?.
let
{
intent
?.
getParcelableExtra
<
DownloadSubject
>(
ARG_URL
)
?.
let
{
subject
->
update
(
it
.
hashCode
())
update
(
subject
.
notifyID
())
coroutineScope
.
launch
{
coroutineScope
.
launch
{
try
{
try
{
s
tart
(
it
)
s
ubject
.
startDownload
(
)
}
catch
(
e
:
IOException
)
{
}
catch
(
e
:
IOException
)
{
Timber
.
e
(
e
)
Timber
.
e
(
e
)
failNotify
(
i
t
)
notifyFail
(
subjec
t
)
}
}
}
}
}
}
return
START_REDELIVER_INTENT
return
START_REDELIVER_INTENT
}
}
override
fun
createNotification
()
=
Notifications
.
progress
(
this
,
""
)
override
fun
onTaskRemoved
(
rootIntent
:
Intent
?)
{
super
.
onTaskRemoved
(
rootIntent
)
notifications
.
forEach
{
cancel
(
it
.
key
)
}
notifications
.
clear
()
}
// ---
override
fun
onDestroy
()
{
super
.
onDestroy
()
coroutineScope
.
cancel
()
}
// -- Download logic
private
suspend
fun
start
(
subject
:
DownloadSubject
)
{
private
suspend
fun
DownloadSubject
.
startDownload
()
{
if
(
subject
!
is
Magisk
||
val
skip
=
this
is
DownloadSubject
.
Magisk
&&
file
.
exists
()
&&
file
.
checkSum
(
"MD5"
,
magisk
.
md5
)
!
subject
.
file
.
exists
()
||
if
(!
skip
)
{
!
subject
.
file
.
checkSum
(
"MD5"
,
subject
.
magisk
.
md5
))
{
val
stream
=
service
.
fetchFile
(
url
).
toProgressStream
(
this
)
val
stream
=
service
.
fetchFile
(
subject
.
url
).
toProgressStream
(
subject
)
when
(
this
)
{
when
(
subject
)
{
is
DownloadSubject
.
Module
->
// Download and process on-the-fly
is
Module
->
stream
.
toModule
(
file
,
service
.
fetchInstaller
().
byteStream
())
stream
.
toModule
(
subject
.
file
,
service
.
fetchInstaller
().
byteStream
())
else
->
else
->
stream
.
writeTo
(
subject
.
file
)
stream
.
writeTo
(
file
)
}
}
}
}
val
newId
=
finishNotify
(
subject
)
val
newId
=
notifyFinish
(
this
)
if
(
ForegroundTracker
.
hasForeground
)
{
if
(
ForegroundTracker
.
hasForeground
)
onFinished
(
subject
,
newId
)
onFinish
(
this
,
newId
)
}
if
(!
hasNotifications
)
stopSelf
()
}
}
private
fun
ResponseBody
.
toProgressStream
(
subject
:
DownloadSubject
):
InputStream
{
private
fun
ResponseBody
.
toProgressStream
(
subject
:
DownloadSubject
):
InputStream
{
val
max
Raw
=
contentLength
()
val
max
=
contentLength
()
val
max
=
maxRaw
/
1
_000_000f
val
total
=
max
.
toFloat
()
/
1048576
val
id
=
subject
.
hashCode
()
val
id
=
subject
.
notifyID
()
update
(
id
)
{
it
.
setContentTitle
(
subject
.
title
)
}
update
(
id
)
{
it
.
setContentTitle
(
subject
.
title
)
}
return
ProgressInputStream
(
byteStream
())
{
return
ProgressInputStream
(
byteStream
())
{
val
progress
=
it
/
1
_000_000f
val
progress
=
it
.
toFloat
()
/
1048576
update
(
id
)
{
notification
->
update
(
id
)
{
notification
->
if
(
max
Raw
>
0
)
{
if
(
max
>
0
)
{
send
(
progress
/
max
,
subject
)
broadcast
(
progress
/
total
,
subject
)
notification
notification
.
setProgress
(
max
Raw
.
toInt
(),
it
.
toInt
(),
false
)
.
setProgress
(
max
.
toInt
(),
it
.
toInt
(),
false
)
.
setContentText
(
"%.2f / %.2f MB"
.
format
(
progress
,
max
))
.
setContentText
(
"%.2f / %.2f MB"
.
format
(
progress
,
total
))
}
else
{
}
else
{
send
(-
1f
,
subject
)
broadcast
(-
1f
,
subject
)
notification
.
setContentText
(
"%.2f MB / ??"
.
format
(
progress
))
notification
.
setContentText
(
"%.2f MB / ??"
.
format
(
progress
))
}
}
}
}
}
}
}
}
private
fun
failNotify
(
subject
:
DownloadSubject
)
=
lastNotify
(
subject
.
hashCode
())
{
// --- Notification managements
send
(
0f
,
subject
)
fun
DownloadSubject
.
notifyID
()
=
hashCode
()
private
fun
notifyFail
(
subject
:
DownloadSubject
)
=
lastNotify
(
subject
.
notifyID
())
{
broadcast
(-
1f
,
subject
)
it
.
setContentText
(
getString
(
R
.
string
.
download_file_error
))
it
.
setContentText
(
getString
(
R
.
string
.
download_file_error
))
.
setSmallIcon
(
android
.
R
.
drawable
.
stat_notify_error
)
.
setSmallIcon
(
android
.
R
.
drawable
.
stat_notify_error
)
.
setOngoing
(
false
)
.
setOngoing
(
false
)
}
}
private
fun
finishNotify
(
subject
:
DownloadSubject
)
=
lastNotify
(
subject
.
hashCode
())
{
private
fun
notifyFinish
(
subject
:
DownloadSubject
)
=
lastNotify
(
subject
.
notifyID
())
{
send
(
1f
,
subject
)
broadcast
(
1f
,
subject
)
it
.
addActions
(
subject
)
it
.
setIntent
(
subject
)
.
setContentText
(
getString
(
R
.
string
.
download_complete
))
.
setContentText
(
getString
(
R
.
string
.
download_complete
))
.
setSmallIcon
(
android
.
R
.
drawable
.
stat_sys_download_done
)
.
setSmallIcon
(
android
.
R
.
drawable
.
stat_sys_download_done
)
.
setProgress
(
0
,
0
,
false
)
.
setProgress
(
0
,
0
,
false
)
...
@@ -102,27 +129,73 @@ abstract class RemoteFileService : NotificationService() {
...
@@ -102,27 +129,73 @@ abstract class RemoteFileService : NotificationService() {
.
setAutoCancel
(
true
)
.
setAutoCancel
(
true
)
}
}
// ---
private
fun
create
()
=
Notifications
.
progress
(
this
,
""
)
fun
update
(
id
:
Int
,
editor
:
(
Notification
.
Builder
)
->
Unit
=
{})
{
val
wasEmpty
=
!
hasNotifications
val
notification
=
notifications
.
getOrPut
(
id
,
::
create
).
also
(
editor
)
if
(
wasEmpty
)
updateForeground
()
else
notify
(
id
,
notification
.
build
())
}
private
fun
lastNotify
(
id
:
Int
,
editor
:
(
Notification
.
Builder
)
->
Notification
.
Builder
?
=
{
null
}
)
:
Int
{
val
notification
=
remove
(
id
)
?.
run
(
editor
)
?:
return
-
1
val
newId
:
Int
=
nextInt
()
notify
(
newId
,
notification
.
build
())
return
newId
}
private
fun
remove
(
id
:
Int
)
=
notifications
.
remove
(
id
)
?.
also
{
updateForeground
()
cancel
(
id
)
}
private
fun
notify
(
id
:
Int
,
notification
:
Notification
)
{
Notifications
.
mgr
.
notify
(
id
,
notification
)
}
protected
fun
cancel
(
id
:
Int
)
{
Notifications
.
mgr
.
cancel
(
id
)
}
private
fun
updateForeground
()
{
if
(
hasNotifications
)
{
val
(
id
,
notification
)
=
notifications
.
entries
.
first
()
startForeground
(
id
,
notification
.
build
())
}
else
{
stopForeground
(
false
)
}
}
protected
abstract
suspend
fun
onFinished
(
subject
:
DownloadSubject
,
id
:
Int
)
// --- Implement custom logic
protected
abstract
fun
Notification
.
Builder
.
addActions
(
subject
:
DownloadSubject
)
protected
abstract
suspend
fun
onFinish
(
subject
:
DownloadSubject
,
id
:
Int
)
protected
abstract
fun
Notification
.
Builder
.
setIntent
(
subject
:
DownloadSubject
)
:
Notification
.
Builder
:
Notification
.
Builder
// ---
companion
object
:
KoinComponent
{
companion
object
:
KoinComponent
{
const
val
ARG_URL
=
"arg_url"
const
val
ARG_URL
=
"arg_url"
private
val
internalProgressBroadcast
=
MutableLiveData
<
Pair
<
Float
,
DownloadSubject
>>()
private
val
progressBroadcast
=
MutableLiveData
<
Pair
<
Float
,
DownloadSubject
>>()
val
progressBroadcast
:
LiveData
<
Pair
<
Float
,
DownloadSubject
>>
get
()
=
internalProgressBroadcast
fun
send
(
progress
:
Float
,
subject
:
DownloadSubject
)
{
fun
observeProgress
(
owner
:
LifecycleOwner
,
callback
:
(
Float
,
DownloadSubject
)
->
Unit
)
{
internalProgressBroadcast
.
postValue
(
progress
to
subject
)
progressBroadcast
.
value
=
null
progressBroadcast
.
observe
(
owner
)
{
val
(
progress
,
subject
)
=
it
?:
return
@observe
callback
(
progress
,
subject
)
}
}
}
fun
reset
(
)
{
private
fun
broadcast
(
progress
:
Float
,
subject
:
DownloadSubject
)
{
internalProgressBroadcast
.
value
=
null
progressBroadcast
.
postValue
(
progress
to
subject
)
}
}
}
}
}
}
app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt
View file @
abc54571
...
@@ -6,139 +6,84 @@ import android.app.PendingIntent
...
@@ -6,139 +6,84 @@ import android.app.PendingIntent
import
android.content.Context
import
android.content.Context
import
android.content.Intent
import
android.content.Intent
import
android.os.Build
import
android.os.Build
import
android.webkit.MimeTypeMap
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.core.download.Configuration.*
import
com.topjohnwu.magisk.core.download.Configuration.*
import
com.topjohnwu.magisk.core.download.Configuration.Flash.Secondary
import
com.topjohnwu.magisk.core.download.Configuration.Flash.Secondary
import
com.topjohnwu.magisk.core.download.DownloadSubject.*
import
com.topjohnwu.magisk.core.download.DownloadSubject.*
import
com.topjohnwu.magisk.core.intent
import
com.topjohnwu.magisk.core.intent
import
com.topjohnwu.magisk.core.tasks.EnvFixTask
import
com.topjohnwu.magisk.core.tasks.EnvFixTask
import
com.topjohnwu.magisk.ktx.chooser
import
com.topjohnwu.magisk.ktx.exists
import
com.topjohnwu.magisk.ktx.provide
import
com.topjohnwu.magisk.ui.flash.FlashFragment
import
com.topjohnwu.magisk.ui.flash.FlashFragment
import
com.topjohnwu.magisk.utils.APKInstall
import
com.topjohnwu.magisk.utils.APKInstall
import
org.koin.core.get
import
java.io.File
import
kotlin.random.Random.Default.nextInt
import
kotlin.random.Random.Default.nextInt
/* More of a facade for [RemoteFileService], but whatever... */
@SuppressLint
(
"Registered"
)
@SuppressLint
(
"Registered"
)
open
class
DownloadService
:
RemoteFile
Service
()
{
open
class
DownloadService
:
BaseDownload
Service
()
{
private
val
context
get
()
=
this
private
val
context
get
()
=
this
private
val
File
.
type
get
()
=
MimeTypeMap
.
getSingleton
()
override
suspend
fun
onFinish
(
subject
:
DownloadSubject
,
id
:
Int
)
=
when
(
subject
)
{
.
getMimeTypeFromExtension
(
extension
)
is
Magisk
->
subject
.
onFinish
(
id
)
?:
"resource/folder"
is
Module
->
subject
.
onFinish
(
id
)
is
Manager
->
subject
.
onFinish
(
id
)
override
suspend
fun
onFinished
(
subject
:
DownloadSubject
,
id
:
Int
)
=
when
(
subject
)
{
is
Magisk
->
onFinished
(
subject
,
id
)
is
Module
->
onFinished
(
subject
,
id
)
is
Manager
->
onFinished
(
subject
,
id
)
}
}
private
suspend
fun
onFinished
(
private
suspend
fun
Magisk
.
onFinish
(
id
:
Int
)
=
when
(
val
conf
=
configuration
)
{
subject
:
Magisk
,
Uninstall
->
FlashFragment
.
uninstall
(
file
,
id
)
id
:
Int
)
=
when
(
val
conf
=
subject
.
configuration
)
{
Uninstall
->
FlashFragment
.
uninstall
(
subject
.
file
,
id
)
EnvFix
->
{
EnvFix
->
{
remove
(
id
)
cancel
(
id
)
EnvFixTask
(
subject
.
file
).
exec
()
EnvFixTask
(
file
).
exec
()
Unit
Unit
}
}
is
Patch
->
FlashFragment
.
patch
(
subject
.
file
,
conf
.
fileUri
,
id
)
is
Patch
->
FlashFragment
.
patch
(
file
,
conf
.
fileUri
,
id
)
is
Flash
->
FlashFragment
.
flash
(
subject
.
file
,
conf
is
Secondary
,
id
)
is
Flash
->
FlashFragment
.
flash
(
file
,
conf
is
Secondary
,
id
)
else
->
Unit
else
->
Unit
}
}
private
fun
onFinished
(
private
fun
Module
.
onFinish
(
id
:
Int
)
=
when
(
configuration
)
{
subject
:
Module
,
is
Flash
->
FlashFragment
.
install
(
file
,
id
)
id
:
Int
)
=
when
(
subject
.
configuration
)
{
is
Flash
->
FlashFragment
.
install
(
subject
.
file
,
id
)
else
->
Unit
else
->
Unit
}
}
private
suspend
fun
onFinished
(
private
suspend
fun
Manager
.
onFinish
(
id
:
Int
)
{
subject
:
Manager
,
handleAPK
(
this
)
id
:
Int
cancel
(
id
)
)
{
handleAPK
(
subject
)
remove
(
id
)
when
(
subject
.
configuration
)
{
is
APK
.
Upgrade
->
APKInstall
.
install
(
this
,
subject
.
file
)
is
APK
.
Restore
->
Unit
}
}
}
// ---
// ---
Customize finish notification
override
fun
Notification
.
Builder
.
addActions
(
subject
:
DownloadSubject
)
override
fun
Notification
.
Builder
.
setIntent
(
subject
:
DownloadSubject
)
=
when
(
subject
)
{
=
when
(
subject
)
{
is
Magisk
->
addActions
(
subject
)
is
Magisk
->
setIntent
(
subject
)
is
Module
->
addActions
(
subject
)
is
Module
->
setIntent
(
subject
)
is
Manager
->
addActions
(
subject
)
is
Manager
->
setIntent
(
subject
)
}
}
private
fun
Notification
.
Builder
.
addActions
(
subject
:
Magisk
)
private
fun
Notification
.
Builder
.
setIntent
(
subject
:
Magisk
)
=
when
(
val
conf
=
subject
.
configuration
)
{
=
when
(
val
conf
=
subject
.
configuration
)
{
Download
->
apply
{
fileIntent
(
subject
.
file
.
parentFile
!!
)
.
takeIf
{
it
.
exists
(
get
())
}
?.
let
{
addAction
(
0
,
R
.
string
.
download_open_parent
,
it
.
chooser
())
}
fileIntent
(
subject
.
file
)
.
takeIf
{
it
.
exists
(
get
())
}
?.
let
{
addAction
(
0
,
R
.
string
.
download_open_self
,
it
.
chooser
())
}
}
Uninstall
->
setContentIntent
(
FlashFragment
.
uninstallIntent
(
context
,
subject
.
file
))
Uninstall
->
setContentIntent
(
FlashFragment
.
uninstallIntent
(
context
,
subject
.
file
))
is
Flash
->
setContentIntent
(
FlashFragment
.
flashIntent
(
context
,
subject
.
file
,
conf
is
Secondary
))
is
Flash
->
setContentIntent
(
FlashFragment
.
flashIntent
(
context
,
subject
.
file
,
conf
is
Secondary
))
is
Patch
->
setContentIntent
(
FlashFragment
.
patchIntent
(
context
,
subject
.
file
,
conf
.
fileUri
))
is
Patch
->
setContentIntent
(
FlashFragment
.
patchIntent
(
context
,
subject
.
file
,
conf
.
fileUri
))
else
->
this
else
->
setContentIntent
(
Intent
())
}
}
private
fun
Notification
.
Builder
.
addActions
(
subject
:
Module
)
private
fun
Notification
.
Builder
.
setIntent
(
subject
:
Module
)
=
when
(
subject
.
configuration
)
{
=
when
(
subject
.
configuration
)
{
Download
->
this
.
apply
{
fileIntent
(
subject
.
file
.
parentFile
!!
)
.
takeIf
{
it
.
exists
(
get
())
}
?.
let
{
addAction
(
0
,
R
.
string
.
download_open_parent
,
it
.
chooser
())
}
fileIntent
(
subject
.
file
)
.
takeIf
{
it
.
exists
(
get
())
}
?.
let
{
addAction
(
0
,
R
.
string
.
download_open_self
,
it
.
chooser
())
}
}
is
Flash
->
setContentIntent
(
FlashFragment
.
installIntent
(
context
,
subject
.
file
))
is
Flash
->
setContentIntent
(
FlashFragment
.
installIntent
(
context
,
subject
.
file
))
else
->
this
else
->
setContentIntent
(
Intent
())
}
}
private
fun
Notification
.
Builder
.
addActions
(
subject
:
Manager
)
private
fun
Notification
.
Builder
.
setIntent
(
subject
:
Manager
)
=
when
(
subject
.
configuration
)
{
=
when
(
subject
.
configuration
)
{
APK
.
Upgrade
->
setContentIntent
(
APKInstall
.
installIntent
(
context
,
subject
.
file
))
APK
.
Upgrade
->
setContentIntent
(
APKInstall
.
installIntent
(
context
,
subject
.
file
))
else
->
this
else
->
setContentIntent
(
Intent
())
}
}
@Suppress
(
"ReplaceSingleLineLet"
)
private
fun
Notification
.
Builder
.
setContentIntent
(
intent
:
Intent
)
=
private
fun
Notification
.
Builder
.
setContentIntent
(
intent
:
Intent
)
=
setContentIntent
(
setContentIntent
(
PendingIntent
.
getActivity
(
context
,
nextInt
(),
intent
,
PendingIntent
.
FLAG_ONE_SHOT
)
PendingIntent
.
getActivity
(
context
,
nextInt
(),
intent
,
PendingIntent
.
FLAG_ONE_SHOT
)
)
)
@Suppress
(
"ReplaceSingleLineLet"
)
private
fun
Notification
.
Builder
.
addAction
(
icon
:
Int
,
title
:
Int
,
intent
:
Intent
)
=
addAction
(
icon
,
getString
(
title
),
PendingIntent
.
getActivity
(
context
,
nextInt
(),
intent
,
PendingIntent
.
FLAG_ONE_SHOT
)
)
// ---
// ---
private
fun
fileIntent
(
file
:
File
):
Intent
{
return
Intent
(
Intent
.
ACTION_VIEW
)
.
setDataAndType
(
file
.
provide
(
this
),
file
.
type
)
.
addFlags
(
Intent
.
FLAG_ACTIVITY_NEW_TASK
)
.
addFlags
(
Intent
.
FLAG_GRANT_READ_URI_PERMISSION
)
}
class
Builder
{
class
Builder
{
lateinit
var
subject
:
DownloadSubject
lateinit
var
subject
:
DownloadSubject
}
}
...
@@ -150,7 +95,7 @@ open class DownloadService : RemoteFileService() {
...
@@ -150,7 +95,7 @@ open class DownloadService : RemoteFileService() {
val
builder
=
Builder
().
apply
(
argBuilder
)
val
builder
=
Builder
().
apply
(
argBuilder
)
val
intent
=
app
.
intent
<
DownloadService
>().
putExtra
(
ARG_URL
,
builder
.
subject
)
val
intent
=
app
.
intent
<
DownloadService
>().
putExtra
(
ARG_URL
,
builder
.
subject
)
if
(
Build
.
VERSION
.
SDK_INT
>=
Build
.
VERSION_CODES
.
O
)
{
if
(
Build
.
VERSION
.
SDK_INT
>=
26
)
{
app
.
startForegroundService
(
intent
)
app
.
startForegroundService
(
intent
)
}
else
{
}
else
{
app
.
startService
(
intent
)
app
.
startService
(
intent
)
...
...
app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt
View file @
abc54571
...
@@ -12,10 +12,11 @@ import com.topjohnwu.magisk.core.intent
...
@@ -12,10 +12,11 @@ import com.topjohnwu.magisk.core.intent
import
com.topjohnwu.magisk.core.isRunningAsStub
import
com.topjohnwu.magisk.core.isRunningAsStub
import
com.topjohnwu.magisk.core.utils.PatchAPK
import
com.topjohnwu.magisk.core.utils.PatchAPK
import
com.topjohnwu.magisk.ktx.writeTo
import
com.topjohnwu.magisk.ktx.writeTo
import
com.topjohnwu.magisk.utils.APKInstall
import
com.topjohnwu.superuser.Shell
import
com.topjohnwu.superuser.Shell
import
java.io.File
import
java.io.File
private
fun
RemoteFile
Service
.
patch
(
apk
:
File
,
id
:
Int
)
{
private
fun
Download
Service
.
patch
(
apk
:
File
,
id
:
Int
)
{
if
(
packageName
==
BuildConfig
.
APPLICATION_ID
)
if
(
packageName
==
BuildConfig
.
APPLICATION_ID
)
return
return
...
@@ -31,7 +32,7 @@ private fun RemoteFileService.patch(apk: File, id: Int) {
...
@@ -31,7 +32,7 @@ private fun RemoteFileService.patch(apk: File, id: Int) {
patched
.
renameTo
(
apk
)
patched
.
renameTo
(
apk
)
}
}
private
suspend
fun
RemoteFile
Service
.
upgrade
(
apk
:
File
,
id
:
Int
)
{
private
suspend
fun
Download
Service
.
upgrade
(
apk
:
File
,
id
:
Int
)
{
if
(
isRunningAsStub
)
{
if
(
isRunningAsStub
)
{
// Move to upgrade location
// Move to upgrade location
apk
.
copyTo
(
DynAPK
.
update
(
this
),
overwrite
=
true
)
apk
.
copyTo
(
DynAPK
.
update
(
this
),
overwrite
=
true
)
...
@@ -49,9 +50,10 @@ private suspend fun RemoteFileService.upgrade(apk: File, id: Int) {
...
@@ -49,9 +50,10 @@ private suspend fun RemoteFileService.upgrade(apk: File, id: Int) {
}
else
{
}
else
{
patch
(
apk
,
id
)
patch
(
apk
,
id
)
}
}
APKInstall
.
install
(
this
,
apk
)
}
}
private
fun
RemoteFile
Service
.
restore
(
apk
:
File
,
id
:
Int
)
{
private
fun
Download
Service
.
restore
(
apk
:
File
,
id
:
Int
)
{
update
(
id
)
{
update
(
id
)
{
it
.
setProgress
(
0
,
0
,
true
)
it
.
setProgress
(
0
,
0
,
true
)
.
setProgress
(
0
,
0
,
true
)
.
setProgress
(
0
,
0
,
true
)
...
@@ -64,8 +66,8 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
...
@@ -64,8 +66,8 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
Shell
.
su
(
"pm install $apk && pm uninstall $packageName"
).
exec
()
Shell
.
su
(
"pm install $apk && pm uninstall $packageName"
).
exec
()
}
}
suspend
fun
RemoteFile
Service
.
handleAPK
(
subject
:
DownloadSubject
.
Manager
)
=
suspend
fun
Download
Service
.
handleAPK
(
subject
:
DownloadSubject
.
Manager
)
=
when
(
subject
.
configuration
)
{
when
(
subject
.
configuration
)
{
is
Upgrade
->
upgrade
(
subject
.
file
,
subject
.
hashCode
())
is
Upgrade
->
upgrade
(
subject
.
file
,
subject
.
notifyID
())
is
Restore
->
restore
(
subject
.
file
,
subject
.
hashCode
())
is
Restore
->
restore
(
subject
.
file
,
subject
.
notifyID
())
}
}
app/src/main/java/com/topjohnwu/magisk/core/download/NotificationService.kt
deleted
100644 → 0
View file @
4b238a9c
package
com.topjohnwu.magisk.core.download
import
android.app.Notification
import
android.content.Intent
import
android.os.IBinder
import
com.topjohnwu.magisk.core.base.BaseService
import
com.topjohnwu.magisk.view.Notifications
import
kotlinx.coroutines.CoroutineScope
import
kotlinx.coroutines.Dispatchers
import
kotlinx.coroutines.cancel
import
org.koin.core.KoinComponent
import
java.util.*
import
kotlin.collections.HashMap
import
kotlin.random.Random.Default.nextInt
abstract
class
NotificationService
:
BaseService
(),
KoinComponent
{
private
val
hasNotifications
get
()
=
notifications
.
isNotEmpty
()
private
val
notifications
=
Collections
.
synchronizedMap
(
HashMap
<
Int
,
Notification
.
Builder
>())
val
coroutineScope
=
CoroutineScope
(
Dispatchers
.
IO
)
override
fun
onTaskRemoved
(
rootIntent
:
Intent
?)
{
super
.
onTaskRemoved
(
rootIntent
)
notifications
.
forEach
{
cancel
(
it
.
key
)
}
notifications
.
clear
()
}
override
fun
onDestroy
()
{
super
.
onDestroy
()
coroutineScope
.
cancel
()
}
abstract
fun
createNotification
():
Notification
.
Builder
// --
fun
update
(
id
:
Int
,
body
:
(
Notification
.
Builder
)
->
Unit
=
{}
)
{
val
wasEmpty
=
notifications
.
isEmpty
()
val
notification
=
notifications
.
getOrPut
(
id
,
::
createNotification
).
also
(
body
)
if
(
wasEmpty
)
updateForeground
()
else
notify
(
id
,
notification
.
build
())
}
protected
fun
lastNotify
(
id
:
Int
,
editBody
:
(
Notification
.
Builder
)
->
Notification
.
Builder
?
=
{
null
}
)
:
Int
{
val
currentNotification
=
remove
(
id
)
?.
run
(
editBody
)
var
newId
=
-
1
currentNotification
?.
let
{
newId
=
nextInt
(
Int
.
MAX_VALUE
)
notify
(
newId
,
it
.
build
())
}
if
(!
hasNotifications
)
{
stopSelf
()
}
return
newId
}
protected
fun
remove
(
id
:
Int
)
=
notifications
.
remove
(
id
).
also
{
cancel
(
id
)
updateForeground
()
}
// ---
private
fun
notify
(
id
:
Int
,
notification
:
Notification
)
{
Notifications
.
mgr
.
notify
(
id
,
notification
)
}
private
fun
cancel
(
id
:
Int
)
{
Notifications
.
mgr
.
cancel
(
id
)
}
private
fun
updateForeground
()
{
if
(
hasNotifications
)
{
val
first
=
notifications
.
entries
.
first
()
startForeground
(
first
.
key
,
first
.
value
.
build
())
}
else
{
stopForeground
(
true
)
}
}
// --
override
fun
onBind
(
p0
:
Intent
?):
IBinder
?
=
null
}
app/src/main/java/com/topjohnwu/magisk/core/utils/ProgressInputStream.kt
View file @
abc54571
package
com.topjohnwu.magisk.core.utils
package
com.topjohnwu.magisk.core.utils
import
com.topjohnwu.superuser.internal.UiThreadHandler
import
java.io.FilterInputStream
import
java.io.FilterInputStream
import
java.io.InputStream
import
java.io.InputStream
...
@@ -16,7 +15,7 @@ class ProgressInputStream(
...
@@ -16,7 +15,7 @@ class ProgressInputStream(
val
cur
=
System
.
currentTimeMillis
()
val
cur
=
System
.
currentTimeMillis
()
if
(
cur
-
lastUpdate
>
1000
)
{
if
(
cur
-
lastUpdate
>
1000
)
{
lastUpdate
=
cur
lastUpdate
=
cur
UiThreadHandler
.
run
{
progressEmitter
(
bytesRead
)
}
progressEmitter
(
bytesRead
)
}
}
}
}
...
...
app/src/main/java/com/topjohnwu/magisk/ui/home/HomeFragment.kt
View file @
abc54571
...
@@ -4,6 +4,7 @@ import android.os.Bundle
...
@@ -4,6 +4,7 @@ import android.os.Bundle
import
android.view.*
import
android.view.*
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.arch.BaseUIFragment
import
com.topjohnwu.magisk.arch.BaseUIFragment
import
com.topjohnwu.magisk.core.download.BaseDownloadService
import
com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import
com.topjohnwu.magisk.databinding.FragmentHomeMd2Binding
import
com.topjohnwu.magisk.events.RebootEvent
import
com.topjohnwu.magisk.events.RebootEvent
import
com.topjohnwu.superuser.Shell
import
com.topjohnwu.superuser.Shell
...
@@ -18,6 +19,7 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
...
@@ -18,6 +19,7 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
super
.
onStart
()
super
.
onStart
()
activity
.
title
=
resources
.
getString
(
R
.
string
.
section_home
)
activity
.
title
=
resources
.
getString
(
R
.
string
.
section_home
)
setHasOptionsMenu
(
true
)
setHasOptionsMenu
(
true
)
BaseDownloadService
.
observeProgress
(
this
,
viewModel
::
onProgressUpdate
)
}
}
override
fun
onCreateView
(
override
fun
onCreateView
(
...
...
app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt
View file @
abc54571
...
@@ -8,8 +8,8 @@ import com.topjohnwu.magisk.R
...
@@ -8,8 +8,8 @@ import com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.arch.*
import
com.topjohnwu.magisk.arch.*
import
com.topjohnwu.magisk.core.Config
import
com.topjohnwu.magisk.core.Config
import
com.topjohnwu.magisk.core.Info
import
com.topjohnwu.magisk.core.Info
import
com.topjohnwu.magisk.core.download.DownloadSubject
import
com.topjohnwu.magisk.core.download.DownloadSubject.Manager
import
com.topjohnwu.magisk.core.download.DownloadSubject.Manager
import
com.topjohnwu.magisk.core.download.RemoteFileService
import
com.topjohnwu.magisk.core.model.MagiskJson
import
com.topjohnwu.magisk.core.model.MagiskJson
import
com.topjohnwu.magisk.core.model.ManagerJson
import
com.topjohnwu.magisk.core.model.ManagerJson
import
com.topjohnwu.magisk.data.repository.MagiskRepository
import
com.topjohnwu.magisk.data.repository.MagiskRepository
...
@@ -77,14 +77,6 @@ class HomeViewModel(
...
@@ -77,14 +77,6 @@ class HomeViewModel(
private
var
shownDialog
=
false
private
var
shownDialog
=
false
init
{
RemoteFileService
.
progressBroadcast
.
observeForever
{
when
(
it
?.
second
)
{
is
Manager
->
stateManagerProgress
=
it
.
first
.
times
(
100f
).
roundToInt
()
}
}
}
override
fun
refresh
()
=
viewModelScope
.
launch
{
override
fun
refresh
()
=
viewModelScope
.
launch
{
notifyPropertyChanged
(
BR
.
showUninstall
)
notifyPropertyChanged
(
BR
.
showUninstall
)
repoMagisk
.
fetchUpdate
()
?.
apply
{
repoMagisk
.
fetchUpdate
()
?.
apply
{
...
@@ -119,6 +111,12 @@ class HomeViewModel(
...
@@ -119,6 +111,12 @@ class HomeViewModel(
}
}
}.
publish
()
}.
publish
()
fun
onProgressUpdate
(
progress
:
Float
,
subject
:
DownloadSubject
)
{
when
(
subject
)
{
is
Manager
->
stateManagerProgress
=
progress
.
times
(
100f
).
roundToInt
()
}
}
fun
onLinkPressed
(
link
:
String
)
=
OpenInappLinkEvent
(
link
).
publish
()
fun
onLinkPressed
(
link
:
String
)
=
OpenInappLinkEvent
(
link
).
publish
()
fun
onDeletePressed
()
=
UninstallDialog
().
publish
()
fun
onDeletePressed
()
=
UninstallDialog
().
publish
()
...
...
app/src/main/java/com/topjohnwu/magisk/ui/install/InstallFragment.kt
View file @
abc54571
...
@@ -4,6 +4,7 @@ import android.content.Intent
...
@@ -4,6 +4,7 @@ import android.content.Intent
import
androidx.lifecycle.viewModelScope
import
androidx.lifecycle.viewModelScope
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.arch.BaseUIFragment
import
com.topjohnwu.magisk.arch.BaseUIFragment
import
com.topjohnwu.magisk.core.download.BaseDownloadService
import
com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import
com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import
com.topjohnwu.magisk.events.RequestFileEvent
import
com.topjohnwu.magisk.events.RequestFileEvent
import
org.koin.androidx.viewmodel.ext.android.viewModel
import
org.koin.androidx.viewmodel.ext.android.viewModel
...
@@ -24,6 +25,7 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
...
@@ -24,6 +25,7 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
// Allow markwon to run in viewmodel scope
// Allow markwon to run in viewmodel scope
binding
.
releaseNotes
.
tag
=
viewModel
.
viewModelScope
binding
.
releaseNotes
.
tag
=
viewModel
.
viewModelScope
BaseDownloadService
.
observeProgress
(
this
,
viewModel
::
onProgressUpdate
)
}
}
}
}
app/src/main/java/com/topjohnwu/magisk/ui/install/InstallViewModel.kt
View file @
abc54571
...
@@ -11,7 +11,6 @@ import com.topjohnwu.magisk.core.Info
...
@@ -11,7 +11,6 @@ import com.topjohnwu.magisk.core.Info
import
com.topjohnwu.magisk.core.download.Configuration
import
com.topjohnwu.magisk.core.download.Configuration
import
com.topjohnwu.magisk.core.download.DownloadService
import
com.topjohnwu.magisk.core.download.DownloadService
import
com.topjohnwu.magisk.core.download.DownloadSubject
import
com.topjohnwu.magisk.core.download.DownloadSubject
import
com.topjohnwu.magisk.core.download.RemoteFileService
import
com.topjohnwu.magisk.data.repository.StringRepository
import
com.topjohnwu.magisk.data.repository.StringRepository
import
com.topjohnwu.magisk.events.RequestFileEvent
import
com.topjohnwu.magisk.events.RequestFileEvent
import
com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
import
com.topjohnwu.magisk.events.dialog.SecondSlotWarningDialog
...
@@ -60,22 +59,21 @@ class InstallViewModel(
...
@@ -60,22 +59,21 @@ class InstallViewModel(
set
(
value
)
=
set
(
value
,
field
,
{
field
=
it
},
BR
.
notes
)
set
(
value
)
=
set
(
value
,
field
,
{
field
=
it
},
BR
.
notes
)
init
{
init
{
RemoteFileService
.
reset
()
RemoteFileService
.
progressBroadcast
.
observeForever
{
val
(
progress
,
subject
)
=
it
?:
return
@observeForever
if
(
subject
!
is
DownloadSubject
.
Magisk
)
{
return
@observeForever
}
this
.
progress
=
progress
.
times
(
100
).
roundToInt
()
if
(
this
.
progress
>=
100
)
{
state
=
State
.
LOADED
}
}
viewModelScope
.
launch
{
viewModelScope
.
launch
{
notes
=
stringRepo
.
getString
(
Info
.
remote
.
magisk
.
note
)
notes
=
stringRepo
.
getString
(
Info
.
remote
.
magisk
.
note
)
}
}
}
}
fun
onProgressUpdate
(
progress
:
Float
,
subject
:
DownloadSubject
)
{
if
(
subject
!
is
DownloadSubject
.
Magisk
)
{
return
}
this
.
progress
=
progress
.
times
(
100
).
roundToInt
()
if
(
this
.
progress
>=
100
)
{
state
=
State
.
LOADED
}
}
fun
step
(
nextStep
:
Int
)
{
fun
step
(
nextStep
:
Int
)
{
step
=
nextStep
step
=
nextStep
}
}
...
...
app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleFragment.kt
View file @
abc54571
...
@@ -12,6 +12,7 @@ import com.topjohnwu.magisk.R
...
@@ -12,6 +12,7 @@ import com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.arch.BaseUIFragment
import
com.topjohnwu.magisk.arch.BaseUIFragment
import
com.topjohnwu.magisk.arch.ReselectionTarget
import
com.topjohnwu.magisk.arch.ReselectionTarget
import
com.topjohnwu.magisk.arch.ViewEvent
import
com.topjohnwu.magisk.arch.ViewEvent
import
com.topjohnwu.magisk.core.download.BaseDownloadService
import
com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import
com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import
com.topjohnwu.magisk.events.InstallExternalModuleEvent
import
com.topjohnwu.magisk.events.InstallExternalModuleEvent
import
com.topjohnwu.magisk.ktx.hideKeyboard
import
com.topjohnwu.magisk.ktx.hideKeyboard
...
@@ -50,6 +51,7 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
...
@@ -50,6 +51,7 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
super
.
onStart
()
super
.
onStart
()
setHasOptionsMenu
(
true
)
setHasOptionsMenu
(
true
)
activity
.
title
=
resources
.
getString
(
R
.
string
.
modules
)
activity
.
title
=
resources
.
getString
(
R
.
string
.
modules
)
BaseDownloadService
.
observeProgress
(
this
,
viewModel
::
onProgressUpdate
)
}
}
override
fun
onViewCreated
(
view
:
View
,
savedInstanceState
:
Bundle
?)
{
override
fun
onViewCreated
(
view
:
View
,
savedInstanceState
:
Bundle
?)
{
...
...
app/src/main/java/com/topjohnwu/magisk/ui/module/ModuleViewModel.kt
View file @
abc54571
...
@@ -2,14 +2,12 @@ package com.topjohnwu.magisk.ui.module
...
@@ -2,14 +2,12 @@ package com.topjohnwu.magisk.ui.module
import
androidx.databinding.Bindable
import
androidx.databinding.Bindable
import
androidx.databinding.ObservableArrayList
import
androidx.databinding.ObservableArrayList
import
androidx.lifecycle.Observer
import
androidx.lifecycle.viewModelScope
import
androidx.lifecycle.viewModelScope
import
com.topjohnwu.magisk.BR
import
com.topjohnwu.magisk.BR
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.R
import
com.topjohnwu.magisk.arch.*
import
com.topjohnwu.magisk.arch.*
import
com.topjohnwu.magisk.core.Config
import
com.topjohnwu.magisk.core.Config
import
com.topjohnwu.magisk.core.download.DownloadSubject
import
com.topjohnwu.magisk.core.download.DownloadSubject
import
com.topjohnwu.magisk.core.download.RemoteFileService
import
com.topjohnwu.magisk.core.model.module.Module
import
com.topjohnwu.magisk.core.model.module.Module
import
com.topjohnwu.magisk.core.tasks.RepoUpdater
import
com.topjohnwu.magisk.core.tasks.RepoUpdater
import
com.topjohnwu.magisk.data.database.RepoByNameDao
import
com.topjohnwu.magisk.data.database.RepoByNameDao
...
@@ -47,7 +45,7 @@ class ModuleViewModel(
...
@@ -47,7 +45,7 @@ class ModuleViewModel(
private
val
repoName
:
RepoByNameDao
,
private
val
repoName
:
RepoByNameDao
,
private
val
repoUpdated
:
RepoByUpdatedDao
,
private
val
repoUpdated
:
RepoByUpdatedDao
,
private
val
repoUpdater
:
RepoUpdater
private
val
repoUpdater
:
RepoUpdater
)
:
BaseViewModel
(),
Queryable
,
Observer
<
Pair
<
Float
,
DownloadSubject
>>
{
)
:
BaseViewModel
(),
Queryable
{
override
val
queryDelay
=
1000L
override
val
queryDelay
=
1000L
private
var
queryJob
:
Job
?
=
null
private
var
queryJob
:
Job
?
=
null
...
@@ -125,9 +123,6 @@ class ModuleViewModel(
...
@@ -125,9 +123,6 @@ class ModuleViewModel(
// ---
// ---
init
{
init
{
RemoteFileService
.
reset
()
RemoteFileService
.
progressBroadcast
.
observeForever
(
this
)
itemsInstalled
.
addOnListChangedCallback
(
itemsInstalled
.
addOnListChangedCallback
(
onItemRangeInserted
=
{
_
,
_
,
_
->
onItemRangeInserted
=
{
_
,
_
,
_
->
if
(
installSectionList
.
isEmpty
())
if
(
installSectionList
.
isEmpty
())
...
@@ -152,13 +147,7 @@ class ModuleViewModel(
...
@@ -152,13 +147,7 @@ class ModuleViewModel(
// ---
// ---
override
fun
onCleared
()
{
fun
onProgressUpdate
(
progress
:
Float
,
subject
:
DownloadSubject
)
{
super
.
onCleared
()
RemoteFileService
.
progressBroadcast
.
removeObserver
(
this
)
}
override
fun
onChanged
(
it
:
Pair
<
Float
,
DownloadSubject
>?)
{
val
(
progress
,
subject
)
=
it
?:
return
if
(
subject
!
is
DownloadSubject
.
Module
)
if
(
subject
!
is
DownloadSubject
.
Module
)
return
return
...
...
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