Here is some kotlin code I have that is used to implement a download feature in an Android app (using DownloadManager):
(It is not the whole code, but the relevant part for my question)
var brdCstRcvr = object: BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
val id = p1?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id == downloadID) {
val mgr = applicationContext.getSystemService(DOWNLOAD_SERVICE) as DownloadManager
val uri:Uri? = mgr.getUriForDownloadedFile(downloadID)
if (uri == null) {
println("We have a problem with this URL: ${urlStr.text.toString()}")
return
}
val inputStream: InputStream = getContentResolver().openInputStream(uri!!)!!
.... more useful code not relevant to the question .....
}
}
.........
}
Though it is mostly working I have a problem with one case. This is the shape of the URL causing trouble:
http://192.168.77.123/FOLDER/Sample.mp3
When running the app I get this message:
We have a problem with this URL: http://192.168.77.123/FOLDER/Sample.mp3
(showing that uri is null)
The other URLs I have used with no issue are all of type https://example.site.net/Sample.mp3
The URL with problems is http (instead of https) and it is on my local network.
But I am not sure this is the cause of the issue.
Any idea on how to catch the cause of this would be helpful.
Related
I have an external camera that's connected via a USB C dongle to my Android tablet. My goal is to have a constant stream of data from the camera into my phone, showing it to the user and allowing him to record it and save it to the local storage.
I am following the official docs from the following link -
https://developer.android.com/guide/topics/connectivity/usb/host#working-d
And I have spent the last couple of hours trying to figure out how things work, mapping the interfaces and endpoints, eventually finding an interface that has an endpoint that when I call bulkTransfer() on, does not return a failed value (-1).
I currently am facing 2 issues:
I have indeed got a valid response from the bulkTransfer() function, but my ByteArray does not fill with relevant information - when trying to print out the values they are all 0's. I though it may be a wrong endpoint as suggested in the official docs, but I have tried all combinations of interfaces and endpoints until I get an indexOutOfBoundException. That combination of interface + endpoint that I used is the only one that produced a valid bulk response. What am I missing?
I am looking for a stream of data that doesn't stop, but it seems like when calling bulkTransfer() it's one a one time oppression, unlike CameraX library for example that I get a constant callback each time a new chunck of data is available.
Here is the code on my main screen -
LaunchedEffect(key1 = true) {
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(context, UsbBroadcastReceiver(), filter, RECEIVER_NOT_EXPORTED)
val hdCamera = usbManager.deviceList.values.find { device ->
val name = device.productName ?: return#LaunchedEffect
name.contains("HD camera")
} ?: return#LaunchedEffect
val permissionIntent = PendingIntent.getBroadcast(
context,
0, Intent(ACTION_USB_PERMISSION),
0
)
usbManager.requestPermission(hdCamera, permissionIntent)
}
And here is my BroadcastReceiver -
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != ACTION_USB_PERMISSION) return
synchronized(this) {
val usbManager = context?.getSystemService(Context.USB_SERVICE) as UsbManager
val device: UsbDevice? = intent.getParcelable(UsbManager.EXTRA_DEVICE)
val usbInterface = device?.getInterface(0)
val endpoint = usbInterface?.getEndpoint(1) ?: return#synchronized
usbManager.openDevice(device)?.apply {
val array = ByteArray(endpoint.maxPacketSize)
claimInterface(usbInterface, true)
val bulkTransfer = bulkTransfer(endpoint, array, array.size, 0)
Log.d("defaultAppDebuger", "bulk array: $bulkTransfer") //prints a valid number - 512
array.forEach {
Log.d("defaultAppDebuger", "bulk array: $it") //the array values are empty
}
}
}
}
edit:
I have tried to move the BroadcastReceiver code to an async coroutine thinking that the loading of the information is related to the fact that I am in the wrong thread. Still didn't work, I get a valid result from the bulkTransfer and the byteArray is not filled -
fun BroadcastReceiver.goAsync(
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> Unit
) {
val pendingResult = goAsync()
CoroutineScope(SupervisorJob()).launch(context) {
try {
block()
} finally {
pendingResult.finish()
}
}
}
override fun onReceive(context: Context?, intent: Intent?) = goAsync { .... }
Thanks!
After carefully researching I was not able to get an answer and ditched that mini project that I worked on. I followed this comment on the following thread -
https://stackoverflow.com/a/68120774/8943516
That, combined with a 2.5 days of deep researched of both USB Host protocol which was not able to connect to my camera and Camera2API which couldn't recognize my external camera brought me to a dead end.
Here is my Retrofit API:
#GET
suspend fun downloadMedia(#Url url: String): Response<ResponseBody>
Here is the code that actually downloads the image from the URL and saves it to the device storage:
override fun downloadMedia(url: String): Flow<RedditResult<DownloadState>> = flow {
preferences.downloadDirFlow.collect {
if (it.isEmpty()) {
emit(RedditResult.Success(DownloadState.NoDefinedLocation))
} else {
// Actually download
val response = authRedditApi.downloadMedia(url)
if (response.isSuccessful) {
val treeUri = context.contentResolver.persistedUriPermissions.firstOrNull()?.uri
treeUri?.let { uri ->
val directory = DocumentFile.fromTreeUri(context, uri)
val file = directory?.createFile(
response.headers()["Content-Type"] ?: "image/jpeg",
UUID.randomUUID().toString().replace("-", ""))
file?.let {
context.contentResolver.openOutputStream(file.uri)?.use { output ->
response.body()?.byteStream()?.copyTo(output)
output.close()
emit(RedditResult.Success(DownloadState.Success))
}
} ?: run {
emit(RedditResult.Error(Exception("Unknown!")))
}
}
} else {
emit(RedditResult.Error(IOException(response.message())))
}
}
}
}
The file downloads and is the correct size in MB, but it somehow becomes corrupted with dimensions of 0x0 and just a blank image (when on my PC it can't even be opened).
I don't really know what I'm doing wrong as the file is being created and written to fine (which was difficult with SAF in and of itself).
Edit: I've also tried with and without #Streaming on the API function with the same results.
Turns out I was being an idiot. I was using a Retrofit instance which was using a MoshiConverter, this caused the content length to be changed and therefore the file was corrupted. Solved by using a Retrofit instance without a MoshiConverter.
Background
In the past, I've found a special app called "Purchased apps" that somehow gets a list of the apps you've purchased. Not seeing any API for this, I asked how does it do it (and sadly still couldn't find a clear answer and a POC to demonstrate it).
The problem
Time passed, and I've noticed there is actually an open sourced app called "Aurora Store" (repository here) that can get about as much information as the Play Store. Screenshot from it:
Thing is, I got issues trying to figure out how to use its code properly, and the weird thing is that those apps get the information from different sources.
What I've tried
So, seeing it allows you to login to Google, and then get the "library" information (history of installed apps), I decided to give it a go (full sample on Github, here) :
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private val cookieManager = CookieManager.getInstance()
#SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val cachedEmail = defaultSharedPreferences.getString("email", null)
val cachedAasToken = defaultSharedPreferences.getString("aasToken", null)
if (cachedEmail != null && cachedAasToken != null) {
onGotAasToken(applicationContext, cachedEmail, cachedAasToken)
} else {
webView = findViewById(R.id.webView)
cookieManager.removeAllCookies(null)
cookieManager.acceptThirdPartyCookies(webView)
cookieManager.setAcceptThirdPartyCookies(webView, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
webView.settings.safeBrowsingEnabled = false
}
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
val cookies = CookieManager.getInstance().getCookie(url)
val cookieMap: MutableMap<String, String> = AC2DMUtil.parseCookieString(cookies)
val oauthToken: String? = cookieMap[AUTH_TOKEN]
oauthToken?.let {
webView.evaluateJavascript("(function() { return document.getElementById('profileIdentifier').innerHTML; })();") {
val email = it.replace("\"".toRegex(), "")
Log.d("AppLog", "got email?${email.isNotBlank()} got oauthToken?${oauthToken.isNotBlank()}")
buildAuthData(applicationContext, email, oauthToken)
}
} ?: Log.d("AppLog", "could not get oauthToken")
}
}
webView.settings.apply {
allowContentAccess = true
databaseEnabled = true
domStorageEnabled = true
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
}
webView.loadUrl(EMBEDDED_SETUP_URL)
}
}
companion object {
const val EMBEDDED_SETUP_URL =
"https://accounts.google.com/EmbeddedSetup/identifier?flowName=EmbeddedSetupAndroid"
const val AUTH_TOKEN = "oauth_token"
private fun buildAuthData(context: Context, email: String, oauthToken: String?) {
thread {
try {
val aC2DMResponse: Map<String, String> =
AC2DMTask().getAC2DMResponse(email, oauthToken)
val aasToken = aC2DMResponse["Token"]!!
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putString("email", email).putString("aasToken", aasToken).apply()
onGotAasToken(context, email, aasToken)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun onGotAasToken(context: Context, email: String, aasToken: String) {
thread {
val properties = NativeDeviceInfoProvider(context).getNativeDeviceProperties()
val authData = AuthHelper.build(email, aasToken, properties)
val purchaseHelper = PurchaseHelper(authData).using(HttpClient.getPreferredClient())
var offset = 0
Log.d("AppLog", "list of purchase history:")
while (true) {
val purchaseHistory = purchaseHelper.getPurchaseHistory(offset)
if (purchaseHistory.isNullOrEmpty())
break
val size = purchaseHistory.size
offset += size
purchaseHistory.forEach {
Log.d("AppLog", "${it.packageName} ${it.displayName}")
}
}
Log.d("AppLog", "done")
}
}
}
}
It seems it got the token it needs (and the email), but sadly it seems to get 2 apps and that's it, and then when I try to get the next ones, I get the same 2 apps, twice more, meaning as such:
list of purchase history:
dev.southpaw.dungeon Dungeon Live Wallpaper
com.crydata.mylivewallpaper Hex AMOLED Neon Live Wallpaper 2021
dev.southpaw.dungeon Dungeon Live Wallpaper
com.crydata.mylivewallpaper Hex AMOLED Neon Live Wallpaper 2021
dev.southpaw.dungeon Dungeon Live Wallpaper
com.crydata.mylivewallpaper Hex AMOLED Neon Live Wallpaper 2021
and on the last time it tries to get the next chunk of apps, it crashes with this exception:
FATAL EXCEPTION: Thread-4
Process: com.lb.getplaystoreinstalledappshistory, PID: 6149
Server(code=400, reason=Bad Request)
at com.aurora.gplayapi.helpers.AppDetailsHelper.getAppByPackageName(AppDetailsHelper.kt:115)
at com.aurora.gplayapi.helpers.PurchaseHelper.getPurchaseHistory(PurchaseHelper.kt:63)
at com.lb.getplaystoreinstalledappshistory.MainActivity$Companion$onGotAasToken$1.invoke(MainActivity.kt:96)
at com.lb.getplaystoreinstalledappshistory.MainActivity$Companion$onGotAasToken$1.invoke(MainActivity.kt:68)
at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)
The questions
What's wrong with how I tried to get the list of apps? How can I get it right, ordered by time installed?
Is there any way to get the time they were installed (or any clue about it) ? Somehow the "Purchased apps" app got the time. Granted it was only for purchased apps, but still...
The "Purchased apps" app even got login better, as it doesn't require user-name and password. Instead it offers a dialog to choose the account. Assuming I get it right, is it possible to get the same information using the same login dialog ?
Not sure if this information is remotely useful but might as well mention it...
I accidentally decompiled their archived APK from 2015 when they didn't minimize their code and at least back then, they were using a JSoup HTML parser spider. Possibly they still are, which is probably not allowed by Google and incredibly prone to maintenance.
I'm kind of new developing with Android, and have problems understanding how things works. I made an app to scrape content from a page, and download elements, first with AsyncTask and worked, but since AsyncTask doesn't let me communicate with the UI Activity on progress, i decided to change to coroutines, checked an example, and the same code i used doesn't seems to work.
I used a few logs to try to determine the problem, and seems like it doesn't wait for the Jsoup request. The coroutine first calls a method scrapePage() to download the HTML and scrape the links, and then calls downloadImages() to add the links to Android's DownloadManager. In the logs Log.d("action", "Start Scraping") is printed, but Log.d("action", "Page downloaded") doesn't, still we get Log.d("action", "End") from the coroutine, which makes me think that instead of waiting for the Jsoup request to answer, it goes with an empty response, causing the rest of the code to not work correctly.
DownloadService.kt
object DownloadService {
private val parentJob = Job()
...
private val coroutineScope = CoroutineScope(Dispatchers.Main + parentJob +
coroutineExceptionHandler)
fun StartService(URL: String, location:String, contx:Context) {
coroutineScope.launch(Dispatchers.Main) {
Log.d("action", "Start")
val links = scrapePage(URL)
val download = downloadImages(links, location, contx)
Log.d("action", "End")
}
}
private suspend fun scrapePage(url: String): MainActivity.Scraped =
withContext(Dispatchers.IO) {
var URL = url
var scrape = MainActivity.Scraped()
try {
Log.d("action", "Start Scraping")
var response = Jsoup.connect(URL).get()
Log.d("action", "Page downloaded")
response.getElementsByClass("link").forEach {
/*Scrape URLs*/
Log.d("action", "Add "+link)
}
} catch (e: Exception) {
when(e) {
is HttpStatusException -> {
System.out.println(e.getStatusCode())
scrape.error = true
error = true
}
}
}
return#withContext scrape
}
...
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
...
fun makeRequest(URL : String) {
WorkingURL = URL
var uri = ""
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
uri = MediaStore.Downloads.EXTERNAL_CONTENT_URI.toString()
log.text = uri
} else {
uri = getStoragePath()
log.text = uri
}
DownloadService.StartService(URL, uri, this)
Log.d("links", DownloadService.getError().toString())
}
}
I am not sure where the problem is, nor where to start searching. I know the code for the Scraping works, because i used it before with AsyncTask, so the problem seems to be passing it to coroutines.
Here Jsoup.connect(URL).get() is throwing an error. so, Log.d("action", "Page downloaded") is not called.
But since you are handling the exception, the code runs the catch part and completes the suspend function and moves on to downloadImages().
Solution
First, add a log in the catch part of scrapePage() function and find out what is causing the exception. Everything else in your code is good.
So I'm making my first Android app and I'm trying to get it to allow the user to pick a video from their gallery before seeing the video and the video's current details in the next activity.
My problem is that when I use FFmpegMediaMetadataRetriever and pass it the video's filepath, I receive the error java.lang.IllegalArgumentException: setDataSource failed: status = 0xFFFFFFFF.
I've heard through the grapevine that this means my filepath is invalid. When I Log.d the filepath, I get content://media/external/file/3565, which to me looks like a proper filepath!
I hope somebody can help me figure this out.
Here is my activity class for context:
class NewProject : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_project)
val videoPath = intent.getStringExtra("video")
initVideo(videoPath)
backButtonText.setOnClickListener{ goBack() }
}
private fun goBack() {
val intent = Intent(this,MainActivity::class.java)
startActivity(intent)
}
private fun initVideo(videoPath:String) {
newProjVideoView.setVideoPath(videoPath)
newProjVideoView.start()
newProjVideoView.setOnCompletionListener {
newProjVideoView.pause()
}
getVideoMetadata(videoPath)
}
private fun getVideoMetadata(videoPath: String) {
try {
e("videoPath", videoPath)
val receiver = FFmpegMediaMetadataRetriever()
receiver.setDataSource(videoPath)
} catch (e:IOException) {
e("retrieve1","There was an issue", e)
}
}
}
I'm also happy to hear any constructive feedback on my code!
Please, thank you and have a nice day!
So, I think my issue stemmed from trying to pass the video through an intent and then running the MetadataRetriever. I solved it by getting all the info in the previous activity before passing each value as an extra to be used on the next screen.