Updating my app remotely without Google Play access - java

I am working on an app for my company's internal use which will collect performance stats from network and post them on our Grafana server.
The app works fine with this context, but there is a problem:
App will run on a phone at a datacenter and it will be very difficult to access it if we need to update the app for adding features.
Also the phone will not have internet access. So I won't be able to update the app manually , or using Google Play.
I thought of writing a function to check a static URL and when we put an updated apk there, it would download it and install.
I wrote this class (copying from another Stackoverflow question):
class updateApp extends AsyncTask<String, Void, String> {
protected String doInBackground(String... sUrl) {
File file = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS),"updates");
if(!file.mkdir()){
}
File f = new File(file.getAbsolutePath(),"YourApp.apk");
Uri fUri = FileProvider.getUriForFile(MainActivity.this,"com.aktuna.vtv.monitor.fileprovider",f);
String path = fUri.getPath();
try {
URL url = new URL(sUrl[0]);
URLConnection connection = url.openConnection();
connection.connect();
InputStream input = new BufferedInputStream(url.openStream());
OutputStream output = new FileOutputStream(path);
byte data[] = new byte[1024];
int count;
while ((count = input.read(data)) != -1) {
output.write(data, 0, count);
}
output.flush();
output.close();
input.close();
} catch (Exception e) {
Log.e("YourApp", "Well that didn't work out so well...");
Log.e("YourApp", e.getMessage());
}
return path;
}
#Override
protected void onPostExecute(String path) {
Intent i = new Intent();
i.setAction(Intent.ACTION_VIEW);
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
File file = new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS),"updates");
File f = new File(file.getAbsolutePath(),"YourApp.apk");
Uri fUri = FileProvider.getUriForFile(MainActivity.this,"com.aktuna.vtv.monitor.fileprovider",f);
i.setDataAndType(fUri, "application/vnd.android.package-archive" );
myCtx.startActivity(i);
}
}
It seems to download the file successfully. And then it sends the file in the intent to the installer (I can see this because the packageinstaller selection prompt comes)
But then it does not install the new apk.
Since the previous Stackoverflow question is 7 years old, I thought that updating with no user interaction may be forbidden in new API levels.
But I am not sure.
How can I troubleshoot this further ?
Also, I am open to any suggestions to achieve this, maybe something making use of older API levels or anything that would solve the "updating with no internet access through an internal static URL" issue.
Thanks.

I followed recommendation from #keag and it worked.
1.
With no "root" on the device, I made the app "device-owner"
For this I added a device admin receiver class. SampleAdminReceiver.class:
import android.app.admin.DeviceAdminReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
public class SampleAdminReceiver extends DeviceAdminReceiver {
void showToast(Context context, CharSequence msg) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
#Override
public void onEnabled(Context context, Intent intent) {
showToast(context, "Device admin enabled");
}
#Override
public void onDisabled(Context context, Intent intent) {
showToast(context, "Device admin disabled");
}
}
added receiver to the manifest:
<receiver
android:name=".SampleAdminReceiver"
android:description="#string/app_name"
android:label="#string/app_name"
android:permission="android.permission.BIND_DEVICE_ADMIN" >
<meta-data
android:name="android.app.device_admin"
android:resource="#xml/device_admin_receiver" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
Then using the adb interface I run the following dpm command:
$ dpm set-device-owner com.sample.app/.SampleAdminReceiver
Added following permission to manifest :
<uses-permission android:name="android.permission.INSTALL_PACKAGES" />
The with the following function I am able to install the apk from URL:
public static boolean installPackageX(final Context context, final String url)
throws IOException {
//Use an async task to run the install package method
AsyncTask<Void,Void,Void> task = new AsyncTask<Void, Void, Void>() {
#Override
protected Void doInBackground(Void... voids) {
try {
PackageInstaller packageInstaller = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
packageInstaller = context.getPackageManager().getPackageInstaller();
}
PackageInstaller.SessionParams params = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
}
// set params
int sessionId = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sessionId = packageInstaller.createSession(params);
}
PackageInstaller.Session session = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
session = packageInstaller.openSession(sessionId);
}
OutputStream out = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
out = session.openWrite("COSU", 0, -1);
}
//get the input stream from the url
HttpURLConnection apkConn = (HttpURLConnection) new URL(url).openConnection();
InputStream in = apkConn.getInputStream();
byte[] buffer = new byte[65536];
int c;
while ((c = in.read(buffer)) != -1) {
out.write(buffer, 0, c);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
session.fsync(out);
}
in.close();
out.close();
//you can replace this intent with whatever intent you want to be run when the applicaiton is finished installing
//I assume you have an activity called InstallComplete
Intent intent = new Intent(context, MainActivity.class);
intent.putExtra("info", "somedata"); // for extra data if needed..
Random generator = new Random();
PendingIntent i = PendingIntent.getActivity(context, generator.nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
session.commit(i.getIntentSender());
}
} catch (Exception ex){
ex.printStackTrace();
Log.e("AppStore","Error when installing application. Error is " + ex.getMessage());
}
return null;
}
};
task.execute(null,null);
return true;
}
After that, it is just a matter of automating the process.
Btw, following code in the app is useful for removing "device owner" property.
DevicePolicyManager dpm = (DevicePolicyManager) getApplicationContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
dpm.clearDeviceOwnerApp(getApplicationContext().getPackageName());

