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,9 +30,11 @@ import android.app.NotificationManager;
|
|||
public class Channels {
|
||||
|
||||
enum ID {
|
||||
FOREGROUND(R.string.foreground_channel_expl,
|
||||
NBSPROXY(R.string.nbsproxy_channel_expl,
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
GAME_EVENT(R.string.gameevent_channel_expl,
|
||||
NotificationManager.IMPORTANCE_LOW),
|
||||
SERVICE_STALL(R.string.servicestall_channel_expl,
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
|
||||
private int mExpl;
|
||||
|
|
|
@ -2449,6 +2449,23 @@ public class DBUtils {
|
|||
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 )
|
||||
{
|
||||
// Log.df( "DBUtils.setBoolFor(key=%s, val=%b)", key, value );
|
||||
|
|
|
@ -56,6 +56,7 @@ public class DlgDelegate {
|
|||
SET_HIDE_NEWGAME_BUTTONS,
|
||||
DWNLD_LOC_DICT,
|
||||
NEW_GAME_DFLT_NAME,
|
||||
SEND_EMAIL,
|
||||
|
||||
// BoardDelegate
|
||||
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 ALERT_MSG = "alert_msg";
|
||||
private static final String WITH_EMAIL = "with_email";
|
||||
|
||||
private static class MySIS implements Serializable {
|
||||
public MySIS(){
|
||||
|
@ -1352,6 +1353,10 @@ public class GamesListDelegate extends ListDelegateBase
|
|||
askDefaultName();
|
||||
break;
|
||||
|
||||
case SEND_EMAIL:
|
||||
Utils.emailAuthor( m_activity );
|
||||
break;
|
||||
|
||||
case ASKED_PHONE_STATE:
|
||||
rematchWithNameAndPerm( true, params );
|
||||
break;
|
||||
|
@ -2297,7 +2302,13 @@ public class GamesListDelegate extends ListDelegateBase
|
|||
{
|
||||
String msg = intent.getStringExtra( ALERT_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;
|
||||
}
|
||||
|
||||
public static Intent makeAlertWithEmailIntent( Context context, String msg )
|
||||
{
|
||||
return makeAlertIntent( context, msg )
|
||||
.putExtra( WITH_EMAIL, true )
|
||||
;
|
||||
}
|
||||
|
||||
public static void sendNFCToSelf( Context context, String data )
|
||||
{
|
||||
Intent intent = makeSelfIntent( context );
|
||||
|
|
|
@ -68,9 +68,6 @@ public class MainActivity extends XWActivity
|
|||
Log.e( TAG, "isTaskRoot() => false!!! What to do?" );
|
||||
}
|
||||
|
||||
Utils.setLaunchedSinceInstall( this, true );
|
||||
Utils.cancelLaunchSinceInstall( this );
|
||||
|
||||
m_dpEnabled = XWPrefs.getIsTablet( this );
|
||||
|
||||
m_dlgt = m_dpEnabled ? new DualpaneDelegate( this, savedInstanceState )
|
||||
|
|
|
@ -35,8 +35,6 @@ public class OnBootReceiver extends BroadcastReceiver {
|
|||
Log.d( TAG, "got %s", action );
|
||||
switch( action ) {
|
||||
case Intent.ACTION_MY_PACKAGE_REPLACED:
|
||||
Utils.setLaunchedSinceInstall( context, false );
|
||||
// FALLTHRU
|
||||
case Intent.ACTION_BOOT_COMPLETED:
|
||||
startTimers( context );
|
||||
break;
|
||||
|
|
|
@ -246,6 +246,14 @@ public class Utils {
|
|||
public static void postNotification( Context context, Intent intent,
|
||||
String title, String body,
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
String channelID = Channels.getChannelID( context, Channels.ID.GAME_EVENT );
|
||||
Notification notification =
|
||||
new NotificationCompat.Builder( context, channelID )
|
||||
.setContentIntent( pi )
|
||||
|
@ -283,6 +290,31 @@ public class Utils {
|
|||
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 )
|
||||
{
|
||||
NotificationManager nm = (NotificationManager)
|
||||
|
@ -570,39 +602,6 @@ public class Utils {
|
|||
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 )
|
||||
{
|
||||
return Base64.encodeToString( in, Base64.NO_WRAP );
|
||||
|
|
|
@ -150,7 +150,7 @@ public class XWApp extends Application
|
|||
public void onRegResponse( boolean appReached, boolean 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 );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,14 +21,23 @@
|
|||
package org.eehouse.android.xw4;
|
||||
|
||||
|
||||
import android.support.v4.app.JobIntentService;
|
||||
import android.content.Context;
|
||||
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.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
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";
|
||||
private static final String TIMESTAMP = "TIMESTAMP";
|
||||
|
||||
|
@ -47,6 +56,8 @@ abstract class XWJIService extends JobIntentService {
|
|||
@Override
|
||||
public final void onHandleWork( Intent intent )
|
||||
{
|
||||
forget( getClass(), intent );
|
||||
|
||||
long timestamp = getTimestamp(intent);
|
||||
XWJICmds cmd = cmdFrom( intent );
|
||||
Log.d( getClass().getSimpleName(),
|
||||
|
@ -59,7 +70,9 @@ abstract class XWJIService extends JobIntentService {
|
|||
|
||||
protected static void enqueueWork( Context context, Class clazz, Intent intent )
|
||||
{
|
||||
remember( clazz, intent );
|
||||
enqueueWork( context, clazz, sJobIDs.get(clazz), intent );
|
||||
checkForStall( context );
|
||||
}
|
||||
|
||||
static XWJICmds cmdFrom( Intent intent, XWJICmds[] values )
|
||||
|
@ -86,4 +99,129 @@ abstract class XWJIService extends JobIntentService {
|
|||
.putExtra( TIMESTAMP, System.currentTimeMillis() );
|
||||
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>
|
||||
|
||||
<!-- 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 -->
|
||||
<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
|
||||
• First, just Rescan\n
|
||||
|
@ -2803,10 +2805,24 @@
|
|||
• If all else fails, reboot this device\n
|
||||
</string>
|
||||
|
||||
<string name="bt_need_launch_title">Tap to launch CrossWords</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
|
||||
messages in the background.</string>
|
||||
<string name="notify_stall_title">Message sending is stalled</string>
|
||||
|
||||
<string name="notify_stall_body_fmt">Though it normally takes less
|
||||
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
|
||||
CrossWords no longer supports inviting or playing by Data
|
||||
|
|
Loading…
Reference in a new issue