2
\$\begingroup\$

I am writing my first Android application and have mainly been using code examples online. However, I have been going hard at it for a while now and realize that I have some overlapping methods and such in my code.

I have three classes:

  1. AndroidBluetooth (main activity)
  2. BluetoothModel (the class that holds all of the Bluetooth information)
  3. DeviceList (activity to prompt user for Bluetooth device selection)

I still don't really know what I'm doing and don't want to blindly delete things, so if anyone sees any unneeded methods I'd appreciate your opinions.

AndroidBluetooth:

public class AndroidBluetooth extends Activity {
 /** Called when the activity is first created. */
 private static BluetoothAdapter myBtAdapter;
 private static BluetoothDevice myBtDevice;
 private ArrayAdapter<String> btArrayAdapter;
 private ArrayList<BluetoothDevice> btDevicesFound = new ArrayList<BluetoothDevice>();
 private Button btnScanDevice;
 private TextView stateBluetooth;
 private ListView listDevicesFound;
 private InputStream iStream;
 private OutputStream oStream;
 private BluetoothSocket btSocket;
 private String newDeviceAddress;
 private BroadcastReceiver mReceiver;
 private static BluetoothSerialService mSerialService = null;
 // Intent request codes
 private static final int REQUEST_CONNECT_DEVICE = 1;
 private static TextView mTitle;
 // Key names received from the BluetoothChatService Handler
 public static final String DEVICE_NAME = "device_name";
 public static final String TOAST = "toast";
 // Message types sent from the BluetoothReadService Handler
 public static final int MESSAGE_STATE_CHANGE = 1;
 public static final int MESSAGE_READ = 2;
 public static final int MESSAGE_WRITE = 3;
 public static final int MESSAGE_DEVICE_NAME = 4;
 public static final int MESSAGE_TOAST = 5; 
 // Name of the connected device
 private String mConnectedDeviceName = null;
 /**
 * Set to true to add debugging code and logging.
 */
 public static final boolean D = true;
 /**
 * Set to true to log each character received from the remote process to the
 * android log, which makes it easier to debug some kinds of problems with
 * emulating escape sequences and control codes.
 */
 public static final boolean LOG_CHARACTERS_FLAG = D && false;
 /**
 * Set to true to log unknown escape sequences.
 */
 public static final boolean LOG_UNKNOWN_ESCAPE_SEQUENCES = D && false;
 private static final String TAG = "ANDROID BLUETOOTH";
 private static final int REQUEST_ENABLE_BT = 2;
 // Member fields
 //private final Handler mHandler;
 private ConnectThread mConnectThread;
 private ConnectedThread mConnectedThread;
 private int mState;
 //private EmulatorView mEmulatorView;
 // Constants that indicate the current connection state
 public static final int STATE_NONE = 0; // we're doing nothing
 public static final int STATE_LISTEN = 1; // now listening for incoming connections
 public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection
 public static final int STATE_CONNECTED = 3; // now connected to a remote device
 public int currentState;
 public boolean customTitleSupported;
 public BluetoothModel btModel;
 @Override
 public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 currentState = 0;
 customTitleSupported = requestWindowFeature( Window.FEATURE_CUSTOM_TITLE );
 // Set up window View
 setContentView(R.layout.main);
 stateBluetooth = new TextView(this);
 myBtAdapter = null;
 startBluetooth();
 CheckBlueToothState();
 customTitleBar( getText( R.string.app_name).toString(), stateBluetooth.getText().toString() );
 }
 public void customTitleBar( String left, String right ) {
 if( right.length() > 30 ) right = right.substring( 0, 20 );
 if( customTitleSupported ) {
 getWindow().setFeatureInt( Window.FEATURE_CUSTOM_TITLE, R.layout.customlayoutbar );
 TextView titleTvLeft = (TextView) findViewById( R.id.titleTvLeft );
 TextView titleTvRight = (TextView) findViewById( R.id.titleTvRight );
 titleTvLeft.setText( left );
 titleTvRight.setText( right );
 }
 }
 public boolean onCreateOptionsMenu( Menu menu ) {
 MenuInflater inflater = getMenuInflater();
 inflater.inflate( R.menu.option_menu, menu );
 return true;
 }
 public boolean onOptionsItemSelected( MenuItem item ) {
 switch( item.getItemId() ) {
 case R.id.connect:
 startActivityForResult( new Intent( this, DeviceList.class ), REQUEST_CONNECT_DEVICE );
 return true;
 case R.id.preferences:
 return true;
 default:
 return super.onContextItemSelected( item );
 }
 }
 private void CheckBlueToothState() {
 if( myBtAdapter == null ) {
 stateBluetooth.setText("Bluetooth NOT supported" );
 } else {
 if( myBtAdapter.isEnabled() ) {
 if( myBtAdapter.isDiscovering() ) {
 stateBluetooth.setText( "Bluetooth is currently " +
 "in device discovery process." );
 } else {
 stateBluetooth.setText( "Bluetooth is Enabled." );
 }
 } else {
 stateBluetooth.setText( "Bluetooth is NOT enabled" );
 Intent enableBtIntent = new Intent( BluetoothAdapter.ACTION_REQUEST_ENABLE );
 startActivityForResult( enableBtIntent, REQUEST_ENABLE_BT );
 }
 }
 }
 public void onActivityResult(int requestCode, int resultCode, Intent data) {
 if(D) Log.d( TAG, "onActivityResult " + resultCode);
 switch (requestCode) {
 case REQUEST_CONNECT_DEVICE:
 // When DeviceListActivity returns with a device to connect
 if (resultCode == Activity.RESULT_OK) {
 // Get the device MAC address
 String address = data.getExtras()
 .getString(DeviceList.EXTRA_DEVICE_ADDRESS);
 // Get the BLuetoothDevice object
 BluetoothDevice device = myBtAdapter.getRemoteDevice(address);
 // Attempt to connect to the device
 btModel.connect(device); 
 }
 break;
 case REQUEST_ENABLE_BT:
 // When the request to enable Bluetooth returns
 CheckBlueToothState();
 }
 }
 //In SDK15 (4.0.3) this method is now public as
 //Bluetooth.fetchUuisWithSdp() and BluetoothDevice.getUuids()
 public ParcelUuid[] servicesFromDevice(BluetoothDevice device) {
 try {
 Class cl = Class.forName("android.bluetooth.BluetoothDevice");
 Class[] par = {};
 Method method = cl.getMethod("getUuids", par);
 Object[] args = {};
 ParcelUuid[] retval = (ParcelUuid[]) method.invoke(device, args);
 return retval;
 } catch (Exception e) {
 e.printStackTrace();
 return null;
 }
 }
 private final BroadcastReceiver ActionFoundReceiver = new BroadcastReceiver() {
 public void onReceive( Context context, Intent intent ) {
 String action = intent.getAction();
 if( BluetoothDevice.ACTION_FOUND.equals( action ) ) {
 BluetoothDevice btDevice = intent.getParcelableExtra( BluetoothDevice.EXTRA_DEVICE );
 btDevicesFound.add( btDevice );
 btArrayAdapter.add( btDevice.getName() + "\n" + btDevice.getAddress() );
 btArrayAdapter.notifyDataSetChanged();
 } 
 }
 };
 public static void startBluetooth(){
 try {
 myBtAdapter = BluetoothAdapter.getDefaultAdapter();
 myBtAdapter.enable();
 } catch ( NullPointerException ex ) {
 Log.e( "Bluetooth", "Device not available" );
 }
 }
 public static void stopBluetooth() {
 myBtAdapter.disable();
 }
}

