package com.kedzie.vbox.machine.group;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import android.content.ClipData;
import android.content.ClipData.Item;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import android.view.DragEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.ViewFlipper;
import com.kedzie.vbox.R;
import com.kedzie.vbox.api.IHost;
import com.kedzie.vbox.api.IMachine;
import com.kedzie.vbox.api.ISession;
import com.kedzie.vbox.api.jaxb.LockType;
import com.kedzie.vbox.api.jaxb.SessionState;
import com.kedzie.vbox.app.Utils;
import com.kedzie.vbox.host.HostView;
import com.kedzie.vbox.machine.MachineView;
import com.kedzie.vbox.machine.group.VMGroupPanel.OnDrillDownListener;
import com.kedzie.vbox.soap.VBoxSvc;
/**
* Scrollable list of {@link VMGroup} objects with drill-down support to focus on a particular group.
*
* @author Marek Kędzierski
*/
public class VMGroupListView extends ViewFlipper implements OnClickListener, OnLongClickListener, OnDrillDownListener {
private static final String TAG = "VMGroupListView";
/**
* Callback for element selection
*/
public static interface OnTreeNodeSelectListener {
/**
* An element has been selected
* @param node the selected element
*/
public void onTreeNodeSelect(TreeNode node);
}
static final int DRAG_ACCEPT_COLOR = Color.GREEN;
static final int VIEW_BACKGROUND = android.R.color.background_dark;
/** Currently selected view */
private View _selected;
/** Is element selection enabled */
private boolean mSelectionEnabled;
private OnTreeNodeSelectListener _listener;
/** Maps Machine ID to all views which reference it. Used for updating views when events are received. */
private Map<String, List<MachineView>> mMachineViewMap = new HashMap<String, List<MachineView>>();
/** Maps {@link VMGroup} to all views which reference it. Used for updating views when groups change. */
private Map<String, List<VMGroupPanel>> mGroupViewMap = new HashMap<String, List<VMGroupPanel>>();
private HostView mHostView;
/** Cache of {@link VMGroup}s */
private Map<String, VMGroup> mGroupCache = new HashMap<String, VMGroup>();
private Animation mSlideInLeft = AnimationUtils.loadAnimation(getContext(), R.anim.slide_in_left);
private Animation mSlideInRight = AnimationUtils.loadAnimation(getContext(), R.anim.slide_in_right);
private Animation mSlideOutLeft = AnimationUtils.loadAnimation(getContext(), R.anim.slide_out_left);
private Animation mSlideOutRight = AnimationUtils.loadAnimation(getContext(), R.anim.slide_out_right);
private Dragger mDragger;
private VMGroup mDraggedGroup;
private IMachine mDraggedMachine;
private IHost mHost;
private VBoxSvc _vmgr;
public VMGroupListView(Context context, VBoxSvc vmgr) {
super(context);
_vmgr=vmgr;
if(Utils.isVersion(Build.VERSION_CODES.HONEYCOMB))
mDragger = new Dragger();
}
public void setRoot(VMGroup group, IHost host) {
mHost = host;
mGroupCache.put(group.getName(), group);
addView(new GroupSection(getContext(), group));
}
@Override
public void onDrillDown(VMGroup group) {
addView(new GroupSection(getContext(), group));
setInAnimation(mSlideInRight);
setOutAnimation(mSlideOutLeft);
showNext();
}
public void drillOut() {
setInAnimation(mSlideInLeft);
setOutAnimation(mSlideOutRight);
showPrevious();
removeViewAt(getChildCount()-1);
}
public void setOnTreeNodeSelectListener(OnTreeNodeSelectListener listener) {
_listener = listener;
}
public boolean isSelectionEnabled() {
return mSelectionEnabled;
}
public void setSelectionEnabled(boolean selectionEnabled) {
mSelectionEnabled = selectionEnabled;
}
public TreeNode getSelectedObject() {
if(_selected instanceof VMGroupPanel) {
return ((VMGroupPanel)_selected).getGroup();
} else if(_selected instanceof MachineView) {
return ((MachineView)_selected).getMachine();
} else if(_selected instanceof HostView) {
return ((HostView)_selected).getHost();
}
return null;
}
public void setSelectedObject(TreeNode object) {
if(object instanceof IHost) {
} else if(object instanceof IMachine) {
} else if(object instanceof VMGroup) {
}
}
/**
* Build a scrollable list of everything below a group
*/
private class GroupSection extends LinearLayout {
private VMGroup mGroup;
private LinearLayout mContents;
public GroupSection(Context context, VMGroup group) {
super(context);
mGroup = group;
setOrientation(LinearLayout.VERTICAL);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
mContents = new LinearLayout(getContext());
mContents.setOrientation(LinearLayout.VERTICAL);
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
if(!mGroup.getName().equals("")) {
LinearLayout header = (LinearLayout)LayoutInflater.from(getContext()).inflate(R.layout.vmgroup_list_header, null);
((ImageView)header.findViewById(R.id.group_back)).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
drillOut();
}
});
Utils.setTextView(header, R.id.group_title, group.getName());
Utils.setTextView(header, R.id.group_num_groups, group.getNumGroups());
Utils.setTextView(header, R.id.group_num_machine, group.getNumMachines());
lp.bottomMargin = Utils.dpiToPx(getContext(), 4);
super.addView(header, lp);
} else {
//add mHostViewost view
mHostView = new HostView(getContext());
mHostView.update(mHost);
mHostView.setBackgroundResource(R.drawable.list_selector_color);
mHostView.setClickable(true);
mHostView.setOnClickListener(VMGroupListView.this);
mContents.addView(mHostView);
}
for(TreeNode child : group.getChildren())
mContents.addView(createView(child), lp);
ScrollView scrollView = new ScrollView(getContext());
scrollView.addView(mContents);
super.addView(scrollView);
if(Utils.isVersion(Build.VERSION_CODES.HONEYCOMB))
setOnDragListener(mDragger);
}
@Override
public void addView(View child, android.view.ViewGroup.LayoutParams params) {
mContents.addView(child, params);
}
/**
* Create a view for a single node in the tree
* @param node tree node
* @return Fully populated view representing the node
*/
public View createView(TreeNode node) {
if(node instanceof IMachine) {
MachineView view = new MachineView(getContext());
IMachine m = (IMachine)node;
view.update(m);
view.setBackgroundResource(R.drawable.list_selector_color);
view.setClickable(true);
view.setOnClickListener(VMGroupListView.this);
view.setOnLongClickListener(VMGroupListView.this);
if(!mMachineViewMap.containsKey(m.getIdRef()))
mMachineViewMap.put(m.getIdRef(), new ArrayList<MachineView>());
mMachineViewMap.get(m.getIdRef()).add(view);
return view;
} else if (node instanceof VMGroup) {
VMGroup group = (VMGroup)node;
mGroupCache.put(group.getName(), group);
VMGroupPanel groupView = new VMGroupPanel(getContext(), group);
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
groupView.setOnClickListener(VMGroupListView.this);
groupView.setOnDrillDownListener(VMGroupListView.this);
groupView.setOnLongClickListener(VMGroupListView.this);
for(TreeNode child : group.getChildren())
groupView.addChild(createView(child));
if(!mGroupViewMap.containsKey(group.getName()))
mGroupViewMap.put(group.getName(), new ArrayList<VMGroupPanel>());
mGroupViewMap.get(group.getName()).add(groupView);
groupView.setBackgroundColor(VIEW_BACKGROUND);
return groupView;
}
throw new IllegalArgumentException("Only views of type MachineView or VMGroupView are allowed");
}
public VMGroup getGroup() {
return mGroup;
}
public List<VMGroupPanel> getNodeViews() {
List<VMGroupPanel> children = new ArrayList<VMGroupPanel>(mContents.getChildCount());
for(int i=0; i<mContents.getChildCount(); i++)
if(mContents.getChildAt(i) instanceof VMGroupPanel)
children.add((VMGroupPanel)mContents.getChildAt(i));
return children;
}
}
/**
* Update all machine views with new data
* @param machine the machine to update (properties must be cached)
*/
public void update(IMachine machine) {
for(MachineView view : mMachineViewMap.get(machine.getIdRef()))
view.update(machine);
}
@Override
public void onClick(View v) {
if(_listener==null)
return;
if(!mSelectionEnabled) {
notifyListener(v);
return;
}
//Deselect existing selection
if(_selected==v) {
_selected.setSelected(false);
_selected=null;
_listener.onTreeNodeSelect(null);
return;
}
//Make new Selection
if(_selected!=null)
_selected.setSelected(false);
_selected=v;
_selected.setSelected(true);
notifyListener(_selected);
}
private void notifyListener(View v) {
if(v instanceof MachineView)
_listener.onTreeNodeSelect(((MachineView)v).getMachine());
else if(v instanceof VMGroupPanel)
_listener.onTreeNodeSelect(((VMGroupPanel)v).getGroup());
else if(v instanceof HostView)
_listener.onTreeNodeSelect(((HostView)v).getHost());
}
@Override
public boolean onLongClick(View view) {
if(!Utils.isVersion(Build.VERSION_CODES.HONEYCOMB))
return true;
if(view instanceof MachineView)
new DragMachineTask().execute((MachineView)view);
else if(view instanceof VMGroupPanel)
new DragGroupTask().execute((VMGroupPanel)view);
return true;
}
private class DragMachineTask extends AsyncTask<MachineView, Void, IMachine> {
private MachineView mView;
@Override
protected IMachine doInBackground(MachineView... params) {
mView = params[0];
IMachine machine = mView.getMachine();
if(machine.getSessionState().equals(SessionState.UNLOCKED)) {
return machine;
}
return null;
}
@Override
protected void onPostExecute(IMachine result) {
super.onPostExecute(result);
if(result!=null) {
mDraggedMachine=result;
ClipData data = new ClipData("VM", new String[] {"vbox/machine"}, new Item(result.getIdRef()));
mView.startDrag(data, new DragShadowBuilder(mView), null, 0);
}
}
}
private class DragGroupTask extends AsyncTask<VMGroupPanel, Void, VMGroup> {
private VMGroupPanel mView;
@Override
protected VMGroup doInBackground(VMGroupPanel... params) {
mView = params[0];
VMGroup group = mView.getGroup();
if(!hasLockedMachines(group))
return group;
return null;
}
private boolean hasLockedMachines(VMGroup group) {
boolean locked = false;
for(TreeNode child : group.getChildren()) {
if(child instanceof IMachine) {
IMachine machine = (IMachine)child;
locked |= !machine.getSessionState().equals(SessionState.UNLOCKED);
} else {
VMGroup g = (VMGroup)child;
locked |= hasLockedMachines(g);
}
}
return locked;
}
@Override
protected void onPostExecute(VMGroup result) {
super.onPostExecute(result);
if(result!=null) {
mDraggedGroup = result;
ClipData data = new ClipData(result.getName(), new String[] {"vbox/group"}, new Item(result.getName()));
mView.startDrag(data, new DragShadowBuilder(mView.getTitleView()), null, 0);
}
}
}
private class Dragger implements OnDragListener {
private VMGroupPanel mGroupView;
private GroupSection mSectionView;
private VMGroup mParentGroup;
private List<VMGroupPanel> mNewParentViews;
@Override
public boolean onDrag(View view, DragEvent event) {
mSectionView = (GroupSection)view;
final int action = event.getAction();
switch(action) {
case DragEvent.ACTION_DRAG_STARTED:
return true;
case DragEvent.ACTION_DRAG_ENTERED:
mSectionView.setBackgroundColor(DRAG_ACCEPT_COLOR);
mSectionView.invalidate();
return true;
case DragEvent.ACTION_DRAG_LOCATION:
VMGroupPanel current = Utils.getDeepestView(mSectionView,
new Point((int)event.getX(), (int)event.getY())
, VMGroupPanel.class);
if(mGroupView!=null && current!=mGroupView) { //exited group panel
Log.d(TAG, "Exited " + mGroupView.getGroup());
mGroupView.setBackgroundColor(VIEW_BACKGROUND);
mGroupView.invalidate();
}
if(current!=null && current!=mGroupView) { //entered group panel
Log.d(TAG, "Entered " + current.getGroup());
mParentGroup = current.getGroup();
if(doAcceptDragEnter()) {
current.setBackgroundColor(DRAG_ACCEPT_COLOR);
current.invalidate();
}
}
if(current==null && mGroupView!=null) { //entered root group
Log.v(TAG, "Entered Root");
mParentGroup = mSectionView.getGroup();
if(doAcceptDragEnter()) {
mSectionView.setBackgroundColor(DRAG_ACCEPT_COLOR);
mSectionView.invalidate();
}
} else if(current!=null && mGroupView==null) { //exited root group
Log.v(TAG, "Exited Root");
mSectionView.setBackgroundColor(VIEW_BACKGROUND);
mSectionView.invalidate();
}
mGroupView = current;
return true;
case DragEvent.ACTION_DRAG_EXITED:
mParentGroup = null;
view.setBackgroundColor(VIEW_BACKGROUND);
view.invalidate();
return true;
case DragEvent.ACTION_DROP:
if(!doAcceptDragEnter())
return false;
mNewParentViews = mGroupViewMap.get(mParentGroup.getName());
if(mDraggedMachine!=null)
dropMachine(mParentGroup, mDraggedMachine);
else if(mDraggedGroup!=null)
dropGroup(mParentGroup, mDraggedGroup);
return true;
case DragEvent.ACTION_DRAG_ENDED:
if(mGroupView!=null) {
mGroupView.setBackgroundColor(VIEW_BACKGROUND);
mGroupView.invalidate();
}
view.setBackgroundColor(VIEW_BACKGROUND);
view.invalidate();
mGroupView = null;
mDraggedGroup=null;
mDraggedMachine=null;
mParentGroup=null;
return true;
}
return false;
}
private boolean doAcceptDragEnter() {
Log.d(TAG, "doAccept? parent:" + mParentGroup.getName());
if(mDraggedMachine!=null) {
if(mDraggedMachine.getGroups().get(0).equals(mParentGroup.getName()))
return false;
} else if(mDraggedGroup!=null) {
if(mDraggedGroup.equals(mParentGroup))
return false;
String oldParentName = mDraggedGroup.getName().substring(0, mDraggedGroup.getName().lastIndexOf('/'));
if(oldParentName.equals(mParentGroup.getName()))
return false;
}
return true;
}
private void dropMachine(VMGroup parent, IMachine child) {
List<MachineView> machineViews = mMachineViewMap.get(mDraggedMachine.getIdRef());
//move the views
for(MachineView mv : machineViews)
((ViewGroup)mv.getParent()).removeView(mv);
for(int i=0; i<machineViews.size(); i++) {
MachineView mv = machineViews.get(i);
if(i<mNewParentViews.size()) //in case we are dragging to root group, there are less group panels than machine views
mNewParentViews.get(i).addView(mv, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
else if(mSectionView!=null)
mSectionView.addView(mv, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
//update the data
VMGroup oldParent = mGroupCache.get(mDraggedMachine.getGroups().get(0));
oldParent.removeChild(mDraggedMachine);
mParentGroup.addChild(mDraggedMachine);
_vmgr.getExecutor().execute(new Runnable() {
@Override
public void run() {
try {
ISession session = _vmgr.getVBox().getSessionObject();
mDraggedMachine.lockMachine(session, LockType.WRITE);
IMachine mutable = session.getMachine();
mutable.setGroups(mParentGroup.getName());
mutable.saveSettings();
session.unlockMachine();
} catch (IOException e) {
Log.e(TAG, "Error", e);
}
}
});
}
private void dropGroup(VMGroup parent, VMGroup child) {
String oldParentName = mDraggedGroup.getName().substring(0, mDraggedGroup.getName().lastIndexOf('/'));
//move the views
List<VMGroupPanel> groupViews = mGroupViewMap.get(mDraggedGroup.getName());
for(VMGroupPanel gv : groupViews)
((ViewGroup)gv.getParent()).removeView(gv);
for(int i=0; i<groupViews.size(); i++) {
VMGroupPanel gv = groupViews.get(i);
if(i<mNewParentViews.size()) //in case we are dragging to root group, there are less parent group panels than child views
mNewParentViews.get(i).addView(gv, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
else if(mSectionView!=null)
mSectionView.addView(gv, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
//update the data
final VMGroup dragged = mGroupCache.get(mDraggedGroup.getName());
VMGroup oldParent = mGroupCache.get(oldParentName);
Log.d(TAG, String.format("Dropping group %1$s --> %2$s", dragged, mParentGroup));
Log.d(TAG, "Old Parent: " + oldParentName);
oldParent.removeChild(dragged);
_vmgr.getExecutor().execute(new Runnable() {
@Override
public void run() {
try {
moveGroup(_vmgr.getVBox().getSessionObject(), mParentGroup, dragged);
} catch(IOException e) {
Log.e(TAG, "Exception moving group", e);
}
}
});
}
private void moveGroup(ISession session, VMGroup parent, VMGroup group) throws IOException {
Log.d(TAG, "Fixing groups: " + group);
String oldName = group.getName();
group.setName(parent.getName() + "/" + group.getSimpleGroupName() );
//update the group cache
mGroupCache.remove(oldName);
mGroupCache.put(group.getName(), group);
Log.d(TAG, String.format("Changed group name %1$s --> %2$s", oldName, group.getName()));
for(TreeNode c : group.getChildren()) {
if(c instanceof IMachine) {
IMachine child = (IMachine)c;
Log.d(TAG, "Processing: " + child.getName());
child.lockMachine(session, LockType.WRITE);
IMachine mutable = session.getMachine();
mutable.setGroups(group.getName());
mutable.saveSettings();
session.unlockMachine();
} else {
VMGroup child = (VMGroup)c;
moveGroup(session, group, child);
}
}
}
}
}