diff --git a/app/build.gradle b/app/build.gradle
index 561df276..c2f72e42 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -6,8 +6,8 @@ android {
applicationId "com.adam.aslfms"
minSdkVersion 14
targetSdkVersion 26
- versionCode = 60
- versionName = "1.6.9"
+ versionCode = 61
+ versionName = "1.7.0"
testApplicationId "com.adam.aslfms.test"
testInstrumentationRunner "android.test.InstrumentationTestRunner"
diff --git a/app/src/main/assets/changelog.txt b/app/src/main/assets/changelog.txt
index cddd7694..f61c0851 100644
--- a/app/src/main/assets/changelog.txt
+++ b/app/src/main/assets/changelog.txt
@@ -4,10 +4,14 @@ For more details.
https://github.com/simple-last-fm-scrobbler/sls/wiki/Privacy-Concerns
For privacy concerns.
+- 1.7.0 (2019-9-22) codename: mingus
+ * Login notification bug
+ * Music App Activity bug
+ * Rate Limit Exceeded status for Last.fm
+
- 1.6.9 (2019-9-15) codename: lingus
* major database patch
-
- 1.6.8 (2019-9-15) codename: kingus
* Database problems fixed
* Database migration
diff --git a/app/src/main/java/com/adam/aslfms/MusicAppsActivity.java b/app/src/main/java/com/adam/aslfms/MusicAppsActivity.java
index dcf00d5c..73dd372f 100644
--- a/app/src/main/java/com/adam/aslfms/MusicAppsActivity.java
+++ b/app/src/main/java/com/adam/aslfms/MusicAppsActivity.java
@@ -186,7 +186,7 @@ private void update() {
break;
case 2:
default:
- appPref.setIcon(android.R.drawable.stat_sys_warning);
+ appPref.setIcon(android.R.drawable.ic_menu_help);
break;
}
@@ -225,8 +225,7 @@ private void setSMASummary(Preference pref, MusicAPI mapi) {
pref.setSummary(getString(R.string.incompatability_short)
.replaceAll("%1", mScrobbleDroidLabel));
} else {
- if (!mapi.getMessage().equals("generic receiver")) pref.setSummary(mapi.getMessage());
- else pref.setSummary("");
+ pref.setSummary(mapi.getMessage());
}
}
}
diff --git a/app/src/main/java/com/adam/aslfms/StatusActivity.java b/app/src/main/java/com/adam/aslfms/StatusActivity.java
index bc8a3b22..f8879c37 100644
--- a/app/src/main/java/com/adam/aslfms/StatusActivity.java
+++ b/app/src/main/java/com/adam/aslfms/StatusActivity.java
@@ -38,6 +38,7 @@
import com.adam.aslfms.service.NetApp;
import com.adam.aslfms.util.AppSettings;
+import com.adam.aslfms.util.AuthStatus;
import com.adam.aslfms.util.ScrobblesDatabase;
import com.adam.aslfms.util.Util;
@@ -139,7 +140,7 @@ private void setupViewPager(ViewPager viewPager) {
TabAdapter adapter = new TabAdapter(getSupportFragmentManager());
for (NetApp napp : NetApp.values()) {
- if (settings.isAuthenticated(napp)) {
+ if(settings.getAuthStatus(napp) != AuthStatus.AUTHSTATUS_NOAUTH) {
adapter.addFragment(StatusFragment.newInstance(napp.getValue()), napp.getName());
}
}
diff --git a/app/src/main/java/com/adam/aslfms/StatusFragment.java b/app/src/main/java/com/adam/aslfms/StatusFragment.java
index af6c19d1..c35599da 100644
--- a/app/src/main/java/com/adam/aslfms/StatusFragment.java
+++ b/app/src/main/java/com/adam/aslfms/StatusFragment.java
@@ -149,7 +149,7 @@ protected void fillData() {
auth.setKey(getString(R.string.logged_in_just));
auth.setValue(settings.getUsername(mNetApp));
} else {
- auth.setKey(getString(R.string.not_logged_in));
+ auth.setKey(Util.getStatusSummary(getContext(), settings, mNetApp));
auth
.setValue(Util.getStatusSummary(getActivity(), settings, mNetApp,
false));
diff --git a/app/src/main/java/com/adam/aslfms/receiver/AbstractPlayStatusReceiver.java b/app/src/main/java/com/adam/aslfms/receiver/AbstractPlayStatusReceiver.java
index 73f86b21..0aa736d5 100644
--- a/app/src/main/java/com/adam/aslfms/receiver/AbstractPlayStatusReceiver.java
+++ b/app/src/main/java/com/adam/aslfms/receiver/AbstractPlayStatusReceiver.java
@@ -30,6 +30,7 @@
import com.adam.aslfms.MusicAppsActivity;
import com.adam.aslfms.R;
import com.adam.aslfms.UserCredActivity;
+import com.adam.aslfms.service.NetApp;
import com.adam.aslfms.service.ScrobblingService;
import com.adam.aslfms.util.AppSettings;
import com.adam.aslfms.util.InternalTrackTransmitter;
@@ -92,9 +93,9 @@ public final void onReceive(Context context, Intent intent) {
bundle = Bundle.EMPTY;
}
+ // start/call the Scrobbling Service
mService = new Intent(context, ScrobblingService.class);
mService.setAction(ScrobblingService.ACTION_PLAYSTATECHANGED);
-
try {
parseIntent(context, action, bundle); // might throw
@@ -114,24 +115,34 @@ public final void onReceive(Context context, Intent intent) {
} else if (mMusicAPI.getEnabledValue() == 2) {
Util.myNotify(context, mMusicAPI.getName(), context.getString(R.string.new_music_app), 12473, new Intent(context, MusicAppsActivity.class));
Log.d(TAG, "App: " + mMusicAPI.getName()
- + " has been ignored, will propagate");
+ + " has been ignored, won't propagate");
+ return;
}
-
// submit track for the ScrobblingService
InternalTrackTransmitter.appendTrack(mTrack);
AppSettings settings = new AppSettings(context);
- // start/call the Scrobbling Service
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && settings.isActiveAppEnabled(Util.checkPower(context))) {
context.startForegroundService(mService);
} else {
context.startService(mService);
}
+
+ // we must be logged in to scrobble
+ if (!settings.isAnyAuthenticated()) {
+ Intent i = new Intent(context, UserCredActivity.class);
+ i.putExtra("netapp", NetApp.LASTFM.getIntentExtraValue());
+ Util.myNotify(context, context.getResources().getString(R.string.warning) , context.getResources().getString(R.string.not_logged_in),05233, i);
+ Log
+ .d(TAG,
+ "The user has not authenticated, won't propagate the submission request");
+ return;
+ }
} catch (IllegalArgumentException e) {
Log.i(TAG, "Got a bad track from: "
+ ((mMusicAPI == null) ? "null" : mMusicAPI.getName())
+ ", ignoring it (" + e.getMessage() + ")");
}
-
}
/**
diff --git a/app/src/main/java/com/adam/aslfms/receiver/MusicAPI.java b/app/src/main/java/com/adam/aslfms/receiver/MusicAPI.java
index beff4f00..8f46679f 100644
--- a/app/src/main/java/com/adam/aslfms/receiver/MusicAPI.java
+++ b/app/src/main/java/com/adam/aslfms/receiver/MusicAPI.java
@@ -297,7 +297,7 @@ public static MusicAPI fromReceiver(Context ctx, String name, String pkg,
vals.put("pkg", pkg);
vals.put("msg", msg);
vals.put("sdclash", clashWithScrobbleDroid ? 1 : 0);
- if (msg.equals("generic receiver")) vals.put("enabled", 2);
+ if (msg != null && msg.equals("generic receiver")) vals.put("enabled", 2);
else vals.put("enabled", 1);
long id = db.insert("music_api", null, vals);
@@ -309,7 +309,7 @@ public static MusicAPI fromReceiver(Context ctx, String name, String pkg,
Log.d(TAG, "new mapiinserted into db");
}
- if (msg.equals("generic receiver")) mapi = new MusicAPI(id, name, pkg, msg, clashWithScrobbleDroid, 2);
+ if (msg != null && msg.equals("generic receiver")) mapi = new MusicAPI(id, name, pkg, msg, clashWithScrobbleDroid, 2);
else mapi = new MusicAPI(id, name, pkg, msg, clashWithScrobbleDroid, 1);
Log.d(TAG, mapi.toString());
}
diff --git a/app/src/main/java/com/adam/aslfms/service/Handshaker.java b/app/src/main/java/com/adam/aslfms/service/Handshaker.java
index fed4731d..7af090e1 100644
--- a/app/src/main/java/com/adam/aslfms/service/Handshaker.java
+++ b/app/src/main/java/com/adam/aslfms/service/Handshaker.java
@@ -508,13 +508,13 @@ public HandshakeResult handshake() throws BadAuthException,
}
} else if (jObject.has("error")) {
int code = jObject.getInt("error");
- if (code == 4 || code == 6) {
+ if (code == 9) {
Log.e(TAG, "Handshake fails: wrong username/password");
+ settings.setSessionKey(netApp, "");
throw new BadAuthException(getContext().getString(
R.string.auth_bad_auth));
} else if (code == 26 || code == 10) {
Log.e(TAG, "Handshake fails: client banned: " + netAppName);
- settings.setSessionKey(netApp, "");
throw new ClientBannedException(getContext().getString(
R.string.auth_client_banned));
} else {
diff --git a/app/src/main/java/com/adam/aslfms/service/Heart.java b/app/src/main/java/com/adam/aslfms/service/Heart.java
index 4bad59aa..bb020b56 100644
--- a/app/src/main/java/com/adam/aslfms/service/Heart.java
+++ b/app/src/main/java/com/adam/aslfms/service/Heart.java
@@ -55,11 +55,14 @@ public class Heart extends NetRunnable {
protected ScrobblesDatabase db;
protected AppSettings settings;
+ protected NetApp napp;
+
Context mCtx;
public Heart(NetApp napp, Context ctx, Networker net, ScrobblesDatabase db) {
super(napp, ctx, net);
+ this.napp = napp;
this.db = db;
this.mCtx = ctx;
}
@@ -71,11 +74,11 @@ public final void run() {
// can't heart track
- String[][] strings = db.fetchHeartsArray();
+ String[][] strings = db.fetchHeartsArray(napp);
for (String[] s : strings) {
- boolean failure = false;
+ boolean submitted = false;
String sigText = "api_key"
+ settings.rcnvK(settings.getAPIkey())
+ "artist" + s[1]
@@ -91,6 +94,7 @@ public final void run() {
// TODO: ascertain if string is Json
if (response.equals("okSuccess")) {
Log.d(TAG, "Successful heart track: " + getNetApp().getName());
+ submitted = true;
} else {
JSONObject jObject = new JSONObject(response);
if (jObject.has("error")) {
@@ -100,15 +104,13 @@ public final void run() {
}
} else {
Log.d(TAG, "Failed heart track.");
- failure = true;
}
}
} catch (Exception e) {
Log.e(TAG, "Heart track fail " + e);
//e.printStackTrace();
- failure = true;
}
- if (failure) db.deleteHeart(s);
+ if (submitted) db.deleteHeart(s);
}
}
diff --git a/app/src/main/java/com/adam/aslfms/service/NPNotifier.java b/app/src/main/java/com/adam/aslfms/service/NPNotifier.java
index d1b7decb..9bbe0747 100644
--- a/app/src/main/java/com/adam/aslfms/service/NPNotifier.java
+++ b/app/src/main/java/com/adam/aslfms/service/NPNotifier.java
@@ -105,6 +105,15 @@ protected boolean doRun(HandshakeResult hInfo) {
notifySubmissionStatusFailure(getContext().getString(
R.string.auth_network_error_retrying));
ret = false;
+ } catch (AuthStatus.RetryLaterFailureException e){
+ Log.i(TAG, "Tempfail: " + e.getMessage() + ": "
+ + getNetApp().getName());
+ notifyAuthStatusUpdate(AuthStatus.AUTHSTATUS_RETRYLATER_RATE_LIMIT_EXCEEDED);
+ notifySubmissionStatusFailure(getContext().getString(
+ R.string.auth_rate_limit_exceeded));
+ getNetworker().launchSleeper();
+ e.getStackTrace();
+ ret = false;
} catch (AuthStatus.ClientBannedException e) {
Log.e(TAG, "This version of the client has been banned!!" + ": "
+ getNetApp().getName());
@@ -163,7 +172,7 @@ private void notifyAuthStatusUpdate(int st) {
* @throws UnknownResponseException {@link UnknownResponseException}
*/
public void notifyNowPlaying(Track track, HandshakeResult hInfo)
- throws BadSessionException, TemporaryFailureException, AuthStatus.ClientBannedException, AuthStatus.UnknownResponseException {
+ throws BadSessionException, TemporaryFailureException, AuthStatus.ClientBannedException, AuthStatus.UnknownResponseException, AuthStatus.RetryLaterFailureException {
NetApp netApp = getNetApp();
String netAppName = netApp.getName();
@@ -465,17 +474,20 @@ public void notifyNowPlaying(Track track, HandshakeResult hInfo)
Log.i(TAG, "Now Playing success: " + netAppName);
} else if (jObject.has("error")) {
int code = jObject.getInt("error");
- if (code == 26 || code == 10) {
- Log.e(TAG, "Now Playing failed: client banned: " + netAppName);
- settings.setSessionKey(netApp, "");
- throw new AuthStatus.ClientBannedException("Now Playing failed because of client banned");
+ if (code == 26 || code == 10 ) {
+ Log.e(TAG, "Now playing failed: client banned: " + netApp.getName());
+ throw new AuthStatus.ClientBannedException("Now playing failed because of client banned");
} else if (code == 9) {
- Log.e(TAG, "Now Playing failed: bad auth: " + netAppName);
+ Log.i(TAG, "Now playing failed: bad auth: " + netApp.getName());
settings.setSessionKey(netApp, "");
- throw new BadSessionException("Now Playing failed because of badsession");
+ throw new BadSessionException("Now playing failed because of badsession");
+ } else if (code == 29) {
+ Log.i(TAG, "Now playing failed: rate limit exceeded: " + netApp.getName());
+ throw new AuthStatus.RetryLaterFailureException("Now playing failed because of client rate limit");
} else {
- Log.e(TAG, "Now Playing fails: FAILED " + response + ": " + netAppName);
+ Log.e(TAG, "Now playing fails: FAILED " + response + ": " + netApp.getName());
//settings.setSessionKey(netApp, "");
+
throw new TemporaryFailureException("Now playing failed because of " + response);
}
} else {
diff --git a/app/src/main/java/com/adam/aslfms/service/Scrobbler.java b/app/src/main/java/com/adam/aslfms/service/Scrobbler.java
index ddd62843..ae0c1b9e 100644
--- a/app/src/main/java/com/adam/aslfms/service/Scrobbler.java
+++ b/app/src/main/java/com/adam/aslfms/service/Scrobbler.java
@@ -138,9 +138,19 @@ public boolean doRun(HandshakeResult hInfo) {
Util.myNotify(mCtx, getNetApp().getName(),
mCtx.getString(R.string.auth_bad_auth), 39201, new Intent(mCtx, SettingsActivity.class));
ret = true;
+ } catch (AuthStatus.RetryLaterFailureException e){
+ Log.i(TAG, "Tempfail: " + e.getMessage() + ": "
+ + getNetApp().getName());
+ notifyAuthStatusUpdate(AuthStatus.AUTHSTATUS_RETRYLATER);
+ notifySubmissionStatusFailure(getContext().getString(
+ R.string.auth_rate_limit_exceeded));
+ getNetworker().launchSleeper();
+ e.getStackTrace();
+ ret = false;
} catch (TemporaryFailureException e) {
Log.i(TAG, "Tempfail: " + e.getMessage() + ": "
+ getNetApp().getName());
+ notifyAuthStatusUpdate(AuthStatus.AUTHSTATUS_RETRYLATER);
notifySubmissionStatusFailure(getContext().getString(
R.string.auth_network_error_retrying));
e.getStackTrace();
@@ -200,7 +210,7 @@ private void notifyAuthStatusUpdate(int st) {
* @throws AuthStatus.BadSessionException
*/
public void scrobbleCommit(HandshakeResult hInfo, Track[] tracks)
- throws BadSessionException, TemporaryFailureException, AuthStatus.ClientBannedException, AuthStatus.UnknownResponseException {
+ throws BadSessionException, TemporaryFailureException, AuthStatus.ClientBannedException, AuthStatus.UnknownResponseException, AuthStatus.RetryLaterFailureException {
NetApp netApp = getNetApp();
String netAppName = netApp.getName();
@@ -551,14 +561,16 @@ public void scrobbleCommit(HandshakeResult hInfo, Track[] tracks)
Log.i(TAG, "Scrobble success: " + netAppName + ": Ignored Count: " + Integer.toString(scrobsIgnored));
} else if (jObject.has("error")) {
int code = jObject.getInt("error");
- if (code == 26 || code == 10) {
- Log.e(TAG, "Scobble failed: client banned: " + netApp.getName());
- settings.setSessionKey(netApp, "");
- throw new AuthStatus.ClientBannedException("Now Playing failed because of client banned");
+ if (code == 26 || code == 10 || code == 15) { // code 15 is for token has expired
+ Log.e(TAG, "Scrobble failed: client banned: " + netApp.getName());
+ throw new AuthStatus.ClientBannedException("Scrobbling failed because of client banned");
} else if (code == 9) {
Log.i(TAG, "Scrobble failed: bad auth: " + netApp.getName());
settings.setSessionKey(netApp, "");
- throw new BadSessionException("Now Playing failed because of badsession");
+ throw new BadSessionException("Scrobbling failed because of badsession");
+ } else if (code == 29) {
+ Log.i(TAG, "Scrobble failed: rate limit exceeded: " + netApp.getName());
+ throw new AuthStatus.RetryLaterFailureException("Scrobbling failed because of client rate limit");
} else {
Log.e(TAG, "Scrobble fails: FAILED " + response + ": " + netApp.getName());
//settings.setSessionKey(netApp, "");
diff --git a/app/src/main/java/com/adam/aslfms/service/ScrobblingService.java b/app/src/main/java/com/adam/aslfms/service/ScrobblingService.java
index ef5122de..031528b4 100644
--- a/app/src/main/java/com/adam/aslfms/service/ScrobblingService.java
+++ b/app/src/main/java/com/adam/aslfms/service/ScrobblingService.java
@@ -286,21 +286,6 @@ private synchronized void onPlayStateChanged(Track track, Track.State state) {
tryNotifyNP(mCurrentTrack);
foreGroundService();
- // we must be logged in to scrobble
- if (!settings.isAnyAuthenticated()) {
-
- Intent intent = new Intent(mCtx, UserCredActivity.class);
- for (NetApp netApp : NetApp.values()) {
- if (settings.isAuthenticated(netApp)) {
- intent.putExtra("netappid", netApp.getIntentExtraValue());
- break;
- }
- }
- Util.myNotify(this, this.getResources().getString(R.string.warning) , this.getResources().getString(R.string.not_logged_in),05233, intent);
- Log
- .d(TAG,
- "The user has not authenticated, won't propagate the submission request");
- }
} else if (state == Track.State.PAUSE) { // pause
// TODO: test this state
if (mCurrentTrack == null) {
diff --git a/app/src/main/java/com/adam/aslfms/util/AuthStatus.java b/app/src/main/java/com/adam/aslfms/util/AuthStatus.java
index 2a59c048..7ef097bb 100644
--- a/app/src/main/java/com/adam/aslfms/util/AuthStatus.java
+++ b/app/src/main/java/com/adam/aslfms/util/AuthStatus.java
@@ -37,6 +37,7 @@ public class AuthStatus {
public static final int AUTHSTATUS_OK = 5;
public static final int AUTHSTATUS_CLIENTBANNED = 6;
public static final int AUTHSTATUS_NETWORKUNFIT = 7;
+ public static final int AUTHSTATUS_RETRYLATER_RATE_LIMIT_EXCEEDED = 8;
public static class StatusException extends Exception {
private static final long serialVersionUID = 7204759787220898684L;
@@ -78,6 +79,15 @@ public TemporaryFailureException(String detailMessage) {
}
}
+ public static class RetryLaterFailureException extends StatusException {
+
+ private static final long serialVersionUID = 8815752389387226812L;
+
+ public RetryLaterFailureException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
public static class UnknownResponseException extends StatusException {
private static final long serialVersionUID = 7351097754868391707L;
diff --git a/app/src/main/java/com/adam/aslfms/util/ScrobblesDatabase.java b/app/src/main/java/com/adam/aslfms/util/ScrobblesDatabase.java
index fe7a79d8..1c9202e4 100644
--- a/app/src/main/java/com/adam/aslfms/util/ScrobblesDatabase.java
+++ b/app/src/main/java/com/adam/aslfms/util/ScrobblesDatabase.java
@@ -356,13 +356,12 @@ public long verifyOrUpdateScrobblesAlreadyInCache(NetApp napp){
ContentValues iVals = new ContentValues();
iVals.put("netappid", napp.getValue());
iVals.put("trackid", c.getInt(c.getColumnIndex("_id")));
- Cursor c2 = mDb.rawQuery("SELECT * FROM " + TABLENAME_CORRNETAPP_REPAIRED + " WHERE " + scrobbles_netapp_strings[0] + " =? AND " + scrobbles_netapp_strings[1] + " =? ", new String[] { Integer.toString(napp.getValue()), Integer.toString(c.getInt(c.getColumnIndex("_id")))});
- if (c2.moveToFirst()){
- // do nothing
- } else {
- temp = mDb.insert(TABLENAME_CORRNETAPP_REPAIRED, null, iVals);
- count += temp < 0 ? 1 : 0;
- }
+ // insert not duplicated scrobble for newly authenticated app
+ mDb.execSQL("insert into " + TABLENAME_CORRNETAPP_REPAIRED + "(" + scrobbles_netapp_strings[0] + "," + scrobbles_netapp_strings[1] +
+ ") SELECT ?, ? WHERE NOT EXISTS ( SELECT 1 FROM " + TABLENAME_CORRNETAPP_REPAIRED +
+ " WHERE " + scrobbles_netapp_strings[0] + " =? AND " + scrobbles_netapp_strings[1] + " =? )",
+ new String[] { Integer.toString(napp.getValue()), Integer.toString(c.getInt(c.getColumnIndex("_id"))),
+ Integer.toString(napp.getValue()), Integer.toString(c.getInt(c.getColumnIndex("_id")))});
c.moveToNext();
}
return count;
@@ -474,7 +473,7 @@ public Track[] fetchTracksArray(NetApp napp, int maxFetch) {
Cursor c;
// try {
String sql = "select * from scrobbles, " + TABLENAME_CORRNETAPP_REPAIRED
- + " where scrobbles._id = trackid and netappid = " + napp.getValue();
+ + " where scrobbles._id = trackid and sentstatus = '' and netappid = " + napp.getValue();
c = mDb.rawQuery(sql, null);
/*
* } catch (SQLiteException e) { Log.e(TAG,
@@ -495,10 +494,10 @@ public Track[] fetchTracksArray(NetApp napp, int maxFetch) {
return tracks;
}
- public String[][] fetchHeartsArray(){
+ public String[][] fetchHeartsArray(NetApp netApp){
Cursor c;
// try {
- String sql = "select * from " + TABLENAME_HEARTS ;
+ String sql = "select * from " + TABLENAME_HEARTS + " where netapp = " + netApp.getValue() ;
c = mDb.rawQuery(sql, null);
int count = c.getCount();
diff --git a/app/src/main/java/com/adam/aslfms/util/Util.java b/app/src/main/java/com/adam/aslfms/util/Util.java
index f6103112..d49a8976 100644
--- a/app/src/main/java/com/adam/aslfms/util/Util.java
+++ b/app/src/main/java/com/adam/aslfms/util/Util.java
@@ -442,6 +442,8 @@ public static String getStatusSummary(Context ctx, AppSettings settings,
return ctx.getString(R.string.auth_internal_error);
} else if (settings.getAuthStatus(napp) == AuthStatus.AUTHSTATUS_RETRYLATER) {
return ctx.getString(R.string.auth_network_error_retrying);
+ } else if (settings.getAuthStatus(napp) == AuthStatus.AUTHSTATUS_RETRYLATER_RATE_LIMIT_EXCEEDED) {
+ return ctx.getString(R.string.auth_network_error_retrying);
} else if (settings.getAuthStatus(napp) == AuthStatus.AUTHSTATUS_NETWORKUNFIT) {
return ctx.getString(R.string.auth_network_unfit);
} else if (settings.getAuthStatus(napp) == AuthStatus.AUTHSTATUS_OK) {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 10a20e9e..99a89215 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -81,6 +81,7 @@
Server error: %1, retrying…
Client side timing error, retrying…
Network error, retrying…
+ Rate limit exceeded
Not allowed to submit data on network, change in Options screen
Authenticating…
Internal error