BluetoothModel:

public class BluetoothModel {
 // Debugging
 private static final String TAG = "BluetoothModel";
 private static final boolean D = true;
 // Member fields
 private final BluetoothAdapter myAdapter;
 private final Handler myHandler;
 private Context myContext;
 private ConnectThread myConnectThread;
 private ConnectedThread myConnectedThread;
 private int myState;
 // Constants that indicate the current connection state
 public static final int STATE_NONE = 0;
 public static final int STATE_LISTEN = 1;
 public static final int STATE_CONNECTING = 2;
 public static final int STATE_CONNECTED = 3;
 public BluetoothModel( Context context, Handler handler ) {
 myContext = context;
 myAdapter = BluetoothAdapter.getDefaultAdapter();
 myState = STATE_NONE;
 myHandler = handler;
 }
 /**
 * Set the connection state.
 * 
 * @param state
 */
 public synchronized void setState( int state ) {
 if( D ) Log.d( TAG, "setState() " + myState + " -> " + state );
 myState = state;
 myHandler.obtainMessage( AndroidBluetooth.MESSAGE_STATE_CHANGE, state, -1 ).sendToTarget();
 }
 /**
 * Get the connection state.
 * 
 * @return
 */
 public synchronized int getState() {
 return myState;
 }
 /**
 * Indicate that the connection attempt failed and notify the UI Activity.
 *
 */
 private void connectionFailed() {
 setState(STATE_NONE);
 // Send a failure message back to the Activity
 Message msg = myHandler.obtainMessage(AndroidBluetooth.MESSAGE_TOAST);
 Bundle bundle = new Bundle();
 bundle.putString(AndroidBluetooth.TOAST, "Unable to connect device");
 msg.setData(bundle);
 myHandler.sendMessage(msg);
 }
 /**
 * Indicate that the connection was lost and notify the UI Activity.
 *
 */
 private void connectionLost() {
 setState(STATE_NONE);
 // Send a failure message back to the Activity
 Message msg = myHandler.obtainMessage(AndroidBluetooth.MESSAGE_TOAST);
 Bundle bundle = new Bundle();
 bundle.putString(AndroidBluetooth.TOAST, "Device connection was lost");
 msg.setData(bundle);
 myHandler.sendMessage(msg);
 }
 //private EmulatorView mEmulatorView;
 /**
 * Start the chat service. Specifically start AcceptThread to begin a
 * session in listening (server) mode. Called by the Activity onResume() */
 public synchronized void start() {
 if (D) Log.d(TAG, "start");
 // Cancel any thread attempting to make a connection
 if (myConnectThread != null) {
 myConnectThread.cancel(); 
 myConnectThread = null;
 }
 // Cancel any thread currently running a connection
 if (myConnectedThread != null) {
 myConnectedThread.cancel(); 
 myConnectedThread = null;
 }
 setState(STATE_NONE);
 }
 /**
 * Start the ConnectThread to initiate a connection to a remote device.
 * @param device The BluetoothDevice to connect
 */
 public synchronized void connect(BluetoothDevice device) {
 if (D) Log.d(TAG, "connect to: " + device);
 // Cancel any thread attempting to make a connection
 if (myState == STATE_CONNECTING) {
 if (myConnectThread != null) {myConnectThread.cancel(); myConnectThread = null;}
 }
 // Cancel any thread currently running a connection
 if (myConnectedThread != null) {myConnectedThread.cancel(); myConnectedThread = null;}
 // Start the thread to connect with the given device
 myConnectThread = new ConnectThread(device);
 myConnectThread.start();
 setState(STATE_CONNECTING);
 }
 /**
 * Start the ConnectedThread to begin managing a Bluetooth connection
 * @param socket The BluetoothSocket on which the connection was made
 * @param device The BluetoothDevice that has been connected
 */
 public synchronized void connected(BluetoothSocket socket, BluetoothDevice device) {
 if (D) Log.d(TAG, "connected");
 // Cancel the thread that completed the connection
 if (myConnectThread != null) {
 myConnectThread.cancel(); 
 myConnectThread = null;
 }
 // Cancel any thread currently running a connection
 if (myConnectedThread != null) {
 myConnectedThread.cancel(); 
 myConnectedThread = null;
 }
 // Start the thread to manage the connection and perform transmissions
 myConnectedThread = new ConnectedThread(socket);
 myConnectedThread.start();
 // Send the name of the connected device back to the UI Activity
 Message msg = myHandler.obtainMessage(AndroidBluetooth.MESSAGE_DEVICE_NAME);
 Bundle bundle = new Bundle();
 bundle.putString(AndroidBluetooth.DEVICE_NAME, device.getName());
 msg.setData(bundle);
 myHandler.sendMessage(msg);
 setState(STATE_CONNECTED);
 }
 /**
 * Stop all threads
 */
 public synchronized void stop() {
 if (D) Log.d(TAG, "stop");
 if (myConnectThread != null) {
 myConnectThread.cancel(); 
 myConnectThread = null;
 }
 if (myConnectedThread != null) {
 myConnectedThread.cancel(); 
 myConnectedThread = null;
 }
 setState(STATE_NONE);
 }
 /**
 * Write to the ConnectedThread in an unsynchronized manner
 * @param out The bytes to write
 * @see ConnectedThread#write(byte[])
 */
 public void write(byte[] out) {
 // Create temporary object
 ConnectedThread r;
 // Synchronize a copy of the ConnectedThread
 synchronized (this) {
 if (myState != STATE_CONNECTED) return;
 r = myConnectedThread;
 }
 // Perform the write unsynchronized
 r.write(out);
 }
 /**
 * This thread runs while attempting to make an outgoing connection
 * with a device. It runs straight through; the connection either
 * succeeds or fails.
 */
 public class ConnectThread extends Thread {
 private final BluetoothSocket mmSocket;
 private final BluetoothDevice mmDevice;
 public ConnectThread(BluetoothDevice device) {
 mmDevice = device;
 BluetoothSocket tmp = null;
 // Get a BluetoothSocket for a connection with the
 // given BluetoothDevice
 try {
 tmp = device.createRfcommSocketToServiceRecord(SPP_UUID);
 } catch (IOException e) {
 Log.e(TAG, "create() failed", e);
 }
 mmSocket = tmp;
 }
 public void run() {
 Log.i(TAG, "BEGIN mConnectThread");
 setName("ConnectThread");
 // Always cancel discovery because it will slow down a connection
 myAdapter.cancelDiscovery();
 // Make a connection to the BluetoothSocket
 try {
 // This is a blocking call and will only return on a
 // successful connection or an exception
 mmSocket.connect();
 } catch (IOException e) {
 connectionFailed();
 // Close the socket
 try {
 mmSocket.close();
 } catch (IOException e2) {
 Log.e(TAG, "unable to close() socket during connection failure", e2);
 }
 // Start the service over to restart listening mode
 //BluetoothSerialService.this.start();
 return;
 }
 // Reset the ConnectThread because we're done
 synchronized (BluetoothModel.this) {
 myConnectThread = null;
 }
 // Start the connected thread
 connected(mmSocket, mmDevice);
 }
 public void cancel() {
 try {
 mmSocket.close();
 } catch (IOException e) {
 Log.e(TAG, "close() of connect socket failed", e);
 }
 }
 }
 /**
 * This thread runs during a connection with a remote device.
 * It handles all incoming and outgoing transmissions.
 */
 public class ConnectedThread extends Thread {
 private final BluetoothSocket mmSocket;
 private final InputStream mmInStream;
 private final OutputStream mmOutStream;
 public ConnectedThread(BluetoothSocket socket) {
 Log.d(TAG, "create ConnectedThread");
 mmSocket = socket;
 InputStream tmpIn = null;
 OutputStream tmpOut = null;
 // Get the BluetoothSocket input and output streams
 try {
 tmpIn = socket.getInputStream();
 tmpOut = socket.getOutputStream();
 } catch (IOException e) {
 Log.e(TAG, "temp sockets not created", e);
 }
 mmInStream = tmpIn;
 mmOutStream = tmpOut;
 }
 public void run() {
 Log.i(TAG, "BEGIN mConnectedThread");
 byte[] buffer = new byte[1024];
 int bytes;
 // Keep listening to the InputStream while connected
 while (true) {
 try {
 // Read from the InputStream
 bytes = mmInStream.read(buffer);
 //mEmulatorView.write(buffer, bytes);
 // Send the obtained bytes to the UI Activity
 //mHandler.obtainMessage(BlueTerm.MESSAGE_READ, bytes, -1, buffer).sendToTarget();
 String a = buffer.toString();
 a = "";
 } catch (IOException e) {
 Log.e(TAG, "disconnected", e);
 connectionLost();
 break;
 }
 }
 }
 /**
 * Write to the connected OutStream.
 * @param buffer The bytes to write
 */
 public void write(byte[] buffer) {
 try {
 mmOutStream.write(buffer);
 // Share the sent message back to the UI Activity
 //mHandler.obtainMessage(BlueTerm.MESSAGE_WRITE, buffer.length, -1, buffer)
 //.sendToTarget();
 } catch (IOException e) {
 Log.e(TAG, "Exception during write", e);
 }
 }
 public void cancel() {
 try {
 mmSocket.close();
 } catch (IOException e) {
 Log.e(TAG, "close() of connect socket failed", e);
 }
 }
 }
}

