package net.oschina.gitapp.ui; import java.io.File; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; import java.util.Stack; import net.oschina.gitapp.AppContext; import net.oschina.gitapp.AppException; import net.oschina.gitapp.R; import net.oschina.gitapp.adapter.ProjectCodeTreeListAdapter; import net.oschina.gitapp.api.ApiClient; import net.oschina.gitapp.bean.Branch; import net.oschina.gitapp.bean.CodeTree; import net.oschina.gitapp.bean.CommonList; import net.oschina.gitapp.bean.MoreMenuItem; import net.oschina.gitapp.bean.Project; import net.oschina.gitapp.bean.URLs; import net.oschina.gitapp.common.Contanst; import net.oschina.gitapp.common.DataRequestThreadHandler; import net.oschina.gitapp.common.StringUtils; import net.oschina.gitapp.common.UIHelper; import net.oschina.gitapp.ui.baseactivity.BaseActionBarActivity; import net.oschina.gitapp.util.TypefaceUtils; import net.oschina.gitapp.widget.DropDownMenu; import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.os.Message; import android.os.Vibrator; import android.support.v4.view.MenuItemCompat; import android.text.SpannableString; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.view.Gravity; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; /** * 项目代码列表 * * @created 2014-07-18 * @author 火蚁(http://my.oschina.net/LittleDY) */ public class ProjectCodeActivity extends BaseActionBarActivity implements OnItemClickListener, OnItemLongClickListener, OnClickListener { private final int MORE_MENU_OPENDEFAULT = 00; // 分享 private final int MORE_MENU_OPEN_WITH_BROWS = 01; // 在浏览器中打开 private final int MENU_REFRESH_ID = 0; private static final int ACTION_INIT = 0; // 初始化 private static final int ACTION_REFRESH = 1; // 刷新 private static final int ACTION_LOADING_TREE = 2; // 加载代码层级树 private static final int ACTION_PRE_TREE = 3; // 前一级代码树 private Menu optionsMenu; private Project mProject; private ListView mCodeTree; private LinearLayout mSwitch_branch; private TextView mBranchIcon; private TextView mBranchName; private ProjectCodeTreeListAdapter mAdapter; private Stack<List<CodeTree>> mCodeFolders = new Stack<List<CodeTree>>(); private List<CodeTree> mTrees; private CodeTree mOnCilkCodeTree; private String mPath = ""; private String mBranch; private AppContext mAppContext; private LinearLayout mContentLayout; // 上一级目录 private LinearLayout mCodeTreePreFolder; private TextView mCodeFloders; private ProgressBar mLoading; private ProgressDialog mLoadBranch; private List<Branch> mBranchList = new ArrayList<Branch>(); // 标签和分支列表 private int mBranchIndex = 0; private AlertDialog.Builder dialog; private DataRequestThreadHandler mRequestThreadHandler = new DataRequestThreadHandler(); private DropDownMenu mMoreMenuWindow; private List<MoreMenuItem> mMoreItems = new ArrayList<MoreMenuItem>(); private View.OnClickListener onMoreMenuItemClick = new OnClickListener() { @Override public void onClick(View v) { if (mMoreMenuWindow != null && mMoreMenuWindow.isShowing()) { mMoreMenuWindow.dismiss(); } if (mProject == null) { return; } if (!mProject.isPublic()) { UIHelper.ToastMessage(mAppContext, "私有项目不支持该操作"); return; } int id = v.getId(); switch (id) { case MORE_MENU_OPENDEFAULT: showDetail(mOnCilkCodeTree.getName(), mBranch); break; case MORE_MENU_OPEN_WITH_BROWS: openWithBrowser(mOnCilkCodeTree); break; default: break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.projectcode_fragment); mAppContext = getGitApplication(); Intent intent = getIntent(); if (intent != null) { mProject = (Project) intent.getSerializableExtra(Contanst.PROJECT); mTitle = "代码列表"; mSubTitle = mProject.getOwner().getName() + "/" + mProject.getName(); } mBranch = "master"; initView(); setupListView(); loadDatas("", "master", ACTION_INIT); TypefaceUtils.setOcticons(mBranchIcon); } @Override public void onDestroy() { super.onDestroy(); mRequestThreadHandler.quit(); } @Override public boolean onCreateOptionsMenu(Menu menu) { optionsMenu = menu; // 刷新按钮 MenuItem refreshItem = menu.add(0, MENU_REFRESH_ID, MENU_REFRESH_ID, "刷新"); refreshItem.setIcon(R.drawable.abc_ic_menu_refresh); MenuItemCompat.setShowAsAction(refreshItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS); return true; } private void initMoreOption() { MoreMenuItem openDefault = new MoreMenuItem(MORE_MENU_OPENDEFAULT, R.drawable.more_menu_icon_app, "在该app中打开"); mMoreItems.add(openDefault); MoreMenuItem open_with_brows = new MoreMenuItem(MORE_MENU_OPEN_WITH_BROWS, R.drawable.more_menu_icon_browser, "在浏览器中打开"); mMoreItems.add(open_with_brows); mMoreMenuWindow.addItems(mMoreItems); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); switch (id) { case MENU_REFRESH_ID: loadDatas(mPath, mBranch, ACTION_REFRESH); break; } return super.onOptionsItemSelected(item); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { if (!StringUtils.isEmpty(mPath)) { loadPreFolder(); return true; } } return super.onKeyDown(keyCode, event); } private void initView() { mContentLayout = (LinearLayout) findViewById(R.id.projectcode_content_layout); mCodeTreePreFolder = (LinearLayout) findViewById(R.id.projectcode_tree_prefolder); mCodeFloders = (TextView) findViewById(R.id.projectcode_floders); mCodeFloders.setMovementMethod(LinkMovementMethod.getInstance()); TypefaceUtils.setOcticons((TextView) findViewById(R.id.projectcode_pre_img)); // mCodeTreePreFolder.setOnClickListener(this); mLoading = (ProgressBar) findViewById(R.id.projectcode_loading); mCodeTree = (ListView) findViewById(R.id.projectcode_tree); mSwitch_branch = (LinearLayout) findViewById(R.id.projectcode_switch_branch); mBranchIcon = (TextView) findViewById(R.id.projectcode_branch_icon); mBranchName = (TextView) findViewById(R.id.projectcode_branch_name); } private void setupListView() { mTrees = new ArrayList<CodeTree>(); mAdapter = new ProjectCodeTreeListAdapter(getGitApplication(), mTrees, R.layout.projectcodetree_listitem); mCodeTree.setAdapter(mAdapter); mCodeTree.setOnItemClickListener(this); mCodeTree.setOnItemLongClickListener(this); mSwitch_branch.setOnClickListener(this); } private void loadDatas(final String path, final String ref_name, int action) { mRequestThreadHandler.request(0, new AsyncDataHandler(path, ref_name, action)); } /** * 加载上一级代码树 */ private void loadPreFolder() { mTrees.clear(); mCodeFolders.pop(); mTrees.addAll(mCodeFolders.peek()); mAdapter.notifyDataSetChanged(); savePathAndBranch(getPrePath(), mBranch); } private void beforeLoading(int action) { if (action == ACTION_REFRESH) { mContentLayout.setVisibility(View.GONE); mSwitch_branch.setVisibility(View.GONE); mLoading.setVisibility(View.VISIBLE); } else if (action != ACTION_INIT) { MenuItemCompat.setActionView(optionsMenu.findItem(MENU_REFRESH_ID), R.layout.actionbar_indeterminate_progress); } } private void afterLoading(int action) { if (action == ACTION_INIT || action == ACTION_REFRESH) { mContentLayout.setVisibility(View.VISIBLE); mSwitch_branch.setVisibility(View.VISIBLE); mLoading.setVisibility(View.GONE); } else { MenuItemCompat.setActionView(optionsMenu.findItem(MENU_REFRESH_ID), null); } } /** * 记录路径和分支 * * @param _mPath * @param _mRef_name */ private void savePathAndBranch(String path, String branch) { mPath = path; mBranch = branch; if (StringUtils.isEmpty(mPath)) { mCodeTreePreFolder.setVisibility(View.GONE); } else { mCodeTreePreFolder.setVisibility(View.VISIBLE); } String floders = mProject.getName() + (StringUtils.isEmpty(mPath) ? "" : "/" + mPath); PathString ps = new PathString(floders); mCodeFloders.setText(ps); } // 加载代码树 private class AsyncDataHandler implements DataRequestThreadHandler.AsyncDataHandler<Message> { private String _mPath; private String _mRef_name; private int _mAction; AsyncDataHandler(String path, String ref_name, int action) { _mPath = path; _mRef_name = ref_name; _mAction = action; } // 加载前 @Override public void onPreExecute() { beforeLoading(_mAction); } // 加载ing @Override public Message execute() { Message msg = new Message(); try { boolean refresh = true; if (_mAction == ACTION_INIT) { refresh = false; } CommonList<CodeTree> list = mAppContext.getProjectCodeTree(StringUtils.toInt(mProject.getId()), _mPath, _mRef_name, refresh); List<CodeTree> tree = list.getList(); msg.what = 1; msg.obj = tree; } catch (Exception e) { msg.what = -1; msg.obj = e; } return msg; } // 加载完成 @SuppressWarnings("unchecked") @Override public void onPostExecute(Message result) { afterLoading(_mAction); if (result.what == 1 && result.obj != null) { mTrees.clear(); mTrees.addAll((List<CodeTree>) result.obj); // 加载成功,记录相关信息 if (_mAction == ACTION_LOADING_TREE || _mAction == ACTION_PRE_TREE) { savePathAndBranch(_mPath, _mRef_name); } mAdapter.notifyDataSetChanged(); mCodeFolders.push((List<CodeTree>) result.obj); } else { if (result.obj instanceof AppException) { ((AppException) result.obj).makeToast(mAppContext); } else { UIHelper.ToastMessage(getActivity(), ((Exception) result.obj).getMessage()); } } } } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { position -= mCodeTree.getHeaderViewsCount(); mOnCilkCodeTree = mTrees.get(position); if (mOnCilkCodeTree.getType().equalsIgnoreCase(CodeTree.TYPE_TREE)) { loadDatas(getPath(mOnCilkCodeTree.getName()), mBranch, ACTION_LOADING_TREE); } else { checkShow(mOnCilkCodeTree); } } @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { position -= mCodeTree.getHeaderViewsCount(); // 震动 Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(80); mOnCilkCodeTree = mTrees.get(position); if (mOnCilkCodeTree.getType().equalsIgnoreCase(CodeTree.TYPE_BLOB)) { if (mMoreMenuWindow == null) { mMoreMenuWindow = new DropDownMenu(ProjectCodeActivity.this, onMoreMenuItemClick); initMoreOption(); mMoreMenuWindow.getViewGroup().setPadding(0, 0, 0, 0); } mMoreMenuWindow.showAtLocation(view, Gravity.CENTER, 0, 0); } return true; } // 获得访问的路径 private String getPath(String path) { if (mPath == null || StringUtils.isEmpty(mPath)) { return path; } else { return mPath + "/" + path; } } // 获得上一级路径 private String getPrePath() { int index = mPath.lastIndexOf("/"); return index != -1 ? mPath.substring(0, index) : ""; } /** * 判断code的文件的类型显示不同的操作 * * @param codeTree */ @SuppressWarnings("deprecation") private void checkShow(CodeTree codeTree) { String fileName = codeTree.getName(); if (isCodeTextFile(fileName)) { showDetail(fileName, mBranch); } else if (isImage(fileName)) { String url = URLs.URL_HOST + mProject.getOwner().getUsername() + URLs.URL_SPLITTER + mProject.getPath() + URLs.URL_SPLITTER + "raw" + URLs.URL_SPLITTER + mBranch + URLs.URL_SPLITTER + URLEncoder.encode(getFilePath(fileName)) + "?private_token=" + ApiClient.getToken(mAppContext); UIHelper.showImageZoomActivity(ProjectCodeActivity.this, url); } else { openWithBrowser(codeTree); } } /** * 在浏览器中在打开 * * @param codeTree */ @SuppressWarnings("deprecation") private void openWithBrowser(CodeTree codeTree) { String url = URLs.URL_HOST + mProject.getOwner().getUsername() + URLs.URL_SPLITTER + mProject.getPath() + URLs.URL_SPLITTER + "blob" + URLs.URL_SPLITTER + mBranch + URLs.URL_SPLITTER + URLEncoder.encode(getFilePath(codeTree.getName())) + "?private_token=" + ApiClient.getToken(mAppContext); UIHelper.openBrowser(ProjectCodeActivity.this, url); } // 判断是不是代码文件 private boolean isCodeTextFile(String fileName) { boolean res = false; // 文件的后缀 int index = fileName.lastIndexOf("."); if (index > 0) { fileName = fileName.substring(index); } String codeFileSuffix[] = new String[] { ".java", ".confg", ".ini", ".xml", ".json", ".txt", ".go", ".php", ".php3", ".php4", ".php5", ".js", ".css", ".html", ".properties", ".c", ".hpp", ".h", ".hh", ".cpp", ".cfg", ".rb", ".example", ".gitignore", ".project", ".classpath", ".m", ".md", ".rst", ".vm", ".cl", ".py", ".pl", ".haml", ".erb", ".scss", ".bat", ".coffee", ".as", ".sh", ".m", ".pas", ".cs", ".groovy", ".scala", ".sql", ".bas", ".xml", ".vb", ".xsl", ".swift", ".ftl", ".yml", ".ru", ".jsp", ".markdown", ".cshap", ".apsx", ".sass", ".less", ".ftl", ".haml", ".log", ".tx", ".csproj", ".sln", ".clj", ".scm", ".xhml", ".xaml", ".lua" }; for (String string : codeFileSuffix) { if (fileName.equalsIgnoreCase(string)) { res = true; } } // 特殊的文件 String fileNames[] = new String[] { "LICENSE", "TODO", "README", "readme", "makefile", "gemfile", "gemfile.*", "gemfile.lock", "CHANGELOG" }; for (String string : fileNames) { if (fileName.equalsIgnoreCase(string)) { res = true; } } return res; } // 判断是否是图片 private boolean isImage(String fileName) { boolean res = false; // 图片后缀 int index = fileName.lastIndexOf("."); if (index > 0) { fileName = fileName.substring(index); } String imageSuffix[] = new String[] { ".png", ".jpg", ".jpeg", ".jpe", ".bmp", ".exif", ".dxf", ".wbmp", ".ico", ".jpe", ".gif", ".pcx", ".fpx", ".ufo", ".tiff", ".svg", ".eps", ".ai", ".tga", ".pcd", ".hdri" }; for (String string : imageSuffix) { if (fileName.equalsIgnoreCase(string)) { res = true; } } return res; } // 查看代码文件详情 private void showDetail(String fileName, String ref) { Intent intent = new Intent(getGitApplication(), CodeFileDetailActivity.class); Bundle bundle = new Bundle(); bundle.putSerializable(Contanst.PROJECT, mProject); bundle.putString("fileName", fileName); bundle.putString("path", getFilePath(fileName)); bundle.putString("ref", ref); intent.putExtras(bundle); startActivity(intent); } /** * 获得文件的访问路径 * * @param fileName * @return */ private String getFilePath(String fileName) { return mPath == null || StringUtils.isEmpty(mPath) ? fileName : mPath + "/" + fileName; } @Override public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.projectcode_switch_branch: switchBranch(); break; // case R.id.projectcode_tree_prefolder: // if (StringUtils.isEmpty(mPath)) // { // return; // } // loadPreFolder(); // break; default: break; } } private void switchBranch() { if (mProject == null) { return; } if (dialog == null || mBranchList.isEmpty()) { loadBranchAndTags(true); } else { loadBranchAndTags(false); } } // 加载分支和标签数据 private void loadBranchAndTags(final boolean isRefalsh) { // 异步 new AsyncTask<Void, Void, Message>() { @Override protected Message doInBackground(Void... params) { Message msg = new Message(); try { msg.what = 1; if (!isRefalsh) { return msg; } // 1.加载分支 CommonList<Branch> branchs = mAppContext.getProjectBranchsOrTagsLsit(mProject.getId(), 1, Branch.TYPE_BRANCH, isRefalsh); for (Branch branch : branchs.getList()) { // 设置为分支类型 branch.setType(Branch.TYPE_BRANCH); mBranchList.add(branch); } // 2.加载标签 CommonList<Branch> tags = mAppContext.getProjectBranchsOrTagsLsit(mProject.getId(), 1, Branch.TYPE_TAG, isRefalsh); for (Branch branch : tags.getList()) { // 设置为标签类型 branch.setType(Branch.TYPE_TAG); mBranchList.add(branch); } } catch (Exception e) { msg.what = -1; msg.obj = e; } return msg; } @Override protected void onPreExecute() { if (mLoadBranch == null) { mLoadBranch = new ProgressDialog(getActivity()); mLoadBranch.setCancelable(true); mLoadBranch.setCanceledOnTouchOutside(false); mLoadBranch.setMessage("加载分支和标签..."); mLoadBranch.setProgressStyle(R.style.Spinner); mLoadBranch.show(); } if (dialog == null) { dialog = new AlertDialog.Builder(getActivity()).setTitle("选择分支或标签"); } } @Override protected void onPostExecute(Message msg) { if (mLoadBranch != null) mLoadBranch.dismiss(); if (msg.what == 1) { final String baArrays[] = new String[mBranchList.size()]; for (int i = 0; i < mBranchList.size(); i++) { baArrays[i] = mBranchList.get(i).getName(); if (baArrays[i].equals(mBranch)) { mBranchIndex = i; } } dialog.setSingleChoiceItems(baArrays, mBranchIndex, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); if (which == mBranchIndex) return; mBranchIndex = which; mBranch = baArrays[which]; mPath = ""; loadDatas(mPath, mBranch, ACTION_REFRESH); mBranchName.setText(mBranch); } }).setNegativeButton("取消", null); dialog.show(); } else { if (msg.obj instanceof AppException) { ((AppException) msg.obj).makeToast(getActivity()); } else { UIHelper.ToastMessage(mAppContext, "加载分支和标签失败"); } } } }.execute(); } public void onPathClick(String path, int index) { // loadDatas(path, mBranch, ACTION_LOADING_TREE); // 使用缓存数据 mTrees.clear(); // 移除当前层级之后的数据 for (int i = 0, count = mCodeFolders.size() - index; i < count - 1; i++) { mCodeFolders.pop(); } mTrees.addAll(mCodeFolders.peek()); mAdapter.notifyDataSetChanged(); savePathAndBranch(path, mBranch); } class PathString extends SpannableString { public PathString(String text) { super(text); setup(text); } private void setup(String text) { int start = 0; int chatIndex = text.indexOf(File.separatorChar); int pathStart = chatIndex + 1;// 路径String位置,text最开始为工程名称,不包含在Path内,所以标注开始位置用于截取Path int pathIndex = 0;// 标注层级,用于获取缓存 while (chatIndex >= 0) { String path = chatIndex > pathStart ? text.substring(pathStart, chatIndex) : ""; setSpan(new Clickable(path, pathIndex), start, chatIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); pathIndex++; start = chatIndex + 1; chatIndex = text.indexOf(File.separatorChar, start); } } class Clickable extends ClickableSpan { private final String mPath; private final int mIndex; public Clickable(String path, int index) { mPath = path; mIndex = index; } @Override public void onClick(View widget) { onPathClick(mPath, mIndex); } } } }