From d723b5b555c033b7402526ac21d6c2396ca31feb Mon Sep 17 00:00:00 2001 From: izzy2lost Date: Mon, 22 Sep 2025 00:26:24 -0400 Subject: [PATCH] Implement Google Play Store compliance improvements - Add EdgeToEdge.enable() for Android 15+ compatibility - Replace deprecated setSystemUiVisibility with modern WindowInsetsController - Add proper window insets handling for edge-to-edge display - Implement comprehensive Picture-in-Picture support with: * Automatic PiP entry when home button pressed during gameplay * Play/Pause controls in PiP mode * Proper UI hiding/showing when entering/exiting PiP * 16:9 aspect ratio optimized for gaming - Add androidx.core:core-ktx dependency for EdgeToEdge support - Update AndroidManifest.xml with supportsPictureInPicture attribute Addresses Google Play Console suggestions: - Edge-to-edge compatibility for Android 15 - Deprecated API warnings - Picture-in-picture recommendation --- .gitignore | 1 + app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 +- .../java/com/izzy2lost/psx2/MainActivity.java | 240 ++++++++++++++++-- 4 files changed, 225 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 0914b15..e9426c5 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ build/ *.iml *.exe /WARP.md +*.aab diff --git a/app/build.gradle b/app/build.gradle index d97ab0d..e64506c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,6 +74,7 @@ dependencies { implementation 'androidx.documentfile:documentfile:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.4.0' implementation 'androidx.activity:activity:1.10.1' + implementation 'androidx.core:core-ktx:1.15.0' implementation 'com.github.bumptech.glide:glide:4.16.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e5491b0..dcf464f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,11 +45,12 @@ - diff --git a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java index c41de18..58d9a30 100644 --- a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java +++ b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java @@ -32,6 +32,12 @@ import android.view.WindowInsetsController; import android.view.WindowManager; import android.util.TypedValue; import android.widget.Toast; +import android.app.PictureInPictureParams; +import android.util.Rational; +import android.app.RemoteAction; +import android.graphics.drawable.Icon; +import android.app.PendingIntent; +import android.content.pm.PackageManager; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -43,6 +49,10 @@ import androidx.core.content.ContextCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.core.view.GravityCompat; import androidx.activity.OnBackPressedCallback; +import androidx.activity.EdgeToEdge; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.graphics.Insets; import com.google.android.material.button.MaterialButton; @@ -218,8 +228,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF // Make immersive (hide status + navigation) and allow swipe to reveal temporarily Window w = getWindow(); if (w == null) return; - // Request full screen window flags for additional assurance - w.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + // Allow drawing into display cutouts (notches) on API 28+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowManager.LayoutParams lp = w.getAttributes(); @@ -227,23 +236,23 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF w.setAttributes(lp); } - // Common flags for legacy immersive (works as belt-and-suspenders on newer API too) - final int legacyFlags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - | View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_FULLSCREEN - | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Tell Window to lay out edge-to-edge and hide all system bars + // Modern API for Android 11+ (API 30+) w.setDecorFitsSystemWindows(false); WindowInsetsController controller = w.getInsetsController(); if (controller != null) { controller.hide(WindowInsets.Type.systemBars()); controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); } - // Also apply legacy flags to decor; helps on 3-button nav devices + } else { + // Legacy API for Android 10 and below + final int legacyFlags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_FULLSCREEN + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + View decor = w.getDecorView(); decor.setSystemUiVisibility(legacyFlags); decor.setOnSystemUiVisibilityChangeListener(vis -> { @@ -251,13 +260,20 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF decor.setSystemUiVisibility(legacyFlags); } }); - } else { - View decor = w.getDecorView(); - decor.setSystemUiVisibility(legacyFlags); - decor.setOnSystemUiVisibilityChangeListener(vis -> { - if ((vis & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) { - decor.setSystemUiVisibility(legacyFlags); - } + } + } + + private void setupWindowInsets() { + View mainView = findViewById(R.id.drawer_layout); // Use the actual root layout ID + if (mainView != null) { + ViewCompat.setOnApplyWindowInsetsListener(mainView, (v, windowInsets) -> { + Insets systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + + // For a gaming app, we typically want to draw behind the system bars + // but ensure important UI elements are not obscured + v.setPadding(0, 0, 0, 0); // Draw edge-to-edge + + return windowInsets; }); } } @@ -398,11 +414,20 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // Enable edge-to-edge for Android 15+ compatibility + EdgeToEdge.enable(this); + // Hide action bar to improve immersive appearance if (getSupportActionBar() != null) { getSupportActionBar().hide(); } + setContentView(R.layout.activity_main); + + // Handle window insets for edge-to-edge display + setupWindowInsets(); + enableImmersiveMode(); // Setup back button handler @@ -469,6 +494,20 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF // Setup right drawer quick actions setupRightDrawerActions(); + + // Setup picture-in-picture support + addPictureInPictureSupport(); + } + + @Override + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + // Automatically enter PiP mode when user presses home button during gameplay + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (hasSelectedGame() && isThread() && !isInPictureInPictureMode()) { + enterPictureInPictureMode(); + } + } } public void setSetupWizardActive(boolean active) { @@ -2256,4 +2295,167 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF } catch (Throwable ignored) {} } + // Picture-in-Picture support methods + + public void enterPictureInPictureMode() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { + if (hasSelectedGame() && isThread()) { + try { + PictureInPictureParams.Builder pipBuilder = new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(16, 9)) // 16:9 aspect ratio for gaming + .setSourceRectHint(null); // Use entire activity + + // Add custom actions for PiP mode + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + java.util.ArrayList actions = new java.util.ArrayList<>(); + + // Play/Pause action + Intent playPauseIntent = new Intent("TOGGLE_PAUSE"); + PendingIntent playPausePendingIntent = PendingIntent.getBroadcast( + this, 0, playPauseIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + boolean isPaused = false; + try { isPaused = NativeApp.isPaused(); } catch (Throwable ignored) {} + + Icon playPauseIcon = Icon.createWithResource(this, + isPaused ? R.drawable.play_circle_24px : R.drawable.pause_circle_24px); + RemoteAction playPauseAction = new RemoteAction(playPauseIcon, + isPaused ? "Resume" : "Pause", + isPaused ? "Resume Game" : "Pause Game", + playPausePendingIntent); + actions.add(playPauseAction); + + pipBuilder.setActions(actions); + } + + enterPictureInPictureMode(pipBuilder.build()); + } catch (Exception e) { + Toast.makeText(this, "Picture-in-Picture not available", Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(this, "Start a game to use Picture-in-Picture", Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(this, "Picture-in-Picture not supported on this device", Toast.LENGTH_SHORT).show(); + } + } + } + + @Override + public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); + + if (isInPictureInPictureMode) { + // Hide UI elements that are not needed in PiP mode + hideUiForPiP(); + } else { + // Restore UI when exiting PiP mode + showUiAfterPiP(); + } + } + + private void hideUiForPiP() { + // Hide control buttons, drawers, etc. in PiP mode + View settingsBtn = findViewById(R.id.btn_settings); + if (settingsBtn != null) settingsBtn.setVisibility(View.GONE); + + View pauseBtn = findViewById(R.id.btn_pause_play); + if (pauseBtn != null) pauseBtn.setVisibility(View.GONE); + + View quickActions = findViewById(R.id.ll_quick_actions); + if (quickActions != null) quickActions.setVisibility(View.GONE); + + // Hide all gamepad controls + hideGamepadControls(); + } + + private void showUiAfterPiP() { + // Restore UI elements when exiting PiP mode + View settingsBtn = findViewById(R.id.btn_settings); + if (settingsBtn != null) settingsBtn.setVisibility(View.VISIBLE); + + updatePausePlayButton(); // This will show/hide based on game state + + View quickActions = findViewById(R.id.ll_quick_actions); + if (quickActions != null) quickActions.setVisibility(View.VISIBLE); + + // Show gamepad controls based on preferences + updateUiForControllerPresence(); + } + + private void hideGamepadControls() { + View joy = findViewById(R.id.ll_pad_joy); + if (joy != null) joy.setVisibility(View.GONE); + + View dpad = findViewById(R.id.ll_pad_dpad); + if (dpad != null) dpad.setVisibility(View.GONE); + + View rightButtons = findViewById(R.id.ll_pad_right_buttons); + if (rightButtons != null) rightButtons.setVisibility(View.GONE); + + View selectStart = findViewById(R.id.ll_pad_select_start); + if (selectStart != null) selectStart.setVisibility(View.GONE); + } + + // Add PiP button to quick actions or settings + public void addPictureInPictureSupport() { + // This method can be called to add a PiP button to your UI + // For now, users can enter PiP mode by pressing home button during gameplay + + // Register broadcast receiver for PiP actions + registerPiPBroadcastReceiver(); + } + + private void registerPiPBroadcastReceiver() { + // Register receiver for PiP remote actions + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + android.content.BroadcastReceiver pipReceiver = new android.content.BroadcastReceiver() { + @Override + public void onReceive(android.content.Context context, Intent intent) { + String action = intent.getAction(); + if ("TOGGLE_PAUSE".equals(action)) { + togglePauseState(); + // Update PiP params with new play/pause state + if (isInPictureInPictureMode()) { + updatePictureInPictureParams(); + } + } + } + }; + + android.content.IntentFilter filter = new android.content.IntentFilter(); + filter.addAction("TOGGLE_PAUSE"); + registerReceiver(pipReceiver, filter, Context.RECEIVER_NOT_EXPORTED); + } + } + + private void updatePictureInPictureParams() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isInPictureInPictureMode()) { + try { + boolean isPaused = NativeApp.isPaused(); + + Intent playPauseIntent = new Intent("TOGGLE_PAUSE"); + PendingIntent playPausePendingIntent = PendingIntent.getBroadcast( + this, 0, playPauseIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + Icon playPauseIcon = Icon.createWithResource(this, + isPaused ? R.drawable.play_circle_24px : R.drawable.pause_circle_24px); + RemoteAction playPauseAction = new RemoteAction(playPauseIcon, + isPaused ? "Resume" : "Pause", + isPaused ? "Resume Game" : "Pause Game", + playPausePendingIntent); + + java.util.ArrayList actions = new java.util.ArrayList<>(); + actions.add(playPauseAction); + + PictureInPictureParams params = new PictureInPictureParams.Builder() + .setActions(actions) + .build(); + + setPictureInPictureParams(params); + } catch (Exception ignored) {} + } + } + }