I want to make Android App, which will run its local HTTP server. Through this server I want to load index.html to WebView. index.html is not working on its own. It is web app and has to be initialized on localhost. Also it is running other .js files inside assets folder.
To run this before I used python HTTP server and run it in browser from there.
I used Ktor library to create simple HTTP server but it shows just blank page. I don't know if I'm on right path with this solution at all.
My entire App:
import android.annotation.SuppressLint
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.gson.gson
import io.ktor.http.content.default
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.web_view)
initHttpServer()
initWebViewContent()
}
private fun initHttpServer(){
embeddedServer(Netty, 8080) {
install(ContentNegotiation) {
gson {}
}
routing {
default("/app/src/main/assets/index.html")
}
}.start(wait = true)
}
#SuppressLint("SetJavaScriptEnabled")
private fun initWebViewContent(){
webView.apply {
loadUrl("http://127.0.0.1:8080")
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
view.loadUrl(url)
return false
}
}
settings.apply {
setSupportZoom(true)
builtInZoomControls = true
displayZoomControls = false
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = true
}
}
}
}
Ok I dont need HTTP server - index.html can be loaded as:
webView.loadUrl("file:///android_asset/index.html")
index.html is placed inside /app/src/main/assets/ subfolder which is default asset folder for Android project.
class AssertFileContent(
private val context: Context,
private val path: String
) : OutgoingContent.ReadChannelContent() {
override val contentLength: Long?
get() {
val inputStream = context.assets.open(path)
val available = inputStream.available()
inputStream.close()
return available.toLong()
}
override fun readFrom() = context.assets.open(path).toByteReadChannel()
}
private const val pathParameterName = "static-content-path-parameter"
fun Route.assetsFiles(context: Context, path: String = "") {
get("{$pathParameterName...}") {
val relativePath =
call.parameters.getAll(pathParameterName)
?.joinToString(File.separator, prefix = path)
?: return#get
try {
val content =
AssertFileContent(context, relativePath)
call.respond(content)
} catch (e: FileNotFoundException) {
call.respond(HttpStatusCode.NotFound)
}
}
}
// Application.routing
static("/") {
assetsFiles(context, "/webapp")
}
// assets folder structure
webapp
- index.html
You are trying to serve using a file that doesn't exist. assets folder are packaged into APK and you can only access it through AssetManager. Ktor doesn't know about AssetManager and uses standard facilities to access local files.
I suggest copying all the web resources to internal storage and serve from there (filesDir is the path to the root directory):
private fun copyWebResources() {
val files = assets.list("web")
files?.forEach { path ->
val input = assets.open("web/$path")
val outFile = File(filesDir, path)
val outStream = FileOutputStream(outFile)
outStream.write(input.readBytes())
outStream.close()
input.close()
}
}
For the server, you can use the following setup:
embeddedServer(Netty, 3333) {
install(ContentNegotiation) {
gson {}
}
routing {
static("static") {
files(filesDir)
}
}
}
Here is example of assets folder structure:
web
index.html
script.js
Example URL for loadUrl is http://127.0.0.1:3333/static/index.html.
Also, there is a full sample project.
I see that the OP problem is fixed with local access but for anyone who genuinely want to have a web server on an android device to get to from other devices, this is what I ended up doing.
I read the html from asset using AssetManager and then serve that up with ktor:
routing {
get("/") {
val html = application.assets.open("index.html").bufferedReader()
.use {
it.readText()
}
call.respondText(html, ContentType.Text.Html)
}
}
private fun copyFile(context: Context,filePath: String){
val file = context.assets.open(filePath)
val outFile = File(context.filesDir, filePath)
val outStream = FileOutputStream(outFile)
file.copyTo(outStream)
outStream.close()}
private fun copyDir(context: Context,path:String){
val assets = context.assets
val asset = assets.list(path)
asset?.forEach { list ->
val listPath = "$path/$list"
//文件夹
if(!list.toString().contains(".")){
println("Dir::$listPath")
File(context.filesDir.path,listPath).mkdir()
copyDir(context,listPath)
return
}
println("File::$listPath")
copyFile(context,listPath)
}}
fun main(){
File(context.filesDir.path,"www").mkdir()
copyDir(context,path = "www")}
Related
I use implementation 'com.microsoft.signalr:signalr:6.0.8' for android (kotlin) and backend is .Net 6
but the emulator cannot connect to the server (localhost). I try to code a function to check hubConnection.connectionState, it is DISCONNECTED.
no error happened. Can anyone guide me to find the error, here is the code:
import com.microsoft.signalr.Action1
import com.microsoft.signalr.HubConnection
import com.microsoft.signalr.HubConnectionBuilder
import com.microsoft.signalr.HubConnectionState
import io.reactivex.rxjava3.core.Single
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class SignalRListener private constructor(){
private var hubConnection: HubConnection
private var logger: Logger
init {
logger = LoggerFactory.getLogger(HubConnection::class.java)
// define in constructor
hubConnection = HubConnectionBuilder.create("http://10.0.2.2:5291/hubs/presence")
.withAccessTokenProvider(Single.defer { Single.just("${Constanst.TOKEN}") })
.build()
hubConnection.on("UserIsOnline",
Action1 { member: Member -> println(member.DisplayName + "online") },
Member::class.java
)
hubConnection.on("UserIsOffline",
Action1 { username: String -> println(username+" offline") },
String::class.java
)
hubConnection.on(
"GetOnlineUsers",
Action1 { usersOnline : List<Member> ->
for (item in usersOnline) {
println(item.DisplayName)
}
},
List::class.java
)
hubConnection.start().doOnError({ logger.info("Client connected error.") })
}
private object Holder { val INSTANCE = SignalRListener() }
companion object {
#JvmStatic
fun getInstance(): SignalRListener{
return Holder.INSTANCE
}
}
fun stopHubConnection(){
if(hubConnection.connectionState == HubConnectionState.CONNECTED){
hubConnection.stop()
}
}
fun getConnectionState(){
println(hubConnection.connectionState.toString())
}
fun log(){
logger.info("Debug infor siganlR {}", hubConnection.connectionId)
}
}
Web (React) runs well with the backend.
class MainActivity : AppCompatActivity() {
lateinit var signalR: SignalRListener;
var btnCheck: Button? = null
var btnLog: Button? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
signalR = SignalRListener.getInstance()
btnCheck = findViewById(R.id.btnCheck)
btnCheck?.setOnClickListener {
signalR.getConnectionState()
}
btnLog = findViewById(R.id.btnLog)
btnLog?.setOnClickListener {
signalR.log()
}
}
}
As you are in the android emulator, You have to access your localhost so that it reaches your server. If you need internet through proxy you can also set it from the Settings and Proxy and there you can define your proxy settings.
I fixed the problem with the following:
in BE(.Net Core) remove this line:
app.UseHttpsRedirection();
and the client calls http not https:
hubConnection = HubConnectionBuilder.create("http://10.0.2.2:5291/hubs/presence")
hubConnection.start().blockingAwait()
It worked fine
I am testing with MockWebServer.
And I need a lot of json files for request and response data.
Hard coded json values seem messy and I want to create json files instead.
So, I created json files in resources(test). And I tried to read file with these methods.
object TestHelper {
fun read(fileName: String): String {
val resource = javaClass.classLoader?.getResource(fileName)
return resource?.readText() ?: ""
}
fun readJson(fileName: String): String {
val byteArray = readBinaryFileFromResources(fileName)
val sb = StringBuilder("")
byteArray.forEach {
println("byte: $it")
sb.append(it as Char)
}
return sb.toString()
}
#Throws(IOException::class)
fun readBinaryFileFromResources(fileName: String): ByteArray {
var inputStream: InputStream? = null
val byteStream = ByteArrayOutputStream()
try {
inputStream = javaClass.classLoader?.getResourceAsStream(fileName)
var nextValue = inputStream?.read() ?: -1
while (nextValue != -1) {
byteStream.write(nextValue)
nextValue = inputStream?.read() ?: -1
}
return byteStream.toByteArray()
} catch (e: Exception) {
println(e.stackTraceToString())
return byteStream.toByteArray()
} finally {
inputStream?.close()
byteStream.close()
}
}
}
None of them seems work. What's the problem with this code?
I've had trouble with this before, and I believe it has to do with getting the correct classLoader from the call site, as well as having resources in the src/test/resources not being accessible properly. I eventually got it to work by passing in the calling test class as a reified type parameter:
inline fun <reified T> loadFileText(
caller: T,
filePath: String
): String =
T::class.java.getResource(filePath)?.readText() ?: throw IllegalArgumentException(
"Could not find file $filePath. Make sure to put it in the correct resources folder for $caller's runtime."
)
For my setup I have a separate shared module :testtools that I use testImplementation to include in my :app's gradle build so they don't get compiled into the production APK. I have my test resources in:
/testtools/src/main/resources/customfolder
And then calling this from a unit test class in :app like so:
class UnitTestClass {
#Test
fun myTest() {
loadFileText(this, "/customfolder/file_name.txt")
}
}
You might have some luck putting your resources straight into /app/src/test/resources/customfolder, I haven't tried in a while.
I want to save images taken from my app directly to a ssd drive (removable storage) plugged in my device.
The issue I have now, is that with Android 11, I didn't manage to get the path of this storage, and so I can't write the files...
I tried use Storage Access Framework to ask the user to specify the path directly for each images but I can't use this solution as I need to write 30 images per seconds and it kept asking the user select an action on the screen.
This application is only for internal use, so I can grant all the permission without any Google deployment politics issues.
Can anybody help me, i'm so desperate...
So here's my code, I can write on a folder the user choose with SAF. Still have speed issue using DocumentFile.createFile function.
package com.example.ssdwriter
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.*
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
class MainActivity : AppCompatActivity() {
private val TAG = "SSDActivity"
private val CONTENT = ByteArray(2 * 1024 * 1024)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
grantDirectoryAccess()
}
private fun grantDirectoryAccess() {
val treeUri = contentResolver.persistedUriPermissions
if (treeUri.size > 0) {
Log.e(TAG, treeUri.size.toString())
startWriting(treeUri[0].uri)
} else {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
var resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
result.data?.data?.let {
contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
startWriting(result.data?.data!!)
}
}
resultLauncher.launch(intent)
}
}
private fun startWriting(uri: Uri) {
var handlerThread = HandlerThread("writer")
handlerThread.start()
var counter = 0
val handler = Handler(handlerThread.looper)
val runnableCode: Runnable = object : Runnable {
override fun run() {
Log.e(TAG, "Writing File $counter")
createFile(uri, counter++)
Log.e(TAG, "File $counter written ")
if(counter <= 150){
handler.postDelayed(this, 33)
}
}
}
handler.post(runnableCode)
}
private fun createFile(treeUri: Uri, counter: Int) {
val dir = DocumentFile.fromTreeUri(this, treeUri)
val file = dir!!.createFile("*/bmp", "Test$counter.bmp")
if (file != null) {
var outputStream = contentResolver.openOutputStream(file.uri)
if (outputStream != null) {
outputStream.write(CONTENT)
outputStream.flush()
outputStream.close()
}
}
}
}
If anyone got some clues to make this faster, it would be great !
Before the introduction of scoped storage i was using Download Manager to download pdf in my app and get the pdf from getExternalStorageDirectory, but due to scoped storage i can no longer use getExternalStorageDirectory as it is deprecated. I decided to move away from Download Manager as well as it downloads files in public directory and instead use retrofit to download pdf file.
I know i can use the requiredLegacyStorage tag in Android Manifest but it wont be applicable to Android 11 so i am not using that.
Here is my code
fun readAndDownloadFile(context: Context) {
readQuraanInterface?.downloadFile()
Coroutines.io {
file = File(context.filesDir,"$DESTINATION_DIRECTORY/$FILE_NAME$FILE_EXTENSION")
if (file?.exists() == true) {
renderPDF()
showPdf(mPageIndex, Direction.None)
} else {
Log.i("new","new0")
val response = readQuraanRepository.downloadPdf()
if (response.isSuccessful) {
Log.i("new","new00 ${file!!.path} ${response.body()?.byteStream().toString()}")
response.body()?.byteStream()?.let {
file!!.copyInputStreamToFile(
it
)
}
Log.i("new","new1")
// renderPDF()
// showPdf(mPageIndex, Direction.None)
} else {
Log.i("new","new2")
Coroutines.main {
response.errorBody()?.string()
?.let { readQuraanInterface?.downloadFailed(it) }
}
}
}
}
}
private fun File.copyInputStreamToFile(inputStream: InputStream) {
this.outputStream().use { fileOut ->
Log.i("new","new30")
inputStream.copyTo(fileOut)
}
}
Though the pdf id downloaded but the file is never stored using InputStream helper function which i have written. I need to add that pdf to my app's internal storage as well as render it which i am rendering using PDFRenderer.
You can use below code to download and save PDF using scoped storage. Here I am using Downloads directory. Don't forget to give required permissions.
#RequiresApi(Build.VERSION_CODES.Q)
fun downloadPdfWithMediaStore() {
CoroutineScope(Dispatchers.IO).launch {
try {
val url =
URL("https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf")
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.doOutput = true
connection.connect()
val pdfInputStream: InputStream = connection.inputStream
val values = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, "test")
put(MediaStore.Downloads.MIME_TYPE, "application/pdf")
put(MediaStore.Downloads.IS_PENDING, 1)
}
val resolver = context.contentResolver
val collection =
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val itemUri = resolver.insert(collection, values)
if (itemUri != null) {
resolver.openFileDescriptor(itemUri, "w").use { parcelFileDescriptor ->
ParcelFileDescriptor.AutoCloseOutputStream(parcelFileDescriptor)
.write(pdfInputStream.readBytes())
}
values.clear()
values.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(itemUri, values, null, null)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
It is a more clean solution if you save file with Retrofit dynamic Urls.
Create Api
interface DownloadFileApi {
#Streaming
#GET
suspend fun downloadFile(#Url fileUrl: String): Response<ResponseBody>
}
And you can create the instance like
Retrofit.Builder()
.baseUrl("http://localhost/") /* We use dynamic URL (#Url) the base URL will be ignored */
.build()
.create(DownloadFileApi::class.java)
NOTE: You need to set a valid baseUrl even if you don't consume it since it is required by the retrofit builder
Save InputStream result in storage device (you can create a UseCase to do that)
class SaveInputStreamAsPdfFileOnDirectoryUseCase {
/**
* Create and save inputStream as a file in the indicated directory
* the inputStream to save will be a PDF file with random UUID as name
*/
suspend operator fun invoke(inputStream: InputStream, directory: File): File? {
var outputFile: File? = null
withContext(Dispatchers.IO) {
try {
val name = UUID.randomUUID().toString() + ".pdf"
val outputDir = File(directory, "outputPath")
outputFile = File(outputDir, name)
makeDirIfShould(outputDir)
val outputStream = FileOutputStream(outputFile, false)
inputStream.use { fileOut -> fileOut.copyTo(outputStream) }
outputStream.close()
} catch (e: IOException) {
// Something went wrong
}
}
return outputFile
}
private fun makeDirIfShould(outputDir: File) {
if (outputDir.exists().not()) {
outputDir.mkdirs()
}
}
}
Call the api and apply the use case :D
class DownloadFileRepository constructor(
private val service: DownloadFileApi,
private val saveInputStreamAsPdfFileOnDirectory: SaveInputStreamAsPdfFileOnDirectoryUseCase
) {
/**
* Download pdfUrl and save result as pdf file in the indicated directory
*
* #return Downloaded pdf file
*/
suspend fun downloadFileIn(pdfUrl: String, directory: File): File? {
val response = service.downloadFile(pdfUrl)
val responseBody = responseToBody(response)
return responseBody?.let { saveInputStreamAsFileOnDirectory(it.byteStream(), directory) }
}
fun responseToBody(response: Response<ResponseBody>): ResponseBody? {
if (response.isSuccessful.not() || response.code() in 400..599) {
return null
}
return response.body()
}
}
NOTE: You can use ContextCompat.getExternalFilesDirs(applicationContext, "documents").firstOrNull() as save directory
I am using the below code with targeted API 30 and after downloading its saving on the internal Download directory
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));//url=The download url of file
request.setMimeType(mimetype);
//------------------------COOKIE!!------------------------
String cookies = CookieManager.getInstance().getCookie(url);
request.addRequestHeader("cookie", cookies);
//------------------------COOKIE!!------------------------
request.addRequestHeader("User-Agent", userAgent);
request.setDescription("Qawmi Library Downloading");//Description
request.setTitle(pdfFileName);//pdfFileName=String Name of Pdf file
request.allowScanningByMediaScanner();
request.setAllowedOverMetered(true);
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
request.setDestinationInExternalPublicDir("/Qawmi Library"/*Custom directory name below api 29*/, pdfFileName);
} else {
//Higher then or equal api-29
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,"/"+pdfFileName);
}
DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
dm.enqueue(request);
In my phone's Storage, There is a file which is "MyFile.sql". There are 200 records at the file. What should I do is to import those 200 records into the app.
First, I just Initialize
llUpdate.setOnClickListener { UpgradeDB(txtUpdate!!).execute("", "", "") }
After that, I start a method, I don't know why It found the file and read already, But it doesn't import to the app. Is this because I write return = null So it didn't import to the app?
override fun doInBackground(vararg params: String): String? {
val filename = "MyFile.sql"
val sdcard = Environment.getExternalStorageDirectory()
val file = File(sdcard, filename)
if (!file.exists()) isCancelled
var dbHelper: MyDBHelper? = null
dbHelper?.writableDatabase.use { db ->
var intTotalLine = 0
var intLine = 1
BufferedReader(FileReader(file)).useLines { _ -> intTotalLine++ }
BufferedReader(FileReader(file)).use { r ->
r.lineSequence().forEach {
if (it.isNotEmpty()) {
db?.execSQL(it)
publishProgress(String.format("Updating %s/%s records", intLine, intTotalLine))
intLine++
}
}
}
}
return null
}
Can you guys Please Help me to check where are the mistakes? Thanks in advance.