diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index 96d388548..17098ee13 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -130,6 +130,7 @@ void AndroidHostInterface::SetUserDirectory() void AndroidHostInterface::LoadSettings() { CommonHostInterface::LoadSettings(m_settings_interface); + CommonHostInterface::UpdateInputMap(m_settings_interface); } void AndroidHostInterface::UpdateInputMap() @@ -396,6 +397,19 @@ void AndroidHostInterface::SetControllerButtonState(u32 index, s32 button_code, false); } +void AndroidHostInterface::RefreshGameList(bool invalidate_cache, bool invalidate_database) +{ + m_game_list->SetSearchDirectoriesFromSettings(m_settings_interface); + m_game_list->Refresh(invalidate_cache, invalidate_database); +} + +void AndroidHostInterface::ApplySettings() +{ + Settings old_settings = std::move(m_settings); + CommonHostInterface::LoadSettings(m_settings_interface); + CheckForSettingsChanges(old_settings); +} + extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) { Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEV); @@ -530,28 +544,18 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerButtonCode, jobje return code.value_or(-1); } -DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_cache_path, jstring j_redump_dat_path, - jarray j_search_directories, jboolean search_recursively) +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_refreshGameList, jobject obj, jboolean invalidate_cache, jboolean invalidate_database) { - // const std::string cache_path = AndroidHelpers::JStringToString(env, j_cache_path); - std::string redump_dat_path = AndroidHelpers::JStringToString(env, j_redump_dat_path); + AndroidHelpers::GetNativeClass(env, obj)->RefreshGameList(invalidate_cache, invalidate_database); +} - // TODO: This should use the base HostInterface. - GameList gl; - if (!redump_dat_path.empty()) - gl.SetDatabaseFilename(std::move(redump_dat_path)); - - const jsize search_directories_size = env->GetArrayLength(j_search_directories); - for (jsize i = 0; i < search_directories_size; i++) - { - jobject search_dir_obj = env->GetObjectArrayElement(reinterpret_cast(j_search_directories), i); - const std::string search_dir = AndroidHelpers::JStringToString(env, reinterpret_cast(search_dir_obj)); - if (!search_dir.empty()) - gl.AddDirectory(search_dir.c_str(), search_recursively); - } - - gl.Refresh(false, false, nullptr); +static const char* DiscRegionToString(DiscRegion region) { + static std::array names = {{"NTSC_J", "NTSC_U", "PAL", "Other"}}; + return names[static_cast(region)]; +} +DEFINE_JNI_ARGS_METHOD(jarray, AndroidHostInterface_getGameListEntries, jobject obj) +{ jclass entry_class = env->FindClass("com/github/stenzek/duckstation/GameListEntry"); Assert(entry_class != nullptr); @@ -560,11 +564,12 @@ DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_ca "String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); Assert(entry_constructor != nullptr); - jobjectArray entry_array = env->NewObjectArray(gl.GetEntryCount(), entry_class, nullptr); + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + jobjectArray entry_array = env->NewObjectArray(hi->GetGameList()->GetEntryCount(), entry_class, nullptr); Assert(entry_array != nullptr); u32 counter = 0; - for (const GameListEntry& entry : gl.GetEntries()) + for (const GameListEntry& entry : hi->GetGameList()->GetEntries()) { const Timestamp modified_ts( Timestamp::FromUnixTimestamp(static_cast(entry.last_modified_time))); @@ -572,7 +577,7 @@ DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_ca jstring path = env->NewStringUTF(entry.path.c_str()); jstring code = env->NewStringUTF(entry.code.c_str()); jstring title = env->NewStringUTF(entry.title.c_str()); - jstring region = env->NewStringUTF(Settings::GetDiscRegionName(entry.region)); + jstring region = env->NewStringUTF(DiscRegionToString(entry.region)); jstring type = env->NewStringUTF(GameList::EntryTypeToString(entry.type)); jstring compatibility_rating = env->NewStringUTF(GameList::EntryCompatibilityRatingToString(entry.compatibility_rating)); @@ -587,3 +592,42 @@ DEFINE_JNI_ARGS_METHOD(jarray, GameList_getEntries, jobject unused, jstring j_ca return entry_array; } + +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_applySettings, jobject obj) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + if (hi->IsEmulationThreadRunning()) + { + hi->RunOnEmulationThread([hi]() { + hi->ApplySettings(); + }); + } + else + { + hi->ApplySettings(); + } +} + +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_resetSystem, jobject obj, jboolean global, jint slot) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + hi->RunOnEmulationThread([hi]() { + hi->ResetSystem(); + }); +} + +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_loadState, jobject obj, jboolean global, jint slot) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + hi->RunOnEmulationThread([hi, global, slot]() { + hi->LoadState(global, slot); + }); +} + +DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_saveState, jobject obj, jboolean global, jint slot) +{ + AndroidHostInterface* hi = AndroidHelpers::GetNativeClass(env, obj); + hi->RunOnEmulationThread([hi, global, slot]() { + hi->SaveState(global, slot); + }); +} diff --git a/android/app/src/cpp/android_host_interface.h b/android/app/src/cpp/android_host_interface.h index ac699edaf..54820675c 100644 --- a/android/app/src/cpp/android_host_interface.h +++ b/android/app/src/cpp/android_host_interface.h @@ -44,6 +44,9 @@ public: void SetControllerType(u32 index, std::string_view type_name); void SetControllerButtonState(u32 index, s32 button_code, bool pressed); + void RefreshGameList(bool invalidate_cache, bool invalidate_database); + void ApplySettings(); + protected: void SetUserDirectory() override; void LoadSettings() override; diff --git a/android/app/src/cpp/android_settings_interface.cpp b/android/app/src/cpp/android_settings_interface.cpp index ba0e9182e..53d801019 100644 --- a/android/app/src/cpp/android_settings_interface.cpp +++ b/android/app/src/cpp/android_settings_interface.cpp @@ -141,7 +141,7 @@ void AndroidSettingsInterface::DeleteValue(const char* section, const char* key) std::vector AndroidSettingsInterface::GetStringList(const char* section, const char* key) { JNIEnv* env = AndroidHelpers::GetJNIEnv(); - jobject values_set = env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, nullptr); + jobject values_set = env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, env->NewStringUTF(GetSettingKey(section, key)), nullptr); if (!values_set) return {}; 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 79eb44374..2488e695c 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 @@ -7,10 +7,6 @@ public class AndroidHostInterface { private long nativePointer; - static { - System.loadLibrary("duckstation-native"); - } - static public native AndroidHostInterface create(Context context); public AndroidHostInterface(long nativePointer) @@ -28,4 +24,26 @@ public class AndroidHostInterface public native void setControllerType(int index, String typeName); public native void setControllerButtonState(int index, int buttonCode, boolean pressed); public static native int getControllerButtonCode(String controllerType, String buttonName); + + public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase); + public native GameListEntry[] getGameListEntries(); + + public native void resetSystem(); + public native void loadState(boolean global, int slot); + public native void saveState(boolean global, int slot); + public native void applySettings(); + + static { + System.loadLibrary("duckstation-native"); + } + + static private AndroidHostInterface mInstance; + static public boolean createInstance(Context context) { + mInstance = create(context); + return mInstance != null; + } + + static public AndroidHostInterface getInstance() { + return mInstance; + } } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/DiscRegion.java b/android/app/src/main/java/com/github/stenzek/duckstation/DiscRegion.java new file mode 100644 index 000000000..813d9e717 --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/DiscRegion.java @@ -0,0 +1,8 @@ +package com.github.stenzek.duckstation; + +public enum DiscRegion { + NTSC_J, + NTSC_U, + PAL, + Other +} diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java index 1aecad6dc..15756551c 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java @@ -6,6 +6,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.os.Handler; import android.util.Log; @@ -18,6 +19,7 @@ import android.view.MenuItem; import android.widget.FrameLayout; import androidx.core.app.NavUtils; +import androidx.preference.PreferenceManager; /** * An example full-screen activity that shows and hides the system UI (i.e. @@ -25,9 +27,17 @@ import androidx.core.app.NavUtils; */ public class EmulationActivity extends AppCompatActivity implements SurfaceHolder.Callback { /** - * Interface to the native emulator core + * Settings interfaces. */ - AndroidHostInterface mHostInterface; + SharedPreferences mPreferences; + private boolean getBooleanSetting(String key, boolean defaultValue) { + return mPreferences.getBoolean(key, defaultValue); + } + private void setBooleanSetting(String key, boolean value) { + SharedPreferences.Editor editor = mPreferences.edit(); + editor.putBoolean(key, value); + editor.apply(); + } /** * Touchscreen controller overlay @@ -96,15 +106,16 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Once we get a surface, we can boot. - if (mHostInterface.isEmulationThreadRunning()) { - mHostInterface.surfaceChanged(holder.getSurface(), format, width, height); + if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) { + AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height); return; } String bootPath = getIntent().getStringExtra("bootPath"); String bootSaveStatePath = getIntent().getStringExtra("bootSaveStatePath"); + boolean resumeState = getIntent().getBooleanExtra("resumeState", false); - if (!mHostInterface + if (!AndroidHostInterface.getInstance() .startEmulationThread(holder.getSurface(), bootPath, bootSaveStatePath)) { Log.e("EmulationActivity", "Failed to start emulation thread"); finishActivity(0); @@ -114,16 +125,17 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde @Override public void surfaceDestroyed(SurfaceHolder holder) { - if (!mHostInterface.isEmulationThreadRunning()) + if (!AndroidHostInterface.getInstance().isEmulationThreadRunning()) return; Log.i("EmulationActivity", "Stopping emulation thread"); - mHostInterface.stopEmulationThread(); + AndroidHostInterface.getInstance().stopEmulationThread(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + mPreferences = PreferenceManager.getDefaultSharedPreferences(this); setContentView(R.layout.activity_emulation); ActionBar actionBar = getSupportActionBar(); @@ -142,19 +154,15 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde } }); - mHostInterface = AndroidHostInterface.create(this); - if (mHostInterface == null) - throw new InstantiationError("Failed to create host interface"); - // Create touchscreen controller. FrameLayout activityLayout = findViewById(R.id.frameLayout); mTouchscreenController = new TouchscreenControllerView(this); activityLayout.addView(mTouchscreenController); - mTouchscreenController.init(0, "DigitalController", mHostInterface); + mTouchscreenController.init(0, "DigitalController", AndroidHostInterface.getInstance()); setTouchscreenControllerVisibility(true); // Hook up controller input. - mContentView.initControllerKeyMapping(mHostInterface, "DigitalController"); + mContentView.initControllerKeyMapping("DigitalController"); } @Override @@ -172,6 +180,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_emulation, menu); menu.findItem(R.id.show_controller).setChecked(mTouchscreenControllerVisible); + menu.findItem(R.id.enable_speed_limiter).setChecked(getBooleanSetting("Main/SpeedLimiterEnabled", true)); return true; } @@ -191,6 +200,18 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde setTouchscreenControllerVisibility(!mTouchscreenControllerVisible); item.setChecked(mTouchscreenControllerVisible); return true; + } else if (id == R.id.enable_speed_limiter) { + boolean newSetting = !getBooleanSetting("Main/SpeedLimiterEnabled", true); + setBooleanSetting("Main/SpeedLimiterEnabled", newSetting); + item.setChecked(newSetting); + AndroidHostInterface.getInstance().applySettings(); + return true; + } else if (id == R.id.reset) { + AndroidHostInterface.getInstance().resetSystem(); + } else if (id == R.id.quick_load) { + AndroidHostInterface.getInstance().loadState(false, 0); + } else if (id == R.id.quick_save) { + AndroidHostInterface.getInstance().saveState(false, 0); } else if (id == R.id.quit) { finish(); return true; diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java index 9669d1a77..5a879d947 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java @@ -48,7 +48,6 @@ public class EmulationSurfaceView extends SurfaceView { return super.onKeyDown(keyCode, event); } - private AndroidHostInterface mHostInterface; private ArrayMap mControllerKeyMapping; private void addControllerKeyMapping(int keyCode, String controllerType, String buttonName) { @@ -59,9 +58,7 @@ public class EmulationSurfaceView extends SurfaceView { mControllerKeyMapping.put(keyCode, mapping); } - public void initControllerKeyMapping(AndroidHostInterface hostInterface, - String controllerType) { - mHostInterface = hostInterface; + public void initControllerKeyMapping(String controllerType) { mControllerKeyMapping = new ArrayMap<>(); // TODO: Don't hardcode... @@ -86,7 +83,7 @@ public class EmulationSurfaceView extends SurfaceView { return false; final int mapping = mControllerKeyMapping.get(keyCode); - mHostInterface.setControllerButtonState(0, mapping, pressed); + AndroidHostInterface.getInstance().setControllerButtonState(0, mapping, pressed); return true; } } 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 94ee34279..75d15487b 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 @@ -15,44 +15,21 @@ import androidx.preference.PreferenceManager; import java.util.Set; public class GameList { - static { - System.loadLibrary("duckstation-native"); - } - private Context mContext; - private String mCachePath; - private String mRedumpDatPath; - private String[] mSearchDirectories; - private boolean mSearchRecursively; private GameListEntry[] mEntries; - - static private native GameListEntry[] getEntries(String cachePath, String redumpDatPath, - String[] searchDirectories, - boolean searchRecursively); + private ListViewAdapter mAdapter; public GameList(Context context) { mContext = context; - refresh(); + mAdapter = new ListViewAdapter(); + mEntries = new GameListEntry[0]; } - public void refresh() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext); - mCachePath = preferences.getString("GameList/CachePath", ""); - mRedumpDatPath = preferences.getString("GameList/RedumpDatPath", ""); - - Set searchDirectories = - preferences.getStringSet("GameList/SearchDirectories", null); - if (searchDirectories != null) { - mSearchDirectories = new String[searchDirectories.size()]; - searchDirectories.toArray(mSearchDirectories); - } else { - mSearchDirectories = new String[0]; - } - - mSearchRecursively = preferences.getBoolean("GameList/SearchRecursively", true); - + public void refresh(boolean invalidateCache, boolean invalidateDatabase) { // Search and get entries from native code - mEntries = getEntries(mCachePath, mRedumpDatPath, mSearchDirectories, mSearchRecursively); + AndroidHostInterface.getInstance().refreshGameList(invalidateCache, invalidateDatabase); + mEntries = AndroidHostInterface.getInstance().getGameListEntries(); + mAdapter.notifyDataSetChanged(); } public int getEntryCount() { @@ -97,6 +74,6 @@ public class GameList { } public BaseAdapter getListViewAdapter() { - return new ListViewAdapter(); + return mAdapter; } } 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 5b01e43ce..eb3fd8d63 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 @@ -28,7 +28,7 @@ public class GameListEntry { private String mTitle; private long mSize; private String mModifiedTime; - private ConsoleRegion mRegion; + private DiscRegion mRegion; private EntryType mType; private CompatibilityRating mCompatibilityRating; @@ -42,9 +42,9 @@ public class GameListEntry { mModifiedTime = modifiedTime; try { - mRegion = ConsoleRegion.valueOf(region); + mRegion = DiscRegion.valueOf(region); } catch (IllegalArgumentException e) { - mRegion = ConsoleRegion.NTSC_U; + mRegion = DiscRegion.NTSC_U; } try { @@ -74,7 +74,7 @@ public class GameListEntry { public String getModifiedTime() { return mModifiedTime; } - public ConsoleRegion getRegion() { + public DiscRegion getRegion() { return mRegion; } 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 5135aa94a..8b28a4078 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 @@ -43,22 +43,26 @@ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + if (!AndroidHostInterface.createInstance(this)) { + Log.i("MainActivity", "Failed to create host interface"); + throw new RuntimeException("Failed to create host interface"); + } + setContentView(R.layout.activity_main); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - FloatingActionButton fab = findViewById(R.id.fab); - fab.setOnClickListener(new View.OnClickListener() { + findViewById(R.id.fab_add_game_directory).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - if (!checkForExternalStoragePermissions()) - return; - - Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); - i.addCategory(Intent.CATEGORY_DEFAULT); - i.putExtra(Intent.EXTRA_LOCAL_ONLY, true); - startActivityForResult(Intent.createChooser(i, "Choose directory"), - REQUEST_ADD_DIRECTORY_TO_GAME_LIST); + startAddGameDirectory(); + } + }); + findViewById(R.id.fab_resume).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startEmulation(null, true); } }); @@ -69,7 +73,7 @@ public class MainActivity extends AppCompatActivity { mGameListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - startEmulation(mGameList.getEntry(position).getPath()); + startEmulation(mGameList.getEntry(position).getPath(), true); } }); mGameListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @@ -89,6 +93,18 @@ public class MainActivity extends AppCompatActivity { return true; } }); + mGameList.refresh(false, false); + } + + private void startAddGameDirectory() { + if (!checkForExternalStoragePermissions()) + return; + + Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + i.addCategory(Intent.CATEGORY_DEFAULT); + i.putExtra(Intent.EXTRA_LOCAL_ONLY, true); + startActivityForResult(Intent.createChooser(i, "Choose directory"), + REQUEST_ADD_DIRECTORY_TO_GAME_LIST); } @Override @@ -106,6 +122,13 @@ public class MainActivity extends AppCompatActivity { int id = item.getItemId(); //noinspection SimplifiableIfStatement + if (id == R.id.action_add_game_directory) { + startAddGameDirectory(); + } else if (id == R.id.action_scan_for_new_games) { + mGameList.refresh(false, false); + } if (id == R.id.action_rescan_all_games) { + mGameList.refresh(true, false); + } if (id == R.id.action_settings) { Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); @@ -132,16 +155,16 @@ public class MainActivity extends AppCompatActivity { } SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); - Set currentValues = prefs.getStringSet("GameList/SearchDirectories", null); + Set currentValues = prefs.getStringSet("GameList/RecursivePaths", null); if (currentValues == null) currentValues = new HashSet(); currentValues.add(path); SharedPreferences.Editor editor = prefs.edit(); - editor.putStringSet("GameList/SearchDirectories", currentValues); + editor.putStringSet("GameList/RecursivePaths", currentValues); editor.apply(); Log.i("MainActivity", "Added path '" + path + "' to game list search directories"); - mGameList.refresh(); + mGameList.refresh(false, false); } break; } @@ -175,13 +198,14 @@ public class MainActivity extends AppCompatActivity { } } - private boolean startEmulation(String bootPath) { + private boolean startEmulation(String bootPath, boolean resumeState) { if (!checkForExternalStoragePermissions()) { return false; } Intent intent = new Intent(this, EmulationActivity.class); intent.putExtra("bootPath", bootPath); + intent.putExtra("resumeState", resumeState); startActivity(intent); return true; } diff --git a/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml b/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml new file mode 100644 index 000000000..13c137a92 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_play_arrow_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 39a7c463f..963330c6e 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -24,7 +24,17 @@ + + - - - + + + + + + + + + +