From 4ab283a2aeae23169dfb00d5b34926fe12d4ad60 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 21 Mar 2021 02:44:14 +1000 Subject: [PATCH] Android: Add "Choose Cover Image" to game list menu --- .../duckstation/AndroidHostInterface.java | 15 +++-- .../github/stenzek/duckstation/FileUtil.java | 18 ++++++ .../github/stenzek/duckstation/GameList.java | 16 ++++- .../stenzek/duckstation/GameListEntry.java | 2 + .../stenzek/duckstation/MainActivity.java | 62 +++++++++++++++++++ .../main/res/menu/menu_game_list_entry.xml | 3 + android/app/src/main/res/values/strings.xml | 1 + 7 files changed, 109 insertions(+), 8 deletions(-) diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java index 6c25986b2..927552eb9 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java @@ -168,16 +168,17 @@ public class AndroidHostInterface { } static private AndroidHostInterface mInstance; + static private String mUserDirectory; static public boolean createInstance(Context context) { // Set user path. - String externalStorageDirectory = Environment.getExternalStorageDirectory().getAbsolutePath(); - if (externalStorageDirectory.isEmpty()) - externalStorageDirectory = "/sdcard"; + mUserDirectory = Environment.getExternalStorageDirectory().getAbsolutePath(); + if (mUserDirectory.isEmpty()) + mUserDirectory = "/sdcard"; - externalStorageDirectory += "/duckstation"; - Log.i("AndroidHostInterface", "User directory: " + externalStorageDirectory); - mInstance = create(context, externalStorageDirectory); + mUserDirectory += "/duckstation"; + Log.i("AndroidHostInterface", "User directory: " + mUserDirectory); + mInstance = create(context, mUserDirectory); return mInstance != null; } @@ -189,6 +190,8 @@ public class AndroidHostInterface { return mInstance; } + static public String getUserDirectory() { return mUserDirectory; } + static public boolean hasInstanceAndEmulationThreadIsRunning() { return hasInstance() && getInstance().isEmulationThreadRunning(); } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java b/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java index 434aa2548..85128dec0 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/FileUtil.java @@ -6,6 +6,9 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.ImageDecoder; import android.net.Uri; import android.os.Build; import android.os.storage.StorageManager; @@ -272,4 +275,19 @@ public final class FileUtil { cursor.close(); } } + + public static Bitmap loadBitmapFromUri(final Context context, final Uri uri) { + InputStream stream = null; + try { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { + final ImageDecoder.Source source =ImageDecoder.createSource(context.getContentResolver(), uri); + return ImageDecoder.decodeBitmap(source); + } else { + return MediaStore.Images.Media.getBitmap(context.getContentResolver(), uri); + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java index f6d0bd8de..f835dce22 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java @@ -32,6 +32,10 @@ public class GameList { public void removeRefreshListener(OnRefreshListener listener) { mRefreshListeners.remove(listener); } + public void fireRefreshListeners() { + for (OnRefreshListener listener : mRefreshListeners) + listener.onGameListRefresh(); + } private class GameListEntryComparator implements Comparator { @Override @@ -56,8 +60,7 @@ public class GameList { e.printStackTrace(); } mEntries = newEntries; - for (OnRefreshListener listener : mRefreshListeners) - listener.onGameListRefresh(); + fireRefreshListeners(); }); }); } @@ -69,4 +72,13 @@ public class GameList { public GameListEntry getEntry(int index) { return mEntries[index]; } + + public GameListEntry getEntryForPath(String path) { + for (final GameListEntry entry : mEntries) { + if (entry.getPath().equals(path)) + return entry; + } + + return null; + } } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java index 2806dc365..22e69cb4d 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java @@ -100,6 +100,8 @@ public class GameListEntry { public String getCoverPath() { return mCoverPath; } + public void setCoverPath(String coverPath) { mCoverPath = coverPath; } + public static String getFileNameForPath(String path) { int lastSlash = path.lastIndexOf('/'); if (lastSlash > 0 && lastSlash < path.length() - 1) diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java index fc2d53d29..26798a762 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/MainActivity.java @@ -8,6 +8,8 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -29,9 +31,12 @@ import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.Locale; public class MainActivity extends AppCompatActivity { @@ -41,6 +46,7 @@ public class MainActivity extends AppCompatActivity { private static final int REQUEST_START_FILE = 4; private static final int REQUEST_SETTINGS = 5; private static final int REQUEST_EDIT_GAME_DIRECTORIES = 6; + private static final int REQUEST_CHOOSE_COVER_IMAGE = 7; private GameList mGameList; private ListView mGameListView; @@ -48,6 +54,7 @@ public class MainActivity extends AppCompatActivity { private GameGridFragment mGameGridFragment; private boolean mHasExternalStoragePermissions = false; private boolean mIsShowingGameGrid = false; + private String mPathForChosenCoverImage = null; public GameList getGameList() { return mGameList; @@ -297,6 +304,16 @@ public class MainActivity extends AppCompatActivity { mGameList.refresh(false, false, this); } break; + + case REQUEST_CHOOSE_COVER_IMAGE: { + final String gamePath = mPathForChosenCoverImage; + mPathForChosenCoverImage = null; + if (resultCode != RESULT_OK) + return; + + finishChooseCoverImage(gamePath, data.getData()); + } + break; } } @@ -355,6 +372,9 @@ public class MainActivity extends AppCompatActivity { } else if (id == R.id.game_list_entry_menu_properties) { openGameProperties(entry.getPath()); return true; + } else if (id == R.id.game_list_entry_menu_choose_cover_image) { + startChooseCoverImage(entry.getPath()); + return true; } return false; }); @@ -387,6 +407,48 @@ public class MainActivity extends AppCompatActivity { startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_disc_image)), REQUEST_START_FILE); } + private void startChooseCoverImage(String gamePath) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + mPathForChosenCoverImage = gamePath; + startActivityForResult(Intent.createChooser(intent, getString(R.string.menu_game_list_entry_choose_cover_image)), + REQUEST_CHOOSE_COVER_IMAGE); + } + + private void finishChooseCoverImage(String gamePath, Uri uri) { + final GameListEntry gameListEntry = mGameList.getEntryForPath(gamePath); + if (gameListEntry == null) + return; + + final Bitmap bitmap = FileUtil.loadBitmapFromUri(this, uri); + if (bitmap == null) { + Toast.makeText(this, "Failed to open/decode image.", Toast.LENGTH_LONG).show(); + return; + } + + final String coverFileName = String.format("%s/covers/%s.png", + AndroidHostInterface.getUserDirectory(), gameListEntry.getTitle()); + try { + final File file = new File(coverFileName); + final OutputStream outputStream = new FileOutputStream(file); + final boolean result = bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); + outputStream.close();; + if (!result) { + file.delete(); + throw new Exception("Failed to compress bitmap."); + } + + gameListEntry.setCoverPath(coverFileName); + mGameList.fireRefreshListeners(); + } catch (Exception e) { + e.printStackTrace(); + Toast.makeText(this, "Failed to save image.", Toast.LENGTH_LONG).show(); + } + + bitmap.recycle(); + } + private boolean doBIOSCheck() { if (AndroidHostInterface.getInstance().hasAnyBIOSImages()) return true; diff --git a/android/app/src/main/res/menu/menu_game_list_entry.xml b/android/app/src/main/res/menu/menu_game_list_entry.xml index 83b71f244..e1ce8dab3 100644 --- a/android/app/src/main/res/menu/menu_game_list_entry.xml +++ b/android/app/src/main/res/menu/menu_game_list_entry.xml @@ -10,4 +10,7 @@ + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index cdd67aedf..67bcebc1d 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -311,4 +311,5 @@ Failed to read \'%s\'. Failed to import card \'%s\'. It may not be a supported format. Imported card \'%s\'. + Choose Cover Image