nativescript-community / ui-document-picker

A NativeScript plugin that allows you to select files from the device.
https://nativescript-community.github.io/ui-document-picker/
Apache License 2.0
10 stars 8 forks source link

file.read or file.readSync error: EACCES (Permission denied) #17

Open xpalacincreditoh opened 2 years ago

xpalacincreditoh commented 2 years ago

Good morning,

My system details:

When Open filepicker and select a PDF file from downloads folder the file.readSync sayas error:

Error: java.io.FileNotFoundException: /storage/emulated/0/Download/Test.pdf: open failed: EACCES (Permission denied)

And when call file.read says error:

Error: ReadTask returns no result.

This just happens only when targetSdkVersion is set to 30 and not in 29, but now to publish and app in Google App Store needs targetSdkVersion 30.

Thanks

gdsoftdev commented 2 years ago

Hi,

I've exactly the same problem. From the Download directory I can read a JPG file but not a PDF file (permission denied) @xpalacincreditoh Did you find a solution ?

Thanks

xpalacincreditoh commented 2 years ago

I'm waiting for you to merge the PR pending fix https://github.com/NativeScript/NativeScript/pull/9661

nikoTM commented 2 years ago
// REVIEW: Not sure why DocumentsContact is not there yet
type ProviderWithDocumentsContact = typeof android.provider & {
  DocumentsContract: any;
};

