Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request for app permissions, remove restore errors #1096

Merged
merged 1 commit into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />

<!-- Storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
Expand Down Expand Up @@ -41,7 +37,6 @@

<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
Expand All @@ -59,7 +54,6 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lnreader" />
<data android:scheme="com.rajarsheechatterjee.LNReader" />
</intent-filter>
</activity>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.rajarsheechatterjee.FileManager

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.DocumentsContract
import android.util.Base64
import com.facebook.react.bridge.BaseActivityEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
Expand All @@ -29,9 +31,37 @@ class FileManager(context: ReactApplicationContext) :
return "FileManager"
}

private var _promise: Promise? = null
private val coroutineScope = MainScope()
private val activityEventListener = object : BaseActivityEventListener() {
override fun onActivityResult(
activity: Activity?,
requestCode: Int,
resultCode: Int,
intent: Intent?
) {
if (requestCode == FOLDER_PICKER_REQUEST) {
when (resultCode) {
Activity.RESULT_CANCELED -> resolveUri()
Activity.RESULT_OK -> {
if (intent != null) {
intent.data?.let { uri ->
val flags =
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
resolveUri(uri)
}
}
}
}
}
}
}

init {
context.addActivityEventListener(activityEventListener)
}

@Throws(Exception::class)
private fun getFileUri(filepath: String): Uri {
var uri = Uri.parse(filepath)
if (uri.scheme == null) {
Expand All @@ -45,7 +75,6 @@ class FileManager(context: ReactApplicationContext) :
return uri
}

@Throws(Exception::class)
private fun getInputStream(filepath: String): InputStream {
val uri = getFileUri(filepath)
return reactApplicationContext.contentResolver.openInputStream(uri)
Expand Down Expand Up @@ -93,7 +122,6 @@ class FileManager(context: ReactApplicationContext) :
}
}

@SuppressLint("StaticFieldLeak")
@ReactMethod
fun copyFile(filepath: String, destPath: String, promise: Promise) {
try {
Expand All @@ -103,7 +131,6 @@ class FileManager(context: ReactApplicationContext) :
}
}

@SuppressLint("StaticFieldLeak")
@ReactMethod
fun moveFile(filepath: String, destPath: String, promise: Promise) {
try {
Expand Down Expand Up @@ -138,24 +165,6 @@ class FileManager(context: ReactApplicationContext) :
}
}

@ReactMethod
fun resolveExternalContentUri(uriString: String, promise: Promise) {
val uri = Uri.parse(uriString)
try {
val docId = DocumentsContract.getTreeDocumentId(uri)
val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if ("primary" == split[0]) {
promise.resolve(
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
)
} else {
promise.resolve(null)
}
} catch (e: Exception) {
promise.resolve(null)
}
}

@ReactMethod
fun exists(filepath: String, promise: Promise) {
try {
Expand Down Expand Up @@ -231,4 +240,36 @@ class FileManager(context: ReactApplicationContext) :
}
return constants
}

private fun resolveUri(uri: Uri? = null) {
_promise?.let { promise ->
try {
if (uri == null) promise.resolve(null)
else {
val docId = DocumentsContract.getTreeDocumentId(uri)
val split =
docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
promise.resolve(
Environment.getExternalStorageDirectory().toString() + "/" + split[1]
)
}
} catch (e: Exception) {
promise.reject(e)
} finally {
_promise = null
}
}
}

@ReactMethod
fun pickFolder(promise: Promise) {
if (_promise != null) promise.reject(Exception("Can not perform action"))
_promise = promise
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
currentActivity?.startActivityForResult(intent, FOLDER_PICKER_REQUEST)
}

companion object {
const val FOLDER_PICKER_REQUEST = 1
}
}
1 change: 1 addition & 0 deletions src/native/FileManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface FileManagerInterface {
mkdir: (filePath: string) => Promise<void>; // create parents;
unlink: (filePath: string) => Promise<void>; // remove recursively
readDir: (dirPath: string) => Promise<ReadDirResult[]>; // file/sub-folder names
pickFolder: () => Promise<string | null>; // return path of folderc
ExternalDirectoryPath: string;
ExternalCachesDirectoryPath: string;
}
Expand Down
131 changes: 106 additions & 25 deletions src/screens/onboarding/StorageStep.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useTheme } from '@hooks/persisted';
import { StyleSheet, Text, View } from 'react-native';
import { PermissionsAndroid, StyleSheet, View } from 'react-native';
import { Button } from '@components';
import { StorageAccessFramework } from 'expo-file-system';
import FileManager from '@native/FileManager';
import { showToast } from '@utils/showToast';
import { Text } from 'react-native-paper';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';