Related

Open PDF after download with DownloadManager and FileProvider on Android Q (10)

targetSdkVersion: 30
In our App we have a feature, where we download files (mostly pdf) to the public download folder and start an intent afterwards to open it. Our code works fine for android apps with api >= 28 and >= 30. Just our app on Android 10 (sdkVersion 29) will try to open the document and instantly closes the activity that tried to display the pdf. The logcat shows following error:
22-03-17 14:23:42.486 12161-15168/? E/DisplayData: openFd: java.io.FileNotFoundException: open failed: EACCES (Permission denied)
22-03-17 14:23:42.486 12161-15168/? E/PdfLoader: Can't load file (doesn't open) Display Data [PDF : download.pdf] +ContentOpenable, uri: content://com.example.fileprovider/Download/download.pdf
If I grant the files permission in the app settings, it will work flawlessly but if I understood the android documentation correctly this should not be necessary since Android 10. Especially because there are no problems on Android 11 and Android 12 devices that do not have this permission. On all Android versions the file will be downloaded correctly and the user could manually open it from the download section of his device.
This is the Android Manifest section for the fileprovider
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/filepaths" />
</provider>
The filepaths XML file
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="external_files"
path="." />
<external-path
name="Download"
path="Download/"/>
<files-path
name="files"
path="." />
<external-cache-path
name="external_cache"
path="." />
</paths>
This is the code with the DownloadManager usage to download the files
public static long downloadFile(Context context, String url, String fileName) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
try {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));
request.setTitle(fileName)
.setDescription(Localization.getStringClient("file_download_progress"))
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
return downloadManager.enqueue(request);
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(context, Localization.getStringClient("error_downloading_asset"), Toast.LENGTH_SHORT).show();
}
return -1;
}
The broadcast receiver that listens on the download progress
public class DownloadBroadcastReceiver extends BroadcastReceiver {
private long downloadId = -2;
public void setDownloadId(long downloadId) {
this.downloadId = downloadId;
}
#Override
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (id == downloadId) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(id);
Cursor c = downloadManager.query(query);
if (c != null) {
c.moveToFirst();
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
String uriString = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
String mediaType = c.getString(c.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE));
Uri fileUri = Uri.parse(uriString);
DownloadUtils.openFile(context, fileUri, mediaType);
} else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)) {
Toast.makeText(context, Localization.getStringClient("error_downloading_asset"), Toast.LENGTH_SHORT).show();
}
c.close();
}
}
}
}
The code to open the file. Only on Android 10 the app closes the activity instantly again.
public static void openFile(Context context, Uri fileUri, String mediaType) {
File file = new File(fileUri.getPath());
Uri uri = FileProvider.getUriForFile(
context,
context.getApplicationContext().getPackageName() + ".fileprovider",
file
);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, mediaType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
Toast.makeText(context, Localization.getStringClient("open_file_no_activity"), Toast.LENGTH_SHORT).show();
}
}
And a download call in the looks like this
#Override
public void startPDFDownload(String pdfDownloadUrl, String fileName) {
long downloadId = DownloadUtils.downloadFile(requireContext(), pdfDownloadUrl, fileName);
if (downloadId > -1) {
DownloadBroadcastReceiver receiver = new DownloadBroadcastReceiver();
receiver.setDownloadId(downloadId);
requireContext().registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
}
I think I have a false understanding of how file handling works in Android 10 but I do not know where I have to adjust the code or configuration. Help is really appreciated. Currently as a workaround we ask for the permission for WRITE_EXTERNAL_STORAGE to open downloaded files on Android 10. But I would prefer to do it the right way.
Solution:
I adjusted the BroadcastReceiver to the following code. I removed the LOCAL_URI from the cursor and used the URI from the DownloadManager method.
public class DownloadBroadcastReceiver extends BroadcastReceiver {
private long downloadId = -2;
public void setDownloadId(long downloadId) {
this.downloadId = downloadId;
}
#Override
public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if (id == downloadId) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(id);
Cursor c = downloadManager.query(query);
if (c != null) {
c.moveToFirst();
int columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS);
if (DownloadManager.STATUS_SUCCESSFUL == c.getInt(columnIndex)) {
String mediaType = c.getString(c.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE));
DownloadUtils.openFile(context, downloadManager.getUriForDownloadedFile(id), mediaType);
} else if (DownloadManager.STATUS_FAILED == c.getInt(columnIndex)) {
Toast.makeText(context, Localization.getStringClient("error_downloading_asset"), Toast.LENGTH_SHORT).show();
}
c.close();
}
}
}
}
And I adjusted the method to open the file with the uri to following code. I removed the FileProvider code and used the uri from the DownloadManager.
public static void openFile(Context context, Uri fileUri, String mediaType) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(fileUri, mediaType);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
Toast.makeText(context, Localization.getStringClient("open_file_no_activity"), Toast.LENGTH_SHORT).show();
}
}
The code of the other methods remains the same.
You should not use FileProvider to obtain an uri for your file.
You can get an uri from DownloadManager and use it to serve your file.
Code would be the same for all Android versions.
Not a single permission needed.

