/******************************************************************************* * This file is part of Zandy. * * Zandy is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Zandy is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Zandy. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package com.gimranov.zandy.app; import java.util.ArrayList; import android.app.AlertDialog; import android.app.Dialog; import android.app.ExpandableListActivity; import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.text.Editable; import android.text.Html; import android.text.InputType; import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnCreateContextMenuListener; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.BaseExpandableListAdapter; import android.widget.EditText; import android.widget.ExpandableListView; import android.widget.TextView; import android.widget.TextView.BufferType; import android.widget.Toast; import com.gimranov.zandy.app.data.Database; import com.gimranov.zandy.app.data.Item; import com.gimranov.zandy.app.task.APIRequest; import com.gimranov.zandy.app.task.ZoteroAPITask; public class ItemDataActivity extends ExpandableListActivity { private static final String TAG = "com.gimranov.zandy.app.ItemDataActivity"; static final int DIALOG_SINGLE_VALUE = 0; static final int DIALOG_ITEM_TYPE = 1; static final int DIALOG_CONFIRM_NAVIGATE = 4; static final int DIALOG_CONFIRM_DELETE = 5; public Item item; private Database db; /** * For Bundle passing to Dialogs in API <= 7 */ protected Bundle b = new Bundle(); /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); db = new Database(this); /* Get the incoming data from the calling activity */ String itemKey = getIntent().getStringExtra("com.gimranov.zandy.app.itemKey"); item = Item.load(itemKey, db); // When an item in the view has been updated via a sync, the temporary key may have // been swapped out, so we fall back on the DB ID if (item == null) { String itemDbId = getIntent().getStringExtra("com.gimranov.zandy.app.itemDbId"); if (itemDbId == null) { Log.d(TAG, "Failed to load item using itemKey and no dbId specified. Give up and finish activity."); finish(); return; } item = Item.loadDbId(itemDbId, db); } // Set the activity title to the current item's title, if the title works if (item.getTitle() != null && !item.getTitle().equals("")) this.setTitle(item.getTitle()); else this.setTitle(getResources().getString(R.string.item_details)); ArrayList<Bundle> rows = item.toBundleArray(db); BundleListAdapter mBundleListAdapter = new BundleListAdapter(); mBundleListAdapter.bundles = rows; setListAdapter(mBundleListAdapter); registerForContextMenu(getExpandableListView()); ExpandableListView lv = getExpandableListView(); lv.setGroupIndicator(getResources().getDrawable(R.drawable.list_child_indicator)); lv.setTextFilterEnabled(true); lv.setOnChildClickListener(new ExpandableListView.OnChildClickListener() { @Override public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { return true; } }); lv.setOnGroupClickListener(new ExpandableListView.OnGroupClickListener() { // Warning here because Eclipse can't tell whether my ArrayAdapter is // being used with the correct parametrization. public boolean onGroupClick(ExpandableListView parent, View view, int position, long id) { // If we have a click on an entry, do something... BundleListAdapter adapter = (BundleListAdapter) parent.getExpandableListAdapter(); Bundle row = adapter.getGroup(position); if (row.getString("label").equals("url")) { row.putString("url", row.getString("content")); removeDialog(DIALOG_CONFIRM_NAVIGATE); ItemDataActivity.this.b = row; showDialog(DIALOG_CONFIRM_NAVIGATE); return true; } else if (row.getString("label").equals("DOI")) { String url = "http://dx.doi.org/"+Uri.encode(row.getString("content")); row.putString("url", url); removeDialog(DIALOG_CONFIRM_NAVIGATE); ItemDataActivity.this.b = row; showDialog(DIALOG_CONFIRM_NAVIGATE); return true; } else if (row.getString("label").equals("creators")) { Log.d(TAG, "Trying to start creators activity"); Intent i = new Intent(getBaseContext(), CreatorActivity.class); i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); startActivity(i); return true; } else if (row.getString("label").equals("tags")) { Log.d(TAG, "Trying to start tag activity"); Intent i = new Intent(getBaseContext(), TagActivity.class); i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); startActivity(i); return true; } else if (row.getString("label").equals("children")) { Log.d(TAG, "Trying to start attachment activity"); Intent i = new Intent(getBaseContext(), AttachmentActivity.class); i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); startActivity(i); return true; } else if (row.getString("label").equals("collections")) { Log.d(TAG, "Trying to start collection membership activity"); Intent i = new Intent(getBaseContext(), CollectionMembershipActivity.class); i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); startActivity(i); return true; } // Suppress toast if we're going to expand the view anyway if (!"abstractNote".equals(row.getSerializable("label"))) { Toast.makeText(getApplicationContext(), row.getString("content"), Toast.LENGTH_SHORT).show(); } return false; } }); /* * On long click, we bring up an edit dialog. */ lv.setOnCreateContextMenuListener(new OnCreateContextMenuListener() { @Override public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { ExpandableListView.ExpandableListContextMenuInfo info = (ExpandableListView.ExpandableListContextMenuInfo) menuInfo; int type = ExpandableListView.getPackedPositionType(info.packedPosition); int group = ExpandableListView.getPackedPositionGroup(info.packedPosition); @SuppressWarnings("unused") int child = ExpandableListView.getPackedPositionChild(info.packedPosition); if (type == 0) { // If we have a long click on an entry, we'll provide a way of editing it BundleListAdapter adapter = (BundleListAdapter) ((ExpandableListView) info.targetView.getParent()).getExpandableListAdapter();; Bundle row = adapter.getGroup(group); // Show the right type of dialog for the row in question if (row.getString("label").equals("itemType")) { // XXX don't need i18n, since this should be overcome Toast.makeText(getApplicationContext(), "Item type cannot be changed.", Toast.LENGTH_SHORT).show(); //removeDialog(DIALOG_ITEM_TYPE); //showDialog(DIALOG_ITEM_TYPE, row); return; } else if (row.getString("label").equals("children")) { Log.d(TAG, "Not starting children activity on click-and-hold"); return; } else if (row.getString("label").equals("creators")) { Log.d(TAG, "Trying to start creators activity"); Intent i = new Intent(getBaseContext(), CreatorActivity.class); i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); startActivity(i); return; } else if (row.getString("label").equals("tags")) { Log.d(TAG, "Trying to start tag activity"); Intent i = new Intent(getBaseContext(), TagActivity.class); i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); startActivity(i); return; } removeDialog(DIALOG_SINGLE_VALUE); ItemDataActivity.this.b = row; showDialog(DIALOG_SINGLE_VALUE); return; } } }); } protected Dialog onCreateDialog(int id) { final String label = b.getString("label"); final String itemKey = b.getString("itemKey"); final String content = b.getString("content"); AlertDialog dialog; switch (id) { /* Simple editor for a single value */ case DIALOG_SINGLE_VALUE: final EditText input = new EditText(this); input.setText(content, BufferType.EDITABLE); if ("url".equals(label)) { input.setInputType(InputType.TYPE_TEXT_VARIATION_URI); } dialog = new AlertDialog.Builder(this) .setTitle(getResources().getString(R.string.edit_item_field, Item.localizedStringForString(label))) .setView(input) .setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { Editable value = input.getText(); String fixed; if ("note".equals(label)) { fixed = value.toString().replaceAll("\n\n", "\n<br>"); } else { fixed = value.toString(); } Item.set(itemKey, label, fixed, db); Item item = Item.load(itemKey, db); BundleListAdapter la = (BundleListAdapter) getExpandableListAdapter(); la.bundles = item.toBundleArray(db); la.notifyDataSetChanged(); } }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // do nothing } }).create(); return dialog; /* Item type selector */ case DIALOG_ITEM_TYPE: dialog = new AlertDialog.Builder(this) .setTitle(getResources().getString(R.string.item_type_change)) // XXX i18n .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int pos) { Item.set(itemKey, label, Item.ITEM_TYPES[pos], db); Item item = Item.load(itemKey, db); BundleListAdapter la = (BundleListAdapter) getExpandableListAdapter(); la.bundles = item.toBundleArray(db); la.notifyDataSetChanged(); } }).create(); return dialog; case DIALOG_CONFIRM_NAVIGATE: dialog = new AlertDialog.Builder(this) .setTitle(getResources().getString(R.string.view_online_warning)) .setPositiveButton(getResources().getString(R.string.view), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { String fixedUri = Util.doiToUri(content); // The behavior for invalid URIs might be nasty, but // we'll cross that bridge if we come to it. Uri uri = Uri.parse(fixedUri); try { startActivity(new Intent(Intent.ACTION_VIEW) .setData(uri)); } catch (ActivityNotFoundException e) { //noinspection UnnecessaryReturnStatement Toast.makeText(ItemDataActivity.this, ItemDataActivity.this.getString(R.string.attachment_intent_failed_for_uri, fixedUri), Toast.LENGTH_LONG).show(); return; } } }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // do nothing } }).create(); return dialog; case DIALOG_CONFIRM_DELETE: dialog = new AlertDialog.Builder(this) .setTitle(getResources().getString(R.string.item_delete_confirm)) .setPositiveButton(getResources().getString(R.string.menu_delete), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { Item i = Item.load(itemKey, db); i.delete(db); finish(); } }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // do nothing } }).create(); return dialog; default: Log.e(TAG, "Invalid dialog requested"); return null; } } @Override public void onDestroy() { if (db != null) db.close(); super.onDestroy(); } @Override public void onResume() { if (db == null) db = new Database(this); super.onResume(); } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.zotero_menu, menu); // Remove new item-- should be created from context of an item list menu.removeItem(R.id.do_new); // Turn on delete item MenuItem del = menu.findItem(R.id.do_delete); del.setEnabled(true); del.setVisible(true); return true; } @Override public boolean onOptionsItemSelected(MenuItem i) { // Handle item selection switch (i.getItemId()) { case R.id.do_sync: if (!ServerCredentials.check(getApplicationContext())) { Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_log_in_first), Toast.LENGTH_SHORT).show(); return true; } Log.d(TAG, "Preparing sync requests, starting with present item"); APIRequest req; if (APIRequest.API_CLEAN.equals(item.dirty)) { ArrayList<Item> items = new ArrayList<Item>(); items.add(item); req = APIRequest.add(items); } else { req = APIRequest.update(item); } new ZoteroAPITask(getBaseContext()).execute(req); Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), Toast.LENGTH_SHORT).show(); return true; case R.id.do_prefs: startActivity(new Intent(this, SettingsActivity.class)); return true; case R.id.do_delete: Bundle b = new Bundle(); b.putString("itemKey",item.getKey()); removeDialog(DIALOG_CONFIRM_DELETE); this.b = b; showDialog(DIALOG_CONFIRM_DELETE); return true; default: return super.onOptionsItemSelected(i); } } /** * List adapter that provides our bundles in the appropriate way */ public class BundleListAdapter extends BaseExpandableListAdapter { public ArrayList<Bundle> bundles; public Bundle getChild(int groupPosition, int childPosition) { return bundles.get(groupPosition); } public long getChildId(int groupPosition, int childPosition) { return childPosition; } public int getChildrenCount(int groupPosition) { if ("abstractNote".equals(bundles.get(groupPosition).getString("label"))) { return 1; } else { return 0; } } public TextView getGenericView() { // Layout parameters for the ExpandableListView AbsListView.LayoutParams lp = new AbsListView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, 64); TextView textView = new TextView(ItemDataActivity.this); textView.setLayoutParams(lp); // Center the text vertically textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); // Set the text starting position textView.setPadding(0, 0, 0, 0); return textView; } public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { TextView textView = getGenericView(); Bundle b = getGroup(groupPosition); String label = b.getString("label"); String content = b.getString("content"); if ("title".equals(label) || "note".equals(label)) { textView.setText(Html.fromHtml(content)); } else { textView.setText(content); } return textView; } public Bundle getGroup(int groupPosition) { return bundles.get(groupPosition); } public int getGroupCount() { return bundles.size(); } public long getGroupId(int groupPosition) { return groupPosition; } public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { View row; Bundle b = getGroup(groupPosition); String label = b.getString("label"); String content = ""; if ("children".equals(label)) { int notes = b.getInt("noteCount", 0); int atts = b.getInt("attachmentCount", 0); if (notes == 0 && atts == 0) getResources().getString(R.string.item_attachment_info_none); else content = getResources().getString(R.string.item_attachment_info_template, notes, atts); } else if ("collections".equals((getGroup(groupPosition).getString("label")))) { int count = b.getInt("collectionCount", 0); content = getResources().getString(R.string.item_collection_count, count); } else { content = b.getString("content"); } // We are reusing views, but we need to initialize it if null if (null == convertView) { LayoutInflater inflater = getLayoutInflater(); row = inflater.inflate(R.layout.list_data, null); } else { row = convertView; } /* Our layout has just two fields */ TextView tvLabel = (TextView) row.findViewById(R.id.data_label); tvLabel.setPadding(0, 0, 0, 0); TextView tvContent = (TextView) row.findViewById(R.id.data_content); tvContent.setPadding(0, 0, 0, 0); /* Since the field names are the API / internal form, we * attempt to get a localized, human-readable version. */ tvLabel.setText(Item.localizedStringForString(label)); if ("title".equals(label) || "note".equals(label)) { tvContent.setText(Html.fromHtml(content)); } else { tvContent.setText(content); } if ("abstractNote".equals(label)) { tvLabel.setText(getResources().getString(R.string.item_more, Item.localizedStringForString("abstractNote"))); } tvContent.setMaxLines(2); tvContent.setEllipsize(TextUtils.TruncateAt.MARQUEE); return row; } public boolean isChildSelectable(int groupPosition, int childPosition) { return false; } public boolean hasStableIds() { return true; } } }