mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2025-01-09 05:24:44 +01:00
track all pending service Intents; warn of stalls
When enqueuing Intents for any of the three services, cache the Intent. When onHandleWork() is called, remove the equivalent cached intent. Then periodically check for intents that have been stored for more than 60 seconds, and post a Notification alerting user to stall. The user will likely have noticed that messages aren't flowing, so this will simply explain the problem. Includes an "email author" button.
This commit is contained in:
parent
f5afba8fd4
commit
1291310a0c
10 changed files with 236 additions and 50 deletions
|
@ -30,10 +30,12 @@ import android.app.NotificationManager;
|
||||||
public class Channels {
|
public class Channels {
|
||||||
|
|
||||||
enum ID {
|
enum ID {
|
||||||
FOREGROUND(R.string.foreground_channel_expl,
|
NBSPROXY(R.string.nbsproxy_channel_expl,
|
||||||
NotificationManager.IMPORTANCE_LOW),
|
NotificationManager.IMPORTANCE_LOW),
|
||||||
GAME_EVENT(R.string.gameevent_channel_expl,
|
GAME_EVENT(R.string.gameevent_channel_expl,
|
||||||
NotificationManager.IMPORTANCE_LOW);
|
NotificationManager.IMPORTANCE_LOW),
|
||||||
|
SERVICE_STALL(R.string.servicestall_channel_expl,
|
||||||
|
NotificationManager.IMPORTANCE_LOW);
|
||||||
|
|
||||||
private int mExpl;
|
private int mExpl;
|
||||||
private int mImportance;
|
private int mImportance;
|
||||||
|
|
|
@ -2449,6 +2449,23 @@ public class DBUtils {
|
||||||
return dflt;
|
return dflt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setLongFor( Context context, String key, long value )
|
||||||
|
{
|
||||||
|
// Log.d( TAG, "DBUtils.setIntFor(key=%s, val=%d)", key, value );
|
||||||
|
String asStr = String.format( "%d", value );
|
||||||
|
setStringFor( context, key, asStr );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getLongFor( Context context, String key, long dflt )
|
||||||
|
{
|
||||||
|
String asStr = getStringFor( context, key, null );
|
||||||
|
if ( null != asStr ) {
|
||||||
|
dflt = Long.parseLong( asStr );
|
||||||
|
}
|
||||||
|
// Log.d( TAG, "DBUtils.getIntFor(key=%s)=>%d", key, dflt );
|
||||||
|
return dflt;
|
||||||
|
}
|
||||||
|
|
||||||
public static void setBoolFor( Context context, String key, boolean value )
|
public static void setBoolFor( Context context, String key, boolean value )
|
||||||
{
|
{
|
||||||
// Log.df( "DBUtils.setBoolFor(key=%s, val=%b)", key, value );
|
// Log.df( "DBUtils.setBoolFor(key=%s, val=%b)", key, value );
|
||||||
|
|
|
@ -56,6 +56,7 @@ public class DlgDelegate {
|
||||||
SET_HIDE_NEWGAME_BUTTONS,
|
SET_HIDE_NEWGAME_BUTTONS,
|
||||||
DWNLD_LOC_DICT,
|
DWNLD_LOC_DICT,
|
||||||
NEW_GAME_DFLT_NAME,
|
NEW_GAME_DFLT_NAME,
|
||||||
|
SEND_EMAIL,
|
||||||
|
|
||||||
// BoardDelegate
|
// BoardDelegate
|
||||||
UNDO_LAST_ACTION,
|
UNDO_LAST_ACTION,
|
||||||
|
|
|
@ -99,6 +99,7 @@ public class GamesListDelegate extends ListDelegateBase
|
||||||
private static final String REMATCH_P2PADDR_EXTRA = "rm_p2pma";
|
private static final String REMATCH_P2PADDR_EXTRA = "rm_p2pma";
|
||||||
|
|
||||||
private static final String ALERT_MSG = "alert_msg";
|
private static final String ALERT_MSG = "alert_msg";
|
||||||
|
private static final String WITH_EMAIL = "with_email";
|
||||||
|
|
||||||
private static class MySIS implements Serializable {
|
private static class MySIS implements Serializable {
|
||||||
public MySIS(){
|
public MySIS(){
|
||||||
|
@ -1352,6 +1353,10 @@ public class GamesListDelegate extends ListDelegateBase
|
||||||
askDefaultName();
|
askDefaultName();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case SEND_EMAIL:
|
||||||
|
Utils.emailAuthor( m_activity );
|
||||||
|
break;
|
||||||
|
|
||||||
case ASKED_PHONE_STATE:
|
case ASKED_PHONE_STATE:
|
||||||
rematchWithNameAndPerm( true, params );
|
rematchWithNameAndPerm( true, params );
|
||||||
break;
|
break;
|
||||||
|
@ -2297,7 +2302,13 @@ public class GamesListDelegate extends ListDelegateBase
|
||||||
{
|
{
|
||||||
String msg = intent.getStringExtra( ALERT_MSG );
|
String msg = intent.getStringExtra( ALERT_MSG );
|
||||||
if ( null != msg ) {
|
if ( null != msg ) {
|
||||||
makeOkOnlyBuilder( msg ).show();
|
DlgDelegate.DlgDelegateBuilder builder =
|
||||||
|
makeOkOnlyBuilder( msg );
|
||||||
|
if ( intent.getBooleanExtra( WITH_EMAIL, false ) ) {
|
||||||
|
builder.setActionPair( Action.SEND_EMAIL,
|
||||||
|
R.string.board_menu_file_email );
|
||||||
|
}
|
||||||
|
builder.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2781,6 +2792,13 @@ public class GamesListDelegate extends ListDelegateBase
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Intent makeAlertWithEmailIntent( Context context, String msg )
|
||||||
|
{
|
||||||
|
return makeAlertIntent( context, msg )
|
||||||
|
.putExtra( WITH_EMAIL, true )
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
public static void sendNFCToSelf( Context context, String data )
|
public static void sendNFCToSelf( Context context, String data )
|
||||||
{
|
{
|
||||||
Intent intent = makeSelfIntent( context );
|
Intent intent = makeSelfIntent( context );
|
||||||
|
|
|
@ -68,9 +68,6 @@ public class MainActivity extends XWActivity
|
||||||
Log.e( TAG, "isTaskRoot() => false!!! What to do?" );
|
Log.e( TAG, "isTaskRoot() => false!!! What to do?" );
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils.setLaunchedSinceInstall( this, true );
|
|
||||||
Utils.cancelLaunchSinceInstall( this );
|
|
||||||
|
|
||||||
m_dpEnabled = XWPrefs.getIsTablet( this );
|
m_dpEnabled = XWPrefs.getIsTablet( this );
|
||||||
|
|
||||||
m_dlgt = m_dpEnabled ? new DualpaneDelegate( this, savedInstanceState )
|
m_dlgt = m_dpEnabled ? new DualpaneDelegate( this, savedInstanceState )
|
||||||
|
|
|
@ -35,8 +35,6 @@ public class OnBootReceiver extends BroadcastReceiver {
|
||||||
Log.d( TAG, "got %s", action );
|
Log.d( TAG, "got %s", action );
|
||||||
switch( action ) {
|
switch( action ) {
|
||||||
case Intent.ACTION_MY_PACKAGE_REPLACED:
|
case Intent.ACTION_MY_PACKAGE_REPLACED:
|
||||||
Utils.setLaunchedSinceInstall( context, false );
|
|
||||||
// FALLTHRU
|
|
||||||
case Intent.ACTION_BOOT_COMPLETED:
|
case Intent.ACTION_BOOT_COMPLETED:
|
||||||
startTimers( context );
|
startTimers( context );
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -246,6 +246,14 @@ public class Utils {
|
||||||
public static void postNotification( Context context, Intent intent,
|
public static void postNotification( Context context, Intent intent,
|
||||||
String title, String body,
|
String title, String body,
|
||||||
int id )
|
int id )
|
||||||
|
{
|
||||||
|
String channelID = Channels.getChannelID( context, Channels.ID.GAME_EVENT );
|
||||||
|
postNotification( context, intent, title, body, id, channelID );
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void postNotification( Context context, Intent intent,
|
||||||
|
String title, String body,
|
||||||
|
int id, String channelID )
|
||||||
{
|
{
|
||||||
/* nextRandomInt: per this link
|
/* nextRandomInt: per this link
|
||||||
http://stackoverflow.com/questions/10561419/scheduling-more-than-one-pendingintent-to-same-activity-using-alarmmanager
|
http://stackoverflow.com/questions/10561419/scheduling-more-than-one-pendingintent-to-same-activity-using-alarmmanager
|
||||||
|
@ -265,7 +273,6 @@ public class Utils {
|
||||||
defaults |= Notification.DEFAULT_VIBRATE;
|
defaults |= Notification.DEFAULT_VIBRATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
String channelID = Channels.getChannelID( context, Channels.ID.GAME_EVENT );
|
|
||||||
Notification notification =
|
Notification notification =
|
||||||
new NotificationCompat.Builder( context, channelID )
|
new NotificationCompat.Builder( context, channelID )
|
||||||
.setContentIntent( pi )
|
.setContentIntent( pi )
|
||||||
|
@ -283,6 +290,31 @@ public class Utils {
|
||||||
nm.notify( id, notification );
|
nm.notify( id, notification );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String KEY_LAST_STALL_NOT = TAG + ".last_stall_note";
|
||||||
|
private static final long MIN_STALL_NOT_INTERVAL_MS = 1000 * 60 * 30;
|
||||||
|
public static void showStallNotification( Context context, long ageMS )
|
||||||
|
{
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long lastStallNotify = DBUtils.getLongFor( context, KEY_LAST_STALL_NOT, 0 );
|
||||||
|
if ( now - lastStallNotify > MIN_STALL_NOT_INTERVAL_MS ) {
|
||||||
|
String title = LocUtils.getString( context, R.string.notify_stall_title );
|
||||||
|
String body = LocUtils.getString( context, R.string.notify_stall_body_fmt,
|
||||||
|
(ageMS + 500) / 1000,
|
||||||
|
MIN_STALL_NOT_INTERVAL_MS / (1000 * 60));
|
||||||
|
String channelID = Channels.getChannelID( context,
|
||||||
|
Channels.ID.SERVICE_STALL );
|
||||||
|
|
||||||
|
Intent intent = GamesListDelegate
|
||||||
|
.makeAlertWithEmailIntent( context, body );
|
||||||
|
postNotification( context, intent, title, body,
|
||||||
|
R.string.notify_stall_title, channelID );
|
||||||
|
DBUtils.setLongFor( context, KEY_LAST_STALL_NOT, now );
|
||||||
|
} else {
|
||||||
|
// Log.d( TAG, "showStallNotification(): not posting for another %d ms",
|
||||||
|
// MIN_STALL_NOT_INTERVAL_MS - (now - lastStallNotify) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void cancelNotification( Context context, int id )
|
public static void cancelNotification( Context context, int id )
|
||||||
{
|
{
|
||||||
NotificationManager nm = (NotificationManager)
|
NotificationManager nm = (NotificationManager)
|
||||||
|
@ -570,39 +602,6 @@ public class Utils {
|
||||||
return Looper.getMainLooper().equals(Looper.myLooper());
|
return Looper.getMainLooper().equals(Looper.myLooper());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String KEY_LAUNCHED_SINCE_INSTALL
|
|
||||||
= TAG + "_LAUNCHED_SINCE_INSTALL";
|
|
||||||
public static void setLaunchedSinceInstall( Context context, boolean val )
|
|
||||||
{
|
|
||||||
DBUtils.setBoolFor( context, KEY_LAUNCHED_SINCE_INSTALL, val );
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean getLaunchedSinceInstall( Context context )
|
|
||||||
{
|
|
||||||
boolean result = DBUtils
|
|
||||||
.getBoolFor( context, KEY_LAUNCHED_SINCE_INSTALL, false );
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int LAUNCH_SINCE_INSTALL_MSG_ID
|
|
||||||
= R.string.bt_need_launch_body; // whatever
|
|
||||||
public static void showLaunchSinceInstall( Context context )
|
|
||||||
{
|
|
||||||
if ( ! getLaunchedSinceInstall( context ) ) {
|
|
||||||
Intent intent = GamesListDelegate
|
|
||||||
.makeGameIDIntent( context, 0 );
|
|
||||||
postNotification( context, intent,
|
|
||||||
R.string.bt_need_launch_title,
|
|
||||||
R.string.bt_need_launch_body,
|
|
||||||
LAUNCH_SINCE_INSTALL_MSG_ID );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void cancelLaunchSinceInstall( Context context )
|
|
||||||
{
|
|
||||||
cancelNotification( context, LAUNCH_SINCE_INSTALL_MSG_ID );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String base64Encode( byte[] in )
|
public static String base64Encode( byte[] in )
|
||||||
{
|
{
|
||||||
return Base64.encodeToString( in, Base64.NO_WRAP );
|
return Base64.encodeToString( in, Base64.NO_WRAP );
|
||||||
|
|
|
@ -150,7 +150,7 @@ public class XWApp extends Application
|
||||||
public void onRegResponse( boolean appReached, boolean needsInitialLaunch )
|
public void onRegResponse( boolean appReached, boolean needsInitialLaunch )
|
||||||
{
|
{
|
||||||
if ( needsInitialLaunch ) {
|
if ( needsInitialLaunch ) {
|
||||||
String channelID = Channels.getChannelID( this, Channels.ID.FOREGROUND );
|
String channelID = Channels.getChannelID( this, Channels.ID.NBSPROXY );
|
||||||
NBSProxy.postLaunchNotification( this, channelID, R.drawable.notify );
|
NBSProxy.postLaunchNotification( this, channelID, R.drawable.notify );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,14 +21,23 @@
|
||||||
package org.eehouse.android.xw4;
|
package org.eehouse.android.xw4;
|
||||||
|
|
||||||
|
|
||||||
import android.support.v4.app.JobIntentService;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.app.JobIntentService;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
abstract class XWJIService extends JobIntentService {
|
abstract class XWJIService extends JobIntentService {
|
||||||
|
private static final String TAG = XWJIService.class.getSimpleName();
|
||||||
|
private static final boolean LOG_COUNTS = false;
|
||||||
|
|
||||||
static final String CMD_KEY = "CMD";
|
static final String CMD_KEY = "CMD";
|
||||||
private static final String TIMESTAMP = "TIMESTAMP";
|
private static final String TIMESTAMP = "TIMESTAMP";
|
||||||
|
|
||||||
|
@ -47,6 +56,8 @@ abstract class XWJIService extends JobIntentService {
|
||||||
@Override
|
@Override
|
||||||
public final void onHandleWork( Intent intent )
|
public final void onHandleWork( Intent intent )
|
||||||
{
|
{
|
||||||
|
forget( getClass(), intent );
|
||||||
|
|
||||||
long timestamp = getTimestamp(intent);
|
long timestamp = getTimestamp(intent);
|
||||||
XWJICmds cmd = cmdFrom( intent );
|
XWJICmds cmd = cmdFrom( intent );
|
||||||
Log.d( getClass().getSimpleName(),
|
Log.d( getClass().getSimpleName(),
|
||||||
|
@ -59,7 +70,9 @@ abstract class XWJIService extends JobIntentService {
|
||||||
|
|
||||||
protected static void enqueueWork( Context context, Class clazz, Intent intent )
|
protected static void enqueueWork( Context context, Class clazz, Intent intent )
|
||||||
{
|
{
|
||||||
|
remember( clazz, intent );
|
||||||
enqueueWork( context, clazz, sJobIDs.get(clazz), intent );
|
enqueueWork( context, clazz, sJobIDs.get(clazz), intent );
|
||||||
|
checkForStall( context );
|
||||||
}
|
}
|
||||||
|
|
||||||
static XWJICmds cmdFrom( Intent intent, XWJICmds[] values )
|
static XWJICmds cmdFrom( Intent intent, XWJICmds[] values )
|
||||||
|
@ -86,4 +99,129 @@ abstract class XWJIService extends JobIntentService {
|
||||||
.putExtra( TIMESTAMP, System.currentTimeMillis() );
|
.putExtra( TIMESTAMP, System.currentTimeMillis() );
|
||||||
return intent;
|
return intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Map<String, List<Intent>> sPendingIntents = new HashMap<>();
|
||||||
|
|
||||||
|
private static void remember( Class clazz, Intent intent )
|
||||||
|
{
|
||||||
|
String name = clazz.getSimpleName();
|
||||||
|
synchronized ( sPendingIntents ) {
|
||||||
|
if ( !sPendingIntents.containsKey( name )) {
|
||||||
|
sPendingIntents.put( name, new ArrayList<Intent>() );
|
||||||
|
}
|
||||||
|
sPendingIntents.get(name).add( intent );
|
||||||
|
if ( LOG_COUNTS ) {
|
||||||
|
Log.d( TAG, "remember(): now have %d intents for class %s",
|
||||||
|
sPendingIntents.get(name).size(), name );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long AGE_THRESHOLD_MS = 1000 * 60; // one minute to start
|
||||||
|
private static void checkForStall( Context context )
|
||||||
|
{
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long maxAge = 0;
|
||||||
|
synchronized ( sPendingIntents ) {
|
||||||
|
for ( String simpleName : sPendingIntents.keySet() ) {
|
||||||
|
List<Intent> intents = sPendingIntents.get( simpleName );
|
||||||
|
if ( 1 <= intents.size() ) {
|
||||||
|
Intent intent = intents.get(0);
|
||||||
|
long timestamp = intent.getLongExtra( TIMESTAMP, -1 );
|
||||||
|
long age = now - timestamp;
|
||||||
|
if ( age > maxAge ) {
|
||||||
|
maxAge = age;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( maxAge > AGE_THRESHOLD_MS ) {
|
||||||
|
Utils.showStallNotification( context, maxAge );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void forget( Class clazz, Intent intent )
|
||||||
|
{
|
||||||
|
String name = clazz.getSimpleName();
|
||||||
|
synchronized ( sPendingIntents ) {
|
||||||
|
String found = null;
|
||||||
|
if ( sPendingIntents.containsKey( name ) ) {
|
||||||
|
List<Intent> intents = sPendingIntents.get( name );
|
||||||
|
for (Iterator<Intent> iter = intents.iterator();
|
||||||
|
iter.hasNext(); ) {
|
||||||
|
Intent candidate = iter.next();
|
||||||
|
if ( areSame( candidate, intent ) ) {
|
||||||
|
found = name;
|
||||||
|
iter.remove();
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
Log.d( TAG, "skipping intent: %s",
|
||||||
|
DbgUtils.extrasToString( candidate ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( found != null ) {
|
||||||
|
if ( LOG_COUNTS ) {
|
||||||
|
Log.d( TAG, "forget(): now have %d intents for class %s",
|
||||||
|
sPendingIntents.get(found).size(), found );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e( TAG, "intent %s not found", intent );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean areSame( Intent intent1, Intent intent2 )
|
||||||
|
{
|
||||||
|
boolean equal = intent1.filterEquals( intent2 );
|
||||||
|
if ( equal ) {
|
||||||
|
Bundle bundle1 = intent1.getExtras();
|
||||||
|
equal = null != bundle1;
|
||||||
|
if ( equal ) {
|
||||||
|
Bundle bundle2 = intent2.getExtras();
|
||||||
|
equal = null != bundle2 && bundle1.size() == bundle2.size();
|
||||||
|
if ( equal ) {
|
||||||
|
for ( final String key : bundle1.keySet()) {
|
||||||
|
if ( ! bundle2.containsKey( key ) ) {
|
||||||
|
equal = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object obj1 = bundle1.get( key );
|
||||||
|
Object obj2 = bundle2.get( key );
|
||||||
|
if ( obj1.getClass() != obj2.getClass() ) {
|
||||||
|
equal = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( obj1 instanceof byte[] ) {
|
||||||
|
equal = Arrays.equals( (byte[])obj1, (byte[])obj2 );
|
||||||
|
} else if ( obj1 instanceof String[] ) {
|
||||||
|
equal = Arrays.equals( (String[])obj1, (String[])obj2 );
|
||||||
|
} else {
|
||||||
|
if ( BuildConfig.DEBUG ) {
|
||||||
|
if ( obj1 instanceof Long
|
||||||
|
|| obj1 instanceof String
|
||||||
|
|| obj1 instanceof Boolean
|
||||||
|
|| obj1 instanceof Integer ) {
|
||||||
|
// expected class; log nothing
|
||||||
|
} else {
|
||||||
|
Log.d( TAG, "areSame: using default for class %s",
|
||||||
|
obj1.getClass().getSimpleName() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
equal = obj1.equals( obj2 );
|
||||||
|
}
|
||||||
|
if ( ! equal ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return equal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2792,9 +2792,11 @@
|
||||||
<string name="toast_no_permission">Permission not granted</string>
|
<string name="toast_no_permission">Permission not granted</string>
|
||||||
|
|
||||||
<!-- Explanation in settings for always-on BT notification -->
|
<!-- Explanation in settings for always-on BT notification -->
|
||||||
<string name="foreground_channel_expl">Accepting Bluetooth in background</string>
|
<string name="nbsproxy_channel_expl">Alerts about NBSProxy</string>
|
||||||
<!-- Explanation in settings for traditional move-arrived notification -->
|
<!-- Explanation in settings for traditional move-arrived notification -->
|
||||||
<string name="gameevent_channel_expl">In-game events</string>
|
<string name="gameevent_channel_expl">In-game events</string>
|
||||||
|
<!-- Notification that the OS isn't scheduling background services -->
|
||||||
|
<string name="servicestall_channel_expl">Stalled messaging alerts</string>
|
||||||
|
|
||||||
<string name="not_again_emptybtscan">If a scan doesn\'t find the device you expect:\n
|
<string name="not_again_emptybtscan">If a scan doesn\'t find the device you expect:\n
|
||||||
• First, just Rescan\n
|
• First, just Rescan\n
|
||||||
|
@ -2803,10 +2805,24 @@
|
||||||
• If all else fails, reboot this device\n
|
• If all else fails, reboot this device\n
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<string name="bt_need_launch_title">Tap to launch CrossWords</string>
|
<string name="notify_stall_title">Message sending is stalled</string>
|
||||||
<string name="bt_need_launch_body">After each upgrade the app needs
|
|
||||||
to be launched once before it can do things like receive Bluetooth
|
<string name="notify_stall_body_fmt">Though it normally takes less
|
||||||
messages in the background.</string>
|
than a second, Android has been unable to process CrossWords\'
|
||||||
|
outbound messages for more than %1$d seconds. This might be because
|
||||||
|
your device is especially busy, or because CrossWords hasn\'t run in
|
||||||
|
the foreground for a while.
|
||||||
|
|
||||||
|
\n\n
|
||||||
|
|
||||||
|
If the problem persists (i.e. you see this frequently), please
|
||||||
|
consider emailing me with information about your device so I can try
|
||||||
|
to reproduce and fix it.
|
||||||
|
|
||||||
|
\n\n
|
||||||
|
|
||||||
|
You will see this message at most once every %2$d minutes.
|
||||||
|
</string>
|
||||||
|
|
||||||
<string name="sms_banned_ok_only">The Google Play Store version of
|
<string name="sms_banned_ok_only">The Google Play Store version of
|
||||||
CrossWords no longer supports inviting or playing by Data
|
CrossWords no longer supports inviting or playing by Data
|
||||||
|
|
Loading…
Reference in a new issue