Can't install apk from uri on Android 10 in Eclipse projects

We are installing apk from file uri, its working upto Android Pie(9) ,in Android 10 mobile its showing "There was problem while Parsing". we have stored file in the application storage only and the build version is 4.4
I have shared the code below.
String PATH = Objects.requireNonNull(this.myActivity.getExternalFilesDir(null)).getAbsolutePath();
File file = new File(PATH + "/"+Utils.apk_name);
intent = new Intent("android.intent.action.VIEW");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri uri = FileProvider.getUriForFile(this.myActivity, this.myActivity.getApplicationContext().getPackageName() + ".provider", file);
//Intrinsics.checkExpressionValueIsNotNull(uri, "FileProvider.getUriForFi…eProvider.install\", file)");
intent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(intent);
Provider in Manifest
<provider
android:name="com.kirubha.helpers.ESEProvider"
android:authorities="com.kirubha.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/provider_paths" />
</provider>
provider_paths
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="external_storage_root"
path="."/>
</paths>
I have found most of my file provider worries disapear when I add
StrictMode.VmPolicy.Builder buildervm = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(buildervm.build());
to my main activities.
The above was found in this answer which might be useful to you.
android.os.FileUriExposedException: file:///storage/emulated/0/test.txt exposed beyond app through Intent.getData()
In Android 10, ACTION_VIEW is deprecated, need to use PackageInstaller instead.
Follow the below code to implement PackageInstaller in Java.
private static int REQUEST_OEN_DOCUMENT = 1337;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent intent1 = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent1.setType("application/vnd.android.package-archive");
intent1.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent1, REQUEST_OPEN_DOCUMENT);
}
This code will redirect to the files folder. Then need to select the .apk file.
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_OEN_DOCUMENT) {
if (resultCode == Activity.RESULT_OK) {
if (data != null) {
Uri uri = data.getData();
installAfterQ(uri);
}
}
}
}
public void installAfterQ(Uri uri) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
InputStream inputStream = getApplication().getContentResolver().openInputStream(uri);
long length = DocumentFile.fromSingleUri(getApplication(), uri).length();
PackageInstaller installer = getApplication().getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
int sessionId = installer.createSession(params);
PackageInstaller.Session session = installer.openSession(sessionId);
File file = new File(uri.getPath());
OutputStream outputStream = session.openWrite(file.getName(), 0, length);
byte[] buf = new byte[1024];
int len;
while ((len = inputStream.read(buf)) > 0) {
outputStream.write(buf, 0, (int) len);
}
session.fsync(outputStream);
outputStream.close();
inputStream.close();
Intent intent = new Intent(getApplication(), UpdateReceiver.class);
PendingIntent pi = PendingIntent.getBroadcast(getApplication(), 3439, intent, PendingIntent.FLAG_UPDATE_CURRENT);
session.commit(pi.getIntentSender());
session.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
Update Receiver Class:
public class UpdateReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1);
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
Intent activityIntent =intent.getParcelableExtra(Intent.EXTRA_INTENT);
context.startActivity(activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
} else if (status == PackageInstaller.STATUS_SUCCESS) {
Toast.makeText(context, "Successfully Installed", Toast.LENGTH_SHORT).show();
} else {
String msg =intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
Toast.makeText(context, "Error while installing" + msg, Toast.LENGTH_SHORT).show();
}
}
}
Android Manifest.xml
Inside Application tag.
<receiver android:name=".UpdateReceiver"/>
<meta-data
android:name="com.android.packageinstaller.notification.smallIcon"
android:resource="#drawable/ic_install_notification" />
Permission:
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
Try this code. Hope this code is helpful.

