In React Native I have this module: //screens\mul...
Создано: 11 сентября 2025 г.
Создано: 11 сентября 2025 г.
In React Native I have this module:
//screens\multifileSearch.js
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TouchableOpacity, Keyboard, TextInput, ActivityIndicator,
SafeAreaView, FlatList, PermissionsAndroid, Image, BackHandler } from 'react-native';
import Share from 'react-native-share';
import { DocumentsSearchIcons, GeneralIcons, EditorIcons } from '../myLibs/GUI_Elements/Icons';
import multiStyles from '../styles/multifileSearchStyle';
import mainStyles from '../styles/mainStyles';
import { cosineSimilarity, FOLDER_NAME_SEPARATOR, getAppVersion,
getFileNameEmbedding, requestReadPermission,
NEW_FILE_FOLDER_PATH, getParentDirFromPath, HOME_FOLDER_PATH,
GOOGLE_DRIVE,
IMPORTED_FILES_FOLDER_PATH,
NEW_FILE_NAME,
getFileName,
updateFileNameEmbedding,
EMBEDDING_LENGTH,
getFileContentFromGDriveOrDeleteFromBase,
paidFunction,
TEMP_FOLDER_PATH,
ensureTextExtension,
getUniqueFilePath,
} from '../myLibs/constantsAndFunctions';
import { MyStatusBar } from '../myLibs/GUI_Elements/сommonElements';
import { SearchedItem } from '../myLibs/GUI_Elements/SearchedItem';
import { deleteGDriveFolderIds, getAllGDriveFolderIds, getCacheUpdateTime, updateRelativeFilePath } from '../myLibs/DBManager';
import { deleteFromBase, executeQuery, isThereExternalFile} from '../myLibs/DBManager';
import CustomTooltip from '../myLibs/GUI_Elements/CustomTooltip';
import { isProApp } from '../myLibs/appSwitcher';
import { t } from '../language/i18n';
import { fetch } from "@react-native-community/netinfo";
import Toast from 'react-native-toast-message';
import { toastConfig } from '../myLibs/GUI_Elements/CustomToast';
import { useFocusEffect } from '@react-navigation/native';
import UserInfoWindow from '../myLibs/GUI_Elements/UserInfoWindow';
import SystemNavigationBar from 'react-native-system-navigation-bar';
import DocumentPicker from 'react-native-document-picker';
import AppFile, { FileType } from '../myLibs/AppFile';
import { adjustSize } from '../styles/adjustFontSize';
import AiSearchButton from '../myLibs/AI_Modules/AiSearchButton';
import ProgressBar from '../myLibs/GUI_Elements/ProgressBar';
import { AISearchByFileContent } from '../myLibs/AISearchByFileContent';
import { GDriveFileCacheManager, readFromCache } from '../myLibs/GDriveFileCacheManager';
import threadManagerInstance from '../myLibs/ThreadManager';
import MiniProgressBar from '../myLibs/GUI_Elements/MiniProgressBar';
import { gdriveFileManager, localFileManager } from '../myLibs/file_manager/FileManagerFactory';
import { GoogleAuthManager } from '../myLibs/GoogleAuthManager';
import { PaymentManager, paymentManager, PaymentModals } from '../myLibs/Paymants/PaymentManager';
import { useAppContext } from '../myLibs/AppContextProvider';
import { CustomAlert } from '../myLibs/GUI_Elements/CustomAlert';
import { logDebug, logError } from '../myLibs/Logging';
import RemainingUsage from '../myLibs/Paymants/RemainingUsage';
export const SearchResultTypes = {
CONTENT: 0,
FILE_NAME: 1,
AI: 2,
AI_LITE: 3,
ALL: 4,
};
export default function MultifileSearch({navigation}) {
textconst canBeAutoFocused = useRef(true); const [isSearchMode, setIsSearchModeLocaly] = useState(false); const isSearchModeRef = useRef(false); const setIsSearchMode = (isSearchMode, canBeFocused = true) => { isSearchModeRef.current = isSearchMode; canBeAutoFocused.current = canBeFocused; setIsSearchModeLocaly(isSearchMode); } const [isSearching, setIsSearching] = useState(false); const {userRef, user, setUser} = useAppContext(); const [query, setQuery] = useState(''); const [searchList, setSearchList] = useState([]); const flatListRef = useRef(null); const searchRef = useRef(null); const subTextLength = 100; //Length of text preview in list item const currentSearchResultType = useRef(null); const miniProgress = useRef(null); const currentThreadId = useRef(0); const [rerender, setRerender] = useState(false); useEffect(() => { const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', keyboardDidHide); BackHandler.addEventListener('hardwareBackPress', backPressHandler); const init = async () => { const localUser = await GoogleAuthManager.getCurrentUser(); setUser(localUser); PaymentManager.init().then(() => logDebug("Initialized")).catch(error => logError(error)); GoogleAuthManager.initialize(); isThereExternalFile().then(async(result)=>{ if(result===true) { try { var status = await requestReadPermission(t('To correctly display all files ever opened, you must grant permission to access them.'), t); } catch (error) { CustomAlert.alert(t('Error'),t('Error while processing read permission. The list of previously opened files will be opened without read permission.'), [], { cancelable: true }); showAllFiles(false); } showAllFiles(status === PermissionsAndroid.RESULTS.GRANTED); } else { showAllFiles(true); } }).catch((error)=>{ logError(error); CustomAlert.alert(t('Error'),t('Something went wrong, it failed to display the list of previously opened files correctly.'), [], { cancelable: true }); setQuery(''); }); } init(); return () => { BackHandler.removeEventListener('hardwareBackPress', backPressHandler); keyboardDidHideListener.remove(); }; }, []); const isFirstLoad = useRef(true); useFocusEffect( React.useCallback(() => { /*I paint the system bottom panel in the desired color. This line of code is located here, because if you press Back on this screen and then return to the application, then the panel will lose the desired color, but thanks to this line I get the desired color back.*/ SystemNavigationBar.setNavigationColor('#ddd', 'dark', 'navigation'); GoogleAuthManager.getCurrentUser().then((user)=>{ setUser(user); runAtScreenFocus(); }).catch(error=>{ logError(error); runAtScreenFocus(); }); }, [])); //Runs every time the search screen is focused on function runAtScreenFocus() { if(!isFirstLoad.current) { if(currentSearchResultType.current === SearchResultTypes.ALL) { handleSearchByFileContent(''); } } else { isFirstLoad.current = false; } } function backPressHandler() { CustomTooltip.setIsHelpMode(false); if(selectedFilesRef.current.length !== 0){ clearSelection(); return true; } if(isSearchModeRef.current) { closeSearchPanel(); return true; } } const keyboardDidHide = () => { if(searchRef.current!==null) searchRef.current.blur(); }; const uploadContentToListFromGDrive = async (gDriveFiles, threadId) => { const usedFolderIds = new Set(); if(gDriveFiles.length > 0) { let errorMessage = null; let filesDetails = {}; try { let state = await fetch(); if( state.isConnected ) { //Checking if wifi or mobile internet is connected const gDriveFilesIds = gDriveFiles.map(file => file.fileId); const gDriveFoldersIds = await getAllGDriveFolderIds(); const gDriveIds = gDriveFilesIds.concat(gDriveFoldersIds); filesDetails = await gdriveFileManager.getFilesDetails(gDriveIds); } else { errorMessage = t('There is no internet connection, but the most recently saved version from the cache is available.'); } } catch(error) { logError('Error in first catch of uploadContentToListFromGDrive:', error); switch (error.message) { case 'Network request failed': case 'NETWORK_ERROR': errorMessage = t('There is no internet connection, but the most recently saved version from the cache is available.'); break; case 'No user to be signed in': errorMessage = t('You’re logged out of the Google account linked to this file, so only the cached version is available.'); break; default: errorMessage = t('For some reason this file could not be read.'); } } let content; let isError; let newRelativeFilePath; let ancestors; //In this second loop, we start loading content from google drive. for(let j=0; j < gDriveFiles.length; j++) { isError = false; try { if ( errorMessage !== null ){ content = errorMessage; isError = true; } else { content = (await GDriveFileCacheManager.getFileStartContentAndModifiedTime( gDriveFiles[j].fileId, subTextLength, filesDetails[gDriveFiles[j].fileId]?.modifiedTime )).content; const result = await GDriveFileCacheManager.getRelativeFilePathById( gDriveFiles[j].fileId, filesDetails, usedFolderIds ); newRelativeFilePath = result.relativeFilePath; ancestors = result.ancestors; isError = false; } } catch (error) { logError('Error in second catch of uploadContentToListFromGDrive:', error); if(error.message === 'File not found' || error.message === 'Access denied') { deleteFromBase(gDriveFiles[j].fileId); GDriveFileCacheManager.deleteFromCache(gDriveFiles[j].fileId); setSearchList((searchList) => { for(let i=0; i<searchList.length; i++) { if(searchList[i].fileId===gDriveFiles[j].fileId) { searchList.splice(i, 1); return searchList; } } return searchList; }); setRerender(r=>!r); continue; } else { switch(error.message) { case 'Network request failed': case 'NETWORK_ERROR': content = t('There is no internet connection, but the most recently saved version from the cache is available.'); break; case 'OutOfMemoryError': content = t('The file size is too large to be supported.'); break; default: content = t('For some reason this file could not be read.'); } isError = true; } } if(currentThreadId.current!==threadId) return; if( content !== null ) { setSearchList((searchList) => { return searchList.map(item=>{ if (item.fileId === gDriveFiles[j].fileId) { if( newRelativeFilePath !== item.relativeFilePath && isError === false ) { updateRelativeFilePath(item.fileId, newRelativeFilePath); const oldName = getFileName(FileType.G_DRIVE_FILE, newRelativeFilePath); const newName = getFileName(FileType.G_DRIVE_FILE, item.relativeFilePath); if( oldName !== newName ){ updateFileNameEmbedding(item.fileId, ""); } } return { ...item, //text: content.substring(0, subTextLength), text: content, isError: isError, relativeFilePath: isError ? item.relativeFilePath : newRelativeFilePath, ancestors: isError ? item.ancestors : ancestors, isLoading:false}; } return item; }); }); } } } // Clean up unused folder IDs const allFolderIds = await getAllGDriveFolderIds(); const unusedFolderIds = allFolderIds.filter(id => !usedFolderIds.has(id)); await deleteGDriveFolderIds(unusedFolderIds); } async function showAllFiles(hasPermission) { const threadId = (currentThreadId.current = performance.now()); currentSearchResultType.current = SearchResultTypes.ALL; try { const rows = await executeQuery( "SELECT fileId, isAiSearchable, fileType, relativeFilePath, email FROM files ORDER BY lastOpening DESC;" ); setIsSearching(true); if(miniProgress.current) miniProgress.current.setProgress(null); const length = rows.length - 1; const gDriveFiles = []; let isLoading = undefined; let id = undefined; // Create a Set of fileIds from the database for efficient lookup //const dbFileIds = new Set(rows.map(row => row.fileId)); let dbFileIds = []; for (let i = 0; i < rows.length; i++) { dbFileIds.push(rows.item(i).fileId); } dbFileIds = new Set(dbFileIds); // First, remove items from searchList that aren't in the database anymore setSearchList(searchList => { return searchList.filter(item => dbFileIds.has(item.fileId)); }); for (let i = 0, j = 0; i <= length; i++) { const fileId = rows.item(i).fileId; const fileType = rows.item(i).fileType; const email = rows.item(i).email; let content = ''; let isError = false; if (fileType !== FileType.G_DRIVE_FILE) { // Local or shared file if (!hasPermission && fileType === FileType.SHARED_FILE) { // The "no-permission" scenario for shared files content = t('This content is not available because you have not granted permission to access this file.'); isError = true; isLoading = false; } else { // Handle local file (with or without permission, or not shared) try { if (await localFileManager.fileExists(fileId)) { content = await localFileManager.getFileStartContent(fileId, subTextLength); isLoading = false; if(fileType === FileType.NEW_FILE) { const fileDetails = await localFileManager.getFileDetails(fileId); if(fileDetails.size === 0) { deleteFromBase(fileId); localFileManager.deleteFile(fileId); content = null; } } } else { deleteFromBase(fileId); setSearchList(searchList => { for (let k = j; k < searchList.length; k++) { if (searchList[k].fileId === fileId) { searchList.splice(k, 1); return searchList; } } return searchList; }); content = null; } } catch (error) { logError(error); content = t('For some reason this file could not be read.'); isError = true; isLoading = false; } } } else { // G_DRIVE_FILE if (userRef.current === null || userRef.current.email !== email) { content = t( 'You’re logged out of the Google account linked to this file, so only the cached version is available.' ); isError = true; isLoading = false; } else { //We assign not null, because we need the processing to occur in the operator if (content !== null). content = ''; gDriveFiles.push({ fileId, email }); isError = false; isLoading = true; } } // Update the UI if we actually have something to display if (content !== null) { id = fileId; if (currentThreadId.current !== threadId) return; setSearchList(searchList => { const newItem = { ...rows.item(i), text: content, queryIndex: { start: 0, end: 0 }, isError, isLoading }; // If the file is "loading", keep the existing text or set it to empty for (let k = j; k < searchList.length; k++) { if (searchList[k].fileId === id) { if (isLoading) { newItem.text = searchList[k].text; } if (k !== j) { searchList.splice(k, 1); searchList.splice(j, 0, newItem); } else { searchList.splice(j, 1, newItem); } return searchList; } } if (isLoading) { newItem.text = ''; } searchList.splice(j, 0, newItem); return searchList; }); j++; } } setIsSearching(false); setRerender(r => !r); if (currentThreadId.current !== threadId) return; // Asynchronously fetch GDrive file contents await uploadContentToListFromGDrive(gDriveFiles, threadId); } catch(error){ logError(error); CustomAlert.alert( t('Error'), t('Something went wrong, it failed to display the list of previously opened files correctly.'), [], { cancelable: true } ); } } async function aiSearchLite(query, hasPermission) { // some code } async function searchByFileName(query, hasPermission) { // some code } async function searchByFileContent(query, hasPermission) { // some code } const timerQueryRef = useRef(null); function handleChangeQuery(value) { setQuery(value); // check if there is a timer set if (timerQueryRef.current) clearTimeout(timerQueryRef.current); // set a new timer that will save the text after 5 seconds of inactivity timerQueryRef.current = setTimeout(async ()=>{ const hasReadPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE); searchByFileName(value, hasReadPermission); }, 900); } function handleSearchFocus() { } async function deleteSelectedFilesFromList() { const isOk = await new Promise((resolve, reject)=>{ CustomAlert.alert( '', t('Are you sure you want to delete?'), [ {text: t('Cancel'), onPress:()=>{resolve(false)}}, {text: t('Ok'), onPress: ()=> {resolve(true)}} ], { cancelable: true, onDismiss:()=>{resolve(false)} } ); }); if(!isOk) return; try { for(let i=0; i<selectedFilesRef.current.length; i++) { const fileId = searchList[selectedFilesRef.current[i]].fileId; await deleteFromBase(fileId) } for(let i=0; i<selectedFilesRef.current.length; i++){ if(searchList[selectedFilesRef.current[i]].fileType===FileType.NEW_FILE ){ await localFileManager.deleteFile(searchList[selectedFilesRef.current[i]].fileId); } else if(searchList[selectedFilesRef.current[i]].fileType===FileType.IMPORTED_FILE) { await localFileManager.deleteFile(searchList[selectedFilesRef.current[i]].fileId); } else if(searchList[selectedFilesRef.current[i]].fileType===FileType.G_DRIVE_FILE) { await GDriveFileCacheManager.deleteFromCache(searchList[selectedFilesRef.current[i]].fileId); } } }catch(error){ logError(error); CustomAlert.alert(t('Error'),t('Something went wrong, failed to delete correctly.'), [], { cancelable: true }); return; } setSearchList((searchList)=>{ selectedFilesRef.current.sort((a,b)=>b-a); selectedFilesRef.current.map((index)=>{ searchList.splice(index,1); }) return searchList; }); selectedFilesRef.current=[]; setRerender(r=>!r); } async function shareSelectedFilesFromList() { const selectedIndices = selectedFilesRef.current; const selectedItems = selectedIndices.map(index => searchList[index]); // Fetch Google Drive file details once for all selected GDrive files let fileDetails = {}; const gdriveItems = selectedItems.filter(item => item.fileType === FileType.G_DRIVE_FILE); if (gdriveItems.length > 0) { const gdriveFileIds = gdriveItems.map(item => item.fileId); try { fileDetails = await gdriveFileManager.getFilesDetails(gdriveFileIds); } catch (error) { logError(error, 'Failed to fetch Google Drive file details'); // Proceed, handling individual failures below } } // Create a unique temporary directory const tempDir = await localFileManager.createFolder(`share_${Date.now()}`, TEMP_FOLDER_PATH); // Prepare files concurrently const preparePromises = selectedItems.map(async (item) => { let sourcePath; let originalName; if (item.fileType !== FileType.G_DRIVE_FILE) { // Local file (NEW_FILE, IMPORTED_FILE, SHARED_FILE) sourcePath = item.fileId; if (!await localFileManager.fileExists(sourcePath)) { return { error: 'File not found', name: getFileName(item.fileType, item.relativeFilePath) }; } originalName = getFileName(item.fileType, item.relativeFilePath); } else { // Google Drive file const fileId = item.fileId; const details = fileDetails[fileId]; if (!details || !details.name) { return { error: 'Failed to get file details', name: getFileName(item.fileType, item.relativeFilePath) }; } originalName = details.name; try { // Ensure cache is up-to-date await GDriveFileCacheManager.getFileContentAndModifiedTime(fileId, details.modifiedTime); sourcePath = GDriveFileCacheManager.getLocalFilePath(fileId); } catch (error) { logError(error, `Error preparing Google Drive file ${fileId}`); return { error: error.message, name: originalName }; } } // Adjust file name to ensure it has an extension const adjustedName = ensureTextExtension(originalName); // Get a unique file path in the temporary directory const tempFilePath = await getUniqueFilePath(tempDir, adjustedName); try { // Copy the file to the temporary path await localFileManager.copyFile(sourcePath, tempFilePath); return { uri: `file://${tempFilePath}` }; } catch (error) { logError(error, `Error copying file to temporary directory: ${sourcePath}`); return { error: error.message, name: adjustedName }; } }); // Wait for all preparations to complete const results = await Promise.all(preparePromises); // Separate successful URIs and failed files const shareUris = []; const failedFiles = []; results.forEach(result => { if (result.uri) { shareUris.push(result.uri); } else if (result.error) { failedFiles.push(result.name); } }); try { // Check for failed files before sharing if (failedFiles.length > 0) { const continueSharing = await new Promise((resolve) => { CustomAlert.alert( t('Warning'), `${t("You won't be able to share these files now:")}\n${failedFiles.join(',\n')}`, [ { text: t('Cancel'), onPress: () => resolve(false) }, { text: t('Continue'), onPress: () => resolve(true) }, ], { cancelable: true }, () => resolve(false) // Resolve to false if dismissed ); }); if (!continueSharing) { // User chose to cancel or dismissed the alert return; } } // Prepare share options const options = { title: 'Share selected files', type: 'text/plain', }; console.log(shareUris); //Output example: ["file:///data/user/0/app.brainlog/files/Temp/share_1757534422027/fileName_1.txt", "file:///data/user/0/app.brainlog/files/Temp/share_1757534422027/fileName_1.txt"] //todo clarify the answer by providing the example of shareUris if (shareUris.length > 0) { options.urls = shareUris; await Share.open(options); } else { CustomAlert.alert(t('Error'), t('None of the selected files can be shared at this time.'), [], { cancelable: true }); } } catch (error) { if (error.message !== 'User did not share') { logError(error, 'Share operation failed'); CustomAlert.alert(t('Error'), t('Failed to share files.'), [], { cancelable: true }); } } finally { // Clean up temporary directory try { await localFileManager.deleteFile(tempDir); } catch (error) { logError(error, 'Failed to delete temporary directory'); } } } async function handleAiLiteSearchByFileName(query) { if(searchRef.current) searchRef.current.blur(); try { // Disable auto-search trigger by file names if (timerQueryRef.current) clearTimeout(timerQueryRef.current); const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE); await aiSearchLite(query, hasPermission); } catch (error) { logError(error); CustomAlert.alert(t('Error'), t('Something went wrong, failed to display search results.'), [], { cancelable: true }); } } async function handleSearchByFileContent(query) { if(searchRef.current) searchRef.current.blur(); try { if (timerQueryRef.current) clearTimeout(timerQueryRef.current); const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE); // If no query, show all files depending on permission if (!query) { await showAllFiles(hasPermission); } else { await searchByFileContent(query, hasPermission); } } catch (error) { logError(error); CustomAlert.alert(t('Error'), t('Something went wrong, failed to display search results.'), [], { cancelable: true }); } } async function handleAISearchByFileContent(passedQuery) { if(searchRef.current) searchRef.current.blur(); try { if (timerQueryRef.current) clearTimeout(timerQueryRef.current); const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE); // If no query, show all files depending on permission if (!passedQuery) { showAllFiles(hasPermission); } else { const threadId = threadManagerInstance.getNewCurrentThreadId(); setIsSearching(true); await AISearchByFileContent( passedQuery, hasPermission, setSearchList, setRerender, currentSearchResultType, userRef, threadId); if(threadManagerInstance.isThereThread()){ if(!threadManagerInstance.isThreadNotCurrent(threadId)) { setTimeout(()=>{ ProgressBar.closeProgressBar(); },1000); setIsSearching(false); setIsSearchMode(true, false); setQuery(passedQuery); } } else { ProgressBar.closeProgressBar(); setIsSearching(false); } } } catch (error) { ProgressBar.closeProgressBar(); setIsSearching(false); logError('Semantic search error:', error); CustomAlert.alert(t('Error'), t('Something went wrong, failed to display search results.'), [], { cancelable: true }); } } const isAiSearchableRef = useRef(1); function isAiSearchableButton(){ if(!isProApp) return null; if(isAiSearchableRef.current) { return( <CustomTooltip text={t('excludeFromSemanticSearchButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.button} onPress={excludeFileFromAiSearch} accessibilityLabel={t("excludeFromSemanticSearchLabelOnMultifileScreen")}> <DocumentsSearchIcons name="ai_search_minus" size={26} color="black"/> </TouchableOpacity> </CustomTooltip> ); } else { return( <CustomTooltip text={t('addToSemanticSearchButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.button} onPress={addFileToAiSearch} accessibilityLabel={t("addToSemanticSearchLabelOnMultifileScreen")}> <DocumentsSearchIcons name="ai_search_plus" size={26} color="black"/> </TouchableOpacity> </CustomTooltip> ); } } function modifyFileName(fileName, number) { // Find the last dot to get the file extension let lastDotIndex = fileName.lastIndexOf('.'); if (lastDotIndex === -1) { // If there's no dot, just append " (1)" at the end return fileName + ` (${number})`; } // Split the file name and the extension let name = fileName.substring(0, lastDotIndex); let extension = fileName.substring(lastDotIndex); // Insert " (1)" before the extension return name + ` (${number})` + extension; } async function importFileHandler() { try { const res = await DocumentPicker.pick({ type: [DocumentPicker.types.plainText], }); setIsLoading(true); const content = await localFileManager.getFileContent(res[0].uri); const fileName = res[0].name; let filePath = IMPORTED_FILES_FOLDER_PATH + fileName; let i = 1; while(await localFileManager.fileExists(filePath)) { filePath = IMPORTED_FILES_FOLDER_PATH + modifyFileName(fileName, i++); } await localFileManager.uploadContent(filePath, content); const modificationTime = (await localFileManager.getFileDetails(filePath)).modifiedTime; const file = new AppFile({id:filePath, type:FileType.IMPORTED_FILE, relativePath:filePath.replace(IMPORTED_FILES_FOLDER_PATH,''), content, modificationTime}); await file.initializeName(); navigation.navigate('TextEditor', {file:file.toPlainObject()}); setIsLoading(false); } catch (error) { setIsLoading(false); if (!(DocumentPicker.isCancel(error))) { logError(error); } } } async function createNewFileHandler(){ try { let number = 1; const newFiles = await localFileManager.listFilesInFolder(NEW_FILE_FOLDER_PATH); if(newFiles.length > 0) { const existingNumbers = newFiles.map(item => parseInt(item.name)).sort((a, b) => a - b); for(let i=0; i<existingNumbers.length; i++) { if(number===existingNumbers[i]) { number++; } else { break; } } } const path = NEW_FILE_FOLDER_PATH + `${number}`; const content = ''; await localFileManager.uploadContent(path, content); const name = t(NEW_FILE_NAME) + ' ' + number; const relativePath = name; const file = new AppFile({id:path, name, type:FileType.NEW_FILE, relativePath, content}); navigation.navigate('TextEditor', {file:file.toPlainObject()}); } catch(error){ logError(error); } } async function openFileHandler(){ let currentDir = undefined; let fileId = undefined; let ancestors = undefined; if(searchList.length>0) { if(searchList[0].isError) { currentDir = HOME_FOLDER_PATH; } else { if(searchList[0].fileType===FileType.G_DRIVE_FILE) { let item = await new Promise((res) => { setSearchList((searchList) => { res(searchList[0]); return searchList; }); }); ancestors = item.ancestors || []; const path = GOOGLE_DRIVE + FOLDER_NAME_SEPARATOR + searchList[0].relativeFilePath; //Calculate the path of the parent's folder const driveFolder = path.substring(0, path.lastIndexOf(FOLDER_NAME_SEPARATOR) + FOLDER_NAME_SEPARATOR.length); currentDir = {driveId:undefined, driveFolder: driveFolder}; fileId = searchList[0].fileId; } else { if(searchList[0].fileType === FileType.NEW_FILE || searchList[0].fileType === FileType.IMPORTED_FILE) { currentDir = HOME_FOLDER_PATH; } else { currentDir = getParentDirFromPath(searchList[0].fileId); fileId = searchList[0].fileId } } } } else { currentDir = HOME_FOLDER_PATH; } navigation.navigate('OpenFile', { currentDir, fileId, ancestors }); } async function clearQuery(){ setQuery(''); const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE); showAllFiles(hasPermission); } async function signIn() { try { const result = await GoogleAuthManager.signIn(); if( result === 'cancelled' ) return; setUser(await GoogleAuthManager.getCurrentUser()); if(currentSearchResultType.current===SearchResultTypes.ALL){ handleSearchByFileContent(''); } } catch(error){ logError(error); switch (error.message) { case 'NETWORK_ERROR': CustomAlert.alert(t('Error'), t('There is no internet connection.'), [], { cancelable: true }); break; case 'Sign in action cancelled': break; default: CustomAlert.alert(t('Error'), t("For some reason, you can't sign in to your Google Account."), [], { cancelable: true }); break; } } } async function signOut() { UserInfoWindow.show(async () => { try{ await GoogleAuthManager.signOut(); if(currentSearchResultType.current===SearchResultTypes.ALL){ handleSearchByFileContent(''); } }catch(error){ logError(error); } }, { name: userRef.current.name, email: userRef.current.email, photo: userRef.current.photo }); } function getApplicationVersion(){ const version = getAppVersion(); if(version===undefined) return null; else return '\n\n'+t('Version: ')+version; } const openSearchPanel = async () => { setIsSearchMode(true); } const closeSearchPanel = () => { setIsSearchMode(false); clearQuery(); } function showMenuPanel(){ if(selectedFilesRef.current.length === 0) { if(isSearchMode) { return( <View> <View style={{backgroundColor:'#eee'}}> <View style={mainStyles.searchPanel}> <TextInput style={mainStyles.searchInput} placeholderTextColor={'gray'} placeholder={t("searchFilePlaceholderOnMultifileScreen")} multiline={true} ref={searchRef} onChangeText={(value)=>handleChangeQuery(value)} onFocus={handleSearchFocus} onLayout={()=>{ if(canBeAutoFocused.current) { searchRef.current.focus(); } }} value={query} /> {query && <View style={mainStyles.viewCloseSearch}> <TouchableOpacity style={mainStyles.clearTextInput} onPress={clearQuery} accessibilityLabel={t('Erase the text')}> <GeneralIcons name="close" size={25} color="gray"/> </TouchableOpacity> </View>} </View> </View> <View style={mainStyles.menuPanel}> <CustomTooltip text={t('searchPanelHelpButtonDescriptionOnMultifileScreen')} isTooltipSwitch={true}> <TouchableOpacity style={mainStyles.bigMenuButton} onPress={()=>{setRerender(r=>!r)}} accessibilityLabel={t('helpLabelOnMultifileScreen')}> {CustomTooltip.getIsHelpMode() ? <GeneralIcons name="help_circle" size={mainStyles.sizeButton} color="black"/> : <GeneralIcons name="help_circle_outline" size={mainStyles.sizeButton} color="black"/> } </TouchableOpacity> </CustomTooltip> <RemainingUsage productId='global_semantic_search'> <AiSearchButton query={query} handleFunction={(query)=>paidFunction(()=>handleAISearchByFileContent(query), 'global_semantic_search')} iconName="ai_search" helpDescription={t('contentSemanticSearchButtonDescriptionOnMultifileScreen')+"\n\n"+t("Feature name:\n")+"Global Semantic Search"} accessibilityLabel={t("contentSemanticSearchLabelOnMultifileScreen")} iconColor={'black'} /> </RemainingUsage> <RemainingUsage productId='file_name_semantic_search'> <AiSearchButton query={query} handleFunction={(query)=>paidFunction(()=>handleAiLiteSearchByFileName(query), 'file_name_semantic_search')} iconName="ai_search_lite" helpDescription={t('namesSemanticSearchButtonDescriptionOnMultifileScreen')+"\n\n"+t("Feature name:\n")+"File Name Semantic Search"} accessibilityLabel={t("namesSemanticSearchLabelOnMultifileScreen")} iconColor={'black'} /> </RemainingUsage> <CustomTooltip text={t('contentSearchButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.bigMenuButton} onPress={()=>handleSearchByFileContent(query)} accessibilityLabel={t("contentSearchLabelOnMultifileScreen")}> <DocumentsSearchIcons name="search" size={mainStyles.newSizeButton} color="black"/> </TouchableOpacity> </CustomTooltip> <CustomTooltip text={t('closeSearchPanelButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.button} onPress={closeSearchPanel} accessibilityLabel={t('closeSearchPanelLabelOnMultifileScreen')}> <GeneralIcons name="close" size={mainStyles.sizeButton} color="black"/> </TouchableOpacity> </CustomTooltip> </View> </View> ); } else { return( <View style={mainStyles.menuPanel}> <CustomTooltip text={t('helpButtonDescriptionOnMultifileScreen') + getApplicationVersion()} isTooltipSwitch={true}> <TouchableOpacity style={mainStyles.bigMenuButton} onPress={()=>{setRerender(r=>!r)}} accessibilityLabel={t('helpLabelOnMultifileScreen')}> {CustomTooltip.getIsHelpMode() ? <GeneralIcons name="help_circle" size={mainStyles.sizeButton} color="black"/> : <GeneralIcons name="help_circle_outline" size={mainStyles.sizeButton} color="black"/> } </TouchableOpacity> </CustomTooltip> <CustomTooltip text={t('searchPanelButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.bigMenuButton} onPress={openSearchPanel} accessibilityLabel={t("searchPanelLabelOnMultifileScreen")}> <DocumentsSearchIcons name="multipal_document_search" size={mainStyles.newSizeButton} color="black"/> </TouchableOpacity> </CustomTooltip> <CustomTooltip text={t('createNewFileButtonDesription')}> <TouchableOpacity style={mainStyles.bigMenuButton} onPress={createNewFileHandler} accessibilityLabel={t('createNewLabel')}> <EditorIcons name="new_file" size={mainStyles.mySizeButton} color="black"/> </TouchableOpacity> </CustomTooltip> <CustomTooltip text={t('importFileButtonDescription')}> <TouchableOpacity style={mainStyles.bigMenuButton} onPress={importFileHandler} accessibilityLabel={t('importFileLabel')}> <DocumentsSearchIcons name="import" size={mainStyles.newSizeButton} color="black"/> </TouchableOpacity> </CustomTooltip> <CustomTooltip text={t('openFileButtonDescription')}> <TouchableOpacity style={mainStyles.bigMenuButton} onPress={openFileHandler} accessibilityLabel={t('openFileLabel')}> <EditorIcons name="folder" size={mainStyles.sizeButton} color="black"/> </TouchableOpacity> </CustomTooltip> {user ? <CustomTooltip text={t('profileButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.bigMenuButton} onPress={signOut} accessibilityLabel={t("profileLabelOnMultifileScreen")}> <Image source={{ uri: user.photo }} style={multiStyles.profileAvatar} /> </TouchableOpacity> </CustomTooltip> : <CustomTooltip text={t('signInButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.bigMenuButton} onPress={signIn} accessibilityLabel={t("signInLabelOnMultifileScreen")}> <DocumentsSearchIcons name="log_in" size={mainStyles.sizeButton} color="black"/> </TouchableOpacity> </CustomTooltip>} </View> ); } } else { return( <> <View style={mainStyles.menuPanel}> <CustomTooltip text={t('selectHelpButtonDescriptionOnMultifileScreen')}isTooltipSwitch={true}> <TouchableOpacity style={mainStyles.button} onPress={()=>{setRerender(r=>!r)}} accessibilityLabel={t('helpLabelOnMultifileScreen')}> {CustomTooltip.getIsHelpMode() ? <GeneralIcons name="help_circle" size={mainStyles.sizeButton} color="black"/> : <GeneralIcons name="help_circle_outline" size={mainStyles.sizeButton} color="black"/> } </TouchableOpacity> </CustomTooltip> <CustomTooltip text={t('fromSearchDeleteButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.button} onPress={deleteSelectedFilesFromList} accessibilityLabel={t('fromSearchDeleteLabelOnMultifileScreen')}> <GeneralIcons name="delete" size={mainStyles.sizeButton} color="black"/> </TouchableOpacity> </CustomTooltip> <CustomTooltip text={t('fromSearchShareButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.button} onPress={shareSelectedFilesFromList} accessibilityLabel={t('fromSearchShareLabelOnMultifileScreen')}> <GeneralIcons name="share" size={mainStyles.sizeButton} color="black"/> </TouchableOpacity> </CustomTooltip> {isAiSearchableButton()} <CustomTooltip text={t('deselectButtonDescriptionOnMultifileScreen')}> <TouchableOpacity style={mainStyles.button} onPress={clearSelection} accessibilityLabel={t('deselectLabelOnMultifileScreen')}> <GeneralIcons name="close" size={mainStyles.sizeButton} color="black"/> </TouchableOpacity> </CustomTooltip> </View> </> ); } }; async function clearSelection() { await new Promise((resolve)=>{ setSearchList((searchList)=>{ selectedFilesRef.current.map((index)=>{ searchList.splice(index, 1, {...searchList[index], isSelected: false}) }); resolve(); return searchList; }); }) selectedFilesRef.current = []; setRerender(r=>!r); } const selectedFilesRef = useRef([]); function selectFile(index) { setSearchList((searchList)=>{ if(searchList[index].isSelected) { searchList.splice(index, 1, {...searchList[index], isSelected: false}) const i = selectedFilesRef.current.indexOf(index); selectedFilesRef.current.splice(i, 1); } else { searchList.splice(index, 1, {...searchList[index], isSelected: true}) selectedFilesRef.current.push(index); } return searchList; }); setRerender(r=>!r); return; } async function openCachedVersion(item, reason, threadId) { if (threadId !== pressOnThreadId.current) return; const cachedContent = await readFromCache(item.fileId); if (cachedContent !== null) { const cachedModifiedTime = await getCacheUpdateTime(item.fileId); const shouldContinue = await new Promise((resolve) => { CustomAlert.alert( t('No Online Access'), t("The last saved version from the cache will be opened."), [ { text: t('Cancel'), onPress: () => resolve(false) }, { text: t('Continue'), onPress: () => resolve(true) } ], { cancelable: true, onDismiss: () => resolve(false) } ); }); if (shouldContinue && threadId === pressOnThreadId.current) { const file = new AppFile({ id: item.fileId, type: FileType.G_DRIVE_FILE, relativePath: item.relativeFilePath, content: cachedContent, modificationTime: cachedModifiedTime, email: item.email, ancestors: item.ancestors }); const localQuery = currentSearchResultType.current === SearchResultTypes.CONTENT || currentSearchResultType.current === SearchResultTypes.AI ? query : ''; const isAiSearch = currentSearchResultType.current === SearchResultTypes.AI; navigation.navigate( 'TextEditor', { file: file.toPlainObject(), query: localQuery, firstCharIndex: item.firstCharIndex, isAiSearch: isAiSearch, otherSearchResults: item.otherSearchResults } ); } } else { CustomAlert.alert( t('No Access'), `${t("This file could not be opened because:")} \"${reason}\". ${t("In addition, there is no cached version of the file.")}`, [], { cancelable: true } ); } if (threadId === pressOnThreadId.current) { setSearchList(searchList => searchList.map(it => it.fileId === item.fileId ? { ...it, isOpening: false } : it ) ); } } //Used to terminate irrelevant threads of ‘handlePressOnGDriveFile’ function execution const pressOnThreadId = useRef(0); async function handlePressOnGDriveFile(item) { const threadId = (pressOnThreadId.current = performance.now()); try { // Indicate the file is opening setSearchList(searchList => searchList.map(it => it.fileId === item.fileId ? { ...it, isOpening: true } : it ) ); // Check if user is signed in to the correct account if (userRef.current === null || userRef.current.email !== item.email) { await openCachedVersion(item, t('You are not signed in to the corresponding Google account'), threadId); return; } // Check internet connection const state = await fetch(); const isConnected = state.isConnected; if (!isConnected) { await openCachedVersion(item, t('There is no internet connection'), threadId); return; } // Collect file and ancestor IDs const ancestorsIds = item.ancestors ? item.ancestors.map(a => a.id) : []; // Fetch metadata for the file and its ancestors const filesDetails = await gdriveFileManager.getFilesDetails(ancestorsIds); // Calculate updated relativeFilePath and ancestors const { relativeFilePath, ancestors } = await GDriveFileCacheManager.getRelativeFilePathById( item.fileId, filesDetails, new Set() ); // Update database if relativeFilePath changed if (relativeFilePath !== item.relativeFilePath) { updateRelativeFilePath(item.fileId, relativeFilePath); const oldName = getFileName(FileType.G_DRIVE_FILE, relativeFilePath); const newName = getFileName(FileType.G_DRIVE_FILE, item.relativeFilePath); if (oldName !== newName) { updateFileNameEmbedding(item.fileId, ""); } } // Get file content with the latest modified time const { content, modificationTime } = await GDriveFileCacheManager.getFileContentAndModifiedTime( item.fileId, filesDetails[item.fileId]?.modifiedTime ); // Check if the thread is still active if (threadId !== pressOnThreadId.current) { setSearchList(searchList => searchList.map(it => it.fileId === item.fileId ? { ...it, isOpening: false } : it ) ); return; } // Optionally update search list with new path and ancestors setSearchList(searchList => searchList.map(it => it.fileId === item.fileId ? { ...it, relativeFilePath, ancestors } : it ) ); const file = new AppFile({id:item.fileId, type:FileType.G_DRIVE_FILE, relativePath:relativeFilePath, content, modificationTime, email:item.email, ancestors}); const localQuery = currentSearchResultType.current === SearchResultTypes.CONTENT || currentSearchResultType.current === SearchResultTypes.AI ? query : ''; const isAiSearch = currentSearchResultType.current === SearchResultTypes.AI; navigation.navigate( 'TextEditor', { file: file.toPlainObject(), query:localQuery, firstCharIndex:item.firstCharIndex, isAiSearch: isAiSearch, otherSearchResults: item.otherSearchResults } ); } catch (error) { //It is used to prevent an error from popping up if the thread has changed. if (threadId !== pressOnThreadId.current) { setSearchList(searchList => searchList.map(it => it.fileId === item.fileId ? { ...it, isOpening: false } : it ) ); return; } logError("Error in handlePressOnGDriveFile: ", error); switch (error.message) { case 'Network request failed': case 'NETWORK_ERROR': CustomAlert.alert('',t('Could not open this file, no internet connection.'), [], { cancelable: true }); break; case 'File not found': case 'Access denied': if(error.message ==='File not found') CustomAlert.alert('',t('Failed to read the file, it no longer exists.'), [], { cancelable: true }); else CustomAlert.alert('',t('The file cannot be read, it is in a location to which you do not have access rights.'), [], { cancelable: true }); //if the user is not just logged into the appropriate Google Account, the file will be not deleted. if(item.email === userRef.current.email) { deleteFromBase(item.fileId); GDriveFileCacheManager.deleteFromCache(item.fileId); setSearchList(prev => prev.filter(it => it.fileId !== item.fileId)); setRerender(r => !r); } break; case 'Sign in action cancelled': case 'No user to be signed in': CustomAlert.alert(t(''),t("You must be logged into the appropriate Google Account to open this file."), [], { cancelable: true }); break; case 'OutOfMemoryError': CustomAlert.alert(t(''),t("This file is too large and the application cannot open it at this time."), [], { cancelable: true }); break; default: CustomAlert.alert(t('Error'),t('For some reason this file could not open.'), [], { cancelable: true }); break; } } //It works if everything is completed normally if (threadId === pressOnThreadId.current) { setSearchList(searchList => searchList.map(it => it.fileId === item.fileId ? { ...it, isOpening: false } : it ) ); pressOnThreadId.current = 0; } } async function handlePressOnLocalFile(item) { pressOnThreadId.current = performance.now(); const file = new AppFile({id:item.fileId, type:item.fileType, relativePath:item.relativeFilePath, email:item.email}); try { if(item.fileType !== FileType.SHARED_FILE || await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE)) { const localQuery = currentSearchResultType.current === SearchResultTypes.CONTENT || currentSearchResultType.current === SearchResultTypes.AI ? query : ''; const isAiSearch = currentSearchResultType.current === SearchResultTypes.AI ? true : false; navigation.navigate( 'TextEditor', { file:file.toPlainObject(), query:localQuery, firstCharIndex:item.firstCharIndex, isAiSearch: isAiSearch, otherSearchResults: item.otherSearchResults } ); } else { Toast.show({ type: 'errorToast', text1: t('You have not granted permission to open this file.'), visibilityTime: 4000, topOffset:30, }); } } catch (error){ logError(error); CustomAlert.alert(t('Error'),t('Something went wrong, failed to open this file.'), [], { cancelable: true }); } } async function handlePress(item, index) { if(selectedFilesRef.current.length===0) { if(!CustomTooltip.getIsHelpMode()) { if(item.fileType === FileType.G_DRIVE_FILE){ await handlePressOnGDriveFile(item); }else { await handlePressOnLocalFile(item); } } } else { selectFile(index); } } const renderItem = ({ item, index }) => { return( <SearchedItem index = {index} item = {item} onLongPress={() => selectFile(index)} onPress={() => handlePress(item, index)} headStyle = {[item.isError && multiStyles.errorItemHead, item.isSelected && multiStyles.selectItemHead]} bodyStyle = {[item.isError && multiStyles.errorItemBody, item.isSelected && multiStyles.selectItemBody]} bottomStyle = {[item.isError && multiStyles.errorItemBottom, item.isSelected && multiStyles.selectItemBottom]} /> ); }; const keyExtr = (item) => { return item.fileId + item.matchIndex; }; const [isLoading, setIsLoading] = useState(false); const showList = ()=>{ return( <View style={mainStyles.viewForScrollView}> <View style={mainStyles.viewViewForScrollView}> <FlatList inverted horizontal={false} contentContainerStyle={multiStyles.flatListContent} style={multiStyles.flatList} keyExtractor={keyExtr} data={searchList} renderItem={renderItem} ref={flatListRef} /> </View> {isLoading && <View style={{width:'100%', height:'100%', position: 'absolute', justifyContent: "center",}}> <ActivityIndicator size={adjustSize(100)} color="#888" /> </View>} </View> ); } function showNoFilesNotification(){ let notification = null; if(isSearching) { if(currentSearchResultType.current === SearchResultTypes.ALL) { notification = <Text style={multiStyles.noFilesNotificationText}>{t('Loading...')}</Text>; } else { notification = <Text style={multiStyles.noFilesNotificationText}>{t('Searching...')}</Text>; } } else { if(currentSearchResultType.current === SearchResultTypes.ALL) { notification = <View style={multiStyles.firstBrief}> <View style={multiStyles.firstBriefTop}> <Text style={multiStyles.noFilesNotificationText}>{t('Click on ')}</Text> <GeneralIcons name='help_circle_outline' size={30} color="#6b6b6b"/> <Text style={multiStyles.noFilesNotificationText}>{t(',')}</Text> </View> <Text style={multiStyles.noFilesNotificationText}>{t('to know how to use the app.')}</Text> </View>; } else if (currentSearchResultType.current === null) { notification = null; } else { notification = <Text style={multiStyles.noFilesNotificationText}>{t('No search results')}</Text>; } } return ( <View style={multiStyles.noFilesNotificationContainer}> {notification} {isLoading && <View style={{width:'100%', height:'100%', position: 'absolute', justifyContent: "center",}}> <ActivityIndicator size={adjustSize(100)} color="#888" /> </View>} </View> ); } const memoizedList = React.useMemo(()=>{ if(searchList.length===0) { return showNoFilesNotification(); } else { return showList(); } },[searchList, rerender, isSearching, isLoading]); const memoizedMenuPanel = React.useMemo(()=>{ return showMenuPanel(); },[user, rerender, query, isSearchMode]); return ( <SafeAreaView style={mainStyles.container}> <View style={{borderColor:"#ccc", borderBottomWidth:1}}> <MyStatusBar /> </View> {memoizedList} <MiniProgressBar ref={miniProgress}/> <View style={{width:'100%', minHeight:2, height:adjustSize(2), backgroundColor:'#eee'}}/> <ProgressBar/> {memoizedMenuPanel} <PaymentModals paymentManager={paymentManager} /> <UserInfoWindow t={t}/> <Toast config={toastConfig}/> </SafeAreaView> );
};
Please pay attention to the shareSelectedFilesFromList function. When I try to run this function I get the following error message from logcat and then my app crashes:
09-10 01:33:30.816 5566 5566 E AndroidRuntime: FATAL EXCEPTION: main
09-10 01:33:30.816 5566 5566 E AndroidRuntime: Process: android:ui, PID: 5566
09-10 01:33:30.816 5566 5566 E AndroidRuntime: java.lang.NullPointerException: url
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at com.android.internal.util.Preconditions.checkNotNull(Preconditions.java:133)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at android.content.ContentResolver.getType(ContentResolver.java:716)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at com.android.internal.app.ChooserActivity.displayImageContentPreview(ChooserActivity.java:901)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at com.android.internal.app.ChooserActivity.displayContentPreview(ChooserActivity.java:821)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at com.android.internal.app.ChooserActivity.accessChooserRowAdapter.createContentPreviewView(ChooserActivity.java:3030)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at com.android.internal.app.ChooserActivityTraversalRunnable.run(ViewRootImpl.java:7602)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at android.view.ChoreographerFrameDisplayEventReceiver.run(Choreographer.java:1014)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:883)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:100)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at android.os.Looper.loop(Looper.java:214)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:7397)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
09-10 01:33:30.816 5566 5566 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:935)
Also I suppose you would like to take a look at the content of this file:
//android\app\src\main\AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"></manifest>text<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="com.android.vending.BILLING" /> <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:supportsRtl="true"> <activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/*" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.SEND" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="application/json" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:mimeType="text/plain" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:mimeType="text/*" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:mimeType="application/json" /> </intent-filter> </activity> </application>
And take a look at the content of this file:
// myLibs\file_manager\LocalFileManager.ts
import { FileSystem } from "react-native-file-access";
import { FileManager, FileMetadata } from './FileManager';
import { logError } from "../Logging";
export class LocalFileManager implements FileManager {
text/** * List files in a local directory. * @param parentPath - The path of the parent directory. */ async listFiles(parentPath: string): Promise<FileMetadata[]> { throw new Error('The listFiles method is not implemented'); } /** * Create a new local directory. * @param name - Name of the new folder. * @param parentPath - Path of the parent directory. */ async createFolder(name: string, parentPath: string): Promise<string> { //const newPath = `${parentPath}/${name}`; const newPath = `${parentPath}${name}`; await FileSystem.mkdir(newPath); return newPath; } /** * Create a new local file. * @param name - Name of the new file. * @param parentPath - Path of the parent directory. */ async createFile(name: string, parentPath: string): Promise<string> { const filePath = `${parentPath}/${name}`; await FileSystem.writeFile(filePath, ''); return filePath; } /** * Delete a local file or directory. * @param path - Path of the file or directory to delete. */ async deleteFile(path: string): Promise<void> { await FileSystem.unlink(path); } /** * Write content to a local file. * @param path - Path of the file. * @param content - Content to write. */ async uploadContent(path: string, content: string): Promise<number> { await FileSystem.writeFile(path, content, 'utf8'); return await this.getFileModifiedTime(path); } /** * Rename a local file and return its modification time. * @param path - The current path of the file. * @param newName - The new name for the file (without path separators). * @returns A promise that resolves to the file's last modified time (timestamp in milliseconds). * @throws Error if the path is invalid, the root directory is targeted, or the new name contains '/'. */ async renameFile(path: string, newName: string): Promise<number> { // Prevent renaming the root directory if (path === '/') { throw new Error('Cannot rename root directory'); } // Ensure newName is a simple name without path separators if (newName.includes('/')) { throw new Error('New name cannot contain "/"'); } // Get the parent directory of the current path const parentPath = this.getParentPath(path); // Construct the new file path const newPath = `${parentPath}/${newName}`; try { // Rename the file by moving it to the new path await FileSystem.mv(path, newPath); // Get the file stats after renaming const stat = await FileSystem.stat(newPath); // Return the modification time return stat.lastModified; } catch (error: any) { logError(error) const errorMessage:string = error.message; if(errorMessage.includes("The source file doesn't exist")) { throw new Error("File not found"); } throw error; } } /** * Copy a local file to a new location. * @param sourcePath - The path of the source file. * @param targetPath - The path where the file should be copied to. * @throws Error if the source and target paths are the same, the source file does not exist, * the source is not a file, or if the copy operation fails. */ async copyFile(sourcePath: string, targetPath: string): Promise<void> { // Prevent copying a file to itself if (sourcePath === targetPath) { throw new Error('Source and target paths are the same'); } // Check if source exists const sourceExists = await FileSystem.exists(sourcePath); if (!sourceExists) { throw new Error('Source file not found'); } // Verify source is a file, not a directory const sourceStat = await FileSystem.stat(sourcePath); if (sourceStat.type !== 'file') { throw new Error('Source is not a file'); } // Perform the copy operation try { await FileSystem.cp(sourcePath, targetPath); } catch (error: any) { logError(error); throw error; } } /** * Extract the parent directory path from a file path. * @param path - The full path of the file. * @returns The parent directory path. * @throws Error if the path is invalid. */ private getParentPath(path: string): string { // Remove trailing slash if present (e.g., '/path/to/dir/' becomes '/path/to/dir') const trimmed = path.endsWith('/') ? path.slice(0, -1) : path; // Find the last slash to separate directory and file name const lastSlashIndex = trimmed.lastIndexOf('/'); // Validate the path if (lastSlashIndex === -1) { throw new Error('Invalid path'); } // If the slash is at the start (root level), return '/' if (lastSlashIndex === 0) { return '/'; } // Return the parent directory return trimmed.substring(0, lastSlashIndex); } /** * Read content from a local file. * @param path - Path of the file. * @returns The file content as a string. */ async getFileContent(path: string): Promise<string> { try{ return await FileSystem.readFile(path, 'utf8'); } catch(error:any) { if(error.message.includes('OOM')) { throw new Error('OutOfMemoryError'); } else { throw error; } } } async getFileStartContent(path: string, textLength: number) { const chunk = await FileSystem.readFileChunk(path, 0, textLength * 2, 'utf8'); return chunk.slice(0, textLength); } /** * Get the last modified time of a local file. * @param path - Path of the file. * @returns The modified time as a timestamp. */ async getFileModifiedTime(path: string): Promise<number> { const stat = await FileSystem.stat(path); return stat.lastModified; } /** * Check if a local file exists. * @param path - The path of the file. * @returns True if the file exists, false otherwise. */ async fileExists(path: string): Promise<boolean> { return await FileSystem.exists(path); } async getFileDetails(path: string): Promise<{modifiedTime: number, size: number, type: 'directory' | 'file'}> { const stats = await FileSystem.stat(path); return {modifiedTime: stats.lastModified, size: stats.size, type: stats.type} } /** * Lists all files and directories within a specified folder. * * @param {string} dirId - The directory ID or path to list contents from * @returns {Promise<object[]>} Array of file/directory objects with the following properties: * - id {string} - Full path of the file/directory * - name {string} - Name of the file/directory * - isDir {boolean} - Whether the item is a directory * - modifiedTime {number} - Last modified timestamp * - size {number} - Size in bytes * @throws {Error} If the directory cannot be read or does not exist */ async listFilesInFolder(dirId: string): Promise<object[]> { const files = await FileSystem.statDir(dirId); return files.map(file => ({ id: file.path, name: file.filename, isDir: file.type === 'directory', modifiedTime: file.lastModified, size: file.size })); } /** * * @param ids * @returns A promise that resolves to an object where the keys are file IDs * and the values are the file ditails. */ async getFilesDetails(ids: string[]): Promise<object> { throw new Error('The method getFileModifiedTimes in LocalFileManager is not implemented yet') }
}
Please make it so that I can share my files with other apps again.
P.S. I think that the file URIs being passed to the native share intent are not in a format that the system can handle.
Root cause
/data/user/0/<package>/files/Temp/… and pass the path to
react-native-share as a file://… URI.content://… URI that is backed by a FileProvider.FileProvider.getUriForFile() returns null.Intent, and the
system UI crashes with the stack-trace you posted.xml<!-- android/app/src/main/AndroidManifest.xml --> <application … > … <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/file_paths"/> </provider> </application>
Create android/app/src/main/res/xml/file_paths.xml
xml<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <!-- everything under getFilesDir() --> <files-path name="files" path="." /> <!-- the cache directory you already use for Temp/share_xxx --> <cache-path name="cache" path="." /> <!-- if you ever move the temp folder to external cache --> <external-cache-path name="ext" path="." /> </paths>
Now the OS is allowed to serve
content://<package>.fileprovider/files/Temp/… to other apps.
Replace the final call in shareSelectedFilesFromList with:
tsawait Share.open({ title: 'Share selected files', type : 'text/plain', // common MIME type urls : shareUris, // still "file://…" paths useInternalStorage: true,/* <-- critical on Android */ failOnCancel: false });
With useInternalStorage: true the library takes each file://
path, converts it to a content URI via
FileProvider.getUriForFile(…, "${applicationId}.fileprovider", file)
and adds FLAG_GRANT_READ_URI_PERMISSION so the receiver can open it.
If you prefer you can copy the files to
tsconst tempDir = `${FileSystem.CachesDirectoryPath}/share_${Date.now()}`;
which already matches the <cache-path> you declared.
After these three (or four) tweaks the chooser no longer crashes and the same code successfully shares your text files with Gmail, WhatsApp, Google Drive, etc.