I am building an Android app where I want to export some values to a CSV.
I have to problem: either I get an error saying that the permission is denied or I get no errors but no CSV is created.
FileWriter writer;
File root = Environment.getExternalStorageDirectory();
File csvFile = new File(root, "xxx.csv");
writer = new FileWriter(csvFile);
String headers = String.format("%s,%s\n", "Timestamp",
"Value");
writer.write(headers);
writer.flush();
writer.close();
But i get nothing...
From Android 10 and beyond, Android enforces Storage Scope Model. You can either write your CSV file in App-Specific Location which is private to your App and doesn't require any user permission, but no one can access this location. Here is the code snippet to write the file into the App-Specific directory:
public void writeIntoFile(Context context, String fileName, String content) {
// File appSpecificExternalStorageDirectory = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
File appSpecificInternalStorageDirectory = context.getFilesDir();
File file = new File(appSpecificInternalStorageDirectory, fileName);
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file, false);
fos.write(content.getBytes());
fos.close();
}
If you want to share this file with the User, then you should write this file in Shared Storage. To write a file in Shared Storage, this has to be done in 3 steps:-
Step 1: Launch System Picker to choose the destination by the user. This will return Uri of the destination directory.
private ActivityResultLauncher<Intent> launcher; // Initialise this object in Activity.onCreate()
private Uri baseDocumentTreeUri;
public void launchBaseDirectoryPicker() {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
launcher.launch(intent);
}
Step 2: Launch System Picker to choose the destination by the user. This will return the Uri of the destination directory. Also, you can optionally persist the permissions and Uri for future use.
#Override
public void onActivityResult(ActivityResult result) {
if (result.getResultCode() == Activity.RESULT_OK) {
baseDocumentTreeUri = Objects.requireNonNull(result.getData()).getData();
final int takeFlags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// take persistable Uri Permission for future use
context.getContentResolver().takePersistableUriPermission(result.getData().getData(), takeFlags);
SharedPreferences preferences = context.getSharedPreferences("com.example.fileutility", Context.MODE_PRIVATE);
preferences.edit().putString("filestorageuri", result.getData().getData().toString()).apply();
} else {
Log.e("FileUtility", "Some Error Occurred : " + result);
}
}
Step 3: Write CSV content into a file.
public void writeFile(String fileName, String content) {
try {
DocumentFile directory = DocumentFile.fromTreeUri(context, baseDocumentTreeUri);
DocumentFile file = directory.createFile("text/*", fileName);
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(file.getUri(), "w");
FileOutputStream fos = new FileOutputStream(pfd.getFileDescriptor());
fos.write(content.getBytes());
fos.close();
} catch (IOException e) {
}
}
For more explanation, you can read "How to Save a file in Shared Storage in Android 10 or Higher" or Android official documentation.
Related
So my folder structure is Documents/XML/xml.xml
I create the XML folder upon starting the app. The user then has to put there his xml file manually.
In my manifest I have READ and WRITE EXTERNAL STORAGE permissions. I can check if the .xml file exists and it returns true if there is any, but when I try to read and print it: open failed: EACCES (Permission denied).
How can I bypass this without using MANAGE_EXTERNAL_STORAGE in Android 11?
Thanks, if anyone is wondering, here is how I implemented it:
private void openFile(Uri pickerInitialUri) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/xml");
// Optionally, specify a URI for the file that should appear in the
// system file picker when it loads.
intent.putExtra("android.provider.extra.EXTRA_INITIAL_URI", pickerInitialUri);
startActivityForResult(intent, 200);
}
#Override
public void onActivityResult(int requestCode, int resultCode,
Intent resultData) {
if (requestCode == 200
&& resultCode == Activity.RESULT_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.
try {
System.out.println(readXMLFromUri(uri));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private String readXMLFromUri(Uri uri) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
try (InputStream inputStream =
getContext().getContentResolver().openInputStream(uri);
BufferedReader reader = new BufferedReader(
new InputStreamReader(Objects.requireNonNull(inputStream)))) {
String line;
while ((line = reader.readLine()) != null) {
stringBuilder.append(line);
}
}
return stringBuilder.toString();
}
The old external storage permission have been progressively revoked with every new API release, starting with Android 10. You'll need to use the new shared storage API to access data on external storage. Follow the instructions here
Description: I'm trying to log the data I have coming from firestore into a csv file, and I have the following methods to do it.
public interface ExportPojo {
String generateExportValues();
String generateExportHeaders();}
public static File generateCSV(Context context, Collection<? extends ExportPojo> values, Class<? extends ExportPojo> type) {
StringBuilder csv = new StringBuilder();
String header;
try {
header = type.newInstance().generateExportHeaders();
} catch (Exception e) {
e.printStackTrace();
return null;
}
csv.append(header).append("\n");
for (ExportPojo entry : values) {
csv.append(entry.generateExportValues());
csv.append("\n");
}
return writeStringToFile(context, csv.toString(), ".csv");
}
public static File writeStringToFile(Context context, String data, String format) {
File dir = new File(context.getFilesDir(), "/manage/");
// create this directory if not already created
dir.mkdir();
// create the file in which we will write the contents
String timestamp =
new SimpleDateFormat("E MMM yyyy H-m-s", Locale.getDefault()).format(new Date());
final File file = new File(dir, timestamp + format);
try {
FileOutputStream os = new FileOutputStream(file);
os.write(data.getBytes());
os.close();
return file;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Problem: The client would like the user to navigate the file directory, find the csv file, and open it. But after running the methods, I can't find the exported file. I've logged csv.tostring() and it looks like the data is okay. What am I doing wrong?
context.getFilesDir() give you the path for files directory that lies inside the app folder present in /data of Android which a normal user cannot access. To make the file accessible to user, you need to save it in a public directory.
You can do it like this: (the code is in kotlin but you can easily convert it in JAVA)
val path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
val dir = File(path.absolutePath)
if(!dir.exists())
dir.mkdirs()
val file = File("$path/Text.txt")
if (!file.exists()) {
file.createNewFile();
}
var content = "" //todo: write your csv content here
val fw = FileWriter(file.absoluteFile)
val bw = BufferedWriter(fw)
bw.write(content)
bw.close()
file.renameTo(File("$path/${name}.csv"))
File("$path/Text.txt").delete()
You can also try writing the content directly to .csv instead of .txt but it failed for me.
I have written codes for adding sticker pack dynamically. But I want to know how to update the sticker pack after adding to whatsapp?
I can add sticker file to the pack which is listed on my app only but it is not reflecting in whatsapp. I tried adding files to the same location (file:///...) from where the pack was sent to whatsapp.
I want to try updating content provider. But how to do that? Can I add files to whatsapp's 'content://...' uri or should I update my app's content provider or anything else?
I am using react-native-whatsapp-stickers module for react-native.
react-native code
invoking after adding single sticker from UI
const addOne = (path, packName) =>{
// log('path ',path[0])
// log('packName ',packName)
RNWhatsAppStickers.addSticker(path[0],packName)
.then(res=>RNWhatsAppStickers.send(packName,packName))
.then(res=>console.log('response ',res))
}
Java code of module RNWhatsAppStickers
#ReactMethod
public void send(String identifier, String stickerPackName, Promise promise) {
Intent intent = new Intent();
intent.setAction("com.whatsapp.intent.action.ENABLE_STICKER_PACK");
intent.putExtra(EXTRA_STICKER_PACK_ID, identifier);
intent.putExtra(EXTRA_STICKER_PACK_AUTHORITY, getContentProviderAuthority(reactContext));
intent.putExtra(EXTRA_STICKER_PACK_NAME, stickerPackName);
try {
Activity activity = getCurrentActivity();
ResolveInfo should = activity.getPackageManager().resolveActivity(intent, 0);
if (should != null) {
activity.startActivityForResult(intent, ADD_PACK);
promise.resolve("OK");
} else {
promise.resolve("OK, but not opened");
}
} catch (ActivityNotFoundException e) {
promise.reject(ERROR_ADDING_STICKER_PACK, e);
} catch (Exception e) {
promise.reject(ERROR_ADDING_STICKER_PACK, e);
}
}
// saving image to same pack
public static void SaveImage(Bitmap finalBitmap, String name, String identifier) {
String root = path + "/" + identifier;
File myDir = new File(root);
myDir.mkdirs();
String fname = name;
File file = new File(myDir, fname);
if (file.exists()){
// Log.d("ReactNative","root "+root);
file.delete();
}
try {
FileOutputStream out = new FileOutputStream(file);
finalBitmap.compress(Bitmap.CompressFormat.WEBP, 90, out);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
Please give any idea what to do? Thanks
I found the solution on official doc. You have to increase the image_data_Version for that pack after adding new images.
To track the image_data_Version you can query the ContentProvider of your app or Store the image_data_Version somewhere and make changes accordingly .
The application KDE Connect allows remotely browsing an Android device from a desktop computer through SFTP. Since Android 4.4, developers don't have write permission to SD cards directly through the filesystem anymore. So I am trying to port the SFTP module using the Storage Access Framework (DocumentFile, etc.)
I am taking the permission with an Intent.ACTION_OPEN_DOCUMENT_TREE and FLAG_GRANT_WRITE_URI_PERMISSION and passing the context to my classes.
I am able to create new empty files, rename files and delete files on the SD card inside my class so I believe I am getting the necessary permissions. However, transferring a file results in an empty file (0 bytes) being created. I can see the transfer taking a certain time and a progress bar on the desktop side, so it doesn't just abort.
Here is the relevant part of the SftpSubsystem class from the Apache SSHD library (see doc here) with my own comments to explain what's going on:
public class SftpSubsystem implements Command, Runnable, SessionAware, FileSystemAware {
// This method receives a buffer from an InputStream and processes it
// according to its type. In this situation, it would also contain
// a block of the file being transferred (4096 bytes)
protected void process(Buffer buffer) {
int type = buffer.getByte();
switch (type) {
case WRITE:
FileHandle fh = getHandleFromString(buffer.getString());
long offset = buffer.getLong();
byte[] data = buffer.getBytes();
fh.write(data, offset);
break;
// other cases
}
}
// This class is a handle to a file (duh) with
// an OutputStream to write and InputStream to read
protected static class FileHandle {
SshFile file;
OutputStream output;
long outputPos;
InputStream input;
long inputPos;
// Method called inside process()
public void write(byte[] data, long offset) throws IOException {
if (output != null && offset != outputPos) {
IoUtils.closeQuietly(output);
output = null;
}
if (output == null) {
// This is called once at the start of the transfer.
// This is what I think I need to rewrite to make
// it work with DocumentFile objects.
output = file.createOutputStream(offset);
}
output.write(data);
outputPos += data.length;
}
}
}
The original implementation of createOutputStream() that I want to rewrite because RandomAccessFile doesn't work with DocumentFile:
public class NativeSshFile implements SshFile {
private File file;
public OutputStream createOutputStream(final long offset)
throws IOException {
// permission check
if (!isWritable()) {
throw new IOException("No write permission : " + file.getName());
}
// move to the appropriate offset and create output stream
final RandomAccessFile raf = new RandomAccessFile(file, "rw");
try {
raf.setLength(offset);
raf.seek(offset);
// The IBM jre needs to have both the stream and the random access file
// objects closed to actually close the file
return new FileOutputStream(raf.getFD()) {
public void close() throws IOException {
super.close();
raf.close();
}
};
} catch (IOException e) {
raf.close();
throw e;
}
}
}
One of the ways I tried to implement it:
class SimpleSftpServer {
static class AndroidSshFile extends NativeSshFile {
// This is the DocumentFile that is stored after
// create() created the empty file
private DocumentFile docFile;
public OutputStream createOutputStream(final long offset) throws IOException {
// permission check
if (!isWritable()) {
throw new IOException("No write permission : " + docFile.getName());
}
ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(docFile.getUri(), "rw");
FileDescriptor fd = pfd.getFileDescriptor();
try {
android.system.Os.lseek(fd, offset, OsConstants.SEEK_SET);
} catch (ErrnoException e) {
Log.e("SimpleSftpServer", "" + e);
return null;
}
return new FileOutputstream(fd, offset);
}
}
}
I also tried a simple (the offset is ignored but it's just a test):
public OutputStream createOutputStream(final long offset) throws IOException {
// permission check
if (!isWritable()) {
throw new IOException("No write permission : " + docFile.getName());
}
return context.getContentResolver().openOutputStream(docFile.getUri());
}
I also tried with a FileChannel and to flush and sync the FileOutputStream.
Any idea why I end up with an empty file?
EDIT: here is a small example of a test I did to just write a new file from an existing file. It works, but this is not what I actually want to do (see code above) but I thought I'd provide an example to show that I understand the basics of how to write to an OutputStream.
private void createDocumentFileFromFile() {
File fileToRead = new File("/storage/0123-4567/lady.m4a");
File fileToWrite = new File("/storage/0123-4567/lady2.m4a");
File dir = fileToWrite.getParentFile();
DocumentFile docDir = DocumentFile.fromTreeUri(context, SimpleSftpServer.externalStorageUri);
try {
DocumentFile createdFile = docDir.createFile(null, fileToWrite.getName());
Uri uriToRead = Uri.fromFile(fileToRead);
InputStream in = context.getContentResolver().openInputStream(uriToRead);
OutputStream out = context.getContentResolver().openOutputStream(createdFile.getUri());
try {
int nbOfBytes = 0;
final int BLOCKSIZE = 4096;
byte[] bytesRead = new byte[BLOCKSIZE];
while (true) {
nbOfBytes = in.read(bytesRead);
if (nbOfBytes == -1) {
break;
}
out.write(bytesRead, 0, nbOfBytes);
}
} finally {
in.close();
out.close();
}
} catch (IOException e) {
}
}
"When using ACTION_OPEN_DOCUMENT_TREE, your app gains access only to the files in the directory that the user selects. You don't have access to other apps' files that reside outside this user-selected directory.
This user-controlled access allows users to choose exactly what content they're comfortable sharing with your app."
This means, you can only read/write/delete the content/meta data of already existing files or in sub directories in the selected directory, the scope that the user accept to be "comfortable" with.
Actually the user granted permmision to a list of Uri's in this folder for ea file/sub directory there is seperate uri permmision.
Now for example if I will try to create new file in the selected Uri using DocumentFile Ill success but if i will try to outputatream new data to this file I will fail because the user did not grant permision to write to this newly created file.
He only granted to write in the directory path level, means create new file here.
So same happens when you try to move/transfer file to other path that does not have permission from the user.
Path can be folder or file and for ea new path the user needs to grant new access.
move file = new path
write to just created file = new path
I'm trying to write a file from an Http post reply to a file on the sdcard. Everything works fine until the byte array of data is retrieved.
I've tried setting WRITE_EXTERNAL_STORAGE permission in the manifest
and tried many different combinations of tutorials I found on the net.
All I could find was using the openFileOutput("",MODE_WORLD_READABLE) method, of the activity but how my app writes file is by using a thread. Specifically, a thread is invoked from another thread when a file has to be written,
so giving an activity object didn't work even though I tried it.
The app has come a long way and I cannot change how the app is currently written.
Please, someone help me?
CODE:
File file = new File(bgdmanip.savLocation);
FileOutputStream filecon = null;
filecon = new FileOutputStream(file);
byte[] myByte;
myByte = Base64Coder.decode(seReply);
bos.write(myByte);
filecon.write(myByte);
myvals = x * 11024;
bgdmanip.savLocation holds the whole files path. seReply is a string reply from HttpPost response. The second set of code is looped with reference to x. The file is created but remains 0 bytes.
//------------------------------WRITING DATA TO THE FILE ---------------------------------
btnWriteSDFile.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{
try {
File myFile = new File("/sdcard/mysdfile.txt");
myFile.createNewFile();
FileOutputStream fOut = new FileOutputStream(myFile);
OutputStreamWriter myOutWriter =new OutputStreamWriter(fOut);
myOutWriter.append(txtData.getText());
myOutWriter.close();
fOut.close();
Toast.makeText(v.getContext(),"Done writing SD 'mysdfile.txt'", Toast.LENGTH_SHORT).show();
txtData.setText("");
}
catch (Exception e)
{
Toast.makeText(v.getContext(), e.getMessage(),Toast.LENGTH_SHORT).show();
}
}
});
//---------------------------READING DATA FROM THE FILE PLACED IN SDCARD-------------------//
btnReadSDFile.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{
try {
File myFile = new File("/sdcard/mysdfile.txt");
FileInputStream fIn = new FileInputStream(myFile);
BufferedReader myReader = new BufferedReader(new InputStreamReader(fIn));
String aDataRow = "";
String aBuffer = "";
while ((aDataRow = myReader.readLine()) != null)
{
aBuffer += aDataRow ;
}
txtData.setText(aBuffer);
myReader.close();
Toast.makeText(v.getContext(),"Done reading SD 'mysdfile.txt'",Toast.LENGTH_SHORT).show();
}
catch (Exception e)
{
Toast.makeText(v.getContext(), e.getMessage(),Toast.LENGTH_SHORT).show();
}
}
});
ALONG WITH THIS ALSO WRITE THIS PERMISSION IN Android.Manifest
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
The openFileOutput() method writes data to your application's private data area (not the SD card), so that's probably not what you want. You should be able to call Environment.getExternalStorageDirectory() to get the root path to the SD card and use that to create a FileOutputStream. From there, just use the standard java.io routines.
Here is a sample:
// Log used as debug
File log = new File(Environment.getExternalStorageDirectory(), "Log.txt");
try {
out = new BufferedWriter(new FileWriter(log.getAbsolutePath(), false));
out.write(new Date().toString());
out.write(" : \n");
} catch (Exception e) {
Log.e(TAG, "Error opening Log.", e);
}