// Per https://stackoverflow.com/questions/17546101/get-real-path-for-uri-android
export function getPathFromURI(uri: android.net.Uri) {
  const getMediaFilePathForN = (uri: android.net.Uri, context: any) => {
    let returnUri = uri;
    let returnCursor = context
      .getContentResolver()
      .query(returnUri, null, null, null, null);
    /*
     * Get the column indexes of the data in the Cursor,
     *     * move to the first row in the Cursor, get the data,
     *     * and display it.
     * */
    let nameIndex = returnCursor.getColumnIndex(
      android.provider.OpenableColumns.DISPLAY_NAME
    );
    let sizeIndex = returnCursor.getColumnIndex(
      android.provider.OpenableColumns.SIZE
    );
    returnCursor.moveToFirst();
    let name = returnCursor.getString(nameIndex);
    let size = java.lang.Long.toString(returnCursor.getLong(sizeIndex));
    let file = new java.io.File(context.getFilesDir(), name);
    try {
      let inputStream = context.getContentResolver().openInputStream(uri);
      let outputStream = new java.io.FileOutputStream(file);
      let read = 0;
      let maxBufferSize = 1 * 1024 * 1024;
      let bytesAvailable = inputStream.available();

      //int bufferSize = 1024;
      let bufferSize = Math.min(bytesAvailable, maxBufferSize);
      let buffers = java.lang.reflect.Array.newInstance(
        java.lang.Byte.class.getField('TYPE').get(null),
        bufferSize
      );
      while ((read = inputStream.read(buffers)) != -1) {
        outputStream.write(buffers, 0, read);
      }
      inputStream.close();
      outputStream.close();
      console.log('File Path', 'Path ' + file.getPath());
      console.log('File Size', 'Size ' + file.length());
    } catch (e) {
      console.log('Exception', e);
    }
    return file.getPath();
  };

  const getDriveFilePath = (uri: android.net.Uri, context: any) => {
    let returnUri = uri;
    let returnCursor = context
      .getContentResolver()
      .query(returnUri, null, null, null, null);
    /*
     * Get the column indexes of the data in the Cursor,
     *     * move to the first row in the Cursor, get the data,
     *     * and display it.
     * */
    let nameIndex = returnCursor.getColumnIndex(
      android.provider.OpenableColumns.DISPLAY_NAME
    );
    returnCursor.moveToFirst();
    let name = returnCursor.getString(nameIndex);
    let file = new java.io.File(context.getCacheDir(), name);
    try {
      let inputStream = context.getContentResolver().openInputStream(uri);
      let outputStream = new java.io.FileOutputStream(file);
      let read = 0;
      let maxBufferSize = 1 * 1024 * 1024;
      let bytesAvailable = inputStream.available();

      //int bufferSize = 1024;
      let bufferSize = Math.min(bytesAvailable, maxBufferSize);

      let buffers = java.lang.reflect.Array.newInstance(
        java.lang.Byte.class.getField('TYPE').get(null),
        bufferSize
      );
      while ((read = inputStream.read(buffers)) != -1) {
        outputStream.write(buffers, 0, read);
      }
      inputStream.close();
      outputStream.close();
      console.log('File Path', 'Path ' + file.getPath());
      console.log('File Size', 'Size ' + file.length());
    } catch (e) {
      console.log('Exception', e);
    }
    return file.getPath();
  };

  const getDataColumn = (
    context: any,
    uri: android.net.Uri,
    selection: any,
    selectionArgs: any
  ) => {
    let cursor = null;
    const column = '_data';
    const projection = [column];

    try {
      cursor = context
        .getContentResolver()
        .query(uri, projection, selection, selectionArgs, null);
      if (cursor != null && cursor.moveToFirst()) {
        let column_index = cursor.getColumnIndexOrThrow(column);
        return cursor.getString(column_index);
      }
    } catch (e) {
      return getMediaFilePathForN(uri, context);
    } finally {
      if (cursor != null) cursor.close();
    }
    return null;
  };

  const isExternalStorageDocument = (uri: android.net.Uri) => {
    return 'com.android.externalstorage.documents' === uri.getAuthority();
  };

  const isDownloadsDocument = (uri: android.net.Uri) => {
    return 'com.android.providers.downloads.documents' === uri.getAuthority();
  };

  const isMediaDocument = (uri: android.net.Uri) => {
    return 'com.android.providers.media.documents' === uri.getAuthority();
  };

  const isGooglePhotosUri = (uri: android.net.Uri) => {
    return 'com.google.android.apps.photos.content' === uri.getAuthority();
  };
  const isGoogleDriveUri = (uri: android.net.Uri) => {
    return (
      'com.google.android.apps.docs.storage' === uri.getAuthority() ||
      'com.google.android.apps.docs.storage.legacy' === uri.getAuthority()
    );
  };
  const activity =
    Application.android.startActivity || Application.android.foregroundActivity;
  const context = activity.getApplicationContext();
  const isKitKat = parseInt(Device.sdkVersion, 10) >= 19;

  if (typeof uri === 'string') {
    uri = android.net.Uri.parse(uri);
  }
  // DocumentProvider
  if (
    isKitKat &&
    (<ProviderWithDocumentsContact>(
      android.provider
    )).DocumentsContract.isDocumentUri(context, uri)
  ) {
    // ExternalStorageProvider
    if (isExternalStorageDocument(uri)) {
      const docId: string = (<ProviderWithDocumentsContact>(
        android.provider
      )).DocumentsContract.getDocumentId(uri);
      const split = docId.split(':');
      const type: string = split[0].toLowerCase();
      if ('primary' === type) {
        return (
          android.os.Environment.getExternalStorageDirectory() + '/' + split[1]
        );
      } else {
        // https://stackoverflow.com/questions/44226029/how-get-a-file-path-by-uri-which-authority-is-com-android-externalstorage-docum
        let external = context.getExternalMediaDirs();

        if (external.length > 0) {
          let filePath = external[0].getAbsolutePath();
          filePath =
            filePath.substring(0, filePath.indexOf('Android')) + split[1];
          return filePath;
        }
        return uri.getPath();
      }
    }
    // DownloadsProvider
    else if (isDownloadsDocument(uri)) {
      if (parseInt(Device.sdkVersion, 10) >= 23) {
        let cursor = null;
        try {
          cursor = context
            .getContentResolver()
            .query(
              uri,
              [android.provider.MediaStore.MediaColumns.DISPLAY_NAME],
              null,
              null,
              null
            );
          if (cursor != null && cursor.moveToFirst()) {
            let fileName = cursor.getString(0);
            let path =
              android.os.Environment.getExternalStorageDirectory().toString() +
              '/Download/' +
              fileName;
            if (!android.text.TextUtils.isEmpty(path)) {
              return path;
            }
          }
        } finally {
          if (cursor != null) cursor.close();
        }
        const id = (<ProviderWithDocumentsContact>(
          android.provider
        )).DocumentsContract.getDocumentId(uri);
        if (!android.text.TextUtils.isEmpty(id)) {
          if (id.startsWith('raw:')) {
            return id.replace(/^raw:/, '');
          }
          const contentUriPrefixesToTry = [
            'content://downloads/public_downloads',
            'content://downloads/my_downloads'
          ];
          for (let contentUriPrefix of contentUriPrefixesToTry) {
            try {
              let contentUri = android.content.ContentUris.withAppendedId(
                android.net.Uri.parse(contentUriPrefix),
                 java.lang.Long.valueOf(id) as any
              );

              /*   final Uri contentUri = ContentUris.withAppendedId(
                          Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));*/

              return getDataColumn(context, contentUri, null, null);
            } catch (e) {
              //In Android 8 and Android P the id is not a number
              return uri
                .getPath()
                .replace(/^\/document\/raw:/, '')
                .replace(/^raw:/, '');
            }
          }
        }
      } else {
        const id: string = (<ProviderWithDocumentsContact>(
          android.provider
        )).DocumentsContract.getDocumentId(uri);
        if (id.startsWith('raw:')) {
          return id.slice(4);
        } else {
          const contentUri = android.content.ContentUris.withAppendedId(
            android.net.Uri.parse('content://downloads/public_downloads'),
            java.lang.Long.valueOf(id) as any
          );

          return getDataColumn(context, contentUri, null, null);
        }
      }
    }
    // MediaProvider
    else if (isMediaDocument(uri)) {
      const docId = (<ProviderWithDocumentsContact>(
        android.provider
      )).DocumentsContract.getDocumentId(uri);
      const split = docId.split(':');
      const type = split[0];

      let contentUri = null;
      if ('image' === type) {
        contentUri =
          android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
      } else if ('video' === type) {
        contentUri =
          android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
      } else if ('audio' === type) {
        contentUri =
          android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
      } else {
        return getMediaFilePathForN(uri, context);
      }

      const selection = '_id=?';
      const selectionArgs = [split[1]]; // js Array?

      return getDataColumn(context, contentUri, selection, selectionArgs);
    } else if (isGoogleDriveUri(uri)) {
      return getDriveFilePath(uri, context);
    }
  }
  // MediaStore (and general)
  else if ('content' === uri.getScheme().toLowerCase()) {
    if (isGooglePhotosUri(uri)) {
      return uri.getLastPathSegment();
    }

    if (isGoogleDriveUri(uri)) {
      return getDriveFilePath(uri, context);
    }
    if (parseInt(Device.sdkVersion) === 24) {
      // return getFilePathFromURI(context,uri);
      return getMediaFilePathForN(uri, context);
      // return getRealPathFromURI(context,uri);
    } else {
      return getDataColumn(context, uri, null, null);
    }
  }
  // File
  else if ('file' === uri.getScheme().toLowerCase()) {
    return uri.getPath();
  }

  return null;
}

