package org.fdroid.fdroid.views.swap;
import android.annotation.TargetApi;
import android.bluetooth.BluetoothAdapter;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WifiConfiguration;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.widget.SwitchCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.peers.Peer;
import org.fdroid.fdroid.net.WifiStateChangeService;
import java.util.ArrayList;
import cc.mvdan.accesspoint.WifiApControl;
import rx.Subscriber;
import rx.Subscription;
public class StartSwapView extends RelativeLayout implements SwapWorkflowActivity.InnerView {
private static final String TAG = "StartSwapView";
// TODO: Is there a way to guarantee which of these constructors the inflater will call?
// Especially on different API levels? It would be nice to only have the one which accepts
// a Context, but I'm not sure if that is correct or not. As it stands, this class provides
// constructors which match each of the ones available in the parent class.
// The same is true for the other views in the swap process too.
public StartSwapView(Context context) {
super(context);
}
public StartSwapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(11)
public StartSwapView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(21)
public StartSwapView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
private class PeopleNearbyAdapter extends ArrayAdapter<Peer> {
PeopleNearbyAdapter(Context context) {
super(context, 0, new ArrayList<Peer>());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.swap_peer_list_item, parent, false);
}
Peer peer = getItem(position);
((TextView) convertView.findViewById(R.id.peer_name)).setText(peer.getName());
((ImageView) convertView.findViewById(R.id.icon)).setImageDrawable(getResources().getDrawable(peer.getIcon()));
return convertView;
}
}
private SwapWorkflowActivity getActivity() {
return (SwapWorkflowActivity) getContext();
}
private SwapService getManager() {
return getActivity().getState();
}
@Nullable /* Emulators typically don't have bluetooth adapters */
private final BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
private SwitchCompat wifiSwitch;
private SwitchCompat bluetoothSwitch;
private TextView textWifiVisible;
private TextView viewBluetoothId;
private TextView textBluetoothVisible;
private TextView viewWifiId;
private TextView viewWifiNetwork;
private TextView peopleNearbyText;
private ListView peopleNearbyList;
private ProgressBar peopleNearbyProgress;
private PeopleNearbyAdapter peopleNearbyAdapter;
/**
* When peers are emitted by the peer finder, add them to the adapter
* so that they will show up in the list of peers.
*/
private final Subscriber<Peer> onPeerFound = new Subscriber<Peer>() {
@Override
public void onCompleted() {
uiShowNotSearchingForPeers();
}
@Override
public void onError(Throwable e) {
uiShowNotSearchingForPeers();
}
@Override
public void onNext(Peer peer) {
Utils.debugLog(TAG, "Found peer: " + peer + ", adding to list of peers in UI.");
peopleNearbyAdapter.add(peer);
}
};
private Subscription peerFinderSubscription;
/**
* Remove relevant listeners/subscriptions/etc so that they do not receive and process events
* when this view is not in use.
*
* TODO: Not sure if this is the best place to handle being removed from the view.
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (peerFinderSubscription != null) {
peerFinderSubscription.unsubscribe();
peerFinderSubscription = null;
}
if (wifiSwitch != null) {
wifiSwitch.setOnCheckedChangeListener(null);
}
if (bluetoothSwitch != null) {
bluetoothSwitch.setOnCheckedChangeListener(null);
}
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onWifiSwapStateChanged);
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onBluetoothSwapStateChanged);
LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(onWifiNetworkChanged);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (peerFinderSubscription == null) {
peerFinderSubscription = getManager().scanForPeers().subscribe(onPeerFound);
}
uiInitPeers();
uiInitBluetooth();
uiInitWifi();
uiInitButtons();
uiShowSearchingForPeers();
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
onWifiNetworkChanged, new IntentFilter(WifiStateChangeService.BROADCAST));
}
private final BroadcastReceiver onWifiNetworkChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
uiUpdateWifiNetwork();
}
};
private void uiInitButtons() {
findViewById(R.id.btn_send_fdroid).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
getActivity().sendFDroid();
}
});
findViewById(R.id.btn_qr_scanner).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
getActivity().startQrWorkflow();
}
});
}
/**
* Setup the list of nearby peers with an adapter, and hide or show it and the associated
* message for when no peers are nearby depending on what is happening.
*/
private void uiInitPeers() {
peopleNearbyText = (TextView) findViewById(R.id.text_people_nearby);
peopleNearbyList = (ListView) findViewById(R.id.list_people_nearby);
peopleNearbyProgress = (ProgressBar) findViewById(R.id.searching_people_nearby);
peopleNearbyAdapter = new PeopleNearbyAdapter(getContext());
peopleNearbyList.setAdapter(peopleNearbyAdapter);
peopleNearbyList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Peer peer = peopleNearbyAdapter.getItem(position);
onPeerSelected(peer);
}
});
}
private void uiShowSearchingForPeers() {
peopleNearbyText.setText(getContext().getString(R.string.swap_scanning_for_peers));
peopleNearbyProgress.setVisibility(View.VISIBLE);
}
private void uiShowNotSearchingForPeers() {
peopleNearbyProgress.setVisibility(View.GONE);
if (peopleNearbyList.getAdapter().getCount() > 0) {
peopleNearbyText.setText(getContext().getString(R.string.swap_people_nearby));
} else {
peopleNearbyText.setText(getContext().getString(R.string.swap_no_peers_nearby));
}
}
private void uiInitBluetooth() {
if (bluetooth != null) {
textBluetoothVisible = (TextView) findViewById(R.id.bluetooth_visible);
viewBluetoothId = (TextView) findViewById(R.id.device_id_bluetooth);
viewBluetoothId.setText(bluetooth.getName());
viewBluetoothId.setVisibility(bluetooth.isEnabled() ? View.VISIBLE : View.GONE);
int textResource = getManager().isBluetoothDiscoverable() ? R.string.swap_visible_bluetooth : R.string.swap_not_visible_bluetooth;
textBluetoothVisible.setText(textResource);
bluetoothSwitch = (SwitchCompat) findViewById(R.id.switch_bluetooth);
Utils.debugLog(TAG, getManager().isBluetoothDiscoverable() ? "Initially marking switch as checked, because Bluetooth is discoverable." : "Initially marking switch as not-checked, because Bluetooth is not discoverable.");
bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
setBluetoothSwitchState(getManager().isBluetoothDiscoverable(), true);
LocalBroadcastManager.getInstance(getContext()).registerReceiver(onBluetoothSwapStateChanged, new IntentFilter(SwapService.BLUETOOTH_STATE_CHANGE));
} else {
findViewById(R.id.bluetooth_info).setVisibility(View.GONE);
}
}
/**
* @see StartSwapView#onWifiSwapStateChanged
*/
private final BroadcastReceiver onBluetoothSwapStateChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
Utils.debugLog(TAG, "Bluetooth service is starting (setting toggle to disabled, not checking because we will wait for an intent that bluetooth is actually enabled)");
bluetoothSwitch.setEnabled(false);
textBluetoothVisible.setText(R.string.swap_setting_up_bluetooth);
// bluetoothSwitch.setChecked(true);
} else {
if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
Utils.debugLog(TAG, "Bluetooth service has started (updating text to visible, but not marking as checked).");
textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
bluetoothSwitch.setEnabled(true);
// bluetoothSwitch.setChecked(true);
} else {
Utils.debugLog(TAG, "Bluetooth service has stopped (setting switch to not-visible).");
textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
setBluetoothSwitchState(false, true);
}
}
}
};
/**
* @see StartSwapView#setWifiSwitchState(boolean, boolean)
*/
private void setBluetoothSwitchState(boolean isChecked, boolean isEnabled) {
bluetoothSwitch.setOnCheckedChangeListener(null);
bluetoothSwitch.setChecked(isChecked);
bluetoothSwitch.setEnabled(isEnabled);
bluetoothSwitch.setOnCheckedChangeListener(onBluetoothSwitchToggled);
}
/**
* @see StartSwapView#onWifiSwitchToggled
*/
private final CompoundButton.OnCheckedChangeListener onBluetoothSwitchToggled = new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
Utils.debugLog(TAG, "Received onCheckChanged(true) for Bluetooth swap, prompting user as to whether they want to enable Bluetooth.");
getActivity().startBluetoothSwap();
textBluetoothVisible.setText(R.string.swap_visible_bluetooth);
viewBluetoothId.setVisibility(View.VISIBLE);
Utils.debugLog(TAG, "Received onCheckChanged(true) for Bluetooth swap (prompting user or setup Bluetooth complete)");
// TODO: When they deny the request for enabling bluetooth, we need to disable this switch...
} else {
Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, disabling Bluetooth swap.");
getManager().getBluetoothSwap().stop();
textBluetoothVisible.setText(R.string.swap_not_visible_bluetooth);
viewBluetoothId.setVisibility(View.GONE);
Utils.debugLog(TAG, "Received onCheckChanged(false) for Bluetooth swap, Bluetooth swap disabled successfully.");
}
}
};
private void uiInitWifi() {
viewWifiId = (TextView) findViewById(R.id.device_id_wifi);
viewWifiNetwork = (TextView) findViewById(R.id.wifi_network);
wifiSwitch = (SwitchCompat) findViewById(R.id.switch_wifi);
wifiSwitch.setOnCheckedChangeListener(onWifiSwitchToggled);
setWifiSwitchState(getManager().isBonjourDiscoverable(), true);
textWifiVisible = (TextView) findViewById(R.id.wifi_visible);
int textResource = getManager().isBonjourDiscoverable() ? R.string.swap_visible_wifi : R.string.swap_not_visible_wifi;
textWifiVisible.setText(textResource);
// Note that this is only listening for the WifiSwap, whereas we start both the WifiSwap
// and the Bonjour service at the same time. Technically swap will work fine without
// Bonjour, and that is more of a convenience. Thus, we should show feedback once wifi
// is ready, even if Bonjour is not yet.
LocalBroadcastManager.getInstance(getContext()).registerReceiver(onWifiSwapStateChanged, new IntentFilter(SwapService.WIFI_STATE_CHANGE));
viewWifiNetwork.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
getActivity().promptToSelectWifiNetwork();
}
});
uiUpdateWifiNetwork();
}
/**
* When the WiFi swap service is started or stopped, update the UI appropriately.
* This includes both the in-transit states of "Starting" and "Stopping". In these two cases,
* the UI should be disabled to prevent the user quickly switching back and forth - causing
* multiple start/stop actions to be sent to the swap service.
*/
private final BroadcastReceiver onWifiSwapStateChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.hasExtra(SwapService.EXTRA_STARTING)) {
Utils.debugLog(TAG, "WiFi service is starting (setting toggle to checked, but disabled).");
textWifiVisible.setText(R.string.swap_setting_up_wifi);
setWifiSwitchState(true, false);
} else if (intent.hasExtra(SwapService.EXTRA_STOPPING)) {
Utils.debugLog(TAG, "WiFi service is stopping (setting toggle to unchecked and disabled).");
textWifiVisible.setText(R.string.swap_stopping_wifi);
setWifiSwitchState(false, false);
} else {
if (intent.hasExtra(SwapService.EXTRA_STARTED)) {
Utils.debugLog(TAG, "WiFi service has started (setting toggle to visible).");
textWifiVisible.setText(R.string.swap_visible_wifi);
setWifiSwitchState(true, true);
} else {
Utils.debugLog(TAG, "WiFi service has stopped (setting toggle to not-visible).");
textWifiVisible.setText(R.string.swap_not_visible_wifi);
setWifiSwitchState(false, true);
}
}
uiUpdateWifiNetwork();
}
};
/**
* Helper function to set the "enable wifi" switch, but prevents the listeners from
* being notified. This enables the UI to be updated without triggering further enable/disable
* events being queued.
*
* This is required because the SwitchCompat and its parent classes will always try to notify
* their listeners if there is one (e.g. http://stackoverflow.com/a/15523518).
*
* The fact that this method also deals with enabling/disabling the switch is more of a convenience
* Nigh on all times this UI wants to change the state of the switch, it is also interested in
* ensuring the enabled state of the switch.
*/
private void setWifiSwitchState(boolean isChecked, boolean isEnabled) {
wifiSwitch.setOnCheckedChangeListener(null);
wifiSwitch.setChecked(isChecked);
wifiSwitch.setEnabled(isEnabled);
wifiSwitch.setOnCheckedChangeListener(onWifiSwitchToggled);
}
/**
* When the wifi switch is:
*
* Toggled on: Ask the swap service to ensure wifi swap is running.
* Toggled off: Ask the swap service to prevent the wifi swap service from running.
*
* Both of these actions will be performed in a background thread which will send broadcast
* intents when they are completed.
*/
private final CompoundButton.OnCheckedChangeListener onWifiSwitchToggled = new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
Utils.debugLog(TAG, "Received onCheckChanged(true) for WiFi swap, asking in background thread to ensure WiFi swap is running.");
getManager().getWifiSwap().ensureRunningInBackground();
} else {
Utils.debugLog(TAG, "Received onCheckChanged(false) for WiFi swap, disabling WiFi swap in background thread.");
getManager().getWifiSwap().stopInBackground();
}
uiUpdateWifiNetwork();
}
};
private void uiUpdateWifiNetwork() {
viewWifiId.setText(FDroidApp.ipAddressString);
viewWifiId.setVisibility(TextUtils.isEmpty(FDroidApp.ipAddressString) ? View.GONE : View.VISIBLE);
WifiApControl wifiAp = WifiApControl.getInstance(getActivity());
if (wifiAp != null && wifiAp.isWifiApEnabled()) {
WifiConfiguration config = wifiAp.getConfiguration();
viewWifiNetwork.setText(getContext().getString(R.string.swap_active_hotspot, config.SSID));
} else if (TextUtils.isEmpty(FDroidApp.ssid)) {
// not connected to or setup with any wifi network
viewWifiNetwork.setText(R.string.swap_no_wifi_network);
} else {
// connected to a regular wifi network
viewWifiNetwork.setText(FDroidApp.ssid);
}
}
private void onPeerSelected(Peer peer) {
getActivity().swapWith(peer);
}
@Override
public boolean buildMenu(Menu menu, @NonNull MenuInflater inflater) {
return false;
}
@Override
public int getStep() {
return SwapService.STEP_INTRO;
}
@Override
public int getPreviousStep() {
// TODO: Currently this is handleed by the SwapWorkflowActivity as a special case, where
// if getStep is STEP_INTRO, don't even bother asking for getPreviousStep. But that is a
// bit messy. It would be nicer if this was handled using the same mechanism as everything
// else.
return SwapService.STEP_INTRO;
}
@Override
@ColorRes
public int getToolbarColour() {
return R.color.swap_bright_blue;
}
@Override
public String getToolbarTitle() {
return getResources().getString(R.string.swap_nearby);
}
}