package eu.hgross.blaubot.android.views; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; import android.os.Handler; import android.os.Looper; import android.text.Editable; import android.text.Html; import android.text.Spanned; import android.text.TextWatcher; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.SeekBar; import android.widget.Spinner; import android.widget.TextView; import android.widget.ToggleButton; import java.util.Arrays; import java.util.List; import java.util.concurrent.ConcurrentSkipListSet; import eu.hgross.blaubot.android.R; import eu.hgross.blaubot.messaging.BlaubotChannel; import eu.hgross.blaubot.messaging.BlaubotChannelConfig; import eu.hgross.blaubot.messaging.ChannelInfo; import eu.hgross.blaubot.messaging.IBlaubotChannel; import eu.hgross.blaubot.util.Log; /** * Represents a Channel that has to be injected via setChannel(ChannelInfo) * * @author Henning Gross {@literal (mail.to@henning-gross.de)} */ public class ChannelView extends FrameLayout { private static final String LOG_TAG = "ChannelView"; private Handler mUiHandler; private View mMainView; private ChannelInfo mChannelInfo; private ToggleButton mSubscribeButton; private Button mEditChannelConfigButton; private boolean mShowEditButton = true; private Button mShowSubscriberButton; public ChannelView(Context context, AttributeSet attrs) { super(context, attrs); initView(context, attrs); } public ChannelView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initView(context, attrs); } private void initView(Context context, AttributeSet attrs) { if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.ChannelView, 0, 0); try { mShowEditButton = a.getBoolean(R.styleable.ChannelView_showEditButton, mShowEditButton); } finally { a.recycle(); } } this.mMainView = inflate(getContext(), R.layout.blaubot_channel_view, null); mUiHandler = new Handler(Looper.getMainLooper()); addView(this.mMainView); this.mEditChannelConfigButton = (Button) this.mMainView.findViewById(R.id.editChannelConfigButton); this.mSubscribeButton = (ToggleButton) this.mMainView.findViewById(R.id.channelInfoSubscribeButton); this.mSubscribeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { final boolean subscriberToChannel = mChannelInfo.isOwnDeviceSubscriberToChannel(); final BlaubotChannel channel = mChannelInfo.getChannel(); if (subscriberToChannel) { channel.unsubscribe(); } else { channel.subscribe(); } setEnabled(false); } }); this.mShowSubscriberButton = (Button) this.mMainView.findViewById(R.id.showSubscribersButton); this.mEditChannelConfigButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mChannelInfo == null) { if (Log.logWarningMessages()) { Log.w(LOG_TAG, "The edit channel config button was pressed but i have no channel info object."); } return; } final Dialog channelEditDialog = createChannelEditDialog(getContext(), mChannelInfo.getChannel()); if (Log.logDebugMessages()) { Log.d(LOG_TAG, "Showing edit channel config dialog."); } channelEditDialog.show(); } }); } private void updateUI() { // append the views mUiHandler.post(new Runnable() { @Override public void run() { final boolean subscriberToChannel = mChannelInfo.isOwnDeviceSubscriberToChannel(); final short channelId = mChannelInfo.getChannelConfig().getChannelId(); final int queueCapacity = mChannelInfo.getQueueCapacity(); final int queueSize = mChannelInfo.getQueueSize(); final long receivedBytes = mChannelInfo.getReceivedBytes(); final long receivedMessages = mChannelInfo.getReceivedMessages(); final long sentBytes = mChannelInfo.getSentBytes(); final long sentMessages = mChannelInfo.getSentMessages(); final ConcurrentSkipListSet<String> subscriptions = mChannelInfo.getSubscriptions(); final String htmlStr = "<br><b>Channel #" + channelId + "</b><br>" + "<span style=\"width:100px\">MessageQueue:</span> " + queueSize + "/" + queueCapacity + "<br>" + "Subscriptions: \t" + subscriptions.size() + "<br>" + "Rx/Tx bytes: \t" + ViewUtils.humanReadableByteCount(receivedBytes, false) + "/" + ViewUtils.humanReadableByteCount(sentBytes, false) + "<br>" + "Rx/Tx messages: \t" + receivedMessages + "/" + sentMessages + "<br>" + ""; final Spanned html = Html.fromHtml(htmlStr); mSubscribeButton.setTextOff(html); mSubscribeButton.setTextOn(html); mSubscribeButton.setChecked(subscriberToChannel); mSubscribeButton.setEnabled(true); mShowSubscriberButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // custom view SubscribersView subscribersView = new SubscribersView(getContext()); subscribersView.setPingMeasureResult(mChannelInfo.getSubscriptions()); // show dialog AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); builder.setView(subscribersView); builder.setTitle("Subscribers of channel #" + channelId); builder.setPositiveButton("close", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }); final AlertDialog alertDialog = builder.create(); alertDialog.show(); } }); mEditChannelConfigButton.setVisibility(mShowEditButton ? VISIBLE : GONE); } }); } /** * Sets the channel to be displayed * * @param channelInfo the channel info model that is to be rendered */ public void setChannelInfo(ChannelInfo channelInfo) { this.mChannelInfo = channelInfo; updateUI(); } /** * Creates a dialog which lets a user change the properties of the channel's config. * * @param context the current android context * @param blaubotChannel the blaubot channel to edit * @return the dialog */ public static Dialog createChannelEditDialog(Context context, final IBlaubotChannel blaubotChannel) { /* TODO: NTH: put this stuff into a own view and use the observer functionality of BlaubotChannelConfig to update on external changes */ final int MAX_MESSAGE_RATE = 10000; final View view = inflate(context, R.layout.blaubot_channel_config_edit_view, null); final SeekBar messageRateSeekBar = (SeekBar) view.findViewById(R.id.messageRateSeekBar); final TextView messageRateSeekBarValueTextView = (TextView) view.findViewById(R.id.messageRateSeekBarValueTextView); final Spinner pickingStrategySpinner = (Spinner) view.findViewById(R.id.pickingStrategySpinner); final EditText messageQueueCapacityEditText = (EditText) view.findViewById(R.id.messageQueueCapacityEditText); final ToggleButton transmitReflexiveMessagesToggleButton = (ToggleButton) view.findViewById(R.id.transmitReflexivChannelMessagesToggleButton); final ToggleButton transmitIfNoSubscribersToggleButton = (ToggleButton) view.findViewById(R.id.transmitIfNoSubscribersToggleButton); /* SeekBar stuff */ messageRateSeekBar.setMax(MAX_MESSAGE_RATE); messageRateSeekBar.setProgress(blaubotChannel.getChannelConfig().getMinMessageRateDelay()); messageRateSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { private int progress = messageRateSeekBar.getProgress(); @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { this.progress = progress; updateMessageRateSeekbarTextView(progress); } private void updateMessageRateSeekbarTextView(int progress) { messageRateSeekBarValueTextView.setText(progress + " ms"); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { blaubotChannel.getChannelConfig().setMessageRateLimit(progress); } }); messageRateSeekBarValueTextView.setText(messageRateSeekBar.getProgress() + " ms"); /* Picker strategy stuff */ List<BlaubotChannelConfig.MessagePickerStrategy> pickerStrategies = Arrays.asList(BlaubotChannelConfig.MessagePickerStrategy.values()); final PickerStrategyArrayAdapter pickerStrategyArrayAdapter = new PickerStrategyArrayAdapter(context, pickerStrategies); pickingStrategySpinner.setAdapter(pickerStrategyArrayAdapter); pickingStrategySpinner.setSelection(pickerStrategies.indexOf(blaubotChannel.getChannelConfig().getPickerStrategy())); pickingStrategySpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { BlaubotChannelConfig.MessagePickerStrategy strategy = pickerStrategyArrayAdapter.getItem(position); blaubotChannel.getChannelConfig().setMessagePickerStrategy(strategy); } @Override public void onNothingSelected(AdapterView<?> parent) { } }); /* Queue capacity stuff */ messageQueueCapacityEditText.setText(blaubotChannel.getChannelConfig().getQueueCapacity() + ""); messageQueueCapacityEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { // validate Integer number = null; try { number = Integer.parseInt(messageQueueCapacityEditText.getText().toString()); } catch (NumberFormatException e) { } if (number == null) { messageQueueCapacityEditText.setError("Not a number"); return; } else if (number <= 0) { messageQueueCapacityEditText.setError("Has to be greater than zero."); return; } messageQueueCapacityEditText.setError(null); // set blaubotChannel.getChannelConfig().setQueueCapacity(number); } }); // reflexive message transmission transmitReflexiveMessagesToggleButton.setChecked(blaubotChannel.getChannelConfig().isTransmitReflexiveMessages()); transmitReflexiveMessagesToggleButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { final boolean currentState = blaubotChannel.getChannelConfig().isTransmitReflexiveMessages(); blaubotChannel.getChannelConfig().setTransmitReflexiveMessages(!currentState); } }); transmitIfNoSubscribersToggleButton.setChecked(blaubotChannel.getChannelConfig().isTransmitIfNoSubscribers()); transmitIfNoSubscribersToggleButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { final boolean transmitIfNoSubscribers = blaubotChannel.getChannelConfig().isTransmitIfNoSubscribers(); blaubotChannel.getChannelConfig().setTransmitIfNoSubscribers(!transmitIfNoSubscribers); } }); // build the dialog AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setView(view) .setPositiveButton("Close", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .setTitle("Channel configuration"); return builder.create(); } private static class PickerStrategyArrayAdapter extends ArrayAdapter<BlaubotChannelConfig.MessagePickerStrategy> { public PickerStrategyArrayAdapter(Context context, List<BlaubotChannelConfig.MessagePickerStrategy> strategies) { super(context, R.layout.blaubot_picker_strategy_list_item, R.id.name, strategies); } @Override public View getDropDownView(int position, View convertView, ViewGroup parent) { return getCustomView(position, convertView, parent); } @Override public View getView(int position, View convertView, ViewGroup parent) { return getCustomView(position, convertView, parent); } private View getCustomView(int position, View convertView, ViewGroup parent) { final BlaubotChannelConfig.MessagePickerStrategy strategy = getItem(position); final View view = convertView != null ? convertView : View.inflate(this.getContext(), R.layout.blaubot_picker_strategy_list_item, null); final TextView nameTextView = (TextView) view.findViewById(R.id.name); final TextView descriptionTextView = (TextView) view.findViewById(R.id.description); // ImageView iconImageView = (ImageView) view.findViewById(R.id.icon); // iconImageView.setImageDrawable(getResources().getDrawable(R.drawable.ic_merge)); nameTextView.setText(strategy.name() + ""); descriptionTextView.setText(getDescriptionForPickerStrategy(strategy)); return view; } } /** * Returns a string describing the given strategy * * @param strategy the strategy * @return a description for the given strategy */ public static Spanned getDescriptionForPickerStrategy(BlaubotChannelConfig.MessagePickerStrategy strategy) { if (strategy.equals(BlaubotChannelConfig.MessagePickerStrategy.PROCESS_ALL)) { return Html.fromHtml("<html>All messages added to the message queue are processed sequentially.</html>"); } else if (strategy.equals(BlaubotChannelConfig.MessagePickerStrategy.DISCARD_NEW)) { return Html.fromHtml("<html>On each pick, only the <b>oldest</b> message is picked from the message queue and all newer messages will be discarded.</html>"); } else if (strategy.equals(BlaubotChannelConfig.MessagePickerStrategy.DISCARD_OLD)) { return Html.fromHtml("<html>On each pick, only the <b>newest</b> message is picked from the message queue and all older messages will be discarded.</html>"); } else { throw new RuntimeException("unknown strategy"); } } }