Android DownloadManager not working when redirected - android

I'm working on an app that will download a zip file stored on Amazon S3 via a Rails Heroku server after authenticating via oAuth 2. Here's the flow:
Request to authenticate with the server running on Heroku via
oAuth2.
Receive oAuth2 access token.
Request to download the zip file from the server (passing the
oAuth token as bearer).
The server authorizes the request and redirects to an Amazon S3
URL containing a expiring signature (to stop anyone downloading the
content without being authenticated).
At this point, I want the DownloadManager to just follow the redirect and get the zip file from S3, however it's failing. Is there some way I can work around this? Or is it just a limitation of DownloadManager?
I'm new to Android and still not totally up on the best debugging methods, so I don't have a lot of output to show you. However, it seems that DownloadManager.COLUMN_STATUS == DownloadManager.STATUS_FAILED and DownloadManager.COLUMN_REASON is returning "placeholder"!
EDIT - Here is the code I'm using. Edited to hide the client etc...
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
Log.i("ChapterListActivity", "Item clicked: " + id);
final DownloadManager downloadManager = (DownloadManager)getSystemService(Context.DOWNLOAD_SERVICE);
Uri uri = Uri.parse("http://myapphere.herokuapp.com/api/v1/volumes/2.zip");
DownloadManager.Request request = new Request(uri);
String accessToken = getSharedPreferences("keyhere", MODE_PRIVATE).getString("access_token", null);
Log.i("SLEChapterListActivity", "Getting file with access token... " + accessToken);
request.addRequestHeader("Authorization", "Bearer " + accessToken);
long reference = downloadManager.enqueue(request);
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
BroadcastReceiver receiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
long downloadReference = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
Log.i("ChapterListActivity", "Download completed");
Query query = new Query();
query.setFilterById(downloadReference);
Cursor cur = downloadManager.query(query);
if (cur.moveToFirst()) {
int columnIndex = cur.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == cur.getInt(columnIndex)) {
String uriString = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
File mFile = new File(Uri.parse(uriString).getPath());
} else if (DownloadManager.STATUS_FAILED == cur.getInt(columnIndex)){
String statusResult = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_REASON));
Toast.makeText(context, "FAILED " + statusResult, Toast.LENGTH_SHORT).show();
} else if (DownloadManager.ERROR_TOO_MANY_REDIRECTS == cur.getInt(columnIndex)){
String statusResult = cur.getString(cur.getColumnIndex(DownloadManager.COLUMN_REASON));
Toast.makeText(context, "TOO MANY REDIRS " + statusResult, Toast.LENGTH_SHORT).show();
}
}
}
};
registerReceiver(receiver, filter);
}

I've found in Download Manager sources (line 500):
3xx: redirects (not used by the download manager)
It's not supported, yet.
In my current project, downloads are made in two steps:
Get Amazon url from our own server via oAuth2
Enqueue DownloadManager with the Amazon url.
If you don't like the two step process, I don't, then take a look at RoboSpice project, it has similar philosophy as DownloadManager.

Just answering a sub-part of this question. The reason why you get the reason as a "placeholder" String is because the reason column is an integer, not a String. See Android DownloadManager: Download fails, but COLUMN_REASON only returns “placeholder”.

Related

Download Manager with Google Drive URL

