I have an issue regarding writing to external files in an Android app, tailored for Chrome OS, compiled with App Runtime for Chrome. This app function works as intended on two Android test device (one API 19, same as ARC, I believe), but always fails when tested on a Chromebook. The minSdkVersion
of my app is 19
.
My app is a text editor, and what I want to achieve is to load text using Intent.ACTION_OPEN_DOCUMENT
, saving the file Uri
in the process. Later, when I want to save, I can use this Uri
to access the file and update its contents, without needing to disturb the user with a "Share" dialog every time they want to save the file (or if they have set up an Autosave preference. My app defaults to an ACTION_SEND
Intent if there is a FileNotFoundException
).
Here is the relevant code. Could anyone suggest a workaround, or any improvement I could make?
public class EditorHost extends AppCompatActivity implements EditorFragment.EditorInterface {
private static final int REQUEST_OPEN = 15;
//...
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (mEditorFragmentChild == null)
mEditorFragmentChild = (EditorFragment) mFragmentManager.findFragmentByTag(EDITOR_FRAGMENT_TAG);
switch (item.getItemId()) {
case R.id.load_file:
open();
break;
case R.id.save_file:
if (mEditorFragmentChild.getUri() == null){
shareTask();
break;
}
mEditorFragmentChild.saveTask();
break;
case R.id.share:
mIntent = new Intent(Intent.ACTION_SEND);
mIntent.putExtra(Intent.EXTRA_TEXT, mEditorFragmentChild.mExtendedEditText.getText().toString());
mIntent.setType("text/plain");
startActivity(Intent.createChooser(mIntent, getString(R.string.share_title)));
break;
@TargetApi(Build.VERSION_CODES.KITKAT)
private void open(){
mIntent = new Intent().setType("text/plain");
startActivityForResult(mIntent.setAction(Intent.ACTION_OPEN_DOCUMENT).addCategory(Intent.CATEGORY_OPENABLE), REQUEST_OPEN);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData){
if (resultCode == Activity.RESULT_OK){
Uri uri = null;
if (resultData != null) {
uri = resultData.getData();
if (requestCode == REQUEST_OPEN) {
Cursor c = getContentResolver().query(uri,null, null, null, null);
if (c != null && c.moveToFirst()){
title = c.getString(c.getColumnIndex(OpenableColumns.DISPLAY_NAME));
String[] cols = c.getColumnNames();
for (int i = 0; i < c.getColumnCount(); i++){
Log.e(LOG_TAG, cols[i]);
}
content = readFromFile(uri);
if (title == null || title.equals("")){
title = getString(R.string.app_name);
}
getSupportActionBar().setTitle(title);
mEditorFragmentChild.updateTextFromFile(content.trim());
mEditorFragmentChild.setUri(uri.toString());
if (!c.isClosed()) {
c.close();
}
}
}
}
}
}
and in the EditorFragment
class:
public class EditorFragment extends Fragment implements ... {
//...
String getUri(){
return mUri;
}
void setUri(String newUri){
mUri = newUri;
}
void saveTask(){
if (mUri == null || fromShareTask) {
fromShareTask = false;
return;
}
try {
OutputStream outputStream = mAppCompatActivity.getContentResolver().openOutputStream(Uri.parse(mUri));
outputStream.write(mExtendedEditText.getText().toString().getBytes());
outputStream.close();
//Toast.makeText(mAppCompatActivity, mAppCompatActivity.getString(R.string.file_updated_confirmation), Toast.LENGTH_SHORT).show();
} catch (IOException e){
e.printStackTrace();
dispatchShareEvent();
Toast.makeText(mAppCompatActivity, mAppCompatActivity.getString(R.string.save_failed), Toast.LENGTH_LONG).show();
mExtendedEditText.setText(e.toString() + "\n\n" + exceptionStacktraceToString(e) + "\n\n" + "mUri is " + mUri);
}
}
And finally, the error message (to repeat myself from above, this only occurs on my Chromebook and not an Android test device):
java.io.FileNotFoundException: Can't access data/data/org.chromium.arc/external/(etc.)...
Thanks!
EDIT:
Whilst I couldn't figure out how to get the stack trace from my Chromebook, I was able to add the following method from the accepted answer on this question to print it to an EditText
. I also printed the Uri
at the end. Here is the output:
java.io.FileNotFoundException: Can't access /data/data/org.chromium.arc/external/E5783234425D1719508FC512B2BEE2A5/1kTest.txt
java.io.FileNotFoundException: Can't access /data/data/org.chromium.arc/external/E5783234425D1719508FC512B2BEE2A5/1kTest.txt
at com.android.providers.media.MediaProvider.checkAccess(MediaProvider.java:4620)
at com.android.providers.media.MediaProvider.openFileAndEnforcePathPermissionsHelper(MediaProvider.java:4569)
at com.android.providers.media.MediaProvider.openFile(MediaProvider.java:4493)
at android.content.ContentProvider.openAssetFile(ContentProvider.java:1211)
at android.content.ContentProvider.openAssetFile(ContentProvider.java:1274)
at android.content.ContentProvider$Transport.openAssetFile(ContentProvider.java:314)
at android.content.ContentProvider$Transport.openAssetFile(Native Method)
at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:926)
at android.content.ContentResolver.openOutputStream(ContentResolver.java:673)
at android.content.ContentResolver.openOutputStream(ContentResolver.java:649)
at com.werdpressed.partisan.filterforchrome.EditorFragment.saveTask(EditorFragment.java:370)
at com.werdpressed.partisan.filterforchrome.EditorHost.onOptionsItemSelected(EditorHost.java:119)
at android.app.Activity.onMenuItemSelected(Activity.java:2619)
at android.support.v4.app.FragmentActivity.onMenuItemSelected(FragmentActivity.java:353)
at android.support.v7.app.AppCompatActivity.onMenuItemSelected(AppCompatActivity.java:144)
at android.support.v7.internal.view.WindowCallbackWrapper.onMenuItemSelected(WindowCallbackWrapper.java:99)
at android.support.v7.app.AppCompatDelegateImplV7.onMenuItemSelected(AppCompatDelegateImplV7.java:538)
at android.support.v7.internal.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:802)
at android.support.v7.internal.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:153)
at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:949)
at android.support.v7.internal.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:939)
at android.support.v7.widget.ActionMenuView.invokeItem(ActionMenuView.java:598)
at android.support.v7.internal.view.menu.ActionMenuItemView.onClick(ActionMenuItemView.java:139)
at android.view.View.performClick(View.java:4440)
at android.view.View$PerformClick.run(View.java:18407)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:5181)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at android.os.Process$1.run(Process.java:418)
mUri is content://media/external/file/8
The error occurs in the following try/catch
block:
try {
OutputStream outputStream = mAppCompatActivity.getContentResolver().openOutputStream(Uri.parse(mUri));
outputStream.write(mExtendedEditText.getText().toString().getBytes());
outputStream.close();
//Toast.makeText(mAppCompatActivity, mAppCompatActivity.getString(R.string.file_updated_confirmation), Toast.LENGTH_SHORT).show();
} catch (IOException e){
e.printStackTrace();
dispatchShareEvent();
Toast.makeText(mAppCompatActivity, mAppCompatActivity.getString(R.string.save_failed), Toast.LENGTH_LONG).show();
mExtendedEditText.setText(e.toString() + "\n" + exceptionStacktraceToString(e) + "\n\n" + "mUri is " + mUri);
}
Specifically, this is line 370
referenced in the error log:
OutputStream outputStream = mAppCompatActivity.getContentResolver().openOutputStream(Uri.parse(mUri));
I think you have simply found an interface to access external files we didn't think of (or felt wasn't as important to get working). We patch the filesystem to handle sdcard paths, but do not have any changes to MediaProvider to handle content Uri's like the one you tried to use.
Please feel free to file a bug with the sample code (or at least linking to this post) so we can prioritize fixing it with our other work.