DeviceList:

public class DeviceList extends Activity {
 // Debugging
 private static final String TAG = "DeviceListActivity";
 private static final boolean D = true;
 // Return Intent extra
 public static String EXTRA_DEVICE_ADDRESS = "device_address";
 // Member fields
 private BluetoothAdapter mBtAdapter;
 private ArrayAdapter<String> mPairedDevicesArrayAdapter;
 private ArrayAdapter<String> mNewDevicesArrayAdapter;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 // Setup the window
 requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
 setContentView(R.layout.device_list);
 // Set result CANCELED incase the user backs out
 setResult(Activity.RESULT_CANCELED);
 // Initialize the button to perform device discovery
 Button scanButton = (Button) findViewById(R.id.button_scan);
 scanButton.setOnClickListener(new OnClickListener() {
 public void onClick(View v) {
 doDiscovery();
 v.setVisibility(View.GONE);
 }
 });
 // Initialize array adapters. One for already paired devices and
 // one for newly discovered devices
 mPairedDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name);
 mNewDevicesArrayAdapter = new ArrayAdapter<String>(this, R.layout.device_name);
 // Find and set up the ListView for paired devices
 ListView pairedListView = (ListView) findViewById(R.id.paired_devices);
 pairedListView.setAdapter(mPairedDevicesArrayAdapter);
 pairedListView.setOnItemClickListener(mDeviceClickListener);
 // Find and set up the ListView for newly discovered devices
 ListView newDevicesListView = (ListView) findViewById(R.id.new_devices);
 newDevicesListView.setAdapter(mNewDevicesArrayAdapter);
 newDevicesListView.setOnItemClickListener(mDeviceClickListener);
 // Register for broadcasts when a device is discovered
 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
 this.registerReceiver(mReceiver, filter);
 // Register for broadcasts when discovery has finished
 filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
 this.registerReceiver(mReceiver, filter);
 // Get the local Bluetooth adapter
 mBtAdapter = BluetoothAdapter.getDefaultAdapter();
 // Get a set of currently paired devices
 Set<BluetoothDevice> pairedDevices = mBtAdapter.getBondedDevices();
 // If there are paired devices, add each one to the ArrayAdapter
 if (pairedDevices.size() > 0) {
 findViewById(R.id.title_paired_devices).setVisibility(View.VISIBLE);
 for (BluetoothDevice device : pairedDevices) {
 mPairedDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
 }
 } else {
 String noDevices = getResources().getText(R.string.none_paired).toString();
 mPairedDevicesArrayAdapter.add(noDevices);
 }
 }
 @Override
 protected void onDestroy() {
 super.onDestroy();
 // Make sure we're not doing discovery anymore
 if (mBtAdapter != null) {
 mBtAdapter.cancelDiscovery();
 }
 // Unregister broadcast listeners
 this.unregisterReceiver(mReceiver);
 }
 /**
 * Start device discover with the BluetoothAdapter
 */
 private void doDiscovery() {
 if (D) Log.d(TAG, "doDiscovery()");
 // Indicate scanning in the title
 setProgressBarIndeterminateVisibility(true);
 setTitle(R.string.scanning);
 // Turn on sub-title for new devices
 findViewById(R.id.title_new_devices).setVisibility(View.VISIBLE);
 // If we're already discovering, stop it
 if (mBtAdapter.isDiscovering()) {
 mBtAdapter.cancelDiscovery();
 }
 // Request discover from BluetoothAdapter
 mBtAdapter.startDiscovery();
 }
 // The on-click listener for all devices in the ListViews
 private OnItemClickListener mDeviceClickListener = new OnItemClickListener() {
 public void onItemClick(AdapterView<?> av, View v, int arg2, long arg3) {
 // Cancel discovery because it's costly and we're about to connect
 mBtAdapter.cancelDiscovery();
 // Get the device MAC address, which is the last 17 chars in the View
 String info = ((TextView) v).getText().toString();
 String address = info.substring(info.length() - 17);
 // Create the result Intent and include the MAC address
 Intent intent = new Intent();
 intent.putExtra(EXTRA_DEVICE_ADDRESS, address);
 // Set result and finish this Activity
 setResult(Activity.RESULT_OK, intent);
 finish();
 }
 };
 // The BroadcastReceiver that listens for discovered devices and
 // changes the title when discovery is finished
 private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
 @Override
 public void onReceive(Context context, Intent intent) {
 String action = intent.getAction();
 // When discovery finds a device
 if (BluetoothDevice.ACTION_FOUND.equals(action)) {
 // Get the BluetoothDevice object from the Intent
 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
 // If it's already paired, skip it, because it's been listed already
 if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
 mNewDevicesArrayAdapter.add(device.getName() + "\n" + device.getAddress());
 }
 // When discovery is finished, change the Activity title
 } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
 setProgressBarIndeterminateVisibility(false);
 setTitle(R.string.select_device);
 if (mNewDevicesArrayAdapter.getCount() == 0) {
 String noDevices = getResources().getText(R.string.none_found).toString();
 mNewDevicesArrayAdapter.add(noDevices);
 }
 }
 }
 };
}
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Jun 14, 2012 at 19:21
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Things you can do : 1) First follow a fixed convention to name your functions. Ex: CheckBlueToothState should be checkBlueToothState (notice small case) in your main activity 2)BluetoothModel is doing lot of other stuff than just being a model. Things like estabilishing the connections should be moved to a new class probably called BluetoothConnectionManager. SO things like ConnectionManager and all should be moved to this class.

