diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java
index c9d00cc225..199d80cf72 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/NativeLibrary.java
@@ -326,6 +326,11 @@ public final class NativeLibrary
*/
public static native void LoadStateAs(String path);
+ /**
+ * Returns when the savestate in the given slot was created, or 0 if the slot is empty.
+ */
+ public static native long GetUnixTimeOfStateSlot(int slot);
+
/**
* Sets the current working user directory
* If not set, it auto-detects a location
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java
index 9a145d9dce..3454b6d83d 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/SaveLoadStateFragment.java
@@ -1,6 +1,7 @@
package org.dolphinemu.dolphinemu.fragments;
import android.os.Bundle;
+import android.text.format.DateUtils;
import android.util.SparseIntArray;
import android.view.LayoutInflater;
import android.view.View;
@@ -11,6 +12,7 @@ import android.widget.GridLayout;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.EmulationActivity;
@@ -22,40 +24,35 @@ public final class SaveLoadStateFragment extends Fragment implements View.OnClic
}
private static final String KEY_SAVEORLOAD = "saveorload";
- private static SparseIntArray saveButtonsActionsMap = new SparseIntArray();
+
+ private static int[] saveActionsMap = new int[]{
+ EmulationActivity.MENU_ACTION_SAVE_SLOT1,
+ EmulationActivity.MENU_ACTION_SAVE_SLOT2,
+ EmulationActivity.MENU_ACTION_SAVE_SLOT3,
+ EmulationActivity.MENU_ACTION_SAVE_SLOT4,
+ EmulationActivity.MENU_ACTION_SAVE_SLOT5,
+ EmulationActivity.MENU_ACTION_SAVE_SLOT6,
+ };
+
+ private static int[] loadActionsMap = new int[]{
+ EmulationActivity.MENU_ACTION_LOAD_SLOT1,
+ EmulationActivity.MENU_ACTION_LOAD_SLOT2,
+ EmulationActivity.MENU_ACTION_LOAD_SLOT3,
+ EmulationActivity.MENU_ACTION_LOAD_SLOT4,
+ EmulationActivity.MENU_ACTION_LOAD_SLOT5,
+ EmulationActivity.MENU_ACTION_LOAD_SLOT6,
+ };
+
+ private static SparseIntArray buttonsMap = new SparseIntArray();
static
{
- saveButtonsActionsMap
- .append(R.id.loadsave_state_button_1, EmulationActivity.MENU_ACTION_SAVE_SLOT1);
- saveButtonsActionsMap
- .append(R.id.loadsave_state_button_2, EmulationActivity.MENU_ACTION_SAVE_SLOT2);
- saveButtonsActionsMap
- .append(R.id.loadsave_state_button_3, EmulationActivity.MENU_ACTION_SAVE_SLOT3);
- saveButtonsActionsMap
- .append(R.id.loadsave_state_button_4, EmulationActivity.MENU_ACTION_SAVE_SLOT4);
- saveButtonsActionsMap
- .append(R.id.loadsave_state_button_5, EmulationActivity.MENU_ACTION_SAVE_SLOT5);
- saveButtonsActionsMap
- .append(R.id.loadsave_state_button_6, EmulationActivity.MENU_ACTION_SAVE_SLOT6);
- }
-
- private static SparseIntArray loadButtonsActionsMap = new SparseIntArray();
-
- static
- {
- loadButtonsActionsMap
- .append(R.id.loadsave_state_button_1, EmulationActivity.MENU_ACTION_LOAD_SLOT1);
- loadButtonsActionsMap
- .append(R.id.loadsave_state_button_2, EmulationActivity.MENU_ACTION_LOAD_SLOT2);
- loadButtonsActionsMap
- .append(R.id.loadsave_state_button_3, EmulationActivity.MENU_ACTION_LOAD_SLOT3);
- loadButtonsActionsMap
- .append(R.id.loadsave_state_button_4, EmulationActivity.MENU_ACTION_LOAD_SLOT4);
- loadButtonsActionsMap
- .append(R.id.loadsave_state_button_5, EmulationActivity.MENU_ACTION_LOAD_SLOT5);
- loadButtonsActionsMap
- .append(R.id.loadsave_state_button_6, EmulationActivity.MENU_ACTION_LOAD_SLOT6);
+ buttonsMap.append(R.id.loadsave_state_button_1, 0);
+ buttonsMap.append(R.id.loadsave_state_button_2, 1);
+ buttonsMap.append(R.id.loadsave_state_button_3, 2);
+ buttonsMap.append(R.id.loadsave_state_button_4, 3);
+ buttonsMap.append(R.id.loadsave_state_button_5, 4);
+ buttonsMap.append(R.id.loadsave_state_button_6, 5);
}
private SaveOrLoad mSaveOrLoad;
@@ -88,6 +85,7 @@ public final class SaveLoadStateFragment extends Fragment implements View.OnClic
for (int childIndex = 0; childIndex < grid.getChildCount(); childIndex++)
{
Button button = (Button) grid.getChildAt(childIndex);
+ setButtonText(button, childIndex);
button.setOnClickListener(this);
}
@@ -99,20 +97,31 @@ public final class SaveLoadStateFragment extends Fragment implements View.OnClic
@SuppressWarnings("WrongConstant")
@Override
- public void onClick(View button)
+ public void onClick(View view)
{
- int action = 0;
- switch (mSaveOrLoad)
+ int buttonIndex = buttonsMap.get(view.getId(), -1);
+
+ int action = (mSaveOrLoad == SaveOrLoad.SAVE ? saveActionsMap : loadActionsMap)[buttonIndex];
+ ((EmulationActivity) getActivity()).handleMenuAction(action);
+
+ // The savestate most likely hasn't gotten saved yet (it happens asynchronously),
+ // so we unfortunately can't rely on setButtonText/GetUnixTimeOfStateSlot here.
+ Button button = (Button) view;
+ CharSequence time = DateUtils.getRelativeTimeSpanString(0, 0, DateUtils.MINUTE_IN_MILLIS);
+ button.setText(getString(R.string.emulation_state_slot, buttonIndex + 1, time));
+ }
+
+ private void setButtonText(Button button, int index)
+ {
+ long creationTime = NativeLibrary.GetUnixTimeOfStateSlot(index);
+ if (creationTime != 0)
{
- case SAVE:
- action = saveButtonsActionsMap.get(button.getId(), -1);
- break;
- case LOAD:
- action = loadButtonsActionsMap.get(button.getId(), -1);
+ CharSequence relativeTime = DateUtils.getRelativeTimeSpanString(creationTime);
+ button.setText(getString(R.string.emulation_state_slot, index + 1, relativeTime));
}
- if (action >= 0)
+ else
{
- ((EmulationActivity) getActivity()).handleMenuAction(action);
+ button.setText(getString(R.string.emulation_state_slot_empty, index + 1));
}
}
}
diff --git a/Source/Android/app/src/main/res/layout/fragment_saveload_state.xml b/Source/Android/app/src/main/res/layout/fragment_saveload_state.xml
index d8c0f6354a..3d493a64c5 100644
--- a/Source/Android/app/src/main/res/layout/fragment_saveload_state.xml
+++ b/Source/Android/app/src/main/res/layout/fragment_saveload_state.xml
@@ -11,42 +11,36 @@
android:id="@+id/loadsave_state_button_1"
android:layout_width="128dp"
android:layout_height="96dp"
- android:text="@string/emulation_slot1"
style="@style/OverlayInGameMenuOption"/>
diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml
index e2ca4df7e2..902665ba05 100644
--- a/Source/Android/app/src/main/res/values/strings.xml
+++ b/Source/Android/app/src/main/res/values/strings.xml
@@ -380,12 +380,8 @@ It can efficiently compress both junk data and encrypted Wii data.
Save State
Load State
Exit Emulation
- Slot 1
- Slot 2
- Slot 3
- Slot 4
- Slot 5
- Slot 6
+ Slot %1$d\n\n%2$s
+ Slot %1$d\n\nEmpty
Quick Save
Quick Load
Refresh Wii Remotes
diff --git a/Source/Android/app/src/main/res/values/styles.xml b/Source/Android/app/src/main/res/values/styles.xml
index 5bb491c418..9d05da9d09 100644
--- a/Source/Android/app/src/main/res/values/styles.xml
+++ b/Source/Android/app/src/main/res/values/styles.xml
@@ -78,6 +78,9 @@
diff --git a/Source/Android/jni/MainAndroid.cpp b/Source/Android/jni/MainAndroid.cpp
index 7acc59b16b..24eb23b75e 100644
--- a/Source/Android/jni/MainAndroid.cpp
+++ b/Source/Android/jni/MainAndroid.cpp
@@ -333,6 +333,12 @@ JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_LoadStateAs(
State::LoadAs(GetJString(env, path));
}
+JNIEXPORT jlong JNICALL Java_org_dolphinemu_dolphinemu_NativeLibrary_GetUnixTimeOfStateSlot(
+ JNIEnv* env, jobject obj, jint slot)
+{
+ return static_cast(State::GetUnixTimeOfSlot(slot));
+}
+
JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_utils_DirectoryInitialization_SetSysDirectory(
JNIEnv* env, jobject obj, jstring jPath)
{
diff --git a/Source/Core/Core/State.cpp b/Source/Core/Core/State.cpp
index d5c6bf74c0..29a79f9e42 100644
--- a/Source/Core/Core/State.cpp
+++ b/Source/Core/Core/State.cpp
@@ -478,6 +478,17 @@ std::string GetInfoStringOfSlot(int slot, bool translate)
return Common::Timer::GetDateTimeFormatted(header.time);
}
+u64 GetUnixTimeOfSlot(int slot)
+{
+ State::StateHeader header;
+ if (!ReadHeader(MakeStateFilename(slot), header))
+ return 0;
+
+ constexpr u64 MS_PER_SEC = 1000;
+ return static_cast(header.time * MS_PER_SEC) +
+ (Common::Timer::DOUBLE_TIME_OFFSET * MS_PER_SEC);
+}
+
static void LoadFileStateData(const std::string& filename, std::vector& ret_data)
{
Flush();
diff --git a/Source/Core/Core/State.h b/Source/Core/Core/State.h
index d8c4b0e8ed..9fcb9d1773 100644
--- a/Source/Core/Core/State.h
+++ b/Source/Core/Core/State.h
@@ -36,6 +36,9 @@ bool ReadHeader(const std::string& filename, StateHeader& header);
// which can be presented to the user for identification purposes
std::string GetInfoStringOfSlot(int slot, bool translate = true);
+// Returns when the savestate in the given slot was created, or 0 if the slot is empty.
+u64 GetUnixTimeOfSlot(int slot);
+
// These don't happen instantly - they get scheduled as events.
// ...But only if we're not in the main CPU thread.
// If we're in the main CPU thread then they run immediately instead