How to retrieve and open PDF file saved to downloads through MediaStore API in Android?

I am downloading a PDF file from a server and passing the response body bytestream into the function below, which is storing the PDF file successfully in the user downloads folder.
#RequiresApi(Build.VERSION_CODES.Q)
fun saveDownload(pdfInputStream: InputStream) {
val values = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, "test")
put(MediaStore.Downloads.MIME_TYPE, "application/pdf")
put(MediaStore.Downloads.IS_PENDING, 1)
}
val resolver = context.contentResolver
val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val itemUri = resolver.insert(collection, values)
if (itemUri != null) {
resolver.openFileDescriptor(itemUri, "w").use { parcelFileDescriptor ->
ParcelFileDescriptor.AutoCloseOutputStream(parcelFileDescriptor)
.write(pdfInputStream.readBytes())
}
values.clear()
values.put(MediaStore.Downloads.IS_PENDING, 0)
resolver.update(itemUri, values, null, null)
}
}
Now once this function returns I want to open the saved PDF file. I've tried several ways to get this to work but the pickers always say that there is nothing to open the file. I think that there is either still a permissions issue going on (maybe I'm using the FileProvider wrong?), or perhaps the path is wrong, or it could be something else entirely.
Here's a couple of examples of what I've tried:
fun uriFromFile(context: Context, file: File): Uri {
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
}
a)
val openIntent = Intent(Intent.ACTION_VIEW)
openIntent.putExtra(Intent.EXTRA_STREAM, uriFromFile(this, File(this.getExternalFilesDir(DIRECTORY_DOWNLOADS)?.absolutePath.toString(), "test")))
openIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
openIntent.type = "application/pdf"
startActivity(Intent.createChooser(openIntent, "share.."))
b)
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.putExtra(Intent.EXTRA_STREAM, uriFromFile(this, File(this.getExternalFilesDir(null)?.absolutePath.toString(), "test.pdf")))
shareIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
shareIntent.type = "application/pdf"
startActivity(Intent.createChooser(shareIntent, "share.."))
c)
val file = File(itemUri.toString()) //itemUri from the saveDownload function
val target = Intent(Intent.ACTION_VIEW)
val newFile = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".provider", file);
target.setDataAndType(newFile, "application/pdf")
target.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
val intent = Intent.createChooser(target, "Open File")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
ContextCompat.startActivity(this, intent, null)
d)
val target = Intent(Intent.ACTION_VIEW)
target.setDataAndType(Uri.parse("content://media/external_primary/downloads/2802"), "application/pdf"
target.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
val intent = Intent.createChooser(target, "Open File")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
ContextCompat.startActivity(this, intent, null)
(also tried /test.pdf on the end of this URI, and replacing media with my authority name)
I have also added this to my manifest file within the application tags:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/provider_paths" />
</provider>
#xml/provider_paths is as follows, although I have tried various combinations in addition to this including the paths as ".":
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path name="files_root" path="/"/>
<files-path name="files_root" path="/"/>
<external-path name="files_root" path="/"/>
</paths>
As a side note, there is definitely pickers available capable of opening PDFs, and going into the file explorer and opening it from there works fine. When attempting to share instead of opening the sharing also fails.
Follow this step and code, it will manage everything from downloading your pdf and opening it.
Create a class name as DownloadTask and put the complete code given below
public class DownloadTask {
private static final String TAG = "Download Task";
private Context context;
private String downloadFileUrl = "", downloadFileName = "";
private ProgressDialog progressDialog;
long downloadID;
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) {
downloadCompleted(downloadID);
}
}
};
public DownloadTask(Context context, String downloadUrl) {
this.context = context;
this.downloadFileUrl = downloadUrl;
downloadFileName = downloadFileUrl.substring(downloadFileUrl.lastIndexOf('/') + 1);//Create file name by picking download file name from URL
Log.e(TAG, downloadFileName);
context.registerReceiver(onDownloadComplete, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
downloadFile(downloadFileUrl);
}
public void downloadFile(String url) {
try {
File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(), downloadFileName);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url))
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)// Visibility of the download Notification
.setDestinationInExternalPublicDir(
Environment.DIRECTORY_DOWNLOADS,
downloadFileName
)
.setDestinationUri(Uri.fromFile(file))
.setTitle(downloadFileName)// Title of the Download Notification
.setDescription("Downloading")// Description of the Download Notification
.setAllowedOverMetered(true)// Set if download is allowed on Mobile network
.setAllowedOverRoaming(true);// Set if download is allowed on roaming network
request.allowScanningByMediaScanner();
DownloadManager downloadManager = (DownloadManager) context.getSystemService(DOWNLOAD_SERVICE);
downloadID = downloadManager.enqueue(request);// enqueue puts the download request in the queue.
progressDialog = new ProgressDialog(context);
progressDialog.setMessage("Downloading...");
progressDialog.setCancelable(false);
progressDialog.show();
} catch (Exception e) {
Log.d("Download", e.toString());
}
}
void downloadCompleted(long downloadID) {
progressDialog.dismiss();
new AlertDialog.Builder(context)
.setTitle("Document")
.setMessage("Document Downloaded Successfully")
.setPositiveButton("Open", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
openDownloadedAttachment(downloadID);
}
})
// A null listener allows the button to dismiss the dialog and take no further action.
.setNegativeButton(android.R.string.no, null)
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
context.unregisterReceiver(onDownloadComplete);
}
Uri path;
private void openDownloadedAttachment(final long downloadId) {
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(downloadId);
Cursor cursor = downloadManager.query(query);
if (cursor.moveToFirst()) {
int downloadStatus = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
String downloadLocalUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
String downloadMimeType = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_MEDIA_TYPE));
if ((downloadStatus == DownloadManager.STATUS_SUCCESSFUL) && downloadLocalUri != null) {
path = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".provider", new File(Uri.parse(downloadLocalUri).getPath()));
//path = Uri.parse(downloadLocalUri);
Intent pdfIntent = new Intent(Intent.ACTION_VIEW);
pdfIntent.setDataAndType(path, downloadMimeType);
pdfIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_CLEAR_TOP);
try {
context.startActivity(pdfIntent);
} catch (ActivityNotFoundException e) {
Toast.makeText(context, "No Application available to view PDF", Toast.LENGTH_SHORT).show();
}
}
}
cursor.close();
}
}
And then download your pdf like this from your activity.
new DownloadTask(this, "PDF_URL");
And from your fragment
new DownloadTask(getContext(), "PDF_URL");
After download completed it will open your pdf automatically.
According to Android Developer, MediaStore isn't being used for accessing non-media files such as pdf files:
If your app works with documents and files that don't exclusively
contain media content, such as files that use the EPUB or PDF file
extension, use the ACTION_OPEN_DOCUMENT intent action, as described in
the guide on how to store and access documents and other files.
Moreover, there isn't any official solution to access non-media files by means of using Cursor and Content Provider. However, there is an official and clean code approach which I've tested it on Android 11 and worked as expected. here is:
public class retrieve_pdf_file {
#RequiresApi(Build.VERSION_CODES.Q)
public static void get(Activity activity) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("application/pdf");
// Optionally, specify a URI for the file that should appear in the
// system file picker when it loads.
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI);
activity.startActivityForResult(intent, main_activity.PICK_PDF_FILE);
}
public static void get(Activity activity, String filename) { // filename is used for lower that API level 29
// older that API level 29 approaches
File file = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
// TODO
}
}
And also, to get the selected pdf file's Uri you must listen for the activity's result:
#Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
if (requestCode == PICK_PDF_FILE && resultCode == Activity.RESULT_OK) {
System.out.println("request code: PICK_PDF_FILE && result code: OK");
// The result data contains a URI for the document or directory that
// the user selected.
Uri uri = null;
if (resultData != null) {
uri = resultData.getData();
// Perform operations on the document using its URI.
System.out.println(uri);
} else {
System.out.println("resultData is null");
}
} else {
System.out.println("result code: NOT OK");
}
}
This is the official solution that can be found in Android Developer for API level 29 or higher.
Here is the code that i use to open doc file with Uri.
fun viewPDFIntent(fileUri: Uri?, context: Context, title: String?, type: String) {
val viewPDFIntent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(fileUri, type)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
context.startActivity(Intent.createChooser(viewPDFIntent, title))
}
Here type for pdf is "application/pdf".
You are getting created pdf uri in itemUri variable, pass this to first argument of this function.

