Skip to content

Commit

Permalink
Merge pull request #78 from solrudev/develop
Browse files Browse the repository at this point in the history
0.7.4
  • Loading branch information
solrudev authored Oct 4, 2024
2 parents 688d2e0 + 1621314 commit 6df630b
Show file tree
Hide file tree
Showing 18 changed files with 282 additions and 78 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<span style="color:#808080">_**Ackpine** - **A**ndroid **C**oroutines-friendly **K**otlin-first **P**ackage **In**staller **e**xtensions_</span>

See the [project website](https://solrudev.github.io/Ackpine/) for documentation and API reference.
See the [project website](https://ackpine.solrudev.ru) for documentation and API reference.

A library providing consistent APIs for installing and uninstalling apps on an Android device.

Expand All @@ -31,7 +31,7 @@ Ackpine depends on Jetpack libraries, so it's necessary to declare the `google()

```kotlin
dependencies {
val ackpineVersion = "0.7.3"
val ackpineVersion = "0.7.4"
implementation("ru.solrudev.ackpine:ackpine-core:$ackpineVersion")

// optional - Kotlin extensions and Coroutines support
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import androidx.core.net.toUri
import ru.solrudev.ackpine.helpers.closeWithException
import ru.solrudev.ackpine.helpers.entries
import ru.solrudev.ackpine.helpers.toFile
import ru.solrudev.ackpine.plugin.AckpinePlugin
Expand Down Expand Up @@ -206,38 +207,47 @@ public class ZippedFileProvider : ContentProvider() {
return try {
val zipEntry = zipFile.getEntry(uri.encodedQuery)
ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile)
} catch (t: Throwable) {
zipFile.close()
throw t
} catch (throwable: Throwable) {
zipFile.closeWithException(throwable)
throw throwable
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return try {
openZipEntryStreamUsingFileChannel(zipFileUri, uri.encodedQuery)
} catch (exception: Exception) {
exception.printStackTrace()
openZipEntryStreamUsingZipInputStream(zipFileUri, uri.encodedQuery, signal)
}
return openZipEntryStreamApi26(zipFileUri, uri.encodedQuery, signal)
}
return openZipEntryStreamUsingZipInputStream(zipFileUri, uri.encodedQuery, signal)
}

@RequiresApi(Build.VERSION_CODES.O)
private fun openZipEntryStreamUsingFileChannel(zipFileUri: Uri, zipEntryName: String?): ZipEntryStream {
val fd = context?.contentResolver?.openFileDescriptor(zipFileUri, "r")
?: throw NullPointerException("ParcelFileDescriptor was null: $zipFileUri")
val fileInputStream = FileInputStream(fd.fileDescriptor)
val zipFile = org.apache.commons.compress.archivers.zip.ZipFile.builder()
.setSeekableByteChannel(fileInputStream.channel)
.get()
return try {
private fun openZipEntryStreamApi26(
zipFileUri: Uri,
zipEntryName: String?,
signal: CancellationSignal?
): ZipEntryStream {
var fd: ParcelFileDescriptor? = null
var fileInputStream: FileInputStream? = null
val zipFile: org.apache.commons.compress.archivers.zip.ZipFile
try {
fd = context?.contentResolver?.openFileDescriptor(zipFileUri, "r", signal)
?: throw NullPointerException("ParcelFileDescriptor was null: $zipFileUri")
fileInputStream = FileInputStream(fd.fileDescriptor)
zipFile = org.apache.commons.compress.archivers.zip.ZipFile.builder()
.setSeekableByteChannel(fileInputStream.channel)
.get()
} catch (throwable: Throwable) {
fd?.closeWithException(throwable)
fileInputStream?.closeWithException(throwable)
throwable.printStackTrace()
return openZipEntryStreamUsingZipInputStream(zipFileUri, zipEntryName, signal)
}
try {
val zipEntry = zipFile.getEntry(zipEntryName)
ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile, fileInputStream, fd)
} catch (t: Throwable) {
fd.close()
fileInputStream.close()
zipFile.close()
throw t
return ZipEntryStream(zipFile.getInputStream(zipEntry), zipEntry.size, zipFile, fileInputStream, fd)
} catch (throwable: Throwable) {
fd.closeWithException(throwable)
fileInputStream.closeWithException(throwable)
zipFile.closeWithException(throwable)
throw throwable
}
}

Expand All @@ -251,9 +261,9 @@ public class ZippedFileProvider : ContentProvider() {
zipStream.entries()
.onEach { signal?.throwIfCanceled() }
.first { it.name == zipEntryName }
} catch (t: Throwable) {
zipStream.close()
throw t
} catch (throwable: Throwable) {
zipStream.closeWithException(throwable)
throw throwable
}
return ZipEntryStream(zipStream, zipEntry.size)
}
Expand Down Expand Up @@ -285,17 +295,17 @@ public class ZippedFileProvider : ContentProvider() {
var exception: Throwable? = null
try {
FileOutputStream(fileDescriptor).buffered().use(block)
} catch (t: Throwable) {
exception = t
} catch (throwable: Throwable) {
exception = throwable
} finally {
try {
if (exception != null) {
closeWithError(exception.message)
} else {
close()
}
} catch (t: Throwable) {
exception?.addSuppressed(t)
} catch (throwable: Throwable) {
exception?.addSuppressed(throwable)
exception?.printStackTrace()
}
}
Expand Down Expand Up @@ -366,7 +376,7 @@ private object ZippedFileProviderPlugin : AckpinePlugin {
private class ZipEntryStream(
private val inputStream: InputStream,
val size: Long,
private vararg val resources: AutoCloseable
private vararg var resources: AutoCloseable
) : InputStream() {

override fun read(): Int = inputStream.read()
Expand All @@ -380,8 +390,9 @@ private class ZipEntryStream(

override fun close() {
for (resource in resources) {
resource.close()
runCatching { resource.close() }
}
resources = emptyArray()
inputStream.close()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (C) 2024 Ilya Fomichev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ru.solrudev.ackpine.helpers

import java.io.Closeable

@JvmSynthetic
internal fun Closeable.closeWithException(cause: Throwable) {
try {
close()
} catch (closeException: Throwable) {
cause.addSuppressed(closeException)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ package ru.solrudev.ackpine.splits
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import androidx.annotation.RequiresApi
import ru.solrudev.ackpine.helpers.closeWithException
import ru.solrudev.ackpine.helpers.entries
import ru.solrudev.ackpine.helpers.toFile
import java.io.File
Expand Down Expand Up @@ -64,12 +66,7 @@ public object ZippedApkSplits {
return@closeableSequence
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
yieldAllUsingFileChannel(applicationContext, uri)
} catch (exception: Exception) {
exception.printStackTrace()
yieldAllUsingZipInputStream(applicationContext, uri)
}
yieldAllApi26(applicationContext, uri)
return@closeableSequence
}
yieldAllUsingZipInputStream(applicationContext, uri)
Expand All @@ -92,24 +89,42 @@ public object ZippedApkSplits {
}

@RequiresApi(Build.VERSION_CODES.O)
private suspend inline fun CloseableSequenceScope<Apk>.yieldAllUsingFileChannel(context: Context, uri: Uri) {
val fd = context.contentResolver.openFileDescriptor(uri, "r")?.use()
?: throw NullPointerException("ParcelFileDescriptor was null: $uri")
val fileInputStream = FileInputStream(fd.fileDescriptor).use()
val zipFile = org.apache.commons.compress.archivers.zip.ZipFile.builder()
.setSeekableByteChannel(fileInputStream.channel)
.get()
.use()
zipFile.entries
.asSequence()
.filterNot { isClosed }
.mapNotNull { zipEntry ->
zipFile.getInputStream(zipEntry).use { entryStream ->
entryStream.use()
Apk.fromZipEntry(uri.toString(), zipEntry, entryStream)
private suspend inline fun CloseableSequenceScope<Apk>.yieldAllApi26(context: Context, uri: Uri) {
var fd: ParcelFileDescriptor? = null
var fileInputStream: FileInputStream? = null
val zipFile: org.apache.commons.compress.archivers.zip.ZipFile
try {
fd = context.contentResolver.openFileDescriptor(uri, "r")?.use()
?: throw NullPointerException("ParcelFileDescriptor was null: $uri")
fileInputStream = FileInputStream(fd.fileDescriptor).use()
zipFile = org.apache.commons.compress.archivers.zip.ZipFile.builder()
.setSeekableByteChannel(fileInputStream.channel)
.get()
.use()
} catch (throwable: Throwable) {
fd?.closeWithException(throwable)
fileInputStream?.closeWithException(throwable)
throwable.printStackTrace()
yieldAllUsingZipInputStream(context, uri)
return
}
try {
zipFile.entries
.asSequence()
.filterNot { isClosed }
.mapNotNull { zipEntry ->
zipFile.getInputStream(zipEntry).use { entryStream ->
entryStream.use()
Apk.fromZipEntry(uri.toString(), zipEntry, entryStream)
}
}
}
.forEach { yield(it) }
.forEach { yield(it) }
} catch (throwable: Throwable) {
fd.closeWithException(throwable)
fileInputStream.closeWithException(throwable)
zipFile.closeWithException(throwable)
throw throwable
}
}

private suspend inline fun CloseableSequenceScope<Apk>.yieldAllUsingZipInputStream(context: Context, uri: Uri) {
Expand Down
13 changes: 13 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
Change Log
==========

Version 0.7.4 (2024-10-04)
--------------------------

### Dependencies

- Added direct dependency on `androidx.coordinatorlayout:coordinatorlayout:1.2.0` to sample projects.

### Bug fixes and improvements

- Improve error handling in `ackpine-splits`. This also allowed to avoid duplicate `Apk` objects in sequences returned from `ZippedApkSplits` factories in some possible cases of errors.
- Fix incorrect sessions' progress when there are list items beyond visible area in sample apps.
- Hide floating Install button when scrolling down in sample apps.

Version 0.7.3 (2024-10-01)
--------------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Ackpine depends on Jetpack libraries, so it's necessary to declare the `google()

```kotlin
dependencies {
val ackpineVersion = "0.7.3"
val ackpineVersion = "0.7.4"
implementation("ru.solrudev.ackpine:ackpine-core:$ackpineVersion")

// optional - Kotlin extensions and Coroutines support
Expand Down
1 change: 1 addition & 0 deletions gradle/androidx.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" }
concurrent-futures-core = { module = "androidx.concurrent:concurrent-futures", version.ref = "concurrent" }
concurrent-futures-ktx = { module = "androidx.concurrent:concurrent-futures-ktx", version.ref = "concurrent" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.4" }
coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorlayout", version = "1.2.0" }
core-ktx = { module = "androidx.core:core-ktx", version = "1.12.0" }
lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata", version.ref = "lifecycle" }
lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
Expand Down
1 change: 1 addition & 0 deletions sample-java/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ dependencies {
implementation(androidx.appcompat)
implementation(androidx.recyclerview)
implementation(androidx.constraintlayout)
implementation(androidx.coordinatorlayout)
implementation(androidx.bundles.lifecycle)
implementation(androidx.bundles.navigation)
implementation(androidx.swiperefreshlayout)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2024 Ilya Fomichev
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package ru.solrudev.ackpine.sample.install;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.recyclerview.widget.RecyclerView;

import com.google.android.material.behavior.HideBottomViewOnScrollBehavior;
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton;

@SuppressWarnings("unused")
public class HideFabOnScrollBehavior<V extends View> extends HideBottomViewOnScrollBehavior<V> {

public HideFabOnScrollBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}

public HideFabOnScrollBehavior() {
super();
}

@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull View dependency) {
return child instanceof ExtendedFloatingActionButton && dependency instanceof RecyclerView;
}

@Override
public boolean onLayoutChild(@NonNull CoordinatorLayout parent, @NonNull V child, int layoutDirection) {
var canScroll = false;
final var childCount = parent.getChildCount();
for (var i = 0; i < childCount; i++) {
final var dependency = parent.getChildAt(i);
if (dependency instanceof RecyclerView) {
canScroll = dependency.canScrollVertically(1) || dependency.canScrollVertically(-1);
break;
}
}
if (child instanceof ExtendedFloatingActionButton && !canScroll && isScrolledDown()) {
slideUp(child);
}
return super.onLayoutChild(parent, child, layoutDirection);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
import androidx.core.view.ViewKt;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;

Expand Down Expand Up @@ -96,12 +95,6 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
.setLiftOnScrollTargetView(binding.recyclerViewInstall);
binding.fabInstall.setOnClickListener(v -> onInstallButtonClick());
binding.recyclerViewInstall.setAdapter(adapter);
binding.recyclerViewInstall.setItemAnimator(new DefaultItemAnimator() {
@Override
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
return true;
}
});
new ItemTouchHelper(new SwipeCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT))
.attachToRecyclerView(binding.recyclerViewInstall);
observeViewModel();
Expand Down
Loading

0 comments on commit 6df630b

Please sign in to comment.