Skip to content
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

Profile-controlled public downloads.

- Adds a compiled Public Downloads runtime flag used by Android DownloadManager.
- Android 10+ writes enabled downloads to the shared user-visible Downloads directory.
- Android 6 through 9 request WRITE_EXTERNAL_STORAGE only when public downloads are enabled.
- Legacy devices fall back to app-specific external storage if storage permission is unavailable.
- Disabling Public Downloads intentionally keeps downloaded files inside app-specific external storage.
- Keeps authenticated download cookies, wrapper request headers and Android 6/API 23 compatibility.

Configurable native interface controls.

- Adds profile-controlled pull-to-refresh using Android SwipeRefreshLayout.
Expand Down
20 changes: 20 additions & 0 deletions app/src/main/java/de/starlightunit/wrapper/MainActivity.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package de.starlightunit.wrapper;

import android.Manifest;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
Expand All @@ -24,6 +26,7 @@
import de.starlightunit.wrapper.bridge.QuantumNativeMediaBridge;
import de.starlightunit.wrapper.config.AppConfig;
import de.starlightunit.wrapper.download.AppDownloadListener;
import de.starlightunit.wrapper.download.DownloadDestinationPolicy;
import de.starlightunit.wrapper.launch.QuantumIntroController;
import de.starlightunit.wrapper.media.QuantumNativeMediaPlayer;
import de.starlightunit.wrapper.navigation.NavigationPolicy;
Expand All @@ -42,6 +45,7 @@ public final class MainActivity extends Activity
GameWebChromeClient.Callbacks {

private static final int FILE_CHOOSER_REQUEST = 7001;
private static final int LEGACY_DOWNLOAD_PERMISSION_REQUEST = 7002;

private WebView webView;
private LoadingIndicatorController loadingIndicator;
Expand Down Expand Up @@ -119,6 +123,7 @@ protected void onCreate(Bundle savedInstanceState) {
chromeClient = new GameWebChromeClient(fullscreenContainer, this, this);
webView.setWebChromeClient(chromeClient);
webView.setDownloadListener(new AppDownloadListener(this, requestHeaders));
requestLegacyPublicDownloadPermissionIfNeeded();

retryButton.setOnClickListener(v -> {
errorPanel.setVisibility(View.GONE);
Expand All @@ -142,6 +147,21 @@ private void loadTrustedUrl(String requestedUrl) {
webView.loadUrl(targetUrl, requestHeaders);
}

private void requestLegacyPublicDownloadPermissionIfNeeded() {
boolean granted = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
if (DownloadDestinationPolicy.needsLegacyWritePermission(
AppConfig.PUBLIC_DOWNLOADS_ENABLED,
Build.VERSION.SDK_INT,
granted
)) {
requestPermissions(
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
LEGACY_DOWNLOAD_PERMISSION_REQUEST
);
}
}

private void configureWindow() {
Window window = getWindow();
window.setStatusBarColor(Color.BLACK);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public final class AppConfig {
public static final int FONT_SCALE_PERCENT = 100;
public static final boolean ALLOW_AUTOPLAY_MEDIA = true;
public static final boolean ALLOW_THIRD_PARTY_COOKIES = true;
public static final boolean PUBLIC_DOWNLOADS_ENABLED = true;

private AppConfig() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.util.Map;

import de.starlightunit.wrapper.R;
import de.starlightunit.wrapper.config.AppConfig;

public final class AppDownloadListener implements DownloadListener {
private final Context context;
Expand Down Expand Up @@ -72,7 +73,13 @@ public void onDownloadStart(
request.addRequestHeader("Cookie", cookies);
}

if (canWritePublicDownloads()) {
boolean legacyPermissionGranted = context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
if (DownloadDestinationPolicy.usePublicDownloads(
AppConfig.PUBLIC_DOWNLOADS_ENABLED,
Build.VERSION.SDK_INT,
legacyPermissionGranted
)) {
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
} else {
request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, fileName);
Expand Down Expand Up @@ -104,11 +111,4 @@ private static boolean isDownloadSafeHeader(String name, String value) {
&& !"connection".equals(normalized);
}

private boolean canWritePublicDownloads() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return true;
}
return context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package de.starlightunit.wrapper.download;

public final class DownloadDestinationPolicy {
private DownloadDestinationPolicy() {
}

public static boolean usePublicDownloads(
boolean publicDownloadsEnabled,
int sdkInt,
boolean legacyWritePermissionGranted
) {
if (!publicDownloadsEnabled) {
return false;
}
if (sdkInt >= 29) {
return true;
}
return legacyWritePermissionGranted;
}

public static boolean needsLegacyWritePermission(
boolean publicDownloadsEnabled,
int sdkInt,
boolean legacyWritePermissionGranted
) {
return publicDownloadsEnabled
&& sdkInt >= 23
&& sdkInt <= 28
&& !legacyWritePermissionGranted;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package de.starlightunit.wrapper.download;

import org.junit.Test;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public final class DownloadDestinationPolicyTest {
@Test
public void android10PlusUsesPublicDownloadsWhenEnabled() {
assertTrue(DownloadDestinationPolicy.usePublicDownloads(true, 29, false));
assertTrue(DownloadDestinationPolicy.usePublicDownloads(true, 36, false));
}

@Test
public void disabledAlwaysUsesAppPrivateDestination() {
assertFalse(DownloadDestinationPolicy.usePublicDownloads(false, 36, true));
assertFalse(DownloadDestinationPolicy.usePublicDownloads(false, 28, true));
}

@Test
public void android6Through9RequireLegacyPermission() {
assertFalse(DownloadDestinationPolicy.usePublicDownloads(true, 23, false));
assertTrue(DownloadDestinationPolicy.usePublicDownloads(true, 23, true));
assertFalse(DownloadDestinationPolicy.usePublicDownloads(true, 28, false));
assertTrue(DownloadDestinationPolicy.usePublicDownloads(true, 28, true));
}

@Test
public void legacyPermissionPromptOnlyNeededOnApi23Through28() {
assertTrue(DownloadDestinationPolicy.needsLegacyWritePermission(true, 23, false));
assertTrue(DownloadDestinationPolicy.needsLegacyWritePermission(true, 28, false));
assertFalse(DownloadDestinationPolicy.needsLegacyWritePermission(true, 29, false));
assertFalse(DownloadDestinationPolicy.needsLegacyWritePermission(false, 28, false));
assertFalse(DownloadDestinationPolicy.needsLegacyWritePermission(true, 28, true));
}
}
Loading