Android: Add "Choose Cover Image" to game list menu

This commit is contained in:
Connor McLaughlin 2021-03-21 02:44:14 +10:00
parent f68836206b
commit 4ab283a2ae
7 changed files with 109 additions and 8 deletions

View file

@ -168,16 +168,17 @@ public class AndroidHostInterface {
} }
static private AndroidHostInterface mInstance; static private AndroidHostInterface mInstance;
static private String mUserDirectory;
static public boolean createInstance(Context context) { static public boolean createInstance(Context context) {
// Set user path. // Set user path.
String externalStorageDirectory = Environment.getExternalStorageDirectory().getAbsolutePath(); mUserDirectory = Environment.getExternalStorageDirectory().getAbsolutePath();
if (externalStorageDirectory.isEmpty()) if (mUserDirectory.isEmpty())
externalStorageDirectory = "/sdcard"; mUserDirectory = "/sdcard";
externalStorageDirectory += "/duckstation"; mUserDirectory += "/duckstation";
Log.i("AndroidHostInterface", "User directory: " + externalStorageDirectory); Log.i("AndroidHostInterface", "User directory: " + mUserDirectory);
mInstance = create(context, externalStorageDirectory); mInstance = create(context, mUserDirectory);
return mInstance != null; return mInstance != null;
} }
@ -189,6 +190,8 @@ public class AndroidHostInterface {
return mInstance; return mInstance;
} }
static public String getUserDirectory() { return mUserDirectory; }
static public boolean hasInstanceAndEmulationThreadIsRunning() { static public boolean hasInstanceAndEmulationThreadIsRunning() {
return hasInstance() && getInstance().isEmulationThreadRunning(); return hasInstance() && getInstance().isEmulationThreadRunning();
} }

View file

@ -6,6 +6,9 @@ import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageDecoder;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.storage.StorageManager; import android.os.storage.StorageManager;
@ -272,4 +275,19 @@ public final class FileUtil {
cursor.close(); 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;
}
}
} }

View file

@ -32,6 +32,10 @@ public class GameList {
public void removeRefreshListener(OnRefreshListener listener) { public void removeRefreshListener(OnRefreshListener listener) {
mRefreshListeners.remove(listener); mRefreshListeners.remove(listener);
} }
public void fireRefreshListeners() {
for (OnRefreshListener listener : mRefreshListeners)
listener.onGameListRefresh();
}
private class GameListEntryComparator implements Comparator<GameListEntry> { private class GameListEntryComparator implements Comparator<GameListEntry> {
@Override @Override
@ -56,8 +60,7 @@ public class GameList {
e.printStackTrace(); e.printStackTrace();
} }
mEntries = newEntries; mEntries = newEntries;
for (OnRefreshListener listener : mRefreshListeners) fireRefreshListeners();
listener.onGameListRefresh();
}); });
}); });
} }
@ -69,4 +72,13 @@ public class GameList {
public GameListEntry getEntry(int index) { public GameListEntry getEntry(int index) {
return mEntries[index]; return mEntries[index];
} }
public GameListEntry getEntryForPath(String path) {
for (final GameListEntry entry : mEntries) {
if (entry.getPath().equals(path))
return entry;
}
return null;
}
} }

View file

@ -100,6 +100,8 @@ public class GameListEntry {
public String getCoverPath() { return mCoverPath; } public String getCoverPath() { return mCoverPath; }
public void setCoverPath(String coverPath) { mCoverPath = coverPath; }
public static String getFileNameForPath(String path) { public static String getFileNameForPath(String path) {
int lastSlash = path.lastIndexOf('/'); int lastSlash = path.lastIndexOf('/');
if (lastSlash > 0 && lastSlash < path.length() - 1) if (lastSlash > 0 && lastSlash < path.length() - 1)

View file

@ -8,6 +8,8 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -29,9 +31,12 @@ import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale; import java.util.Locale;
public class MainActivity extends AppCompatActivity { 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_START_FILE = 4;
private static final int REQUEST_SETTINGS = 5; private static final int REQUEST_SETTINGS = 5;
private static final int REQUEST_EDIT_GAME_DIRECTORIES = 6; private static final int REQUEST_EDIT_GAME_DIRECTORIES = 6;
private static final int REQUEST_CHOOSE_COVER_IMAGE = 7;
private GameList mGameList; private GameList mGameList;
private ListView mGameListView; private ListView mGameListView;
@ -48,6 +54,7 @@ public class MainActivity extends AppCompatActivity {
private GameGridFragment mGameGridFragment; private GameGridFragment mGameGridFragment;
private boolean mHasExternalStoragePermissions = false; private boolean mHasExternalStoragePermissions = false;
private boolean mIsShowingGameGrid = false; private boolean mIsShowingGameGrid = false;
private String mPathForChosenCoverImage = null;
public GameList getGameList() { public GameList getGameList() {
return mGameList; return mGameList;
@ -297,6 +304,16 @@ public class MainActivity extends AppCompatActivity {
mGameList.refresh(false, false, this); mGameList.refresh(false, false, this);
} }
break; 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) { } else if (id == R.id.game_list_entry_menu_properties) {
openGameProperties(entry.getPath()); openGameProperties(entry.getPath());
return true; return true;
} else if (id == R.id.game_list_entry_menu_choose_cover_image) {
startChooseCoverImage(entry.getPath());
return true;
} }
return false; 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); 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() { private boolean doBIOSCheck() {
if (AndroidHostInterface.getInstance().hasAnyBIOSImages()) if (AndroidHostInterface.getInstance().hasAnyBIOSImages())
return true; return true;

View file

@ -10,4 +10,7 @@
<item <item
android:id="@+id/game_list_entry_menu_properties" android:id="@+id/game_list_entry_menu_properties"
android:title="@string/menu_game_list_entry_game_properties" /> android:title="@string/menu_game_list_entry_game_properties" />
<item
android:id="@+id/game_list_entry_menu_choose_cover_image"
android:title="@string/menu_game_list_entry_choose_cover_image" />
</menu> </menu>

View file

@ -311,4 +311,5 @@
<string name="memory_card_editor_import_card_read_failed">Failed to read \'%s\'.</string> <string name="memory_card_editor_import_card_read_failed">Failed to read \'%s\'.</string>
<string name="memory_card_editor_import_card_failed">Failed to import card \'%s\'. It may not be a supported format.</string> <string name="memory_card_editor_import_card_failed">Failed to import card \'%s\'. It may not be a supported format.</string>
<string name="memory_card_editor_import_card_success">Imported card \'%s\'.</string> <string name="memory_card_editor_import_card_success">Imported card \'%s\'.</string>
<string name="menu_game_list_entry_choose_cover_image">Choose Cover Image</string>
</resources> </resources>