PendingIntent with implicit intent returning cancelled exception when using OpenId AppAuth-Android library

I am trying to implement oauth2 to enable users to login with Reddit. I have created my app on reddit with the appropriate redirect uri.
What I did:
A MainActivity with a login button. Clicking the login button, starts the authorization flow. To create the authorization request, we need to pass a pending intent that the library uses to call the appropriate component that we want it to call after authorization is successful.
Problem:
When the pending intent is made using an implicit intent (setting only action string while creating intent), the library gets a cancelled exception while invoking the pending intent. I have mentioned the action string in the intent filter for the MainActivity in manifest file also.
What I have tried:
1. I tried creating pending intent using an explicit intent (defining the activity class I want to open while creating intent), my activity's onStart is getting called with the correct intent.
2. I tried by directly invoking the pending intent (with implicit intent) from the activity itself and it got called successfully.
Observation:
1. If I use an older version of the library (v0.2.0), the pending intent with implicit intent works fine.
Current version of OpenId AppAuth library - 0.7.1
Tested on Android 9 (Pie) - OnePlus 3T
Below is my MainActivity.java
package com.prateekgrover.redditline;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import com.prateekgrover.redditline.services.RedditAuthService;
import net.openid.appauth.AuthState;
import net.openid.appauth.AuthorizationException;
import net.openid.appauth.AuthorizationRequest;
import net.openid.appauth.AuthorizationResponse;
import net.openid.appauth.AuthorizationService;
import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.TokenRequest;
import net.openid.appauth.TokenResponse;
import java.util.UUID;
public class MainActivity extends AppCompatActivity {
private String USED_INTENT = "1";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button loginButton = findViewById(R.id.reddit_login);
loginButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Intent intent = new Intent(MainActivity.this, RedditAuthService.class);
// startService(intent);
performRedditAuthAction(MainActivity.this, "com.prateekgrover.redditline.HANDLE_AUTHORIZATION_RESPONSE");
}
});
}
public void performRedditAuthAction(Context context, String actionRedirect) {
String uuid = UUID.randomUUID().toString();
AuthorizationServiceConfiguration serviceConfiguration = new AuthorizationServiceConfiguration(
Uri.parse("https://www.reddit.com/api/v1/authorize") /* auth endpoint */,
Uri.parse("https://www.reddit.com/api/v1/access_token") /* token endpoint */
);
String clientId = "<my client id>";
Uri redirectUri = Uri.parse("com.prateekgrover.redditline://oauth2callback");
AuthorizationRequest.Builder builder = new AuthorizationRequest.Builder(
serviceConfiguration,
clientId,
"code",
redirectUri
);
builder.setState(uuid);
builder.setScopes("identity", "mysubreddits", "read", "save", "submit", "subscribe", "vote");
AuthorizationRequest request = builder.build();
AuthorizationService authorizationService = new AuthorizationService(context);
String action = actionRedirect;
Intent postAuthorizationIntent = new Intent("com.prateekgrover.redditline.HANDLE_AUTHORIZATION_RESPONSE");
PendingIntent pendingIntent = PendingIntent.getActivity(this, request.hashCode(), postAuthorizationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
authorizationService.performAuthorizationRequest(request, pendingIntent);
}
#Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (intent != null && intent.getAction() != null) {
String action = intent.getAction();
switch (action) {
case "com.prateekgrover.redditline.HANDLE_AUTHORIZATION_RESPONSE":
redirectIntent(intent);
break;
default:
}
}
}
private void redirectIntent(#Nullable Intent intent) {
if (!intent.hasExtra(USED_INTENT)) {
handleAuthorizationResponse(intent);
intent.putExtra(USED_INTENT, true);
}
}
private void handleAuthorizationResponse(Intent intent) {
AuthorizationResponse response = AuthorizationResponse.fromIntent(intent);
AuthorizationException error = AuthorizationException.fromIntent(intent);
final AuthState authState = new AuthState(response, error);
if (response != null) {
AuthorizationService service = new AuthorizationService(this);
service.performTokenRequest(response.createTokenExchangeRequest(), new AuthorizationService.TokenResponseCallback() {
#Override
public void onTokenRequestCompleted(#Nullable TokenResponse tokenResponse, #Nullable AuthorizationException exception) {
if (exception != null) {
} else {
if (tokenResponse != null) {
authState.update(tokenResponse, exception);
System.out.println(tokenResponse.accessToken + " refresh_token " + tokenResponse.refreshToken);
}
}
}
});
}
}
#Override
protected void onActivityResult(int requestCode, int resultCode, #Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
#Override
protected void onStart() {
super.onStart();
Intent intent = getIntent();
if (intent != null && intent.getAction() != null) {
String action = intent.getAction();
switch (action) {
case "com.prateekgrover.redditline.HANDLE_AUTHORIZATION_RESPONSE":
redirectIntent(intent);
break;
default:
}
}
}
}
Manifest File:
<activity android:name=".MainActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="com.prateekgrover.redditline.HANDLE_AUTHORIZATION_RESPONSE"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
Relevant parts of the library - mCompleteIntent is the PendingIntent that I sending to the library
private void extractState(Bundle state) {
if (state == null) {
Logger.warn("No stored state - unable to handle response");
finish();
return;
}
mAuthIntent = state.getParcelable(KEY_AUTH_INTENT);
mAuthorizationStarted = state.getBoolean(KEY_AUTHORIZATION_STARTED, false);
try {
String authRequestJson = state.getString(KEY_AUTH_REQUEST, null);
mAuthRequest = authRequestJson != null
? AuthorizationRequest.jsonDeserialize(authRequestJson)
: null;
} catch (JSONException ex) {
throw new IllegalStateException("Unable to deserialize authorization request", ex);
}
mCompleteIntent = state.getParcelable(KEY_COMPLETE_INTENT);
mCancelIntent = state.getParcelable(KEY_CANCEL_INTENT);
}
private void handleAuthorizationComplete() {
Uri responseUri = getIntent().getData();
Intent responseData = extractResponseData(responseUri);
if (responseData == null) {
Logger.error("Failed to extract OAuth2 response from redirect");
return;
}
responseData.setData(responseUri);
if (mCompleteIntent != null) {
Logger.debug("Authorization complete - invoking completion intent");
try {
mCompleteIntent.send(this, 0, responseData);
} catch (CanceledException ex) {
Logger.error("Failed to send completion intent", ex);
}
} else {
setResult(RESULT_OK, responseData);
}
}
In case anybody else stumbles upon this issue.
Use the example app within app-auth android github project.
Don't use Google CodeLabs app-auth example! The code from the question above is from Google CodeLabs, it is very old and no longer works (state at July 2020).
I did the same mistake, app-auth links codelabs on their own page/readme, so I started using codelabs code and ended up with lots of problems and errors.
The new app-auth version 0.7.x uses a json configuration file and the example app shows how to handle errors around pending intents etc. .

