In my Facebook Video Downloader android application i want to show video resolutions like SD, HD with size. Currently i am using InputStreamReader and Pattern.compile method to find SD and HD URL of video.
This method rarely gets me HD link of videos and provides only SD URL which can be downloaded.
Below is my code of link parsing
fun linkParsing(url: String, loaded: (item: DownloadItem) -> Unit) {
val showLogs: Boolean = true
Log.e("post_url", url)
return try {
val getUrl = URL(url)
val urlConnection =
getUrl.openConnection() as HttpURLConnection
var reader: BufferedReader? = null
urlConnection.setRequestProperty("User-Agent", POST_USER_AGENT)
urlConnection.setRequestProperty("Accept", "*/*")
val streamMap = StringBuilder()
try {
reader =
BufferedReader(InputStreamReader(urlConnection.inputStream))
var line: String?
while (reader.readLine().also {
line = it
} != null) {
streamMap.append(line)
}
} catch (E: Exception) {
E.printStackTrace()
reader?.close()
urlConnection.disconnect()
} finally {
reader?.close()
urlConnection.disconnect()
}
if (streamMap.toString().contains("You must log in to continue.")) {
} else {
val metaTAGTitle =
Pattern.compile("<meta property=\"og:title\"(.+?)\" />")
val metaTAGTitleMatcher = metaTAGTitle.matcher(streamMap)
val metaTAGDescription =
Pattern.compile("<meta property=\"og:description\"(.+?)\" />")
val metaTAGDescriptionMatcher =
metaTAGDescription.matcher(streamMap)
var authorName: String? = ""
var fileName: String? = ""
if (metaTAGTitleMatcher.find()) {
var author =
streamMap.substring(metaTAGTitleMatcher.start(), metaTAGTitleMatcher.end())
Log.e("Extractor", "AUTHOR :: $author")
author = author.replace("<meta property=\"og:title\" content=\"", "")
.replace("\" />", "")
authorName = author
} else {
authorName = "N/A"
}
if (metaTAGDescriptionMatcher.find()) {
var name = streamMap.substring(
metaTAGDescriptionMatcher.start(),
metaTAGDescriptionMatcher.end()
)
Log.e("Extractor", "FILENAME :: $name")
name = name.replace("<meta property=\"og:description\" content=\"", "")
.replace("\" />", "")
fileName = name
} else {
fileName = "N/A"
}
val sdVideo =
Pattern.compile("<meta property=\"og:video\"(.+?)\" />")
val sdVideoMatcher = sdVideo.matcher(streamMap)
val imagePattern =
Pattern.compile("<meta property=\"og:image\"(.+?)\" />")
val imageMatcher = imagePattern.matcher(streamMap)
val thumbnailPattern =
Pattern.compile("<img class=\"_3chq\" src=\"(.+?)\" />")
val thumbnailMatcher = thumbnailPattern.matcher(streamMap)
val hdVideo = Pattern.compile("(hd_src):\"(.+?)\"")
val hdVideoMatcher = hdVideo.matcher(streamMap)
val facebookFile = DownloadItem()
facebookFile?.author = authorName
facebookFile?.filename = fileName
facebookFile?.postLink = url
if (sdVideoMatcher.find()) {
var vUrl = sdVideoMatcher.group()
vUrl = vUrl.substring(8, vUrl.length - 1) //sd_scr: 8 char
facebookFile?.sdUrl = vUrl
facebookFile?.ext = "mp4"
var imageUrl = streamMap.substring(sdVideoMatcher.start(), sdVideoMatcher.end())
imageUrl = imageUrl.replace("<meta property=\"og:video\" content=\"", "")
.replace("\" />", "").replace("&", "&")
Log.e("Extractor", "FILENAME :: NULL")
Log.e("Extractor", "FILENAME :: $imageUrl")
facebookFile?.sdUrl = URLDecoder.decode(imageUrl, "UTF-8")
if (showLogs) {
Log.e("Extractor", "SD_URL :: Null")
Log.e("Extractor", "SD_URL :: $imageUrl")
}
if (thumbnailMatcher.find()) {
var thumbNailUrl =
streamMap.substring(thumbnailMatcher.start(), thumbnailMatcher.end())
thumbNailUrl = thumbNailUrl.replace("<img class=\"_3chq\" src=\"", "")
.replace("\" />", "").replace("&", "&")
Log.e("Extractor", "Thumbnail :: NULL")
Log.e("Extractor", "Thumbnail :: $thumbNailUrl")
facebookFile?.thumbNailUrl = URLDecoder.decode(thumbNailUrl, "UTF-8")
}
}
if (hdVideoMatcher.find()) {
var vUrl1 = hdVideoMatcher.group()
vUrl1 = vUrl1.substring(8, vUrl1.length - 1) //hd_scr: 8 char
facebookFile?.hdUrl = vUrl1
if (showLogs) {
Log.e("Extractor", "HD_URL :: Null")
Log.e("Extractor", "HD_URL :: $vUrl1")
}
} else {
facebookFile?.hdUrl = null
}
if (imageMatcher.find()) {
var imageUrl =
streamMap.substring(imageMatcher.start(), imageMatcher.end())
imageUrl = imageUrl.replace("<meta property=\"og:image\" content=\"", "")
.replace("\" />", "").replace("&", "&")
Log.e("Extractor", "FILENAME :: NULL")
Log.e("Extractor", "FILENAME :: $imageUrl")
facebookFile?.imageUrl = URLDecoder.decode(imageUrl, "UTF-8")
}
if (facebookFile?.sdUrl == null && facebookFile?.hdUrl == null) {
}
loaded(facebookFile!!)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
I want to implement a feature where i can show different Resolutions with Sizes as shown in this image.
Please note that i have tested my linkParsing method with videos that has HD URL but it gives only SD URL.
This a sample video link: https://fb.watch/aENyxV7gxs/
How this can be done? I am unable to find any proper method or GitHub library for this.
Found a solution for this so posting as answer.
This can be done by extracting Page Source of a webpage and then parsing that XML and fetching list of BASE URLs.
Steps as follow:
1- Load that specific video URL in Webview and get Page Source inside onPageFinished
private fun webViewSetupNotLoggedIn() {
webView?.settings?.javaScriptEnabled = true
webView?.settings?.userAgentString = AppConstants.USER_AGENT
webView?.settings?.useWideViewPort = true
webView?.settings?.loadWithOverviewMode = true
webView?.addJavascriptInterface(this, "mJava")
webView?.post {
run {
webView?.loadUrl(“url of your video")
}
}
object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (url == "https://m.facebook.com/login.php" || url.contains("https://m.facebook.com/login.php")
) {
webView?.loadUrl("url of your video")
}
return true
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
}
}
webView.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
super.onProgressChanged(view, newProgress)
if (progressBarBottomSheet != null) {
if (newProgress == 100) {
progressBarBottomSheet.visibility = View.GONE
} else {
progressBarBottomSheet.visibility = View.VISIBLE
}
progressBarBottomSheet.progress = newProgress
}
}
}
webView?.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
try {
if (webView?.progress == 100) {
var original = webView?.originalUrl
var post_link = "url of your video"
if (original.equals(post_link)) {
var listOfResolutions = arrayListOf<ResolutionDetail>()
val progressDialog = activity?.getProgressDialog(false)
progressDialog?.show()
//Fetch resoultions
webView.evaluateJavascript(
"(function(){return window.document.body.outerHTML})();"
) { value ->
val reader = JsonReader(StringReader(value))
reader.isLenient = true
try {
if (reader.peek() == JsonToken.STRING) {
val domStr = reader.nextString()
domStr?.let {
val xmlString = it
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).async {
try {
getVideoResolutionsFromPageSource((xmlString)) {
listOfResolutions = it
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
Log.e("Exception", e.message!!)
}
}.await()
progressDialog?.hide()
if (listOfResolutions.size > 0) {
setupResolutionsListDialog(listOfResolutions)
} else {
Toast.makeText(
context,
"No Resolutions Found",
Toast.LENGTH_SHORT
).show()
}
}
}
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
reader.close()
}
}
}
}
} catch (ex: Exception) {
ex.printStackTrace()
}
super.onPageFinished(view, url)
}
#TargetApi(android.os.Build.VERSION_CODES.M)
override fun onReceivedError(
view: WebView?,
request: WebResourceRequest?,
error: WebResourceError
) {
}
#SuppressWarnings("deprecation")
override fun onReceivedError(
view: WebView?,
errorCode: Int,
description: String?,
failingUrl: String?
) {
super.onReceivedError(view, errorCode, description, failingUrl)
}
override fun onLoadResource(view: WebView?, url: String?) {
Log.e("getData", "onLoadResource")
super.onLoadResource(view, url)
}
}
}
2- When Page source is fetched parse to get video Resolution URLs
fun getVideoResolutionsFromPageSource(
pageSourceXmlString: String?,
finished: (listOfRes: ArrayList<ResolutionDetail>) -> Unit
) {
//pageSourceXmlString is the Page Source of WebPage of that specific copied video
//We need to find list of Base URLs from pageSourceXmlString
//Base URLs are inside an attribute named data-store which is inside a div whose class name starts with '_53mw;
//We need to find that div then get data-store which has a JSON as string
//Parse that JSON and we will get list of adaptationset
//Each adaptationset has list of representation tags
// representation is the actual div which contains BASE URLs
//Note that: BASE URLs have a specific attribute called mimeType
//mimeType has audio/mp4 and video/mp4 which helps us to figure out whether the url is of an audio or a video
val listOfResolutions = arrayListOf<ResolutionDetail>()
if (!pageSourceXmlString?.isEmpty()!!) {
val document: org.jsoup.nodes.Document = Jsoup.parse(pageSourceXmlString)
val sampleDiv = document.getElementsByTag("body")
if (!sampleDiv.isEmpty()) {
val bodyDocument: org.jsoup.nodes.Document = Jsoup.parse(sampleDiv.html())
val dataStoreDiv: org.jsoup.nodes.Element? = bodyDocument.select("div._53mw").first()
val dataStoreAttr = dataStoreDiv?.attr("data-store")
val jsonObject = JSONObject(dataStoreAttr)
if (jsonObject.has("dashManifest")) {
val dashManifestString: String = jsonObject.getString("dashManifest")
val dashManifestDoc: org.jsoup.nodes.Document = Jsoup.parse(dashManifestString)
val mdpTagVal = dashManifestDoc.getElementsByTag("MPD")
val mdpDoc: org.jsoup.nodes.Document = Jsoup.parse(mdpTagVal.html())
val periodTagVal = mdpDoc.getElementsByTag("Period")
val periodDocument: org.jsoup.nodes.Document = Jsoup.parse(periodTagVal.html())
val subBodyDiv: org.jsoup.nodes.Element? = periodDocument.select("body").first()
subBodyDiv?.children()?.forEach {
val adaptionSetDiv: org.jsoup.nodes.Element? =
it.select("adaptationset").first()
adaptionSetDiv?.children()?.forEach {
if (it is org.jsoup.nodes.Element) {
val representationDiv: org.jsoup.nodes.Element? =
it.select("representation").first()
val resolutionDetail = ResolutionDetail()
if (representationDiv?.hasAttr("mimetype")!!) {
resolutionDetail.mimetype = representationDiv?.attr("mimetype")
}
if (representationDiv?.hasAttr("width")!!) {
resolutionDetail.width =
representationDiv?.attr("width")?.toLong()!!
}
if (representationDiv?.hasAttr("height")!!) {
resolutionDetail.height =
representationDiv.attr("height").toLong()
}
if (representationDiv?.hasAttr("FBDefaultQuality")!!) {
resolutionDetail.FBDefaultQuality =
representationDiv.attr("FBDefaultQuality")
}
if (representationDiv?.hasAttr("FBQualityClass")!!) {
resolutionDetail.FBQualityClass =
representationDiv.attr("FBQualityClass")
}
if (representationDiv?.hasAttr("FBQualityLabel")!!) {
resolutionDetail.FBQualityLabel =
representationDiv.attr("FBQualityLabel")
}
val representationDoc: org.jsoup.nodes.Document =
Jsoup.parse(representationDiv.html())
val baseUrlTag = representationDoc.getElementsByTag("BaseURL")
if (!baseUrlTag.isEmpty() && !resolutionDetail.FBQualityLabel.equals(
"Source",
ignoreCase = true
)
) {
resolutionDetail.videoQualityURL = baseUrlTag[0].text()
listOfResolutions.add(resolutionDetail)
}
}
}
}
}
}
}
finished(listOfResolutions)
}
class ResolutionDetail {
var width: Long = 0
var height: Long = 0
var FBQualityLabel = ""
var FBDefaultQuality = ""
var FBQualityClass = ""
var videoQualityURL = ""
var mimetype = "" // [audio/mp4 for audios and video/mp4 for videos]
}
3- Pass videoQualityURL to your video download function and video in that selected resolution will be downloaded.
Related
I'm attempting to install an APK programmatically on Android 12 but seem to be running into unknown issues at this point. All advice I've found regarding installing an APK programmatically seem to be deprecated.
Currently, I'm able to save my file but whenever I attempt to install it using PackageManager.PackageInstaller, it fails silently and I'm unable to find anything in the logs suggesting what the failure might've been.
Here's my package installer object.
object PackageInstaller {
#SuppressLint("WrongConstant")
#Throws(IOException::class)
fun installPackage(
context: Context,
installSessionId: String?,
packageName: String?,
apkStream: InputStream?
) {
val packageManger = context.packageManager
val packageInstaller = packageManger.packageInstaller
val params = android.content.pm.PackageInstaller.SessionParams(
android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
)
params.setAppPackageName(packageName)
var session: android.content.pm.PackageInstaller.Session? = null
try {
val sessionId = packageInstaller.createSession(params)
session = packageInstaller.openSession(sessionId)
val out = session.openWrite(installSessionId!!, 0, -1)
val buffer = ByteArray(1024)
var length: Int
var count = 0
if (apkStream != null) {
while (apkStream.read(buffer).also { length = it } != -1) {
out.write(buffer, 0, length)
count += length
}
}
session.fsync(out)
out.close()
val intent = Intent
intent.addFlags(Intent.ACTION_PACKAGE_ADDED)
Log.v("installer", "Installing..?")
session.commit(
PendingIntent.getBroadcast(
context, sessionId,
intent, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
).intentSender
)
}finally {
session?.close()
}
}
}
At this point I'm pretty lost as to where to look next. Does anyone even know if this is still possible? Or a solution to this issue?
You can try with this it is working with android 12
class DownloadApk(private var context: WeakReference<Context>) {
#JvmOverloads
fun startDownloadingApk(url: String, fileName: String = "App Update") {
if (URLUtil.isValidUrl(url)) {
DownloadNewVersion(context, url, fileName).execute()
}
}
#Suppress("DEPRECATION")
private class DownloadNewVersion(
private val context: WeakReference<Context>,
val downloadUrl: String,
val fileName: String
) : AsyncTask<String, Int, Boolean>() {
private lateinit var bar: ProgressDialog
override fun onPreExecute() {
super.onPreExecute()
bar = ProgressDialog(context.get()).apply {
setCancelable(false)
setMessage("Downloading...")
isIndeterminate = true
setCanceledOnTouchOutside(false)
show()
}
}
override fun onProgressUpdate(vararg values: Int?) {
super.onProgressUpdate(*values)
var msg = ""
val progress = values[0]
if (progress != null) {
bar.progress = progress
msg = if (progress > 99) "Finishing... " else "Downloading... $progress%"
}
bar.apply {
isIndeterminate = false
max = 100
setMessage(msg)
}
}
override fun onPostExecute(result: Boolean?) {
super.onPostExecute(result)
bar.dismiss()
if (result != null && result) {
context.get()?.let {
Toast.makeText(it, "Update Done", Toast.LENGTH_SHORT).show()
}
} else {
context.get()?.let {
Toast.makeText(it, "Error: Try Again", Toast.LENGTH_SHORT).show()
}
}
}
override fun doInBackground(vararg p0: String?): Boolean {
var flag = false
try {
val path =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.toString() + "/"
var outputFile = File("$path$fileName.apk")
var repetition = 1
while (outputFile.exists()) {
outputFile = File("$path$fileName ($repetition).apk")
repetition++
}
val directory = File(path)
if (!directory.exists()) {
directory.mkdirs()
}
val url = URL(downloadUrl)
val c = url.openConnection() as HttpURLConnection
c.requestMethod = "GET"
c.connect()
val fos = FileOutputStream(outputFile)
val inputStream = c.inputStream
val totalSize = c.contentLength.toFloat() //size of apk
val buffer = ByteArray(1024)
var len1: Int
var per: Float
var downloaded = 0f
while (inputStream.read(buffer).also { len1 = it } != -1) {
fos.write(buffer, 0, len1)
downloaded += len1
per = (downloaded * 100 / totalSize)
publishProgress(per.toInt())
}
fos.close()
inputStream.close()
openNewVersion(outputFile.path)
flag = true
} catch (e: MalformedURLException) {
Log.e("DownloadApk", "Update Error: " + e.message)
flag = false
} catch (e: IOException) {
e.printStackTrace()
}
return flag
}
private fun openNewVersion(location: String) {
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(
getUriFromFile(location),
"application/vnd.android.package-archive"
)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.get()?.startActivity(intent)
}
private fun getUriFromFile(filePath: String): Uri? {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(File(filePath))
} else {
context.get()?.let {
FileProvider.getUriForFile(
it,
it.packageName + ".provider",
File(filePath)
)
}
}
}
}
}
I need to fetch data inside WhatsApp folders on External Storage.
As i am targeting API Level 30 i am no longer able to access WhatsApp folders on External Storage. I have implemented Storage Access Framework and got Android/media folder Uri and Document File. And using listFiles() i am able to list files but with filter() and sortedByDescending() functions it becomes very slow.
What i have tried?
Used Cursor loader with Projection and Selection Arguments but it only
worked for non hidden folders like WhatsApp Images and WhatsApp Videos
It returns empty cursor for hidden folder .Statuses
Tried replacing MediaStore.Video.Media.EXTERNAL_CONTENT_URI with MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
What is required?
List images and videos from .Statuses folder same as i am listing WhatsApp Images using Media Store in HomeActivity.java
Below is my code
In this activity i get permision to Android/media and set all WhatsApp folders URIs for status fetching and other use, but fetched WhatsApp Images with projection and selection from WhatsApp Images folder
class HomeActivity : AppCompatActivity(), InternetListener, PurchasesUpdatedListener,
CoroutineScope {
private val exceptionHandler = CoroutineExceptionHandler { context, exception ->
Toast.makeText(this, exception.message, Toast.LENGTH_LONG).show()
}
private val dataRepository: DataRepository by inject()
val tinyDB: TinyDB by inject()
val REQUEST_CODE = 12123
init {
newNativeAdSetUp = null
}
val sharedViewModel by viewModel<SharedViewModel>()
val viewModel by viewModel<HomeViewModel>()
val handler = CoroutineExceptionHandler { _, exception ->
Log.d("CoroutineException", "$exception handled !")
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + handler
private lateinit var job: Job
val sdk30PermissionListener = object : PermissionListener {
override fun onPermissionGranted() {
openDocumentTree()
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
handlePermissionsByVersion()
}
private fun handlePermissionsByVersion() {
if (SDK_INT >= Build.VERSION_CODES.R) {
if ((ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
== PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
)
== PackageManager.PERMISSION_GRANTED)
) {
//if granted load whatsapp images and some uris setup to viewmodel
loadWhatsAppImages()
if (arePermissionsGranted()) {
if (dataRepository.mrWhatsAppImages == null || dataRepository.mrWhatsAppBusinessImages == null) {
setUpWAURIs()
}
}
} else {
TedPermission.with(this)
.setPermissionListener(sdk30PermissionListener)
.setDeniedMessage("If you reject permission,you can not use this service\n\nPlease turn on permissions at [Setting] > [Permission]")
.setPermissions(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
.check()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, #Nullable data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE) {
if (data != null) {
//this is the uri user has provided us
val treeUri: Uri? = data.data
if (treeUri != null) {
sharedViewModel.treeUri = treeUri
val decoded = Uri.decode(treeUri.toString())
Log.i(LOGTAG, "got uri: ${treeUri.toString()}")
// here we should do some checks on the uri, we do not want root uri
// because it will not work on Android 11, or perhaps we have some specific
// folder name that we want, etc
if (Uri.decode(treeUri.toString()).endsWith(":")) {
showWrongFolderSelection()
return
}
if (!decoded.equals(Constants.WHATSAPP_MEDIA_URI_DECODED)) {
showWrongFolderSelection()
return
}
// here we ask the content resolver to persist the permission for us
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(
treeUri,
takeFlags
)
val treeUriAsString = treeUri.toString()
tinyDB.putString("FOLDER_URI", treeUriAsString)
if (SDK_INT >= Build.VERSION_CODES.R) {
setupPaths()
}
}
}
}
}
private fun setupPaths() {
setUpOverlay()
fetchWhatsAppRootURIs(
this,
sharedViewModel,
dataRepository,
tinyDB
) {
fetchWhatsAppBusinessRootURIs(
this,
sharedViewModel,
dataRepository,
tinyDB
) {
tinyDB.putBoolean("WARootPathsDone", true)
removeOverlay()
}
}
}
override fun onDestroy() {
dialogHandler.removeCallbacksAndMessages(null)
super.onDestroy()
}
val loadmanagerImages = object : LoaderManager.LoaderCallbacks<Cursor> {
val whatsAppImagesArrayList = arrayListOf<File>()
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
var location: File = File(
Environment.getExternalStorageDirectory()
.toString() + Constants.whatsapp_images_path
)
if (!location.exists()) {
location = File(
Environment.getExternalStorageDirectory()
.toString() + Constants.whatsapp_images_path11
)
}
if (location != null && location.exists()) {
whatsAppImagesArrayList.clear()
Timber.e("checkLoaded-onCreateLoader $id")
if (id == 0) {
var folder = location.absolutePath
val projection = arrayOf(
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DATE_MODIFIED
)
val selection = MediaStore.Images.Media.DATA + " like ? "
val selectionArgs: String = "%$folder%"
return CursorLoader(
this#HomeActivity,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
arrayOf(selectionArgs),
"${MediaStore.Images.Media.DATE_MODIFIED} DESC"
)
}
}
return null!!
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
Timber.e("checkLoaded-onLoadFinished")
var absolutePathOfImage: String
if (loader.id == 0) {
cursor?.let {
val columnIndexData = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
GlobalScope.launch(Dispatchers.Main + exceptionHandler) {
async(Dispatchers.IO + exceptionHandler) {
while (!cursor.isClosed && cursor.moveToNext() == true) {
absolutePathOfImage = cursor.getString(columnIndexData!!)
whatsAppImagesArrayList.add(File(absolutePathOfImage))
}
}.await()
LoaderManager.getInstance(this#HomeActivity).destroyLoader(0)
Timber.e("checkLoaded-Completion")
galleryViewModel.whatsAppImagesList.postValue(whatsAppImagesArrayList)
}
}
}
}
override fun onLoaderReset(loader: Loader<Cursor>) {
}
}
fun loadWhatsAppImages() {
try {
tinyDB.putBoolean("whatsAppMediaLoadCalled", true)
LoaderManager.getInstance(this).initLoader(
0,
null,
loadmanagerImages
)
} catch (e: RuntimeException) {
Log.e("exVideos ", "ex : ${e.localizedMessage}")
}
}
companion object {
const val ANDROID_DOCID = "primary:Android/media/"
const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
private val androidUri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
val androidTreeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
}
private fun openDocumentTree() {
val uriString = tinyDB.getString("FOLDER_URI", "")
when {
uriString == "" -> {
Log.w(LOGTAG, "uri not stored")
askPermission()
}
arePermissionsGranted() -> {
}
else -> {
Log.w(LOGTAG, "uri permission not stored")
askPermission()
}
}
}
// this will present the user with folder browser to select a folder for our data
private fun askPermission() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidUri)
startActivityForResult(intent, REQUEST_CODE)
}
private fun arePermissionsGranted(): Boolean {
var uriString = tinyDB.getString("FOLDER_URI", "")
val list = contentResolver.persistedUriPermissions
for (i in list.indices) {
val persistedUriString = list[i].uri.toString()
if (persistedUriString == uriString && list[i].isWritePermission && list[i].isReadPermission) {
return true
}
}
return false
}
private fun showWrongFolderSelection() {
val layoutInflaterAndroid = LayoutInflater.from(this)
val mView = layoutInflaterAndroid.inflate(R.layout.layout_dialog_wrong_folder, null)
val builder = AlertDialog.Builder(this, R.style.ThemePageSearchDialog)
builder.setView(mView)
val alertDialog = builder.show()
alertDialog.setCancelable(false)
val btnOk = mView.findViewById(R.id.tvExit) as TextView
val tvCancel = mView.findViewById(R.id.tvCancel) as TextView
btnOk.setOnClickListener {
alertDialog.dismiss()
openDocumentTree()
}
tvCancel.setOnClickListener {
alertDialog.dismiss()
}
}
private fun setUpWAURIs() {
dataRepository.mrWhatsAppImages =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppImages")
)
dataRepository.mrWhatsAppVN =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppVN")
)
dataRepository.mrWhatsAppDocs =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppDocs")
)
dataRepository.mrWhatsAppVideo =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppVideo")
)
dataRepository.mrWhatsAppAudio =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppAudio")
)
dataRepository.WhatsAppStatuses =
getDocumentFileFromStringURIStatuses(
this,
tinyDB.getString("WhatsAppStatuses")
)
dataRepository.mrWhatsAppBusinessImages =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessImages")
)
dataRepository.mrWhatsAppBusinessVN =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessVN")
)
dataRepository.mrWhatsAppBusinessDocs =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessDocs")
)
dataRepository.mrWhatsAppBusinessVideo =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessVideo")
)
dataRepository.mrWhatsAppBusinessAudio =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessAudio")
)
dataRepository.WhatsAppBusinessStatuses =
getDocumentFileFromStringURIStatuses(
this,
tinyDB.getString("WhatsAppBusinessStatuses")
)
}
fun setUpOverlay() {
val dialogfragment = FullScreenLoadingDialog()
dialogfragment.isCancelable = false
dialogfragment.setisAdmobAd(true)
val ft: FragmentTransaction =
supportFragmentManager.beginTransaction()
ft.add(dialogfragment, "DialogFragment_FLAG")
ft.commitAllowingStateLoss()
}
fun removeOverlay() {
val fragment: Fragment? = supportFragmentManager.findFragmentByTag("DialogFragment_FLAG")
if (fragment != null && fragment is DialogFragment) {
fragment.dismissAllowingStateLoss()
}
}
fun fetchWhatsAppRootURIs(
context: Context,
sharedViewModel: SharedViewModel,
dataRepository: DataRepository,
tinyDB: TinyDB, completed: () -> Unit
) {
val selectedPackageName = Constants.WHATSAPP_PKG_NAME
val selectedRootName = Constants.WHATSAPP_ROOT_NAME
var waImages: DocumentFile? = null
var waVN: DocumentFile? = null
var waDocs: DocumentFile? = null
var waVideos: DocumentFile? = null
var waAudio: DocumentFile? = null
var waStatus: DocumentFile? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && sharedViewModel.treeUri != null) {
CoroutineScope(Dispatchers.Main).launch {
async(Dispatchers.IO) {
val dir = DocumentFile.fromTreeUri(
context,
sharedViewModel.treeUri!!
)
dir?.listFiles()?.forEach {
if (it.name.equals(selectedPackageName)) {
it.listFiles().forEach {
if (it.name.equals(selectedRootName)) {
it.listFiles().forEach {
if (it.name.equals(Constants.WHATSAPP_MEDIA_FOLDER_NAME)) {
it.listFiles().forEach {
if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_IMAGES)) {
waImages = it
} else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_VN)) {
waVN = it
} else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_DOCUMENTS)) {
waDocs = it
} else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_VIDEO)) {
waVideos = it
} else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_AUDIO)) {
waAudio = it
} else if (it.name.equals(Constants.FOLDER_NAME_STATUSES)) {
waStatus = it
}
}
}
}
}
}
}
}
}.await()
Timber.e("processStatusFetch:Done")
tinyDB.putString("mrWhatsAppImages", waImages?.uri.toString())
tinyDB.putString("mrWhatsAppVN", waImages?.uri.toString())
tinyDB.putString("mrWhatsAppDocs", waImages?.uri.toString())
tinyDB.putString("mrWhatsAppVideo", waImages?.uri.toString())
tinyDB.putString("mrWhatsAppAudio", waImages?.uri.toString())
tinyDB.putString("WhatsAppStatuses", waStatus?.uri.toString())
dataRepository.mrWhatsAppImages = waImages
dataRepository.mrWhatsAppVN = waVN
dataRepository.mrWhatsAppDocs = waDocs
dataRepository.mrWhatsAppVideo = waVideos
dataRepository.mrWhatsAppAudio = waAudio
dataRepository.WhatsAppStatuses = waStatus
completed()
}
}
}
Here i am using .Statuses folder URI to list DocumentFiles and display but this way it is slow
class StatusImageFragment : Fragment(), StatusListener, CoroutineScope {
companion object {
fun newInstance() = StatusImageFragment()
}
val handler = CoroutineExceptionHandler { _, exception ->
Log.d("CoroutineException", "$exception handled !")
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + handler
private lateinit var job: Job
private var adapterSDK30 = StatusImageAdapterSDK30()
private var no_image: ImageView? = null
private var no_image_txt: TextView? = null
val tinyDB: TinyDB by inject()
val sharedViewModel by viewModel<SharedViewModel>()
private val dataRepository: DataRepository by inject()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
job = Job()
return inflater.inflate(R.layout.status_image_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
swipeRefresh(false, false)
}
public fun swipeRefresh(isReloadRequired: Boolean, isFromModeChanged: Boolean) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (isFromModeChanged) {
status_image_recycler.visibility = View.GONE
progressbar.visibility = View.VISIBLE
no_image?.let {
it.visibility = View.GONE
}
no_image_txt?.let {
it.visibility = View.GONE
}
go_to_app?.let {
it.visibility = View.GONE
}
} else {
if (adapterSDK30.listImages == null || adapterSDK30.listImages.size == 0) {
no_image?.let {
it.visibility = View.GONE
}
no_image_txt?.let {
it.visibility = View.GONE
}
go_to_app?.let {
it.visibility = View.GONE
}
progressbar.visibility = View.VISIBLE
}
}
if (isReloadRequired) {
processStatusFetchFromChild({
sharedViewModel.statusImages.observe(viewLifecycleOwner, Observer {
val arrayList = it
adapterSDK30.listImages = arrayList
postFetchingExecutionSDK30()
})
})
} else {
sharedViewModel.statusImages.observe(viewLifecycleOwner, Observer {
val arrayList = it
adapterSDK30.listImages = arrayList
adapterSDK30.listImages = it
postFetchingExecutionSDK30()
})
}
}
} catch (ex: Exception) {
ex.printStackTrace()
}
}
private fun postFetchingExecutionSDK30() {
progressbar.visibility = View.GONE
status_image_recycler.visibility = View.VISIBLE
if (adapterSDK30!!.listImages != null && adapterSDK30!!.listImages.size > 0) {
no_image?.let {
it.visibility = View.GONE
}
no_image_txt?.let {
it.visibility = View.GONE
}
go_to_app?.let {
it.visibility = View.GONE
}
} else {
no_image?.let {
it.visibility = View.VISIBLE
}
no_image_txt?.let {
it.visibility = View.VISIBLE
}
go_to_app?.let {
it.visibility = View.VISIBLE
}
}
adapterSDK30!!.notifyDataSetChanged()
status_img_swipe.isRefreshing = false
}
override fun onDestroyView() {
job.cancel()
super.onDestroyView()
}
fun processStatusFetchFromChild(completed: () -> Unit) {
val statusSelection = tinyDB.getInt(Constants.status_accounts)
if (statusSelection == 0 || statusSelection == 1) {
if (dataRepository.WhatsAppStatuses == null) {
(activity as StatusActivity).setUpWAURIs()
}
var documentFileStatuses: DocumentFile? = dataRepository.WhatsAppStatuses
if (statusSelection == 1) {
documentFileStatuses = dataRepository.WhatsAppBusinessStatuses
}
if (documentFileStatuses != null) {
launch(Dispatchers.Main) {
val statusImages1 = arrayListOf<DocumentFile>()
async(Dispatchers.IO) {
//this takes time ; want to fetch this same as WhatsApp Gallery
statusImages1.addAll(documentFileStatuses!!.listFiles().filter {
it.mimeType.equals(Constants.MIME_TYPE_IMG_PNG) || it.mimeType.equals(
Constants.MIME_TYPE_IMG_JPG
) || it.mimeType.equals(Constants.MIME_TYPE_IMG_JPEG)
}.sortedByDescending { it.lastModified() })
}.await()
Timber.e("processStatusFetch:Done")
sharedViewModel.statusImages.postValue(statusImages1)
completed()
}
} else {
Timber.e("processStatusFetch:Done")
sharedViewModel.statusImages.postValue(arrayListOf<DocumentFile>())
completed()
}
} else {
Timber.e("processStatusFetch:Done")
sharedViewModel.statusImages.postValue(arrayListOf<DocumentFile>())
completed()
}
}
}
Please note WhatsApp folder path which i used is
val whatsapp_images_path11 = "/Android/media/“ +"com.whatsapp" +"/WhatsApp/Media/WhatsAppImages/"
How i can use MediaStore in this case so that i don't need to use sort and filter functions of list? Its not important to get java.io File only i can work with URIs as well.
What I have finally implemented, in android 10+ you need to ask the user for your specific directory access. Then you can use this functions to fetch statuses:
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun readSDKFrom30(): ArrayList<String> {
val treeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
"primary:Android/media/com.whatsapp/WhatsApp/Media/.Statuses"
)
val tree = DocumentFile.fromTreeUri(context, treeUri)!!
val pathList = ArrayList<String>()
listFolderContent(tree).forEach { uri ->
val file = createFileFromContentUri(uri)
pathList.add(file.toString())
}
return pathList
}
private fun listFolderContent(folder: DocumentFile): List<Uri> {
return if (folder.isDirectory) {
val files = folder.listFiles().toMutableList()
files.sortByDescending { it.lastModified() }
files.mapNotNull { file ->
if (file.name != null) file.uri else null
}
} else {
emptyList()
}
}
#RequiresApi(Build.VERSION_CODES.O)
private fun createFileFromContentUri(fileUri: Uri): File {
var fileName = ""
fileUri.let { returnUri ->
context.contentResolver.query(returnUri, null, null, null)
}?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
fileName = cursor.getString(nameIndex)
}
val iStream: InputStream =
context.contentResolver.openInputStream(fileUri)!!
val outputDir: File = context.cacheDir!!
val outputFile = File(outputDir, fileName)
copyStreamToFile(iStream, outputFile)
iStream.close()
return outputFile
}
private fun copyStreamToFile(inputStream: InputStream, outputFile: File) {
inputStream.use { input ->
val outputStream = FileOutputStream(outputFile)
outputStream.use { output ->
val buffer = ByteArray(4 * 1024) // buffer size
while (true) {
val byteCount = input.read(buffer)
if (byteCount < 0) break
output.write(buffer, 0, byteCount)
}
output.flush()
}
}
}
Using DocumentFile to handle SAF uries is slow indeed.
Better use DocumentsContract to do so.
Its about twenty times as fast as DocumentFile and about as fast as classic File class stuff.
Using MediaStore for hidden folders should be possible. You cannot create hidden folders with the mediastore. But if you managed to make them not using mediastore you should be able to list files in them using mediastore. Well if they are scanned. And if they belong to your app.
I have a problem with loading 2gis floor widget webview in Xiaomi mi 9t (android X).
Оn the screenshots you can see that the points are not shown properly.
no places and interface buttons loaded (on xiaomi)
everything is ok (on huawey p30 lite)
This is the code i used for it:
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
class MollMapFragment : BaseFragment(R.layout.fragment_moll_map) {
companion object {
private const val MAP_HTML = "file:///android_asset/2gis/complex_map.html"
private const val ASK_PERMISSIONS_DELAY = 200L
}
//ViewModel
private val mollMapViewModel by viewModels<MollMapViewModel>()
//Bottom sheet dialog popup
private val bottomSheetPopup: BottomSheetDialog by lazy {
BottomSheetDialog(requireContext(), R.style.MollMapBottomSheetDialog).apply {
setContentView(requireActivity().layoutInflater.inflate(R.layout.popup_marker_info, null))
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Handler().postDelayed(
{
if (checkLocationAndForegroundServicePermissions()) {
mollMapViewModel.getLocationAndPlaces(false)
} else {
requestLocationAndForegroundServicePermissions()
}
},
ASK_PERMISSIONS_DELAY
)
initWebView()
initObservers()
}
override fun onResume() {
super.onResume()
bottomSheetPopup.hide()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (checkLocationAndForegroundServicePermissions()) {
mollMapViewModel.askGeoPush()
}
mollMapViewModel.getLocationAndPlaces(true)
}
private fun showGeoPushPermission() = requireContext().showGeoPopup(
{
mollMapViewModel.sendGeoPushRequestAnswer(true)
GeoPushWorker.switchGeoPushWorker(context = requireContext(), isAfterLocationRequest = true)
},
{ mollMapViewModel.sendGeoPushRequestAnswer(false) }
)
#SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
web_view.apply {
with(settings) {
setSupportZoom(true)
javaScriptEnabled = true
domStorageEnabled = true
allowUniversalAccessFromFileURLs = true
allowFileAccessFromFileURLs = true
allowContentAccess = true
setAppCacheEnabled(false)
cacheMode = WebSettings.LOAD_NO_CACHE
loadWithOverviewMode = true
useWideViewPort = true
builtInZoomControls = true
displayZoomControls = false
defaultTextEncodingName = "utf-8"
}
addJavascriptInterface(MapCallback(), "Callback")
}
}
private fun initObservers() {
with(mollMapViewModel) {
getPlacesLiveData().observe(viewLifecycleOwner, Observer { loadable ->
updateUi(loadable,
successAction = { places ->
places?.let {
val complexId = getComplexId()
if (complexId.isNotEmpty()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
showBuilding(complexId, places)
} ?: showPlacesNotLoadedError()
},
errorAction = {
handleCustomException(
exception = it,
networkOfflineAction = {
showPlacesNotLoadedError()
},
serverExceptionAction = { _, _ ->
showToast(R.string.error_server)
}
)
}
)
})
getPlaceRulesLiveData().observe(viewLifecycleOwner, Observer { loadable ->
updateUi(loadable,
successAction = { rules ->
rules?.let {
showPlaceRules(it)
}
},
errorAction = {
handleCustomException(
exception = it,
networkOfflineAction = {
showToast(R.string.error_connection)
},
serverExceptionAction = {_,_ ->
showToast(R.string.error_server)
})
})
})
getSelectedPlaceLiveData().observe(viewLifecycleOwner, Observer { place ->
place?.let {
setPlaceInfo(it)
bottomSheetPopup.show()
}
})
getGeoPushPermissionShowFlag().observe(viewLifecycleOwner, Observer { loadable ->
updateUi(loadable,
successAction = { needToShow ->
needToShow?.let {
if (it) showGeoPushPermission()
} ?: showToast(R.string.error_connection)
},
errorAction = {
handleCustomException(it, networkOfflineAction = {
showToast(R.string.error_connection)
})
})
})
}
}
private fun showBuilding(complexId: String, places: List<Place>) {
web_view.apply {
progress_map.enable()
webViewClient = MollMapWebViewClient(complexId, places)
loadUrl(MAP_HTML)
}
}
private fun showSharing(place: Place) {
val sharingIntent = Intent(Intent.ACTION_SEND).apply {
type = MapFragment.SHARING_INTENT_TYPE
putExtra(Intent.EXTRA_TEXT,
getString(R.string.place_sharing_message, place.title, place.getAddressTextShort())
)
}
startActivity(Intent.createChooser(sharingIntent, requireContext().resources.getString(R.string.share)))
}
private fun focusFirm(firmId: String) {
web_view.post {
web_view.loadUrl("javascript:showFirm('${firmId}')")
}
}
private fun showPlaceRules(rules: String) {
bottomSheetPopup.place_rules.text = rules
}
private fun showContacts(phone: String) {
val intent = Intent(Intent.ACTION_DIAL).apply {
data = Uri.parse("tel:$phone")
}
startActivity(intent)
}
private fun showRouteScreen(place: Place) {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse("google.navigation:q=" + place.getLat() + "," + place.getLng())
).apply {
setPackage(MapFragment.GOOGLE_PACKAGE)
}
if (intent.resolveActivity(requireContext().packageManager) != null) {
startActivity(intent)
} else {
requireContext().showToast(MapFragment.GOOGLE_ERROR_TOAST)
}
}
private fun setPlaceInfo(place: Place) {
bottomSheetPopup.place_name.text = place.title
bottomSheetPopup.place_address.text = place.getAddressTextShortWithoutCity()
mollMapViewModel.getPlaceRules(place.id ?: 0)
setPlaceDistance(place)
setPlaceRating(place)
val phone = place.phone ?: ""
if (phone.isBlank()) {
bottomSheetPopup.call_button.disable()
} else {
bottomSheetPopup.call_button.enable()
bottomSheetPopup.call_button.setOnClickListener { showContacts(phone) }
}
bottomSheetPopup.place_share_button.setOnClickListener { showSharing(place) }
bottomSheetPopup.route_button.setOnClickListener { showRouteScreen(place) }
bottomSheetPopup.marker_info.setOnClickListener {
(activity as MainActivity).openPlaceFullScreenDialog(placeItem = place)
}
}
private fun setPlaceDistance(place: Place) {
var dist = getString(R.string.distance, place.distance.toInt().toString())
place.distance.let {
bottomSheetPopup.place_distance.enable()
if (it > 100000f || it <= 0.0) {
bottomSheetPopup.place_distance.disable()
} else if (it > 1000f) {
dist = if (place.distance.toInt() % 1000 == 0) {
getString(R.string.distance_km, String.format("%.0f", (place.distance / 1000f)))
} else {
getString(R.string.distance_km, String.format("%.2f", (place.distance / 1000f)))
}
}
bottomSheetPopup.place_distance.text = dist
}
}
private fun setPlaceRating(place: Place) {
bottomSheetPopup.place_rating.rating = 0f
place.rating?.let {
bottomSheetPopup.place_rating.rating = if (it.isEmpty()) 5f else it.toFloat()
}
}
private fun showPlacesNotLoadedError() = showToast(R.string.places_load_error)
private inner class MapCallback {
#JavascriptInterface
fun onFirmSelected(firmIds: String) {
Log.d("onFirmSelected", "ids: $firmIds")
focusFirm(firmIds)
mollMapViewModel.setSelectedPlace(firmIds)
}
}
private inner class MollMapWebViewClient(
private val complexId: String,
private val places: List<Place>
) : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
Log.d("wvclient", "onPageStarted: $url")
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
Log.d("wvclient", "onPageFinished: $url")
var styleString = ""
places.forEach {
if (it.gisFirmId.isNotEmpty()){
//Добавление строки стиля для конкретного id
styleString += ",'${it.gisFirmId}':{color:0xbf6d60,highlightColor:0xaf5d50}"
}
}
//функция js с настройкой стиля, которую мы передаем в качестве строки
val style = "( function() { return {backgroundColor:0xd8ccc3,defaultIndoor:{color:0xe6e6e6},defaultOutdoor:{color:0xd8ccc3},0:{color:0xfafafa},110200:{color:0xe6e6e6}${styleString}}; } )()"
view?.loadUrl("javascript:loadMap('${complexId}', ${style})")
}
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
val url = request?.url.toString()
Log.d("wvclient", "shouldInterceptRequest: ${request?.url}, headers: ${request?.requestHeaders}")
if (url.contains("floorGeometries")) {
val connection = URL(url).openConnection()
val `is` = connection.getInputStream()
val res = String(`is`.readBytes())
Log.d("floorGeometries", res)
val correctedMap = GisFloorsHelper.distinguishFirms(res, places)
val correctedResponse = correctedMap.first
mollMapViewModel.onMapRendered(places)
val ret = WebResourceResponse(
"application/json",
"utf-8",
correctedResponse.byteInputStream(Charset.forName("UTF-8"))
).apply {
responseHeaders = connection.headerFields.map { it.key to it.value.first() }.toMap()
}
activity?.runOnUiThread { progress_map.disable() }
return ret
}
return super.shouldInterceptRequest(view, request)
}
override fun onLoadResource(view: WebView?, url: String?) {
Log.d("wvclient.onLoadResource", "url: $url")
super.onLoadResource(view, url)
}
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler, er: SslError?) {
handler.proceed()
}
}
}
viewmodelclass:
class MollMapViewModel : BaseViewModel() {
private val placesUc by inject<PlacesUc>()
private var places: List<Place>? = null
//Job
private var placeRulesJob: Job? = null
private var placesJob: Job? = null
private var showGeoPushPermissionJob: Job? = null
//liveData
private val selectedPlaceLiveData = MutableLiveData<Place?>()
private val placesLiveData = MutableLiveData<Loadable<List<Place>?>>()
private val needToShowGeoPushPermissionLiveData = MutableLiveData<Loadable<Boolean?>>()
private val placeRulesLiveData = MutableLiveData<Loadable<String?>>()
fun getPlacesLiveData(): LiveData<Loadable<List<Place>?>> = placesLiveData
fun getPlaceRulesLiveData(): LiveData<Loadable<String?>> = placeRulesLiveData
fun getSelectedPlaceLiveData(): LiveData<Place?> = selectedPlaceLiveData
fun getGeoPushPermissionShowFlag(): LiveData<Loadable<Boolean?>> = needToShowGeoPushPermissionLiveData
fun onMapRendered(places: List<Place>) {
if (places.size < this.places?.size ?: 0) return
this.places = places
}
fun setSelectedPlace(firmId: String) {
selectedPlaceLiveData.postValue(places?.findLast { it.gisFirmId == firmId })
}
fun getLocationAndPlaces(forceUpdate: Boolean) {
val locationManager = App.context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val userLocation: UserLocation? = placesUc.getCurrentUserLocation(lm = locationManager)
if (userLocation != null) {
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
placesUc.saveLocation(userLocation)
getPlaces(userLocation, forceUpdate)
} else {
placesUc.removeLocation()
getPlaces(null, forceUpdate)
}
} else {
if (App.context.checkLocationPermission()) {
LocationServices
.getFusedLocationProviderClient(App.context)
.lastLocation
.addOnCompleteListener { task ->
val lastLocation = task.result
if (lastLocation != null) {
getPlaces(UserLocation(lastLocation.latitude, lastLocation.longitude), forceUpdate)
} else {
getPlaces(null, forceUpdate)
}
}
} else {
getPlaces(null, forceUpdate)
}
}
}
fun getComplexId() = CloneUtils.getComplexId()
fun getPlaces(userLocation: UserLocation?, forceUpdate: Boolean) {
if (checkJob(placesJob)) {
placesJob = launchLoadingErrorJob(context = Dispatchers.IO, liveData = placesLiveData) {
placesUc.getPlaces(userLocation, forceUpdate)
}
}
}
fun askGeoPush() {
if (checkJob(showGeoPushPermissionJob)) {
showGeoPushPermissionJob = launchLoadingErrorJob(
context = Dispatchers.IO,
liveData = needToShowGeoPushPermissionLiveData
) {
placesUc.doWeNeedToAskForGeoPush()
}
}
}
fun sendGeoPushRequestAnswer(answer: Boolean) {
placesUc.geoPushRequestGotAnswer(answer)
}
fun getPlaceRules(id: Int) {
if (checkJob(placeRulesJob)) {
placeRulesJob = launchLoadingErrorJob(context = Dispatchers.IO, liveData = placeRulesLiveData) {
val placeUnitList = placesUc.getPlaceUnitList(id)
val rules = StringBuilder()
if (placeUnitList.isNotEmpty()) {
placeUnitList.forEach { unit ->
if (unit == placeUnitList.last() ||
unit.title?.last() == '.' ||
unit.title?.last() == '!') {
rules.append(unit.title)
} else {
rules.append("${unit.title}. ")
}
}
}
rules.toString()
}
}
}
}
And some additional code
layout:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="#+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="#+id/progress_map"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:indeterminateDrawable="#drawable/progress_moll" />
</FrameLayout>
my html file for access 2gis api:
<!DOCTYPE html>
<html>
<head lang="en">
<meta name="viewport"
content="width=device-width, height=device-height, initial-scale=1, user-scalable=no, viewport-fit=cover">
<style>
html, body {
margin: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<script charset="utf-8" src="https://floors-widget.api.2gis.ru/loader.js"
id="dg-floors-widget-loader">
</script>
<script charset="utf-8">
var widget;
function loadMap(complexId, style) {
console.log(style)
widget = DG.FloorsWidget.init({
width: '100%',
height: '100%',
initData: {
complexId: complexId,
options: {
hideSearchPanel: true,
locale: 'ru_RU',
initialZoom: 17,
minZoom: 17,
maxZoom: 19.5,
//rotatable: false,
initialRotation: 0.45,
resetStyle: true,
initialFloor: 1,
style: style
}
}
});
document.body.style.margin = "0px";
//Событие по клику на точку
//Отдает массив из id точек
widget.on('click', (event) => {
let message = {"firmIds": event.firmIds };
let str = (event.firmIds).toString();
Callback.onFirmSelected(str);
webkit.messageHandlers.handler.postMessage(message);
});
}
//Функция открывающая конкретную точку, если необходимо так же переключает этаж
function showFirm(firmId) {
widget.focusOnFirm(firmId);
}
//Функция поиска, с помощью нее можно фильтровать точки по категориям
function searchQuery(string) {
widget.search(string);
}
</script>
</body>
</html>
I want to load rtf files with webview using raw folder. Below is my code;
val urlWebView = findViewById<WebView>(R.id.webview)
readTextFromResource(R.raw.lettemp_369)?.let { urlWebView.loadData(it, "text/rtf", "utf-8") };
Below is function;
private fun readTextFromResource(resourceID: Int): String? {
val raw: InputStream = resources.openRawResource(resourceID)
val stream = ByteArrayOutputStream()
var i: Int
try {
i = raw.read()
while (i != -1) {
stream.write(i)
i = raw.read()
}
raw.close()
} catch (e: IOException) {
e.printStackTrace()
}
return stream.toString()
}
I want to load rtf files with webview using raw folder. Below is my code;
val urlWebView = findViewById<WebView>(R.id.webview)
readTextFromResource(R.raw.lettemp_369)?.let { urlWebView.loadData(it, "text/rtf", "utf-8") };
Below is function;
private fun readTextFromResource(resourceID: Int): String? {
val raw: InputStream = resources.openRawResource(resourceID)
val stream = ByteArrayOutputStream()
var i: Int
try {
i = raw.read()
while (i != -1) {
stream.write(i)
i = raw.read()
}
raw.close()
} catch (e: IOException) {
e.printStackTrace()
}
return stream.toString()
}