interface StorageStepProps {
rootStorage: string;
Expand All @@ -15,34 +16,105 @@ export default function StorageStep({
rootStorage,
onPathChange,
}: StorageStepProps) {
const [granted, setGranted] = useState<boolean | undefined>(undefined);
const theme = useTheme();
const pickFolder = () => {
StorageAccessFramework.requestDirectoryPermissionsAsync().then(res => {
if (res.granted) {
FileManager.resolveExternalContentUri(res.directoryUri).then(path => {
if (path) {
FileManager.readDir(path).then(res => {
if (res.length > 0) {
showToast('Please select an empty folder');
} else {
onPathChange(path);
}
});
FileManager.pickFolder().then(path => {
if (path) {
FileManager.readDir(path).then(res => {
if (res.length > 0) {
showToast('Please select an empty folder');
} else {
onPathChange(path);
}
});
}
});
};
const grantPermissions = () => {
PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
]).then(value => {
if (
value['android.permission.READ_EXTERNAL_STORAGE'] === 'granted' &&
value['android.permission.WRITE_EXTERNAL_STORAGE'] === 'granted'
) {
setGranted(true);
} else {
setGranted(false);
}
});
};
useEffect(() => {
Promise.all([
PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
),
PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
),
])
.then(([readGranted, writeGRanted]) => {
if (readGranted && writeGRanted) {
setGranted(true);
} else {
setGranted(false);
}
})
.catch(() => {
setGranted(false);
});
}, []);
return (
<View style={{ paddingHorizontal: 16 }}>
<Text style={[styles.text, { color: theme.onSurfaceVariant }]}>
Select an empty folder.
</Text>
{rootStorage ? (
<Text style={[styles.text, { color: theme.onSurfaceVariant }]}>
Selected folder: {rootStorage}
</Text>
) : null}
<View style={styles.section}>
<View>
<Text style={[styles.title, { color: theme.onSurface }]}>
App permissions
</Text>
<Text style={[{ color: theme.onSurfaceVariant }]}>
Read and write external storage.
</Text>
</View>
<Button
mode="outlined"
compact
style={styles.grantButton}
loading={granted === undefined}
onPress={() => {
if (granted === false) {
grantPermissions();
}
}}
>
{granted === true ? (
<MaterialCommunityIcons name="check" size={24} />
) : (
<Text style={{ color: theme.primary, fontWeight: '600' }}>
{granted === undefined ? 'Checking' : 'Allow'}
</Text>
)}
</Button>
</View>
<View style={styles.section}>
<View>
<Text style={[styles.title, { color: theme.onSurface }]}>
Select an empty folder.
</Text>
{rootStorage ? (
<Text
style={{
color: theme.onSurfaceVariant,
fontSize: 12,
fontWeight: '500',
}}
>
Selected folder: {rootStorage}
</Text>
) : null}
</View>
</View>

<Button
labelStyle={{
Expand All @@ -51,15 +123,24 @@ export default function StorageStep({
title="Select a folder"
mode="contained"
onPress={pickFolder}
disabled={!granted}
/>
</View>
);
}

const styles = StyleSheet.create({
text: {
section: {
flexDirection: 'row',
marginBottom: 16,
fontSize: 12,
fontWeight: '500',
justifyContent: 'space-between',
},
title: {
fontSize: 16,
fontWeight: '600',
},
grantButton: {
alignSelf: 'center',
paddingHorizontal: 8,
},
});
10 changes: 0 additions & 10 deletions src/screens/settings/SettingsBackupScreen/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import SelfHostModal from './Components/SelfHostModal';
import {
createBackup as deprecatedCreateBackup,
restoreBackup as deprecatedRestoreBackup,
restoreError as deprecatedRestoreError,
} from '@services/backup/legacy';
import { ScrollView } from 'react-native-gesture-handler';
import { BACKGROUND_ACTION } from '@services/constants';
Expand Down Expand Up @@ -84,15 +83,6 @@ const BackupSettings = ({ navigation }: BackupSettingsScreenProps) => {
theme={theme}
disabled={Boolean(hasAction)}
/>
<List.Item
title={`${getString('backupScreen.restoreError')} (${getString(
'common.deprecated',
)})`}
description={getString('backupScreen.restoreErrorDesc')}
onPress={deprecatedRestoreError}
theme={theme}
disabled={Boolean(hasAction)}
/>
<List.InfoItem
title={getString('backupScreen.restoreLargeBackupsWarning')}
icon="information-outline"
Expand Down
Loading
Loading