I'm trying to download a file stored in Google Drive using android DownloadManager.
I get the sign in token from Google like following:
GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(AppConfig.getInstance().get("google_client_id"))
.requestProfile()
.requestEmail()
.build();
I receive a notification with google drive file url and i pass it to the DownloadManager, passing to it the token:
String cookie = CookieManager.getInstance().getCookie(d.getURL());
request.addRequestHeader("Cookie",cookie);
request.addRequestHeader("Authorization", "OAuth " + profile.getToken());
//d is the document object, that contains url, file name, etcc
//Profile is a simple object class that hold the user data taken from sign Google, like token, name, email, etcc
Using a simple Broadcast Receiver to manage the download result (ACTION_DOWNLOAD_COMPLETE).
The download is done successfully, but the file is corrupted.
If i try to open it, the device (and pc) gives me a format error.
The file contains a HTML code of a Google page that says that there war an error, no more.
(The account that i'm using is enabled to read and dwonload the document form this specific drive storage)
Is this the correct way to download a Google Drive file using DownloadManager? Is it possible to do that?
Try whether this helps...
As in #saddamkamal 's answer, use the Google Drive download URL.
AsyncTask.execute(new Runnable() {
#Override
public void run() {
DownloadManager downloadmanager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
Uri uri = Uri.parse("https://drive.google.com/uc?id=<FILE_ID>&export=download");
DownloadManager.Request request = new DownloadManager.Request(uri);
request.setTitle("My File");
request.setDescription("Downloading");
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "file.extension");
downloadmanager.enqueue(request);
}
});
You can also downlaod file form google via this method
Refer to this Url
https://drive.google.com/uc?id=<FILE_ID>&export=download
Replace <FILE_ID> with your shareable file ID.
Further you can take help from this solution Download a file with Android, and showing the progress in a ProgressDialog
You can use the doInBackground function in it to solve your query.
Since file is downloaded.
Check the size of file in Google drive and your android.
Then make sure your file extension is correct.
Because file extension may not be present and android will treat it as binary file.
Now you have file extension in android. Install proper application to open it.
This is updated code
public class MainActivity extends AppCompatActivity {
private Button btn_download;
private long downloadID;
// using broadcast method
private BroadcastReceiver onDownloadComplete = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
//Fetching the download id received with the broadcast
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
//Checking if the received broadcast is for our enqueued download by matching download id
if (downloadID == id) {
Toast.makeText(MainActivity.this, "Download Completed", Toast.LENGTH_SHORT).show();
}
}
};
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_download = findViewById(R.id.download_btn);
// using broadcast method
registerReceiver(onDownloadComplete,new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
button.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
beginDownload();
}
});
}
#Override
public void onDestroy() {
super.onDestroy();
// using broadcast method
unregisterReceiver(onDownloadComplete);
}
private void beginDownload(){
String url = "http://speedtest.ftp.otenet.gr/files/test10Mb.db";
String fileName = url.substring(url.lastIndexOf('/') + 1);
fileName = fileName.substring(0,1).toUpperCase() + fileName.substring(1);
File file = Util.createDocumentFile(fileName, context);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url))
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)// Visibility of the download Notification
.setDestinationUri(Uri.fromFile(file))// Uri of the destination file
.setTitle(fileName)// Title of the Download Notification
.setDescription("Downloading")// Description of the Download Notification
.setRequiresCharging(false)// Set if charging is required to begin the download
.setAllowedOverMetered(true)// Set if download is allowed on Mobile network
.setAllowedOverRoaming(true);// Set if download is allowed on roaming network
DownloadManager downloadManager= (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
downloadID = downloadManager.enqueue(request);// enqueue puts the download request in the queue.
// using query method
boolean finishDownload = false;
int progress;
while (!finishDownload) {
Cursor cursor = downloadManager.query(new DownloadManager.Query().setFilterById(downloadID));
if (cursor.moveToFirst()) {
int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch (status) {
case DownloadManager.STATUS_FAILED: {
finishDownload = true;
break;
}
case DownloadManager.STATUS_PAUSED:
break;
case DownloadManager.STATUS_PENDING:
break;
case DownloadManager.STATUS_RUNNING: {
final long total = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
if (total >= 0) {
final long downloaded = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
progress = (int) ((downloaded * 100L) / total);
// if you use downloadmanger in async task, here you can use like this to display progress.
// Don't forget to do the division in long to get more digits rather than double.
// publishProgress((int) ((downloaded * 100L) / total));
}
break;
}
case DownloadManager.STATUS_SUCCESSFUL: {
progress = 100;
// if you use aysnc task
// publishProgress(100);
finishDownload = true;
Toast.makeText(MainActivity.this, "Download Completed", Toast.LENGTH_SHORT).show();
break;
}
}
}
}
}
}

