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 int MAX_SEND_FAIL = 3;
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_JSONS = 1; // using jsons instead of lots of fields
@ -159,7 +160,6 @@ public class BTService extends XWJIService {
private BluetoothAdapter m_adapter;
private BTMsgSink m_btMsgSink;
private Notification m_notification; // make once use many
private Handler mHandler;
private BTServiceHelper mHelper;
@ -246,6 +246,7 @@ public class BTService extends XWJIService {
private static void enqueueWork( Context context, Intent intent )
{
if ( BTEnabled() ) {
setCheckTimerOnce( context );
enqueueWork( context, BTService.class, sJobID, intent );
// Log.d( TAG, "enqueueWork(%s)", cmdFrom( intent, BTAction.values() ) );
} 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 )
{
Log.d( TAG, "onACLConnected()" );
@ -341,6 +384,8 @@ public class BTService extends XWJIService {
Log.d( TAG, "%s.onCreate()", this );
super.onCreate();
clearCheckTimer();
mHelper = new BTServiceHelper( this );
m_btMsgSink = new BTMsgSink();
@ -390,10 +435,10 @@ public class BTService extends XWJIService {
startScanThread( timeout );
break;
case INVITE:
String btAddr = intent.getStringExtra( ADDR_KEY );
String jsonData = intent.getStringExtra( GAMEDATA_KEY );
NetLaunchInfo nli = NetLaunchInfo.makeFrom( this, jsonData );
Log.i( TAG, "handleCommand: nli: %s", nli );
String btAddr = intent.getStringExtra( ADDR_KEY );
// Log.i( TAG, "onHandleWorkImpl(): nli: %s", nli );
getSenderFor( btAddr ).addInvite( nli );
break;
case PINGHOST:
@ -918,7 +963,7 @@ public class BTService extends XWJIService {
dos.writeByte( BT_PROTO );
break; // success!!!
} catch (IOException ioe) {
Log.d( TAG, "connect(): %s", ioe.getMessage() );
// Log.d( TAG, "connect(): %s", ioe.getMessage() );
long msLeft = end - System.currentTimeMillis();
if ( msLeft <= 0 ) {
break;
@ -1107,26 +1152,23 @@ public class BTService extends XWJIService {
waitFromNow = 0;
} else {
// 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);
}
Log.d( TAG, "%s.getNextReadyMS() => %dms", this, waitFromNow );
}
}
Log.d( TAG, "%s.getNextReadyMS() => %dms", this, waitFromNow );
return waitFromNow;
}
void setNoHost()
void setNoHost()
{
// Log.d( TAG, "setNoHost() IN" );
try ( DbgUtils.DeadlockWatch dw = new DbgUtils.DeadlockWatch( this ) ) {
synchronized ( this ) {
mLastFailTime = System.currentTimeMillis();
++mFailCount;
}
}
// Log.d( TAG, "setNoHost() OUT" );
}
@Override
@ -1340,27 +1382,32 @@ public class BTService extends XWJIService {
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" );
try ( DbgUtils.DeadlockWatch dw = new DbgUtils.DeadlockWatch( this ) ) {
synchronized ( this ) {
if ( 0 == mCmds.size() ) {
mStamp = System.currentTimeMillis();
haveSpace = mOP.length() + op.length() + 3 < MAX_PACKET_LEN;
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 );
return haveSpace;
}
void resetBackoff()
@ -1424,54 +1471,59 @@ public class BTService extends XWJIService {
{
try {
short isLen = inStream.readShort();
byte[] data = new byte[isLen];
inStream.readFully( data );
if ( isLen >= MAX_PACKET_LEN ) {
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 );
DataInputStream dis = new DataInputStream( bis );
int nMessages = dis.readByte();
ByteArrayInputStream bis = new ByteArrayInputStream( data );
DataInputStream dis = new DataInputStream( bis );
int nMessages = dis.readByte();
Log.d( TAG, "dispatchAll(): read %d-byte payload with sum %s containing %d messages",
data.length, Utils.getMD5SumFor( data ), nMessages );
Log.d( TAG, "dispatchAll(): read %d-byte payload with sum %s containing %d messages",
data.length, Utils.getMD5SumFor( data ), nMessages );
for ( int ii = 0; ii < nMessages; ++ii ) {
BTCmd cmd = BTCmd.values()[dis.readByte()];
final short oneLen = dis.readShort(); // used only to skip
int availableBefore = dis.available();
switch ( cmd ) {
case PING:
int gameID = dis.readInt();
processor.receivePing( gameID, socket );
break;
case INVITE:
data = new byte[dis.readShort()];
dis.readFully( data );
NetLaunchInfo nli = XwJNI.nliFromStream( data );
processor.receiveInvitation( nli, socket );
break;
case MESG_SEND:
gameID = dis.readInt();
data = new byte[dis.readShort()];
dis.readFully( data );
processor.receiveMessage( gameID, data, socket );
break;
case MESG_GAMEGONE:
gameID = dis.readInt();
processor.receiveGameGone( gameID, socket );
break;
default:
Log.e( TAG, "unexpected command %s; skipping %d bytes", cmd, oneLen );
if ( oneLen <= dis.available() ) {
dis.readFully( new byte[oneLen] );
Assert.assertFalse( BuildConfig.DEBUG );
for ( int ii = 0; ii < nMessages; ++ii ) {
BTCmd cmd = BTCmd.values()[dis.readByte()];
final short oneLen = dis.readShort(); // used only to skip
int availableBefore = dis.available();
switch ( cmd ) {
case PING:
int gameID = dis.readInt();
processor.receivePing( gameID, socket );
break;
case INVITE:
data = new byte[dis.readShort()];
dis.readFully( data );
NetLaunchInfo nli = XwJNI.nliFromStream( data );
processor.receiveInvitation( nli, socket );
break;
case MESG_SEND:
gameID = dis.readInt();
data = new byte[dis.readShort()];
dis.readFully( data );
processor.receiveMessage( gameID, data, socket );
break;
case MESG_GAMEGONE:
gameID = dis.readInt();
processor.receiveGameGone( gameID, socket );
break;
default:
Log.e( TAG, "unexpected command %s; skipping %d bytes", cmd, oneLen );
if ( oneLen <= dis.available() ) {
dis.readFully( new byte[oneLen] );
Assert.assertFalse( BuildConfig.DEBUG );
}
break;
}
break;
}
// sanity-check based on packet length
int availableAfter = dis.available();
Assert.assertTrue( oneLen == availableBefore - availableAfter
|| !BuildConfig.DEBUG );
// sanity-check based on packet length
int availableAfter = dis.available();
Assert.assertTrue( oneLen == availableBefore - availableAfter
|| !BuildConfig.DEBUG );
}
}
} catch ( IOException ioe ) {
Log.e( TAG, "dispatchAll() got ioe: %s", ioe );
@ -1484,7 +1536,7 @@ public class BTService extends XWJIService {
}
Log.d( TAG, "dispatchAll() done" );
}
}
} // class PacketParser
// Blocks until can return an Accumulator with data
private static Object sBlocker = new Object();
@ -1493,7 +1545,7 @@ public class BTService extends XWJIService {
// Log.d( TAG, "getHasData() IN" );
List<PacketAccumulator> result = new ArrayList<>();
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 ) ) {
synchronized ( sSenders ) {
for ( String addr : sSenders.keySet() ) {
@ -1508,17 +1560,15 @@ public class BTService extends XWJIService {
}
}
if ( result.size() == 0 ) {
if ( 0 == result.size() ) {
synchronized ( sBlocker ) {
long whileDebugging = Math.min( newMin, 10 * 1000 );
Log.d( TAG, "getHasData(): waiting %dms (should be %dms)",
whileDebugging, newMin );
sBlocker.wait( 1 + whileDebugging ); // 0 might mean forever
Log.d( TAG, "getHasData(): waiting %dms", newMin );
sBlocker.wait( 1 + newMin ); // 0 might mean forever
Log.d( TAG, "getHasData(): DONE waiting" );
}
}
}
Log.d( TAG, "getHasData() => %s", result );
// Log.d( TAG, "getHasData() => %s", result );
return result;
}

View file

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

View file

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

View file

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

View file

@ -2760,6 +2760,11 @@
• 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>
<!-- 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
SMS_RECEIVE permission. Message displayed with the "do not show