diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTCheckBoxPreference.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTCheckBoxPreference.java new file mode 100644 index 000000000..a9d27a352 --- /dev/null +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTCheckBoxPreference.java @@ -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 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 ); + } + } + } +} diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTUtils.java index d7019eda9..7d2feb3ab 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/BTUtils.java @@ -47,6 +47,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import org.eehouse.android.xw4.DbgUtils.DeadlockWatch; import org.eehouse.android.xw4.MultiService.DictFetchOwner; @@ -99,7 +100,7 @@ public class BTUtils { public static boolean BTAvailable() { - BluetoothAdapter adapter = getAdapterIf(); + BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); return null != adapter; } @@ -109,22 +110,38 @@ public class BTUtils { return null != adapter && adapter.isEnabled(); } - public static void enable() + public static void enable( Context context ) { BluetoothAdapter adapter = getAdapterIf(); if ( null != adapter ) { // Only do this after explicit action from user -- Android guidelines 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() { BluetoothAdapter result = null; - // Later this will change to include at least a test whether we're - // running as background user account, a situation in which BT crashes - // a lot inside the OS. - if ( !sBackUser.get() ) { + // BT crashes a lot inside the OS when running on behalf of a + // background user account. We catch exceptions that indicate that's + // going on and set this flag. + if ( ! XWPrefs.getBTDisabled( getContext() ) && !sBackUser.get() ) { result = BluetoothAdapter.getDefaultAdapter(); } return result; @@ -195,6 +212,12 @@ public class BTUtils { sBackUser.set( false ); } + private static void stopThreads() + { + ListenThread.stopSelf(); + ReadThread.stopSelf(); + } + private static String nameForAddr( BluetoothAdapter adapter, String btAddr ) { String result = null; @@ -316,14 +339,14 @@ public class BTUtils { private static void updateStatusIn( boolean success ) { - Context context = XWApp.getContext(); + Context context = getContext(); ConnStatusHandler .updateStatusIn( context, CommsConnType.COMMS_CONN_BT, success ); } private static void updateStatusOut( boolean success ) { - Context context = XWApp.getContext(); + Context context = getContext(); ConnStatusHandler .updateStatusOut( context, CommsConnType.COMMS_CONN_BT, success ); } @@ -398,32 +421,36 @@ public class BTUtils { return btAddr; } - private static void clearInstance( Thread[] holder, Thread instance ) + private static void clearInstance( AtomicReference holder, + Thread instance ) { synchronized ( holder ) { - if ( holder[0] == null ) { + Thread curThread = holder.get(); + if ( null == curThread ) { // nothing to do - } else if ( holder[0] == instance ) { - holder[0] = null; + } else if ( instance == curThread ) { + holder.set( null ); } else { 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 Thread[] sInstance = {null}; + private static AtomicReference sInstance = new AtomicReference<>(); private int mTimeoutMS; private Set mDevs; static void startOnce( int timeoutMS, Set devs ) { synchronized ( sInstance ) { - if ( sInstance[0] == null ) { + if ( null == sInstance.get() ) { ScanThread thread = new ScanThread( timeoutMS, devs ); - sInstance[0] = thread; + Assert.assertTrueNR( thread == sInstance.get() ); thread.start(); } } @@ -433,12 +460,13 @@ public class BTUtils { { mTimeoutMS = timeoutMS; mDevs = devs; + sInstance.set( this ); } @Override public void run() { - Assert.assertTrueNR( this == sInstance[0] ); + Assert.assertTrueNR( this == sInstance.get() ); Map pas = new HashMap<>(); for ( BluetoothDevice dev : mDevs ) { @@ -1049,12 +1077,14 @@ public class BTUtils { } // class PacketAccumulator private static class ListenThread extends Thread { - private static Thread[] sInstance = {null}; + private static AtomicReference sInstance = new AtomicReference<>(); private BluetoothAdapter mAdapter; + private BluetoothServerSocket mServerSocket; private ListenThread( BluetoothAdapter adapter ) { mAdapter = adapter; + sInstance.set( this ); } @Override @@ -1062,32 +1092,31 @@ public class BTUtils { { Log.d( TAG, "ListenThread: %s.run() starting", this ); - BluetoothServerSocket serverSocket; try { Assert.assertTrueNR( null != sAppName && null != sUUID ); - serverSocket = mAdapter + mServerSocket = mAdapter .listenUsingRfcommWithServiceRecord( sAppName, sUUID ); } catch ( IOException ioe ) { Log.ex( TAG, ioe ); - serverSocket = null; + mServerSocket = null; } catch ( SecurityException ex ) { // Got this with a message saying not allowed to call // listenUsingRfcommWithServiceRecord() in background (on // Android 9) sBackUser.set( true ); // two-user system: disable BT 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 ); try { - BluetoothSocket socket = serverSocket.accept(); // blocks + BluetoothSocket socket = mServerSocket.accept(); // blocks Log.d( TAG, "%s.run(): accept() returned", this ); ReadThread.handle( socket ); } catch ( IOException ioe ) { Log.ex( TAG, ioe ); - serverSocket = null; + mServerSocket = null; } } @@ -1100,20 +1129,41 @@ public class BTUtils { ListenThread result = null; BluetoothAdapter adapter = getAdapterIf(); if ( null != adapter ) { - synchronized (sInstance) { - result = (ListenThread)sInstance[0]; + synchronized ( sInstance ) { + result = (ListenThread)sInstance.get(); if ( null == result ) { - sInstance[0] = result = new ListenThread( adapter ); + result = new ListenThread( adapter ); + Assert.assertTrueNR( result == sInstance.get() ); result.start(); } } } 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 Thread[] sInstance = {null}; + private static AtomicReference sInstance = new AtomicReference<>(); private LinkedBlockingQueue mQueue; private BTMsgSink mBTMsgSink; @@ -1128,13 +1178,14 @@ public class BTUtils { { mQueue = new LinkedBlockingQueue<>(); mBTMsgSink = new BTMsgSink(); + sInstance.set( this ); } @Override public void run() { Log.d( TAG, "ReadThread: %s.run() starting", this ); - for ( ; ; ) { + while ( this == sInstance.get() ) { try { BluetoothSocket socket = mQueue.take(); DataInputStream inStream = @@ -1144,7 +1195,7 @@ public class BTUtils { BTInviteDelegate.onHeardFromDev( socket.getRemoteDevice() ); parsePacket( proto, inStream, socket ); updateStatusIn( true ); - TimerReceiver.restartBackoff( XWApp.getContext() ); + TimerReceiver.restartBackoff( getContext() ); // nBadCount = 0; } else { writeBack( socket, BTCmd.BAD_PROTO ); @@ -1210,7 +1261,7 @@ public class BTUtils { case INVITE: NetLaunchInfo nli; if ( isOldProto ) { - nli = NetLaunchInfo.makeFrom( XWApp.getContext(), + nli = NetLaunchInfo.makeFrom( getContext(), dis.readUTF() ); } else { data = new byte[dis.readShort()]; @@ -1258,7 +1309,7 @@ public class BTUtils { { Log.d( TAG, "receivePing()" ); boolean deleted = 0 != gameID - && !DBUtils.haveGame( XWApp.getContext(), gameID ); + && !DBUtils.haveGame( getContext(), gameID ); DataOutputStream os = new DataOutputStream( socket.getOutputStream() ); os.writeByte( BTCmd.PONG.ordinal() ); @@ -1314,18 +1365,30 @@ public class BTUtils { { ReadThread result; synchronized ( sInstance ) { - result = (ReadThread)sInstance[0]; + result = (ReadThread)sInstance.get(); if ( null == result ) { - sInstance[0] = result = new ReadThread(); + result = new ReadThread(); + Assert.assertTrueNR( result == sInstance.get() ); result.start(); } } 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 { - public BTMsgSink() { super( XWApp.getContext() ); } + public BTMsgSink() { super( getContext() ); } @Override public int sendViaBluetooth( byte[] buf, String msgID, int gameID, @@ -1348,7 +1411,7 @@ public class BTUtils { private CommsAddrRec mReturnAddr; private Context mContext; - private BTHelper() { super( XWApp.getContext() ); } + private BTHelper() { super( BTUtils.getContext() ); } BTHelper( CommsAddrRec from ) { diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java index 0c3ae612f..a5311a30b 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DBUtils.java @@ -819,7 +819,8 @@ public class DBUtils { return result; } - public static int getRelayGameCount( Context context ) { + public static int getGameCountUsing( Context context, CommsConnType typ ) + { int result = 0; String[] columns = { DBHelper.CONTYPE }; String selection = String.format( "%s = 0", DBHelper.GAME_OVER ); @@ -829,7 +830,7 @@ public class DBUtils { int indx = cursor.getColumnIndex( DBHelper.CONTYPE ); while ( cursor.moveToNext() ) { CommsConnTypeSet typs = new CommsConnTypeSet( cursor.getInt(indx) ); - if ( typs.contains(CommsConnType.COMMS_CONN_RELAY) ) { + if ( typs.contains( typ ) ) { ++result; } } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DelegateBase.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DelegateBase.java index b5d791215..e05c614d1 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DelegateBase.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DelegateBase.java @@ -776,7 +776,7 @@ public abstract class DelegateBase implements DlgClickNotify, XWPrefs.setNBSEnabled( m_activity, true ); break; case ENABLE_BT_DO: - BTUtils.enable(); + BTUtils.enable( m_activity ); break; case ENABLE_RELAY_DO: RelayService.setEnabled( m_activity, true ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java index 8583ee001..f283303c1 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/DlgDelegate.java @@ -123,6 +123,7 @@ public class DlgDelegate { ENABLE_RELAY_DO, ENABLE_RELAY_DO_OR, DISABLE_RELAY_DO, + DISABLE_BT_DO, ASKED_PHONE_STATE, PERMS_QUERY, PERMS_BANNED_INFO, diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java index 65919a3a8..de684af39 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/PrefsDelegate.java @@ -61,6 +61,7 @@ public class PrefsDelegate extends DelegateBase R.string.key_disable_nag, R.string.key_disable_nag_solo, R.string.key_disable_relay, + R.string.key_disable_bt, R.string.key_force_tablet, R.string.key_mqtt_host, R.string.key_mqtt_port, @@ -237,6 +238,9 @@ public class PrefsDelegate extends DelegateBase case R.string.key_disable_relay: RelayService.enabledChanged( m_activity ); break; + case R.string.key_disable_bt: + BTUtils.disabledChanged( m_activity ); + break; case R.string.key_force_tablet: makeOkOnlyBuilder( R.string.after_restart ).show(); break; @@ -265,6 +269,10 @@ public class PrefsDelegate extends DelegateBase RelayService.setEnabled( m_activity, false ); RelayCheckBoxPreference.setChecked(); break; + case DISABLE_BT_DO: + BTUtils.setEnabled( m_activity, false ); + BTCheckBoxPreference.setChecked(); + break; default: handled = super.onPosButton( action, params ); } diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayCheckBoxPreference.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayCheckBoxPreference.java index 886fde7ab..02eef950a 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayCheckBoxPreference.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/RelayCheckBoxPreference.java @@ -26,6 +26,7 @@ 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 RelayCheckBoxPreference extends ConfirmingCheckBoxPreference { @@ -45,7 +46,8 @@ public class RelayCheckBoxPreference extends ConfirmingCheckBoxPreference { String msg = LocUtils.getString( activity, R.string.warn_relay_havegames ); - int count = DBUtils.getRelayGameCount( activity ); + int count = DBUtils + .getGameCountUsing( activity, CommsConnType.COMMS_CONN_RELAY ); if ( 0 < count ) { msg += LocUtils.getQuantityString( activity, R.plurals.warn_relay_games_fmt, count, count ); diff --git a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java index 49c21d8ee..9c366926b 100644 --- a/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java +++ b/xwords4/android/app/src/main/java/org/eehouse/android/xw4/XWPrefs.java @@ -161,6 +161,18 @@ public class XWPrefs { 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 ) { return getPrefsBoolean( context, R.string.key_relay_via_http_first, false ); diff --git a/xwords4/android/app/src/main/res/values/common_rsrc.xml b/xwords4/android/app/src/main/res/values/common_rsrc.xml index 9b6cc07ac..e4332750d 100644 --- a/xwords4/android/app/src/main/res/values/common_rsrc.xml +++ b/xwords4/android/app/src/main/res/values/common_rsrc.xml @@ -68,6 +68,7 @@ key_default_timerenabled key_notify_sound key_disable_relay + key_disable_bt key_notify_vibrate key_enable_nbs key_enable_p2p diff --git a/xwords4/android/app/src/main/res/values/strings.xml b/xwords4/android/app/src/main/res/values/strings.xml index 9013e6509..6c6da97a8 100644 --- a/xwords4/android/app/src/main/res/values/strings.xml +++ b/xwords4/android/app/src/main/res/values/strings.xml @@ -967,6 +967,8 @@ networked games Disable play via the relay Disable all internet communication + Disable play via Bluetooth + Disable all Bluetooth communication Check for updates diff --git a/xwords4/android/app/src/main/res/xml/xwprefs.xml b/xwords4/android/app/src/main/res/xml/xwprefs.xml index aaa33d303..c02c8299c 100644 --- a/xwords4/android/app/src/main/res/xml/xwprefs.xml +++ b/xwords4/android/app/src/main/res/xml/xwprefs.xml @@ -369,6 +369,13 @@ android:defaultValue="false" /> + +