add pref to disable use of bluetooth

It's buggy enough on some devices that a user might need to disable it.
This commit is contained in:
Eric House 2020-11-01 19:19:30 -08:00
parent 04000ddf7e
commit 157332d2cc
11 changed files with 223 additions and 45 deletions

View file

@ -0,0 +1,69 @@
/* -*- compile-command: "find-and-gradle.sh inXw4dDeb"; -*- */
/*
* Copyright 2009 - 2012 by Eric House (xwords@eehouse.org). All
* rights reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.eehouse.android.xw4;
import android.content.Context;
import android.util.AttributeSet;
import java.lang.ref.WeakReference;
import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.loc.LocUtils;
public class BTCheckBoxPreference extends ConfirmingCheckBoxPreference {
private static final String TAG = BTCheckBoxPreference.class.getSimpleName();
private static WeakReference<BTCheckBoxPreference> s_this = null;
public BTCheckBoxPreference( Context context, AttributeSet attrs )
{
super( context, attrs );
s_this = new WeakReference<>( this );
}
@Override
protected void checkIfConfirmed()
{
PrefsActivity activity = (PrefsActivity)getContext();
String msg = LocUtils.getString( activity,
R.string.warn_bt_havegames );
int count = DBUtils
.getGameCountUsing( activity, CommsConnType.COMMS_CONN_BT );
if ( 0 < count ) {
msg += LocUtils.getQuantityString( activity, R.plurals.warn_bt_games_fmt,
count, count );
}
activity.makeConfirmThenBuilder( msg, Action.DISABLE_BT_DO )
.setPosButton( R.string.button_disable_bt )
.show();
}
protected static void setChecked()
{
if ( null != s_this ) {
BTCheckBoxPreference self = s_this.get();
if ( null != self ) {
self.super_setChecked( true );
}
}
}
}

View file

@ -47,6 +47,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.eehouse.android.xw4.DbgUtils.DeadlockWatch; import org.eehouse.android.xw4.DbgUtils.DeadlockWatch;
import org.eehouse.android.xw4.MultiService.DictFetchOwner; import org.eehouse.android.xw4.MultiService.DictFetchOwner;
@ -99,7 +100,7 @@ public class BTUtils {
public static boolean BTAvailable() public static boolean BTAvailable()
{ {
BluetoothAdapter adapter = getAdapterIf(); BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
return null != adapter; return null != adapter;
} }
@ -109,22 +110,38 @@ public class BTUtils {
return null != adapter && adapter.isEnabled(); return null != adapter && adapter.isEnabled();
} }
public static void enable() public static void enable( Context context )
{ {
BluetoothAdapter adapter = getAdapterIf(); BluetoothAdapter adapter = getAdapterIf();
if ( null != adapter ) { if ( null != adapter ) {
// Only do this after explicit action from user -- Android guidelines // Only do this after explicit action from user -- Android guidelines
adapter.enable(); adapter.enable();
} }
XWPrefs.setBTDisabled( context, false );
}
public static void setEnabled( Context context, boolean enabled )
{
if ( enabled ) {
onResume( context );
} else {
stopThreads();
}
}
public static void disabledChanged( Context context )
{
boolean disabled = XWPrefs.getBTDisabled( context );
setEnabled( context, !disabled );
} }
static BluetoothAdapter getAdapterIf() static BluetoothAdapter getAdapterIf()
{ {
BluetoothAdapter result = null; BluetoothAdapter result = null;
// Later this will change to include at least a test whether we're // BT crashes a lot inside the OS when running on behalf of a
// running as background user account, a situation in which BT crashes // background user account. We catch exceptions that indicate that's
// a lot inside the OS. // going on and set this flag.
if ( !sBackUser.get() ) { if ( ! XWPrefs.getBTDisabled( getContext() ) && !sBackUser.get() ) {
result = BluetoothAdapter.getDefaultAdapter(); result = BluetoothAdapter.getDefaultAdapter();
} }
return result; return result;
@ -195,6 +212,12 @@ public class BTUtils {
sBackUser.set( false ); sBackUser.set( false );
} }
private static void stopThreads()
{
ListenThread.stopSelf();
ReadThread.stopSelf();
}
private static String nameForAddr( BluetoothAdapter adapter, String btAddr ) private static String nameForAddr( BluetoothAdapter adapter, String btAddr )
{ {
String result = null; String result = null;
@ -316,14 +339,14 @@ public class BTUtils {
private static void updateStatusIn( boolean success ) private static void updateStatusIn( boolean success )
{ {
Context context = XWApp.getContext(); Context context = getContext();
ConnStatusHandler ConnStatusHandler
.updateStatusIn( context, CommsConnType.COMMS_CONN_BT, success ); .updateStatusIn( context, CommsConnType.COMMS_CONN_BT, success );
} }
private static void updateStatusOut( boolean success ) private static void updateStatusOut( boolean success )
{ {
Context context = XWApp.getContext(); Context context = getContext();
ConnStatusHandler ConnStatusHandler
.updateStatusOut( context, CommsConnType.COMMS_CONN_BT, success ); .updateStatusOut( context, CommsConnType.COMMS_CONN_BT, success );
} }
@ -398,32 +421,36 @@ public class BTUtils {
return btAddr; return btAddr;
} }
private static void clearInstance( Thread[] holder, Thread instance ) private static void clearInstance( AtomicReference<Thread> holder,
Thread instance )
{ {
synchronized ( holder ) { synchronized ( holder ) {
if ( holder[0] == null ) { Thread curThread = holder.get();
if ( null == curThread ) {
// nothing to do // nothing to do
} else if ( holder[0] == instance ) { } else if ( instance == curThread ) {
holder[0] = null; holder.set( null );
} else { } else {
Log.e( TAG, "clearInstance(): cur instance %s not == %s", Log.e( TAG, "clearInstance(): cur instance %s not == %s",
holder[0], instance ); curThread, instance );
} }
} }
} }
// Save a few keystrokes...
private static Context getContext() { return XWApp.getContext(); }
private static class ScanThread extends Thread { private static class ScanThread extends Thread {
private static Thread[] sInstance = {null}; private static AtomicReference<Thread> sInstance = new AtomicReference<>();
private int mTimeoutMS; private int mTimeoutMS;
private Set<BluetoothDevice> mDevs; private Set<BluetoothDevice> mDevs;
static void startOnce( int timeoutMS, Set<BluetoothDevice> devs ) static void startOnce( int timeoutMS, Set<BluetoothDevice> devs )
{ {
synchronized ( sInstance ) { synchronized ( sInstance ) {
if ( sInstance[0] == null ) { if ( null == sInstance.get() ) {
ScanThread thread = new ScanThread( timeoutMS, devs ); ScanThread thread = new ScanThread( timeoutMS, devs );
sInstance[0] = thread; Assert.assertTrueNR( thread == sInstance.get() );
thread.start(); thread.start();
} }
} }
@ -433,12 +460,13 @@ public class BTUtils {
{ {
mTimeoutMS = timeoutMS; mTimeoutMS = timeoutMS;
mDevs = devs; mDevs = devs;
sInstance.set( this );
} }
@Override @Override
public void run() public void run()
{ {
Assert.assertTrueNR( this == sInstance[0] ); Assert.assertTrueNR( this == sInstance.get() );
Map<BluetoothDevice, PacketAccumulator> pas = new HashMap<>(); Map<BluetoothDevice, PacketAccumulator> pas = new HashMap<>();
for ( BluetoothDevice dev : mDevs ) { for ( BluetoothDevice dev : mDevs ) {
@ -1049,12 +1077,14 @@ public class BTUtils {
} // class PacketAccumulator } // class PacketAccumulator
private static class ListenThread extends Thread { private static class ListenThread extends Thread {
private static Thread[] sInstance = {null}; private static AtomicReference<Thread> sInstance = new AtomicReference<>();
private BluetoothAdapter mAdapter; private BluetoothAdapter mAdapter;
private BluetoothServerSocket mServerSocket;
private ListenThread( BluetoothAdapter adapter ) private ListenThread( BluetoothAdapter adapter )
{ {
mAdapter = adapter; mAdapter = adapter;
sInstance.set( this );
} }
@Override @Override
@ -1062,32 +1092,31 @@ public class BTUtils {
{ {
Log.d( TAG, "ListenThread: %s.run() starting", this ); Log.d( TAG, "ListenThread: %s.run() starting", this );
BluetoothServerSocket serverSocket;
try { try {
Assert.assertTrueNR( null != sAppName && null != sUUID ); Assert.assertTrueNR( null != sAppName && null != sUUID );
serverSocket = mAdapter mServerSocket = mAdapter
.listenUsingRfcommWithServiceRecord( sAppName, sUUID ); .listenUsingRfcommWithServiceRecord( sAppName, sUUID );
} catch ( IOException ioe ) { } catch ( IOException ioe ) {
Log.ex( TAG, ioe ); Log.ex( TAG, ioe );
serverSocket = null; mServerSocket = null;
} catch ( SecurityException ex ) { } catch ( SecurityException ex ) {
// Got this with a message saying not allowed to call // Got this with a message saying not allowed to call
// listenUsingRfcommWithServiceRecord() in background (on // listenUsingRfcommWithServiceRecord() in background (on
// Android 9) // Android 9)
sBackUser.set( true ); // two-user system: disable BT sBackUser.set( true ); // two-user system: disable BT
Log.d( TAG, "set sBackUser; outta here (first case)" ); Log.d( TAG, "set sBackUser; outta here (first case)" );
serverSocket = null; mServerSocket = null;
} }
while ( null != serverSocket ) { while ( null != mServerSocket && this == sInstance.get() ) {
Log.d( TAG, "%s.run(): calling accept()", this ); Log.d( TAG, "%s.run(): calling accept()", this );
try { try {
BluetoothSocket socket = serverSocket.accept(); // blocks BluetoothSocket socket = mServerSocket.accept(); // blocks
Log.d( TAG, "%s.run(): accept() returned", this ); Log.d( TAG, "%s.run(): accept() returned", this );
ReadThread.handle( socket ); ReadThread.handle( socket );
} catch ( IOException ioe ) { } catch ( IOException ioe ) {
Log.ex( TAG, ioe ); Log.ex( TAG, ioe );
serverSocket = null; mServerSocket = null;
} }
} }
@ -1100,20 +1129,41 @@ public class BTUtils {
ListenThread result = null; ListenThread result = null;
BluetoothAdapter adapter = getAdapterIf(); BluetoothAdapter adapter = getAdapterIf();
if ( null != adapter ) { if ( null != adapter ) {
synchronized (sInstance) { synchronized ( sInstance ) {
result = (ListenThread)sInstance[0]; result = (ListenThread)sInstance.get();
if ( null == result ) { if ( null == result ) {
sInstance[0] = result = new ListenThread( adapter ); result = new ListenThread( adapter );
Assert.assertTrueNR( result == sInstance.get() );
result.start(); result.start();
} }
} }
} }
return result; return result;
} }
private static void stopSelf()
{
synchronized ( sInstance ) {
ListenThread self = (ListenThread)sInstance.get();
Log.d( TAG, "ListenThread.stopSelf(): self: %s", self );
if ( null != self ) {
sInstance.set( null );
BluetoothServerSocket serverSocket = self.mServerSocket;
if ( null != serverSocket ) {
try {
serverSocket.close();
} catch ( IOException ioe ) {
Log.ex( TAG, ioe );
}
}
}
}
}
} }
private static class ReadThread extends Thread { private static class ReadThread extends Thread {
private static Thread[] sInstance = {null}; private static AtomicReference<Thread> sInstance = new AtomicReference<>();
private LinkedBlockingQueue<BluetoothSocket> mQueue; private LinkedBlockingQueue<BluetoothSocket> mQueue;
private BTMsgSink mBTMsgSink; private BTMsgSink mBTMsgSink;
@ -1128,13 +1178,14 @@ public class BTUtils {
{ {
mQueue = new LinkedBlockingQueue<>(); mQueue = new LinkedBlockingQueue<>();
mBTMsgSink = new BTMsgSink(); mBTMsgSink = new BTMsgSink();
sInstance.set( this );
} }
@Override @Override
public void run() public void run()
{ {
Log.d( TAG, "ReadThread: %s.run() starting", this ); Log.d( TAG, "ReadThread: %s.run() starting", this );
for ( ; ; ) { while ( this == sInstance.get() ) {
try { try {
BluetoothSocket socket = mQueue.take(); BluetoothSocket socket = mQueue.take();
DataInputStream inStream = DataInputStream inStream =
@ -1144,7 +1195,7 @@ public class BTUtils {
BTInviteDelegate.onHeardFromDev( socket.getRemoteDevice() ); BTInviteDelegate.onHeardFromDev( socket.getRemoteDevice() );
parsePacket( proto, inStream, socket ); parsePacket( proto, inStream, socket );
updateStatusIn( true ); updateStatusIn( true );
TimerReceiver.restartBackoff( XWApp.getContext() ); TimerReceiver.restartBackoff( getContext() );
// nBadCount = 0; // nBadCount = 0;
} else { } else {
writeBack( socket, BTCmd.BAD_PROTO ); writeBack( socket, BTCmd.BAD_PROTO );
@ -1210,7 +1261,7 @@ public class BTUtils {
case INVITE: case INVITE:
NetLaunchInfo nli; NetLaunchInfo nli;
if ( isOldProto ) { if ( isOldProto ) {
nli = NetLaunchInfo.makeFrom( XWApp.getContext(), nli = NetLaunchInfo.makeFrom( getContext(),
dis.readUTF() ); dis.readUTF() );
} else { } else {
data = new byte[dis.readShort()]; data = new byte[dis.readShort()];
@ -1258,7 +1309,7 @@ public class BTUtils {
{ {
Log.d( TAG, "receivePing()" ); Log.d( TAG, "receivePing()" );
boolean deleted = 0 != gameID boolean deleted = 0 != gameID
&& !DBUtils.haveGame( XWApp.getContext(), gameID ); && !DBUtils.haveGame( getContext(), gameID );
DataOutputStream os = new DataOutputStream( socket.getOutputStream() ); DataOutputStream os = new DataOutputStream( socket.getOutputStream() );
os.writeByte( BTCmd.PONG.ordinal() ); os.writeByte( BTCmd.PONG.ordinal() );
@ -1314,18 +1365,30 @@ public class BTUtils {
{ {
ReadThread result; ReadThread result;
synchronized ( sInstance ) { synchronized ( sInstance ) {
result = (ReadThread)sInstance[0]; result = (ReadThread)sInstance.get();
if ( null == result ) { if ( null == result ) {
sInstance[0] = result = new ReadThread(); result = new ReadThread();
Assert.assertTrueNR( result == sInstance.get() );
result.start(); result.start();
} }
} }
return result; return result;
} }
private static void stopSelf()
{
synchronized ( sInstance ) {
ReadThread self = (ReadThread)sInstance.get();
if ( null != self ) {
sInstance.set( null );
self.interrupt();
}
}
}
} }
private static class BTMsgSink extends MultiMsgSink { private static class BTMsgSink extends MultiMsgSink {
public BTMsgSink() { super( XWApp.getContext() ); } public BTMsgSink() { super( getContext() ); }
@Override @Override
public int sendViaBluetooth( byte[] buf, String msgID, int gameID, public int sendViaBluetooth( byte[] buf, String msgID, int gameID,
@ -1348,7 +1411,7 @@ public class BTUtils {
private CommsAddrRec mReturnAddr; private CommsAddrRec mReturnAddr;
private Context mContext; private Context mContext;
private BTHelper() { super( XWApp.getContext() ); } private BTHelper() { super( BTUtils.getContext() ); }
BTHelper( CommsAddrRec from ) BTHelper( CommsAddrRec from )
{ {

View file

@ -819,7 +819,8 @@ public class DBUtils {
return result; return result;
} }
public static int getRelayGameCount( Context context ) { public static int getGameCountUsing( Context context, CommsConnType typ )
{
int result = 0; int result = 0;
String[] columns = { DBHelper.CONTYPE }; String[] columns = { DBHelper.CONTYPE };
String selection = String.format( "%s = 0", DBHelper.GAME_OVER ); String selection = String.format( "%s = 0", DBHelper.GAME_OVER );
@ -829,7 +830,7 @@ public class DBUtils {
int indx = cursor.getColumnIndex( DBHelper.CONTYPE ); int indx = cursor.getColumnIndex( DBHelper.CONTYPE );
while ( cursor.moveToNext() ) { while ( cursor.moveToNext() ) {
CommsConnTypeSet typs = new CommsConnTypeSet( cursor.getInt(indx) ); CommsConnTypeSet typs = new CommsConnTypeSet( cursor.getInt(indx) );
if ( typs.contains(CommsConnType.COMMS_CONN_RELAY) ) { if ( typs.contains( typ ) ) {
++result; ++result;
} }
} }

View file

@ -776,7 +776,7 @@ public abstract class DelegateBase implements DlgClickNotify,
XWPrefs.setNBSEnabled( m_activity, true ); XWPrefs.setNBSEnabled( m_activity, true );
break; break;
case ENABLE_BT_DO: case ENABLE_BT_DO:
BTUtils.enable(); BTUtils.enable( m_activity );
break; break;
case ENABLE_RELAY_DO: case ENABLE_RELAY_DO:
RelayService.setEnabled( m_activity, true ); RelayService.setEnabled( m_activity, true );

View file

@ -123,6 +123,7 @@ public class DlgDelegate {
ENABLE_RELAY_DO, ENABLE_RELAY_DO,
ENABLE_RELAY_DO_OR, ENABLE_RELAY_DO_OR,
DISABLE_RELAY_DO, DISABLE_RELAY_DO,
DISABLE_BT_DO,
ASKED_PHONE_STATE, ASKED_PHONE_STATE,
PERMS_QUERY, PERMS_QUERY,
PERMS_BANNED_INFO, PERMS_BANNED_INFO,

View file

@ -61,6 +61,7 @@ public class PrefsDelegate extends DelegateBase
R.string.key_disable_nag, R.string.key_disable_nag,
R.string.key_disable_nag_solo, R.string.key_disable_nag_solo,
R.string.key_disable_relay, R.string.key_disable_relay,
R.string.key_disable_bt,
R.string.key_force_tablet, R.string.key_force_tablet,
R.string.key_mqtt_host, R.string.key_mqtt_host,
R.string.key_mqtt_port, R.string.key_mqtt_port,
@ -237,6 +238,9 @@ public class PrefsDelegate extends DelegateBase
case R.string.key_disable_relay: case R.string.key_disable_relay:
RelayService.enabledChanged( m_activity ); RelayService.enabledChanged( m_activity );
break; break;
case R.string.key_disable_bt:
BTUtils.disabledChanged( m_activity );
break;
case R.string.key_force_tablet: case R.string.key_force_tablet:
makeOkOnlyBuilder( R.string.after_restart ).show(); makeOkOnlyBuilder( R.string.after_restart ).show();
break; break;
@ -265,6 +269,10 @@ public class PrefsDelegate extends DelegateBase
RelayService.setEnabled( m_activity, false ); RelayService.setEnabled( m_activity, false );
RelayCheckBoxPreference.setChecked(); RelayCheckBoxPreference.setChecked();
break; break;
case DISABLE_BT_DO:
BTUtils.setEnabled( m_activity, false );
BTCheckBoxPreference.setChecked();
break;
default: default:
handled = super.onPosButton( action, params ); handled = super.onPosButton( action, params );
} }

View file

@ -26,6 +26,7 @@ import android.util.AttributeSet;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import org.eehouse.android.xw4.DlgDelegate.Action; import org.eehouse.android.xw4.DlgDelegate.Action;
import org.eehouse.android.xw4.jni.CommsAddrRec.CommsConnType;
import org.eehouse.android.xw4.loc.LocUtils; import org.eehouse.android.xw4.loc.LocUtils;
public class RelayCheckBoxPreference extends ConfirmingCheckBoxPreference { public class RelayCheckBoxPreference extends ConfirmingCheckBoxPreference {
@ -45,7 +46,8 @@ public class RelayCheckBoxPreference extends ConfirmingCheckBoxPreference {
String msg = LocUtils.getString( activity, String msg = LocUtils.getString( activity,
R.string.warn_relay_havegames ); R.string.warn_relay_havegames );
int count = DBUtils.getRelayGameCount( activity ); int count = DBUtils
.getGameCountUsing( activity, CommsConnType.COMMS_CONN_RELAY );
if ( 0 < count ) { if ( 0 < count ) {
msg += LocUtils.getQuantityString( activity, R.plurals.warn_relay_games_fmt, msg += LocUtils.getQuantityString( activity, R.plurals.warn_relay_games_fmt,
count, count ); count, count );

View file

@ -161,6 +161,18 @@ public class XWPrefs {
return enabled; return enabled;
} }
public static boolean getBTDisabled( Context context )
{
boolean disabled = getPrefsBoolean( context, R.string.key_disable_bt,
false );
return disabled;
}
public static void setBTDisabled( Context context, boolean disabled )
{
setPrefsBoolean( context, R.string.key_disable_bt, disabled );
}
public static boolean getSkipToWebAPI( Context context ) public static boolean getSkipToWebAPI( Context context )
{ {
return getPrefsBoolean( context, R.string.key_relay_via_http_first, false ); return getPrefsBoolean( context, R.string.key_relay_via_http_first, false );

View file

@ -68,6 +68,7 @@
<string name="key_default_timerenabled">key_default_timerenabled</string> <string name="key_default_timerenabled">key_default_timerenabled</string>
<string name="key_notify_sound">key_notify_sound</string> <string name="key_notify_sound">key_notify_sound</string>
<string name="key_disable_relay">key_disable_relay</string> <string name="key_disable_relay">key_disable_relay</string>
<string name="key_disable_bt">key_disable_bt</string>
<string name="key_notify_vibrate">key_notify_vibrate</string> <string name="key_notify_vibrate">key_notify_vibrate</string>
<string name="key_enable_nbs">key_enable_nbs</string> <string name="key_enable_nbs">key_enable_nbs</string>
<string name="key_enable_p2p">key_enable_p2p</string> <string name="key_enable_p2p">key_enable_p2p</string>

View file

@ -967,6 +967,8 @@
networked games</string> networked games</string>
<string name="disable_relay">Disable play via the relay </string> <string name="disable_relay">Disable play via the relay </string>
<string name="disable_relay_summary">Disable all internet communication</string> <string name="disable_relay_summary">Disable all internet communication</string>
<string name="disable_bt">Disable play via Bluetooth</string>
<string name="disable_bt_summary">Disable all Bluetooth communication</string>
<!-- <!--
############################################################ ############################################################
# :Screens: # :Screens:
@ -1874,9 +1876,9 @@
disabled. No moves will be sent via Data SMS.\n\nYou can enable disabled. No moves will be sent via Data SMS.\n\nYou can enable
play via Data SMS now, or later. play via Data SMS now, or later.
</string> </string>
<string name="warn_bt_disabled">Bluetooth is currently off on this <string name="warn_bt_disabled">Bluetooth play is currently
device. No moves will be sent via Bluetooth.\n\nYou can enable disabled, and no moves will be exchanged via Bluetooth until it is
Bluetooth now, or later. enabled.\n\nYou can enable Bluetooth now, or later.
</string> </string>
<string name="warn_relay_disabled">Relay play is currently disable <string name="warn_relay_disabled">Relay play is currently disable
on this device. No moves will be sent or received via the on this device. No moves will be sent or received via the
@ -1891,14 +1893,26 @@
Most networked games exchange moves via the relay, so only do this Most networked games exchange moves via the relay, so only do this
if you plan to play ALL games against a robot on this same if you plan to play ALL games against a robot on this same
device.</string> device.</string>
<string name="warn_bt_havegames">Are you sure you want to
disable play using Bluetooth?
\n\nBluetooth is useful for exchanging moves, and especially
invitations, with devices physically close to you. Unless youre
certain you wont play against anybody nearby theres little harm
in leaving it on.
</string>
<plurals name="warn_relay_games_fmt"> <plurals name="warn_relay_games_fmt">
<item quantity="one">\n\n(You have one active game using the relay now.)</item> <item quantity="one">\n\n(You have one active game using the relay now.)</item>
<item quantity="other">\n\n(You have %1$d active games using the relay now.)</item> <item quantity="other">\n\n(You have %1$d active games using the relay now.)</item>
</plurals> </plurals>
<plurals name="warn_bt_games_fmt">
<item quantity="one">\n\n(You have one active game using Bluetooth.)</item>
<item quantity="other">\n\n(You have %1$d active games using Bluetooth.)</item>
</plurals>
<string name="button_enable_sms">Enable Data SMS</string> <string name="button_enable_sms">Enable Data SMS</string>
<string name="button_enable_bt">Enable Bluetooth</string> <string name="button_enable_bt">Enable Bluetooth</string>
<string name="button_enable_relay">Enable Relay play</string> <string name="button_enable_relay">Enable Relay play</string>
<string name="button_disable_relay">Disable Relay play</string> <string name="button_disable_relay">Disable Relay play</string>
<string name="button_disable_bt">Disable Bluetooth play</string>
<string name="button_later">Later</string> <string name="button_later">Later</string>
<!-- --> <!-- -->
<string name="gamel_menu_checkupdates">Check for updates</string> <string name="gamel_menu_checkupdates">Check for updates</string>

View file

@ -369,6 +369,13 @@
android:defaultValue="false" android:defaultValue="false"
/> />
<org.eehouse.android.xw4.BTCheckBoxPreference
android:key="@string/key_disable_bt"
android:title="@string/disable_bt"
android:summary="@string/disable_bt_summary"
android:defaultValue="false"
/>
</PreferenceScreen> </PreferenceScreen>
<!-- For Debugging --> <!-- For Debugging -->