Is there a simpler way to check which download has been completed?

I'm currently creating an app that needs to download a couple of videos then save the local path of it on a SQLite database.
At first, I wanted to get the URL of the video I downloaded but I can't seem to find anything that discusses about it. I tried to get COLUMN_MEDIAPROVIDER_URI and COLUMN_URI from the intent passed on the BroadcastReceiver for DownloadManager.ACTION_DOWNLOAD_COMPLETE but they return null.
Then I found about EXTRA_DOWNLOAD_ID. But if I use that, I still need to use something like a new HashMap that got the EXTRA_DOWNLOAD_ID of my download and the id of the video on my SQLite database for checking which is which.
I'm fine with that but I want to know if there's an easier way to do the thing I want.
I did this using OkHttp, as follows:
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(YOUR_URL)
.build();
client.newCall(request).enqueue(new Callback() {
#Override
public void onFailure(Call call, IOException e) {
// ERROR MESSAGE
}
#Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
response.body().byteStream(); // byteStream with your result.
}
}
});
Another thing, maybe would be better if you store the videos on memory and just the address in your SQLite.
Using the code below from the SO question here
#Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
// get the DownloadManager instance
DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query q = new DownloadManager.Query();
Cursor c = manager.query(q);
if(c.moveToFirst()) {
do {
String name = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
Log.i("DOWNLOAD LISTENER", "file name: " + name);
} while (c.moveToNext());
} else {
Log.i("DOWNLOAD LISTENER", "empty cursor :(");
}
c.close();
}
}
and saving the download id on my ArrayList I was able to make a simpler way to check which download is finished.
I modified it to look like this for my use case.
Cursor c = dlMgr.query(new DownloadManager.Query());
boolean found = false;
if(c.moveToFirst()) {
do {
String dlFilePath = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
int dlId = Integer.parseInt( c.getString(c.getColumnIndex(DownloadManager.COLUMN_ID)) );
for(int x = 0; x < vidArrLst.size(); x++){
VideoAd va = vidArrLst.get(x);
if(va.getDownloadId() == dlId){
dbHelper.updateLocalPath(va.getVideoId(), dlFilePath);
va.setLocalPath(dlFilePath);
found = true;
break;
}
}
} while (c.moveToNext() && !found);
} else {
Log.d(TAG, "empty cursor :(");
}
UPDATE:
Sometimes this method will show that 2 downloads finished with the same file name which results to a video item to not have a local path. What I did is check if the local path is empty, download id is greater than 0, and if the download id is still downloading before playing a video so I can redownload a video and fix the gap and play the local file the next time the video needs to be played.

Android Download Manager. status is always pending

I try to use Download Manager to download some files form specific URL,
but the download request was never completed.
So I log some information to see what went wrong, it turns out the request is always in pending status, and the COLUMN_REASON is 0 which I couldn't find the corresponding description on the document.
COLUMN_STATUS: 1
COLUMN_REASON: 0
COLUMN_TOTAL_SIZE_BYTES: -1
COLUMN_BYTES_DOWNLOADED_SO_FAR: 0
Here is how to start a download.
val req = DownloadManager.Request(uri).apply {
addRequestHeader("Cookie", cookie)
allowScanningByMediaScanner()
setTitle(fullname)
setDescription(/* description text */)
setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, fullname)
}
val downloadId = downloadManager.enqueue(req)
And log information for debugging.
val filterQuery = DownloadManager.Query().setFilterById(downloadId)
val cursor = downloadManager.query(filterQuery)
if (cursor.moveToFirst()) {
val total = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
val current = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
Log.d("App", "status: " + status.toString())
Log.d("App", "reason: " + reason.toString())
Log.d("App", "total: " + total.toString())
Log.d("App", "current: " + current.toString())
}
So what's a possible reason that status of request was always pending and how do I debug it?
Any help is going to be appreciated.
In my case, settings up a VPN seem to solve this problem. It looks like google services have been blocked in my network and after I set up a system global VPN the issue has gone.
DownloadManager outputs its logs to logcat but not under your application's id, so you'll need to show logs for all apps. There should clues to the failed download in there. For example, here are a couple of my failure cases.
D/DownloadManager: [1988] Starting
W/DownloadManager: [1988] Stop requested with status 500: Internal Server Error
D/DownloadManager: [1988] Finished with status WAITING_TO_RETRY
and
W/DownloadManager: [1988] Stop requested with status 403: Unhandled HTTP response: 403 Forbidden
You have to wait(delay) before checking the status or set the download ID every time in a timer.
enqueue seems to return the download ID too late.
My code works very well:
private void startDownload(View v)
{
Uri uri=Uri.parse("http://example.com/app/name.apk");
DownloadManager mgr = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request req = new DownloadManager.Request(uri)
.setTitle(title)
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI
|DownloadManager.Request.NETWORK_MOBILE)
.setDescription("downloading")
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,"name.apk");
downloadId = mgr.enqueue(req);
getDownloadStatus();
}
check status method
private void getDownloadStatus()
{
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
Cursor cursor = ((DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE))
.query(query);
if (cursor.moveToFirst())
{
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
#Override
public void run() {
query.setFilterById(downloadId);
Cursor cursor = ((DownloadManager)context.getSystemService(Context.DOWNLOAD_SERVICE))
.query(query);
cursor.moveToFirst();
int status=cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
if (status == DownloadManager.STATUS_RUNNING) {
Log.i("DM_STATUS","status is "+" running");
}else if (status == DownloadManager.STATUS_SUCCESSFUL) {
Log.i("DM_STATUS","status is "+" success");
timer.cancel();
}
}
}, 100,1);
}
}

monitor web browser programmatically in Android?

I've got a tricky question here. I need users to make a payment to a bank (namely Barclaycard) in UK. To do so, I have a https URL , I add the parameters (such as amount to pay, order reference, etc) to the URL, start this http connection as an Intent.ActionView, which will redirect the user to the browser where he can enter his credit card details on the bank's webpage and make the payment to our account successfully. So far so good ?
The code I use is below (I changed values for privacy reasons) The problem is, I need to get back to the app when the user has completed/failed/cancelled the payment. Barclaycardautomatically redirects to a particular URL when the payment has succeeded, another one if it failed. Is there no way of knowing when Barclaycard payment has succeeded so that then I would go back to the android app somehow ?
Button cardbutton = (Button) findViewById(R.id.card_button);
cardbutton.setOnClickListener(new View.OnClickListener()
{
#Override
public void onClick(View arg0)
{
String preHashString = new String();
String proHashString = new String();
String SHAPassPhrase = new String();
SHAPassPhrase = "GSvTh£h70ZkHdAq9b"; // FOR TEST ENVIRONMENT
preHashString = preHashString + "AMOUNT=" + String.valueOf((int) (order.getPaymentAmount() * 100.00)) + SHAPassPhrase;
preHashString = preHashString + "BGCOLOR=cccccc" + SHAPassPhrase;
preHashString = preHashString + "CN=" + user.getString("name") + SHAPassPhrase;
preHashString = preHashString + "CURRENCY=GBP" + SHAPassPhrase;
preHashString = preHashString + "LANGUAGE=en_US" + SHAPassPhrase;
preHashString = preHashString + "ORDERID=" + order.getOrderId() + SHAPassPhrase;
try
{
proHashString = SHA1(preHashString);
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}
String redirecturl = "https://mdepayments.epdq.co.uk/ncol/test/orderstandard.asp";
redirecturl += "?AMOUNT=" + String.valueOf((int) (order.getPaymentAmount() * 100));
redirecturl += "&CN=" + user.getString("name");
redirecturl += "&CURRENCY=GBP";
redirecturl += "&LANGUAGE=en_US";
redirecturl += "&ORDERID=" + order.getOrderId();
redirecturl += "&SHASIGN=" + proHashString;
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(redirecturl));
startActivity(i);
}
});
You can have your own Webview in place inside your app, with some done / close button somewhere.. Then you can track all urls getting open in your WebView and do your stuff accordingly..User will stay in your app always..that solves your purpose..
For tracking all urls inside your WebView you need to register one WebViewClient and ovveride below function
public boolean shouldOverrideUrlLoading (WebView view, String url)
Have a look at WebView here and WebViewClient here
You should never be doing such things on user device. Someone can decompile your code and change it, so your app will "think" they made the payment.
This may lead to small problems like they using app for free to severe problems like you being forced to make all the payments.
Either use server-side solution or in-app-purchase from Google.
If your user gets redirected to a new URL you could use a ContentObserver that observes the bookmark history for any changes:
public class UrlObserver extends ContentObserver {
#Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
// check last URL in history
}
}
Reading the history can be done by:
private static final Uri CONTENT_URI = Browser.BOOKMARKS_URI;
Cursor cursor = context.getContentResolver().query(
CONTENT_URI, Browser.HISTORY_PROJECTION, null, null, null);
Registration of the content observer works with:
UrlObserver observer = new UrlObserver();
context.getContentResolver().registerContentObserver(CONTENT_URI, true, observer);
Once a particular URL has been detected, you can invoke an intent to bring your activity back to front.
This is a sample app which might help you in this case.
I'm not 100% sure what happens if the same site is used for the form transmission. It might be that the content observer won't trigger. In that case you might find some useful log entries.
Note: Chrome and the Android standard browser use different URLs for the query. Search the internet to find the right one.
Hope this helps .... Cheers!

