Skip to content

Commit

Permalink
Merge pull request #42 from LolHens/feature/android11
Browse files Browse the repository at this point in the history
Android 11 Support
  • Loading branch information
LolHens committed Nov 16, 2021
2 parents a0d6693 + 6be60f8 commit ab89c45
Show file tree
Hide file tree
Showing 17 changed files with 167 additions and 99 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,20 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Keystore
run: echo "${{ secrets.RELEASE_STORE_BASE64}}" | base64 -d > keystore.jks
run: |
[ "${{ secrets.RELEASE_STORE_BASE64}}" != "" ] && echo "${{ secrets.RELEASE_STORE_BASE64}}" | base64 -d > keystore.jks
- name: Build with Gradle
env:
RELEASE_STORE_PASSWORD: ${{ secrets.RELEASE_STORE_PASSWORD }}
RELEASE_KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }}
RELEASE_KEY_PASSWORD: ${{ secrets.RELEASE_KEY_PASSWORD }}
run: |
export RELEASE_STORE_FILE="$(readlink -f keystore.jks)"
./gradlew assembleRelease
if [ -e keystore.jks ]; then
./gradlew assembleRelease
else
./gradlew assemble
fi
- uses: actions/upload-artifact@v2
with:
path: 'app/build/outputs/apk/release/*.apk'
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ Please report any issues on the [restic-android issue tracker](https://github.co
- More granular Backup Schedules and Cleanup Policies
- Improve Error messages
- Backup Rules (only backup when charging or only use wifi etc.)
- Figure out how to do storage access on Android 11

## Screenshots
![](https://raw.githubusercontent.com/LolHens/restic-android/main/screenshots/repos.png)
Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0")
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

<application
android:extractNativeLibs="true"
Expand Down
13 changes: 5 additions & 8 deletions app/src/main/java/de/lolhens/resticui/BackupManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,16 @@ class BackupManager private constructor(context: Context) {
},
activeBackup.cancelFuture
).handle { summary, throwable ->
val throwable = if (throwable == null) null else {
if (throwable is CompletionException && throwable.cause != null) throwable.cause!!
val throwable =
if (throwable == null) null
else if (throwable is CompletionException && throwable.cause != null) throwable.cause
else throwable
}

val cancelled = throwable is ResticException && throwable.cancelled

val errorMessage =
if (throwable == null || cancelled) {
null
} else {
throwable.message
}
if (throwable == null || cancelled) null
else throwable.message

val historyEntry = BackupHistoryEntry(
timestamp = ZonedDateTime.now(),
Expand Down
14 changes: 10 additions & 4 deletions app/src/main/java/de/lolhens/resticui/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.lolhens.resticui

import android.Manifest
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
Expand Down Expand Up @@ -39,11 +39,17 @@ class MainActivity : AppCompatActivity() {

val backupManager = BackupManager.instance(applicationContext)

if (!Permissions.granted(applicationContext, Manifest.permission.READ_EXTERNAL_STORAGE)) {
Permissions.request(this, Manifest.permission.READ_EXTERNAL_STORAGE)
if (!Permissions.hasStoragePermission(applicationContext, write = false)) {
Permissions.requestStoragePermission(this, write = false)
.thenApply { granted ->
if (granted) {
backupManager.initRestic(applicationContext)
} else {
Toast.makeText(
this,
"Allow permission for storage access!",
Toast.LENGTH_SHORT
).show()
}
}
}
Expand All @@ -57,6 +63,6 @@ class MainActivity : AppCompatActivity() {
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
Permissions.onRequestPermissionsResult(permissions, grantResults)
Permissions.onRequestPermissionsResult(requestCode)
}
}
98 changes: 64 additions & 34 deletions app/src/main/java/de/lolhens/resticui/Permissions.kt
Original file line number Diff line number Diff line change
@@ -1,52 +1,82 @@
package de.lolhens.resticui

import android.app.Activity
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Environment
import android.provider.Settings
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentLinkedQueue


object Permissions {
fun granted(context: Context, permission: String): Boolean =
ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED

private var callbacks: Map<String, CompletableFuture<Boolean>> = emptyMap()

fun onRequestPermissionsResult(permissions: Array<out String>, grantResults: IntArray) {
val permission = permissions[0]
val granted =
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED

val callback = callbacks.get(permission)
if (callback != null) {
synchronized(this) {
callbacks = callbacks.minus(permission)
// https://stackoverflow.com/questions/62782648/android-11-scoped-storage-permissions
fun hasStoragePermission(context: Context, write: Boolean): Boolean =
if (SDK_INT >= Build.VERSION_CODES.R) {
Environment.isExternalStorageManager()
} else {
ContextCompat.checkSelfPermission(
context,
READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED &&
(!write || ContextCompat.checkSelfPermission(
context,
WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED)
}

callback.complete(granted)
}
}
private const val PERMISSION_REQUEST_CODE = 2290

fun request(
activity: Activity,
permission: String
): CompletableFuture<Boolean> =
if (granted(activity, permission)) {
CompletableFuture.completedFuture(true)
} else {
// Requesting the permission
val future = CompletableFuture<Boolean>()
private var legacyCallbacks: Queue<CompletableFuture<Unit>> = ConcurrentLinkedQueue()

synchronized(this) {
callbacks = callbacks.plus(Pair(permission, future))
fun requestStoragePermission(activity: ComponentActivity, write: Boolean): CompletableFuture<Boolean> {
val future = CompletableFuture<Unit>()

if (SDK_INT >= Build.VERSION_CODES.R) {
val resultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
future.complete(Unit)
}

ActivityCompat.requestPermissions(activity, arrayOf(permission), 0)
try {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
intent.addCategory("android.intent.category.DEFAULT")
intent.data = Uri.parse(String.format("package:%s", activity.packageName))
resultLauncher.launch(intent)
} catch (e: Exception) {
val intent = Intent()
intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
resultLauncher.launch(intent)
}
} else {
//below android 11
legacyCallbacks.offer(future)

future
ActivityCompat.requestPermissions(
activity,
arrayOf(WRITE_EXTERNAL_STORAGE),
PERMISSION_REQUEST_CODE
)
}

return future.thenApply {
hasStoragePermission(activity, write)
}
}

fun onRequestPermissionsResult(requestCode: Int) {
when (requestCode) {
PERMISSION_REQUEST_CODE ->
legacyCallbacks.poll()?.complete(Unit)
}
}
}
18 changes: 8 additions & 10 deletions app/src/main/java/de/lolhens/resticui/restic/Restic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,15 @@ class Restic(
}
}

if (cancel != null) {
cancel.thenRun {
future.completeExceptionally(
ResticException(
0,
emptyList(),
cancelled = true
)
cancel?.thenRun {
future.completeExceptionally(
ResticException(
0,
emptyList(),
cancelled = true
)
process.destroy()
}
)
process.destroy()
}

future
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,21 @@ data class ResticBackupProgress(
fun percentDone100() = percent_done * 100

fun percentDoneString() =
(if (percent_done == 0.0) "0"
else if (percentDone100() < 0.01) "%.4f".format(percentDone100())
else if (percentDone100() < 1) "%.2f".format(percentDone100())
else percentDone100().roundToInt().toString()) + "%"
(when {
percent_done == 0.0 -> "0"
percentDone100() < 0.01 -> "%.4f".format(percentDone100())
percentDone100() < 1 -> "%.2f".format(percentDone100())
else -> percentDone100().roundToInt().toString()
}) + "%"

private fun formatBytes(bytes: Long?) =
if (bytes == null) null
else if (bytes >= 1_000_000_000) "${"%.2f".format(bytes / 10_000_000 / 100.0)} GB"
else if (bytes >= 1_000_000) "${"%.2f".format(bytes / 10_000 / 100.0)} MB"
else if (bytes >= 1_000) "${"%.2f".format(bytes / 10 / 100.0)} KB"
else "$bytes B"
when {
bytes == null -> null
bytes >= 1_000_000_000 -> "${"%.2f".format(bytes / 10_000_000 / 100.0)} GB"
bytes >= 1_000_000 -> "${"%.2f".format(bytes / 10_000 / 100.0)} MB"
bytes >= 1_000 -> "${"%.2f".format(bytes / 10 / 100.0)} KB"
else -> "$bytes B"
}

fun totalBytesString() = formatBytes(total_bytes)
fun bytesDoneString() = formatBytes(bytes_done)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ data class ResticException(
val exitCode: Int,
val stderr: List<String>,
val cancelled: Boolean = false
) :
Exception("Restic error $exitCode:\n${stderr.joinToString("\n")}")
) : Exception("Restic error $exitCode:\n${stderr.joinToString("\n")}")
2 changes: 1 addition & 1 deletion app/src/main/java/de/lolhens/resticui/restic/ResticRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ abstract class ResticRepo(
private val filterJson = { line: String -> line.startsWith("{") || line.startsWith("[") }

val hostname: String by lazy {
BluetoothAdapter.getDefaultAdapter().name
BluetoothAdapter.getDefaultAdapter().name // TODO: handle D/BluetoothAdapter: java.lang.Throwable
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class ResticRepoS3(
restic,
password
) {

override fun repository(): String = "s3:$s3Url"

override fun hosts(): List<String> = listOf(s3Url.host)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package de.lolhens.resticui.restic

import android.Manifest
import android.content.Context
import android.os.Environment
import de.lolhens.resticui.Permissions
Expand All @@ -15,7 +14,7 @@ interface ResticStorage {
override fun lib(): File = _lib
override fun cache(): File = _cache
override fun storage(): List<File> {
if (!Permissions.granted(context, Manifest.permission.READ_EXTERNAL_STORAGE))
if (!Permissions.hasStoragePermission(context, write = false))
return emptyList()

val state = Environment.getExternalStorageState()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,20 @@ class FolderEditFragment : Fragment() {
override fun onOptionsItemSelected(item: MenuItem): Boolean =
when (item.itemId) {
R.id.action_done -> {
val selectedRepoName = binding.spinnerRepo.selectedItem.toString()
val selectedRepoName = binding.spinnerRepo.selectedItem?.toString()
val repo =
backupManager.config.repos.find { it.base.name == selectedRepoName }
if (selectedRepoName == null) null
else backupManager.config.repos.find { it.base.name == selectedRepoName }
val path = binding.editFolder.text.toString()
val schedule = binding.spinnerSchedule.selectedItem.toString()
val schedule = binding.spinnerSchedule.selectedItem?.toString()
val keepWithin =
if (retainProfiles[binding.spinnerRetainWithin.selectedItemPosition] < 0) null
else Duration.ofHours(retainProfiles[binding.spinnerRetainWithin.selectedItemPosition].toLong())

if (
repo != null &&
path.isNotEmpty()
path.isNotEmpty() &&
schedule != null
) {
val prevFolder = backupManager.config.folders.find { it.id == folderId }

Expand All @@ -174,9 +176,11 @@ class FolderEditFragment : Fragment() {
FolderActivity.start(this, false, folderId)

requireActivity().finish()
}

true
true
} else {
false
}
}
else -> super.onOptionsItemSelected(item)
}
Expand Down

0 comments on commit ab89c45

Please sign in to comment.