Working on android Java, recently updated SDK to API level 29 now there is a warning shown which states that
Environment.getExternalStorageDirectory() is deprecated in API level 29
My code is
private void saveImage() {
if (requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
final String folderPath = Environment.getExternalStorageDirectory() + "/PhotoEditors";
File folder = new File(folderPath);
if (!folder.exists()) {
File wallpaperDirectory = new File(folderPath);
wallpaperDirectory.mkdirs();
}
showLoading("Saving...");
final String filepath=folderPath
+ File.separator + ""
+ System.currentTimeMillis() + ".png";
File file = new File(filepath);
try {
file.createNewFile();
SaveSettings saveSettings = new SaveSettings.Builder()
.setClearViewsEnabled(true)
.setTransparencyEnabled(true)
.build();
if(isStoragePermissionGranted() ) {
mPhotoEditor.saveAsFile(file.getAbsolutePath(), saveSettings, new PhotoEditor.OnSaveListener() {
#Override
public void onSuccess(#NonNull String imagePath) {
hideLoading();
showSnackbar("Image Saved Successfully");
mPhotoEditorView.getSource().setImageURI(Uri.fromFile(new File(imagePath)));
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,Uri.fromFile(new File(filepath))));
Intent intent = new Intent(EditImageActivity.this, StartActivity.class);
startActivity(intent);
finish();
}
#Override
public void onFailure(#NonNull Exception exception) {
hideLoading();
showSnackbar("Failed to save Image");
}
});
}
What will be the alternative for this?
Use getExternalFilesDir(), getExternalCacheDir(), or getExternalMediaDirs() (methods on Context) instead of Environment.getExternalStorageDirectory().
Or, modify mPhotoEditor to be able to work with a Uri, then:
Use ACTION_CREATE_DOCUMENT to get a Uri to a location of the user's choosing, or
Use MediaStore, ContentResolver, and insert() to get a Uri for a particular type of media (e.g., an image) — see this sample app that demonstrates doing this for downloading MP4 videos from a Web site
Also, note that your Uri.fromFile with ACTION_MEDIA_SCANNER_SCAN_FILE should be crashing on Android 7.0+ with a FileUriExposedException. On Android Q, only the MediaStore/insert() option will get your content indexed by the MediaStore quickly.
Note that you can opt out of these "scoped storage" changes on Android 10 and 11, if your targetSdkVersion is below 30, using android:requestLegacyExternalStorage="true" in the <application> element of the manifest. This is not a long-term solution, as your targetSdkVersion will need to be 30 or higher sometime in 2021 if you are distributing your app through the Play Store (and perhaps elsewhere).
Get the destPath with the new API call:
String destPath = mContext.getExternalFilesDir(null).getAbsolutePath();
For Android Q, you can add android:requestLegacyExternalStorage="true" to your element in the manifest. This opts you into the legacy storage model, and your existing external storage code will work.
<manifest ... >
<!-- This attribute is "false" by default on apps targeting
Android 10 or higher. -->
<application android:requestLegacyExternalStorage="true" ... >
...
</application>
</manifest>
Technically, you only need this once you update your targetSdkVersion to 29. Apps with lower targetSdkVersion values default to opting into legacy storage and would need android:requestLegacyExternalStorage="false" to opt out.
Please use getExternalFilesDir(), getExternalCacheDir() instead of Environment.getExternalStorageDirectory() when you creating file in Android 10.
See below line:
val file = File(this.externalCacheDir!!.absolutePath, "/your_file_name")
This is a small example how to get URI for a file if you want to take photo using default camera and store it in a DCIM folder (DCIM/app_name/filename.jpg):
Open camera (remember about CAMERA permission):
private var photoURI: Uri? = null
private fun openCamera() {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
photoURI = getPhotoFileUri()
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
takePictureIntent.resolveActivity(requireActivity().packageManager)?.also {
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
}
}
}
And get URI:
private fun getPhotoFileUri(): Uri {
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val fileName = "IMG_${timeStamp}.jpg"
var uri: Uri? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = requireContext().contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/app_name/")
}
uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
}
return uri ?: getUriForPreQ(fileName)
}
private fun getUriForPreQ(fileName: String): Uri {
val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
val photoFile = File(dir, "/app_name/$fileName")
if (photoFile.parentFile?.exists() == false) photoFile.parentFile?.mkdir()
return FileProvider.getUriForFile(
requireContext(),
"ru.app_name.fileprovider",
photoFile
)
}
Don't forget about WRITE_EXTERNAL_STORAGE permission for pre Q and add a FileProvider to AndroidManifest.xml.
And get a result:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
REQUEST_IMAGE_CAPTURE -> {
photoURI?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val thumbnail: Bitmap =
requireContext().contentResolver.loadThumbnail(
it, Size(640, 480), null
)
} else {
// pre Q actions
}
}
}
}
}
This worked for me
Add this line in application tag of manifest file
android:requestLegacyExternalStorage="true"
Example
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:networkSecurityConfig="#xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="#style/AppTheme">
</application>
Target SDK is 29
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
To get internal storage directory without hard coding,
Permissions (For all Android versions). Don't forget to get permissions from the user.
Request all files access
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
Legacy permission for Android 10 (add this to AndroidManifest.xml > application tag).
android:requestLegacyExternalStorage="true"
Get internal storage directory path:
public static String getInternalStorageDirectoryPath(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
return storageManager.getPrimaryStorageVolume().getDirectory().getAbsolutePath();
} else {
return Environment.getExternalStorageDirectory().getAbsolutePath();
}
}
This worked
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentResolver?.also { resolver ->
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "Image_"+".jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + File.separator+ "TestFolder")
}
val imageUri: Uri? = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
fos = imageUri?.let { resolver.openOutputStream(it) }
bitmap.compress(Bitmap.CompressFormat.JPEG,100,fos)
Objects.requireNonNull(fos)
}
}
You could make use of StorageManager & StorageVolume classes
StorageVolume.getPrimaryStorageVolume(): This volume is the same storage device returned by Environment#getExternalStorageDirectory() and Context#getExternalFilesDir(String).
public String myGetExternalStorageDir() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
return getPrimaryStorageVolumeForAndroid11AndAbove();
else
return getPrimaryStorageVolumeBeforeAndroid11();
}
#TargetApi(Build.VERSION_CODES.R)
private String getPrimaryStorageVolumeForAndroid11AndAbove() {
StorageManager myStorageManager = (StorageManager) ctx.getSystemService(Context.STORAGE_SERVICE);
StorageVolume mySV = myStorageManager.getPrimaryStorageVolume();
return mySV.getDirectory().getPath();
}
private String getPrimaryStorageVolumeBeforeAndroid11() {
String volumeRootPath = "";
StorageManager myStorageManager = (StorageManager) ctx.getSystemService(Context.STORAGE_SERVICE);
StorageVolume mySV = myStorageManager.getPrimaryStorageVolume();
Class<?> storageVolumeClazz = null;
try {
storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
Method getPath = storageVolumeClazz.getMethod("getPath");
volumeRootPath = (String) getPath.invoke(mySV);
} catch (Exception e) {
e.printStackTrace();
}
return volumeRootPath;
}
private fun saveImage(bitmap: Bitmap, name: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = contentResolver
val contentValues = ContentValues()
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name)
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/" + "YOUR_FOLDER")
val imageUri =
resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
val fos = resolver.openOutputStream(imageUri!!)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos!!.flush()
fos.close()
toast("Saved to gallery")
} else {
if (isPermissionGranted(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
val imagesDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DCIM
).toString() + File.separator + "YOUR_FOLDER"
if (!file.exists()) {
file.mkdir()
}
val image = File(imagesDir, "$name.png")
val fos = FileOutputStream(image)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
fos.flush()
fos.close()
} else {
// ask for permission
}
}
}
Recently I also face similar kind of issues, Due to my code was large and i don't want to add new functionality in existing code so i just simply change path..
Environment.getExternalStorageDirectory() Replace with
context.getExternalFilesDir(null).getAbsolutePath()
Code
private void saveImage() {
if (requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
final String folderPath = this.getExternalFilesDir(null).getAbsolutePath() + "/PhotoEditors";
File folder = new File(folderPath);
if (!folder.exists()) {
File wallpaperDirectory = new File(folderPath);
wallpaperDirectory.mkdirs();
}
showLoading("Saving...");
final String filepath=folderPath
+ File.separator + ""
+ System.currentTimeMillis() + ".png";
File file = new File(filepath);
try {
file.createNewFile();
SaveSettings saveSettings = new SaveSettings.Builder()
.setClearViewsEnabled(true)
.setTransparencyEnabled(true)
.build();
if(isStoragePermissionGranted() ) {
mPhotoEditor.saveAsFile(file.getAbsolutePath(), saveSettings, new PhotoEditor.OnSaveListener() {
#Override
public void onSuccess(#NonNull String imagePath) {
hideLoading();
showSnackbar("Image Saved Successfully");
mPhotoEditorView.getSource().setImageURI(Uri.fromFile(new File(imagePath)));
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,Uri.fromFile(new File(filepath))));
Intent intent = new Intent(EditImageActivity.this, StartActivity.class);
startActivity(intent);
finish();
}
#Override
public void onFailure(#NonNull Exception exception) {
hideLoading();
showSnackbar("Failed to save Image");
}
});
}
var tempDir: File = inContext.getExternalFilesDir("/")!!
tempDir = File(tempDir.getAbsolutePath().toString() + "/.IncidentImages/")
tempDir.mkdir()
If you work with XAMARIN and are confused by all this different answers (like me) just follow this example:
var picture = new Java.IO.File(Environment.DirectoryPictures, "fileName");
Took me some time to figure this out.
Related
I have a recyclerview that displays multiple images from Firebase Realtime Database. The recyclerview also has a button within it. I want this button to allow users to be able to download these images ONE AT A TIME once they click it.
Once users click "download" I want the images to be saved to their device storage. I've tried multiple solutions for this, but they weren't helpful as they were for either Firestore Database or only allowed for one image to be downloaded.
Code
class AbstractAdapter(private val mContext: Context, private val abstractList: List<Abstract>) :
RecyclerView.Adapter<AbstractAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.abstract_image_view, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.download_btn.setOnClickListener {
downloadFile()
}
Glide.with(mContext)
.load(abstractList[position].abstract)
.into(holder.imageView)
}
override fun getItemCount(): Int {
return abstractList.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val imageView: ImageView = itemView.findViewById(R.id.abstractImageView)
val download_btn: Button = itemView.findViewById(R.id.abstractDownloadBtn)
}
private fun downloadFile() {
val storage = FirebaseStorage.getInstance()
val storageRef = storage.getReferenceFromUrl("https://notes-72413.firebaseio.com/")
val islandRef = storageRef.child("Abstract")
val rootPath = File(Environment.getExternalStorageDirectory(), "abstract")
if (!rootPath.exists()) {
rootPath.mkdirs()
}
val localFile = File(rootPath, "imageName.jpeg")
islandRef.getFile(localFile)
.addOnSuccessListener(OnSuccessListener<FileDownloadTask.TaskSnapshot?> {
Log.e("firebase ", ";local tem file created created $localFile")
// updateDb(timestamp,localFile.toString(),position);
}).addOnFailureListener(OnFailureListener { exception ->
Log.e(
"firebase ",
";local tem file not created created $exception"
)
})
}
companion object {
private const val Tag = "RecyclerView"
}
I've tried this code, but once I click the "download" button it immediately crashes and Logcat says Firebase Storage URLs must point to an object in your Storage Bucket. Please obtain a URL using the Firebase Console or getDownloadUrl()
My Firebase Realtime Database
There's 64 files in total
Summary
I have a recyclerview that displays images from Firebase Realtime Database. Once users click the "download" button, it only downloads one image at a time to their device storage.
Update
private fun downloadFile() {
val storage = FirebaseStorage.getInstance()
val storageRef = storage.getReferenceFromUrl("abstract")
val rootPath = File(Environment.getExternalStorageDirectory(), "abstract")
if (!rootPath.exists()) {
rootPath.mkdirs()
}
val localFile = File(rootPath, "imageName.jpeg")
storageRef.child("Abstract").downloadUrl.addOnSuccessListener { Log.e("firebase ", ";local tem file created created $localFile")
}.addOnFailureListener(OnFailureListener { exception ->
Log.e("firebase ", ";local tem file not created created $exception")
})
}
These are the changes I made to my downloadFile function, but I still get an error:
The storage Uri could not be parsed
Second update
2022-06-11 21:36:00.536 29751-29751/com.khumomashapa.notes E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.khumomashapa.notes, PID: 29751
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:523)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
Caused by: java.net.MalformedURLException: no protocol:
at java.net.URL.<init>(URL.java:601)
at java.net.URL.<init>(URL.java:498)
at java.net.URL.<init>(URL.java:447)
at com.khumomashapa.notes.adapter.AbstractAdapter.downloadFile(AbstractAdapter.kt:57)
at com.khumomashapa.notes.adapter.AbstractAdapter.onBindViewHolder$lambda-0(AbstractAdapter.kt:35)
at com.khumomashapa.notes.adapter.AbstractAdapter.$r8$lambda$Rrmx0DFlwJu1z6QtjG8WCQp6NQQ(Unknown Source:0)
at com.khumomashapa.notes.adapter.AbstractAdapter$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
at android.view.View.performClick(View.java:7216)
at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1217)
at android.view.View.performClickInternal(View.java:7190)
at android.view.View.access$3500(View.java:827)
at android.view.View$PerformClick.run(View.java:27663)
at android.os.Handler.handleCallback(Handler.java:900)
at android.os.Handler.dispatchMessage(Handler.java:103)
at android.os.Looper.loop(Looper.java:219)
at android.app.ActivityThread.main(ActivityThread.java:8349)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
Code
class AbstractAdapter(private val mContext: Context, private val abstractList: List<Abstract>) :
RecyclerView.Adapter<AbstractAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.abstract_image_view, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.download_btn.setOnClickListener {
downloadFile(url = String(), file = String.toString())
}
Glide.with(mContext)
.load(abstractList[position].abstract)
.into(holder.imageView)
}
override fun getItemCount(): Int {
return abstractList.size
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val imageView: ImageView = itemView.findViewById(R.id.abstractImageView)
val download_btn: Button = itemView.findViewById(R.id.abstractDownloadBtn)
}
#Throws(IOException::class)
private fun downloadFile(url: String, file: String) {
val urlObj = URL(url)
val fileObj = File(file)
val conn = urlObj.openConnection()
val buffer = ByteArray(1024)
object : BufferedOutputStream(FileOutputStream(fileObj)) {
var `in` = BufferedInputStream(conn.getInputStream())
init {
var read: Int
while (`in`.read(buffer, 0, buffer.size) >= 0.also { read = it });
run {
out.write(buffer, 0, read)
}
out.flush()
}
}.use { out -> }
}
I found the perfect solution to my problem. All I had to do was create an OnItemClick interface to get a different result for each item click and use Download manager to download the images.
override fun onItemClick(item: String, pos:Int) {
abstractData = item
positionItem = pos
if (checkSelfPermission(requireActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED ){
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQ_CODE)
}else{
startDownloading()
}
Toast.makeText(requireActivity(), "Saved to Internal storage/Pictures/AbstractWallpaper", Toast.LENGTH_LONG).show()
}
private fun startDownloading() {
val request = DownloadManager.Request(Uri.parse(abstractData))
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
request.setTitle("Abstract Wallpaper")
request.setDescription("Your image is downloading")
request.allowScanningByMediaScanner()
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, "AbstractWallpapers")
val manager = activity?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
manager.enqueue(request)
Toast.makeText(requireActivity(), "Download is starting...", Toast.LENGTH_LONG).show()
}
As you are downloading based on your storage URLs so need to use getDownloadUrl() method.
storageRef.child("Abstract").getDownloadUrl().addOnSuccessListener(new OnSuccessListener<Uri>() {
#Override
public void onSuccess(Uri uri) {
...Uri
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception exception) {
// To handle error
}
});
As those URLs in your database are publicly visible HTTP URLs. You can just download the files with a good old URLConnection and binary streams.
private void downloadFile(String url, String file) throws IOException {
URL urlObj = new URL(url);
File fileObj = new File(file);
URLConnection conn = urlObj.openConnection();
byte[] buffer = new byte[1024];
try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(fileObj)) {
try (BufferedInputStream in = new BufferedInputStream(conn.getInputStream())) {
int read;
while ((read = in.read(buffer, 0, buffer.length) >= 0) {
out.write(buffer, 0, read);
}
out.flush();
}
}
}
Not tested, but for idea,
I'm using okhttp and okio
fun saveImageToPicture(context: Countext, url: String, imageName: String, imageMimeType: String) {
val appImagePath = File(Environment.DIRECTORY_PICTURES, "FolderNameInPictures")
val date = System.currentTimeMillis()
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$imageName.$imageMimeType")
put(MediaStore.Images.Media.MIME_TYPE, "image/$imageMimeType")
put(MediaStore.Images.Media.DATE_ADDED, date)
put(MediaStore.Images.Media.DATE_MODIFIED, date)
}
var collection = if (Build.VERSION.SDK_INT >= 29 {
values.put(MediaStore.Images.Media.RELATIVE_PATH, "$appImagePath${File.separator}")
values.put(MediaStore.Images.Media.IS_PENDING, 1)
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
val resolver = context.contentResolver
val insertedUri = resolver.insert(collection, values)
val outputStream = resolver.openOutputStream(insertedUri!!, "w")
downloadFromUrlToOutputStream(url, outputStream)
values.clear()
if (Build.VERSION.SDK_INT >= 29 {
values.put(MediaStore.Images.Media.IS_PENDING, 0)
}
resolver.update(insertedUri, values, null, null)
}
fun downloadFromUrlToOutputStream(url: String, output: OutputStream) {
val request = Request.Builder().url(url).build()
val response = OkHttpClient().newCall(request).execute();
val body = response.body()
val bufferSource = body.source()
val sink = Okio.buffer(Okio.sink(output))
val sinkBuffer = sink.buffer()
val bufferSize = 1024 * 8
while (bufferSource.read(sinkBuffer, bufferSize) != -1) {
sink.emit()
}
sink.flush()
sink.close()
bufferSource.close()
}
I need to save a file in the shared storage of android. I came across this link => https://developer.android.com/training/data-storage/shared/documents-files
I am using the dependency service and I am able to successfully save a file to desired location. But I am able to create only a blank file. I need to write some content into that file. Actually I created a thread few hours ago with android tag and got the solution where I had to override the OnActivityResult method and get the intent data. Now I have done it and I am able to get the intent data. But Now I dont know which path should i choose from the intent and how to open the file using the chosen path and how to write content into that chosen file. Any android + xamarin expert should be able to help me..
The android platform code to implement the write service:
Activity _activity;
private static int CREATE_FILE = 6835;
public WriteFileService()
{
_activity = CrossCurrentActivity.Current.Activity;
}
void IWriteService.WriteFile(string Content)
{
Intent intent = new Intent(Intent.ActionCreateDocument);
intent.AddCategory(Intent.CategoryOpenable);
intent.SetType("application/txt");
intent.PutExtra(Intent.ExtraTitle, "Invoice.txt");
_activity.StartActivityForResult(intent, CREATE_FILE);
}
The overriden OnActivityResultMethod:
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
Toast.MakeText(Application.Context, "requestCode is " + requestCode, ToastLength.Short).Show();
if (requestCode == 6835)
{
if (data != null)
{
Toast.MakeText(Application.Context,
data.GetType().ToString(),
ToastLength.Short).Show();
}
}
base.OnActivityResult(requestCode, resultCode, data);
}
This is the screen of the Intent Data from OnActivityResult
Use this in your Android project to save a stream to file:
public async Task SaveAndView(string fileName, String contentType, MemoryStream stream)
{
try
{
string root = null;
//Get the root path in android device.
if (Android.OS.Environment.IsExternalStorageEmulated)
{
root = Android.OS.Environment.ExternalStorageDirectory.ToString();
}
else
root = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
//Create directory and file
Java.IO.File myDir = new Java.IO.File(root + "/meusarquivos");
myDir.Mkdir();
Java.IO.File file = new Java.IO.File(myDir, fileName);
//Remove if the file exists
if (file.Exists()) file.Delete();
//Write the stream into the file
FileOutputStream outs = new FileOutputStream(file);
outs.Write(stream.ToArray());
outs.Flush();
outs.Close();
}
catch (Exception ex)
{
PostLog.AppCenterLogExcecao(ex, new Dictionary<string, string> { { "origem", "OrderViewModel - 159" } });
}
}
And in your shared code:
await DependencyService.Get<ISave>().SaveAndView(OrderId.ToString() + ".pdf", "application/pdf", stream);
Be sure to ask for the permissions before using the code.
To save the file. ive used this code
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
if (requestCode == 6835)
{
if (data != null)
{
Android.Net.Uri uri = Android.Net.Uri.Parse(data.DataString);
var stream = ContentResolver.OpenOutputStream(uri, "w");
byte[] byteArray = Encoding.UTF8.GetBytes("Hi Thameem. I am your file.");
MemoryStream stream1 = new MemoryStream(byteArray);
stream1.CopyTo(stream);
stream.Flush();
stream.Close();
Toast.MakeText(Application.Context,
"The file has been exported successfully",
ToastLength.Short).Show();
}
}
base.OnActivityResult(requestCode, resultCode, data);
}
I have created a library project for writing logs into ApplicationInsights as well as table storage and is being consumed my different other WebAPI projects. But due to some reason the logs are not getting logged in Application Insights but it works with table storage.
private void AddTelemetryTarget(string instrumentationKey, LoggerEnumerations.LogLevel minLogLevel, LoggingConfiguration config)
{
try
{ ConfigurationItemFactory.Default.Targets.RegisterDefinition("ApplicationInsightsTarget", typeof(ApplicationInsightsTarget));
ApplicationInsightsTarget aiTarget = new ApplicationInsightsTarget();
aiTarget.InstrumentationKey = instrumentationKey;
aiTarget.Name = "ai";
var wrapper = new AsyncTargetWrapper(aiTarget, 5000, AsyncTargetWrapperOverflowAction.Grow);
config.AddTarget("TelemetryAsyncWrapper", wrapper);
//Applying logging rules.
LoggingRule rule = new LoggingRule("*", ConvertLogType(minLogLevel), aiTarget);
config.LoggingRules.Add(rule);
}
catch
{ }
}
private LogLevel ConvertLogType(LoggerEnumerations.LogLevel type)
{
switch (type)
{
case LoggerEnumerations.LogLevel.Error: return LogLevel.Error;
case LoggerEnumerations.LogLevel.Info: return LogLevel.Info;
case LoggerEnumerations.LogLevel.Warn: return LogLevel.Warn;
default: return LogLevel.Trace;
}
}
public async Task Log(string message, LoggerEnumerations.LogLevel type, Dictionary<string, string> customParams, Exception ex = null, bool isPayload = false)
{
LogEventInfo eventInfo = PopulateEventInfo(message, type, customParams, ex);
if (!isPayload)
{
_logger.Log(eventInfo);
}
else
{
_payloadLogger.Log(eventInfo);
}
}
private LogEventInfo PopulateEventInfo(string message, LoggerEnumerations.LogLevel type, Dictionary<string, string> customParams, Exception ex = null)
{
LogEventInfo eventInfo = new LogEventInfo();
eventInfo.Level = ConvertLogType(type);
eventInfo.Message = message;
eventInfo.LoggerName = this.GetType().ToString();
if (ex != null)
{
eventInfo.Exception = ex;
}
else if (eventInfo.Level == LogLevel.Error)
{
eventInfo.Exception = new Exception(message);
}
//Adding custom properties to LogEventInfo to display in Application insight
if (customParams != null)
{
foreach (KeyValuePair<string, string> param in customParams)
{
eventInfo.Properties.Add(param.Key, param.Value);
}
}
return eventInfo;
}
Version of Nuget packages are
Microsoft.ApplicationInsights.NLogTarget : 2.13.1
NLog : 4.6.8
Thanks
I added Application Insights as Connected Services and I removed the Instrumentation Key from ApplicationInsights.config file and when registering the nlog target I used instrumentation key from my web.config file and it started working.
I would like to save a file in a user selected folder, thats why I would like to provide a directory list to user and user will be able to choose the directory where he wants to export the data. Unfortuntely I could not find any example for directory/folder picker, I just found a file picker which is not useful for me..
https://github.com/jfversluis/FilePicker-Plugin-for-Xamarin-and-Windows
Is there any component for picking a folder for Xamarin.Forms? Actually I am just doing for Android but we use Xamarin.forms
There is none I can think of.
With netstandard everything is way more simple as you can use the classic c# File api to get the folders.
You just have to know the mappings between special folders and android folders (per example):
System.Environment.SpecialFolder Path
ApplicationData INTERNAL_STORAGE/.config
Desktop INTERNAL_STORAGE/Desktop
LocalApplicationData INTERNAL_STORAGE/.local/share
MyDocuments INTERNAL_STORAGE
MyMusic INTERNAL_STORAGE/Music
MyPictures INTERNAL_STORAGE/Pictures
MyVideos INTERNAL_STORAGE/Videos
Personal INTERNAL_STORAGE
source: https://learn.microsoft.com/en-US/xamarin/android/platform/files/
same for ios:
https://learn.microsoft.com/en-US/xamarin/ios/app-fundamentals/file-system
But it's really easy to implement, just enumerate all folders and display them in a ListView.
EDIT: more details on implementation.
In fact you want to code a "directory explorer", it's easy, here is the concept.
You have a ListView in your Page
You have a Cancel button and a Select button in your Page
You have a CurrentPath in your ViewModel
You bind CurrentPath to the Title of your Page
You have an List<DirectoryViewModel> Directories in your ViewModel
Each time a user click on a item from the list:
You add the directory name in your current path
You get all the directories from the new path, and update your Directories property (don't forget RaisePropertyChange(nameof(Directories)))
The ListView will be updated accordingly
Each time you back:
You remove last part of your current path
same as before
If you arrive to root path "/", you do nothing when clicking on back.
Oh and you could use this Grid component to instead of the ListView, will be nicer ;)
https://github.com/roubachof/Sharpnado.Presentation.Forms#grid-Layout
You can edit this to make it work..
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Java.IO;
using Java.Util;
namespace Android.Basic.IO
{
public class DirectoryPicker : ListActivity
{
public const String START_DIR = "startDir";
public const String ONLY_DIRS = "onlyDirs";
public const String SHOW_HIDDEN = "showHidden";
public const String CHOSEN_DIRECTORY = "chosenDir";
public const int PICK_DIRECTORY = 43522;
private File dir;
private Boolean showHidden = false;
private bool onlyDirs = true;
public override void OnCreate(Bundle savedInstanceState, PersistableBundle persistentState)
{
base.OnCreate(savedInstanceState, persistentState);
Bundle extras = Intent.Extras;
dir = OS.Environment.ExternalStorageDirectory;
if (extras != null)
{
String preferredStartDir = extras.GetString(START_DIR);
showHidden = extras.GetBoolean(SHOW_HIDDEN, false);
onlyDirs = extras.GetBoolean(ONLY_DIRS, true);
if (preferredStartDir != null)
{
File startDir = new File(preferredStartDir);
if (startDir.IsDirectory)
{
dir = startDir;
}
}
}
SetContentView(Resource.Layout.folder_chooser_activity);
var title = dir.AbsolutePath.ToString();
Title = (title);
Button btnChoose = (Button)FindViewById(Resource.Id.btnChoose);
String name = dir.Name;
if (name.Length == 0)
name = "/";
btnChoose.Text = ("Choose " + "'" + name + "'");
btnChoose.Click += delegate
{
returnDir(dir.AbsolutePath);
};
ListView lv = this.ListView;
lv.TextFilterEnabled = (true);
if (!dir.CanRead())
{
Context context = ApplicationContext;
String msg = "Could not read folder contents.";
Toast.MakeText(context, msg, ToastLength.Long).Show();
return;
}
var files = filter(dir.ListFiles(), onlyDirs, showHidden);
String[] names = Names(files);
ListAdapter = (new ArrayAdapter<String>(this, Resource.Layout.folder_chooser_item, names));
lv.ItemClick += (ff, gg) =>
{
var position = gg.Position;
if (!files[gg.Position].IsDirectory)
return;
String path = files[position].AbsolutePath;
var intent = new Intent(this, typeof(DirectoryPicker));
intent.PutExtra(DirectoryPicker.START_DIR, path);
intent.PutExtra(DirectoryPicker.SHOW_HIDDEN, showHidden);
intent.PutExtra(DirectoryPicker.ONLY_DIRS, onlyDirs);
StartActivityForResult(intent, PICK_DIRECTORY);
};
}
protected void OnActivityResult(int requestCode, int resultCode, Intent data)
{
if (requestCode == PICK_DIRECTORY && resultCode == (int)Result.Ok)
{
Bundle extras = data.Extras;
String path = (String)extras.Get(DirectoryPicker.CHOSEN_DIRECTORY);
returnDir(path);
}
}
private void returnDir(String path)
{
Intent result = new Intent();
result.PutExtra(CHOSEN_DIRECTORY, path);
SetResult(Result.Ok, result);
Finish();
}
public List<File> filter(File[] file_list, bool onlyDirs, bool showHidden)
{
var files = new List<File>();
foreach (var file in file_list)
{
if (onlyDirs && !file.IsDirectory)
continue;
if (!showHidden && file.IsHidden)
continue;
files.Add(file);
}
Collections.Sort(files);
return files;
}
public String[] Names(List<File> files)
{
String[] names = new String[files.Count];
int i = 0;
foreach (var file in files)
{
names[i] = file.Name;
i++;
}
return names;
}
}
}
Start activity as result then catch in OnActivityResult
if (requestCode == DirectoryPicker.PICK_DIRECTORY && resultCode == Result.Ok)
{
Bundle extras = data.Extras;
String path = (String)extras.Get(DirectoryPicker.CHOSEN_DIRECTORY);
// do stuff with path
}
File newSoundFile = new File("pathFile");
if (DebugUtil.DEBUG) {
DebugUtil.logError(new Exception(), newSoundFile.getAbsolutePath());
}
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DATA, newSoundFile.getAbsolutePath());
values.put(MediaStore.MediaColumns.TITLE, "Title");
values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/*");
values.put(MediaStore.MediaColumns.SIZE, newSoundFile.length());
values.put(MediaStore.Audio.Media.ARTIST, R.string.app_name);
values.put(MediaStore.Audio.Media.DURATION, 230);
values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
values.put(MediaStore.Audio.Media.IS_NOTIFICATION, false);
values.put(MediaStore.Audio.Media.IS_ALARM, false);
values.put(MediaStore.Audio.Media.IS_MUSIC, false);
Uri uri = MediaStore.Audio.Media.getContentUriForPath(newSoundFile.getAbsolutePath());
context.getContentResolver().delete(uri, MediaStore.MediaColumns.DATA + "=\"" + newSoundFile.getAbsolutePath() + "\"", null);
Uri newUri = context.getContentResolver().insert(uri, values);
try {
RingtoneManager.setActualDefaultRingtoneUri(context, RingtoneManager.TYPE_RINGTONE, newUri);
KitKatToast.makeText(context, R.string.msg_setAsRingTuneSuccess, KitKatToast.LENGTH_LONG).show();
}
catch (Exception e) {
if (DebugUtil.DEBUG) {
DebugUtil.logError(new Exception(), e.toString());
}
}
I saw many posts but anyone showed what i should actually have to do. So i decided to create this complete answer. The only think you actually need is a button.
Lets start.
Here is my MainActivity.java which i used
public class MainActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
b2 = (Button) findViewById(R.id.button2);
b2.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View arg0) {
Intent intent1 = new Intent();
intent1.setAction(Intent.ACTION_GET_CONTENT);
intent1.setType("audio/*");
startActivityForResult(Intent.createChooser(intent1, "Choose Sound File"), 6);
}
});
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK && requestCode == 6) {
Uri i = data.getData(); //getDATA
String s = = i.getPath(); //getPath
File k = new File(s); //set File from path
if (s != null) { //(file.exists
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DATA, k.getAbsolutePath());
values.put(MediaStore.MediaColumns.TITLE, "ring");
values.put(MediaStore.MediaColumns.MIME_TYPE, "audio/mp3");
values.put(MediaStore.MediaColumns.SIZE, k.length());
values.put(MediaStore.Audio.Media.ARTIST, R.string.app_name);
values.put(MediaStore.Audio.Media.IS_RINGTONE, true);
values.put(MediaStore.Audio.Media.IS_NOTIFICATION, true);
values.put(MediaStore.Audio.Media.IS_ALARM, true);
values.put(MediaStore.Audio.Media.IS_MUSIC, false);
Uri uri = MediaStore.Audio.Media.getContentUriForPath(k.getAbsolutePath());
getContentResolver().delete(uri, MediaStore.MediaColumns.DATA + "=\"" + k.getAbsolutePath() + "\"", null);
Uri newUri = getContentResolver().insert(uri, values);
try {
RingtoneManager.setActualDefaultRingtoneUri(MainActivity.this, RingtoneManager.TYPE_RINGTONE, newUri);
} catch (Throwable t) {
}
}
}
}
}
Lastly its really important to add those permisions in your AndroidManifest.xml for example if you dont add the permision to write external storage your app will crash like mine.. xD
What you need:
<uses-permission android:name="android.permission.WRITE_SETTINGS" ></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" ></uses-permission>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" ></uses-permission>
You can try my app on Google Play : BackAtel Audio Manager
Hope that helps.... my problem is now solved!! i hope that i solved your problem too :))