mirror of
git://xwords.git.sourceforge.net/gitroot/xwords/xwords
synced 2025-01-09 05:24:44 +01:00
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:
parent
4c9f619a19
commit
ff5d25a53c
5 changed files with 168 additions and 78 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 )
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue