add alert to launch app when OS not scheduling service

Set a boolean every time app's upgraded, and clear it on first launch of
MainActivity. In between, if we try to launch the BTService but the OS
doesn't schedule it, post a notification asking user to launch the app.
This commit is contained in:
Eric House 2019-02-18 22:12:43 -08:00
parent 4c9f619a19
commit ff5d25a53c
5 changed files with 168 additions and 78 deletions

View file

@ -95,6 +95,7 @@ public class BTService extends XWJIService {
private static final long RESEND_TIMEOUT = 5; // seconds private static final long RESEND_TIMEOUT = 5; // seconds
private static final int MAX_SEND_FAIL = 3; private static final int MAX_SEND_FAIL = 3;
private static final int CONNECT_TIMEOUT_MS = 10000; private static final int CONNECT_TIMEOUT_MS = 10000;
private static final int MAX_PACKET_LEN = 4 * 1024;
private static final int BT_PROTO_ORIG = 0; private static final int BT_PROTO_ORIG = 0;
// private static final int BT_PROTO_JSONS = 1; // using jsons instead of lots of fields // private static final int BT_PROTO_JSONS = 1; // using jsons instead of lots of fields
@ -159,7 +160,6 @@ public class BTService extends XWJIService {
private BluetoothAdapter m_adapter; private BluetoothAdapter m_adapter;
private BTMsgSink m_btMsgSink; private BTMsgSink m_btMsgSink;
private Notification m_notification; // make once use many
private Handler mHandler; private Handler mHandler;
private BTServiceHelper mHelper; private BTServiceHelper mHelper;
@ -246,6 +246,7 @@ public class BTService extends XWJIService {
private static void enqueueWork( Context context, Intent intent ) private static void enqueueWork( Context context, Intent intent )
{ {
if ( BTEnabled() ) { if ( BTEnabled() ) {
setCheckTimerOnce( context );
enqueueWork( context, BTService.class, sJobID, intent ); enqueueWork( context, BTService.class, sJobID, intent );
// Log.d( TAG, "enqueueWork(%s)", cmdFrom( intent, BTAction.values() ) ); // Log.d( TAG, "enqueueWork(%s)", cmdFrom( intent, BTAction.values() ) );
} else { } else {
@ -253,6 +254,48 @@ public class BTService extends XWJIService {
} }
} }
// It appears that the OS won't launch my service (calling onCreate() and
// onHandleWork()) after an install, including via adb, until the app's
// been launched manually. So we'll check for that case and post a
// notification if it appears to be a problem.
private static Thread[] sLaunchChecker = {null};
private static void clearCheckTimer()
{
synchronized ( sLaunchChecker ) {
if ( null != sLaunchChecker[0] ) {
sLaunchChecker[0].interrupt();
}
}
}
private static void setCheckTimerOnce( final Context context )
{
synchronized ( sLaunchChecker ) {
if ( null == sLaunchChecker[0] ) {
sLaunchChecker[0] = new Thread( new Runnable() {
@Override
public void run() {
try {
synchronized ( sLaunchChecker ) {
sLaunchChecker.notify();
}
Thread.sleep( 10 * 1000 );
Utils.showLaunchSinceInstall( context );
} catch ( InterruptedException ie ) {
}
}
} );
sLaunchChecker[0].start();
// Don't return until the thread's interruptable
try { sLaunchChecker.wait(); }
catch ( InterruptedException ex ) {}
}
}
}
public static void onACLConnected( Context context ) public static void onACLConnected( Context context )
{ {
Log.d( TAG, "onACLConnected()" ); Log.d( TAG, "onACLConnected()" );
@ -341,6 +384,8 @@ public class BTService extends XWJIService {
Log.d( TAG, "%s.onCreate()", this ); Log.d( TAG, "%s.onCreate()", this );
super.onCreate(); super.onCreate();
clearCheckTimer();
mHelper = new BTServiceHelper( this ); mHelper = new BTServiceHelper( this );
m_btMsgSink = new BTMsgSink(); m_btMsgSink = new BTMsgSink();
@ -390,10 +435,10 @@ public class BTService extends XWJIService {
startScanThread( timeout ); startScanThread( timeout );
break; break;
case INVITE: case INVITE:
String btAddr = intent.getStringExtra( ADDR_KEY );
String jsonData = intent.getStringExtra( GAMEDATA_KEY ); String jsonData = intent.getStringExtra( GAMEDATA_KEY );
NetLaunchInfo nli = NetLaunchInfo.makeFrom( this, jsonData ); NetLaunchInfo nli = NetLaunchInfo.makeFrom( this, jsonData );
Log.i( TAG, "handleCommand: nli: %s", nli ); // Log.i( TAG, "onHandleWorkImpl(): nli: %s", nli );
String btAddr = intent.getStringExtra( ADDR_KEY );
getSenderFor( btAddr ).addInvite( nli ); getSenderFor( btAddr ).addInvite( nli );
break; break;
case PINGHOST: case PINGHOST:
@ -918,7 +963,7 @@ public class BTService extends XWJIService {
dos.writeByte( BT_PROTO ); dos.writeByte( BT_PROTO );
break; // success!!! break; // success!!!
} catch (IOException ioe) { } catch (IOException ioe) {
Log.d( TAG, "connect(): %s", ioe.getMessage() ); // Log.d( TAG, "connect(): %s", ioe.getMessage() );
long msLeft = end - System.currentTimeMillis(); long msLeft = end - System.currentTimeMillis();
if ( msLeft <= 0 ) { if ( msLeft <= 0 ) {
break; break;
@ -1107,26 +1152,23 @@ public class BTService extends XWJIService {
waitFromNow = 0; waitFromNow = 0;
} else { } else {
// If we're failing, use a backoff. // If we're failing, use a backoff.
long wait = 10 * 1000 * 2 * (1 + mFailCount); long wait = 1000 * (long)Math.pow( mFailCount, 2 );
waitFromNow = wait - (System.currentTimeMillis() - mLastFailTime); waitFromNow = wait - (System.currentTimeMillis() - mLastFailTime);
} }
Log.d( TAG, "%s.getNextReadyMS() => %dms", this, waitFromNow );
} }
} }
Log.d( TAG, "%s.getNextReadyMS() => %dms", this, waitFromNow );
return waitFromNow; return waitFromNow;
} }
void setNoHost() void setNoHost()
{ {
// Log.d( TAG, "setNoHost() IN" );
try ( DbgUtils.DeadlockWatch dw = new DbgUtils.DeadlockWatch( this ) ) { try ( DbgUtils.DeadlockWatch dw = new DbgUtils.DeadlockWatch( this ) ) {
synchronized ( this ) { synchronized ( this ) {
mLastFailTime = System.currentTimeMillis(); mLastFailTime = System.currentTimeMillis();
++mFailCount; ++mFailCount;
} }
} }
// Log.d( TAG, "setNoHost() OUT" );
} }
@Override @Override
@ -1340,27 +1382,32 @@ public class BTService extends XWJIService {
append( cmd, 0, op ); append( cmd, 0, op );
} }
private void append( BTCmd cmd, int gameID, OutputPair op ) throws IOException private boolean append( BTCmd cmd, int gameID, OutputPair op ) throws IOException
{ {
boolean haveSpace;
// Log.d( TAG, "append() IN" ); // Log.d( TAG, "append() IN" );
try ( DbgUtils.DeadlockWatch dw = new DbgUtils.DeadlockWatch( this ) ) { try ( DbgUtils.DeadlockWatch dw = new DbgUtils.DeadlockWatch( this ) ) {
synchronized ( this ) { synchronized ( this ) {
if ( 0 == mCmds.size() ) { haveSpace = mOP.length() + op.length() + 3 < MAX_PACKET_LEN;
mStamp = System.currentTimeMillis(); if ( haveSpace ) {
if ( 0 == mCmds.size() ) {
mStamp = System.currentTimeMillis();
}
mCmds.add( cmd );
mGameIDs.add( gameID );
mOP.dos.writeByte( cmd.ordinal() );
byte[] data = op.bos.toByteArray();
mOP.dos.writeShort( data.length );
mOP.dos.write( data, 0, data.length );
mFailCount = 0; // for now, we restart timer on new data
tellSomebody();
} }
mCmds.add( cmd );
mGameIDs.add( gameID );
mOP.dos.writeByte( cmd.ordinal() );
byte[] data = op.bos.toByteArray();
mOP.dos.writeShort( data.length );
mOP.dos.write( data, 0, data.length );
mFailCount = 0; // for now, we restart timer on new data
tellSomebody();
} }
} }
// Log.d( TAG, "append(%s): now %s", cmd, this ); // Log.d( TAG, "append(%s): now %s", cmd, this );
return haveSpace;
} }
void resetBackoff() void resetBackoff()
@ -1424,54 +1471,59 @@ public class BTService extends XWJIService {
{ {
try { try {
short isLen = inStream.readShort(); short isLen = inStream.readShort();
byte[] data = new byte[isLen]; if ( isLen >= MAX_PACKET_LEN ) {
inStream.readFully( data ); Log.e( TAG, "packet too big; dropping!!!" );
Assert.assertFalse( BuildConfig.DEBUG );
} else {
byte[] data = new byte[isLen];
inStream.readFully( data );
ByteArrayInputStream bis = new ByteArrayInputStream( data ); ByteArrayInputStream bis = new ByteArrayInputStream( data );
DataInputStream dis = new DataInputStream( bis ); DataInputStream dis = new DataInputStream( bis );
int nMessages = dis.readByte(); int nMessages = dis.readByte();
Log.d( TAG, "dispatchAll(): read %d-byte payload with sum %s containing %d messages", Log.d( TAG, "dispatchAll(): read %d-byte payload with sum %s containing %d messages",
data.length, Utils.getMD5SumFor( data ), nMessages ); data.length, Utils.getMD5SumFor( data ), nMessages );
for ( int ii = 0; ii < nMessages; ++ii ) { for ( int ii = 0; ii < nMessages; ++ii ) {
BTCmd cmd = BTCmd.values()[dis.readByte()]; BTCmd cmd = BTCmd.values()[dis.readByte()];
final short oneLen = dis.readShort(); // used only to skip final short oneLen = dis.readShort(); // used only to skip
int availableBefore = dis.available(); int availableBefore = dis.available();
switch ( cmd ) { switch ( cmd ) {
case PING: case PING:
int gameID = dis.readInt(); int gameID = dis.readInt();
processor.receivePing( gameID, socket ); processor.receivePing( gameID, socket );
break; break;
case INVITE: case INVITE:
data = new byte[dis.readShort()]; data = new byte[dis.readShort()];
dis.readFully( data ); dis.readFully( data );
NetLaunchInfo nli = XwJNI.nliFromStream( data ); NetLaunchInfo nli = XwJNI.nliFromStream( data );
processor.receiveInvitation( nli, socket ); processor.receiveInvitation( nli, socket );
break; break;
case MESG_SEND: case MESG_SEND:
gameID = dis.readInt(); gameID = dis.readInt();
data = new byte[dis.readShort()]; data = new byte[dis.readShort()];
dis.readFully( data ); dis.readFully( data );
processor.receiveMessage( gameID, data, socket ); processor.receiveMessage( gameID, data, socket );
break; break;
case MESG_GAMEGONE: case MESG_GAMEGONE:
gameID = dis.readInt(); gameID = dis.readInt();
processor.receiveGameGone( gameID, socket ); processor.receiveGameGone( gameID, socket );
break; break;
default: default:
Log.e( TAG, "unexpected command %s; skipping %d bytes", cmd, oneLen ); Log.e( TAG, "unexpected command %s; skipping %d bytes", cmd, oneLen );
if ( oneLen <= dis.available() ) { if ( oneLen <= dis.available() ) {
dis.readFully( new byte[oneLen] ); dis.readFully( new byte[oneLen] );
Assert.assertFalse( BuildConfig.DEBUG ); Assert.assertFalse( BuildConfig.DEBUG );
}
break;
} }
break;
}
// sanity-check based on packet length // sanity-check based on packet length
int availableAfter = dis.available(); int availableAfter = dis.available();
Assert.assertTrue( oneLen == availableBefore - availableAfter Assert.assertTrue( oneLen == availableBefore - availableAfter
|| !BuildConfig.DEBUG ); || !BuildConfig.DEBUG );
}
} }
} catch ( IOException ioe ) { } catch ( IOException ioe ) {
Log.e( TAG, "dispatchAll() got ioe: %s", ioe ); Log.e( TAG, "dispatchAll() got ioe: %s", ioe );
@ -1484,7 +1536,7 @@ public class BTService extends XWJIService {
} }
Log.d( TAG, "dispatchAll() done" ); Log.d( TAG, "dispatchAll() done" );
} }
} } // class PacketParser
// Blocks until can return an Accumulator with data // Blocks until can return an Accumulator with data
private static Object sBlocker = new Object(); private static Object sBlocker = new Object();
@ -1493,7 +1545,7 @@ public class BTService extends XWJIService {
// Log.d( TAG, "getHasData() IN" ); // Log.d( TAG, "getHasData() IN" );
List<PacketAccumulator> result = new ArrayList<>(); List<PacketAccumulator> result = new ArrayList<>();
while ( 0 == result.size() ) { while ( 0 == result.size() ) {
long newMin = 5 * 60 * 1000; long newMin = 60 * 60 * 1000; // longest wait: 1 hour
try ( DbgUtils.DeadlockWatch dw = new DbgUtils.DeadlockWatch( sSenders ) ) { try ( DbgUtils.DeadlockWatch dw = new DbgUtils.DeadlockWatch( sSenders ) ) {
synchronized ( sSenders ) { synchronized ( sSenders ) {
for ( String addr : sSenders.keySet() ) { for ( String addr : sSenders.keySet() ) {
@ -1508,17 +1560,15 @@ public class BTService extends XWJIService {
} }
} }
if ( result.size() == 0 ) { if ( 0 == result.size() ) {
synchronized ( sBlocker ) { synchronized ( sBlocker ) {
long whileDebugging = Math.min( newMin, 10 * 1000 ); Log.d( TAG, "getHasData(): waiting %dms", newMin );
Log.d( TAG, "getHasData(): waiting %dms (should be %dms)", sBlocker.wait( 1 + newMin ); // 0 might mean forever
whileDebugging, newMin );
sBlocker.wait( 1 + whileDebugging ); // 0 might mean forever
Log.d( TAG, "getHasData(): DONE waiting" ); Log.d( TAG, "getHasData(): DONE waiting" );
} }
} }
} }
Log.d( TAG, "getHasData() => %s", result ); // Log.d( TAG, "getHasData() => %s", result );
return result; return result;
} }

View file

@ -68,6 +68,9 @@ 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 )

View file

@ -34,12 +34,11 @@ public class OnBootReceiver extends BroadcastReceiver {
String action = intent.getAction(); String action = intent.getAction();
Log.d( TAG, "got %s", action ); Log.d( TAG, "got %s", action );
switch( action ) { switch( action ) {
case Intent.ACTION_BOOT_COMPLETED:
case Intent.ACTION_MY_PACKAGE_REPLACED: case Intent.ACTION_MY_PACKAGE_REPLACED:
Utils.setLaunchedSinceInstall( context, false );
// FALLTHRU
case Intent.ACTION_BOOT_COMPLETED:
startTimers( context ); startTimers( context );
// Let's not put up the foreground service notification on
// boot. Too likely to annoy.
// BTService.onAppToBackground( context );
break; break;
} }
} }

View file

@ -570,6 +570,39 @@ 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 );

View file

@ -2760,6 +2760,11 @@
• 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="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>
<!-- Message to be shown each time user of Google Play version (post <!-- Message to be shown each time user of Google Play version (post
March 9, 2019) tries to use a feature that requires SMS_SEND or March 9, 2019) tries to use a feature that requires SMS_SEND or
SMS_RECEIVE permission. Message displayed with the "do not show SMS_RECEIVE permission. Message displayed with the "do not show