Android DownloadManager: Download fails, but COLUMN_REASON only returns "placeholder"

DownloadManager appears to be the right choice for an app with lots of background downloads on a flaky mobile internet connection.
Using tutorial code found on the web, the app is able to request a download from the system's DM like so:
// in onCreate()
dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
// in requestData()
Uri u = Uri.parse("http://server:8000/feed/data");
Request dreq = new Request(u);
dreq.setNotificationVisibility(Request.VISIBILITY_HIDDEN);
downloadID = dm.enqueue(dreq);
The URL in that code is a test server on a local computer. The URL works, the browser in Android emulator can retrieve the page and the server returns HTTP code 200 when my app requests that page via DownloadManager and the code quoted above.
This is the relevant code in the ACTION_DOWNLOAD_COMPLETE BroadcastReceiver which gets called when DM has retrieved the file.
Query q = new Query();
q.setFilterById(downloadID);
Log.i("handleData()", "Handling data");
Cursor c = dm.query(q);
if (c.moveToFirst()) {
Log.i("handleData()", "Download ID: " + downloadID + " / " + c.getInt(c.getColumnIndex(DownloadManager.COLUMN_ID)));
Log.i("handleData()", "Download Status: " + c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)));
if (c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)) == DownloadManager.STATUS_SUCCESSFUL) {
String uriString = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
Log.i("handleData()", "Download URI: " + uriString);
} else if (c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS)) == DownloadManager.STATUS_FAILED) {
Log.i("handleData()", "Reason: " + c.getString(c.getColumnIndex(DownloadManager.COLUMN_REASON)));
}
}
The strange result is this:
The DOWNLOAD_STATUS is 16 (or STATUS_FAILED), but the reason is "placeholder".
Why is that? Why does it fail when the server returned a 200 status code? And why is there no reason given by DownloadManager?
Answering myself here.
Here's the problem: COLUMN_REASON is not a string, but a number value.
Log.i("handleData()", "Reason: " + c.getInt(c.getColumnIndex(DownloadManager.COLUMN_REASON)));
will return an actual error code that one can work with.

Categories

Resources