Contract of a class should be precise. Class design is complete only when nothing can be taken out of it, and in BluetoothModel class particularly there are lot of things that can be moved out. Model should not be providing anything above that representing state of an object. Moving code of verifying connection to devices, connecting to devices, disconnetion from devices and else anything related to connection management should be moved to a separate class/module. State of objects should be represented by models and Activity should only be concerned with layout of UI and binding of data coming from utility functions (like ConnectionManager I mentioned) to UI. Activity should be more UI centric.

Once you follow these rules all the redundancy in your code will be removed automatically.

answered Jun 15, 2012 at 2:45
\$\endgroup\$
3
  • \$\begingroup\$ Thank you. I know what you're saying, but I am just confused about Android. My DeviceList activity is where I want devices to be found, so don't i need the bluetooth connection information there? Or are you saying just have an object of a BluetoothConnectionManager which gets passed around all of my Activitys? \$\endgroup\$ Commented Jun 15, 2012 at 12:03
  • \$\begingroup\$ Also, you say ConnectionManager should be moved to the BluetoothConnectionManager class, but I have no object/class named ConnectionManager. Do you just mean everything that entails the connection? If so, then whats the point of my BluetoothModel class? If I move all of the methods for connection into a new class, the Model class will only have getState and setState methods. \$\endgroup\$ Commented Jun 15, 2012 at 12:08
  • \$\begingroup\$ Sorry I meant ConnectThread and not ConnectionManager. BluetoothConnectionManager should be responsible for doing the connections and mantaining the paired devices list using a model (BluetoothModel here). All your activities should ask the data from BluetoothConnectionManager to show any data on UI. Activity is representation of UI and it should be as loosely coupled as possible from doin/maintaining device connections and all \$\endgroup\$ Commented Jun 16, 2012 at 22:16

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.