Download Manager Unable to Resume Download in case of Internet Disconnection and System Reboot

I have created a simple application which is supposed to download large zip files. After some R&D I came to the conclusion that I have to use Download Manager to achieve this. I want the download to resume automatically if the device is restarted or in case of unstable internet connectivity. Right now, the code is able to download large files as expected, but in case of internet connectivity fluctuations or system restart, it stops downloading.
The activity:
public class MainActivity extends ActionBarActivity {
String Download_path = "http://wickedbrains.com/map/mumbai.zip";
String Download_ID = "DOWNLOAD_ID";
SharedPreferences preferenceManager;
DownloadManager downloadManager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
preferenceManager = PreferenceManager.getDefaultSharedPreferences(this);
downloadManager = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
Button btnDownload = (Button)findViewById(R.id.download);
btnDownload.setOnClickListener(new Button.OnClickListener(){
#Override
public void onClick(View arg0) {
// Locate storage location
String filepath = "";
File folder = new File(
Environment.getExternalStorageDirectory() + "/osmdroid");
boolean success = true;
if (!folder.exists()) {
success = folder.mkdir();
}
if (success) {
// Do something on success
filepath = Environment.getExternalStorageDirectory()
.getPath() + "/osmdroid";
// Deleting if zip file exists
File folder2 = Environment.getExternalStorageDirectory();
String fileName = folder2.getPath() + "/osmdroid/mumbai.zip";
File myFile = new File(fileName);
if(myFile.exists())
myFile.delete();
}
//Starting download manager to download file
Uri Download_Uri = Uri.parse(Download_path);
DownloadManager.Request request = new DownloadManager.Request(Download_Uri);
long download_id = downloadManager.enqueue(request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI |DownloadManager.Request.NETWORK_MOBILE)
.setAllowedOverRoaming(false)
.setTitle("Test")
.setDescription("Map Download")
.setDestinationInExternalPublicDir("/osmdroid","mumbai.zip"));
// long download_id = downloadManager.enqueue(request);
//Save the download id
Editor PrefEdit = preferenceManager.edit();
PrefEdit.putLong(Download_ID, download_id);
PrefEdit.commit();
}});
}
#Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
registerReceiver(downloadReceiver, intentFilter);
}
#Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
unregisterReceiver(downloadReceiver);
}
private BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context arg0, Intent arg1) {
// TODO Auto-generated method stub
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(preferenceManager.getLong(Download_ID, 0));
Cursor cursor = downloadManager.query(query);
if(cursor.moveToFirst()){
int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
int status = cursor.getInt(columnIndex);
int columnReason = cursor.getColumnIndex(DownloadManager.COLUMN_REASON);
int reason = cursor.getInt(columnReason);
if(status == DownloadManager.STATUS_SUCCESSFUL){
//Retrieve the saved download id
long downloadID = preferenceManager.getLong(Download_ID, 0);
ParcelFileDescriptor file;
try {
file = downloadManager.openDownloadedFile(downloadID);
Toast.makeText(MainActivity.this,
"File Downloaded: " + file.toString(),
Toast.LENGTH_LONG).show();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Toast.makeText(MainActivity.this,
e.toString(),
Toast.LENGTH_LONG).show();
}
}else if(status == DownloadManager.STATUS_FAILED){
Toast.makeText(MainActivity.this,
"FAILED!\n" + "reason of " + reason,
Toast.LENGTH_LONG).show();
}else if(status == DownloadManager.STATUS_PAUSED){
Toast.makeText(MainActivity.this,
"PAUSED!\n" + "reason of " + reason,
Toast.LENGTH_LONG).show();
}else if(status == DownloadManager.STATUS_PENDING){
Toast.makeText(MainActivity.this,
"PENDING!",
Toast.LENGTH_LONG).show();
}else if(status == DownloadManager.STATUS_RUNNING){
Toast.makeText(MainActivity.this,
"RUNNING!",
Toast.LENGTH_LONG).show();
}
}
}
};
}
Where am I going wrong? What should I do to enable the resume capability of the download?
Quoting from docs,
The download manager will conduct the download in the background, taking care of HTTP interactions and retrying downloads after failures or across connectivity changes and system reboots.
I guess Download Manager, by default takes cares of retries.
If you are having issues you can use DownloadManager.Query class and query for COLUMN_STATUS and COLUMN_REASON to get the download status
Edit:
Starting a download
dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
Request request = new Request( YOUR_DOWNLOAD_URL );
long enqueue = dm.enqueue(request);
enqueue is more like a download reqeust id. You can use that enqueue to fetch the download progress/status
Querying the download Status
Query query = new Query();
query.setFilterById(enqueue);
Cursor c = dm.query(query);
if (c.moveToFirst()) {
int downloadStatus = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
if (DownloadManager.STATUS_SUCCESSFUL == downloadStatus) {
// download succeded
} else if (DownloadManager.STATUS_FAILED == downloadStatus){
String failedReason = c.getString(c.getColumnIndex(DownloadManager.COLUMN_REASON));
// handle failures
}
}
Haven't tested the code myself. But it should work.
I confirm that this problem still exists in 2020, when testing in an emulator and having WiFi enabled, this error consistently appears (even with Android 10).
Switching off WiFi in the emulator seems to solve the problem.
Try to get the reason for the failed download.
e.g does it work on network switch wifi->data
(If your error reason is 1008- there seems to be a reported bug here
https://code.google.com/p/android/issues/detail?id=18462,
further:
http://papaya-backend.net/2013/04/12/why-http-etag-header-may-cause-your-downloading-apps-on-android-failed/)

Categories