@xpalacincreditoh I use this monstrosity for reading files for now. You might only need getMediaFilePathForN from there. But it seems to handle most of things.

xpalacincreditoh commented 2 years ago

When I call: openFilePicker(isIOS ? this.optionsFile.ios : this.optionsFile.android).then((res) => { ... Where do I have to use your method getPathFromURI?

nikoTM commented 2 years ago

@xpalacincreditoh res.android

xpalacincreditoh commented 2 years ago

@xpalacincreditoh res.android

When I use your code I get an error with word type:

Module parse failed: Unexpected token (279:5)
File was processed with these loaders:
 * ./node_modules/@nativescript/webpack/dist/loaders/nativescript-worker-loader/index.js
You may need an additional loader to handle the result of these loaders.
| 
| // REVIEW: Not sure why DocumentsContact is not there yet
> type ProviderWithDocumentsContact = typeof android.provider & {
|     DocumentsContract: any;
| };
nikoTM commented 2 years ago

@xpalacincreditoh seems like you are using typescript code in a JS file?

xpalacincreditoh commented 2 years ago

@xpalacincreditoh seems like you are using typescript code in a JS file?

yes I use your solution of yesterday https://github.com/nativescript-community/ui-document-picker/issues/17#issuecomment-1011052259 I just realized it's TS and my code is JS. Error: JS: TypeError: getUtils(...).getBytes is not a function