diff --git a/build.gradle b/build.gradle index 7c63de8..22c1939 100644 --- a/build.gradle +++ b/build.gradle @@ -3,11 +3,9 @@ import groovy.swing.SwingBuilder buildscript { repositories { mavenCentral() - maven { url 'http://download.crashlytics.com/maven' } } dependencies { - classpath 'com.android.tools.build:gradle:0.12.+' - classpath 'com.crashlytics.tools.gradle:crashlytics-gradle:1.+' + classpath 'com.android.tools.build:gradle:1.2.3' } } @@ -15,42 +13,43 @@ buildscript { repositories { mavenLocal() mavenCentral() - maven { url 'http://download.crashlytics.com/maven' } + jcenter() } apply plugin: 'android' -apply plugin: 'crashlytics' dependencies { + compile 'com.android.support:support-v4:21.0.+' + compile 'com.android.support:appcompat-v7:21.0.+' compile 'oauth.signpost:signpost-commonshttp4:1.2.1.2' compile 'com.intellij:annotations:12.0' - compile 'com.crashlytics.android:crashlytics:1.+' compile 'commons-io:commons-io:2.4' compile 'com.squareup:otto:1.3.4' compile 'com.google.zxing:android-integration:2.3.0' - -// compile project(":libzotero-java") + compile 'com.getbase:floatingactionbutton:1.10.0' + compile 'com.nononsenseapps:filepicker:2.2.3' } android { - compileSdkVersion 19 - buildToolsVersion "20.0.0" + compileSdkVersion 22 + buildToolsVersion "22.0.1" defaultConfig { + minSdkVersion 16 + targetSdkVersion 22 versionCode 1440 versionName "1.4.4" } productFlavors { - amazon google } signingConfigs { release { storeFile file("keystore") - keyAlias "zandy" + keyAlias "zotable" storePassword "" keyPassword "" } @@ -58,9 +57,9 @@ android { buildTypes { release { - zipAlign true + zipAlignEnabled true signingConfig signingConfigs.release - runProguard false + minifyEnabled false proguardFile getDefaultProguardFile('proguard-android.txt') } } @@ -77,7 +76,7 @@ android { gradle.taskGraph.whenReady { taskGraph -> println taskGraph.allTasks - if(taskGraph.hasTask(':zandy:assembleGoogleRelease')) { + if(taskGraph.hasTask(':zotable:assembleGoogleRelease')) { def storePass = '' def keyPass = '' diff --git a/src/instrumentTest/java/com/gimranov/zandy/app/test/ApiTest.java b/src/instrumentTest/java/com/gimranov/zandy/app/test/ApiTest.java deleted file mode 100644 index 19d10c4..0000000 --- a/src/instrumentTest/java/com/gimranov/zandy/app/test/ApiTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.gimranov.zandy.app.test; - -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.test.AndroidTestCase; -import android.test.IsolatedContext; - -import com.gimranov.zandy.app.ServerCredentials; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.task.APIException; -import com.gimranov.zandy.app.task.APIRequest; - - -public class ApiTest extends AndroidTestCase { - - private IsolatedContext mContext; - private Database mDb; - private ServerCredentials mCred; - - /** - * Access information for the Zandy test user on Zotero.org - */ - private static final String TEST_UID = "743083"; - private static final String TEST_KEY = "JFRP2k4qvhRUm62kuDHXUUX3"; - private static final String TEST_COLLECTION = "U8GNSSF3"; - - // unlikely to exist - private static final String TEST_MISSING_ITEM = "ZZZZZZZZ"; - - - @Override - protected void setUp() throws Exception { - super.setUp(); - mContext = new IsolatedContext(null, getContext()); - mDb = new Database(mContext); - - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mContext); - SharedPreferences.Editor editor = settings.edit(); - // For Zotero, the key and secret are identical, it seems - editor.putString("user_key", TEST_KEY); - editor.putString("user_secret", TEST_KEY); - editor.putString("user_id", TEST_UID); - editor.commit(); - - mCred = new ServerCredentials(mContext); - } - - public void testPreConditions() { - // Make sure we do indeed have the key set up - assertTrue(ServerCredentials.check(mContext)); - } - - public void testItemsRequest() throws APIException { - APIRequest items = APIRequest.fetchItems(false, mCred); - items.issue(mDb, mCred); - } - - public void testCollectionsRequest() throws APIException { - APIRequest collections = APIRequest.fetchCollections(mCred); - collections.issue(mDb, mCred); - } - - public void testItemsForCollection() throws APIException { - APIRequest collection = APIRequest.fetchItems(TEST_COLLECTION, false, mCred); - collection.issue(mDb, mCred); - } - - // verify that we fail on this item, which should be missing - public void testMissingItem() throws APIException { - APIRequest missingItem = APIRequest.fetchItem(TEST_MISSING_ITEM, mCred); - try { - missingItem.issue(mDb, mCred); - // We shouldn't get here - assertTrue(false); - } catch (APIException e) { - // We expect only one specific exception message - if (!"Item does not exist".equals(e.getMessage())) - throw e; - } - } -} diff --git a/src/instrumentTest/java/com/gimranov/zandy/app/test/MainTest.java b/src/instrumentTest/java/com/gimranov/zandy/app/test/MainTest.java deleted file mode 100644 index 7cfc14e..0000000 --- a/src/instrumentTest/java/com/gimranov/zandy/app/test/MainTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.gimranov.zandy.app.test; - -import android.test.ActivityInstrumentationTestCase2; -import android.widget.Button; - -import com.gimranov.zandy.app.MainActivity; - -public class MainTest extends ActivityInstrumentationTestCase2 { - - private MainActivity mActivity; - private Button loginButton; - - public MainTest() { - super("com.gimranov.zandy.app", MainActivity.class); - } - - public void testPreconditions() { - assertNotNull(loginButton); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - mActivity = this.getActivity(); - loginButton = (Button) mActivity.findViewById(com.gimranov.zandy.app.R.id.loginButton); - } -} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml old mode 100644 new mode 100755 index e966093..778771a --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1,33 +1,35 @@ + package="com.mattrobertson.zotable.app" > + android:minSdkVersion="16" + android:targetSdkVersion="22" /> + + + android:name=".Application" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:theme="@style/AppTheme" > + android:launchMode="singleInstance" > + - - - + @@ -39,13 +41,26 @@ android:name=".CollectionActivity" android:label="@string/app_name" android:launchMode="standard" /> + + + + + + + + android:launchMode="standard" > + @@ -75,20 +90,44 @@ android:label="@string/app_name" android:launchMode="standard" /> + - + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/gimranov/zandy/app/Application.java b/src/main/java/com/gimranov/zandy/app/Application.java deleted file mode 100644 index 618ffc6..0000000 --- a/src/main/java/com/gimranov/zandy/app/Application.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.gimranov.zandy.app; - -import com.squareup.otto.Bus; - -public class Application extends android.app.Application { - private static final String TAG = Application.class.getCanonicalName(); - - private static Application instance; - - private Bus bus; - - @Override - public void onCreate() { - super.onCreate(); - - bus = new Bus(); - - instance = this; - } - - public Bus getBus() { - return bus; - } - - public static Application getInstance() { - return instance; - } -} diff --git a/src/main/java/com/gimranov/zandy/app/CollectionActivity.java b/src/main/java/com/gimranov/zandy/app/CollectionActivity.java deleted file mode 100644 index 87f178b..0000000 --- a/src/main/java/com/gimranov/zandy/app/CollectionActivity.java +++ /dev/null @@ -1,310 +0,0 @@ -/******************************************************************************* - * 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 . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import android.app.ListActivity; -import android.content.Intent; -import android.database.Cursor; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.gimranov.zandy.app.data.CollectionAdapter; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIEvent; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; - -public class CollectionActivity extends ListActivity { - - private static final String TAG = "com.gimranov.zandy.app.CollectionActivity"; - private ItemCollection collection; - private Database db; - - final Handler handler = new Handler() { - public void handleMessage(Message msg) { - Log.d(TAG,"received message: "+msg.arg1); - refreshView(); - - if (msg.arg1 == APIRequest.UPDATED_DATA) { - //refreshView(); - return; - } - - if (msg.arg1 == APIRequest.QUEUED_MORE) { - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.sync_queued_more, msg.arg2), - Toast.LENGTH_SHORT).show(); - return; - } - - if (msg.arg1 == APIRequest.BATCH_DONE) { - Application.getInstance().getBus().post(SyncEvent.COMPLETE); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.sync_complete), - Toast.LENGTH_SHORT).show(); - return; - } - - if (msg.arg1 == APIRequest.ERROR_UNKNOWN) { - String desc = (msg.arg2 == 0) ? "" : " ("+msg.arg2+")"; - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.sync_error)+desc, - Toast.LENGTH_SHORT).show(); - return; - } - } - }; - - /** - * Refreshes the current list adapter - */ - private void refreshView() { - CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); - Cursor newCursor = (collection == null) ? create() : create(collection); - adapter.changeCursor(newCursor); - adapter.notifyDataSetChanged(); - Log.d(TAG, "refreshing view on request"); - } - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - db = new Database(this); - - setContentView(R.layout.collections); - - CollectionAdapter collectionAdapter; - - String collectionKey = getIntent().getStringExtra("com.gimranov.zandy.app.collectionKey"); - if (collectionKey != null) { - ItemCollection coll = ItemCollection.load(collectionKey, db); - // We set the title to the current collection - this.collection = coll; - this.setTitle(coll.getTitle()); - collectionAdapter = new CollectionAdapter(this, create(coll)); - } else { - this.setTitle(getResources().getString(R.string.collections)); - collectionAdapter = new CollectionAdapter(this, create()); - } - - setListAdapter(collectionAdapter); - - ListView lv = getListView(); - lv.setOnItemLongClickListener(new OnItemLongClickListener() { - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); - Cursor cur = adapter.getCursor(); - // Place the cursor at the selected item - if (cur.moveToPosition(position)) { - // and open activity for the selected collection - ItemCollection coll = ItemCollection.load(cur); - if (coll != null && coll.getKey() != null && coll.getSubcollections(db).size() > 0) { - Log.d(TAG, "Loading child collection with key: "+coll.getKey()); - // We create and issue a specified intent with the necessary data - Intent i = new Intent(getBaseContext(), CollectionActivity.class); - i.putExtra("com.gimranov.zandy.app.collectionKey", coll.getKey()); - startActivity(i); - } else { - Log.d(TAG, "Failed loading child collections for collection"); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.collection_no_subcollections), - Toast.LENGTH_SHORT).show(); - } - } else { - // failed to move cursor-- show a toast - TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.collection_cant_open, tvTitle.getText()), - Toast.LENGTH_SHORT).show(); - } - return true; - } - }); - - lv.setOnItemClickListener(new OnItemClickListener() { - public void onItemClick(AdapterView parent, View view, int position, long id) { - - CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); - Cursor cur = adapter.getCursor(); - // Place the cursor at the selected item - if (cur.moveToPosition(position)) { - // and replace the cursor with one for the selected collection - ItemCollection coll = ItemCollection.load(cur); - if (coll != null && coll.getKey() != null) { - Intent i = new Intent(getBaseContext(), ItemActivity.class); - if (coll.getSize() == 0) { - // Send a message that we need to refresh the collection - i.putExtra("com.gimranov.zandy.app.rerequest", true); - } - i.putExtra("com.gimranov.zandy.app.collectionKey", coll.getKey()); - startActivity(i); - } else { - // collection loaded was null. why? - Log.d(TAG, "Failed loading items for collection at position: "+position); - return; - } - } else { - // failed to move cursor-- show a toast - TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.collection_cant_open, tvTitle.getText()), - Toast.LENGTH_SHORT).show(); - return; - } - return; - } - }); - } - - protected void onResume() { - CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); - // XXX This may be too agressive-- fix if causes issues - Cursor newCursor = (collection == null) ? create() : create(collection); - adapter.changeCursor(newCursor); - adapter.notifyDataSetChanged(); - if (db == null) db = new Database(this); - super.onResume(); - } - - public void onDestroy() { - CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); - Cursor cur = adapter.getCursor(); - if(cur != null) cur.close(); - if (db != null) db.close(); - super.onDestroy(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.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; - } - APIRequest req = APIRequest.fetchCollections(new ServerCredentials(getApplicationContext())); - req.setHandler(new APIEvent() { - private int updates = 0; - - @Override - public void onComplete(APIRequest request) { - Message msg = handler.obtainMessage(); - msg.arg1 = APIRequest.BATCH_DONE; - handler.sendMessage(msg); - Log.d(TAG, "fired oncomplete"); - } - - @Override - public void onUpdate(APIRequest request) { - updates++; - - Message msg = handler.obtainMessage(); - msg.arg1 = APIRequest.UPDATED_DATA; - handler.sendMessage(msg); - } - - @Override - public void onError(APIRequest request, Exception exception) { - Log.e(TAG, "APIException caught", exception); - Message msg = handler.obtainMessage(); - msg.arg1 = APIRequest.ERROR_UNKNOWN; - handler.sendMessage(msg); - } - - @Override - public void onError(APIRequest request, int error) { - Log.e(TAG, "API error caught"); - Message msg = handler.obtainMessage(); - msg.arg1 = APIRequest.ERROR_UNKNOWN; - msg.arg2 = request.status; - handler.sendMessage(msg); - } - }); - ZoteroAPITask task = new ZoteroAPITask(getBaseContext()); - task.setHandler(handler); - task.execute(req); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_collection), - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_new: - Log.d(TAG, "Can't yet make new collections"); - // XXX no i18n for temporary string - Toast.makeText(getApplicationContext(), "Sorry, new collection creation is not yet possible. Soon!", - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - /** - * Gives a cursor for top-level collections - * @return - */ - public Cursor create() { - String[] args = { "false" }; - Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); - if (cursor == null) { - Log.e(TAG, "cursor is null"); - } - - return cursor; - } - - /** - * Gives a cursor for child collections of a given parent - * @param parent - * @return - */ - public Cursor create(ItemCollection parent) { - String[] args = { parent.getKey() }; - Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); - if (cursor == null) { - Log.e(TAG, "cursor is null"); - } - - return cursor; - } - -} diff --git a/src/main/java/com/gimranov/zandy/app/CollectionMembershipActivity.java b/src/main/java/com/gimranov/zandy/app/CollectionMembershipActivity.java deleted file mode 100644 index 7fa3657..0000000 --- a/src/main/java/com/gimranov/zandy/app/CollectionMembershipActivity.java +++ /dev/null @@ -1,249 +0,0 @@ -/******************************************************************************* - * 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 . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import java.util.ArrayList; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.ListActivity; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; - -/** - * This Activity handles displaying and editing collection memberships for a - * given item. - * - * @author ajlyon - * - */ -public class CollectionMembershipActivity extends ListActivity { - - private static final String TAG = "com.gimranov.zandy.app.CollectionMembershipActivity"; - - static final int DIALOG_CONFIRM_NAVIGATE = 4; - static final int DIALOG_COLLECTION_LIST = 1; - - private String itemKey; - private String itemTitle; - - private Database db; - - /** - * For API <= 7, where we can't pass Bundles to dialogs - */ - private 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 */ - itemKey = getIntent().getStringExtra("com.gimranov.zandy.app.itemKey"); - Item item = Item.load(itemKey, db); - if (item == null) { - Log.e(TAG, "Null item for key: "+itemKey); - finish(); - } - itemTitle = item.getTitle(); - - this.setTitle(getResources().getString(R.string.collections_for_item, itemTitle)); - - ArrayList rows = ItemCollection.getCollections(item, db); - - setListAdapter(new ArrayAdapter(this, R.layout.list_data, rows) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View row; - - // 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.setText(""); - TextView tvContent = (TextView) row.findViewById(R.id.data_content); - - tvContent.setText(getItem(position).getTitle()); - - return row; - } - }); - - ListView lv = getListView(); - lv.setTextFilterEnabled(true); - lv.setOnItemClickListener(new OnItemClickListener() { - // Warning here because Eclipse can't tell whether my ArrayAdapter is - // being used with the correct parametrization. - @SuppressWarnings("unchecked") - public void onItemClick(AdapterView parent, View view, int position, long id) { - // If we have a click on an entry, prompt to view that tag's items. - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - ItemCollection row = adapter.getItem(position); - Bundle b = new Bundle(); - b.putString("itemKey", itemKey); - b.putString("collectionKey", row.getKey()); - CollectionMembershipActivity.this.b = b; - removeDialog(DIALOG_CONFIRM_NAVIGATE); - showDialog(DIALOG_CONFIRM_NAVIGATE); - } - }); - - } - - @Override - public void onDestroy() { - if (db != null) db.close(); - super.onDestroy(); - } - - @Override - public void onResume() { - if (db == null) db = new Database(this); - super.onResume(); - } - - protected Dialog onCreateDialog(int id) { - final String collectionKey = b.getString("collectionKey"); - final String itemKey = b.getString("itemKey"); - AlertDialog dialog; - - switch (id) { - case DIALOG_COLLECTION_LIST: - AlertDialog.Builder builder = new AlertDialog.Builder(this); - final ArrayList collections = ItemCollection.getCollections(db); - int size = collections.size(); - String[] collectionNames = new String[size]; - for (int i = 0; i < size; i++) { - collectionNames[i] = collections.get(i).getTitle(); - } - builder.setTitle(getResources().getString(R.string.choose_parent_collection)) - .setItems(collectionNames, new DialogInterface.OnClickListener() { - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int pos) { - Item item = Item.load(itemKey, db); - collections.get(pos).add(item, false, db); - collections.get(pos).saveChildren(db); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (ItemCollection b : ItemCollection.getCollections(item,db)) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }); - dialog = builder.create(); - return dialog; - case DIALOG_CONFIRM_NAVIGATE: - dialog = new AlertDialog.Builder(this) - .setTitle(getResources().getString(R.string.collection_membership_detail)) - .setPositiveButton(getResources().getString(R.string.tag_view), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - Intent i = new Intent(getBaseContext(), ItemActivity.class); - i.putExtra("com.gimranov.zandy.app.collectionKey", collectionKey); - startActivity(i); - } - }).setNeutralButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }).setNegativeButton(getResources().getString(R.string.collection_remove_item), new DialogInterface.OnClickListener() { - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int whichButton) { - Item item = Item.load(itemKey, db); - ItemCollection coll = ItemCollection.load(collectionKey, db); - coll.remove(item, false, db); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (ItemCollection b : ItemCollection.getCollections(item,db)) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }).create(); - return dialog; - default: - Log.e(TAG, "Invalid dialog requested"); - return null; - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.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"); - new ZoteroAPITask(getBaseContext()).execute(APIRequest.update(Item.load(itemKey, db))); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_new: - Bundle b = new Bundle(); - b.putString("itemKey", itemKey); - removeDialog(DIALOG_COLLECTION_LIST); - this.b = b; - showDialog(DIALOG_COLLECTION_LIST); - return true; - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/main/java/com/gimranov/zandy/app/CreatorActivity.java b/src/main/java/com/gimranov/zandy/app/CreatorActivity.java deleted file mode 100644 index 4406fb5..0000000 --- a/src/main/java/com/gimranov/zandy/app/CreatorActivity.java +++ /dev/null @@ -1,381 +0,0 @@ -/******************************************************************************* - * 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 . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import java.util.ArrayList; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.ListActivity; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ArrayAdapter; -import android.widget.CheckBox; -import android.widget.ListView; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; - -import com.gimranov.zandy.app.data.Creator; -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; - -/** - * This Activity handles displaying and editing creators. It works almost the same as - * ItemDataActivity and TagActivity, using a simple ArrayAdapter on Bundles with the creator info. - * - * This currently operates by showing the creators for a given item; it could be - * modified some day to show all creators in the database (when they come to be saved - * that way). - * - * @author ajlyon - * - */ -public class CreatorActivity extends ListActivity { - - private static final String TAG = "com.gimranov.zandy.app.CreatorActivity"; - - static final int DIALOG_CREATOR = 3; - static final int DIALOG_CONFIRM_NAVIGATE = 4; - static final int DIALOG_CONFIRM_DELETE = 5; - - public Item item; - - private Database db; - - /** - * For API <= 7, to pass bundles to activities - */ - private 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 = Item.load(itemKey, db); - this.item = item; - - this.setTitle("Creators for "+item.getTitle()); - - ArrayList rows = item.creatorsToBundleArray(); - - /* - * We use the standard ArrayAdapter, passing in our data as a Bundle. - * Since it's no longer a simple TextView, we need to override getView, but - * we can do that anonymously. - */ - setListAdapter(new ArrayAdapter(this, R.layout.list_data, rows) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View row; - - // 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); - TextView tvContent = (TextView) row.findViewById(R.id.data_content); - - tvLabel.setText(Item.localizedStringForString( - getItem(position).getString("creatorType"))); - tvContent.setText(getItem(position).getString("name")); - - return row; - } - }); - - ListView lv = getListView(); - lv.setTextFilterEnabled(true); - lv.setOnItemClickListener(new OnItemClickListener() { - // Warning here because Eclipse can't tell whether my ArrayAdapter is - // being used with the correct parametrization. - @SuppressWarnings("unchecked") - public void onItemClick(AdapterView parent, View view, int position, long id) { - // If we have a click on an entry, do something... - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - Bundle row = adapter.getItem(position); - -/* TODO Rework this logic to open an ItemActivity showing items with this creator - if (row.getString("label").equals("url")) { - row.putString("url", row.getString("content")); - removeDialog(DIALOG_CONFIRM_NAVIGATE); - showDialog(DIALOG_CONFIRM_NAVIGATE, row); - return; - } - - 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); - showDialog(DIALOG_CONFIRM_NAVIGATE, row); - return; - } - */ - Toast.makeText(getApplicationContext(), row.getString("name"), - Toast.LENGTH_SHORT).show(); - } - }); - - /* - * On long click, we bring up an edit dialog. - */ - lv.setOnItemLongClickListener(new OnItemLongClickListener() { - /* - * Same annotation as in onItemClick(..), above. - */ - @SuppressWarnings("unchecked") - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - // If we have a long click on an entry, show an editor - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - Bundle row = adapter.getItem(position); - CreatorActivity.this.b = row; - removeDialog(DIALOG_CREATOR); - showDialog(DIALOG_CREATOR); - return true; - } - }); - - } - - @Override - public void onDestroy() { - if (db != null) db.close(); - super.onDestroy(); - } - - @Override - public void onResume() { - if (db == null) db = new Database(this); - super.onResume(); - } - - protected Dialog onCreateDialog(int id) { - final String creatorType = b.getString("creatorType"); - final int creatorPosition = b.getInt("position"); - - String name = b.getString("name"); - String firstName = b.getString("firstName"); - String lastName = b.getString("lastName"); - - switch (id) { - /* Editor for a creator - */ - case DIALOG_CREATOR: - AlertDialog.Builder builder; - AlertDialog dialog; - - LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); - final View layout = inflater.inflate(R.layout.creator_dialog, - (ViewGroup) findViewById(R.id.layout_root)); - - TextView textName = (TextView) layout.findViewById(R.id.creator_name); - textName.setText(name); - TextView textFN = (TextView) layout.findViewById(R.id.creator_firstName); - textFN.setText(firstName); - TextView textLN = (TextView) layout.findViewById(R.id.creator_lastName); - textLN.setText(lastName); - - CheckBox mode = (CheckBox) layout.findViewById(R.id.creator_mode); - mode.setChecked((firstName == null || firstName.equals("")) - && (lastName == null || lastName.equals("")) - && (lastName != null && !name.equals(""))); - - // Set up the adapter to get creator types - String[] types = Item.localizedCreatorTypesForItemType(item.getType()); - - // what position are we? - int arrPosition = 0; - String localType = ""; - if (creatorType != null) { - localType = Item.localizedStringForString(creatorType); - } else { - // We default to the first possibility when none specified - localType = Item.localizedStringForString( - Item.creatorTypesForItemType(item.getType())[0]); - } - for (int i = 0; i < types.length; i++) { - if (types[i].equals(localType)) { - arrPosition = i; - break; - } - } - - ArrayAdapter adapter = new ArrayAdapter(this, - android.R.layout.simple_spinner_item, types); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - - Spinner spinner = (Spinner) layout.findViewById(R.id.creator_type); - spinner.setAdapter(adapter); - - spinner.setSelection(arrPosition); - builder = new AlertDialog.Builder(this); - builder.setView(layout); - builder.setPositiveButton(getResources().getString(R.string.ok), new OnClickListener(){ - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int whichButton) { - Creator c; - TextView textName = (TextView) layout.findViewById(R.id.creator_name); - TextView textFN = (TextView) layout.findViewById(R.id.creator_firstName); - TextView textLN = (TextView) layout.findViewById(R.id.creator_lastName); - Spinner spinner = (Spinner) layout.findViewById(R.id.creator_type); - CheckBox mode = (CheckBox) layout.findViewById(R.id.creator_mode); - - String selected = (String) spinner.getSelectedItem(); - // Set up the adapter to get creator types - String[] types = Item.localizedCreatorTypesForItemType(item.getType()); - - // what position are we? - int typePos = 0; - for (int i = 0; i < types.length; i++) { - if (types[i].equals(selected)) { - typePos = i; - break; - } - } - - String realType = Item.creatorTypesForItemType(item.getType())[typePos]; - - if (mode.isChecked()) - c = new Creator(realType, textName.getText().toString(), true); - else - c = new Creator(realType, textFN.getText().toString(), textLN.getText().toString()); - - Item.setCreator(item.getKey(), c, creatorPosition, db); - item = Item.load(item.getKey(), db); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (Bundle b : item.creatorsToBundleArray()) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }); - - builder.setNeutralButton(getResources().getString(R.string.cancel), new OnClickListener(){ - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }); - - builder.setNegativeButton(getResources().getString(R.string.menu_delete), new OnClickListener(){ - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int whichButton) { - Item.setCreator(item.getKey(), null, creatorPosition, db); - item = Item.load(item.getKey(), db); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (Bundle b : item.creatorsToBundleArray()) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }); - - dialog = builder.create(); - return dialog; - - case DIALOG_CONFIRM_NAVIGATE: -/* dialog = new AlertDialog.Builder(this) - .setTitle("View this online?") - .setPositiveButton("View", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // The behavior for invalid URIs might be nasty, but - // we'll cross that bridge if we come to it. - Uri uri = Uri.parse(content); - startActivity(new Intent(Intent.ACTION_VIEW) - .setData(uri)); - } - }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }).create(); - return dialog;*/ - return null; - default: - Log.e(TAG, "Invalid dialog requested"); - return null; - } - } - - /* - * I've been just copying-and-pasting the options menu code from activity to activity. - * It needs to be reworked for some of these activities. - */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.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"); - new ZoteroAPITask(getBaseContext()).execute(APIRequest.update(this.item)); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - - return true; - case R.id.do_new: - Bundle row = new Bundle(); - row.putInt("position", -1); - row.putString("itemKey", this.item.getKey()); - removeDialog(DIALOG_CREATOR); - this.b = row; - showDialog(DIALOG_CREATOR); - return true; - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/main/java/com/gimranov/zandy/app/LookupActivity.java b/src/main/java/com/gimranov/zandy/app/LookupActivity.java deleted file mode 100644 index 936db73..0000000 --- a/src/main/java/com/gimranov/zandy/app/LookupActivity.java +++ /dev/null @@ -1,260 +0,0 @@ -/******************************************************************************* - * 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 . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; - -import org.apache.http.util.ByteArrayBuffer; - -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.text.Editable; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.TextView; - -import com.gimranov.zandy.app.data.Database; - -/** - * Runs lookup routines to create new items - * @author ajlyon - * - */ -public class LookupActivity extends Activity implements OnClickListener { - - private static final String TAG = "com.gimranov.zandy.app.LookupActivity"; - - static final int DIALOG_PROGRESS = 6; - - private ProgressDialog mProgressDialog; - private ProgressThread progressThread; - private Database db; - - private 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 */ - final String identifier = getIntent().getStringExtra("com.gimranov.zandy.app.identifier"); - final String mode = getIntent().getStringExtra("com.gimranov.zandy.app.mode"); - - setContentView(R.layout.lookup); - - Button lookupButton = (Button) findViewById(R.id.lookupButton); - lookupButton.setOnClickListener(this); - - } - - /** - * Implementation of the OnClickListener interface, to handle button events. - * - * Note: When adding a button, it needs to be added here, but the - * ClickListener needs to be set in the main onCreate(..) as well. - */ - public void onClick(View v) { - Log.d(TAG, "Click on: " + v.getId()); - if (v.getId() == R.id.lookupButton) { - Log.d(TAG, "Trying to start search activity"); - TextView field = (TextView) findViewById(R.id.identifier); - Editable fieldContents = (Editable) field.getText(); - Bundle b = new Bundle(); - b.putString("mode", "isbn"); - b.putString("identifier", fieldContents.toString()); - this.b = b; - showDialog(DIALOG_PROGRESS); - } else { - Log.w(TAG, "Uncaught click on: " + v.getId()); - } - } - - - @Override - public void onDestroy() { - super.onDestroy(); - } - - @Override - public void onResume() { - if (db == null) db = new Database(this); - super.onResume(); - } - - protected Dialog onCreateDialog(int id) { - switch (id) { - case DIALOG_PROGRESS: - mProgressDialog = new ProgressDialog(this); - mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - mProgressDialog.setIndeterminate(true); - mProgressDialog.setMax(100); - return mProgressDialog; - default: - Log.e(TAG, "Invalid dialog requested"); - return null; - } - } - - protected void onPrepareDialog(int id, Dialog dialog) { - switch(id) { - case DIALOG_PROGRESS: - mProgressDialog.setProgress(0); - mProgressDialog.setMessage("Looking up item..."); - progressThread = new ProgressThread(handler, b); - progressThread.start(); - } - } - - - final Handler handler = new Handler() { - public void handleMessage(Message msg) { - if (ProgressThread.STATE_DONE == msg.arg2) { - if(mProgressDialog.isShowing()) - dismissDialog(DIALOG_PROGRESS); - // do something-- we're done. - return; - } - - if (ProgressThread.STATE_PARSING == msg.arg2) { - mProgressDialog.setMessage("Parsing item data..."); - return; - } - - int total = msg.arg1; - mProgressDialog.setProgress(total); - if (total >= 100) { - dismissDialog(DIALOG_PROGRESS); - progressThread.setState(ProgressThread.STATE_DONE); - } - } - }; - - private class ProgressThread extends Thread { - Handler mHandler; - Bundle arguments; - final static int STATE_DONE = 5; - final static int STATE_FETCHING = 1; - final static int STATE_PARSING = 6; - int mState; - - ProgressThread(Handler h, Bundle b) { - mHandler = h; - arguments = b; - } - - public void run() { - mState = STATE_FETCHING; - - // Setup - String identifier = arguments.getString("identifier"); - String mode = arguments.getString("mode"); - URL url; - String urlstring; - - if ("isbn".equals(mode)) { - if (identifier == null || identifier.equals("")) - identifier = "0674081250"; - urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" - + identifier - + "?method=getMetadata&fl=*&format=json&count=1"; - } else { - urlstring = ""; - } - - try { - Log.d(TAG, "Fetching from: "+urlstring); - url = new URL(urlstring); - - /* Open a connection to that URL. */ - URLConnection ucon = url.openConnection(); - /* - * Define InputStreams to read from the URLConnection. - */ - InputStream is = ucon.getInputStream(); - BufferedInputStream bis = new BufferedInputStream(is, 16000); - - ByteArrayBuffer baf = new ByteArrayBuffer(50); - int current = 0; - - /* - * Read bytes to the Buffer until there is nothing more to read(-1). - */ - while (mState == STATE_FETCHING - && (current = bis.read()) != -1) { - baf.append((byte) current); - - if (baf.length() % 2048 == 0) { - Message msg = mHandler.obtainMessage(); - // XXX do real length later - Log.d(TAG, baf.length() + " downloaded so far"); - msg.arg1 = baf.length() % 100; - mHandler.sendMessage(msg); - } - } - String content = new String(baf.toByteArray()); - Log.d(TAG, content); - - - } catch (IOException e) { - Log.e(TAG, "Error: ",e); - } - Message msg = mHandler.obtainMessage(); - msg.arg2 = STATE_DONE; - mHandler.sendMessage(msg); - } - - public void setState(int state) { - mState = state; - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/main/java/com/gimranov/zandy/app/MainActivity.java b/src/main/java/com/gimranov/zandy/app/MainActivity.java deleted file mode 100644 index 935dee0..0000000 --- a/src/main/java/com/gimranov/zandy/app/MainActivity.java +++ /dev/null @@ -1,479 +0,0 @@ -/******************************************************************************* - * 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 . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.ListView; -import android.widget.TextView; -import android.widget.Toast; - -import com.crashlytics.android.Crashlytics; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemAdapter; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; -import com.squareup.otto.Subscribe; - -import java.util.ArrayList; - -import oauth.signpost.OAuthProvider; -import oauth.signpost.basic.DefaultOAuthProvider; -import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; -import oauth.signpost.exception.OAuthCommunicationException; -import oauth.signpost.exception.OAuthExpectationFailedException; -import oauth.signpost.exception.OAuthMessageSignerException; -import oauth.signpost.exception.OAuthNotAuthorizedException; -import oauth.signpost.http.HttpParameters; - -public class MainActivity extends Activity implements OnClickListener { - private CommonsHttpOAuthConsumer httpOAuthConsumer; - private OAuthProvider httpOAuthProvider; - - private static final String TAG = "com.gimranov.zandy.app.MainActivity"; - - private static final String DEFAULT_SORT = "timestamp ASC, item_title COLLATE NOCASE"; - - static final int DIALOG_CHOOSE_COLLECTION = 1; - - private Database db; - private Bundle b = new Bundle(); - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Crashlytics.start(this); - - // Let items in on the fun - db = new Database(getBaseContext()); - - Intent intent = getIntent(); - String action = intent.getAction(); - if (action != null - && action.equals("android.intent.action.SEND") - && intent.getExtras() != null) { - // Browser sends us no data, just extras - Bundle extras = intent.getExtras(); - for (String s : extras.keySet()) { - try { - Log.d("TAG","Got extra: "+s +" => "+extras.getString(s)); - } catch (ClassCastException e) { - Log.e(TAG, "Not a string, it seems", e); - } - } - - Bundle b = new Bundle(); - b.putString("url", extras.getString("android.intent.extra.TEXT")); - b.putString("title", extras.getString("android.intent.extra.SUBJECT")); - this.b = b; - showDialog(DIALOG_CHOOSE_COLLECTION); - } - - setContentView(R.layout.main); - - Button collectionButton = (Button) findViewById(R.id.collectionButton); - collectionButton.setOnClickListener(this); - Button itemButton = (Button) findViewById(R.id.itemButton); - itemButton.setOnClickListener(this); - Button loginButton = (Button) findViewById(R.id.loginButton); - loginButton.setOnClickListener(this); - - if (ServerCredentials.check(getBaseContext())) { - setUpLoggedInUser(); - } - } - - @Override - public void onResume() { - Application.getInstance().getBus().register(this); - - Button loginButton = (Button) findViewById(R.id.loginButton); - - if (!ServerCredentials.check(getBaseContext())) { - loginButton.setText(getResources().getString(R.string.log_in)); - loginButton.setClickable(true); - } else { - refreshList(); - } - super.onResume(); - } - - @Override - protected void onPause() { - super.onPause(); - Application.getInstance().getBus().unregister(this); - } - - /** - * Refreshes the list view, safely if possible - */ - private void refreshList() { - ListView lv = ((ListView) findViewById(android.R.id.list)); - if (lv == null) return; - - ItemAdapter adapter = (ItemAdapter) lv.getAdapter(); - if (adapter != null) { - Cursor newCursor = getCursor(DEFAULT_SORT); - adapter.changeCursor(newCursor); - adapter.notifyDataSetChanged(); - } - } - - /** - * Implementation of the OnClickListener interface, to handle button events. - * - * Note: When adding a button, it needs to be added here, but the - * ClickListener needs to be set in the main onCreate(..) as well. - */ - public void onClick(View v) { - Log.d(TAG, "Click on: " + v.getId()); - if (v.getId() == R.id.collectionButton) { - Log.d(TAG, "Trying to start collection activity"); - Intent i = new Intent(this, CollectionActivity.class); - startActivity(i); - } else if (v.getId() == R.id.itemButton) { - Log.d(TAG, "Trying to start all-item activity"); - Intent i = new Intent(this, ItemActivity.class); - startActivity(i); - } else if (v.getId() == R.id.loginButton) { - Log.d(TAG, "Starting OAuth"); - new Thread(new Runnable() { - public void run() { - startOAuth(); - } - }).start(); - - } else { - Log.w(TAG, "Uncaught click on: " + v.getId()); - } - } - - /** - * Makes the OAuth call. The response on the callback is handled by the - * onNewIntent(..) method below. - * - * This will send the user to the OAuth server to get set up. - */ - protected void startOAuth() { - try { - this.httpOAuthConsumer = new CommonsHttpOAuthConsumer( - ServerCredentials.CONSUMERKEY, - ServerCredentials.CONSUMERSECRET); - this.httpOAuthProvider = new DefaultOAuthProvider( - ServerCredentials.OAUTHREQUEST, - ServerCredentials.OAUTHACCESS, - ServerCredentials.OAUTHAUTHORIZE); - - String authUrl; - authUrl = httpOAuthProvider.retrieveRequestToken(httpOAuthConsumer, - ServerCredentials.CALLBACKURL); - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))); - } catch (OAuthMessageSignerException e) { - toastError(e.getMessage()); - } catch (OAuthNotAuthorizedException e) { - toastError(e.getMessage()); - } catch (OAuthExpectationFailedException e) { - toastError(e.getMessage()); - } catch (OAuthCommunicationException e) { - toastError(e.getMessage()); - } - } - - private void toastError(final String message) { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG); - } - }); - } - - /** - * Receives intents that the app knows how to interpret. These will probably - * all be URIs with the protocol "zotero://". - * - * This is currently only used to receive OAuth responses, but it could be - * used with things like zotero://select and zotero://attachment in the - * future. - */ - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - Log.d(TAG, "Got new intent"); - - if (intent == null) return; - - // Here's what we do if we get a share request from the browser - String action = intent.getAction(); - if (action != null - && action.equals("android.intent.action.SEND") - && intent.getExtras() != null) { - // Browser sends us no data, just extras - Bundle extras = intent.getExtras(); - for (String s : extras.keySet()) { - try { - Log.d("TAG","Got extra: "+s +" => "+extras.getString(s)); - } catch (ClassCastException e) { - Log.e(TAG, "Not a string, it seems", e); - } - } - - Bundle b = new Bundle(); - b.putString("url", extras.getString("android.intent.extra.TEXT")); - b.putString("title", extras.getString("android.intent.extra.SUBJECT")); - this.b=b; - showDialog(DIALOG_CHOOSE_COLLECTION); - return; - } - - /* - * It's possible we've lost these to garbage collection, so we - * reinstantiate them if they turn out to be null at this point. - */ - if (this.httpOAuthConsumer == null) - this.httpOAuthConsumer = new CommonsHttpOAuthConsumer( - ServerCredentials.CONSUMERKEY, - ServerCredentials.CONSUMERSECRET); - if (this.httpOAuthProvider == null) - this.httpOAuthProvider = new DefaultOAuthProvider( - ServerCredentials.OAUTHREQUEST, - ServerCredentials.OAUTHACCESS, - ServerCredentials.OAUTHAUTHORIZE); - - /* - * Also double-check that intent isn't null, because something here - * caused a NullPointerException for a user. - */ - Uri uri; - uri = intent.getData(); - - if (uri != null) { - /* - * TODO The logic should have cases for the various things coming in - * on this protocol. - */ - final String verifier = uri - .getQueryParameter(oauth.signpost.OAuth.OAUTH_VERIFIER); - - new Thread(new Runnable() { - public void run() { - try { - /* - * Here, we're handling the callback from the completed OAuth. - * We don't need to do anything highly visible, although it - * would be nice to show a Toast or something. - */ - httpOAuthProvider.retrieveAccessToken( - httpOAuthConsumer, verifier); - HttpParameters params = httpOAuthProvider - .getResponseParameters(); - final String userID = params.getFirst("userID"); - Log.d(TAG, "uid: " + userID); - final String userKey = httpOAuthConsumer.getToken(); - Log.d(TAG, "ukey: " + userKey); - final String userSecret = httpOAuthConsumer.getTokenSecret(); - Log.d(TAG, "usecret: " + userSecret); - - runOnUiThread(new Runnable(){ - public void run(){ - /* - * These settings live in the Zotero preferences tree. - */ - SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); - SharedPreferences.Editor editor = settings.edit(); - // For Zotero, the key and secret are identical, it seems - editor.putString("user_key", userKey); - editor.putString("user_secret", userSecret); - editor.putString("user_id", userID); - - editor.commit(); - - setUpLoggedInUser(); - - doSync(); - - } - }); - } catch (OAuthMessageSignerException e) { - toastError(e.getMessage()); - } catch (OAuthNotAuthorizedException e) { - toastError(e.getMessage()); - } catch (OAuthExpectationFailedException e) { - toastError(e.getMessage()); - } catch (OAuthCommunicationException e) { - toastError("Error communicating with server. Check your time settings, network connectivity, and try again. OAuth error: " + e.getMessage()); - } - } - }).start(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - - // button doesn't make sense here. - menu.removeItem(R.id.do_new); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.do_sync: - return doSync(); - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - case R.id.do_search: - onSearchRequested(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private boolean doSync() { - if (!ServerCredentials.check(getApplicationContext())) { - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - Log.d(TAG, "Making sync request for all collections"); - ServerCredentials cred = new ServerCredentials(getBaseContext()); - APIRequest req = APIRequest.fetchCollections(cred); - new ZoteroAPITask(getBaseContext()).execute(req); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - return true; - } - - private void setUpLoggedInUser() { - Button loginButton = (Button) findViewById(R.id.loginButton); - - loginButton.setVisibility(View.GONE); - - ItemAdapter adapter = new ItemAdapter(this, getCursor(DEFAULT_SORT)); - ListView lv = ((ListView) findViewById(android.R.id.list)); - lv.setAdapter(adapter); - - lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { - public void onItemClick(AdapterView parent, View view, int position, long id) { - // If we have a click on an item, do something... - ItemAdapter adapter = (ItemAdapter) parent.getAdapter(); - Cursor cur = adapter.getCursor(); - // Place the cursor at the selected item - if (cur.moveToPosition(position)) { - // and load an activity for the item - Item item = Item.load(cur); - - Log.d(TAG, "Loading item data with key: "+item.getKey()); - // We create and issue a specified intent with the necessary data - Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); - i.putExtra("com.gimranov.zandy.app.itemDbId", item.dbId); - startActivity(i); - } else { - // failed to move cursor-- show a toast - TextView tvTitle = (TextView)view.findViewById(R.id.item_title); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.cant_open_item, tvTitle.getText()), - Toast.LENGTH_SHORT).show(); - } - } - }); - } - - public Cursor getCursor(String sortBy) { - Cursor cursor = db.query("items", Database.ITEMCOLS, null, null, null, null, sortBy, null); - if (cursor == null) { - Log.e(TAG, "cursor is null"); - } - return cursor; - } - - @Override - protected Dialog onCreateDialog(int id) { - final String url = b.getString("url"); - final String title = b.getString("title"); - AlertDialog dialog; - switch (id) { - case DIALOG_CHOOSE_COLLECTION: - AlertDialog.Builder builder = new AlertDialog.Builder(this); - // Now we're dealing with share link, it seems. - // For now, just add it to the main library-- we'd like to let the person choose a library, - // but not yet. - final ArrayList collections = ItemCollection.getCollections(db); - int size = collections.size(); - String[] collectionNames = new String[size]; - for (int i = 0; i < size; i++) { - collectionNames[i] = collections.get(i).getTitle(); - } - builder.setTitle(getResources().getString(R.string.choose_parent_collection)) - .setItems(collectionNames, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int pos) { - Item item = new Item(getBaseContext(), "webpage"); - item.save(db); - Log.d(TAG,"New item has key: "+item.getKey() + ", dbId: "+item.dbId); - Item.set(item.getKey(), "url", url, db); - Item.set(item.getKey(), "title", title, db); - Item.setTag(item.getKey(), null, "#added-by-zandy", 1, db); - collections.get(pos).add(item); - collections.get(pos).saveChildren(db); - Log.d(TAG, "Loading item data with key: "+item.getKey()); - // We create and issue a specified intent with the necessary data - Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); - i.putExtra("com.gimranov.zandy.app.itemDbId", item.dbId); - startActivity(i); - } - }); - dialog = builder.create(); - return dialog; - default: - Log.e(TAG, "Invalid dialog requested"); - return null; - } - } - - @Subscribe public void syncComplete(SyncEvent event) { - refreshList(); - } -} diff --git a/src/main/java/com/gimranov/zandy/app/Persistence.java b/src/main/java/com/gimranov/zandy/app/Persistence.java deleted file mode 100644 index 39432db..0000000 --- a/src/main/java/com/gimranov/zandy/app/Persistence.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.gimranov.zandy.app; - -import android.content.Context; -import android.content.SharedPreferences; - -import org.jetbrains.annotations.Nullable; - -public class Persistence { - private static final String TAG = Persistence.class.getCanonicalName(); - - private static final String FILE = "Persistence"; - - public static void write(String key, String value) { - SharedPreferences.Editor editor = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE).edit(); - editor.putString(key, value); - editor.commit(); - } - - @Nullable - public static String read(String key) { - SharedPreferences store = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE); - if (!store.contains(key)) return null; - - return store.getString(key, null); - } -} diff --git a/src/main/java/com/gimranov/zandy/app/SyncEvent.java b/src/main/java/com/gimranov/zandy/app/SyncEvent.java deleted file mode 100644 index 720424b..0000000 --- a/src/main/java/com/gimranov/zandy/app/SyncEvent.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.gimranov.zandy.app; - -public class SyncEvent { - private static final String TAG = SyncEvent.class.getCanonicalName(); - - public static final int COMPLETE_CODE = 1; - - public static final SyncEvent COMPLETE = new SyncEvent(COMPLETE_CODE); - - private int status; - - public SyncEvent(int status) { - this.status = status; - } - - public int getStatus() { - return status; - } -} diff --git a/src/main/java/com/gimranov/zandy/app/TagActivity.java b/src/main/java/com/gimranov/zandy/app/TagActivity.java deleted file mode 100644 index f37bbcc..0000000 --- a/src/main/java/com/gimranov/zandy/app/TagActivity.java +++ /dev/null @@ -1,264 +0,0 @@ -/******************************************************************************* - * 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 . - ******************************************************************************/ -package com.gimranov.zandy.app; - -import java.util.ArrayList; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.ListActivity; -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ListView; -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; - -/** - * This Activity handles displaying and editing tags. It works almost the same as - * ItemDataActivity, using a simple ArrayAdapter on Bundles with the tag info. - * - * @author ajlyon - * - */ -public class TagActivity extends ListActivity { - - private static final String TAG = "com.gimranov.zandy.app.TagActivity"; - - static final int DIALOG_TAG = 3; - static final int DIALOG_CONFIRM_NAVIGATE = 4; - - private Item item; - - private Database db; - - 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 = Item.load(itemKey, db); - this.item = item; - - this.setTitle(getResources().getString(R.string.tags_for_item, item.getTitle())); - - ArrayList rows = item.tagsToBundleArray(); - - /* - * We use the standard ArrayAdapter, passing in our data as a Bundle. - * Since it's no longer a simple TextView, we need to override getView, but - * we can do that anonymously. - */ - setListAdapter(new ArrayAdapter(this, R.layout.list_data, rows) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View row; - - // 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); - TextView tvContent = (TextView) row.findViewById(R.id.data_content); - - if (getItem(position).getInt("type") == 1) - tvLabel.setText(getResources().getString(R.string.tag_auto)); - else - tvLabel.setText(getResources().getString(R.string.tag_user)); - tvContent.setText(getItem(position).getString("tag")); - - return row; - } - }); - - ListView lv = getListView(); - lv.setTextFilterEnabled(true); - lv.setOnItemClickListener(new OnItemClickListener() { - // Warning here because Eclipse can't tell whether my ArrayAdapter is - // being used with the correct parametrization. - @SuppressWarnings("unchecked") - public void onItemClick(AdapterView parent, View view, int position, long id) { - // If we have a click on an entry, prompt to view that tag's items. - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - Bundle row = adapter.getItem(position); - removeDialog(DIALOG_CONFIRM_NAVIGATE); - TagActivity.this.b = row; - showDialog(DIALOG_CONFIRM_NAVIGATE); - } - }); - - /* - * On long click, we bring up an edit dialog. - */ - lv.setOnItemLongClickListener(new OnItemLongClickListener() { - /* - * Same annotation as in onItemClick(..), above. - */ - @SuppressWarnings("unchecked") - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - // If we have a long click on an entry, show an editor - ArrayAdapter adapter = (ArrayAdapter) parent.getAdapter(); - Bundle row = adapter.getItem(position); - - removeDialog(DIALOG_TAG); - TagActivity.this.b=row; - showDialog(DIALOG_TAG); - return true; - } - }); - - } - - @Override - public void onDestroy() { - if (db != null) db.close(); - super.onDestroy(); - } - - @Override - public void onResume() { - if (db == null) db = new Database(this); - super.onResume(); - } - - protected Dialog onCreateDialog(int id) { - @SuppressWarnings("unused") - final int type = b.getInt("type"); - final String tag = b.getString("tag"); - final String itemKey = b.getString("itemKey"); - AlertDialog dialog; - - switch (id) { - /* Simple editor for a single tag */ - case DIALOG_TAG: - final EditText input = new EditText(this); - input.setText(tag, BufferType.EDITABLE); - - dialog = new AlertDialog.Builder(this) - .setTitle(getResources().getString(R.string.tag_edit)) - .setView(input) - .setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { - @SuppressWarnings("unchecked") - public void onClick(DialogInterface dialog, int whichButton) { - Editable value = input.getText(); - Log.d(TAG, "Got tag: "+value.toString()); - Item.setTag(itemKey, tag, value.toString(), 0, db); - Item item = Item.load(itemKey, db); - Log.d(TAG, "Have JSON: "+item.getContent().toString()); - ArrayAdapter la = (ArrayAdapter) getListAdapter(); - la.clear(); - for (Bundle b : item.tagsToBundleArray()) { - la.add(b); - } - la.notifyDataSetChanged(); - } - }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // do nothing - } - }).create(); - return dialog; - case DIALOG_CONFIRM_NAVIGATE: - dialog = new AlertDialog.Builder(this) - .setTitle(getResources().getString(R.string.tag_view_confirm)) - .setPositiveButton(getResources().getString(R.string.tag_view), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - Intent i = new Intent(getBaseContext(), ItemActivity.class); - i.putExtra("com.gimranov.zandy.app.tag", tag); - startActivity(i); - } - }).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 boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.zotero_menu, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.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"); - new ZoteroAPITask(getBaseContext()).execute(APIRequest.update(this.item)); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_new: - Bundle row = new Bundle(); - row.putString("tag", ""); - row.putString("itemKey", this.item.getKey()); - row.putInt("type", 0); - removeDialog(DIALOG_TAG); - this.b = row; - showDialog(DIALOG_TAG); - return true; - case R.id.do_prefs: - startActivity(new Intent(this, SettingsActivity.class)); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/src/main/java/com/gimranov/zandy/app/Util.java b/src/main/java/com/gimranov/zandy/app/Util.java deleted file mode 100644 index 81346ce..0000000 --- a/src/main/java/com/gimranov/zandy/app/Util.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.gimranov.zandy.app; - -public class Util { - private static final String TAG = Util.class.getCanonicalName(); - - public static final String DOI_PREFIX = "http://dx.doi.org/"; - - public static String doiToUri(String doi) { - if (isDoi(doi)) { - return DOI_PREFIX + doi.replaceAll("^doi:", ""); - } - return doi; - } - - public static boolean isDoi(String doi) { - return (doi.startsWith("doi:") || doi.startsWith("10.")); - } -} diff --git a/src/main/java/com/gimranov/zandy/app/task/APIEvent.java b/src/main/java/com/gimranov/zandy/app/task/APIEvent.java deleted file mode 100644 index 21028dc..0000000 --- a/src/main/java/com/gimranov/zandy/app/task/APIEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.gimranov.zandy.app.task; - -public interface APIEvent { - public void onComplete(APIRequest request); - - public void onUpdate(APIRequest request); - - public void onError(APIRequest request, Exception exception); - public void onError(APIRequest request, int error); -} diff --git a/src/main/java/com/mattrobertson/zotable/app/Activity_Main.java b/src/main/java/com/mattrobertson/zotable/app/Activity_Main.java new file mode 100755 index 0000000..b6ac7b8 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Activity_Main.java @@ -0,0 +1,500 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; + +import android.preference.PreferenceManager; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.util.Log; +import android.view.Gravity; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; + +import oauth.signpost.OAuthProvider; +import oauth.signpost.basic.DefaultOAuthProvider; +import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; +import oauth.signpost.exception.OAuthCommunicationException; +import oauth.signpost.exception.OAuthExpectationFailedException; +import oauth.signpost.exception.OAuthMessageSignerException; +import oauth.signpost.exception.OAuthNotAuthorizedException; +import oauth.signpost.http.HttpParameters; + +/** + * Created by Matt on 7/22/2015. + */ +public class Activity_Main extends FragmentActivity { + + private static final String TAG = "Activity_Main"; + + private CommonsHttpOAuthConsumer httpOAuthConsumer; + private OAuthProvider httpOAuthProvider; + + private DrawerLayout drawerLayout; + private ActionBarDrawerToggle drawerToggle; + + private Button btnLogin; + private LinearLayout conItems, conCollections, conTags, conFavorites; + private TextView tvItemsLabel, tvCollectionsLabel, tvTagsLabel, tvFavsLabel, tvSettingsLabel; + private ImageView imgItems, imgCollections, imgTags, imgFavs, imgSettings; + + private Fragment fragment; + + private Bundle b = new Bundle(); + + private String curTitle = "All Items"; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + drawerLayout = (DrawerLayout)findViewById(R.id.drawer_layout); + + drawerToggle = new ActionBarDrawerToggle( + this, + drawerLayout, + R.string.drawer_open, + R.string.drawer_close + ) { + public void onDrawerClosed(View view) { + super.onDrawerClosed(view); + + if (getActionBar() != null) + getActionBar().setTitle(curTitle); + + invalidateOptionsMenu(); + } + + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + + if (getActionBar() != null) + getActionBar().setTitle(getResources().getString(R.string.app_name)); + + invalidateOptionsMenu(); + } + }; + + LinearLayout leftDrawerFrame = (LinearLayout)findViewById(R.id.left_drawer); + leftDrawerFrame.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // Do nothing -- intercepts touches to background listview + } + }); + + btnLogin = (Button)findViewById(R.id.btnLogin); + + btnLogin.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new Thread(new Runnable() { + public void run() { + startOAuth(); + } + }).start(); + } + }); + + // If logged in, hide login button -- if not, open drawer for user to log in + if (ServerCredentials.check(getBaseContext())) { + btnLogin.setVisibility(View.GONE); + + RelativeLayout conSearch = (RelativeLayout)findViewById(R.id.conSearch); + conSearch.setVisibility(View.VISIBLE); + final EditText etSearch = (EditText)findViewById(R.id.etSearch); + + Button btnDoSearch = (Button)findViewById(R.id.btnDoSearch); + btnDoSearch.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent iSearch = new Intent(Activity_Main.this, SearchActivity.class); + iSearch.putExtra("query",etSearch.getText().toString()); + startActivity(iSearch); + } + }); + } + else { + drawerLayout.openDrawer(Gravity.START); + } + + conItems = (LinearLayout)findViewById(R.id.conItems); + conCollections = (LinearLayout)findViewById(R.id.conCollections); + conTags = (LinearLayout)findViewById(R.id.conTags); + conFavorites = (LinearLayout)findViewById(R.id.conFavorites); + + tvItemsLabel = (TextView)findViewById(R.id.tvItemsLabel); + tvCollectionsLabel = (TextView)findViewById(R.id.tvCollectionsLabel); + tvTagsLabel = (TextView)findViewById(R.id.tvTagsLabel); + tvFavsLabel = (TextView)findViewById(R.id.tvFavoritesLabel); + tvSettingsLabel = (TextView)findViewById(R.id.tvSettingsLabel); + + DrawerNavListener navListener = new DrawerNavListener(); + + conItems.setOnClickListener(navListener); + conCollections.setOnClickListener(navListener); + conTags.setOnClickListener(navListener); + conFavorites.setOnClickListener(navListener); + tvSettingsLabel.setOnClickListener(navListener); + + imgItems = (ImageView)findViewById(R.id.imgItemsIcon); + imgCollections = (ImageView)findViewById(R.id.imgCollectionsIcon); + imgTags = (ImageView)findViewById(R.id.imgTagsIcon); + imgFavs = (ImageView)findViewById(R.id.imgFavoritesIcon); + + fragment = new Fragment_Items(); + + // Insert the fragment by replacing any existing fragment + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + + if (getActionBar() != null) { + getActionBar().setTitle(curTitle); + + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); + } + + // Set the drawer toggle as the DrawerListener + drawerLayout.setDrawerListener(drawerToggle); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + // Sync the toggle state after onRestoreInstanceState has occurred. + drawerToggle.syncState(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + drawerToggle.onConfigurationChanged(newConfig); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Pass the event to ActionBarDrawerToggle, if it returns + // true, then it has handled the app icon touch event + if (drawerToggle.onOptionsItemSelected(item)) { + return true; + } + // Handle your other action bar items... + + return super.onOptionsItemSelected(item); + } + + /** + * Makes the OAuth call. The response on the callback is handled by the + * onNewIntent(..) method below. + * + * This will send the user to the OAuth server to get set up. + */ + protected void startOAuth() { + try { + this.httpOAuthConsumer = new CommonsHttpOAuthConsumer( + ServerCredentials.CONSUMERKEY, + ServerCredentials.CONSUMERSECRET); + this.httpOAuthProvider = new DefaultOAuthProvider( + ServerCredentials.OAUTHREQUEST, + ServerCredentials.OAUTHACCESS, + ServerCredentials.OAUTHAUTHORIZE); + + String authUrl; + authUrl = httpOAuthProvider.retrieveRequestToken(httpOAuthConsumer, + ServerCredentials.CALLBACKURL); + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(authUrl))); + } catch (OAuthMessageSignerException e) { + toastError(e.getMessage()); + } catch (OAuthNotAuthorizedException e) { + toastError(e.getMessage()); + } catch (OAuthExpectationFailedException e) { + toastError(e.getMessage()); + } catch (OAuthCommunicationException e) { + toastError(e.getMessage()); + } + } + + private void toastError(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(Activity_Main.this, message, Toast.LENGTH_LONG); + } + }); + } + + /** + * Receives intents that the app knows how to interpret. These will probably + * all be URIs with the protocol "zotable://". + * + * This is currently only used to receive OAuth responses, but it could be + * used with things like zotable://select and zotable://attachment in the + * future. + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + Log.d(TAG, "Got new intent"); + + if (intent == null) return; + + // Here's what we do if we get a share request from the browser + String action = intent.getAction(); + if (action != null + && action.equals("android.intent.action.SEND") + && intent.getExtras() != null) { + // Browser sends us no data, just extras + Bundle extras = intent.getExtras(); + for (String s : extras.keySet()) { + try { + Log.d("TAG","Got extra: "+s +" => "+extras.getString(s)); + } catch (ClassCastException e) { + Log.e(TAG, "Not a string, it seems", e); + } + } + + Bundle b = new Bundle(); + b.putString("url", extras.getString("android.intent.extra.TEXT")); + b.putString("title", extras.getString("android.intent.extra.SUBJECT")); + + this.b=b; + + // showDialog(DIALOG_CHOOSE_COLLECTION); -- no longer needed(?) + return; + } + + /* + * It's possible we've lost these to garbage collection, so we + * reinstantiate them if they turn out to be null at this point. + */ + if (this.httpOAuthConsumer == null) + this.httpOAuthConsumer = new CommonsHttpOAuthConsumer( + ServerCredentials.CONSUMERKEY, + ServerCredentials.CONSUMERSECRET); + if (this.httpOAuthProvider == null) + this.httpOAuthProvider = new DefaultOAuthProvider( + ServerCredentials.OAUTHREQUEST, + ServerCredentials.OAUTHACCESS, + ServerCredentials.OAUTHAUTHORIZE); + + /* + * Also double-check that intent isn't null, because something here + * caused a NullPointerException for a user. + */ + Uri uri; + uri = intent.getData(); + + if (uri != null) { + /* + * TODO The logic should have cases for the various things coming in + * on this protocol. + */ + final String verifier = uri + .getQueryParameter(oauth.signpost.OAuth.OAUTH_VERIFIER); + + new Thread(new Runnable() { + public void run() { + try { + /* + * Here, we're handling the callback from the completed OAuth. + * We don't need to do anything highly visible, although it + * would be nice to show a Toast or something. + */ + httpOAuthProvider.retrieveAccessToken( + httpOAuthConsumer, verifier); + HttpParameters params = httpOAuthProvider + .getResponseParameters(); + final String userID = params.getFirst("userID"); + Log.d(TAG, "uid: " + userID); + final String userKey = httpOAuthConsumer.getToken(); + Log.d(TAG, "ukey: " + userKey); + final String userSecret = httpOAuthConsumer.getTokenSecret(); + Log.d(TAG, "usecret: " + userSecret); + + runOnUiThread(new Runnable(){ + public void run(){ + /* + * These settings live in the Zotero preferences_old tree. + */ + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(Activity_Main.this); + SharedPreferences.Editor editor = settings.edit(); + // For Zotero, the key and secret are identical, it seems + editor.putString("user_key", userKey); + editor.putString("user_secret", userSecret); + editor.putString("user_id", userID); + + editor.commit(); + + // setUpLoggedInUser(); -- no longer needed(?) + + // If logged in & Items fragment has been initialized, sync items through + // fragment's sync method + if (fragment != null) { + if (fragment.getClass() == Fragment_Items.class) { + ((Fragment_Items)fragment).sync(); + } + } + + // Close the nav drawer & hide the login button + drawerLayout.closeDrawers(); + btnLogin.setVisibility(View.GONE); + } + }); + } catch (OAuthMessageSignerException e) { + toastError(e.getMessage()); + } catch (OAuthNotAuthorizedException e) { + toastError(e.getMessage()); + } catch (OAuthExpectationFailedException e) { + toastError(e.getMessage()); + } catch (OAuthCommunicationException e) { + toastError("Error communicating with server. Check your time settings, network connectivity, and try again. OAuth error: " + e.getMessage()); + } + } + }).start(); + } + } + + private class DrawerNavListener implements View.OnClickListener { + @Override + public void onClick(View v) { + Fragment fragment = new Fragment_Items(); + + if (v.getId() == R.id.conItems) { + fragment = new Fragment_Items(); + curTitle = "All Items"; + + // Update Drawer appearance + tvItemsLabel.setTypeface(null, Typeface.BOLD); + tvItemsLabel.setTextColor(getResources().getColor(R.color.primary)); + tvCollectionsLabel.setTypeface(null, Typeface.NORMAL); + tvCollectionsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvTagsLabel.setTypeface(null, Typeface.NORMAL); + tvTagsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvFavsLabel.setTypeface(null, Typeface.NORMAL); + tvFavsLabel.setTextColor(getResources().getColor(R.color.text_color)); + + imgItems.setImageResource(R.drawable.ic_item_blue); + imgCollections.setImageResource(R.drawable.ic_collection_black); + imgTags.setImageResource(R.drawable.ic_tag_black); + imgFavs.setImageResource(R.drawable.ic_favorites_black); + + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + } + else if (v.getId() == R.id.conCollections) { + fragment = new Fragment_Collections(); + curTitle = "Collections"; + + // Update Drawer appearance + tvItemsLabel.setTypeface(null, Typeface.NORMAL); + tvItemsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvCollectionsLabel.setTypeface(null, Typeface.BOLD); + tvCollectionsLabel.setTextColor(getResources().getColor(R.color.primary)); + tvTagsLabel.setTypeface(null, Typeface.NORMAL); + tvTagsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvFavsLabel.setTypeface(null, Typeface.NORMAL); + tvFavsLabel.setTextColor(getResources().getColor(R.color.text_color)); + + imgItems.setImageResource(R.drawable.ic_item_black); + imgCollections.setImageResource(R.drawable.ic_collection_blue); + imgTags.setImageResource(R.drawable.ic_tag_black); + imgFavs.setImageResource(R.drawable.ic_favorites_black); + + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + } + else if (v.getId() == R.id.conTags) { + fragment = new Fragment_Tags(); + curTitle = "Tags"; + + // Update Drawer appearance + tvItemsLabel.setTypeface(null, Typeface.NORMAL); + tvItemsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvCollectionsLabel.setTypeface(null, Typeface.NORMAL); + tvCollectionsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvTagsLabel.setTypeface(null, Typeface.BOLD); + tvTagsLabel.setTextColor(getResources().getColor(R.color.primary)); + tvFavsLabel.setTypeface(null, Typeface.NORMAL); + tvFavsLabel.setTextColor(getResources().getColor(R.color.text_color)); + + imgItems.setImageResource(R.drawable.ic_item_black); + imgCollections.setImageResource(R.drawable.ic_collection_black); + imgTags.setImageResource(R.drawable.ic_tag_blue); + imgFavs.setImageResource(R.drawable.ic_favorites_black); + + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + } + else if (v.getId() == R.id.conFavorites) { + fragment = new Fragment_Favorites(); + curTitle = "Favorites"; + + // Update Drawer appearance + tvItemsLabel.setTypeface(null, Typeface.NORMAL); + tvItemsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvCollectionsLabel.setTypeface(null, Typeface.NORMAL); + tvCollectionsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvTagsLabel.setTypeface(null, Typeface.NORMAL); + tvTagsLabel.setTextColor(getResources().getColor(R.color.text_color)); + tvFavsLabel.setTypeface(null, Typeface.BOLD); + tvFavsLabel.setTextColor(getResources().getColor(R.color.primary)); + + imgItems.setImageResource(R.drawable.ic_item_black); + imgCollections.setImageResource(R.drawable.ic_collection_black); + imgTags.setImageResource(R.drawable.ic_tag_black); + imgFavs.setImageResource(R.drawable.ic_favorites_blue); + + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager.beginTransaction() + .replace(R.id.content_frame, fragment) + .commit(); + } + else if (v.getId() == R.id.tvSettingsLabel) { + Intent i = new Intent(getBaseContext(), Activity_Preference.class); + startActivity(i); + } + + drawerLayout.closeDrawers(); + } + } +} diff --git a/src/main/java/com/mattrobertson/zotable/app/Activity_Preference.java b/src/main/java/com/mattrobertson/zotable/app/Activity_Preference.java new file mode 100755 index 0000000..ed5f7d7 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Activity_Preference.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.database.Cursor; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.data.TagAdapter; + +public class Activity_Preference extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getActionBar().setDisplayHomeAsUpEnabled(true); + + // Display the fragment as the main content. + getFragmentManager().beginTransaction() + .replace(android.R.id.content, new ZotPrefFragment()) + .commit(); + } + + public static class ZotPrefFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Load the preferences from an XML resource + addPreferencesFromResource(R.xml.settings); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + LinearLayout v = (LinearLayout) super.onCreateView(inflater, container, savedInstanceState); + + Button btnClearDb = new Button(getActivity().getApplicationContext()); + btnClearDb.setText("Clear local database"); + v.addView(btnClearDb); + + btnClearDb.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + + AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(getActivity(),R.style.AppTheme)); + builder.setTitle(getResources().getString(R.string.settings_reset_database_warning)) + .setPositiveButton(getResources().getString(R.string.menu_delete), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Database db = new Database(getActivity().getBaseContext()); + db.resetAllData(); + getActivity().finish(); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }); + AlertDialog dialog = builder.show(); + + int textViewId = dialog.getContext().getResources().getIdentifier("android:id/alertTitle", null, null); + TextView tv = (TextView) dialog.findViewById(textViewId); + tv.setTextColor(getResources().getColor(R.color.white)); + } + }); + + return v; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/gimranov/zandy/app/AmazonZxingGlue.java b/src/main/java/com/mattrobertson/zotable/app/AmazonZxingGlue.java old mode 100644 new mode 100755 similarity index 64% rename from src/main/java/com/gimranov/zandy/app/AmazonZxingGlue.java rename to src/main/java/com/mattrobertson/zotable/app/AmazonZxingGlue.java index 19fdf60..bcc833b --- a/src/main/java/com/gimranov/zandy/app/AmazonZxingGlue.java +++ b/src/main/java/com/mattrobertson/zotable/app/AmazonZxingGlue.java @@ -1,4 +1,20 @@ -package com.gimranov.zandy.app; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; import android.app.AlertDialog; import android.content.ActivityNotFoundException; diff --git a/src/main/java/com/mattrobertson/zotable/app/Application.java b/src/main/java/com/mattrobertson/zotable/app/Application.java new file mode 100755 index 0000000..ffac725 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Application.java @@ -0,0 +1,44 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import com.squareup.otto.Bus; + +public class Application extends android.app.Application { + private static final String TAG = Application.class.getCanonicalName(); + + private static Application instance; + + private Bus bus; + + @Override + public void onCreate() { + super.onCreate(); + + bus = new Bus(); + + instance = this; + } + + public Bus getBus() { + return bus; + } + + public static Application getInstance() { + return instance; + } +} diff --git a/src/main/java/com/gimranov/zandy/app/AttachmentActivity.java b/src/main/java/com/mattrobertson/zotable/app/AttachmentActivity.java old mode 100644 new mode 100755 similarity index 93% rename from src/main/java/com/gimranov/zandy/app/AttachmentActivity.java rename to src/main/java/com/mattrobertson/zotable/app/AttachmentActivity.java index 371b030..4aea8ed --- a/src/main/java/com/gimranov/zandy/app/AttachmentActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/AttachmentActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.app.AlertDialog; import android.app.Dialog; @@ -50,13 +50,11 @@ import android.widget.TextView.BufferType; import android.widget.Toast; -import com.crashlytics.android.Crashlytics; -import com.gimranov.zandy.app.data.Attachment; -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; -import com.gimranov.zandy.app.webdav.WebDavTrust; +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.webdav.WebDavTrust; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -89,7 +87,7 @@ */ public class AttachmentActivity extends ListActivity { - private static final String TAG = "com.gimranov.zandy.app.AttachmentActivity"; + private static final String TAG = "AttachmentActivity"; static final int DIALOG_CONFIRM_NAVIGATE = 4; static final int DIALOG_FILE_PROGRESS = 6; @@ -119,7 +117,7 @@ public void onCreate(Bundle savedInstanceState) { db = new Database(this); /* Get the incoming data from the calling activity */ - final String itemKey = getIntent().getStringExtra("com.gimranov.zandy.app.itemKey"); + final String itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemKey"); Item item = Item.load(itemKey, db); this.item = item; @@ -199,7 +197,7 @@ public boolean onItemLongClick(AdapterView parent, View view, int position, l if (row.content.has("note")) { Log.d(TAG, "Trying to start note view activity for: " + row.key); Intent i = new Intent(getBaseContext(), NoteActivity.class); - i.putExtra("com.gimranov.zandy.app.attKey", row.key);//row.content.optString("note", "")); + i.putExtra("com.mattrobertson.zotable.app.attKey", row.key);//row.content.optString("note", "")); startActivity(i); } return true; @@ -631,7 +629,7 @@ protected void afterWrite(int n) throws IOException { ServerCredentials.sCacheDir.mkdirs(); } - File tmpFile = File.createTempFile("zandy", ".zip", ServerCredentials.sCacheDir); + File tmpFile = File.createTempFile("zotable", ".zip", ServerCredentials.sCacheDir); FileUtils.copyFile(file, tmpFile); //noinspection ResultOfMethodCallIgnored @@ -688,9 +686,7 @@ protected void afterWrite(int n) throws IOException { Log.d(TAG, "Skipping file: " + name); } } catch (IllegalArgumentException e) { - Crashlytics.logException(new Throwable("b64 " + name64, e)); } catch (NegativeArraySizeException e) { - Crashlytics.logException(new Throwable("b64 " + name64, e)); } } while (entries.hasMoreElements()); @@ -708,7 +704,6 @@ protected void afterWrite(int n) throws IOException { + " sec"); } catch (IOException e) { Log.e(TAG, "Error: ", e); - Crashlytics.logException(e); toastError(R.string.attachment_download_failed, e.getMessage()); } @@ -755,24 +750,6 @@ public boolean onOptionsItemSelected(MenuItem item) { Bundle b = new Bundle(); // Handle item selection switch (item.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"); - new ZoteroAPITask(getBaseContext()).execute(APIRequest.update(this.item)); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - - return true; - case R.id.do_new: - b.putString("itemKey", this.item.getKey()); - b.putString("mode", "new"); - removeDialog(DIALOG_NOTE); - showDialog(DIALOG_NOTE); - return true; case R.id.do_prefs: startActivity(new Intent(this, SettingsActivity.class)); return true; diff --git a/src/main/java/com/mattrobertson/zotable/app/CollectionActivity.java b/src/main/java/com/mattrobertson/zotable/app/CollectionActivity.java new file mode 100755 index 0000000..64fc5b1 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/CollectionActivity.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.mattrobertson.zotable.app.data.CollectionAdapter; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIRequest; + +public class CollectionActivity extends Activity { + + private static final String TAG = "CollectionActivity"; + private ItemCollection collection; + private Database db; + + ListView lvCollections; + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG,"received message: "+msg.arg1); + refreshView(); + + if (msg.arg1 == APIRequest.UPDATED_DATA) { + //refreshView(); + return; + } + + if (msg.arg1 == APIRequest.QUEUED_MORE) { + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.sync_queued_more, msg.arg2), + Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.BATCH_DONE) { + Application.getInstance().getBus().post(SyncEvent.COMPLETE); + //Toast.makeText(getApplicationContext(),getResources().getString(R.string.sync_complete),Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.ERROR_UNKNOWN) { + String desc = (msg.arg2 == 0) ? "" : " ("+msg.arg2+")"; + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.sync_error)+desc, + Toast.LENGTH_SHORT).show(); + return; + } + } + }; + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + CollectionAdapter adapter = (CollectionAdapter) (lvCollections.getAdapter()); + Cursor newCursor = (collection == null) ? create() : create(collection); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getActionBar().setDisplayHomeAsUpEnabled(true); + + db = new Database(this); + + setContentView(R.layout.collections); + + lvCollections = (ListView)findViewById(R.id.lvCollections); + + CollectionAdapter collectionAdapter; + + String collectionKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.collectionKey"); + if (collectionKey != null) { + ItemCollection coll = ItemCollection.load(collectionKey, db); + // We set the title to the current collection + this.collection = coll; + this.setTitle(coll.getTitle()); + collectionAdapter = new CollectionAdapter(this, create(coll)); + } else { + this.setTitle(getResources().getString(R.string.collections)); + collectionAdapter = new CollectionAdapter(this, create()); + } + + lvCollections.setAdapter(collectionAdapter); + + lvCollections.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + + CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and replace the cursor with one for the selected collection + ItemCollection coll = ItemCollection.load(cur); + if (coll != null && coll.getKey() != null) { + Intent i = new Intent(getBaseContext(), ItemActivity.class); + if (coll.getSize() == 0) { + // Send a message that we need to refresh the collection + i.putExtra("com.mattrobertson.zotable.app.rerequest", true); + } + i.putExtra("com.mattrobertson.zotable.app.collectionKey", coll.getKey()); + startActivity(i); + } else { + // collection loaded was null. why? + Log.d(TAG, "Failed loading items for collection at position: " + position); + return; + } + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.collection_title); + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.collection_cant_open, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + return; + } + return; + } + }); + } + + protected void onResume() { + CollectionAdapter adapter = (CollectionAdapter)(lvCollections.getAdapter()); + // XXX This may be too agressive-- fix if causes issues + Cursor newCursor = (collection == null) ? create() : create(collection); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + if (db == null) db = new Database(this); + super.onResume(); + } + + public void onDestroy() { + CollectionAdapter adapter = (CollectionAdapter)(lvCollections.getAdapter()); + Cursor cur = adapter.getCursor(); + if(cur != null) cur.close(); + if (db != null) db.close(); + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.zotero_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * Gives a cursor for top-level collections + * @return + */ + public Cursor create() { + String[] args = { "false" }; + Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + + /** + * Gives a cursor for child collections of a given parent + * @param parent + * @return + */ + public Cursor create(ItemCollection parent) { + String[] args = { parent.getKey() }; + Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + +} diff --git a/src/main/java/com/mattrobertson/zotable/app/CollectionMembershipActivity.java b/src/main/java/com/mattrobertson/zotable/app/CollectionMembershipActivity.java new file mode 100755 index 0000000..c5ce361 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/CollectionMembershipActivity.java @@ -0,0 +1,304 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import java.util.ArrayList; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; + +/** + * This Activity handles displaying and editing collection memberships for a + * given item. + * + * @author ajlyon + * + */ +public class CollectionMembershipActivity extends Activity { + + private static final String TAG = "CollMembershipActivity"; + + private String itemKey; + private String itemTitle; + private Item item; + + ListView lvCollections; + ArrayList rows; + CollectionMembershipAdapter adapter; + + private Database db; + + /** + * For API <= 7, where we can't pass Bundles to dialogs + */ + private Bundle b = new Bundle(); + + ArrayList arrCollKeys,arrCollNames; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.coll_mem_activity); + + getActionBar().setDisplayHomeAsUpEnabled(true); + + db = new Database(this); + + /* Get the incoming data from the calling activity */ + itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemKey"); + item = Item.load(itemKey, db); + if (item == null) { + Log.e(TAG, "Null item for key: "+itemKey); + finish(); + } + itemTitle = item.getTitle(); + + this.setTitle(getResources().getString(R.string.collections_for_item, itemTitle)); + + lvCollections = (ListView)findViewById(R.id.lvCollections); + + rows = ItemCollection.getCollections(item, db); + + adapter = new CollectionMembershipAdapter(rows); + + lvCollections.setAdapter(adapter); + + // Fill Collection arrays - used to add to collection. Pre-pop for better performance. + new Thread(new Runnable() { + public void run() { + fillCollsArr(); + } + }).start(); + + FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.btnFab); + + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + + AlertDialog.Builder builder = new AlertDialog.Builder(CollectionMembershipActivity.this); + builder.setTitle("Add to collection"); + + ListView lvColls = new ListView(CollectionMembershipActivity.this); + + String[] templ = {}; + String[] stringArray = arrCollNames.toArray(templ); + + ArrayAdapter modeAdapter = new ArrayAdapter<>(CollectionMembershipActivity.this, android.R.layout.simple_list_item_1, android.R.id.text1, stringArray); + lvColls.setAdapter(modeAdapter); + + builder.setView(lvColls); + final Dialog dialog = builder.create(); + + lvColls.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + //Toast.makeText(getBaseContext(),"Clicked "+arrCollNames.get(position).trim()+" ("+arrCollKeys.get(position)+")",Toast.LENGTH_LONG).show(); + + ItemCollection coll = ItemCollection.load(arrCollKeys.get(position), db); + coll.add(item, false, db); + coll.saveChildren(db); + + refreshData(); + + dialog.dismiss(); + } + }); + + dialog.show(); + } + }); + } + + @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 onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case android.R.id.home: + onBackPressed(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void refreshData() { + rows = ItemCollection.getCollections(item, db); + + adapter.setData(rows); + + adapter.notifyDataSetChanged(); + lvCollections.invalidate(); + } + + public void fillCollsArr() { + arrCollKeys = new ArrayList<>(); + arrCollNames = new ArrayList<>(); + + String[] args = { "false" }; + Cursor rootCursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + + if (rootCursor == null) + return; + + rootCursor.moveToPrevious(); + + while (rootCursor.moveToNext()) { + String collName = rootCursor.getString(rootCursor.getColumnIndex("collection_name")); + String collKey = rootCursor.getString(rootCursor.getColumnIndex("collection_key")); + + arrCollKeys.add(collKey); + arrCollNames.add(collName); + + getChildren(collKey, 1); + } + + rootCursor.close(); + } + + private void getChildren(String parentKey, int level) { + String[] args = { parentKey }; + Cursor childCursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + + if (childCursor == null) + return; + + childCursor.moveToPrevious(); + + while (childCursor.moveToNext()) { + String collName = childCursor.getString(childCursor.getColumnIndex("collection_name")); + String collKey = childCursor.getString(childCursor.getColumnIndex("collection_key")); + + arrCollKeys.add(collKey); + + // Period in front of name designates a level of depth in hierarchy + for (int i=0; i mList; + + public CollectionMembershipAdapter(ArrayList list) { + mList = list; + } + + public void setData(ArrayList newColl) { + mList = newColl; + } + + @Override + public int getCount() { + return mList.size(); + } + @Override + public Object getItem(int pos) { + return mList.get(pos); + } + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row; + + // We are reusing views, but we need to initialize it if null + if (null == convertView) { + LayoutInflater inflater = getLayoutInflater(); + row = inflater.inflate(R.layout.list_coll_mem, null); + } else { + row = convertView; + } + + final String collKey = ((ItemCollection)getItem(position)).getKey(); + final String title = ((ItemCollection)getItem(position)).getTitle(); + + TextView tvName = (TextView) row.findViewById(R.id.tvCollName); + tvName.setText(title); + + ImageView imgRemove = (ImageView) row.findViewById(R.id.imgRemove); + imgRemove.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + CollectionMembershipActivity.this.runOnUiThread(new Runnable() { + public void run() { + ItemCollection col = ItemCollection.load(collKey,db); + col.remove(item,false,db); + + refreshData(); + } + }); + } + }); + + return row; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/CreatorActivity.java b/src/main/java/com/mattrobertson/zotable/app/CreatorActivity.java new file mode 100755 index 0000000..f9aca61 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/CreatorActivity.java @@ -0,0 +1,369 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import java.util.ArrayList; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.mattrobertson.zotable.app.data.Creator; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; + +public class CreatorActivity extends Activity { + + private static final String TAG = "CreatorActivity"; + + static final int DIALOG_CREATOR = 3; + + ListView lvCreators; + CreatorAdapter adapter; + + public Item item; + + private Database db; + + /** + * For API <= 7, to pass bundles to activities + */ + private Bundle b = new Bundle(); + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.creator_activity); + + db = new Database(this); + + lvCreators = (ListView) findViewById(R.id.lvCreators); + + /* Get the incoming data from the calling activity */ + String itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemKey"); + Item item = Item.load(itemKey, db); + this.item = item; + + this.setTitle("Creators for " + item.getTitle()); + + ArrayList rows = item.creatorsToBundleArray(); + adapter = new CreatorAdapter(rows); + lvCreators.setAdapter(adapter); + + lvCreators.setTextFilterEnabled(true); + lvCreators.setOnItemClickListener(new OnItemClickListener() { + // Warning here because Eclipse can't tell whether my ArrayAdapter is + // being used with the correct parametrization. + @SuppressWarnings("unchecked") + public void onItemClick(AdapterView parent, View view, int position, long id) { + // If we have a click on an entry, do something... + CreatorAdapter adapter = (CreatorAdapter) parent.getAdapter(); + Bundle row = (Bundle) (adapter.getItem(position)); + + CreatorActivity.this.b = row; + removeDialog(DIALOG_CREATOR); + showDialog(DIALOG_CREATOR); + } + }); + + FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.btnFab); + + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + //Toast.makeText(CreatorActivity.this,"Adding",Toast.LENGTH_SHORT).show(); + + // Set local Bundle b to null to show we are adding a new Creator + CreatorActivity.this.b = null; + removeDialog(DIALOG_CREATOR); + showDialog(DIALOG_CREATOR); + } + }); + } + + @Override + public void onDestroy() { + if (db != null) db.close(); + super.onDestroy(); + } + + @Override + public void onResume() { + if (db == null) db = new Database(this); + super.onResume(); + } + + public void refreshData() { + ArrayList rows = item.creatorsToBundleArray(); + adapter = new CreatorAdapter(rows); + lvCreators.setAdapter(adapter); + + adapter.notifyDataSetChanged(); + lvCreators.invalidate(); + } + + protected Dialog onCreateDialog(int id) { + + final String creatorType; + final int creatorPosition; + + String name, firstName, lastName; + + if (b != null) { + creatorType = b.getString("creatorType"); + creatorPosition = b.getInt("position"); + name = b.getString("name"); + firstName = b.getString("firstName"); + lastName = b.getString("lastName"); + } + else { + creatorType = ""; + creatorPosition = -1; + firstName = "First"; + lastName = "Last"; + name = firstName + " " + lastName; + } + + switch (id) { + /* Editor for a creator + */ + case DIALOG_CREATOR: + AlertDialog.Builder builder; + AlertDialog dialog; + + LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); + final View layout = inflater.inflate(R.layout.creator_dialog, (ViewGroup) findViewById(R.id.layout_root)); + + TextView textName = (TextView) layout.findViewById(R.id.creator_name); + TextView textFN = (TextView) layout.findViewById(R.id.creator_firstName); + TextView textLN = (TextView) layout.findViewById(R.id.creator_lastName); + + textName.setText(name); + textFN.setText(firstName); + textLN.setText(lastName); + + CheckBox mode = (CheckBox) layout.findViewById(R.id.creator_mode); + mode.setChecked((firstName == null || firstName.equals("")) + && (lastName == null || lastName.equals("")) + && (lastName != null && !name.equals(""))); + + // Set up the adapter to get creator types + String[] types = Item.localizedCreatorTypesForItemType(item.getType()); + + // what position are we? + int arrPosition = 0; + String localType = ""; + + if (creatorType != null) { + localType = Item.localizedStringForString(creatorType); + } + else { // We default to the first possibility when none specified + localType = Item.localizedStringForString(Item.creatorTypesForItemType(item.getType())[0]); + } + + // Set spinner to display item type + for (int i = 0; i < types.length; i++) { + if (types[i].equals(localType)) { + arrPosition = i; + break; + } + } + + ArrayAdapter adapter = new ArrayAdapter(this,android.R.layout.simple_spinner_item, types); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + Spinner spinner = (Spinner) layout.findViewById(R.id.creator_type); + spinner.setAdapter(adapter); + + spinner.setSelection(arrPosition); + builder = new AlertDialog.Builder(this); + builder.setView(layout); + builder.setPositiveButton(getResources().getString(R.string.ok), new OnClickListener() { + @SuppressWarnings("unchecked") + public void onClick(DialogInterface dialog, int whichButton) { + Creator c; + TextView textName = (TextView) layout.findViewById(R.id.creator_name); + TextView textFN = (TextView) layout.findViewById(R.id.creator_firstName); + TextView textLN = (TextView) layout.findViewById(R.id.creator_lastName); + Spinner spinner = (Spinner) layout.findViewById(R.id.creator_type); + CheckBox mode = (CheckBox) layout.findViewById(R.id.creator_mode); + + String selected = (String) spinner.getSelectedItem(); + // Set up the adapter to get creator types + String[] types = Item.localizedCreatorTypesForItemType(item.getType()); + + // what position are we? + int typePos = 0; + for (int i = 0; i < types.length; i++) { + if (types[i].equals(selected)) { + typePos = i; + break; + } + } + + String realType = Item.creatorTypesForItemType(item.getType())[typePos]; + + if (mode.isChecked()) + c = new Creator(realType, textName.getText().toString(), true); + else + c = new Creator(realType, textFN.getText().toString(), textLN.getText().toString()); + + Item.setCreator(item.getKey(), c, creatorPosition, db); + item = Item.load(item.getKey(), db); + + refreshData(); + } + }); + + builder.setNeutralButton(getResources().getString(R.string.cancel), new OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }); + + builder.setNegativeButton(getResources().getString(R.string.menu_delete), new OnClickListener() { + @SuppressWarnings("unchecked") + public void onClick(DialogInterface dialog, int whichButton) { + Item.setCreator(item.getKey(), null, creatorPosition, db); + item = Item.load(item.getKey(), db); + + refreshData(); + } + }); + + dialog = builder.create(); + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams(); + lp.copyFrom(dialog.getWindow().getAttributes()); + lp.width = WindowManager.LayoutParams.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.MATCH_PARENT; + + dialog.getWindow().setAttributes(lp); + + return dialog; + + default: + Log.e(TAG, "Invalid dialog requested"); + return null; + } + } + + /* + * I've been just copying-and-pasting the options menu code from activity to activity. + * It needs to be reworked for some of these activities. + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.zotero_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_prefs: + startActivity(new Intent(this, SettingsActivity.class)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + class CreatorAdapter extends BaseAdapter { + private ArrayList mList; + + public CreatorAdapter(ArrayList list) { + mList = list; + } + + public void setData(ArrayList newList) { + mList = newList; + } + + @Override + public int getCount() { + return mList.size(); + } + + @Override + public Object getItem(int pos) { + return mList.get(pos); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row; + + // 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; + } + + TextView tvLabel = (TextView) row.findViewById(R.id.data_label); + TextView tvContent = (TextView) row.findViewById(R.id.data_content); + ImageView imgAction = (ImageView)row.findViewById(R.id.imgAction); + + tvLabel.setText(Item.localizedStringForString(((Bundle)getItem(position)).getString("creatorType"))); + tvContent.setText(((Bundle)getItem(position)).getString("name")); + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + + return row; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Fragment_Collections.java b/src/main/java/com/mattrobertson/zotable/app/Fragment_Collections.java new file mode 100755 index 0000000..063f1b2 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Fragment_Collections.java @@ -0,0 +1,707 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.mattrobertson.zotable.app.data.CollectionAdapter; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIEvent; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.ZoteroAPITask; +import com.nononsenseapps.filepicker.FilePickerActivity; + +import org.apache.http.util.ByteArrayBuffer; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +public class Fragment_Collections extends Fragment implements SwipeRefreshLayout.OnRefreshListener { + + private static final String TAG = "Fragment_Collections"; + + static final int DIALOG_NEW = 1; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + static final int FILE_CODE = 21; + + Bundle b = new Bundle(); + + ListView lvCollections; + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + private ItemCollection collection; + private Database db; + + private SwipeRefreshLayout swipeLayout; + + final Handler syncHandler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG,"received message: "+msg.arg1); + refreshView(); + + if (msg.arg1 == APIRequest.UPDATED_DATA) { + //refreshView(); + return; + } + + if (msg.arg1 == APIRequest.QUEUED_MORE) { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.sync_queued_more, msg.arg2), + Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.BATCH_DONE) { + Application.getInstance().getBus().post(SyncEvent.COMPLETE); + //Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.sync_complete),Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.ERROR_UNKNOWN) { + String desc = (msg.arg2 == 0) ? "" : " ("+msg.arg2+")"; + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.sync_error)+desc, + Toast.LENGTH_SHORT).show(); + } + } + }; + + private APIEvent mEvent = new APIEvent() { + private int updates = 0; + + @Override + public void onComplete(APIRequest request) { + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.BATCH_DONE; + syncHandler.sendMessage(msg); + Log.d(TAG, "fired oncomplete"); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + // Stop the Spinning refreshing icon + if (swipeLayout != null && swipeLayout.isRefreshing()) { + swipeLayout.setRefreshing(false); + } + } + }); + } + + @Override + public void onUpdate(APIRequest request) { + updates++; + + if (updates % 10 == 0) { + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.UPDATED_DATA; + syncHandler.sendMessage(msg); + } + } + + @Override + public void onError(APIRequest request, Exception exception) { + Log.e(TAG, "APIException caught", exception); + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.ERROR_UNKNOWN; + syncHandler.sendMessage(msg); + } + + @Override + public void onError(APIRequest request, int error) { + Log.e(TAG, "API error caught"); + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.ERROR_UNKNOWN; + syncHandler.sendMessage(msg); + } + }; + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + CollectionAdapter adapter = (CollectionAdapter) (lvCollections.getAdapter()); + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_collections, container, false); + } + + /** Called when the activity is first created. */ + @Override + public void onStart() { + super.onStart(); + + db = new Database(getActivity()); + + lvCollections = (ListView)getActivity().findViewById(R.id.lvCollections); + + swipeLayout = (SwipeRefreshLayout) getActivity().findViewById(R.id.swipe_container_collections); + swipeLayout.setOnRefreshListener(this); + swipeLayout.setColorScheme(R.color.light_blue, + R.color.blue, + R.color.dark_blue, + R.color.darkest_blue); + + CollectionAdapter collectionAdapter = new CollectionAdapter(getActivity(), create()); + + lvCollections.setAdapter(collectionAdapter); + + Cursor cur = collectionAdapter.getCursor(); + + lvCollections.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + + CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and replace the cursor with one for the selected collection + ItemCollection coll = ItemCollection.load(cur); + if (coll != null && coll.getKey() != null) { + Intent i = new Intent(getActivity().getBaseContext(), ItemActivity.class); + if (coll.getSize() == 0) { + // Send a message that we need to refresh the collection + i.putExtra("com.mattrobertson.zotable.app.rerequest", true); + } + i.putExtra("com.mattrobertson.zotable.app.collectionKey", coll.getKey()); + startActivity(i); + } else { + // collection loaded was null. why? + Log.d(TAG, "Failed loading items for collection at position: "+position); + return; + } + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.collection_cant_open, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + return; + } + } + }); + + if (cur == null || cur.getCount() == 0) { + + if (!ServerCredentials.check(getActivity().getBaseContext())) { + Toast.makeText(getActivity().getBaseContext(), getResources().getString(R.string.sync_log_in_first),Toast.LENGTH_SHORT).show(); + return; + } + + Toast.makeText(getActivity(),getActivity().getResources().getString(R.string.no_collections),Toast.LENGTH_SHORT).show(); + Log.d(TAG, "Running a request to populate missing collections"); + + sync(); + } + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)getActivity().findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)getActivity().findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)getActivity().findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)getActivity().findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(Fragment_Collections.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(getActivity(), FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); + } + + public void onResume() { + CollectionAdapter adapter = (CollectionAdapter)(lvCollections.getAdapter()); + // XXX This may be too agressive-- fix if causes issues + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + if (db == null) db = new Database(getActivity()); + super.onResume(); + } + + public void onDestroy() { + CollectionAdapter adapter = (CollectionAdapter)(lvCollections.getAdapter()); + + if (adapter != null) { + Cursor cur = adapter.getCursor(); + + if (cur != null) + cur.close(); + } + + if (db != null) + db.close(); + + super.onDestroy(); + } + + @Override + public void onRefresh() { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + sync(); + } + }); + } + + /** + * Gives a cursor for top-level collections + * @return + */ + public Cursor create() { + String[] args = { "false" }; + Cursor cursor = db.query("collections", Database.COLLCOLS, "collection_parent=?", args, null, null, "collection_name", null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + + public void showDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getActivity().getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + return; + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + mProgressDialog.show(); + return; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(getActivity()); + input.setHint(getResources().getString(R.string.identifier_hint)); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + + final Fragment_Collections current = this; + + dialog = new AlertDialog.Builder(getActivity()) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + + // TODO: quick & dirty fix... check here if error occurs + Fragment_Collections.this.b = c; + + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + dialog.show(); + return; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + if (requestCode == FILE_CODE) { + if (resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + Util.handleUpload(uri.getPath(),getActivity(),db,false); + } + } + else { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + if (mProgressDialog != null) + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + getActivity().dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getActivity().getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); // NOTE: tags item as DIRTY + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + public void sync() { + // Check log-in -- prompt if not logged in + if (!ServerCredentials.check(getActivity().getBaseContext())) { + Toast.makeText(getActivity().getBaseContext(), getResources().getString(R.string.sync_log_in_first),Toast.LENGTH_SHORT).show(); + return; + } + + Log.d(TAG, "Making sync request for all collections"); + + // Get credentials + ServerCredentials cred = new ServerCredentials(getActivity().getBaseContext()); + APIRequest req = APIRequest.fetchCollections(cred); + req.setHandler(mEvent); + + new ZoteroAPITask(getActivity().getBaseContext()).execute(req); + } + + public void scanBarcode(Fragment_Collections current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + FragmentIntentIntegrator integrator = new FragmentIntentIntegrator(this); + AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current.getActivity()); + } + } + + private final class FragmentIntentIntegrator extends IntentIntegrator { + + private final Fragment fragment; + + public FragmentIntentIntegrator(Fragment fragment) { + super(fragment.getActivity()); + this.fragment = fragment; + } + + @Override + protected void startActivityForResult(Intent intent, int code) { + fragment.startActivityForResult(intent, code); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Fragment_Favorites.java b/src/main/java/com/mattrobertson/zotable/app/Fragment_Favorites.java new file mode 100755 index 0000000..5c81827 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Fragment_Favorites.java @@ -0,0 +1,585 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; +import android.text.Editable; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.mattrobertson.zotable.app.data.CollectionAdapter; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.nononsenseapps.filepicker.FilePickerActivity; + +import org.apache.http.util.ByteArrayBuffer; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +public class Fragment_Favorites extends ListFragment { + + private static final String TAG = "Fragment_Favorites"; + + static final int DIALOG_NEW = 1; + static final int DIALOG_SORT = 2; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + static final int FILE_CODE = 21; + + Bundle b = new Bundle(); + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + private Database db; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_favorites, container, false); + } + + /** Called when the activity is first created. */ + @Override + public void onStart() { + super.onStart(); + + db = new Database(getActivity()); + + CollectionAdapter collectionAdapter = new CollectionAdapter(getActivity(), create()); + + setListAdapter(collectionAdapter); + + ListView lv = getListView(); + + lv.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { + CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and open activity for the selected collection + ItemCollection coll = ItemCollection.load(cur); + if (coll != null && coll.getKey() != null && coll.getSubcollections(db).size() > 0) { + Log.d(TAG, "Loading child collection with key: "+coll.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), CollectionActivity.class); + i.putExtra("com.mattrobertson.zotable.app.collectionKey", coll.getKey()); + startActivity(i); + } else { + Log.d(TAG, "Failed loading child collections for collection"); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.collection_no_subcollections), + Toast.LENGTH_SHORT).show(); + } + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); + Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.collection_cant_open, tvTitle.getText()),Toast.LENGTH_SHORT).show(); + } + return true; + } + }); + + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + + CollectionAdapter adapter = (CollectionAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and replace the cursor with one for the selected collection + ItemCollection coll = ItemCollection.load(cur); + if (coll != null && coll.getKey() != null) { + Intent i = new Intent(getActivity().getBaseContext(), ItemActivity.class); + if (coll.getSize() == 0) { + // Send a message that we need to refresh the collection + i.putExtra("com.mattrobertson.zotable.app.rerequest", true); + } + i.putExtra("com.mattrobertson.zotable.app.collectionKey", coll.getKey()); + startActivity(i); + } else { + // collection loaded was null. why? + Log.d(TAG, "Failed loading items for collection at position: " + position); + } + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.collection_title); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.collection_cant_open, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + } + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)getActivity().findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)getActivity().findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)getActivity().findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)getActivity().findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(Fragment_Favorites.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(getActivity(), FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); + } + + public void onResume() { + CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); + // XXX This may be too agressive-- fix if causes issues + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + if (db == null) db = new Database(getActivity()); + super.onResume(); + } + + public void onDestroy() { + CollectionAdapter adapter = (CollectionAdapter) getListAdapter(); + + if (adapter != null) { + Cursor cur = adapter.getCursor(); + + if (cur != null) + cur.close(); + } + + if (db != null) + db.close(); + + super.onDestroy(); + } + + /** + * Gives a cursor for top-level collections + */ + public Cursor create() { + String[] args = {"1"}; + Cursor cursor = db.query("collections", Database.COLLCOLS, "fav=?", args, null, null, "collection_name", null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + + public void showDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getActivity().getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + return; + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + mProgressDialog.show(); + return; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(getActivity()); + input.setHint(getResources().getString(R.string.identifier_hint)); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + + final Fragment_Favorites current = this; + + dialog = new AlertDialog.Builder(getActivity()) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + + // TODO: quick & dirty fix... check here if error occurs + Fragment_Favorites.this.b = c; + + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + dialog.show(); + return; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + if (requestCode == FILE_CODE) { + if (resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + Util.handleUpload(uri.getPath(),getActivity(),db,false); + } + } + else { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + if (mProgressDialog != null) + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + getActivity().dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getActivity().getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); // NOTE: tags item as DIRTY + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + public void scanBarcode(Fragment_Favorites current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + FragmentIntentIntegrator integrator = new FragmentIntentIntegrator(this); + AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current.getActivity()); + } + } + + private final class FragmentIntentIntegrator extends IntentIntegrator { + + private final Fragment fragment; + + public FragmentIntentIntegrator(Fragment fragment) { + super(fragment.getActivity()); + this.fragment = fragment; + } + + @Override + protected void startActivityForResult(Intent intent, int code) { + fragment.startActivityForResult(intent, code); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Fragment_Items.java b/src/main/java/com/mattrobertson/zotable/app/Fragment_Items.java new file mode 100755 index 0000000..85180eb --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Fragment_Items.java @@ -0,0 +1,915 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; + + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemAdapter; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIEvent; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.ZoteroAPITask; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.nononsenseapps.filepicker.FilePickerActivity; +import com.squareup.otto.Subscribe; + +import org.apache.http.util.ByteArrayBuffer; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; + +public class Fragment_Items extends ListFragment implements SwipeRefreshLayout.OnRefreshListener { + + private static final String TAG = "Fragment_Items"; + + static final int DIALOG_NEW = 1; + static final int DIALOG_SORT = 2; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + static final int FILE_CODE = 21; + + Bundle b = new Bundle(); + + /** + * Allowed sort orderings + */ + static final String[] SORTS = { + "item_year, item_title COLLATE NOCASE", + "item_creator COLLATE NOCASE, item_year", + "item_title COLLATE NOCASE, item_year", + "timestamp ASC, item_title COLLATE NOCASE" + }; + + /** + * Strings providing the names of each ordering, respectively + */ + static final int[] SORT_NAMES = { + R.string.sort_year_title, + R.string.sort_creator_year, + R.string.sort_title_year, + R.string.sort_modified_title + }; + private static final String SORT_CHOICE = "sort_choice"; + + private Database db; + + private SwipeRefreshLayout swipeLayout; + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + public String sortBy = "item_year, item_title"; + + final Handler syncHandler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "received message: " + msg.arg1); + refreshView(); + + if (msg.arg1 == APIRequest.UPDATED_DATA) { + refreshView(); + return; + } + + if (msg.arg1 == APIRequest.QUEUED_MORE) { + //Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.sync_queued_more, msg.arg2),Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.BATCH_DONE) { + Application.getInstance().getBus().post(SyncEvent.COMPLETE); + + // Sync success w/o erros -- set all items to clean status in DB + Item.setAllClean(db); + + Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.sync_complete),Toast.LENGTH_SHORT).show(); + return; + } + + if (msg.arg1 == APIRequest.ERROR_UNKNOWN) { + Toast.makeText(getActivity().getApplicationContext(),getResources().getString(R.string.sync_error),Toast.LENGTH_SHORT).show(); + return; + } + } + }; + + private APIEvent mEvent = new APIEvent() { + private int updates = 0; + + @Override + public void onComplete(APIRequest request) { + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.BATCH_DONE; + syncHandler.sendMessage(msg); + Log.d(TAG, "fired oncomplete"); + + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + // Stop the Spinning refreshing icon + swipeLayout.setRefreshing(false); + } + }); + } + + @Override + public void onUpdate(APIRequest request) { + updates++; + + if (updates % 10 == 0) { + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.UPDATED_DATA; + syncHandler.sendMessage(msg); + } else { + // do nothing + } + } + + @Override + public void onError(APIRequest request, Exception exception) { + Log.e(TAG, "APIException caught", exception); + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.ERROR_UNKNOWN; + syncHandler.sendMessage(msg); + } + + @Override + public void onError(APIRequest request, int error) { + Log.e(TAG, "API error caught"); + Message msg = syncHandler.obtainMessage(); + msg.arg1 = APIRequest.ERROR_UNKNOWN; + syncHandler.sendMessage(msg); + } + }; + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + if (adapter == null) return; + + Cursor newCursor = prepareCursor(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_items, container, false); + } + + /** + * Called when the activity is first created. + */ + @Override + public void onStart() { + super.onStart(); + + setHasOptionsMenu(true); + + String persistedSort = Persistence.read(SORT_CHOICE); + if (persistedSort != null) sortBy = persistedSort; + + db = new Database(getActivity()); + + swipeLayout = (SwipeRefreshLayout) getActivity().findViewById(R.id.swipe_container_items); + swipeLayout.setOnRefreshListener(this); + swipeLayout.setColorScheme(R.color.light_blue, + R.color.blue, + R.color.dark_blue, + R.color.darkest_blue); + + APIRequest req; + + req = APIRequest.fetchItems(false, new ServerCredentials(getActivity())); + + prepareAdapter(); + + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = adapter.getCursor(); + + // TODO: quick & dirty hack -- possible error here? compare with github original + if (cur == null || cur.getCount() == 0) { + + if (!ServerCredentials.check(getActivity().getBaseContext())) { + Toast.makeText(getActivity().getBaseContext(), getResources().getString(R.string.sync_log_in_first), + Toast.LENGTH_SHORT).show(); + return; + } + + Toast.makeText(getActivity(),getActivity().getResources().getString(R.string.collection_empty),Toast.LENGTH_SHORT).show(); + Log.d(TAG, "Running a request to populate missing items"); + ZoteroAPITask task = new ZoteroAPITask(getActivity()); + req.setHandler(mEvent); + task.execute(req); + + } + + final ListView lv = getListView(); + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + // If we have a click on an item, do something... + ItemAdapter adapter = (ItemAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and load an activity for the item + Item item = Item.load(cur); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemDbId", item.dbId); + startActivity(i); + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.item_title); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.cant_open_item, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + } + } + }); + + lv.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView absListView, int i) {} + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + int topRowVerticalPosition = (lv == null || lv.getChildCount() == 0) ? 0 : lv.getChildAt(0).getTop(); + + swipeLayout.setEnabled(firstVisibleItem == 0 && topRowVerticalPosition >= 0); + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)getActivity().findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)getActivity().findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)getActivity().findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)getActivity().findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(Fragment_Items.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(getActivity(), FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); + + fabManual.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_NEW); + } + }); + } + + @Override + public void onResume() { + super.onResume(); + Application.getInstance().getBus().register(this); + refreshView(); + } + + @Override + public void onDestroy() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = null; + + if (adapter != null) + cur = adapter.getCursor(); + + if (cur != null) + cur.close(); + + if (db != null) + db.close(); + + super.onDestroy(); + } + + @Override + public void onPause() { + super.onPause(); + Application.getInstance().getBus().unregister(this); + } + + @Override + public void onRefresh() { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + sync(); + } + }); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + menu.clear(); + + inflater.inflate(R.menu.zotero_menu,menu); + + // Turn on sort item + MenuItem sort = menu.findItem(R.id.do_sort); + sort.setEnabled(true); + sort.setVisible(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_sort: + showDialog(DIALOG_SORT); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void prepareAdapter() { + ItemAdapter adapter = new ItemAdapter(getActivity(), prepareCursor()); + setListAdapter(adapter); + } + + private Cursor prepareCursor() { + + // No longer searchable because it is a fragment... + return getCursor(); +/* + Cursor cursor; + // Be ready for a search + Intent intent = getActivity().getIntent(); + + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + query = intent.getStringExtra(SearchManager.QUERY); + cursor = getCursor(query); + getActivity().setTitle(getResources().getString(R.string.search_results, query)); + } else if (query != null) { + cursor = getCursor(query); + getActivity().setTitle(getResources().getString(R.string.search_results, query)); + } else if (intent.getStringExtra("com.mattrobertson.zotable.app.tag") != null) { + String tag = intent.getStringExtra("com.mattrobertson.zotable.app.tag"); + Query q = new Query(); + q.set("tag", tag); + cursor = getCursor(q); + getActivity().setTitle(getResources().getString(R.string.tag_viewing_items, tag)); + } else { + collectionKey = intent.getStringExtra("com.mattrobertson.zotable.app.collectionKey"); + + ItemCollection coll; + + if (collectionKey != null && (coll = ItemCollection.load(collectionKey, db)) != null) { + cursor = getCursor(coll); + getActivity().setTitle(coll.getTitle()); + } else { + cursor = getCursor(); + getActivity().setTitle(getResources().getString(R.string.all_items)); + } + } + return cursor; +*/ + } + + public void showDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getActivity().getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + return; + case DIALOG_SORT: + + // We generate the sort name list for our current locale + String[] sorts = new String[SORT_NAMES.length]; + for (int j = 0; j < SORT_NAMES.length; j++) { + sorts[j] = getResources().getString(SORT_NAMES[j]); + } + + AlertDialog.Builder builder2 = new AlertDialog.Builder(getActivity()); + builder2.setTitle(getResources().getString(R.string.set_sort_order)) + .setItems(sorts, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Cursor cursor = getCursor(); + setSortBy(SORTS[pos]); + + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + adapter.changeCursor(cursor); + Log.d(TAG, "Re-sorting by: " + SORTS[pos]); + + Persistence.write(SORT_CHOICE, SORTS[pos]); + } + }); + AlertDialog dialog2 = builder2.create(); + dialog2.show(); + return; + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + mProgressDialog.show(); + return; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(getActivity()); + input.setHint(getResources().getString(R.string.identifier_hint)); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + + final Fragment_Items current = this; + + dialog = new AlertDialog.Builder(getActivity()) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + + // TODO: quick & dirty fix... check here if error occurs + Fragment_Items.this.b = c; + + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + dialog.show(); + return; + } + } + + /* Sorting */ + public void setSortBy(String sort) { + this.sortBy = sort; + } + + /* Handling the ListView and keeping it up to date */ + public Cursor getCursor() { + Cursor cursor = db.query("items", Database.ITEMCOLS, null, null, null, null, this.sortBy, null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(ItemCollection parent) { + String[] args = {parent.dbId}; + Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, " + + "items._id, item_key, item_year, item_creator, timestamp, item_children " + + " FROM items, itemtocollections WHERE items._id = item_id AND collection_id=? ORDER BY " + this.sortBy, + args); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(String query) { + String[] args = {"%" + query + "%", "%" + query + "%"}; + Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, " + + "_id, item_key, item_year, item_creator, timestamp, item_children " + + " FROM items WHERE item_title LIKE ? OR item_creator LIKE ?" + + " ORDER BY " + this.sortBy, + args); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(Query query) { + return query.query(db); + } + + @Subscribe + public void syncComplete(SyncEvent event) { + if (event.getStatus() == SyncEvent.COMPLETE_CODE) refreshView(); + } + + /* Thread and helper to run lookups */ + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + if (requestCode == FILE_CODE) { + if (resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + Util.handleUpload(uri.getPath(),getActivity(),db,false); + } + } + else { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + if (mProgressDialog != null) + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + getActivity().dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getActivity().getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); // NOTE: tags item as DIRTY + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + public void sync() { + // Check log-in -- prompt if not logged in + if (!ServerCredentials.check(getActivity().getBaseContext())) { + Toast.makeText(getActivity().getBaseContext(), getResources().getString(R.string.sync_log_in_first),Toast.LENGTH_SHORT).show(); + return; + } + + // Get credentials + ServerCredentials cred = new ServerCredentials(getActivity().getBaseContext()); + + // OLD COMMENT: Make this a collection-specific sync, preceding by de-dirtying + + Item.buildDirtyQueue(db); + ArrayList list = new ArrayList(); + + for (Item i : Item.dirtyQueue) { + Log.d(TAG, "Adding dirty item to sync: " + i.getTitle()); + list.add(cred.prep(APIRequest.update(i))); + } + + Log.d(TAG, "Adding sync request for all items"); + APIRequest req = APIRequest.fetchItems(false, cred); + req.setHandler(mEvent); + list.add(req); + + APIRequest[] templ = {}; // to ensure toArray converts to APIRequests, not Objects(?) + APIRequest[] reqs = list.toArray(templ); + + ZoteroAPITask task = new ZoteroAPITask(getActivity().getBaseContext()); + task.setHandler(syncHandler); + task.execute(reqs); + //Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started),Toast.LENGTH_SHORT).show(); + } + + public void scanBarcode(Fragment_Items current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + FragmentIntentIntegrator integrator = new FragmentIntentIntegrator(this); + AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current.getActivity()); + } + } + + private final class FragmentIntentIntegrator extends IntentIntegrator { + + private final Fragment fragment; + + public FragmentIntentIntegrator(Fragment fragment) { + super(fragment.getActivity()); + this.fragment = fragment; + } + + @Override + protected void startActivityForResult(Intent intent, int code) { + fragment.startActivityForResult(intent, code); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Fragment_Tags.java b/src/main/java/com/mattrobertson/zotable/app/Fragment_Tags.java new file mode 100755 index 0000000..dd18993 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Fragment_Tags.java @@ -0,0 +1,645 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.support.v4.app.Fragment; +import android.support.v4.app.ListFragment; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.InputType; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.data.TagAdapter; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.nononsenseapps.filepicker.FilePickerActivity; + +import org.apache.http.util.ByteArrayBuffer; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +public class Fragment_Tags extends ListFragment { + + private static final String TAG = "Fragment_Tags"; + + static final int DIALOG_NEW = 1; + static final int DIALOG_SORT = 2; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + static final int FILE_CODE = 21; + + Bundle b = new Bundle(); + + /** + * Allowed sort orderings + */ + static final String[] SORTS = { + "tag COLLATE NOCASE", + "COUNT(*) DESC" + }; + + /** + * Strings providing the names of each ordering, respectively + */ + static final int[] SORT_NAMES = { + R.string.sort_tag_name, + R.string.sort_item_count + }; + private static final String SORT_CHOICE_TAGS = "sort_choice_tags"; + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + public String sortBy = "tag COLLATE NOCASE"; + + private Database db; + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + TagAdapter adapter = (TagAdapter) getListAdapter(); + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_tags, container, false); + } + + /** Called when the activity is first created. */ + @Override + public void onStart() { + super.onStart(); + + setHasOptionsMenu(true); + + String persistedSort = Persistence.read(SORT_CHOICE_TAGS); + if (persistedSort != null) sortBy = persistedSort; + + db = new Database(getActivity()); + + TagAdapter TagAdapter = new TagAdapter(getActivity(), create()); + setListAdapter(TagAdapter); + + ListView lv = getListView(); + + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + + TagAdapter adapter = (TagAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + String tagName = cur.getString(2); + + Intent i = new Intent(getActivity().getBaseContext(), TagItemsActivity.class); + i.putExtra("com.mattrobertson.zotable.app.tagName", tagName); + startActivity(i); + } else { + // failed to move cursor-- show a toast + TextView tvTag = (TextView) view.findViewById(R.id.tag_name); + Toast.makeText(getActivity().getApplicationContext(), "Can't view tag " + tvTag.getText(), Toast.LENGTH_SHORT).show(); + return; + } + return; + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)getActivity().findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)getActivity().findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)getActivity().findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)getActivity().findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(Fragment_Tags.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(getActivity(), FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); + + fabManual.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showDialog(DIALOG_NEW); + } + }); + } + + public void onResume() { + TagAdapter adapter = (TagAdapter) getListAdapter(); + // XXX This may be too agressive-- fix if causes issues + Cursor newCursor = create(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + if (db == null) db = new Database(getActivity()); + super.onResume(); + } + + public void onDestroy() { + TagAdapter adapter = (TagAdapter) getListAdapter(); + + if (adapter != null) { + Cursor cur = adapter.getCursor(); + + if (cur != null) + cur.close(); + } + + if (db != null) + db.close(); + + super.onDestroy(); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + menu.clear(); + + inflater.inflate(R.menu.zotero_menu,menu); + + // Turn on sort item + MenuItem sort = menu.findItem(R.id.do_sort); + sort.setEnabled(true); + sort.setVisible(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_sort: + showDialog(DIALOG_SORT); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + public void showDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getActivity().getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + return; + case DIALOG_SORT: + + // We generate the sort name list for our current locale + String[] sorts = new String[SORT_NAMES.length]; + for (int j = 0; j < SORT_NAMES.length; j++) { + sorts[j] = getResources().getString(SORT_NAMES[j]); + } + + AlertDialog.Builder builder2 = new AlertDialog.Builder(getActivity()); + builder2.setTitle(getResources().getString(R.string.set_sort_order)) + .setItems(sorts, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Cursor cursor; + setSortBy(SORTS[pos]); + ItemCollection collection; + + cursor = create(); + + TagAdapter adapter = (TagAdapter) getListAdapter(); + adapter.changeCursor(cursor); + Log.d(TAG, "Re-sorting by: " + SORTS[pos]); + + Persistence.write(SORT_CHOICE_TAGS, SORTS[pos]); + } + }); + AlertDialog dialog2 = builder2.create(); + dialog2.show(); + return; + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(getActivity()); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + mProgressDialog.show(); + return; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(getActivity()); + input.setHint(getResources().getString(R.string.identifier_hint)); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + + final Fragment_Tags current = this; + + dialog = new AlertDialog.Builder(getActivity()) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + + // TODO: quick & dirty fix... check here if error occurs + Fragment_Tags.this.b = c; + + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + dialog.show(); + return; + } + } + + /* Sorting */ + public void setSortBy(String sort) { + this.sortBy = sort; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getActivity().getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + if (mProgressDialog != null) + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + getActivity().dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getActivity().getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getActivity().getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); // NOTE: tags item as DIRTY + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + /** + * Gives a cursor containing all tags + * @return + */ + public Cursor create() { + //Cursor cursor = db.rawQuery("SELECT * FROM tags GROUP BY tag ORDER BY tag COLLATE NOCASE", null); + //Cursor cursor = db.rawQuery("SELECT * FROM tags GROUP BY tag ORDER BY COUNT(*) DESC", null); + Cursor cursor = db.rawQuery("SELECT * FROM tags GROUP BY tag ORDER BY " + sortBy, null); + + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } + + public void scanBarcode(Fragment_Tags current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + FragmentIntentIntegrator integrator = new FragmentIntentIntegrator(this); + @Nullable AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current.getActivity()); + } + } + + private final class FragmentIntentIntegrator extends IntentIntegrator { + + private final Fragment fragment; + + public FragmentIntentIntegrator(Fragment fragment) { + super(fragment.getActivity()); + this.fragment = fragment; + } + + @Override + protected void startActivityForResult(Intent intent, int code) { + fragment.startActivityForResult(intent, code); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/gimranov/zandy/app/ItemActivity.java b/src/main/java/com/mattrobertson/zotable/app/ItemActivity.java old mode 100644 new mode 100755 similarity index 74% rename from src/main/java/com/gimranov/zandy/app/ItemActivity.java rename to src/main/java/com/mattrobertson/zotable/app/ItemActivity.java index 0285120..566336c --- a/src/main/java/com/gimranov/zandy/app/ItemActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/ItemActivity.java @@ -1,54 +1,65 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; +import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.app.SearchManager; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.os.Message; +import android.support.v4.widget.SwipeRefreshLayout; import android.text.Editable; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.EditText; import android.widget.ListView; +import android.widget.SearchView; import android.widget.TextView; import android.widget.Toast; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemAdapter; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIEvent; -import com.gimranov.zandy.app.task.APIRequest; -import com.gimranov.zandy.app.task.ZoteroAPITask; +import com.getbase.floatingactionbutton.FloatingActionButton; + + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemAdapter; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIEvent; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.ZoteroAPITask; import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; +import com.nononsenseapps.filepicker.FilePickerActivity; import com.squareup.otto.Subscribe; import org.apache.http.util.ByteArrayBuffer; @@ -58,22 +69,24 @@ import org.json.JSONObject; import java.io.BufferedInputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; -public class ItemActivity extends ListActivity { +public class ItemActivity extends ListActivity implements SwipeRefreshLayout.OnRefreshListener { - private static final String TAG = "com.gimranov.zandy.app.ItemActivity"; + private static final String TAG = "ItemActivity"; - static final int DIALOG_VIEW = 0; static final int DIALOG_NEW = 1; static final int DIALOG_SORT = 2; static final int DIALOG_IDENTIFIER = 3; static final int DIALOG_PROGRESS = 6; + static final int FILE_CODE = 21; + /** * Allowed sort orderings */ @@ -99,6 +112,8 @@ public class ItemActivity extends ListActivity { private String query; private Database db; + private SwipeRefreshLayout swipeLayout; + private ProgressDialog mProgressDialog; private ProgressThread progressThread; @@ -124,9 +139,10 @@ public void handleMessage(Message msg) { if (msg.arg1 == APIRequest.BATCH_DONE) { Application.getInstance().getBus().post(SyncEvent.COMPLETE); - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.sync_complete), - Toast.LENGTH_SHORT).show(); + // Sync success w/o erros -- set all items to clean status in DB + Item.setAllClean(db); + + //Toast.makeText(getApplicationContext(),getResources().getString(R.string.sync_complete),Toast.LENGTH_SHORT).show(); return; } @@ -209,8 +225,15 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.items); + swipeLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container); + swipeLayout.setOnRefreshListener(this); + swipeLayout.setColorScheme(R.color.light_blue, + R.color.blue, + R.color.dark_blue, + R.color.darkest_blue); + Intent intent = getIntent(); - collectionKey = intent.getStringExtra("com.gimranov.zandy.app.collectionKey"); + collectionKey = intent.getStringExtra("com.mattrobertson.zotable.app.collectionKey"); ItemCollection coll = ItemCollection.load(collectionKey, db); APIRequest req; @@ -228,7 +251,7 @@ public void onCreate(Bundle savedInstanceState) { ItemAdapter adapter = (ItemAdapter) getListAdapter(); Cursor cur = adapter.getCursor(); - if (intent.getBooleanExtra("com.gimranov.zandy.app.rerequest", false) + if (intent.getBooleanExtra("com.mattrobertson.zotable.app.rerequest", false) || cur == null || cur.getCount() == 0) { @@ -238,17 +261,14 @@ public void onCreate(Bundle savedInstanceState) { return; } - Toast.makeText(this, - getResources().getString(R.string.collection_empty), - Toast.LENGTH_SHORT).show(); + Toast.makeText(this,getResources().getString(R.string.collection_empty),Toast.LENGTH_SHORT).show(); Log.d(TAG, "Running a request to populate missing items"); ZoteroAPITask task = new ZoteroAPITask(this); req.setHandler(mEvent); task.execute(req); - } - ListView lv = getListView(); + final ListView lv = getListView(); lv.setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView parent, View view, int position, long id) { // If we have a click on an item, do something... @@ -262,8 +282,8 @@ public void onItemClick(AdapterView parent, View view, int position, long id) Log.d(TAG, "Loading item data with key: " + item.getKey()); // We create and issue a specified intent with the necessary data Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); - i.putExtra("com.gimranov.zandy.app.itemDbId", item.dbId); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemDbId", item.dbId); startActivity(i); } else { // failed to move cursor-- show a toast @@ -274,6 +294,74 @@ public void onItemClick(AdapterView parent, View view, int position, long id) } } }); + + lv.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView absListView, int i) {} + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + int topRowVerticalPosition = (lv == null || lv.getChildCount() == 0) ? 0 : lv.getChildAt(0).getTop(); + + swipeLayout.setEnabled(firstVisibleItem == 0 && topRowVerticalPosition >= 0); + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)findViewById(R.id.btnFabIsbn); + FloatingActionButton fabUpload = (FloatingActionButton)findViewById(R.id.btnFabUpload); + FloatingActionButton fabManual = (FloatingActionButton)findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(ItemActivity.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + removeDialog(DIALOG_IDENTIFIER); + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabManual.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + removeDialog(DIALOG_NEW); + showDialog(DIALOG_NEW); + } + }); + + fabUpload.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // This always works + Intent i = new Intent(ItemActivity.this, FilePickerActivity.class); + // This works if you defined the intent filter + // Intent i = new Intent(Intent.ACTION_GET_CONTENT); + + // Set these depending on your use case. These are the defaults. + i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false); + i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); + i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); + + // Configure initial directory by specifying a String. + // You could specify a String like "/storage/emulated/0/", but that can + // dangerous. Always use Android's API calls to get paths to the SD-card or + // internal memory. + String filepath = Environment.getExternalStorageDirectory().getPath(); + if (new File("/sdcard/").exists()) + filepath = "/sdcard/"; // prefer /sdcard... implemented this way in case DNE + + i.putExtra(FilePickerActivity.EXTRA_START_PATH, filepath); + + startActivityForResult(i, FILE_CODE); + } + }); } @Override @@ -298,6 +386,16 @@ protected void onPause() { Application.getInstance().getBus().unregister(this); } + @Override + public void onRefresh() { + new Handler().postDelayed(new Runnable() { + @Override public void run() { + swipeLayout.setRefreshing(false); + sync(); + } + }, 5000); + } + private void prepareAdapter() { ItemAdapter adapter = new ItemAdapter(this, prepareCursor()); setListAdapter(adapter); @@ -315,14 +413,14 @@ private Cursor prepareCursor() { } else if (query != null) { cursor = getCursor(query); this.setTitle(getResources().getString(R.string.search_results, query)); - } else if (intent.getStringExtra("com.gimranov.zandy.app.tag") != null) { - String tag = intent.getStringExtra("com.gimranov.zandy.app.tag"); + } else if (intent.getStringExtra("com.mattrobertson.zotable.app.tag") != null) { + String tag = intent.getStringExtra("com.mattrobertson.zotable.app.tag"); Query q = new Query(); q.set("tag", tag); cursor = getCursor(q); this.setTitle(getResources().getString(R.string.tag_viewing_items, tag)); } else { - collectionKey = intent.getStringExtra("com.gimranov.zandy.app.collectionKey"); + collectionKey = intent.getStringExtra("com.mattrobertson.zotable.app.collectionKey"); ItemCollection coll; @@ -359,7 +457,7 @@ public void onClick(DialogInterface dialog, int pos) { Log.d(TAG, "Loading item data with key: " + item.getKey()); // We create and issue a specified intent with the necessary data Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); startActivity(i); } }); @@ -425,17 +523,6 @@ public void onClick(DialogInterface dialog, int whichButton) { ItemActivity.this.b = c; showDialog(DIALOG_PROGRESS); } - }).setNeutralButton(getResources().getString(R.string.scan), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int whichButton) { - // If we're about to download from Google play, cancel that dialog - // and prompt from Amazon if we're on an Amazon device - IntentIntegrator integrator = new IntentIntegrator(current); - @Nullable AlertDialog producedDialog = integrator.initiateScan(); - if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { - producedDialog.dismiss(); - AmazonZxingGlue.showDownloadDialog(current); - } - } }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { // do nothing @@ -469,10 +556,9 @@ public boolean onCreateOptionsMenu(Menu menu) { search.setEnabled(true); search.setVisible(true); - // Turn on identifier item - MenuItem identifier = menu.findItem(R.id.do_identifier); - identifier.setEnabled(true); - identifier.setVisible(true); + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + SearchView searchView = (SearchView) menu.findItem(R.id.do_search).getActionView(); + searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); return true; } @@ -481,54 +567,8 @@ public boolean onCreateOptionsMenu(Menu menu) { public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { - case R.id.do_sync: - if (!ServerCredentials.check(getBaseContext())) { - Toast.makeText(getBaseContext(), getResources().getString(R.string.sync_log_in_first), - Toast.LENGTH_SHORT).show(); - return true; - } - - // Get credentials - ServerCredentials cred = new ServerCredentials(getBaseContext()); - - // Make this a collection-specific sync, preceding by de-dirtying - Item.queue(db); - ArrayList list = new ArrayList(); - APIRequest[] templ = {}; - for (Item i : Item.queue) { - Log.d(TAG, "Adding dirty item to sync: " + i.getTitle()); - list.add(cred.prep(APIRequest.update(i))); - } - - if (collectionKey == null) { - Log.d(TAG, "Adding sync request for all items"); - APIRequest req = APIRequest.fetchItems(false, cred); - req.setHandler(mEvent); - list.add(req); - } else { - Log.d(TAG, "Adding sync request for collection: " + collectionKey); - APIRequest req = APIRequest.fetchItems(collectionKey, true, cred); - req.setHandler(mEvent); - list.add(req); - } - APIRequest[] reqs = list.toArray(templ); - - ZoteroAPITask task = new ZoteroAPITask(getBaseContext()); - task.setHandler(syncHandler); - task.execute(reqs); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started), - Toast.LENGTH_SHORT).show(); - return true; - case R.id.do_new: - removeDialog(DIALOG_NEW); - showDialog(DIALOG_NEW); - return true; - case R.id.do_identifier: - removeDialog(DIALOG_IDENTIFIER); - showDialog(DIALOG_IDENTIFIER); - return true; case R.id.do_search: - onSearchRequested(); + //onSearchRequested(); return true; case R.id.do_prefs: Intent i = new Intent(getBaseContext(), SettingsActivity.class); @@ -597,29 +637,31 @@ public void syncComplete(SyncEvent event) { public void onActivityResult(int requestCode, int resultCode, Intent intent) { Log.d(TAG, "_____________________on_activity_result"); - IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); - if (scanResult != null) { - // handle scan result - Bundle b = new Bundle(); - b.putString("mode", "isbn"); - b.putString("identifier", scanResult.getContents()); - if (scanResult != null - && scanResult.getContents() != null) { - Log.d(TAG, b.getString("identifier")); - progressThread = new ProgressThread(handler, b); - progressThread.start(); - this.b = b; - removeDialog(DIALOG_PROGRESS); - showDialog(DIALOG_PROGRESS); + if (requestCode == FILE_CODE) { + if (resultCode == Activity.RESULT_OK) { + Uri uri = intent.getData(); + Util.handleUpload(uri.getPath(), this, db, false); + } + } + else { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + + if (scanResult != null && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(this,getResources().getString(R.string.identifier_scan_failed),Toast.LENGTH_SHORT).show(); + } } else { - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.identifier_scan_failed), - Toast.LENGTH_SHORT).show(); + Toast.makeText(this,getResources().getString(R.string.identifier_scan_failed),Toast.LENGTH_SHORT).show(); } - } else { - Toast.makeText(getApplicationContext(), - getResources().getString(R.string.identifier_scan_failed), - Toast.LENGTH_SHORT).show(); } } @@ -643,7 +685,7 @@ public void handleMessage(Message msg) { Log.d(TAG, "Loading new item data with key: " + itemKey); // We create and issue a specified intent with the necessary data Intent i = new Intent(getBaseContext(), ItemDataActivity.class); - i.putExtra("com.gimranov.zandy.app.itemKey", itemKey); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); startActivity(i); } return; @@ -835,4 +877,52 @@ public void setState(int state) { mState = state; } } + + public void sync() { + if (!ServerCredentials.check(getBaseContext())) { + Toast.makeText(getBaseContext(), getResources().getString(R.string.sync_log_in_first),Toast.LENGTH_SHORT).show(); + return; + } + + // Get credentials + ServerCredentials cred = new ServerCredentials(getBaseContext()); + + // Make this a collection-specific sync, preceding by de-dirtying + Item.buildDirtyQueue(db); + ArrayList list = new ArrayList(); + APIRequest[] templ = {}; + for (Item i : Item.dirtyQueue) { + Log.d(TAG, "Adding dirty item to sync: " + i.getTitle()); + list.add(cred.prep(APIRequest.update(i))); + } + + if (collectionKey == null) { + Log.d(TAG, "Adding sync request for all items"); + APIRequest req = APIRequest.fetchItems(false, cred); + req.setHandler(mEvent); + list.add(req); + } else { + Log.d(TAG, "Adding sync request for collection: " + collectionKey); + APIRequest req = APIRequest.fetchItems(collectionKey, true, cred); + req.setHandler(mEvent); + list.add(req); + } + APIRequest[] reqs = list.toArray(templ); + + ZoteroAPITask task = new ZoteroAPITask(getBaseContext()); + task.setHandler(syncHandler); + task.execute(reqs); + //Toast.makeText(getApplicationContext(), getResources().getString(R.string.sync_started),Toast.LENGTH_SHORT).show(); + } + + public void scanBarcode(ItemActivity current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + IntentIntegrator integrator = new IntentIntegrator(current); + @Nullable AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current); + } + } } diff --git a/src/main/java/com/gimranov/zandy/app/ItemDataActivity.java b/src/main/java/com/mattrobertson/zotable/app/ItemDataActivity.java old mode 100644 new mode 100755 similarity index 74% rename from src/main/java/com/gimranov/zandy/app/ItemDataActivity.java rename to src/main/java/com/mattrobertson/zotable/app/ItemDataActivity.java index 7872503..7b5b199 --- a/src/main/java/com/gimranov/zandy/app/ItemDataActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/ItemDataActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import java.util.ArrayList; @@ -31,42 +31,45 @@ 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.ImageView; 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; + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; public class ItemDataActivity extends ExpandableListActivity { - private static final String TAG = "com.gimranov.zandy.app.ItemDataActivity"; + private static final String TAG = "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; + BundleListAdapter mBundleListAdapter; + + String itemKey = ""; + + ArrayList rows; + /** * For Bundle passing to Dialogs in API <= 7 */ @@ -76,17 +79,19 @@ public class ItemDataActivity extends ExpandableListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - + + getActionBar().setDisplayHomeAsUpEnabled(true); + db = new Database(this); /* Get the incoming data from the calling activity */ - String itemKey = getIntent().getStringExtra("com.gimranov.zandy.app.itemKey"); + itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.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"); + String itemDbId = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemDbId"); if (itemDbId == null) { Log.d(TAG, "Failed to load item using itemKey and no dbId specified. Give up and finish activity."); finish(); @@ -101,20 +106,20 @@ public void onCreate(Bundle savedInstanceState) { else this.setTitle(getResources().getString(R.string.item_details)); - ArrayList rows = item.toBundleArray(db); + rows = item.toBundleArray(db); - BundleListAdapter mBundleListAdapter = new BundleListAdapter(); + mBundleListAdapter = new BundleListAdapter(); mBundleListAdapter.bundles = rows; setListAdapter(mBundleListAdapter); + registerForContextMenu(getExpandableListView()); - ExpandableListView lv = getExpandableListView(); + final 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) { + public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { return true; } }); @@ -125,7 +130,57 @@ public boolean onGroupClick(ExpandableListView parent, View view, int position, // 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")) { + if (row.getString("label").equals("title")) { + final EditText input = new EditText(ItemDataActivity.this); + input.setText(row.getString("content"), BufferType.EDITABLE); + + AlertDialog.Builder builder = new AlertDialog.Builder(ItemDataActivity.this); + builder.setTitle("Title") + .setView(input) + .setPositiveButton("Update", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + item.setTitle(input.getText().toString()); + item.save(db); + refreshData(); + } + }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }); + AlertDialog dialog = builder.show(); + + int textViewId = dialog.getContext().getResources().getIdentifier("android:id/alertTitle", null, null); + TextView tv = (TextView) dialog.findViewById(textViewId); + tv.setTextColor(getResources().getColor(R.color.white)); + } + else if (row.getString("label").equals("date")) { +/* + final EditText input = new EditText(ItemDataActivity.this); + input.setText(row.getString("content"), BufferType.EDITABLE); + + AlertDialog.Builder builder = new AlertDialog.Builder(ItemDataActivity.this); + builder.setTitle("Date") + .setView(input) + .setPositiveButton("Update", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + item.setDate(input.getText().toString()); + item.save(db); + lv.invalidate(); + } + }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }); + AlertDialog dialog = builder.show(); + + int textViewId = dialog.getContext().getResources().getIdentifier("android:id/alertTitle", null, null); + TextView tv = (TextView) dialog.findViewById(textViewId); + tv.setTextColor(getResources().getColor(R.color.white)); +*/ + } + else if (row.getString("label").equals("url")) { row.putString("url", row.getString("content")); removeDialog(DIALOG_CONFIRM_NAVIGATE); ItemDataActivity.this.b = row; @@ -141,40 +196,38 @@ public boolean onGroupClick(ExpandableListView parent, View view, int position, } 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()); + i.putExtra("com.mattrobertson.zotable.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()); + i.putExtra("com.mattrobertson.zotable.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()); + i.putExtra("com.mattrobertson.zotable.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()); + i.putExtra("com.mattrobertson.zotable.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, @@ -193,8 +246,7 @@ public void onCreateContextMenu(ContextMenu menu, View view, // 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(); + Toast.makeText(getApplicationContext(), "Item type cannot be changed.", Toast.LENGTH_SHORT).show(); //removeDialog(DIALOG_ITEM_TYPE); //showDialog(DIALOG_ITEM_TYPE, row); return; @@ -204,13 +256,13 @@ public void onCreateContextMenu(ContextMenu menu, View view, } 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()); + i.putExtra("com.mattrobertson.zotable.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()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); startActivity(i); return; } @@ -221,6 +273,7 @@ public void onCreateContextMenu(ContextMenu menu, View view, } } }); +*/ } protected Dialog onCreateDialog(int id) { @@ -334,17 +387,43 @@ public void onDestroy() { @Override public void onResume() { - if (db == null) db = new Database(this); + if (db == null) + db = new Database(this); + + refreshData(); + super.onResume(); } + + public void refreshData() { + 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.mattrobertson.zotable.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); + } + + if (item != null) { + rows = item.toBundleArray(db); + mBundleListAdapter = new BundleListAdapter(); + mBundleListAdapter.bundles = rows; + setListAdapter(mBundleListAdapter); + } + + getExpandableListView().invalidate(); + } @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); @@ -356,25 +435,6 @@ public boolean onCreateOptionsMenu(Menu menu) { 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 items = new ArrayList(); - 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; @@ -385,6 +445,9 @@ public boolean onOptionsItemSelected(MenuItem i) { this.b = b; showDialog(DIALOG_CONFIRM_DELETE); return true; + case android.R.id.home: + onBackPressed(); + return true; default: return super.onOptionsItemSelected(i); } @@ -415,8 +478,7 @@ public int getChildrenCount(int groupPosition) { public TextView getGenericView() { // Layout parameters for the ExpandableListView - AbsListView.LayoutParams lp = new AbsListView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, 64); + AbsListView.LayoutParams lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 64); TextView textView = new TextView(ItemDataActivity.this); textView.setLayoutParams(lp); @@ -482,20 +544,50 @@ public View getGroupView(int groupPosition, boolean isExpanded, View convertView row = convertView; } - /* Our layout has just two fields */ - TextView tvLabel = (TextView) row.findViewById(R.id.data_label); - tvLabel.setPadding(0, 0, 0, 0); + TextView tvLabel = (TextView) row.findViewById(R.id.data_label); TextView tvContent = (TextView) row.findViewById(R.id.data_content); + ImageView imgAction = (ImageView)row.findViewById(R.id.imgAction); + + if (label.equals("title")) { + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("creators")) { + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("tags")) { + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("collections")) { + imgAction.setImageResource(R.drawable.ic_edit); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("DOI") && ! content.trim().equals("")) { + imgAction.setImageResource(R.drawable.ic_browse); + imgAction.setVisibility(View.VISIBLE); + } + else if (label.equals("url") && ! content.trim().equals("")) { + imgAction.setImageResource(R.drawable.ic_browse); + imgAction.setVisibility(View.VISIBLE); + } + else { + imgAction.setImageResource(android.R.color.transparent); + imgAction.setVisibility(View.GONE); + } + + tvLabel.setPadding(0, 0, 0, 0); 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)) { + + if ("title".equals(label) || "note".equals(label)) { tvContent.setText(Html.fromHtml(content)); - } else { + } + else { tvContent.setText(content); } diff --git a/src/main/java/com/gimranov/zandy/app/NoteActivity.java b/src/main/java/com/mattrobertson/zotable/app/NoteActivity.java old mode 100644 new mode 100755 similarity index 86% rename from src/main/java/com/gimranov/zandy/app/NoteActivity.java rename to src/main/java/com/mattrobertson/zotable/app/NoteActivity.java index 7d67138..68fc01f --- a/src/main/java/com/gimranov/zandy/app/NoteActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/NoteActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.app.Activity; import android.app.AlertDialog; @@ -32,10 +32,11 @@ import android.widget.TextView.BufferType; import android.widget.Toast; -import com.gimranov.zandy.app.data.Attachment; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.task.APIRequest; + +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.task.APIRequest; /** * This Activity handles displaying and editing of notes. @@ -45,7 +46,7 @@ */ public class NoteActivity extends Activity { - private static final String TAG = "com.gimranov.zandy.app.NoteActivity"; + private static final String TAG = "NoteActivity"; static final int DIALOG_NOTE = 3; @@ -62,7 +63,7 @@ public void onCreate(Bundle savedInstanceState) { db = new Database(this); /* Get the incoming data from the calling activity */ - final String attKey = getIntent().getStringExtra("com.gimranov.zandy.app.attKey"); + final String attKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.attKey"); final Attachment att = Attachment.load(attKey, db); if (att == null) { diff --git a/src/main/java/com/mattrobertson/zotable/app/PDFActivity.java b/src/main/java/com/mattrobertson/zotable/app/PDFActivity.java new file mode 100755 index 0000000..8646b4c --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/PDFActivity.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Environment; +import android.util.Log; +import android.widget.TextView; + +import com.mattrobertson.zotable.app.data.Database; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class PDFActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_pdf_handler); + + String filename = ""; + + try { + InputStream inputStream = getContentResolver().openInputStream(getIntent().getData()); + + File newFile = new File(Environment.getExternalStorageDirectory() + File.separator + "Download/temp.pdf"); + filename = newFile.getPath(); + + FileOutputStream output = new FileOutputStream(filename); + int bufferSize = 1024; + byte[] buffer = new byte[bufferSize]; + int len; + while ((len = inputStream.read(buffer)) != -1) { + output.write(buffer, 0, len); + } + } + catch (FileNotFoundException e) { + Log.e("ZZZ", e.getMessage()); + Intent intent = new Intent(this, Activity_Main.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } + catch (IOException e) { + Log.e("ZZZ", e.getMessage()); + Intent intent = new Intent(this, Activity_Main.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + } + + Database db = new Database(this); + + // Handle the upload + Util.handleUpload(filename,this,db,true); + } +} \ No newline at end of file diff --git a/src/main/java/com/mattrobertson/zotable/app/Persistence.java b/src/main/java/com/mattrobertson/zotable/app/Persistence.java new file mode 100755 index 0000000..efc9cb4 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Persistence.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.jetbrains.annotations.Nullable; + +public class Persistence { + private static final String TAG = Persistence.class.getCanonicalName(); + + private static final String FILE = "Persistence"; + + public static void write(String key, String value) { + SharedPreferences.Editor editor = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE).edit(); + editor.putString(key, value); + editor.commit(); + } + + @Nullable + public static String read(String key) { + SharedPreferences store = Application.getInstance().getSharedPreferences(FILE, Context.MODE_PRIVATE); + if (!store.contains(key)) return null; + + return store.getString(key, null); + } +} diff --git a/src/main/java/com/gimranov/zandy/app/Query.java b/src/main/java/com/mattrobertson/zotable/app/Query.java old mode 100644 new mode 100755 similarity index 63% rename from src/main/java/com/gimranov/zandy/app/Query.java rename to src/main/java/com/mattrobertson/zotable/app/Query.java index eb28796..ff5e379 --- a/src/main/java/com/gimranov/zandy/app/Query.java +++ b/src/main/java/com/mattrobertson/zotable/app/Query.java @@ -1,11 +1,27 @@ -package com.gimranov.zandy.app; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; import java.util.ArrayList; import android.database.Cursor; import android.os.Bundle; -import com.gimranov.zandy.app.data.Database; +import com.mattrobertson.zotable.app.data.Database; /** * This class is intended to provide ways of handling queries to the database. @@ -20,7 +36,7 @@ * - normalize queries and data to let * - allow saving of queries * - * Some of this will mean changes to other parts of Zandy's data storage model; + * Some of this will mean changes to other parts of Zotable's data storage model; * specifically, the raw JSON we're using now won't get us much further. We * could in theory maintain an index with tokens drawn from the JSON that * we populate on original save... Not sure about this. diff --git a/src/main/java/com/gimranov/zandy/app/RequestActivity.java b/src/main/java/com/mattrobertson/zotable/app/RequestActivity.java old mode 100644 new mode 100755 similarity index 87% rename from src/main/java/com/gimranov/zandy/app/RequestActivity.java rename to src/main/java/com/mattrobertson/zotable/app/RequestActivity.java index 27806d0..3424547 --- a/src/main/java/com/gimranov/zandy/app/RequestActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/RequestActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.app.ListActivity; import android.content.Context; @@ -29,8 +29,9 @@ import android.widget.TextView; import android.widget.Toast; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.task.APIRequest; + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.task.APIRequest; /** * This activity exists only for debugging, at least at this point @@ -44,7 +45,7 @@ public class RequestActivity extends ListActivity { @SuppressWarnings("unused") - private static final String TAG = "com.gimranov.zandy.app.RequestActivity"; + private static final String TAG = "com.mattrobertson.zotable.app.RequestActivity"; private Database db; /** Called when the activity is first created. */ diff --git a/src/main/java/com/mattrobertson/zotable/app/SearchActivity.java b/src/main/java/com/mattrobertson/zotable/app/SearchActivity.java new file mode 100755 index 0000000..5c9c42a --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/SearchActivity.java @@ -0,0 +1,139 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.ListActivity; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemAdapter; + +public class SearchActivity extends ListActivity { + + private static final String TAG = "SearchActivity"; + + private Database db; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getActionBar().setDisplayHomeAsUpEnabled(true); + + db = new Database(this); + + prepareAdapter(); + + final ListView lv = getListView(); + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + ItemAdapter adapter = (ItemAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + + if (cur.moveToPosition(position)) { + + Item item = Item.load(cur); + + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getBaseContext(), ItemDataActivity.class); + + if (item != null) { + Log.d(TAG, "Loading item data with key: " + item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemDbId", item.dbId); + } + + startActivity(i); + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.item_title); + Toast.makeText(getApplicationContext(),getResources().getString(R.string.cant_open_item, tvTitle.getText()),Toast.LENGTH_SHORT).show(); + } + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + Application.getInstance().getBus().register(this); + } + + @Override + public void onDestroy() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = adapter.getCursor(); + if (cur != null) cur.close(); + if (db != null) db.close(); + super.onDestroy(); + } + + @Override + protected void onPause() { + super.onPause(); + Application.getInstance().getBus().unregister(this); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) + onBackPressed(); + + return super.onOptionsItemSelected(item); + } + + private void prepareAdapter() { + ItemAdapter adapter = new ItemAdapter(this, prepareCursor()); + setListAdapter(adapter); + } + + private Cursor prepareCursor() { + String query = getIntent().getStringExtra("query"); + + setTitle(getResources().getString(R.string.search_results, query)); + + return getCursor(query); + } + + public Cursor getCursor(String query) { + String qLike = "%" + query + "%"; + + String[] args = {qLike,qLike,qLike}; + Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, " + + "_id, item_key, item_year, item_creator, timestamp, item_children " + + " FROM items WHERE item_title LIKE ? OR item_creator LIKE ? OR item_content LIKE ?" + + " ORDER BY item_title", + args); + + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + + return cursor; + } +} \ No newline at end of file diff --git a/src/main/java/com/gimranov/zandy/app/ServerCredentials.java b/src/main/java/com/mattrobertson/zotable/app/ServerCredentials.java old mode 100644 new mode 100755 similarity index 87% rename from src/main/java/com/gimranov/zandy/app/ServerCredentials.java rename to src/main/java/com/mattrobertson/zotable/app/ServerCredentials.java index d5c8184..6ce7521 --- a/src/main/java/com/gimranov/zandy/app/ServerCredentials.java +++ b/src/main/java/com/mattrobertson/zotable/app/ServerCredentials.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import java.io.File; @@ -24,16 +24,20 @@ import android.preference.PreferenceManager; import android.util.Log; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.APIRequest; public class ServerCredentials { /** Application key -- available from Zotero */ + public static final String CONSUMERKEY = "e46bd4963d8b0bc76806"; + public static final String CONSUMERSECRET = "00eae64e4f732b22fcf8"; + +/* Zandy application key public static final String CONSUMERKEY = "93a5aac13612aed2a236"; public static final String CONSUMERSECRET = "196d86bd1298cb78511c"; - +*/ /** This is the zotero:// protocol we intercept * It probably shouldn't be changed. */ - public static final String CALLBACKURL = "zotero://"; + public static final String CALLBACKURL = "zotable://"; /** This is the Zotero API server. Those who set up independent * Zotero installations will need to change this. */ @@ -77,7 +81,7 @@ public class ServerCredentials { "all_groups=write"; /* More constants */ - public static final File sBaseStorageDir = new File(Environment.getExternalStorageDirectory(), "/Android/data/com.gimranov.zandy"); + public static final File sBaseStorageDir = new File(Environment.getExternalStorageDirectory(), "/Android/data/com.mattrobertson.zotable"); public static final File sDocumentStorageDir = new File(sBaseStorageDir, "documents"); public static final File sCacheDir = new File(sBaseStorageDir, "cache"); diff --git a/src/main/java/com/gimranov/zandy/app/SettingsActivity.java b/src/main/java/com/mattrobertson/zotable/app/SettingsActivity.java old mode 100644 new mode 100755 similarity index 86% rename from src/main/java/com/gimranov/zandy/app/SettingsActivity.java rename to src/main/java/com/mattrobertson/zotable/app/SettingsActivity.java index 7608079..c408d94 --- a/src/main/java/com/gimranov/zandy/app/SettingsActivity.java +++ b/src/main/java/com/mattrobertson/zotable/app/SettingsActivity.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.app.AlertDialog; import android.app.Dialog; @@ -27,11 +27,12 @@ import android.view.View.OnClickListener; import android.widget.Button; -import com.gimranov.zandy.app.data.Database; + +import com.mattrobertson.zotable.app.data.Database; public class SettingsActivity extends PreferenceActivity implements OnClickListener { - private static final String TAG = "com.gimranov.zandy.app.SettingsActivity"; + private static final String TAG = "SettingsActivity"; static final int DIALOG_CONFIRM_DELETE = 5; @@ -40,7 +41,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.settings); - setContentView(R.layout.preferences); + setContentView(R.layout.preferences_old); Button requestButton = (Button) findViewById(R.id.requestQueue); requestButton.setOnClickListener(this); @@ -48,7 +49,7 @@ public void onCreate(Bundle savedInstanceState) { Button resetButton = (Button) findViewById(R.id.resetDatabase); resetButton.setOnClickListener(this); } - + public void onClick(View v) { if (v.getId() == R.id.requestQueue) { Intent i = new Intent(getApplicationContext(), RequestActivity.class); diff --git a/src/main/java/com/mattrobertson/zotable/app/SyncEvent.java b/src/main/java/com/mattrobertson/zotable/app/SyncEvent.java new file mode 100755 index 0000000..cc56f5c --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/SyncEvent.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +public class SyncEvent { + private static final String TAG = SyncEvent.class.getCanonicalName(); + + public static final int COMPLETE_CODE = 1; + + public static final SyncEvent COMPLETE = new SyncEvent(COMPLETE_CODE); + + private int status; + + public SyncEvent(int status) { + this.status = status; + } + + public int getStatus() { + return status; + } +} diff --git a/src/main/java/com/mattrobertson/zotable/app/TagActivity.java b/src/main/java/com/mattrobertson/zotable/app/TagActivity.java new file mode 100755 index 0000000..4a480ca --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/TagActivity.java @@ -0,0 +1,262 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import java.util.ArrayList; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.TextView.BufferType; + + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; + +/** + * This Activity handles displaying and editing tags. It works almost the same as + * ItemDataActivity, using a simple ArrayAdapter on Bundles with the tag info. + * + * @author ajlyon + * + */ +public class TagActivity extends Activity { + + private static final String TAG = "TagActivity"; + + static final int DIALOG_TAG = 3; + static final int DIALOG_CONFIRM_NAVIGATE = 4; + + private Item item; + + ListView lvTags; + TagAdapter adapter; + ArrayList rows; + + String itemKey = ""; + + private Database db; + + protected Bundle b = new Bundle(); + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.tag_activity); + + db = new Database(this); + + /* Get the incoming data from the calling activity */ + itemKey = getIntent().getStringExtra("com.mattrobertson.zotable.app.itemKey"); + Item item = Item.load(itemKey, db); + this.item = item; + + this.setTitle(getResources().getString(R.string.tags_for_item, item.getTitle())); + + lvTags = (ListView)findViewById(R.id.lvTags); + + rows = item.tagsToBundleArray(); + + adapter = new TagAdapter(rows); + lvTags.setAdapter(adapter); + + lvTags.setTextFilterEnabled(true); + lvTags.setOnItemClickListener(new OnItemClickListener() { + @SuppressWarnings("unchecked") + public void onItemClick(AdapterView parent, View view, int position, long id) { + + } + }); + + FloatingActionButton fab = (FloatingActionButton)findViewById(R.id.btnFab); + + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + + AlertDialog.Builder builder = new AlertDialog.Builder(TagActivity.this); + builder.setTitle("New tag"); + + final EditText etTagName = new EditText(TagActivity.this); + etTagName.setTextColor(getResources().getColor(R.color.white)); + + builder.setView(etTagName); + builder.setPositiveButton(getResources().getString(R.string.ok), new DialogInterface.OnClickListener() { + @SuppressWarnings("unchecked") + public void onClick(DialogInterface dialog, int whichButton) { + Item item = Item.load(itemKey, db); + item.addTag(etTagName.getText().toString()); + item.save(db); + refreshData(); + } + }); + + builder.setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + @SuppressWarnings("unchecked") + public void onClick(DialogInterface dialog, int whichButton) { + + } + }); + + + final Dialog dialog = builder.create(); + + dialog.show(); + } + }); + } + + @Override + public void onDestroy() { + if (db != null) + db.close(); + + super.onDestroy(); + } + + @Override + public void onResume() { + if (db == null) + db = new Database(this); + + super.onResume(); + } + + public void refreshData() { + item = Item.load(itemKey, db); + rows = item.tagsToBundleArray(); + + adapter.setData(rows); + adapter.notifyDataSetChanged(); + + lvTags.invalidate(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.zotero_menu, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_prefs: + startActivity(new Intent(this, SettingsActivity.class)); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + class TagAdapter extends BaseAdapter { + private ArrayList mList; + + public TagAdapter(ArrayList list) { + mList = list; + } + + public void setData(ArrayList newList) { + mList = newList; + } + + @Override + public int getCount() { + return mList.size(); + } + + @Override + public Object getItem(int pos) { + return mList.get(pos); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View row; + + // 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); + final TextView tvContent = (TextView) row.findViewById(R.id.data_content); + + if (((Bundle)getItem(position)).getInt("type") == 1) + tvLabel.setText(getResources().getString(R.string.tag_auto)); + else + tvLabel.setText(getResources().getString(R.string.tag_user)); + tvContent.setText(((Bundle) getItem(position)).getString("tag")); + + ImageView imgRemove = (ImageView) row.findViewById(R.id.imgAction); + imgRemove.setImageResource(R.drawable.ic_delete); + imgRemove.setVisibility(View.VISIBLE); + + imgRemove.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + TagActivity.this.runOnUiThread(new Runnable() { + public void run() { + item.removeTag(tvContent.getText().toString()); + item.save(db); + refreshData(); + } + }); + } + }); + + return row; + } + } +} diff --git a/src/main/java/com/mattrobertson/zotable/app/TagItemsActivity.java b/src/main/java/com/mattrobertson/zotable/app/TagItemsActivity.java new file mode 100755 index 0000000..31c3e21 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/TagItemsActivity.java @@ -0,0 +1,610 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ListActivity; +import android.app.ProgressDialog; +import android.app.SearchManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.SearchView; +import android.widget.TextView; +import android.widget.Toast; + +import com.getbase.floatingactionbutton.FloatingActionButton; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemAdapter; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.ZoteroAPITask; +import com.squareup.otto.Subscribe; + +import org.apache.http.util.ByteArrayBuffer; +import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.util.ArrayList; + +public class TagItemsActivity extends ListActivity { + + private static final String TAG = "TagItemsActivity"; + + static final int DIALOG_VIEW = 0; + static final int DIALOG_NEW = 1; + static final int DIALOG_SORT = 2; + static final int DIALOG_IDENTIFIER = 3; + static final int DIALOG_PROGRESS = 6; + + private String tagName; + + private String query; + private Database db; + + private ProgressDialog mProgressDialog; + private ProgressThread progressThread; + + protected Bundle b = new Bundle(); + + /** + * Refreshes the current list adapter + */ + private void refreshView() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + if (adapter == null) return; + + Cursor newCursor = prepareCursor(); + adapter.changeCursor(newCursor); + adapter.notifyDataSetChanged(); + Log.d(TAG, "refreshing view on request"); + } + + /** + * Called when the activity is first created. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + db = new Database(this); + + setContentView(R.layout.tag_items); + + Intent intent = getIntent(); + tagName = intent.getStringExtra("com.mattrobertson.zotable.app.tagName"); + + prepareAdapter(); + + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = adapter.getCursor(); + + final ListView lv = getListView(); + lv.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View view, int position, long id) { + // If we have a click on an item, do something... + ItemAdapter adapter = (ItemAdapter) parent.getAdapter(); + Cursor cur = adapter.getCursor(); + // Place the cursor at the selected item + if (cur.moveToPosition(position)) { + // and load an activity for the item + Item item = Item.load(cur); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + i.putExtra("com.mattrobertson.zotable.app.itemDbId", item.dbId); + startActivity(i); + } else { + // failed to move cursor-- show a toast + TextView tvTitle = (TextView) view.findViewById(R.id.item_title); + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.cant_open_item, tvTitle.getText()), + Toast.LENGTH_SHORT).show(); + } + } + }); + + // Create floating action buttons + FloatingActionButton fabScan = (FloatingActionButton)findViewById(R.id.btnFabScan); + FloatingActionButton fabIsbn = (FloatingActionButton)findViewById(R.id.btnFabIsbn); + FloatingActionButton fabManual = (FloatingActionButton)findViewById(R.id.btnFabManual); + + fabScan.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanBarcode(TagItemsActivity.this); + } + }); + + fabIsbn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + removeDialog(DIALOG_IDENTIFIER); + showDialog(DIALOG_IDENTIFIER); + } + }); + + fabManual.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + removeDialog(DIALOG_NEW); + showDialog(DIALOG_NEW); + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + Application.getInstance().getBus().register(this); + refreshView(); + } + + @Override + public void onDestroy() { + ItemAdapter adapter = (ItemAdapter) getListAdapter(); + Cursor cur = adapter.getCursor(); + if (cur != null) cur.close(); + if (db != null) db.close(); + super.onDestroy(); + } + + @Override + protected void onPause() { + super.onPause(); + Application.getInstance().getBus().unregister(this); + } + + private void prepareAdapter() { + ItemAdapter adapter = new ItemAdapter(this, prepareCursor()); + setListAdapter(adapter); + } + + private Cursor prepareCursor() { + Cursor cursor; + // Be ready for a search + Intent intent = getIntent(); + + tagName = ""; + tagName = intent.getStringExtra("com.mattrobertson.zotable.app.tagName"); + + this.setTitle("Tag: " + tagName); + + String[] args = {tagName}; + + cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, I._id, " + + "item_key, item_year, item_creator, timestamp, item_children FROM items AS I " + + "JOIN tags AS T ON I._id=T.item_id " + + "WHERE T.tag=? " + + "ORDER BY item_title",args); +/* + String[] args = { tagName }; + Log.i(TAG, "Loading items with tag: "+tagName); + cursor = db.query("tags", Database.TAGCOLS, "tag=?", args, null, null, null, null); +*/ + //} + return cursor; + } + + protected Dialog onCreateDialog(int id) { + switch (id) { + case DIALOG_NEW: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(getBaseContext(), Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + return dialog; + + case DIALOG_PROGRESS: + mProgressDialog = new ProgressDialog(this); + mProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + mProgressDialog.setIndeterminate(true); + mProgressDialog.setMessage(getResources().getString(R.string.identifier_looking_up)); + return mProgressDialog; + case DIALOG_IDENTIFIER: + final EditText input = new EditText(this); + input.setHint(getResources().getString(R.string.identifier_hint)); + + final TagItemsActivity current = this; + + dialog = new AlertDialog.Builder(this) + .setTitle(getResources().getString(R.string.identifier_message)) + .setView(input) + .setPositiveButton(getResources().getString(R.string.menu_search), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + Editable value = input.getText(); + // run search + Bundle c = new Bundle(); + c.putString("mode", "isbn"); + c.putString("identifier", value.toString()); + removeDialog(DIALOG_PROGRESS); + TagItemsActivity.this.b = c; + showDialog(DIALOG_PROGRESS); + } + }).setNegativeButton(getResources().getString(R.string.cancel), new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + // do nothing + } + }).create(); + return dialog; + default: + return null; + } + } + + @Override + protected void onPrepareDialog(int id, Dialog dialog) { + switch (id) { + case DIALOG_PROGRESS: + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.zotero_menu, menu); + + // Turn on search item + MenuItem search = menu.findItem(R.id.do_search); + search.setEnabled(true); + search.setVisible(true); + + SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); + SearchView searchView = (SearchView) menu.findItem(R.id.do_search).getActionView(); + searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle item selection + switch (item.getItemId()) { + case R.id.do_search: + onSearchRequested(); + return true; + case R.id.do_prefs: + Intent i = new Intent(getBaseContext(), SettingsActivity.class); + startActivity(i); + return true; + case R.id.do_sort: + removeDialog(DIALOG_SORT); + showDialog(DIALOG_SORT); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /* Handling the ListView and keeping it up to date */ + public Cursor getCursor() { + Cursor cursor = db.query("tags", Database.TAGCOLS, null, null, null, null, null, null); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(String query) { + String[] args = {"%" + query + "%", "%" + query + "%"}; + Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, " + + "_id, item_key, item_year, item_creator, timestamp, item_children " + + " FROM items WHERE item_title LIKE ? OR item_creator LIKE ?" + + " ORDER BY item_title", + args); + if (cursor == null) { + Log.e(TAG, "cursor is null"); + } + return cursor; + } + + public Cursor getCursor(Query query) { + return query.query(db); + } + + /* Thread and helper to run lookups */ + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + Log.d(TAG, "_____________________on_activity_result"); + + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null) { + // handle scan result + Bundle b = new Bundle(); + b.putString("mode", "isbn"); + b.putString("identifier", scanResult.getContents()); + if (scanResult != null + && scanResult.getContents() != null) { + Log.d(TAG, b.getString("identifier")); + progressThread = new ProgressThread(handler, b); + progressThread.start(); + this.b = b; + removeDialog(DIALOG_PROGRESS); + showDialog(DIALOG_PROGRESS); + } else { + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.identifier_scan_failed), + Toast.LENGTH_SHORT).show(); + } + } + + final Handler handler = new Handler() { + public void handleMessage(Message msg) { + Log.d(TAG, "______________________handle_message"); + if (ProgressThread.STATE_DONE == msg.arg2) { + Bundle data = msg.getData(); + String itemKey = data.getString("itemKey"); + if (itemKey != null) { + + mProgressDialog.dismiss(); + mProgressDialog = null; + + Log.d(TAG, "Loading new item data with key: " + itemKey); + // We create and issue a specified intent with the necessary data + Intent i = new Intent(getBaseContext(), ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", itemKey); + startActivity(i); + } + return; + } + + if (ProgressThread.STATE_PARSING == msg.arg2) { + mProgressDialog.setMessage(getResources().getString(R.string.identifier_processing)); + return; + } + + if (ProgressThread.STATE_ERROR == msg.arg2) { + dismissDialog(DIALOG_PROGRESS); + Toast.makeText(getApplicationContext(), + getResources().getString(R.string.identifier_lookup_failed), + Toast.LENGTH_SHORT).show(); + progressThread.setState(ProgressThread.STATE_DONE); + return; + } + } + }; + + private class ProgressThread extends Thread { + Handler mHandler; + Bundle arguments; + final static int STATE_DONE = 5; + final static int STATE_FETCHING = 1; + final static int STATE_PARSING = 6; + final static int STATE_ERROR = 7; + + int mState; + + ProgressThread(Handler h, Bundle b) { + mHandler = h; + arguments = b; + Log.d(TAG, "_____________________thread_constructor"); + } + + public void run() { + Log.d(TAG, "_____________________thread_run"); + mState = STATE_FETCHING; + + // Setup + String identifier = arguments.getString("identifier"); + String mode = arguments.getString("mode"); + URL url; + String urlstring; + + String response = ""; + + if ("isbn".equals(mode)) { + urlstring = "http://xisbn.worldcat.org/webservices/xid/isbn/" + + identifier + + "?method=getMetadata&fl=*&format=json&count=1"; + } else { + urlstring = ""; + } + + try { + Log.d(TAG, "Fetching from: " + urlstring); + url = new URL(urlstring); + + /* Open a connection to that URL. */ + URLConnection ucon = url.openConnection(); + /* + * Define InputStreams to read from the URLConnection. + */ + InputStream is = ucon.getInputStream(); + BufferedInputStream bis = new BufferedInputStream(is, 16000); + + ByteArrayBuffer baf = new ByteArrayBuffer(50); + int current = 0; + + /* + * Read bytes to the Buffer until there is nothing more to read(-1). + */ + while (mState == STATE_FETCHING + && (current = bis.read()) != -1) { + baf.append((byte) current); + } + response = new String(baf.toByteArray()); + Log.d(TAG, response); + + + } catch (IOException e) { + Log.e(TAG, "Error: ", e); + } + + Message msg = mHandler.obtainMessage(); + msg.arg2 = STATE_PARSING; + mHandler.sendMessage(msg); + + /* + * { + "stat":"ok", + "list":[{ + "url":["http://www.worldcat.org/oclc/177669176?referer=xid"], + "publisher":"O'Reilly", + "form":["BA"], + "lccn":["2004273129"], + "lang":"eng", + "city":"Sebastopol, CA", + "author":"by Mark Lutz and David Ascher.", + "ed":"2nd ed.", + "year":"2003", + "isbn":["0596002815"], + "title":"Learning Python", + "oclcnum":["177669176", +.. + "748093898"]}]} + */ + + // This is OCLC-specific logic + try { + JSONObject result = new JSONObject(response); + + if (!result.getString("stat").equals("ok")) { + Log.e(TAG, "Error response received"); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + + result = result.getJSONArray("list").getJSONObject(0); + String form = result.getJSONArray("form").getString(0); + String type; + + if ("AA".equals(form)) type = "audioRecording"; + else if ("VA".equals(form)) type = "videoRecording"; + else if ("FA".equals(form)) type = "film"; + else type = "book"; + + // TODO Fix this + type = "book"; + + Item item = new Item(getBaseContext(), type); + + JSONObject content = item.getContent(); + + if (result.has("lccn")) { + String lccn = "LCCN: " + result.getJSONArray("lccn").getString(0); + content.put("extra", lccn); + } + + if (result.has("isbn")) { + content.put("ISBN", result.getJSONArray("isbn").getString(0)); + } + + content.put("title", result.optString("title", "")); + content.put("place", result.optString("city", "")); + content.put("edition", result.optString("ed", "")); + content.put("language", result.optString("lang", "")); + content.put("publisher", result.optString("publisher", "")); + content.put("date", result.optString("year", "")); + + item.setTitle(result.optString("title", "")); + item.setYear(result.optString("year", "")); + + String author = result.optString("author", ""); + + item.setCreatorSummary(author); + JSONArray array = new JSONArray(); + JSONObject member = new JSONObject(); + member.accumulate("creatorType", "author"); + member.accumulate("name", author); + array.put(member); + content.put("creators", array); + + item.setContent(content); + item.save(db); + + msg = mHandler.obtainMessage(); + Bundle data = new Bundle(); + data.putString("itemKey", item.getKey()); + msg.setData(data); + msg.arg2 = STATE_DONE; + mHandler.sendMessage(msg); + return; + } catch (JSONException e) { + Log.e(TAG, "exception parsing response", e); + msg = mHandler.obtainMessage(); + msg.arg2 = STATE_ERROR; + mHandler.sendMessage(msg); + return; + } + } + + public void setState(int state) { + mState = state; + } + } + + public void scanBarcode(TagItemsActivity current) { + // If we're about to download from Google play, cancel that dialog + // and prompt from Amazon if we're on an Amazon device + IntentIntegrator integrator = new IntentIntegrator(current); + @Nullable AlertDialog producedDialog = integrator.initiateScan(); + if (producedDialog != null && "amazon".equals(BuildConfig.FLAVOR)) { + producedDialog.dismiss(); + AmazonZxingGlue.showDownloadDialog(current); + } + } +} diff --git a/src/main/java/com/mattrobertson/zotable/app/Util.java b/src/main/java/com/mattrobertson/zotable/app/Util.java new file mode 100755 index 0000000..6e43886 --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/Util.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.util.Log; + +import com.mattrobertson.zotable.app.data.Creator; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.task.APIRequest; + +import org.pdfparse.model.PDFDocCatalog; +import org.pdfparse.model.PDFDocInfo; +import org.pdfparse.model.PDFDocument; + +public class Util { + private static final String TAG = Util.class.getCanonicalName(); + + public static final String DOI_PREFIX = "http://dx.doi.org/"; + + public static String doiToUri(String doi) { + if (isDoi(doi)) { + return DOI_PREFIX + doi.replaceAll("^doi:", ""); + } + return doi; + } + + public static boolean isDoi(String doi) { + return (doi.startsWith("doi:") || doi.startsWith("10.")); + } + + public static void handleUpload(String filename, final Context context, final Database db, final boolean clearStack) { + + final String title; + final String author; + final String keywords; + + try { + // Create document object. Open file + PDFDocument doc = new PDFDocument(filename); + + // Get document structure elements + PDFDocInfo info = doc.getDocumentInfo(); + PDFDocCatalog cat = doc.getDocumentCatalog(); + + title = info.getTitle(); + author = info.getAuthor(); + keywords = info.getSubject(); + + } catch (Exception e) { + Log.e("ZZZ", e.getMessage()); + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(context.getResources().getString(R.string.item_type)) + // XXX i18n + .setItems(Item.ITEM_TYPES_EN, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int pos) { + Item item = new Item(context, Item.ITEM_TYPES[pos]); + item.dirty = APIRequest.API_DIRTY; + item.setTitle(title); + item.save(db); + + Item.setCreator(item.getKey(), new Creator("author", author, true), 0, db); + item = Item.load(item.getKey(),db); + + String[] arrKeywords = keywords.split(","); + for (String keyword : arrKeywords) + item.addTag(keyword.trim()); + + item.save(db); + + Log.d(TAG, "Loading item data with key: " + item.getKey()); + + // We create and issue a specified intent with the necessary data + Intent i = new Intent(context, ItemDataActivity.class); + i.putExtra("com.mattrobertson.zotable.app.itemKey", item.getKey()); + if (clearStack) + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + context.startActivity(i); + } + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } +} diff --git a/src/main/java/com/gimranov/zandy/app/XMLResponseParser.java b/src/main/java/com/mattrobertson/zotable/app/XMLResponseParser.java old mode 100644 new mode 100755 similarity index 94% rename from src/main/java/com/gimranov/zandy/app/XMLResponseParser.java rename to src/main/java/com/mattrobertson/zotable/app/XMLResponseParser.java index 49083af..7060a97 --- a/src/main/java/com/gimranov/zandy/app/XMLResponseParser.java +++ b/src/main/java/com/mattrobertson/zotable/app/XMLResponseParser.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app; +package com.mattrobertson.zotable.app; import android.sax.Element; import android.sax.ElementListener; @@ -24,12 +24,11 @@ import android.util.Log; import android.util.Xml; -import com.crashlytics.android.Crashlytics; -import com.gimranov.zandy.app.data.Attachment; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemCollection; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; +import com.mattrobertson.zotable.app.task.APIRequest; import org.json.JSONException; import org.json.JSONObject; @@ -40,7 +39,7 @@ import java.util.ArrayList; public class XMLResponseParser extends DefaultHandler { - private static final String TAG = "com.gimranov.zandy.app.XMLResponseParser"; + private static final String TAG = "XMLResponseParser"; private InputStream input; private Item item; @@ -72,14 +71,14 @@ public XMLResponseParser(InputStream in, APIRequest request) { followNext = true; input = in; this.request = request; - // Initialize the request queue if needed + // Initialize the request buildDirtyQueue if needed if (queue == null) queue = new ArrayList(); } public XMLResponseParser(APIRequest request) { followNext = true; this.request = request; - // Initialize the request queue if needed + // Initialize the request buildDirtyQueue if needed if (queue == null) queue = new ArrayList(); } @@ -134,7 +133,7 @@ public void start(Attributes attributes) { Log.d(TAG, "Key extraction failed from root; maybe this isn't a collection listing?"); } } - // If there are more items, queue them up to be handled too + // If there are more items, buildDirtyQueue them up to be handled too if (rel.contains("next")) { Log.d(TAG, "Found continuation: "+href); APIRequest req = new APIRequest(href, "get", null); @@ -155,7 +154,9 @@ public void start(Attributes attributes) { } public void end() { + Log.d("ZZZ-zotable", "XMLResponse end()"); if (items == true) { + Log.d("ZZZ-zotable", "XMLResponse end() items == true"); if (updateKey != null && updateType != null && updateType.equals("item")) { // We have an incoming new version of an item Item existing = Item.load(updateKey, db); @@ -190,6 +191,7 @@ public void end() { } } else { item.dirty = APIRequest.API_CLEAN; + Log.d("ZZZ-zotable", "API_CLEAN 3"); attachment.dirty = APIRequest.API_CLEAN; if ((attachment.url != null && !"".equals(attachment.url)) || attachment.content.optInt("linkMode") == Attachment.MODE_IMPORTED_FILE @@ -235,6 +237,7 @@ public void end() { } if (!items) { + Log.d("ZZZ-zotable-false", "items==false"); if (updateKey != null && updateType != null && updateType.equals("collection")) { // We have an incoming new version of a collection ItemCollection existing = ItemCollection.load(updateKey, db); @@ -388,7 +391,6 @@ public void end(String body) { db.close(); } catch (Exception e) { Log.e(TAG, "exception loading content", e); - Crashlytics.logException(new Exception("Exception parsing data", e)); } } } diff --git a/src/main/java/com/gimranov/zandy/app/data/Attachment.java b/src/main/java/com/mattrobertson/zotable/app/data/Attachment.java old mode 100644 new mode 100755 similarity index 85% rename from src/main/java/com/gimranov/zandy/app/data/Attachment.java rename to src/main/java/com/mattrobertson/zotable/app/data/Attachment.java index 0c73f7b..01d6610 --- a/src/main/java/com/gimranov/zandy/app/data/Attachment.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/Attachment.java @@ -1,4 +1,20 @@ -package com.gimranov.zandy.app.data; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.data; import java.util.ArrayList; import java.util.UUID; @@ -10,7 +26,7 @@ import android.database.Cursor; import android.util.Log; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.APIRequest; public class Attachment { @@ -41,7 +57,7 @@ public class Attachment { */ public JSONObject content; - private static final String TAG = "com.gimranov.zandy.app.data.Attachment"; + private static final String TAG = "Attachment"; public static final int AVAILABLE = 1; public static final int LOCAL = 2; @@ -157,10 +173,10 @@ public void delete(Database db) { */ public static void queue(Database db) { if (queue == null) { - // Initialize the queue if necessary + // Initialize the buildDirtyQueue if necessary queue = new ArrayList(); } - Log.d(TAG, "Clearing attachment dirty queue before repopulation"); + Log.d(TAG, "Clearing attachment dirty buildDirtyQueue before repopulation"); queue.clear(); Attachment attachment; String[] cols = Database.ATTCOLS; @@ -175,7 +191,7 @@ public static void queue(Database db) { } do { - Log.d(TAG, "Adding attachment to dirty queue"); + Log.d(TAG, "Adding attachment to dirty buildDirtyQueue"); attachment = load(cur); queue.add(attachment); } while (cur.moveToNext() != false); diff --git a/src/main/java/com/mattrobertson/zotable/app/data/CollectionAdapter.java b/src/main/java/com/mattrobertson/zotable/app/data/CollectionAdapter.java new file mode 100755 index 0000000..1167d6a --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/data/CollectionAdapter.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.data; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.media.Image; +import android.text.Layout; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.ResourceCursorAdapter; +import android.widget.TextView; +import android.widget.ToggleButton; + + +import com.mattrobertson.zotable.app.CollectionActivity; +import com.mattrobertson.zotable.app.R; +import com.mattrobertson.zotable.app.task.APIRequest; + +/** + * Exposes collection to be displayed by a ListView + * @author ajlyon + * + */ +public class CollectionAdapter extends ResourceCursorAdapter { + public static final String TAG = "CollectionAdapter"; + + public Context context; + LayoutInflater mInflater; + + Cursor cursor; + + public CollectionAdapter(Context context, Cursor cursor) { + super(context, R.layout.list_collection, cursor, false); + this.context = context; + this.cursor = cursor; + mInflater = ((Activity)context).getLayoutInflater(); + } + + /** + * Call this when the data has been updated-- it refreshes the cursor and notifies of the change + */ + public void notifyDataSetChanged() { + super.notifyDataSetChanged(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) + { + cursor = (Cursor)getItem(position); + cursor.moveToPosition(position); + + final ViewHolder holder; + + if ( convertView == null ) + { + // no view at this position - create a new one + convertView = mInflater.inflate(R.layout.list_collection, null); + holder = new ViewHolder(); + + holder.tvTitle = (TextView)convertView.findViewById(R.id.collection_title); + holder.tvInfo = (TextView)convertView.findViewById(R.id.collection_info); + holder.imgFav = (ImageView)convertView.findViewById(R.id.imgFavorite); + holder.imgExpand = (ImageView)convertView.findViewById(R.id.imgExpand); + + convertView.setTag (holder); + } + else + { + // recycle a View that already exists + holder = (ViewHolder) convertView.getTag (); + } + + final Database db = new Database(context); + + final ItemCollection collection = ItemCollection.load(cursor); + + if (collection.isFavorite()) + holder.imgFav.setImageResource(R.drawable.ic_star_filled); + else + holder.imgFav.setImageResource(R.drawable.ic_star_empty); + + // # of subcollections - used to display # & to determine expansion + int subSize = collection.getSubcollections(db).size(); + + holder.tvTitle.setText(collection.getTitle()); + + StringBuilder sb = new StringBuilder(); + sb.append(collection.getSize() + " items"); + sb.append("; " + subSize + " subcollections"); + + if(!collection.dirty.equals(APIRequest.API_CLEAN)) + sb.append("; "+collection.dirty); + + holder.tvInfo.setText(sb.toString()); + + // Allow expansion to subcollections? + if (subSize > 0) { + holder.imgExpand.setVisibility(View.VISIBLE); + + holder.imgExpand.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent i = new Intent(context, CollectionActivity.class); + i.putExtra("com.mattrobertson.zotable.app.collectionKey", collection.getKey()); + context.startActivity(i); + } + }); + } + else { + holder.imgExpand.setVisibility(View.GONE); + } + + // Listener for "Fav" star + holder.imgFav.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (collection.isFavorite()) { + holder.imgFav.setImageResource(R.drawable.ic_star_empty); + collection.setFavorite(false); + collection.save(db); + } else { + holder.imgFav.setImageResource(R.drawable.ic_star_filled); + collection.setFavorite(true); + collection.save(db); + } + } + }); + + + db.close(); + + return convertView; + } + + static class ViewHolder{ + TextView tvTitle; + TextView tvInfo; + ImageView imgFav; + ImageView imgExpand; + } + + public View newView(Context context, Cursor cur, ViewGroup parent) { + LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + return li.inflate(R.layout.list_collection, parent, false); + } + + @Override + public void bindView(View view, final Context context, Cursor cursor) { + TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); + TextView tvInfo = (TextView)view.findViewById(R.id.collection_info); + final ImageView imgFav = (ImageView)view.findViewById(R.id.imgFavorite); + final ImageView imgExpand = (ImageView)view.findViewById(R.id.imgExpand); + + final Database db = new Database(context); + + final ItemCollection collection = ItemCollection.load(cursor); + + if (collection.isFavorite()) + imgFav.setImageResource(R.drawable.ic_star_filled); + else + imgFav.setImageResource(R.drawable.ic_star_empty); + + // # of subcollections - used to display # & to determine expansion + int subSize = collection.getSubcollections(db).size(); + + tvTitle.setText(collection.getTitle()); + StringBuilder sb = new StringBuilder(); + sb.append(collection.getSize() + " items"); + sb.append("; " + subSize + " subcollections"); + if(!collection.dirty.equals(APIRequest.API_CLEAN)) + sb.append("; "+collection.dirty); + tvInfo.setText(sb.toString()); + + // Allow expansion to subcollections? + if (subSize > 0) { + imgExpand.setVisibility(View.VISIBLE); + + imgExpand.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent i = new Intent(context, CollectionActivity.class); + i.putExtra("com.mattrobertson.zotable.app.collectionKey", collection.getKey()); + context.startActivity(i); + } + }); + } + + // Listener for "Fav" star + imgFav.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (collection.isFavorite()) { + imgFav.setImageResource(R.drawable.ic_star_empty); + collection.setFavorite(false); + collection.save(db); + } else { + imgFav.setImageResource(R.drawable.ic_star_filled); + collection.setFavorite(true); + collection.save(db); + } + } + }); + + + db.close(); + } +} diff --git a/src/main/java/com/gimranov/zandy/app/data/Creator.java b/src/main/java/com/mattrobertson/zotable/app/data/Creator.java old mode 100644 new mode 100755 similarity index 89% rename from src/main/java/com/gimranov/zandy/app/data/Creator.java rename to src/main/java/com/mattrobertson/zotable/app/data/Creator.java index 6f8e975..dd09ba2 --- a/src/main/java/com/gimranov/zandy/app/data/Creator.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/Creator.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import org.json.JSONException; import org.json.JSONObject; @@ -33,7 +33,7 @@ public class Creator { private int dbId; - public static final String TAG = "com.gimranov.zandy.app.data.Creator"; + public static final String TAG = "Creator"; /** * A Creator, given type, a single string, and a boolean mode. diff --git a/src/main/java/com/gimranov/zandy/app/data/Database.java b/src/main/java/com/mattrobertson/zotable/app/data/Database.java old mode 100644 new mode 100755 similarity index 83% rename from src/main/java/com/gimranov/zandy/app/data/Database.java rename to src/main/java/com/mattrobertson/zotable/app/data/Database.java index ddeeaab..8aa38ee --- a/src/main/java/com/gimranov/zandy/app/data/Database.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/Database.java @@ -1,4 +1,20 @@ -package com.gimranov.zandy.app.data; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.data; import android.content.Context; import android.database.Cursor; @@ -9,20 +25,21 @@ import android.util.Log; public class Database { - private static final String TAG = "com.gimranov.zandy.app.data.Database"; + private static final String TAG = "Database"; public static final String[] ITEMCOLS = {"item_title", "item_type", "item_content", "etag", "dirty", "_id", "item_key", "item_year", "item_creator", "timestamp", "item_children"}; public static final String[] COLLCOLS = {"collection_name", "collection_parent", "etag", "dirty", "_id", "collection_key", - "collection_size", "timestamp"}; + "collection_size", "timestamp","fav"}; public static final String[] ATTCOLS = { "_id", "attachment_key", "item_key", "title", "filename", "url", "status", "etag", "dirty", "content" }; public static final String[] REQUESTCOLS = {"_id", "uuid", "type", "query", "key", "method", "disposition", "if_match", "update_key", "update_type", "created", "last_attempt", "status", "body"}; + public static final String[] TAGCOLS = {"_id","item_id","tag"}; // the database version; increment to call update private static final int DATABASE_VERSION = 20; @@ -41,7 +58,7 @@ public void resetAllData() { Log.d(TAG, "Dropping tables to reset database"); String[] tables = {"collections", "items", "creators", "children", "itemtocreators", "itemtocollections", "deleteditems", "attachments", - "apirequests", "notes"}; + "apirequests", "notes","tags"}; String[] args = {}; for (int i = 0; i < tables.length; i++) { rawQuery("DROP TABLE IF EXISTS " + tables[i], args); @@ -114,7 +131,7 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { // table creation statements // for temp table creation to work, must have (_id as first field private static final String COLLECTIONS_CREATE = - "create table collections"+ + "create table collections"+ " (_id integer primary key autoincrement, " + "collection_name text not null, " + "collection_key string unique, " + @@ -123,10 +140,11 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { "collection_size int, " + "etag string, " + "dirty string, " + - "timestamp string);"; + "timestamp string, " + + "fav int);"; private static final String ITEMS_CREATE = - "create table items"+ + "create table items"+ " (_id integer primary key autoincrement, " + "item_key string unique, " + "item_title string not null, " + @@ -140,7 +158,7 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { "timestamp string);"; private static final String CREATORS_CREATE = - "create table creators"+ + "create table creators"+ " (_id integer primary key autoincrement, " + "name string, " + "firstName string, " + @@ -148,22 +166,22 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { "creatorType string );"; private static final String ITEM_TO_CREATORS_CREATE = - "create table itemtocreators"+ + "create table itemtocreators"+ " (_id integer primary key autoincrement, " + "creator_id int not null, item_id int not null);"; private static final String ITEM_TO_COLLECTIONS_CREATE = - "create table itemtocollections"+ + "create table itemtocollections"+ " (_id integer primary key autoincrement, " + "collection_id int not null, item_id int not null);"; private static final String DELETED_ITEMS_CREATE = - "create table deleteditems"+ + "create table deleteditems"+ " (_id integer primary key autoincrement, " + "item_key string not null, etag string not null);"; private static final String ATTACHMENTS_CREATE = - "create table attachments"+ + "create table attachments"+ " (_id integer primary key autoincrement, " + "item_key string not null, " + "attachment_key string not null, " @@ -176,7 +194,7 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { + "dirty string);"; private static final String APIREQUESTS_CREATE = - "create table apirequests"+ + "create table apirequests"+ " (_id integer primary key autoincrement, " + "uuid string unique, " + "type string, " @@ -194,7 +212,7 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { /* We don't use this table right now */ private static final String NOTES_CREATE = - "create table notes"+ + "create table notes"+ " (_id integer primary key autoincrement, " + "item_key string, " + "note_key string not null, " @@ -204,6 +222,14 @@ private static class DatabaseOpenHelper extends SQLiteOpenHelper { + "status string, " + "content string, " + "etag string);"; + + // Note: Each pair has a Unique constraint + private static final String TAGS_CREATE = + "create table tags("+ + " _id integer primary key autoincrement, " + + "item_id int not null," + + "tag string not null," + + "UNIQUE(item_id, tag) ON CONFLICT REPLACE);"; DatabaseOpenHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); @@ -229,6 +255,7 @@ public void onCreate(SQLiteDatabase db) db.execSQL(ATTACHMENTS_CREATE); db.execSQL(NOTES_CREATE); db.execSQL(APIREQUESTS_CREATE); + db.execSQL(TAGS_CREATE); } @@ -254,7 +281,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, } if (oldVersion == 15 && newVersion > 15) { // here, we just added a table - db.execSQL("create table if not exists deleteditems"+ + db.execSQL("create table if not exists deleteditems"+ " (_id integer primary key autoincrement, " + "item_key int not null, etag int not null);"); } diff --git a/src/main/java/com/gimranov/zandy/app/data/Item.java b/src/main/java/com/mattrobertson/zotable/app/data/Item.java old mode 100644 new mode 100755 similarity index 90% rename from src/main/java/com/gimranov/zandy/app/data/Item.java rename to src/main/java/com/mattrobertson/zotable/app/data/Item.java index e6915ab..468da50 --- a/src/main/java/com/gimranov/zandy/app/data/Item.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/Item.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import java.util.ArrayList; import java.util.Collections; @@ -30,8 +30,9 @@ import android.os.Bundle; import android.util.Log; -import com.gimranov.zandy.app.R; -import com.gimranov.zandy.app.task.APIRequest; + +import com.mattrobertson.zotable.app.R; +import com.mattrobertson.zotable.app.task.APIRequest; public class Item { private String id; @@ -56,9 +57,9 @@ public class Item { /** * Queue of dirty items to be sent to the server */ - public static ArrayList queue = new ArrayList(); + public static ArrayList dirtyQueue = new ArrayList(); - private static final String TAG = "com.gimranov.zandy.app.data.Item"; + private static final String TAG = "Item"; /** * The next two types are arrays of information on items that we need @@ -145,6 +146,24 @@ public void setTitle(String title) { } } + public void setDate(String date) { + if (date == null) + date = ""; + + this.content.remove("date"); + + try { + this.content.put("date", date); + + // TODO: strip year from date + + if (!APIRequest.API_CLEAN.equals(this.dirty)) + this.dirty = APIRequest.API_DIRTY; + } catch (JSONException e) { + Log.e(TAG, "Exception setting date", e); + } + } + /* * These can't be propagated, so they only make sense before the item has * been saved to the API. @@ -186,7 +205,7 @@ public JSONObject getContent() { } public void setContent(JSONObject content) { - if (!this.content.toString().equals(content.toString())) { + if (!this.content.toString().equals(content.toString())) { // not identical -- some update to the item has occurred if (!APIRequest.API_CLEAN.equals(this.dirty)) this.dirty = APIRequest.API_DIRTY; this.content = content; @@ -387,22 +406,14 @@ public int compare(Bundle b1, Bundle b2) { } /** - * Makes ArrayList from the present item's tags Primarily for use - * with TagActivity, but who knows? + * Makes ArrayList from the present item's tags */ public ArrayList tagsToBundleArray() { JSONObject itemContent = this.content; - /* - * Here we walk through the data and make Bundles to send to the - * ArrayAdapter. There should be no real risk of JSON exceptions, since - * the JSON was checked when initialized in the Item object. - * - * Each Bundle has three keys: "itemKey", "tag", and "type" - */ ArrayList rows = new ArrayList(); - Bundle b = new Bundle(); + Bundle b; if (!itemContent.has("tags")) { return rows; @@ -410,11 +421,11 @@ public ArrayList tagsToBundleArray() { try { JSONArray tags = itemContent.getJSONArray("tags"); + Log.d(TAG, tags.toString()); for (int i = 0; i < tags.length(); i++) { b = new Bundle(); - // Type is not always specified, but we try to get it - // and fall back to 0 when missing. + // Type is not always specified, but we try to get it and fall back to 0 when missing. Log.d(TAG, tags.getJSONObject(i).toString()); if (tags.getJSONObject(i).has("type")) b.putInt("type", tags.getJSONObject(i).optInt("type", 0)); @@ -429,6 +440,64 @@ public ArrayList tagsToBundleArray() { return rows; } + public boolean hasTag(String tag) { + ArrayList tags = tagsToBundleArray(); + + for (Bundle t : tags) { + if (t.getString("tag").equals(tag)) { + return true; + } + } + + return false; + } + + public void addTag(String tag) { + if (hasTag(tag)) + return; + + JSONObject itemContent = this.content; + + try { + // If there is no tags array for some reason, create one + if (!itemContent.has("tags")) + itemContent.put("tags", new JSONArray()); + + JSONArray jsoTags = itemContent.getJSONArray("tags"); + + JSONObject objTag = new JSONObject(); + objTag.put("tag",tag); + objTag.put("type","User"); // assuming tag is being added by user (as opposed to "Auto") + objTag.put("itemKey",this.key); + + jsoTags.put(objTag); + } + catch (JSONException e) { + Log.e(TAG, e.getMessage()); + } + } + + public void removeTag(String tag) { + if ( ! hasTag(tag)) + return; + + JSONObject itemContent = this.content; + + try { + JSONArray jsoTags = itemContent.getJSONArray("tags"); + + for (int i=0; i < jsoTags.length(); i++) { + if (jsoTags.getJSONObject(i).get("tag").equals(tag)) { + jsoTags.remove(i); + return; + } + } + } + catch (JSONException e) { + Log.e(TAG, e.getMessage()); + } + } + /** * Makes ArrayList from the present item's creators. Primarily for * use with CreatorActivity, but who knows? @@ -471,7 +540,7 @@ public ArrayList creatorsToBundleArray() { b.putString("lastName", creators.getJSONObject(i).optString( "lastName")); b.putString("name", creators.getJSONObject(i).optString( - "name")); + "name")); // If name is empty, fill with the others if (b.getString("name").equals("")) b.putString("name", b.getString("firstName") + " " @@ -491,20 +560,44 @@ public ArrayList creatorsToBundleArray() { * Saves the item's current state. Marking dirty should happen before this */ public void save(Database db) { + Item existing = load(key, db); - if (dbId == null && existing == null) { + + if (dbId == null && existing == null) { // Does Not Exist in db String[] args = { title, key, type, year, creatorSummary, content.toString(), etag, dirty, timestamp, children }; - Cursor cur = db - .rawQuery( - "insert into items (item_title, item_key, item_type, item_year, item_creator, item_content, etag, dirty, timestamp, item_children) " - + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - args); + Cursor cur = db.rawQuery( + "insert into items (item_title, item_key, item_type, item_year, item_creator, item_content, etag, dirty, timestamp, item_children) " + + "values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", args); if (cur != null) cur.close(); - Item fromDB = load(key, db); - dbId = fromDB.dbId; - } else { + Item itemFromDb = load(key, db); + dbId = itemFromDb.dbId; + + // Parse tags from JSON content & insert into db + try { + JSONArray jTagsArr = content.getJSONArray("tags"); + + Cursor curTags = null; + + for (int i=0; i atts = Attachment.forItem(this, db); for (Attachment a : atts) { a.delete(db); @@ -692,7 +809,7 @@ public static void setTag(String itemKey, String oldTag, String newTag, } item.content.put("tags", newTags); } catch (JSONException e) { - Log.e(TAG,"Caught JSON exception when we tried to modify the JSON content",e); + Log.e(TAG, "Caught JSON exception when we tried to modify the JSON content", e); } item.dirty = APIRequest.API_DIRTY; item.save(db); @@ -709,7 +826,7 @@ public static void setTag(String itemKey, String oldTag, String newTag, public static void setCreator(String itemKey, Creator c, int position, Database db) { // Load the item Item item = load(itemKey, db); - + try { JSONArray creators = item.content.getJSONArray("creators"); @@ -746,11 +863,11 @@ public static void setCreator(String itemKey, Creator c, int position, Database } /** - * Identifies dirty items in the database and queues them for syncing + * Identifies dirty (actually, non-clean) items in the database and queues them for syncing */ - public static void queue(Database db) { - Log.d(TAG, "Clearing item dirty queue before repopulation"); - queue.clear(); + public static void buildDirtyQueue(Database db) { + Log.d(TAG, "Clearing dirty-item queue before repopulation"); + dirtyQueue.clear(); Item item; String[] cols = Database.ITEMCOLS; String[] args = { APIRequest.API_CLEAN }; @@ -759,20 +876,29 @@ public static void queue(Database db) { if (cur == null) { Log.d(TAG, "No dirty items found in database"); - queue.clear(); + dirtyQueue.clear(); return; } do { - Log.d(TAG, "Adding item to dirty queue"); item = load(cur); - queue.add(item); + dirtyQueue.add(item); + Log.d(TAG, "Adding item to dirty queue: " + item.getTitle()); } while (cur.moveToNext() != false); if (cur != null) cur.close(); } + public static void setAllClean(Database db) { + Log.d(TAG, "Setting all items to sync-status=clean"); + dirtyQueue.clear(); + + String[] args = {APIRequest.API_CLEAN}; + + db.rawQuery("update items set dirty=?", args); + } + /** * Maps types to the resources providing images for them. * diff --git a/src/main/java/com/gimranov/zandy/app/data/ItemAdapter.java b/src/main/java/com/mattrobertson/zotable/app/data/ItemAdapter.java old mode 100644 new mode 100755 similarity index 86% rename from src/main/java/com/gimranov/zandy/app/data/ItemAdapter.java rename to src/main/java/com/mattrobertson/zotable/app/data/ItemAdapter.java index ce16e02..c8a7c6e --- a/src/main/java/com/gimranov/zandy/app/data/ItemAdapter.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/ItemAdapter.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import android.content.Context; import android.database.Cursor; @@ -26,7 +26,8 @@ import android.widget.ResourceCursorAdapter; import android.widget.TextView; -import com.gimranov.zandy.app.R; +import com.mattrobertson.zotable.app.R; + /** * Exposes items to be displayed by a ListView @@ -34,7 +35,7 @@ * */ public class ItemAdapter extends ResourceCursorAdapter { - private static final String TAG = "com.gimranov.zandy.app.data.ItemAdapter"; + private static final String TAG = "ItemAdapter"; public ItemAdapter(Context context, Cursor cursor) { super(context, R.layout.list_item, cursor, false); diff --git a/src/main/java/com/gimranov/zandy/app/data/ItemCollection.java b/src/main/java/com/mattrobertson/zotable/app/data/ItemCollection.java old mode 100644 new mode 100755 similarity index 88% rename from src/main/java/com/gimranov/zandy/app/data/ItemCollection.java rename to src/main/java/com/mattrobertson/zotable/app/data/ItemCollection.java index b1db0f0..b100dfe --- a/src/main/java/com/gimranov/zandy/app/data/ItemCollection.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/ItemCollection.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import java.util.ArrayList; import java.util.HashSet; @@ -24,7 +24,7 @@ import android.database.sqlite.SQLiteStatement; import android.util.Log; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.task.APIRequest; /** * Represents a Zotero collection of Item objects. Collections can @@ -43,7 +43,7 @@ public class ItemCollection extends HashSet { */ private static final long serialVersionUID = -4673800475017605707L; - private static final String TAG = "com.gimranov.zandy.app.data.ItemCollection"; + private static final String TAG = "ItemCollection"; /** * Queue of dirty collections to be sent to the server @@ -54,6 +54,7 @@ public class ItemCollection extends HashSet { private String title; private String key; private String etag; + private boolean isFav; /** * Subcollections of this collection. This is accessed through @@ -148,6 +149,14 @@ public void setTitle(String title) { this.dirty = APIRequest.API_DIRTY; } } + + public boolean isFavorite() { + return isFav; + } + + public void setFavorite(boolean fav) { + isFav = fav; + } /* I'm not sure how easy this is to propagate to the API */ public ItemCollection getParent(Database db) { @@ -225,7 +234,7 @@ public boolean add(Item item, boolean fromAPI, Database db) { } super.add(item); - Log.d(TAG, "Item added to collection"); + Log.d(TAG, "Item added to collection: " + item.getTitle() + " (" + getTitle() + ")"); if (!fromAPI) { Log.d(TAG, "Saving new collection membership request to database"); APIRequest req = APIRequest.add(item, this); @@ -260,21 +269,39 @@ public void save(Database db) { if (existing == null) { try { SQLiteStatement insert = db.compileStatement("insert or replace into collections " + - "(collection_name, collection_key, collection_parent, etag, dirty, collection_size, timestamp)" + - " values (?, ?, ?, ?, ?, ?, ?)"); + "(collection_name, collection_key, collection_parent, etag, dirty, collection_size, timestamp, fav)" + + " values (?, ?, ?, ?, ?, ?, ?, ?)"); // Why, oh why does bind* use 1-based indexing? And cur.get* uses 0-based! insert.bindString(1, title); - if (key == null) insert.bindNull(2); - else insert.bindString(2, key); - if (parentKey == null) insert.bindNull(3); - else insert.bindString(3, parentKey); - if (etag == null) insert.bindNull(4); - else insert.bindString(4, etag); - if (dirty == null) insert.bindNull(5); - else insert.bindString(5, dirty); + if (key == null) + insert.bindNull(2); + else + insert.bindString(2, key); + + if (parentKey == null) + insert.bindNull(3); + else + insert.bindString(3, parentKey); + + if (etag == null) + insert.bindNull(4); + else + insert.bindString(4, etag); + + if (dirty == null) + insert.bindNull(5); + else + insert.bindString(5, dirty); + insert.bindLong(6, size); - if (timestamp == null) insert.bindNull(7); - else insert.bindString(7, timestamp); + + if (timestamp == null) + insert.bindNull(7); + else + insert.bindString(7, timestamp); + + insert.bindLong(8, isFav ? 1 : 0); + insert.executeInsert(); insert.clearBindings(); insert.close(); @@ -294,17 +321,30 @@ public void save(Database db) { dbId = existing.dbId; try { SQLiteStatement update = db.compileStatement("update collections set " + - "collection_name=?, etag=?, dirty=?, collection_size=?, timestamp=?" + + "collection_name=?, etag=?, dirty=?, collection_size=?, timestamp=?, fav=?" + " where _id=?"); update.bindString(1, title); - if (etag == null) update.bindNull(2); - else update.bindString(2, etag); - if (dirty == null) update.bindNull(3); - else update.bindString(3, dirty); + if (etag == null) + update.bindNull(2); + else + update.bindString(2, etag); + + if (dirty == null) + update.bindNull(3); + else + update.bindString(3, dirty); + update.bindLong(4, size); - if (timestamp == null) update.bindNull(5); - else update.bindString(5, timestamp); - update.bindString(6, dbId); + + if (timestamp == null) + update.bindNull(5); + else + update.bindString(5, timestamp); + + update.bindLong(6, isFav ? 1 : 0); + + update.bindString(7, dbId); + update.executeInsert(); update.clearBindings(); update.close(); @@ -381,7 +421,7 @@ public void loadChildren(Database db) { String[] args = { dbId }; Cursor cursor = db.rawQuery("SELECT item_title, item_type, item_content, etag, dirty, items._id, item_key, item_year, item_creator, items.timestamp, item_children" + - " FROM items, itemtocollections WHERE items._id = item_id AND collection_id=? ORDER BY item_title", + " FROM items, itemtocollections WHERE items._id = item_id AND collection_id=? ORDER BY item_title", args); if (cursor != null) { cursor.moveToFirst(); @@ -442,7 +482,7 @@ public static ItemCollection load(String collKey, Database db) { if (collKey == null) return null; String[] cols = Database.COLLCOLS; String[] args = { collKey }; - Log.i(TAG, "Loading collection with key: "+collKey); + Log.i(TAG, "Loading collection with key: " + collKey); Cursor cur = db.query("collections", cols, "collection_key=?", args, null, null, null, null); ItemCollection coll = load(cur); @@ -467,7 +507,7 @@ public static ItemCollection load(Cursor cur) { if (cur == null) { return null; } - + coll.setTitle(cur.getString(0)); coll.setParent(cur.getString(1)); coll.etag = cur.getString(2); @@ -476,6 +516,8 @@ public static ItemCollection load(Cursor cur) { coll.setKey(cur.getString(5)); coll.size = cur.getInt(6); coll.timestamp = cur.getString(7); + coll.isFav = cur.getLong(8) == 1 ? true : false; + return coll; } @@ -483,7 +525,7 @@ public static ItemCollection load(Cursor cur) { * Identifies stale or missing collections in the database and queues them for syncing */ public static void queue(Database db) { - Log.d(TAG,"Clearing dirty queue before repopulation"); + Log.d(TAG,"Clearing dirty buildDirtyQueue before repopulation"); queue.clear(); ItemCollection coll; String[] cols = Database.COLLCOLS; @@ -497,7 +539,7 @@ public static void queue(Database db) { } do { - Log.d(TAG,"Adding collection to dirty queue"); + Log.d(TAG,"Adding collection to dirty buildDirtyQueue"); coll = load(cur); queue.add(coll); } while (cur.moveToNext() != false); @@ -541,7 +583,7 @@ public static ArrayList getCollections(Item i, Database db) { String[] args = { i.dbId }; Cursor cursor = db.rawQuery("SELECT collection_name, collection_parent," + " etag, dirty, collections._id, collection_key, collection_size," + - " timestamp FROM collections, itemtocollections" + + " timestamp, fav FROM collections, itemtocollections" + " WHERE collections._id = collection_id AND item_id=?" + " ORDER BY collection_name", args); diff --git a/src/main/java/com/gimranov/zandy/app/data/CollectionAdapter.java b/src/main/java/com/mattrobertson/zotable/app/data/TagAdapter.java old mode 100644 new mode 100755 similarity index 55% rename from src/main/java/com/gimranov/zandy/app/data/CollectionAdapter.java rename to src/main/java/com/mattrobertson/zotable/app/data/TagAdapter.java index 4972b7e..5216d73 --- a/src/main/java/com/gimranov/zandy/app/data/CollectionAdapter.java +++ b/src/main/java/com/mattrobertson/zotable/app/data/TagAdapter.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.data; +package com.mattrobertson.zotable.app.data; import android.content.Context; import android.database.Cursor; @@ -24,27 +24,27 @@ import android.widget.ResourceCursorAdapter; import android.widget.TextView; -import com.gimranov.zandy.app.R; -import com.gimranov.zandy.app.task.APIRequest; +import com.mattrobertson.zotable.app.R; +import com.mattrobertson.zotable.app.task.APIRequest; /** * Exposes collection to be displayed by a ListView * @author ajlyon * */ -public class CollectionAdapter extends ResourceCursorAdapter { - public static final String TAG = "com.gimranov.zandy.app.data.CollectionAdapter"; +public class TagAdapter extends ResourceCursorAdapter { + public static final String TAG = "TagAdapter"; public Context context; - - public CollectionAdapter(Context context, Cursor cursor) { - super(context, R.layout.list_collection, cursor, false); + + public TagAdapter(Context context, Cursor cursor) { + super(context, R.layout.list_tag, cursor, false); this.context = context; } public View newView(Context context, Cursor cur, ViewGroup parent) { LayoutInflater li = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - return li.inflate(R.layout.list_collection, parent, false); + return li.inflate(R.layout.list_tag, parent, false); } /** @@ -56,19 +56,19 @@ public void notifyDataSetChanged() { @Override public void bindView(View view, Context context, Cursor cursor) { - TextView tvTitle = (TextView)view.findViewById(R.id.collection_title); - TextView tvInfo = (TextView)view.findViewById(R.id.collection_info); - + TextView tvTag = (TextView)view.findViewById(R.id.tag_name); + String tagName = cursor.getString(2); + tvTag.setText(tagName); + Database db = new Database(context); - - ItemCollection collection = ItemCollection.load(cursor); - tvTitle.setText(collection.getTitle()); - StringBuilder sb = new StringBuilder(); - sb.append(collection.getSize() + " items"); - sb.append("; " + collection.getSubcollections(db).size() + " subcollections"); - if(!collection.dirty.equals(APIRequest.API_CLEAN)) - sb.append("; "+collection.dirty); - tvInfo.setText(sb.toString()); + + String[] args = {tagName}; + Cursor c = db.rawQuery("SELECT COUNT(*) FROM tags WHERE tag=?", args); + int itemCount = c.getInt(0); + + TextView tvItemCount = (TextView)view.findViewById(R.id.item_count); + tvItemCount.setText("Items: " + itemCount); + db.close(); } diff --git a/src/main/java/com/mattrobertson/zotable/app/task/APIEvent.java b/src/main/java/com/mattrobertson/zotable/app/task/APIEvent.java new file mode 100755 index 0000000..279b8af --- /dev/null +++ b/src/main/java/com/mattrobertson/zotable/app/task/APIEvent.java @@ -0,0 +1,26 @@ +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.task; + +public interface APIEvent { + public void onComplete(APIRequest request); + + public void onUpdate(APIRequest request); + + public void onError(APIRequest request, Exception exception); + public void onError(APIRequest request, int error); +} diff --git a/src/main/java/com/gimranov/zandy/app/task/APIException.java b/src/main/java/com/mattrobertson/zotable/app/task/APIException.java old mode 100644 new mode 100755 similarity index 81% rename from src/main/java/com/gimranov/zandy/app/task/APIException.java rename to src/main/java/com/mattrobertson/zotable/app/task/APIException.java index 016054e..75f60a7 --- a/src/main/java/com/gimranov/zandy/app/task/APIException.java +++ b/src/main/java/com/mattrobertson/zotable/app/task/APIException.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.task; +package com.mattrobertson.zotable.app.task; public class APIException extends Exception { diff --git a/src/main/java/com/gimranov/zandy/app/task/APIRequest.java b/src/main/java/com/mattrobertson/zotable/app/task/APIRequest.java old mode 100644 new mode 100755 similarity index 97% rename from src/main/java/com/gimranov/zandy/app/task/APIRequest.java rename to src/main/java/com/mattrobertson/zotable/app/task/APIRequest.java index c84297d..b3233bf --- a/src/main/java/com/gimranov/zandy/app/task/APIRequest.java +++ b/src/main/java/com/mattrobertson/zotable/app/task/APIRequest.java @@ -1,20 +1,20 @@ /******************************************************************************* - * This file is part of Zandy. + * This file is part of Zotable. * - * Zandy is free software: you can redistribute it and/or modify + * Zotable 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, + * Zotable 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 . + * along with Zotable. If not, see . ******************************************************************************/ -package com.gimranov.zandy.app.task; +package com.mattrobertson.zotable.app.task; import android.content.Context; import android.database.Cursor; @@ -22,12 +22,12 @@ import android.database.sqlite.SQLiteStatement; import android.os.Handler; import android.util.Log; -import com.gimranov.zandy.app.ServerCredentials; -import com.gimranov.zandy.app.XMLResponseParser; -import com.gimranov.zandy.app.data.Attachment; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; -import com.gimranov.zandy.app.data.ItemCollection; +import com.mattrobertson.zotable.app.ServerCredentials; +import com.mattrobertson.zotable.app.XMLResponseParser; +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; +import com.mattrobertson.zotable.app.data.ItemCollection; import org.apache.http.HttpEntity; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; @@ -67,7 +67,7 @@ * */ public class APIRequest { - private static final String TAG = "com.gimranov.zandy.app.task.APIRequest"; + private static final String TAG = "APIRequest"; /** * Statuses used for items and collections. They are currently strings, but @@ -627,12 +627,12 @@ public void issue(Database db, ServerCredentials cred) throws APIException { * ITEM_ATTACHMENT_UPDATE */ if ("xml".equals(disposition)) { + Log.i(TAG, "Using XMLResponseParser"); XMLResponseParser parse = new XMLResponseParser(this); // These types will always have a temporary key that we've // been using locally, and which should be replaced by the // incoming item key. - if (type == ITEM_NEW - || type == ITEM_ATTACHMENT_NEW) { + if (type == ITEM_NEW || type == ITEM_ATTACHMENT_NEW) { parse.update(updateType, updateKey); } @@ -647,8 +647,7 @@ public void issue(Database db, ServerCredentials cred) throws APIException { // shouldn't be anything else, so we throw in that case // for good measure if (!"get".equals(method)) { - throw new APIException(APIException.INVALID_METHOD, - "Unexpected method: "+method, this); + throw new APIException(APIException.INVALID_METHOD, "Unexpected method: "+method, this); } hr = client.execute(get); } @@ -732,6 +731,7 @@ public void issue(Database db, ServerCredentials cred) throws APIException { * supported; they should have a disposition of their own. */ else { + Log.i(TAG, "Using BasicResponseHandler"); BasicResponseHandler brh = new BasicResponseHandler(); String resp; diff --git a/src/main/java/com/gimranov/zandy/app/task/ZoteroAPITask.java b/src/main/java/com/mattrobertson/zotable/app/task/ZoteroAPITask.java old mode 100644 new mode 100755 similarity index 74% rename from src/main/java/com/gimranov/zandy/app/task/ZoteroAPITask.java rename to src/main/java/com/mattrobertson/zotable/app/task/ZoteroAPITask.java index 0a4e334..21796ab --- a/src/main/java/com/gimranov/zandy/app/task/ZoteroAPITask.java +++ b/src/main/java/com/mattrobertson/zotable/app/task/ZoteroAPITask.java @@ -1,25 +1,20 @@ -/* - * Zandy - * Based in part on Mendroid, Copyright 2011 Martin Paul Eve +/******************************************************************************* + * This file is part of Zotable. * - * This file is part of Zandy. + * Zotable 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 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 General Public License for more details. + * Zotable 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 General Public License - * along with Zandy. If not, see . - * - */ - -package com.gimranov.zandy.app.task; + * You should have received a copy of the GNU Affero General Public License + * along with Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.task; import java.util.ArrayList; @@ -29,11 +24,11 @@ import android.os.Message; import android.util.Log; -import com.gimranov.zandy.app.ServerCredentials; -import com.gimranov.zandy.app.XMLResponseParser; -import com.gimranov.zandy.app.data.Attachment; -import com.gimranov.zandy.app.data.Database; -import com.gimranov.zandy.app.data.Item; +import com.mattrobertson.zotable.app.ServerCredentials; +import com.mattrobertson.zotable.app.XMLResponseParser; +import com.mattrobertson.zotable.app.data.Attachment; +import com.mattrobertson.zotable.app.data.Database; +import com.mattrobertson.zotable.app.data.Item; /** * Executes one or more API requests asynchronously. @@ -48,7 +43,7 @@ * */ public class ZoteroAPITask extends AsyncTask { - private static final String TAG = "com.gimranov.zandy.app.task.ZoteroAPITask"; + private static final String TAG = "ZoteroAPITask"; public ArrayList deletions; public ArrayList queue; @@ -122,16 +117,16 @@ public Message doFetch(APIRequest... reqs) return msg; } - // The XML parser's queue is simply from following continuations in the paged + // The XML parser's buildDirtyQueue is simply from following continuations in the paged // feed. We shouldn't split out its requests... if (XMLResponseParser.queue != null && !XMLResponseParser.queue.isEmpty()) { Log.i(TAG, "Finished call, but adding " + XMLResponseParser.queue.size() + - " items to queue."); + " items to buildDirtyQueue."); queue.addAll(XMLResponseParser.queue); XMLResponseParser.queue.clear(); } else { - Log.i(TAG, "Finished call, and parser's request queue is empty"); + Log.i(TAG, "Finished call, and parser's request buildDirtyQueue is empty"); } } @@ -144,7 +139,7 @@ public Message doFetch(APIRequest... reqs) ArrayList toRemove = new ArrayList(); for (APIRequest r : queue) { if (r.type != APIRequest.ITEMS_CHILDREN) { - Log.d(TAG, "Removing request from queue since last page had old items: "+r.query); + Log.d(TAG, "Removing request from buildDirtyQueue since last page had old items: "+r.query); toRemove.add(r); } } @@ -158,12 +153,12 @@ public Message doFetch(APIRequest... reqs) // XXX I suspect that this calling of doFetch from doFetch might be the cause of our // out-of-memory situations. We may be able to accomplish the same thing by expecting // the code listening to our handler to fetch again if QUEUED_MORE is received. In that - // case, we could just save our queue here and really return. + // case, we could just save our buildDirtyQueue here and really return. // XXX Test: Here, we try to use doInBackground instead doInBackground(requests); - // Return a message with the number of requests added to the queue + // Return a message with the number of requests added to the buildDirtyQueue Message msg = Message.obtain(); msg.arg1 = APIRequest.QUEUED_MORE; msg.arg2 = requests.length; @@ -180,13 +175,13 @@ public Message doFetch(APIRequest... reqs) } Log.d(TAG, "Sending local changes"); - Item.queue(db); + Item.buildDirtyQueue(db); Attachment.queue(db); APIRequest[] templ = {}; ArrayList list = new ArrayList(); - for (Item i : Item.queue) { + for (Item i : Item.dirtyQueue) { list.add(cred.prep(APIRequest.update(i))); } @@ -194,7 +189,7 @@ public Message doFetch(APIRequest... reqs) list.add(cred.prep(APIRequest.update(a, db))); } - // This queue has deletions, collection memberships, and failing requests + // This buildDirtyQueue has deletions, collection memberships, and failing requests // We may want to filter it in the future list.addAll(APIRequest.queue(db)); diff --git a/src/main/java/com/gimranov/zandy/app/webdav/WebDavTrust.java b/src/main/java/com/mattrobertson/zotable/app/webdav/WebDavTrust.java old mode 100644 new mode 100755 similarity index 60% rename from src/main/java/com/gimranov/zandy/app/webdav/WebDavTrust.java rename to src/main/java/com/mattrobertson/zotable/app/webdav/WebDavTrust.java index 61faafb..db5f7d7 --- a/src/main/java/com/gimranov/zandy/app/webdav/WebDavTrust.java +++ b/src/main/java/com/mattrobertson/zotable/app/webdav/WebDavTrust.java @@ -1,4 +1,20 @@ -package com.gimranov.zandy.app.webdav; +/******************************************************************************* + * This file is part of Zotable. + * + * Zotable 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. + * + * Zotable 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 Zotable. If not, see . + ******************************************************************************/ +package com.mattrobertson.zotable.app.webdav; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; diff --git a/src/main/java/org/pdfparse/PDFDefines.java b/src/main/java/org/pdfparse/PDFDefines.java new file mode 100755 index 0000000..91f8178 --- /dev/null +++ b/src/main/java/org/pdfparse/PDFDefines.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse; + + +public final class PDFDefines { + public static final boolean DEBUG = true; + public static final boolean PRETTY_PRINT = true; + public static final int MAX_SCAN_RANGE = 100; + +} diff --git a/src/main/java/org/pdfparse/cds/PDFRectangle.java b/src/main/java/org/pdfparse/cds/PDFRectangle.java new file mode 100755 index 0000000..0bac09c --- /dev/null +++ b/src/main/java/org/pdfparse/cds/PDFRectangle.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cds; + +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.cos.COSArray; +import org.pdfparse.cos.COSObject; +import org.pdfparse.exception.EParseError; + +import java.io.IOException; +import java.io.OutputStream; + +public class PDFRectangle implements COSObject { + /** lower left x */ + private float llx = 0; + + /** lower left y */ + private float lly = 0; + + /** upper right x */ + private float urx = 0; + + /** upper right y */ + private float ury = 0; + + // constructors + + /** + * Constructs a PdfRectangle-object. + * + * @param llx lower left x + * @param lly lower left y + * @param urx upper right x + * @param ury upper right y + * + */ + + public PDFRectangle(float llx, float lly, float urx, float ury) { + this.llx = llx; + this.lly = lly; + this.urx = urx; + this.ury = ury; + normalize(); + } + + public PDFRectangle(COSArray array) { + this.llx = array.getInt(0); + this.lly = array.getInt(1); + this.urx = array.getInt(2); + this.ury = array.getInt(3); + normalize(); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + COSArray array = new COSArray(src, context); + this.llx = array.getInt(0); + this.lly = array.getInt(1); + this.urx = array.getInt(2); + this.ury = array.getInt(3); + normalize(); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + String s = String.format("[%.2f %.2f %.2f %.2f]", llx, lly, urx, ury); + dst.write(s.getBytes()); + } + + /** + * Return a string representation of this rectangle. + * + * @return This object as a string. + */ + public String toString() + { + return String.format("[%.2f %.2f %.2f %.2f]", llx, lly, urx, ury); + } + + public void normalize() { + float t; + if (llx > urx) { + t = llx; + llx = urx; + urx = t; + } + + if (lly > ury) { + t = lly; + lly = ury; + ury = t; + } + } + + /** + * Method to determine if the x/y point is inside this rectangle. + * @param x The x-coordinate to test. + * @param y The y-coordinate to test. + * @return True if the point is inside this rectangle. + */ + public boolean contains( float x, float y ) { + return x >= llx && x <= urx && + y >= lly && y <= ury; + } + + /** + * Get the width of this rectangle as calculated by + * upperRightX - lowerLeftX. + * + * @return The width of this rectangle. + */ + public float getWidth() { + return urx - llx; + } + + /** + * Get the height of this rectangle as calculated by + * upperRightY - lowerLeftY. + * + * @return The height of this rectangle. + */ + public float getHeight() { + return ury - lly; + } + + /** + * Move the rectangle the given relative amount. + * + * @param dx positive values will move rectangle to the right, negative's to the left. + * @param dy positive values will move the rectangle up, negative's down. + */ + public void move(float dx, float dy) { + llx += dx; + lly += dy; + urx += dx; + ury += dy; + } +} diff --git a/src/main/java/org/pdfparse/cos/COSArray.java b/src/main/java/org/pdfparse/cos/COSArray.java new file mode 100755 index 0000000..830c2a5 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSArray.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.parser.PDFParser; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; + + +public class COSArray extends ArrayList implements COSObject { + + public COSArray() { + super(); + } + + public COSArray(PDFRawData src, ParsingContext context) throws EParseError { + super(); + parse(src, context); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + src.pos++; // Skip '[' + src.skipWS(); + + while (src.pos < src.length) { + if (src.src[src.pos] == 0x5D) + break; // ']' + this.add( PDFParser.parseObject(src, context) ); + src.skipWS(); + } + if (src.pos == src.length) + return; + src.pos++; + src.skipWS(); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + dst.write(0x5B); // "[" + for (int i = 0; i < this.size(); i++) { + if (i != 0) { + if (i % 20 == 0) { + dst.write(0x0A); // "\n" + } else { + dst.write(0x20); // " "; + } + } + this.get(i).produce(dst, context); + } + dst.write(0x5D); // "]"; + } + + @Override + public String toString() { + return String.format("[ %d ]", this.size()); + } + + public int getInt(int idx) { + COSObject obj = get(idx); + if (obj instanceof COSNumber) return ((COSNumber)obj).intValue(); + else return 0; + } + + public float getFloat(int idx) { + COSObject obj = get(idx); + if (obj instanceof COSNumber) return ((COSNumber)obj).floatValue(); + else return 0; + } +} diff --git a/src/main/java/org/pdfparse/cos/COSBool.java b/src/main/java/org/pdfparse/cos/COSBool.java new file mode 100755 index 0000000..2b30cd0 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSBool.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; + +public class COSBool implements COSObject { + + static private final byte[] TRUE = {0x74, 0x72, 0x75, 0x65}; + static private final byte[] FALSE = {0x66, 0x61, 0x6c, 0x73, 0x65}; + public boolean value; + + public COSBool(Boolean val) { + value = val; + } + + @Override + public void parse(PDFRawData src, ParsingContext context) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + if (value) { + dst.write(TRUE); + } else { + dst.write(FALSE); + } + } + + @Override + public String toString() { + return String.valueOf(value); + } +} diff --git a/src/main/java/org/pdfparse/cos/COSDictionary.java b/src/main/java/org/pdfparse/cos/COSDictionary.java new file mode 100755 index 0000000..68e0e22 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSDictionary.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.*; +import org.pdfparse.cds.PDFRectangle; +import org.pdfparse.parser.PDFParser; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.parser.ParsingGetObject; +import org.pdfparse.utils.DateConverter; +import org.pdfparse.exception.EParseError; + + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Calendar; +import java.util.LinkedHashMap; + +public class COSDictionary extends LinkedHashMap implements COSObject { + //private LinkedHashMap map; + private static final byte[] S_OPEN = {0x3C, 0x3C}; + private static final byte[] S_OPEN_PP = {0x3C, 0x3C, 0xA}; + private static final byte[] S_CLOSE = {0x3E, 0x3E}; + private static final byte[] S_CLOSE_PP = {0x3E, 0x3E, 0xA}; + private static final byte[] S_NULL = {0x6E, 0x75, 0x6C, 0x6C}; // "null" + + public COSDictionary() { + super(); + } + + public COSDictionary(COSDictionary src, ParsingContext context) { + super.putAll(src); + } + + public COSDictionary(PDFRawData src, ParsingContext context) throws EParseError { + parse(src, context); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + //throw new UnsupportedOperationException("Not supported yet."); + src.pos +=2; + + while (src.pos < src.length) { + src.skipWS(); + if ((src.src[src.pos] == 0x3E)&&(src.src[src.pos+1] == 0x3E)) { // '>' + src.pos+=2; return; + } + + src.skipWS(); + COSName name = new COSName(src, context); + //if ((name.length == 0)||(name[0]!=0x2F)) throw new Error('This token is not a name: ' + name.toString()); // '/' + COSObject obj = PDFParser.parseObject(src, context); + this.put(name, obj); + } + throw new EParseError("Reach end of file while parsing dictionary"); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + COSObject obj; + if (PDFDefines.PRETTY_PRINT) + dst.write(S_OPEN_PP); // "<<\n" + else dst.write(S_OPEN); // "<<" + + for(COSName key: this.keySet()) { + key.produce(dst, context); + dst.write(0x20); + obj = this.get(key); + if (obj == null) + dst.write(S_NULL); + else + obj.produce(dst, context); + if (PDFDefines.PRETTY_PRINT) + dst.write(0xA); + } + + dst.write(S_CLOSE); + } + + @Override + public String toString() { + return String.format("<< %d >>", this.size()); + } + + private COSObject travel(COSObject obj, ParsingGetObject cache) throws EParseError { + int counter = 5; + while (obj instanceof COSReference) { + obj = cache.getObject((COSReference)obj); + if (counter-- == 0) + throw new EParseError("Infinite or too deep loop for " + obj.toString()); + } + return obj; + } + public static COSObject fetchValue(PDFRawData src) { + return null; + } + +// public void clear() { +// map.clear(); +// } + +// public boolean containsKey(String key) { +// return map.containsKey(key); +// } + +// public COSObject get(String key) { +// return map.get(key); +// } + +// public void set(String key, COSObject value) { +// map.put(key, value); +// } + + public boolean getBool(COSName name, boolean def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSBool) return ((COSBool)obj).value; + else return def_value; + } + + public boolean getBool(COSName name, ParsingGetObject cache, boolean def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSBool) return ((COSBool)obj).value; + else return def_value; + } + + public void setBool(COSName name, boolean value) { + COSBool v = new COSBool(value); + this.put(name, v); + } + + public int getInt(COSName name, int def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSNumber) return ((COSNumber)obj).intValue(); + else return def_value; + } + public int getInt(COSName name, ParsingGetObject cache, int def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSNumber) return ((COSNumber)obj).intValue(); + else return def_value; + } + public int getUInt(COSName name, int def_value) { + return getInt(name, def_value); + } + public int getUInt(COSName name, ParsingGetObject cache, int def_value) throws EParseError { + return getInt(name, cache, def_value); + } + + public void setInt(COSName name, int value) { + COSNumber v = new COSNumber(value); + this.put(name, v); + } + + public void setUInt(COSName name, int value) { + setInt(name, value); + } + + public String getStr(COSName name, String def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSString) return ((COSString)obj).getValue(); + else return def_value; + + } + + public String getStr(COSName name, ParsingGetObject cache, String def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj == null) return def_value; + if (obj instanceof COSString) return ((COSString)obj).getValue(); + return def_value; + } + public void setStr(COSName name, String value) { + this.put(name, new COSString(value)); + } + + public void setDate(COSName name, Calendar date) { + setStr(name, DateConverter.toString(date)); + } + + public Calendar getDate(COSName name, Calendar def_value) throws EParseError { + String date = getStr(name, ""); + if (date.equals("")) return null; + return DateConverter.toCalendar( date ); + } + + public Calendar getDate(COSName name, ParsingGetObject cache, Calendar def_value) throws EParseError { + String date = getStr(name, cache, ""); + if (date.equals("")) return null; + return DateConverter.toCalendar( date ); + } + + public COSName getName(COSName name, COSName def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSName) return (COSName)obj; + else return def_value; + } + + public COSName getName(COSName name, ParsingGetObject cache, COSName def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSName) return (COSName)obj; + else return def_value; + } + + public String getNameAsStr(COSName name, ParsingGetObject cache, String def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSName) return ((COSName)obj).asString(); + else return def_value; + } + + public void setName(COSName name, COSName value) { + this.put(name, value); + } + + public COSArray getArray(COSName name, COSArray def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSArray) return (COSArray)obj; + else return def_value; + } + public COSArray getArray(COSName name, ParsingGetObject cache, COSArray def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSArray) return (COSArray)obj; + else return def_value; + } + + public COSDictionary getDictionary(COSName name, COSDictionary def_value) { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSDictionary) return (COSDictionary)obj; + else return def_value; + } + public COSDictionary getDictionary(COSName name, ParsingGetObject cache, COSDictionary def_value) throws EParseError { + COSObject obj = this.get(name); + if (obj == null) return def_value; + if (obj instanceof COSReference) + obj = travel(obj, cache); + if (obj instanceof COSDictionary) return (COSDictionary)obj; + else return def_value; + } + + public byte[] getBlob(COSName name, byte[] def_value) { + throw new UnsupportedOperationException("Not supported yet."); + //COSObject obj = this.get(name); + //if (obj == null) return def_value; + //if (obj instanceof COSDictionary) return (COSDictionary)obj; + //else return def_value; + } + + public void setReference(COSName name, COSReference value) { + this.put(name, value); + } + + public void setReference(COSName name, int id, int gen) { + COSReference ref = new COSReference(id, gen); + this.put(name, ref); + } + + public COSReference getReference(COSName name) { + COSObject obj = this.get(name); + if (obj == null) return null; + if (obj instanceof COSReference) return (COSReference)obj; + else return null; + } + + public PDFRectangle getRectangle(COSName name) { + COSObject obj = this.get(name); + if (obj == null) return null; + if (obj instanceof COSArray) { + PDFRectangle rect = new PDFRectangle((COSArray)obj); + this.put(name, rect); // override existing COSArray with rectangle + return rect; + } + if (obj instanceof PDFRectangle) return (PDFRectangle)obj; + else return null; + } + + public void setRectangle(COSName name, PDFRectangle value) { + this.put(name, value); + } + +} diff --git a/src/main/java/org/pdfparse/cos/COSName.java b/src/main/java/org/pdfparse/cos/COSName.java new file mode 100755 index 0000000..fdfbc60 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSName.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Arrays; + + +public class COSName implements COSObject { + public static final COSName EMPTY = new COSName("/"); + public static final COSName UNKNOWN = new COSName("/Unknown"); + public static final COSName TRUE = new COSName("/True"); + public static final COSName FALSE = new COSName("/False"); + + public static final COSName PREV = new COSName("/Prev"); + public static final COSName XREFSTM = new COSName("/XRefStm"); + public static final COSName LENGTH = new COSName("/Length"); + public static final COSName TYPE = new COSName("/Type"); + public static final COSName XREF = new COSName("/XRef"); + public static final COSName W = new COSName("/W"); + public static final COSName SIZE = new COSName("/Size"); + public static final COSName INDEX = new COSName("/Index"); + public static final COSName FILTER = new COSName("/Filter"); + + public static final COSName FLATEDECODE = new COSName("/FlateDecode"); + public static final COSName FL = new COSName("/Fl"); + public static final COSName ASCIIHEXDECODE = new COSName("/ASCIIHexDecode"); + public static final COSName AHX = new COSName("/AHx"); + public static final COSName ASCII85DECODE = new COSName("/ASCII85Decode"); + public static final COSName A85 = new COSName("/A85"); + public static final COSName LZWDECODE = new COSName("/LZWDecode"); + public static final COSName CRYPT = new COSName("/Crypt"); + public static final COSName RUNLENGTHDECODE = new COSName("/RunLengthDecode"); + public static final COSName JPXDECODE = new COSName("/JPXDecode"); + public static final COSName CCITTFAXDECODE = new COSName("/CCITTFaxDecode"); + public static final COSName JBIG2DECODE = new COSName("/JBIG2Decode"); + + + + public static final COSName DCTDECODE = new COSName("/DCTDecode"); + public static final COSName ENCRYPT = new COSName("/Encrypt"); + public static final COSName DECODEPARMS = new COSName("/DecodeParms"); + public static final COSName PREDICTOR = new COSName("/Predictor"); + public static final COSName COLUMNS = new COSName("/Columns"); + public static final COSName COLORS = new COSName("/Colors"); + public static final COSName BITSPERCOMPONENT = new COSName("/BitsPerComponent"); + public static final COSName ROOT = new COSName("/Root"); + public static final COSName INFO = new COSName("/Info"); + public static final COSName ID = new COSName("/ID"); + + public static final COSName TITLE = new COSName("/Title"); + public static final COSName KEYWORDS = new COSName("/Keywords"); + public static final COSName SUBJECT = new COSName("/Subject"); + public static final COSName AUTHOR = new COSName("/Author"); + public static final COSName CREATOR = new COSName("/Creator"); + public static final COSName PRODUCER = new COSName("/Producer"); + public static final COSName CREATION_DATE = new COSName("/CreationDate"); + public static final COSName MOD_DATE = new COSName("/ModDate"); + public static final COSName TRAPPED = new COSName("/Trapped"); + + public static final COSName PAGES = new COSName("/Pages"); + public static final COSName METADATA = new COSName("/Metadata"); + public static final COSName COUNT = new COSName("/Count"); + public static final COSName CATALOG = new COSName("/Catalog"); + public static final COSName VERSION = new COSName("/Version"); + public static final COSName LANG = new COSName("/Lang"); + public static final COSName PAGELAYOUT = new COSName("/PageLayout"); + public static final COSName PAGEMODE = new COSName("/PageMode"); + + // A name object specifying the page layout shall be used when the document is opened: + public static final COSName PL_SINGLE_PAGE = new COSName("/SinglePage"); + public static final COSName PL_ONECOLUMN = new COSName("/OneColumn"); + public static final COSName PL_TWOCOLUMNLEFT = new COSName("/TwoColumnLeft"); + public static final COSName PL_TWOCOLUMNRIGHT = new COSName("/TwoColumnRight"); + public static final COSName PL_TWOPAGELEFT = new COSName("/TwoPageLeft"); + public static final COSName PL_TWOPAGERIGHT = new COSName("/TwoPageRight"); + + // A name object specifying how the document shall be displayed when opened: + public static final COSName PM_NONE = new COSName("/UseNone"); // Neither document outline nor thumbnail images visible + public static final COSName PM_OUTLINES = new COSName("/UseOutlines"); // Document outline visible + public static final COSName PM_THUMBS = new COSName("/UseThumbs"); // Thumbnail images visible + public static final COSName PM_FULLSCREEN = new COSName("/FullScreen"); // Full-screen mode, with no menu bar, window controls, or any other window visible + public static final COSName PM_OC = new COSName("/UseOC"); // (PDF 1.5) Optional content group panel visible + public static final COSName PM_ATTACHMENTS = new COSName("/UseAttachments"); // (PDF 1.6) Attachments panel visible + + + public static final COSName PARENT = new COSName("/PARENT"); + public static final COSName PAGE = new COSName("/PAGE"); + public static final COSName MEDIABOX = new COSName("/MediaBox"); + public static final COSName CROPBOX = new COSName("CropBox"); + public static final COSName KIDS = new COSName("Kids"); + + + + public static final COSName FIRST = new COSName("/First"); + public static final COSName N = new COSName("/N"); + + private static final int[] HEX = { // '0'..'f' + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15}; + + + + private byte[] value; + private int hc; + + public COSName(PDFRawData src, ParsingContext context) throws EParseError { + parse(src, context); + } + + public COSName(String val) { + value = val.getBytes(Charset.defaultCharset()); + hc = Arrays.hashCode(value); + } + + public String asString() { + return new String(value, Charset.defaultCharset()); + } + + @Override + public int hashCode() { + return hc; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (this == obj) + return true; + + if (getClass() != obj.getClass()) { + return false; + } + final COSName other = (COSName) obj; + if ((this.hc != other.hc) || !Arrays.equals(this.value, other.value)) { + return false; + } + return true; + } + + @Override + public String toString() { + return new String(value, Charset.defaultCharset()); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + src.skipWS(); + int p = src.pos; + int len = src.length; + int i, cnt = 0; + byte b, v1, v2; + boolean stop = false; + + if (src.src[src.pos] != 0x2F) + throw new EParseError("Expected SOLIDUS sign #2F in name object, but got " + Integer.toHexString(src.src[p])); + + p++; // skip '/' + + while ((p <= len) && !stop) { + b = src.src[p]; + context.softAssertFormatError(b >= 0, "Illegal character in name token"); + + switch (b) { + // Whitespace + case 0x00: + case 0x09: + case 0x0A: + case 0x0D: + case 0x20: + stop = true; + break; + + // Escape char + case 0x23: + cnt++; // escape char. skip it + break; + + // Delimeters + case 0x28: // ( - LEFT PARENTHESIS + case 0x29: // ) - RIGHT PARENTHESIS + case 0x3C: // < - LESS-THAN SIGN + case 0x3E: // > - GREATER-THAN SIGN + case 0x5B: // [ - LEFT SQUARE BRACKET + case 0x5D: // ] - RIGHT SQUARE BRACKET + case 0x7B: // { - LEFT CURLY BRACKET + case 0x7D: // } - RIGHT CURLY BRACKET + case 0x2F: // / - SOLIDUS + case 0x25: // % - PERCENT SIGN + stop = true; + break; + + default: + if ((b >= 0) && (b < 0x20)) + throw new EParseError("Illegal character in name token(2)"); + break; + } // switch ... + + if (stop) break; + p++; + } // while ... + + if (cnt == 0) { + value = new byte[p - src.pos]; + System.arraycopy(src.src, src.pos, value, 0, value.length); + src.pos = p; + hc = Arrays.hashCode(value); + return; + } + + value = new byte[p-src.pos - 2*cnt]; + cnt = 0; + for (i=src.pos; i>4)); + dst.write(0x30 + (value[i]&0xF)); + } else + dst.write(value[i]); + } + } +} diff --git a/src/main/java/org/pdfparse/cos/COSNull.java b/src/main/java/org/pdfparse/cos/COSNull.java new file mode 100755 index 0000000..a8cffa7 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSNull.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; + + +public class COSNull implements COSObject { + private static final byte[] S_NULL = {0x6E, 0x75, 0x6C, 0x6C}; // "null" + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + dst.write(S_NULL); + } + + @Override + public String toString() { + return "null"; + } + +} diff --git a/src/main/java/org/pdfparse/cos/COSNumber.java b/src/main/java/org/pdfparse/cos/COSNumber.java new file mode 100755 index 0000000..4879b2b --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSNumber.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.*; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * COSNumber provides two types of numbers, integer and real. + *

+ * Integers may be specified by signed or unsigned constants. Reals may only be + * in decimal format.
+ * This object is described in the 'Portable Document Format Reference Manual + * version 1.7' section 3.3.2 (page 52-53). + * + * @see COSObject + * @see EParseError + */ + +public final class COSNumber implements COSObject { + + /** + * actual value of this COSNumber, represented as a + * double + */ + private double value; + private boolean isInteger; + + public COSNumber(double val) { + value = val; + isInteger = false; + } + public COSNumber(float val) { + value = val; + isInteger = false; + } + public COSNumber(int val) { + value = val; + isInteger = true; + } + public COSNumber(long val) { + value = val; + isInteger = true; + } + + public COSNumber(PDFRawData src, ParsingContext context) { + parse(src, context); + } + + + /** + * {@inheritDoc} + */ + public boolean equals(Object o) + { + return o instanceof COSNumber && (((COSNumber)o).value == value); + // TODO: make decision about precision + // return o instanceof COSNumber && (Math.abs(((COSNumber)o).value - value) < 0.000001); + } + + /** + * {@inheritDoc} + */ + public int hashCode() + { + //taken from java.lang.Long + return Float.floatToIntBits((float)value); + } + + + /** + * Returns the primitive int value of this object. + * + * @return The value as int + */ + public int intValue() { + return (int) value; + } + + /** + * Returns the primitive long value of this object. + * + * @return The value as long + */ + public long longValue() { + return (long) value; + } + + /** + * Returns the primitive double value of this object. + * + * @return The value as double + */ + public double doubleValue() { + return value; + } + + /** + * Returns the primitive float value of this object. + * + * @return The value as float + */ + public float floatValue() { + return (float)value; + } + + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + int prev = src.pos; + float sign = 1; + float divider = 10; + + + boolean hasFractional = false; + isInteger = true; + value = 0; + + while (src.pos < src.length) { + switch (src.src[src.pos]) { + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: // 0..4 + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: // 5..9 + if (hasFractional) { + value += (src.src[src.pos] - 0x30) / divider; + divider *= 10; + } else + value = value * 10 + (src.src[src.pos] - 0x30); + src.pos++; + break; + case 0x2B: // + + if (src.pos == prev) { + sign = 1; + src.pos++; + break; + } + throw new EParseError("'+' not allowed here (invalid number)"); + case 0x2D: // - + if (src.pos == prev) { + sign = -1; + src.pos++; + break; + } + throw new EParseError("'-' not allowed here (invalid number)"); + case 0x2E: // . + if (hasFractional) + throw new EParseError("'.' not allowed here (invalid number)"); + hasFractional = true; + isInteger = false; + src.pos++; + break; + + // Separators + case 0x00: case 0x09: case 0x0A: case 0x0D: case 0x20: + // Delimeters + case 0x28: case 0x29: case 0x3C: case 0x3E: case 0x2F: + case 0x5B: case 0x5D: case 0x7B: case 0x7D: case 0x25: + if (prev == src.pos) + throw new EParseError("Number expected, got no value"); + + value = sign * value; + return; + default: + throw new EParseError("Number expected, got invalid value"); + } // switch + } // while + + if (prev == src.pos) + throw new EParseError("Number expected, got no value (2)"); + + value = sign * value; + return; + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + dst.write(this.toString().getBytes()); + } + + @Override + public String toString() { + if (isInteger) + return String.valueOf((long)value); + else return String.format("%f.3", value); + } + + + static public void writeInteger(int val, OutputStream dst) throws IOException { + String str = Integer.toString(val); + for (int i = 0; i < str.length(); i++) { + dst.write(str.codePointAt(i)); + } + } + + static public int readInteger(PDFRawData src) throws EParseError { + int prev = src.pos; + int res = 0; + int sign = 1; + + while (src.pos < src.length) { + switch (src.src[src.pos]) { + case 0x30: + case 0x31: + case 0x32: + case 0x33: + case 0x34: // 0..4 + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: // 5..9 + res = res * 10 + (src.src[src.pos] - 0x30); + src.pos++; + break; + case 0x2B: // + + if (src.pos == prev) { + sign = 1; + src.pos++; + break; + } + throw new EParseError("Invalid integer value"); + case 0x2D: // - + if (src.pos == prev) { + sign = -1; + src.pos++; + break; + } + throw new EParseError("Invalid integer value"); + + // Separators + case 0x00: case 0x09: case 0x0A: case 0x0D: case 0x20: + // Delimeters + case 0x28: case 0x29: case 0x3C: case 0x3E: case 0x2F: + case 0x5B: case 0x5D: case 0x7B: case 0x7D: case 0x25: + if (prev == src.pos) + throw new EParseError("Number expected, got no value"); + + return sign * res; + + default: + throw new EParseError("Number expected, got invalid value (2)"); + } // switch + } // while + + if (prev == src.pos) + throw new EParseError("Number expected, got no value (3)"); + + return sign * res; + } + +} diff --git a/src/main/java/org/pdfparse/cos/COSObject.java b/src/main/java/org/pdfparse/cos/COSObject.java new file mode 100755 index 0000000..d08315c --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSObject.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.OutputStream; +import java.io.IOException; + +public interface COSObject { + + public void parse(PDFRawData src, ParsingContext context) throws EParseError; + + public void produce(OutputStream dst, ParsingContext context) throws IOException; +} diff --git a/src/main/java/org/pdfparse/cos/COSReference.java b/src/main/java/org/pdfparse/cos/COSReference.java new file mode 100755 index 0000000..15b2a48 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSReference.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + + +public class COSReference implements COSObject { + + public int id; + public int gen; + + public COSReference(int id, int gen) { + this.id = id; + this.gen = gen; + } + + public void set(int id, int gen) { + this.id = id; + this.gen = gen; + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + String s = String.format("%d %d R", id, gen); + dst.write(s.getBytes(Charset.defaultCharset())); + } + + @Override + public String toString() { + return String.format("%d %d R", id, gen); + } + +} diff --git a/src/main/java/org/pdfparse/cos/COSStream.java b/src/main/java/org/pdfparse/cos/COSStream.java new file mode 100755 index 0000000..453fe26 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSStream.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFParser; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; + +import java.io.IOException; +import java.io.OutputStream; + + +public class COSStream extends COSDictionary { + private byte[] data = null; + + public COSStream(COSDictionary dict, PDFRawData src, ParsingContext context) throws EParseError { + super(dict, context); + + data = PDFParser.fetchStream(src, this.getUInt(COSName.LENGTH, context.objectCache, 0), true); + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + super.parse(src, context); + data = PDFParser.fetchStream(src, this.getUInt(COSName.LENGTH, context.objectCache,0), true); + } + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + //throw new ENotSupported(); + super.produce(dst, context); + //dst.write(null); + } + + public byte[] getData() { + return data; + } +} diff --git a/src/main/java/org/pdfparse/cos/COSString.java b/src/main/java/org/pdfparse/cos/COSString.java new file mode 100755 index 0000000..9e416e6 --- /dev/null +++ b/src/main/java/org/pdfparse/cos/COSString.java @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.cos; + +import org.pdfparse.exception.EParseError; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.utils.ByteBuffer; +import org.pdfparse.utils.IntIntHashtable; + +import java.io.IOException; +import java.io.OutputStream; + +public final class COSString implements COSObject { + + private static final byte[] C28 = {'\\', '('}; + private static final byte[] C29 = {'\\', ')'}; + private static final byte[] C5C = {'\\', '\\'}; + private static final byte[] C0A = {'\\', 'n'}; + private static final byte[] C0D = {'\\', 'r'}; + + static final char winansiByteToChar[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 8364, 65533, 8218, 402, 8222, 8230, 8224, 8225, 710, 8240, 352, 8249, 338, 65533, 381, 65533, + 65533, 8216, 8217, 8220, 8221, 8226, 8211, 8212, 732, 8482, 353, 8250, 339, 65533, 382, 376, + 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255}; + static final IntIntHashtable winansi = new IntIntHashtable(); + + static final char pdfEncodingByteToChar[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, + 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, + 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 0x2022, 0x2020, 0x2021, 0x2026, 0x2014, 0x2013, 0x0192, 0x2044, 0x2039, 0x203a, 0x2212, 0x2030, 0x201e, 0x201c, 0x201d, 0x2018, + 0x2019, 0x201a, 0x2122, 0xfb01, 0xfb02, 0x0141, 0x0152, 0x0160, 0x0178, 0x017d, 0x0131, 0x0142, 0x0153, 0x0161, 0x017e, 65533, + 0x20ac, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, + 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, + 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255}; + static final IntIntHashtable pdfEncoding = new IntIntHashtable(); + static { + for (int k = 128; k < 161; ++k) { + char c = winansiByteToChar[k]; + if (c != 65533) + winansi.put(c, k); + } + for (int k = 128; k < 161; ++k) { + char c = pdfEncodingByteToChar[k]; + if (c != 65533) + pdfEncoding.put(c, k); + } + } + + private static final int[] HEX2V = { // '0'..'f' + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15}; + private static final byte[] V2HEX = { // '0'..'f' + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x61, 0x62, 0x63, 0x64, 0x65, 0x66}; + + private static final byte[] EMPTY = {}; + + private String value; + private byte[] binaryValue; + private boolean forceHexForm; + + public COSString(String val) { + value = val; + binaryValue = val.getBytes(); + } + + public COSString(PDFRawData src, ParsingContext context) throws EParseError { + parse(src, context); + } + + public void clear() { + value = ""; + binaryValue = EMPTY; + } + + public String getValue() { + return value; + } + + public void setValue(String val) { + value = val; + binaryValue = convertToBytes(val, null); + } + + public byte[] getBinaryValue() { + return binaryValue; + } + + public void setBinaryValue(byte[] val) { + if (val == null) + binaryValue = EMPTY; + else binaryValue = val; + + value = convertToString(binaryValue); + } + + /** + * Forces the string to be written in literal form instead of hexadecimal form. + * + * @param v + * if v is true the string will be written in literal form, otherwise it will be written in hexa if + * necessary. + */ + + public void setForceLiteralForm(boolean v) + { + forceHexForm = !v; + } + + /** + * Forces the string to be written in hexadecimal form instead of literal form. + * + * @param v + * if v is true the string will be written in hexadecimal form otherwise it will be written in literal if + * necessary. + */ + + public void setForceHexForm(boolean v) + { + forceHexForm = v; + } + + @Override + public void parse(PDFRawData src, ParsingContext context) throws EParseError { + int nesting_brackets = 0; + int v; + byte ch; + value = ""; + binaryValue = EMPTY; + + if (src.src[src.pos] == '<') { + src.pos++; // Skip the opening bracket '<' + byte[] bytes = parseHexStream(src, context); + setBinaryValue(bytes); + forceHexForm = true; + return; + } + + // === this is a literal string + forceHexForm = false; + src.pos++; // Skip the opening bracket '(' + + ByteBuffer buffer = context.tmpBuffer; + buffer.reset(); + + while (src.pos < src.length) { + ch = src.src[src.pos]; + switch (ch) { + case 0x5C: // '\' + src.pos++; + if (src.pos >= src.length) + break; // finish. ignore this reverse solidus + + ch = src.src[src.pos]; + switch (ch) { + case 0x6E: // 'n' + buffer.append(0x0A); + break; + case 0x72: // 'r' + buffer.append(0x0D); + break; + case 0x74: // 't' + buffer.append(0x09); + break; + case 0x62: // 'b' + buffer.append(0x08); + break; + case 0x66: // 'f' + buffer.append(0x0C); + break; + case 0x28: // '(' + buffer.append(0x28); + break; + case 0x29: // ')' + buffer.append(0x29); + break; + case 0x5C: // '\' + buffer.append(0x5C); + break; + case 0x30: // '0'..'7' + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + v = ch - 0x30; // convert first char to number + if ((src.src[src.pos + 1] >= 0x30) && (src.src[src.pos + 1] <= 0x37)) { + src.pos++; + v = v * 8 + (src.src[src.pos] - 0x30); + if ((src.src[src.pos + 1] >= 0x30) && (src.src[src.pos + 1] <= 0x37)) { + src.pos++; + v = v * 8 + (src.src[src.pos] - 0x30); + } + } + buffer.append(v); + break; + case 0x0A: + if ((src.pos < src.length) && (src.src[src.pos + 1] == 0x0D)) { + src.pos++; + } + break; + case 0x0D: + break; + + default: + // If the character following the REVERSE SOLIDUS is not one of those shown in Table 3, + // the REVERSE SOLIDUS shall be ignored. + buffer.append(src.src[src.pos]); //add this char + }//switch after '\' + + src.pos++; + break; + case 0x28: // '(' + nesting_brackets++; + buffer.append(0x28); + src.pos++; + break; + case 0x29: // ')' + nesting_brackets--; + if (nesting_brackets < 0) { //found closing bracket. End of string + src.pos++; + binaryValue = buffer.toByteArray(); + value = convertToString(binaryValue); + return; + } + buffer.append(0x29); + src.pos++; + break; + case 0x0D: // '\r': + case 0x0A: // '\n': + // An end-of-line marker appearing within a literal string without a preceding REVERSE SOLIDUS shall be treated + // as a byte value of (0Ah), irrespective of whether the end-of-line marker was a CARRIAGE RETURN (0Dh), a + // LINE FEED (0Ah), or both. + + buffer.append(0x0A); + src.pos++; + break; + default: + buffer.append(src.src[src.pos]); + src.pos++; + } // switch + } // while ... + + // Reach end-of-file/data? + if (src.pos < src.length) { + src.pos++; + } + + context.softAssertSyntaxComliance(nesting_brackets == 0, "Unbalanced brackets and illegal nesting while parsing string object"); + + binaryValue = buffer.toByteArray(); + value = convertToString(binaryValue); + } + + @Override + public void produce(OutputStream dst, ParsingContext context) throws IOException { + int i, j, len; + len = binaryValue.length; + + if (forceHexForm) { + // === Hexadecimal form + int b; + // TODO: use context.tmpBuffer + byte[] hex = new byte[binaryValue.length * 2]; + for (i = 0, j = 0; i < len; i++, j += 2) { + b = binaryValue[i] & 0xFF; + hex[j] = V2HEX[b >> 4]; + hex[j + 1] = V2HEX[b & 0xF]; + } + dst.write(0x3C); // "<" + dst.write(hex); + dst.write(0x3E); // ">" + return; + } + + // === Literal form + dst.write('('); + for (i = 0; i < len; i++) { + switch (binaryValue[i]) { + case 0x28: + dst.write(C28); + break; + case 0x29: + dst.write(C29); + break; + case 0x5C: + dst.write(C5C); + break; + case 0x0A: + dst.write(C0A); + break; + case 0x0D: + dst.write(C0D); + break; + + default: + dst.write(binaryValue[i]); + break; + } + } + dst.write(')'); + } + + @Override + public String toString() { + return value; + } + + + /** Converts a String to a byte array according + * to the font's encoding. + * @return an array of byte representing the conversion according to the font's encoding + * @param encoding the encoding + * @param text the String to be converted + */ + public static final byte[] convertToBytes(String text, String encoding) { + if (text == null) + return new byte[0]; + if (encoding == null || encoding.length() == 0) { + int len = text.length(); + byte b[] = new byte[len]; + for (int k = 0; k < len; ++k) + b[k] = (byte)text.charAt(k); + return b; + } + + return text.getBytes(); + /* + ExtraEncoding extra = extraEncodings.get(encoding.toLowerCase()); + if (extra != null) { + byte b[] = extra.charToByte(text, encoding); + if (b != null) + return b; + } + IntIntHashtable hash = null; + if (encoding.equals(BaseFont.WINANSI)) + hash = winansi; + else if (encoding.equals(PdfObject.TEXT_PDFDOCENCODING)) + hash = pdfEncoding; + if (hash != null) { + char cc[] = text.toCharArray(); + int len = cc.length; + int ptr = 0; + byte b[] = new byte[len]; + int c = 0; + for (int k = 0; k < len; ++k) { + char char1 = cc[k]; + if (char1 < 128 || char1 > 160 && char1 <= 255) + c = char1; + else + c = hash.get(char1); + if (c != 0) + b[ptr++] = (byte)c; + } + if (ptr == len) + return b; + byte b2[] = new byte[ptr]; + System.arraycopy(b, 0, b2, 0, ptr); + return b2; + } + if (encoding.equals(PdfObject.TEXT_UNICODE)) { + // workaround for jdk 1.2.2 bug + char cc[] = text.toCharArray(); + int len = cc.length; + byte b[] = new byte[cc.length * 2 + 2]; + b[0] = -2; + b[1] = -1; + int bptr = 2; + for (int k = 0; k < len; ++k) { + char c = cc[k]; + b[bptr++] = (byte)(c >> 8); + b[bptr++] = (byte)(c & 0xff); + } + return b; + } + try { + Charset cc = Charset.forName(encoding); + CharsetEncoder ce = cc.newEncoder(); + ce.onUnmappableCharacter(CodingErrorAction.IGNORE); + CharBuffer cb = CharBuffer.wrap(text.toCharArray()); + java.nio.ByteBuffer bb = ce.encode(cb); + bb.rewind(); + int lim = bb.limit(); + byte[] br = new byte[lim]; + bb.get(br); + return br; + } + catch (IOException e) { + throw new ExceptionConverter(e); + } */ + } + + + /** Converts a byte array to a String trying to detect encoding + * @param bytes the bytes to convert + * @return the converted String + */ + public static final String convertToString(byte bytes[], int offset, int length) { + if (bytes == null) + return ""; + // trying to detect encoding + if (bytes.length > 2 && ((bytes[0] & 0xFF) == 0xFE) && ((bytes[1] & 0xFF) == 0xFF)) { // UTF-16 BE + try { + return + new String(bytes, offset, length, "UTF-16"); + } catch (Exception e) {} + } + + if (offset + length > bytes.length) + length = bytes.length - offset; + if (length <= 0) + return ""; + + int dest = offset + length; + char c[] = new char[length]; + int i = 0; + + char[] map = pdfEncodingByteToChar; // use PDFEncoding + + for (int k = offset; k < dest; k++) + c[i++] = (char)map[bytes[k] & 0xff]; + return new String(c); + } + + public static final String convertToString(byte bytes[]) { + return convertToString(bytes, 0, bytes.length); + } + + public static final String convertToString(ByteBuffer bytes) { + return convertToString(bytes.getBuffer(), 0, bytes.size()); + } + + public static final String convertToString(byte bytes[], int offset, int length, String encoding) { + if (bytes == null) + return ""; + // trying to detect encoding + if (bytes.length > 2 && ((bytes[0] & 0xFF) == 0xFE) && ((bytes[1] & 0xFF) == 0xFF)) { // UTF-16 BE + try { + return + new String(bytes, offset, length, "UTF-16"); + } catch (Exception e) {} + } + + if (offset + length > bytes.length) + length = bytes.length - offset; + if (length <= 0) + return ""; + + int dest = offset + length; + char c[] = new char[length]; + int i = 0; + + char[] map; + if (encoding.equals("/WinAnsiEncoding")) + map = winansiByteToChar; + else + map = pdfEncodingByteToChar; // use PDFEncoding + + for (int k = offset; k < dest; k++) + c[i++] = (char)map[bytes[k] & 0xff]; + return new String(c); + } + /** Checks is text only has PdfDocEncoding characters. + * @param text the String to test + * @return true if only PdfDocEncoding characters are present + */ + public static boolean isPdfDocEncoding(String text) { + if (text == null) + return true; + int len = text.length(); + for (int k = 0; k < len; ++k) { + char char1 = text.charAt(k); + if (char1 < 128 || char1 > 160 && char1 <= 255) + continue; + if (!pdfEncoding.containsKey(char1)) + return false; + } + return true; + } + + + public static final byte[] parseHexStream(PDFRawData src, ParsingContext context) throws EParseError { + int ch, n, n1 = 0; + boolean first = true; + + //src.pos++; // Skip the opening bracket '<' + + ByteBuffer out = context.tmpBuffer; + out.reset(); + for (int i = src.pos; i < src.length; i++) { + ch = src.src[i] & 0xFF; + + if (ch == 0x3E) { // '>' - EOD + src.pos = i + 1; + if (!first) + out.append((byte)(n1 << 4)); + return out.toByteArray(); + } + // whitespace ? + if ((ch == 0x00) || (ch == 0x09) || (ch == 0x0A) || (ch == 0x0C) || (ch == 0x0D) || (ch == 0x20)) + continue; + + if ((ch < 0x30) || (ch > 0x66)) + throw new EParseError("Illegal character in hex string"); + + n = HEX2V[ch - 0x30]; + if (n < 0) + throw new EParseError("Illegal character in hex string"); + + if (first) + n1 = n; + else + out.append((byte)((n1 << 4) + n)); + first = !first; + } + + throw new EParseError("Unterminated hexadecimal string"); // ">" + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + + if (obj instanceof COSString) + { + COSString strObj = (COSString) obj; + return this.getValue().equals(strObj.getValue()) && this.forceHexForm == strObj.forceHexForm; + } + return false; + } + + @Override + public int hashCode() + { + int result = getValue().hashCode(); + return result += forceHexForm ? 17 : 0; + } + +} diff --git a/src/main/java/org/pdfparse/exception/EDateConvertError.java b/src/main/java/org/pdfparse/exception/EDateConvertError.java new file mode 100755 index 0000000..0e60f97 --- /dev/null +++ b/src/main/java/org/pdfparse/exception/EDateConvertError.java @@ -0,0 +1,48 @@ + +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.exception; + +import org.pdfparse.exception.EParseError; + +public class EDateConvertError extends EParseError { + + /** + * Creates a new instance of + * EParseError without detail message. + */ + public EDateConvertError() { + } + + /** + * Constructs an instance of + * EParseError with the specified detail message. + * + * @param msg the detail message. + */ + public EDateConvertError(String msg) { + super(msg); + } + + public EDateConvertError(String msg, Object... args) { + super(String.format(msg, args)); + } +} + diff --git a/src/main/java/org/pdfparse/exception/EDecoderException.java b/src/main/java/org/pdfparse/exception/EDecoderException.java new file mode 100755 index 0000000..be8ee08 --- /dev/null +++ b/src/main/java/org/pdfparse/exception/EDecoderException.java @@ -0,0 +1,45 @@ + +/* + * Copyright (c) 2014 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.exception; + +public class EDecoderException extends EParseError { + + public EDecoderException() { + super(); + } + + public EDecoderException(String message) { + super(message); + } + + public EDecoderException(Throwable cause) { + super(cause); + } + + public EDecoderException(String message, Throwable cause) { + super(message, cause); + } + + public EDecoderException(String msg, Object... args) { + super(String.format(msg, args)); + } + +} diff --git a/src/main/java/org/pdfparse/exception/ENotSupported.java b/src/main/java/org/pdfparse/exception/ENotSupported.java new file mode 100755 index 0000000..fd04890 --- /dev/null +++ b/src/main/java/org/pdfparse/exception/ENotSupported.java @@ -0,0 +1,44 @@ + +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.exception; + +import org.pdfparse.exception.EParseError; + +public class ENotSupported extends EParseError { + + /** + * Creates a new instance of + * EParseError without detail message. + */ + public ENotSupported() { + } + + /** + * Constructs an instance of + * EParseError with the specified detail message. + * + * @param msg the detail message. + */ + public ENotSupported(String msg) { + super(msg); + } +} + diff --git a/src/main/java/org/pdfparse/exception/EParseError.java b/src/main/java/org/pdfparse/exception/EParseError.java new file mode 100755 index 0000000..c78b325 --- /dev/null +++ b/src/main/java/org/pdfparse/exception/EParseError.java @@ -0,0 +1,56 @@ + +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.exception; + +public class EParseError extends RuntimeException { + + /** + * Creates a new instance of + * EParseError without detail message. + */ + public EParseError() { + super(); + } + + /** + * Constructs an instance of + * EParseError with the specified detail message. + * + * @param msg the detail message. + */ + public EParseError(String msg) { + super(msg); + } + + public EParseError(Throwable cause) { + super(cause); + } + + public EParseError(String message, Throwable cause) { + super(message, cause); + } + + public EParseError(String msg, Object... args) { + super(String.format(msg, args)); + } + + +} diff --git a/src/main/java/org/pdfparse/filter/LZWDecoder.java b/src/main/java/org/pdfparse/filter/LZWDecoder.java new file mode 100755 index 0000000..b819eb9 --- /dev/null +++ b/src/main/java/org/pdfparse/filter/LZWDecoder.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2008 by Paulo Soares. + * + * This code was originally released in 2001 by SUN (see class + * com.sun.media.imageioimpl.plugins.tiff.TIFFLZWDecompressor.java) + * using the BSD license in a specific wording. In a mail dating from + * January 23, 2008, Brian Burkhalter (@sun.com) gave us permission + * to use the code under the following version of the BSD license: + * + * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any + * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND + * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY + * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL + * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF + * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR + * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, + * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND + * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR + * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for + * use in the design, construction, operation or maintenance of any + * nuclear facility. + */ +package org.pdfparse.filter; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A class for performing LZW decoding. + * + * + */ +public class LZWDecoder { + + byte stringTable[][]; + byte data[] = null; + OutputStream uncompData; + int tableIndex, bitsToGet = 9; + int bytePointer, bitPointer; + int nextData = 0; + int nextBits = 0; + + int andTable[] = { + 511, + 1023, + 2047, + 4095 + }; + + public LZWDecoder() { + } + + /** + * Method to decode LZW compressed data. + * + * @param data The compressed data. + * @param uncompData Array to return the uncompressed data in. + */ + public void decode(byte data[], OutputStream uncompData) { + + if(data[0] == (byte)0x00 && data[1] == (byte)0x01) { + throw new RuntimeException("LZW flavour not supported"); + } + + initializeStringTable(); + + this.data = data; + this.uncompData = uncompData; + + // Initialize pointers + bytePointer = 0; + bitPointer = 0; + + nextData = 0; + nextBits = 0; + + int code, oldCode = 0; + byte string[]; + + while ((code = getNextCode()) != 257) { + + if (code == 256) { + + initializeStringTable(); + code = getNextCode(); + + if (code == 257) { + break; + } + + writeString(stringTable[code]); + oldCode = code; + + } else { + + if (code < tableIndex) { + + string = stringTable[code]; + + writeString(string); + addStringToTable(stringTable[oldCode], string[0]); + oldCode = code; + + } else { + + string = stringTable[oldCode]; + string = composeString(string, string[0]); + writeString(string); + addStringToTable(string); + oldCode = code; + } + } + } + } + + + /** + * Initialize the string table. + */ + public void initializeStringTable() { + + stringTable = new byte[8192][]; + + for (int i=0; i<256; i++) { + stringTable[i] = new byte[1]; + stringTable[i][0] = (byte)i; + } + + tableIndex = 258; + bitsToGet = 9; + } + + /** + * Write out the string just uncompressed. + */ + public void writeString(byte string[]) { + try { + uncompData.write(string); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte oldString[], byte newString) { + int length = oldString.length; + byte string[] = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte string[]) { + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Append newString to the end of oldString. + */ + public byte[] composeString(byte oldString[], byte newString) { + int length = oldString.length; + byte string[] = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + return string; + } + + // Returns the next 9, 10, 11 or 12 bits + public int getNextCode() { + // Attempt to get the next code. The exception is caught to make + // this robust to cases wherein the EndOfInformation code has been + // omitted from a strip. Examples of such cases have been observed + // in practice. + try { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + + if (nextBits < bitsToGet) { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + } + + int code = + (nextData >> (nextBits - bitsToGet)) & andTable[bitsToGet-9]; + nextBits -= bitsToGet; + + return code; + } catch(ArrayIndexOutOfBoundsException e) { + // Strip not terminated as expected: return EndOfInformation code. + return 257; + } + } +} diff --git a/src/main/java/org/pdfparse/filter/StreamDecoder.java b/src/main/java/org/pdfparse/filter/StreamDecoder.java new file mode 100755 index 0000000..7666574 --- /dev/null +++ b/src/main/java/org/pdfparse/filter/StreamDecoder.java @@ -0,0 +1,480 @@ + +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.filter; + + +import org.pdfparse.exception.EDecoderException; +import org.pdfparse.parser.PDFParser; +import org.pdfparse.parser.PDFRawData; +import org.pdfparse.parser.ParsingContext; +import org.pdfparse.cos.*; +import org.pdfparse.exception.EParseError; + +import java.io.ByteArrayOutputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +public class StreamDecoder { + + public static interface FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, final COSDictionary streamDictionary, ParsingContext context) throws EParseError; + } + + private static final Map defaults; + static { + HashMap map = new HashMap(); + + map.put(COSName.FLATEDECODE, new Filter_FLATEDECODE()); + map.put(COSName.FL, new Filter_FLATEDECODE()); + map.put(COSName.ASCIIHEXDECODE, new Filter_ASCIIHEXDECODE()); + map.put(COSName.AHX, new Filter_ASCIIHEXDECODE()); + map.put(COSName.ASCII85DECODE, new Filter_ASCII85DECODE()); + map.put(COSName.A85, new Filter_ASCII85DECODE()); + map.put(COSName.LZWDECODE, new Filter_LZWDECODE()); + //map.put(COSName.CCITTFAXDECODE, new Filter_CCITTFAXDECODE()); + map.put(COSName.CRYPT, new Filter_DoNothing()); + map.put(COSName.RUNLENGTHDECODE, new Filter_RUNLENGTHDECODE()); + + // ignore this filters + map.put(COSName.DCTDECODE, new Filter_DoNothing()); + map.put(COSName.JPXDECODE, new Filter_DoNothing()); + map.put(COSName.CCITTFAXDECODE, new Filter_DoNothing()); + map.put(COSName.JBIG2DECODE, new Filter_DoNothing()); + + defaults = Collections.unmodifiableMap(map); + } + + + + public static byte[] FLATEDecode(final byte[] src) { + byte[] buf = new byte[1024]; + + Inflater decompressor = new Inflater(); + decompressor.setInput(src); + + // Create an expandable byte array to hold the decompressed data + ByteArrayOutputStream bos = new ByteArrayOutputStream(src.length); + + try { + while (!decompressor.finished()) { + int count = decompressor.inflate(buf); + bos.write(buf, 0, count); + } + } catch (DataFormatException e) { + decompressor.end(); + throw new EDecoderException("FlateDecode error", e); + } + decompressor.end(); + + return bos.toByteArray(); + } + + /** Decodes a stream that has the LZWDecode filter. + * @param in the input data + * @return the decoded data + */ + public static byte[] LZWDecode(final byte in[]) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + LZWDecoder lzw = new LZWDecoder(); + lzw.decode(in, out); + return out.toByteArray(); + } + + /** Decodes a stream that has the ASCIIHexDecode filter. + * @param in the input data + * @return the decoded data + */ + public static byte[] ASCIIHexDecode(final byte in[], ParsingContext context) throws EParseError { + PDFRawData data = new PDFRawData(); + data.src = in; + data.length = in.length; + data.pos = 0; + + return COSString.parseHexStream(data, context); + } + + /** Decodes a stream that has the ASCII85Decode filter. + * @param in the input data + * @return the decoded data + */ + public static byte[] ASCII85Decode(final byte in[]) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int state = 0; + int chn[] = new int[5]; + for (int k = 0; k < in.length; ++k) { + int ch = in[k] & 0xff; + if (ch == '~') + break; + + if (PDFRawData.isWhitespace(ch)) + continue; + if (ch == 'z' && state == 0) { + out.write(0); + out.write(0); + out.write(0); + out.write(0); + continue; + } + if (ch < '!' || ch > 'u') + throw new EDecoderException("Illegal character in ascii85decode (#%d)", ch); + chn[state] = ch - '!'; + ++state; + if (state == 5) { + state = 0; + int r = 0; + for (int j = 0; j < 5; ++j) + r = r * 85 + chn[j]; + out.write((byte)(r >> 24)); + out.write((byte)(r >> 16)); + out.write((byte)(r >> 8)); + out.write((byte)r); + } + } + int r = 0; + // We'll ignore the next two lines for the sake of perpetuating broken PDFs +// if (state == 1) +// throw new RuntimeException("illegal.length.in.ascii85decode"); + if (state == 2) { + r = chn[0] * 85 * 85 * 85 * 85 + chn[1] * 85 * 85 * 85 + 85 * 85 * 85 + 85 * 85 + 85; + out.write((byte)(r >> 24)); + } + else if (state == 3) { + r = chn[0] * 85 * 85 * 85 * 85 + chn[1] * 85 * 85 * 85 + chn[2] * 85 * 85 + 85 * 85 + 85; + out.write((byte)(r >> 24)); + out.write((byte)(r >> 16)); + } + else if (state == 4) { + r = chn[0] * 85 * 85 * 85 * 85 + chn[1] * 85 * 85 * 85 + chn[2] * 85 * 85 + chn[3] * 85 + 85; + out.write((byte)(r >> 24)); + out.write((byte)(r >> 16)); + out.write((byte)(r >> 8)); + } + return out.toByteArray(); + } + + public static PDFRawData decodeStream(byte[] src, COSDictionary dic, ParsingContext context) throws EParseError { + // Decompress stream + COSObject objFilter = dic.get(COSName.FILTER); + if (objFilter != null) { + COSArray filters = new COSArray(); + if (objFilter instanceof COSName) + filters.add((COSName)objFilter); + else if (objFilter instanceof COSArray) + filters.addAll((COSArray)objFilter); + + byte[] bytes = src; + for (int i=0; i bytesPerPixel, "Data to small for decoding PNG prediction") ) { + return in_out; + } + + + int filter = 0; + int curr_in_idx = 0, curr_out_idx = 0, prior_idx = 0; + // Decode the first line ------------------- + filter = in_out[curr_in_idx++]; + + switch (filter) { + case 0: //PNG_FILTER_NONE + case 2: //PNG_FILTER_UP + //curr[i] += prior[i]; + for (int i = 0; i < bytesPerRow; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + break; + case 1: //PNG_FILTER_SUB + case 4: //PNG_FILTER_PAETH + //curr[i] += curr[i - bytesPerPixel]; + for (int i = 0; i < bytesPerPixel; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + + for (int i = bytesPerPixel; i < bytesPerRow; i++) + in_out[curr_out_idx + i] = (byte)((in_out[curr_out_idx + i - bytesPerPixel]&0xff + in_out[curr_in_idx + i]&0xff)&0xff); + break; + case 3: //PNG_FILTER_AVERAGE + for (int i = 0; i < bytesPerPixel; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + + for (int i = bytesPerPixel; i < bytesPerRow; i++) + in_out[curr_out_idx + i] = (byte) ((in_out[curr_in_idx + i - bytesPerPixel] & 0xff)/2); + break; + default: + // Error -- unknown filter type + throw new EDecoderException("PNG filter unknown (%d)", filter); + } + curr_in_idx += bytesPerRow; + curr_out_idx += bytesPerRow; + + //------------------------- + + + // Decode the (sub)image row-by-row + while (true) { + if (curr_in_idx >= in_out.length) + break; + + filter = in_out[curr_in_idx++]; + + switch (filter) { + case 0: //PNG_FILTER_NONE + for (int i = 0; i < bytesPerPixel; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + break; + case 1: //PNG_FILTER_SUB + //curr[i] += curr[i - bytesPerPixel]; + for (int i = 0; i < bytesPerPixel; i++) + in_out[curr_out_idx + i] = in_out[curr_in_idx + i]; + + for (int i = bytesPerPixel; i < bytesPerRow; i++) + in_out[curr_out_idx + i] = (byte)(((in_out[curr_out_idx + i - bytesPerPixel]&0xff) + (in_out[curr_in_idx + i]&0xff))&0xff); + break; + case 2: //PNG_FILTER_UP + for (int i = 0; i < bytesPerRow; i++) { + //curr[i] += prior[i]; + in_out[curr_out_idx + i] = (byte) ((in_out[curr_in_idx + i]&0xff) + (in_out[prior_idx + i]&0xff)&0xff); + } + break; + case 3: //PNG_FILTER_AVERAGE + for (int i = 0; i < bytesPerPixel; i++) { + //curr[i] += prior[i] / 2; + in_out[curr_out_idx + i] += (byte) (((in_out[curr_in_idx + i]&0xff) + (in_out[prior_idx + i]&0xff) / 2)&0xff); + } + for (int i = bytesPerPixel; i < bytesPerRow; i++) { + //curr[i] += ((curr[i - bytesPerPixel] & 0xff) + (prior[i] & 0xff))/2; + in_out[curr_out_idx + i] += (byte) (( + (in_out[curr_out_idx + i - bytesPerPixel] & 0xff)+(in_out[prior_idx + i] & 0xff))/2); + } + break; + case 4: //PNG_FILTER_PAETH + for (int i = 0; i < bytesPerPixel; i++) { + //curr[i] += prior[i]; + in_out[curr_out_idx + i] = (byte) (((in_out[curr_in_idx + i]&0xff) + (in_out[prior_idx + i]&0xff)&0xff)); + } + + for (int i = bytesPerPixel; i < bytesPerRow; i++) { + //int a = curr[i - bytesPerPixel] & 0xff; + //int b = prior[i] & 0xff; + //int c = prior[i - bytesPerPixel] & 0xff; + int a = in_out[curr_out_idx + i - bytesPerPixel] & 0xFF; + int b = in_out[prior_idx + i] & 0xFF; + int c = in_out[prior_idx + i - bytesPerPixel] & 0xFF; + + int p = a + b - c; + int pa = Math.abs(p - a); + int pb = Math.abs(p - b); + int pc = Math.abs(p - c); + + int ret; + + if (pa <= pb && pa <= pc) { + ret = a; + } else if (pb <= pc) { + ret = b; + } else { + ret = c; + } + //curr[i] += (byte)ret; + in_out[curr_out_idx + i] += (byte)ret; + } + break; + default: + // Error -- unknown filter type + throw new EDecoderException("PNG filter unknown (%d)", filter); + } + + // Swap curr and prior + prior_idx = curr_out_idx; + curr_in_idx += bytesPerRow; + curr_out_idx += bytesPerRow; + } // while (true) ... + + byte[] res = new byte[curr_out_idx]; + System.arraycopy(in_out,0, res, 0, res.length); + return res; + } + + /** + * Handles FLATEDECODE filter + */ + private static class Filter_FLATEDECODE implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + b = StreamDecoder.FLATEDecode(b); + if (decodeParams != null) + b = StreamDecoder.decodePredictor(b, (COSDictionary)decodeParams, context); + return b; + } + } + + /** + * Handles ASCIIHEXDECODE filter + */ + private static class Filter_ASCIIHEXDECODE implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + b = StreamDecoder.ASCIIHexDecode(b, context); + return b; + } + } + + /** + * Handles ASCIIHEXDECODE filter + */ + private static class Filter_ASCII85DECODE implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + b = StreamDecoder.ASCII85Decode(b); + return b; + } + } + + /** + * Handles LZWDECODE filter + */ + private static class Filter_LZWDECODE implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + b = StreamDecoder.LZWDecode(b); + if (decodeParams != null) + b = StreamDecoder.decodePredictor(b, (COSDictionary)decodeParams, context); + return b; + } + } + + + /** + * A filter that doesn't modify the stream at all + */ + private static class Filter_DoNothing implements FilterHandler{ + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + return b; + } + } + + /** + * Handles RUNLENGTHDECODE filter + */ + private static class Filter_RUNLENGTHDECODE implements FilterHandler{ + + public byte[] decode(byte[] b, COSName filterName, COSObject decodeParams, COSDictionary streamDictionary, ParsingContext context) throws EParseError { + // allocate the output buffer + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte dupCount = -1; + for(int i = 0; i < b.length; i++){ + dupCount = b[i]; + if (dupCount == -128) break; // this is implicit end of data + + if (dupCount >= 0 && dupCount <= 127){ + int bytesToCopy = dupCount+1; + baos.write(b, i, bytesToCopy); + i+=bytesToCopy; + } else { + // make dupcount copies of the next byte + i++; + for(int j = 0; j < 1-(int)(dupCount);j++){ + baos.write(b[i]); + } + } + } + + return baos.toByteArray(); + } + } + +} diff --git a/src/main/java/org/pdfparse/filter/TIFFLZWDecoder.java b/src/main/java/org/pdfparse/filter/TIFFLZWDecoder.java new file mode 100755 index 0000000..dbd583f --- /dev/null +++ b/src/main/java/org/pdfparse/filter/TIFFLZWDecoder.java @@ -0,0 +1,273 @@ +/* + * Copyright 2003-2012 by Paulo Soares. + * + * This code was originally released in 2001 by SUN (see class + * com.sun.media.imageioimpl.plugins.tiff.TIFFLZWDecompressor.java) + * using the BSD license in a specific wording. In a mail dating from + * January 23, 2008, Brian Burkhalter (@sun.com) gave us permission + * to use the code under the following version of the BSD license: + * + * Copyright (c) 2005 Sun Microsystems, Inc. All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistribution of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistribution in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * Neither the name of Sun Microsystems, Inc. or the names of + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * This software is provided "AS IS," without a warranty of any + * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND + * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY + * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL + * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF + * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS + * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR + * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, + * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND + * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR + * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGES. + * + * You acknowledge that this software is not designed or intended for + * use in the design, construction, operation or maintenance of any + * nuclear facility. + * + * ---------- + * Modified & adapted by Anton Golinko'2013 + */ +package org.pdfparse.filter; + +/** + * A class for performing LZW decoding. + * + * + */ +public class TIFFLZWDecoder { + + byte stringTable[][]; + byte data[] = null, uncompData[]; + int tableIndex, bitsToGet = 9; + int bytePointer, bitPointer; + int dstIndex; + int w, h; + int predictor, samplesPerPixel; + int nextData = 0; + int nextBits = 0; + + int andTable[] = { + 511, + 1023, + 2047, + 4095 + }; + + public TIFFLZWDecoder(int w, int predictor, int samplesPerPixel) { + this.w = w; + this.predictor = predictor; + this.samplesPerPixel = samplesPerPixel; + } + + /** + * Method to decode LZW compressed data. + * + * @param data The compressed data. + * @param uncompData Array to return the uncompressed data in. + * @param h The number of rows the compressed data contains. + */ + public byte[] decode(byte data[], byte uncompData[], int h) { + + if(data[0] == (byte)0x00 && data[1] == (byte)0x01) { + throw new UnsupportedOperationException("TIFF 5.0 style lzw-codes are not supported"); + } + + initializeStringTable(); + + this.data = data; + this.h = h; + this.uncompData = uncompData; + + // Initialize pointers + bytePointer = 0; + bitPointer = 0; + dstIndex = 0; + + + nextData = 0; + nextBits = 0; + + int code, oldCode = 0; + byte string[]; + + while ( ((code = getNextCode()) != 257) && + dstIndex < uncompData.length) { + + if (code == 256) { + + initializeStringTable(); + code = getNextCode(); + + if (code == 257) { + break; + } + + writeString(stringTable[code]); + oldCode = code; + + } else { + + if (code < tableIndex) { + + string = stringTable[code]; + + writeString(string); + addStringToTable(stringTable[oldCode], string[0]); + oldCode = code; + + } else { + + string = stringTable[oldCode]; + string = composeString(string, string[0]); + writeString(string); + addStringToTable(string); + oldCode = code; + } + + } + + } + + // Horizontal Differencing Predictor + if (predictor == 2) { + + int count; + for (int j = 0; j < h; j++) { + + count = samplesPerPixel * (j * w + 1); + + for (int i = samplesPerPixel; i < w * samplesPerPixel; i++) { + + uncompData[count] += uncompData[count - samplesPerPixel]; + count++; + } + } + } + + return uncompData; + } + + + /** + * Initialize the string table. + */ + public void initializeStringTable() { + + stringTable = new byte[4096][]; + + for (int i=0; i<256; i++) { + stringTable[i] = new byte[1]; + stringTable[i][0] = (byte)i; + } + + tableIndex = 258; + bitsToGet = 9; + } + + /** + * Write out the string just uncompressed. + */ + public void writeString(byte string[]) { + // Fix for broken tiff files + int max = uncompData.length - dstIndex; + if (string.length < max) + max = string.length; + System.arraycopy(string, 0, uncompData, dstIndex, max); + dstIndex += max; + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte oldString[], byte newString) { + int length = oldString.length; + byte string[] = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Add a new string to the string table. + */ + public void addStringToTable(byte string[]) { + + // Add this new String to the table + stringTable[tableIndex++] = string; + + if (tableIndex == 511) { + bitsToGet = 10; + } else if (tableIndex == 1023) { + bitsToGet = 11; + } else if (tableIndex == 2047) { + bitsToGet = 12; + } + } + + /** + * Append newString to the end of oldString. + */ + public byte[] composeString(byte oldString[], byte newString) { + int length = oldString.length; + byte string[] = new byte[length + 1]; + System.arraycopy(oldString, 0, string, 0, length); + string[length] = newString; + + return string; + } + + // Returns the next 9, 10, 11 or 12 bits + public int getNextCode() { + // Attempt to get the next code. The exception is caught to make + // this robust to cases wherein the EndOfInformation code has been + // omitted from a strip. Examples of such cases have been observed + // in practice. + try { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + + if (nextBits < bitsToGet) { + nextData = (nextData << 8) | (data[bytePointer++] & 0xff); + nextBits += 8; + } + + int code = + (nextData >> (nextBits - bitsToGet)) & andTable[bitsToGet-9]; + nextBits -= bitsToGet; + + return code; + } catch(ArrayIndexOutOfBoundsException e) { + // Strip not terminated as expected: return EndOfInformation code. + return 257; + } + } +} diff --git a/src/main/java/org/pdfparse/model/PDFDocCatalog.java b/src/main/java/org/pdfparse/model/PDFDocCatalog.java new file mode 100755 index 0000000..c55b175 --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFDocCatalog.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + + +import org.pdfparse.cos.*; +import org.pdfparse.parser.ParsingContext; +import java.util.ArrayList; + +public class PDFDocCatalog { + private COSDictionary dRoot; + private COSDictionary dPages; + private ParsingContext context; + private ArrayList pages; + + public PDFDocCatalog(ParsingContext context, COSDictionary dic) { + dRoot = dic; + this.context = context; + + context.softAssertSyntaxComliance(COSName.CATALOG.equals(dic.getName(COSName.TYPE, null)), "Document catalog should be /Catalog type"); + } + + + public COSDictionary getCOSDictionary() { + return dRoot; + } + + /** + * Return the total page count of the PDF document. + * + * @return The total number of pages in the PDF document. + */ + public int getPagesCount() { + if (pages == null) { + COSReference refRootPages = dRoot.getReference(COSName.PAGES); + dPages = context.objectCache.getDictionary(refRootPages); + return dPages.getUInt(COSName.COUNT, context.objectCache, -1); + }; + + return pages.size(); + } + private void loadPage(COSReference cosReference) { + COSDictionary dict = context.objectCache.getDictionary(cosReference); + if (dict.getName(COSName.TYPE, COSName.EMPTY).equals(COSName.PAGES)) { + loadPages(dict); // This is a page node + return; + } + + this.pages.add( new PDFPage(dict) ); + } + + private void loadPages(COSDictionary pages) { + context.softAssertStructure( + pages.getName(COSName.TYPE, COSName.EMPTY).equals(COSName.PAGES), + "This dictionary should be /Type = /Pages"); + + + COSArray kids = pages.getArray(COSName.KIDS, context.objectCache, null); + if (!context.softAssertStructure(kids != null, "Required entry '/Kids' not found")) { + return; // will be zero pages + } + + for (int i=0; i getPages() { + if (pages != null) + return pages; + + getPagesCount(); + loadPages(dPages); + + if (pages == null) { + + } + + return pages; + } + + /** + * Returns the PDF specification version this document conforms to. + * + * @return The PDF version. + */ + public String getVersion() { + return dRoot.getNameAsStr(COSName.VERSION, context.objectCache, ""); + } + + /** Sets the PDF specification version this document conforms to. + * + * @param version the PDF version (ex. "1.4") + */ + public void setVersion(String version) { + dRoot.setName(COSName.VERSION, new COSName(version)); + } + + /** + * Get the metadata that is part of the document catalog. This will + * return null if there is no meta data for this object. + * + * @return The metadata for this object. + */ + public byte[] getXMLMetadata() { + COSReference refMetadata = dRoot.getReference(COSName.METADATA); + if (refMetadata == null) + return null; + COSStream dMetadata = context.objectCache.getStream(refMetadata); + if (dMetadata == null) + return null; + return dMetadata.getData(); + } + + /** + * The language for the document. + * + * @return The language for the document. + */ + public String getLanguage() { + return dRoot.getStr(COSName.LANG, context.objectCache, ""); + } + + /** + * Set the Language for the document. + * + * @param language The new document language. + */ + public void setLanguage( String language ) { + dRoot.setStr( COSName.LANG, language ); + } + + + /** + * Get the page layout, see the PL_XXX constants. + * @return A COSName representing the page layout. + */ + public COSName getPageLayout() { + return dRoot.getName( COSName.PAGELAYOUT, COSName.PL_SINGLE_PAGE ); + } + + /** + * Set the page layout, see the PL_XXX constants for valid values. + * @param layout The new page layout. + */ + public void setPageLayout( COSName layout ) { + dRoot.setName( COSName.PAGELAYOUT, layout ); + } + + /** + * Get the page display mode, see the PM_XXX constants. + * @return A COSName representing the page mode. + */ + public COSName getPageMode() { + return dRoot.getName( COSName.PAGEMODE, COSName.PM_NONE ); + } + + /** + * Set the page mode. See the PM_XXX constants for valid values. + * @param mode The new page mode. + */ + public void setPageMode( COSName mode ) { + dRoot.setName( COSName.PAGEMODE, mode ); + } + + + + + +} diff --git a/src/main/java/org/pdfparse/model/PDFDocInfo.java b/src/main/java/org/pdfparse/model/PDFDocInfo.java new file mode 100755 index 0000000..9700957 --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFDocInfo.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + +import org.pdfparse.parser.PDFParser; +import org.pdfparse.cos.*; +import org.pdfparse.exception.EParseError; +import java.util.Calendar; + +/** + * This is the document metadata. Each getXXX method will return the entry if + * it exists or null if it does not exist. If you pass in null for the setXXX + * method then it will clear the value. + */ + +public class PDFDocInfo { + private COSDictionary info; + private PDFParser pdfParser; + private boolean owned; + + + /** + * Constructor that is used for a preexisting dictionary. + * + * @param dic The underlying dictionary. + * @param pdfParser Reference to the document parser object. + */ + public PDFDocInfo( COSDictionary dic, PDFParser pdfParser ) + { + if (dic == null) { + dic = new COSDictionary(); + owned = true; + } else owned = false; + + this.info = dic; + this.pdfParser = pdfParser; + } + + /** + * This will get the underlying dictionary that this object wraps. + * + * @return The underlying info dictionary. + */ + public COSDictionary getDictionary() + { + return info; + } + + /** + * This will get the title of the document. This will return null if no title exists. + * + * @return The title of the document. + * @throws EParseError If there is a problem retrieving the title + */ + public String getTitle() throws EParseError + { + return info.getStr(COSName.TITLE, pdfParser, ""); + } + + /** + * This will set the title of the document. + * + * @param title The new title for the document. + */ + public void setTitle( String title ) + { + info.setStr( COSName.TITLE, title ); + } + + /** + * This will get the author of the document. This will return null if no author exists. + * + * @return The author of the document. + * @throws EParseError If there is a problem retrieving the author + */ + public String getAuthor() throws EParseError + { + return info.getStr( COSName.AUTHOR, pdfParser, "" ); + } + + /** + * This will set the author of the document. + * + * @param author The new author for the document. + */ + public void setAuthor( String author ) + { + info.setStr( COSName.AUTHOR, author ); + } + + /** + * This will get the subject of the document. This will return null if no subject exists. + * + * @return The subject of the document. + * @throws EParseError If there is a problem retrieving the subject + */ + public String getSubject() throws EParseError + { + return info.getStr( COSName.SUBJECT, pdfParser, "" ); + } + + /** + * This will set the subject of the document. + * + * @param subject The new subject for the document. + */ + public void setSubject( String subject ) + { + info.setStr( COSName.SUBJECT, subject ); + } + + /** + * This will get the keywords of the document. This will return null if no keywords exists. + * + * @return The keywords of the document. + * @throws EParseError If there is a problem retrieving keywords + */ + public String getKeywords() throws EParseError + { + return info.getStr( COSName.KEYWORDS, pdfParser, "" ); + } + + /** + * This will set the keywords of the document. + * + * @param keywords The new keywords for the document. + */ + public void setKeywords( String keywords ) + { + info.setStr( COSName.KEYWORDS, keywords ); + } + + /** + * This will get the creator of the document. This will return null if no creator exists. + * + * @return The creator of the document. + * @throws EParseError If there is a problem retrieving the creator + */ + public String getCreator() throws EParseError + { + return info.getStr( COSName.CREATOR, pdfParser, "" ); + } + + /** + * This will set the creator of the document. + * + * @param creator The new creator for the document. + */ + public void setCreator( String creator ) + { + info.setStr( COSName.CREATOR, creator ); + } + + /** + * This will get the producer of the document. This will return null if no producer exists. + * + * @return The producer of the document. + * @throws EParseError If there is a problem retrieving the producer + */ + public String getProducer() throws EParseError + { + return info.getStr( COSName.PRODUCER, pdfParser, "" ); + } + + /** + * This will set the producer of the document. + * + * @param producer The new producer for the document. + */ + public void setProducer( String producer ) + { + info.setStr( COSName.PRODUCER, producer ); + } + + /** + * This will get the creation date of the document. This will return null if no creation date exists. + * + * @return The creation date of the document. + * + * @throws EParseError If there is an error creating the date. + */ + public Calendar getCreationDate() throws EParseError + { + return info.getDate( COSName.CREATION_DATE, pdfParser, null ); + } + + /** + * This will set the creation date of the document. + * + * @param date The new creation date for the document. + */ + public void setCreationDate( Calendar date ) + { + info.setDate( COSName.CREATION_DATE, date ); + } + + /** + * This will get the modification date of the document. This will return null if no modification date exists. + * + * @return The modification date of the document. + * + * @throws EParseError If there is an error creating the date. + */ + public Calendar getModificationDate() throws EParseError + { + return info.getDate( COSName.MOD_DATE, pdfParser, null ); + } + + /** + * This will set the modification date of the document. + * + * @param date The new modification date for the document. + */ + public void setModificationDate( Calendar date ) + { + info.setDate( COSName.MOD_DATE, date ); + } + + /** + * This will get the trapped value for the document. + * This will return COSName.UNKNOWN if one is not found. + * + * @return The trapped value for the document. + */ + public COSName getTrapped() + { + return info.getName(COSName.TRAPPED, COSName.UNKNOWN); + } + + /** + * This will get the keys of all metadata information fields for the document. + * + * @return all metadata key strings. + */ +// public Set getMetadataKeys() +// { +// Set keys = new TreeSet(); +// for (COSName key : info.keySet()) { +// keys.add(key.getName()); +// } +// return keys; +// } + + /** + * This will get the value of a custom metadata information field for the document. + * This will return null if one is not found. + * + * @param fieldName Name of custom metadata field from pdf document. + * + * @return String Value of metadata field + * + */ + public String getCustomMetadataValue(COSName fieldName) + { + return info.getStr( fieldName, "" ); + } + + /** + * Set the custom metadata value. + * + * @param fieldName The name of the custom metadata field. + * @param fieldValue The value to the custom metadata field. + */ + public void setCustomMetadataValue( COSName fieldName, String fieldValue ) + { + info.setStr( fieldName, fieldValue ); + } + + /** + * This will set the trapped of the document. This will be + * 'True', 'False', or 'Unknown'. + * + * @param value The new trapped value for the document. + */ + public void setTrapped( String value ) + { + if( value != null && + !value.equals( "True" ) && + !value.equals( "False" ) && + !value.equals( "Unknown" ) ) + { + throw new IllegalArgumentException( "Valid values for trapped are 'True', 'False', or 'Unknown'" ); + } + + info.setStr(COSName.TRAPPED, value); + } +} diff --git a/src/main/java/org/pdfparse/model/PDFDocument.java b/src/main/java/org/pdfparse/model/PDFDocument.java new file mode 100755 index 0000000..acf832a --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFDocument.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + +import org.pdfparse.cos.*; +import org.pdfparse.exception.*; +import org.pdfparse.parser.*; +import org.pdfparse.parser.ParsingContext; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; + + +public class PDFDocument implements ParsingEvent { + private String filename; + private String filepath; + private boolean loaded; + + private ParsingContext context; + private PDFParser pdfParser; + + private COSReference rootID = null; + private COSReference infoID = null; + + private COSDictionary encryption = null; + + private PDFDocInfo documentInfo = null; + private PDFDocCatalog documentCatalog = null; + private byte[][] documentId = {null,null}; + private boolean documentIsEncrypted = false; + private float documentVersion = 0.0f; + + public PDFDocument() { + context = new ParsingContext(); + } + + public void close() { + pdfParser.done(); + loaded = false; + } + + public PDFDocument(String filename) throws EParseError, IOException { + this(); + File file = new File(filename); + open(file); + } + + public PDFDocument(File file) throws EParseError, IOException { + this(); + open(file); + } + + public PDFDocument(byte[] buffer) throws EParseError { + this(); + + this.filename = "internal"; + this.filepath = "internal"; + + open(buffer); + } + + private void open(File file) throws EParseError, IOException { + this.filename = file.getName(); + this.filepath = file.getParent(); + + FileInputStream fin = new FileInputStream(file); + FileChannel channel = fin.getChannel(); + + byte[] barray = new byte[(int) file.length()]; + ByteBuffer bb = ByteBuffer.wrap(barray); + bb.order(ByteOrder.BIG_ENDIAN); + channel.read(bb); + + open(barray); + } + + public void open(byte[] buffer) throws EParseError { + PDFRawData data = new PDFRawData(buffer); + pdfParser = new PDFParser(data, context, this); + loaded = true; + } + + /** + * Tell if this document is encrypted or not. + * + * @return true If this document is encrypted. + */ + public boolean isEncrypted() { + return documentIsEncrypted; + } + + public byte[][] getDocumentId() { + return documentId; + } + + /** + * Get the document info dictionary. This is guaranteed to not return null. + * + * @return The documents /Info dictionary + */ + public PDFDocInfo getDocumentInfo() throws EParseError { + if (documentInfo != null) + return documentInfo; + + COSDictionary dictInfo = null; + if (infoID != null) + dictInfo = pdfParser.getDictionary(infoID.id, infoID.gen, false); + + documentInfo = new PDFDocInfo(dictInfo, pdfParser); + return documentInfo; + } + + /** + * This will get the document CATALOG. This is guaranteed to not return null. + * + * @return The documents /Root dictionary + */ + public PDFDocCatalog getDocumentCatalog() throws EParseError { + if (documentCatalog == null) + { + COSDictionary dictRoot; + dictRoot = pdfParser.getDictionary(rootID, true); + + documentCatalog = new PDFDocCatalog(context, dictRoot); + } + return documentCatalog; + } + + public float getDocumentVersion() { + return documentVersion; + } + + @Override + public int onTrailerFound(COSDictionary trailer, int ordering) { + if (ordering == 0) { + rootID = trailer.getReference(COSName.ROOT); + infoID = trailer.getReference(COSName.INFO); + + documentIsEncrypted = trailer.containsKey(COSName.ENCRYPT); + + COSArray Ids = trailer.getArray(COSName.ID, null); + if (((Ids == null) || (Ids.size()!=2)) && documentIsEncrypted) + throw new EParseError("Missing (required) file identifier for encrypted document"); + + if (Ids != null) { + if (Ids.size() != 2) { + if (documentIsEncrypted) + throw new EParseError("Invalid document ID array size (should be 2)"); + context.softAssertSyntaxComliance(false, "Invalid document ID array size (should be 2)"); + + Ids = null; + } else { + if ((Ids.get(0) instanceof COSString) && (Ids.get(1) instanceof COSString)) { + documentId[0] = ((COSString)Ids.get(0)).getBinaryValue(); + documentId[1] = ((COSString)Ids.get(1)).getBinaryValue(); + } else context.softAssertSyntaxComliance(false, "Invalid document ID"); + } + } // Ids != null + } + return ParsingEvent.CONTINUE; + } + + @Override + public int onEncryptionDictFound(COSDictionary enc, int ordering) { + if (ordering == 0) + encryption = enc; + return ParsingEvent.CONTINUE; + } + + @Override + public int onNotSupported(String msg) { + //throw new UnsupportedOperationException("Not supported yet."); + return ParsingEvent.CONTINUE; + } + + @Override + public void onDocumentVersionFound(float version) { + this.documentVersion = version; + } + + public void dbgDump() { + //xref.dbgPrintAll(); + pdfParser.parseAndCacheAll(); + //cache.dbgSaveAllStreams(filepath + File.separator + "[" + filename + "]" ); + //cache.dbgSaveAllObjects(filepath + File.separator + "[" + filename + "]" ); + + } +} diff --git a/src/main/java/org/pdfparse/model/PDFPage.java b/src/main/java/org/pdfparse/model/PDFPage.java new file mode 100755 index 0000000..e2fb71d --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFPage.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + +import org.pdfparse.cds.PDFRectangle; +import org.pdfparse.cos.COSDictionary; +import org.pdfparse.cos.COSName; + +/** + * This represents a single page in a PDF document. + * + *

+ * A Page object is a dictionary whose keys describe a single page containing text, + * graphics, and images. A Page object is a leaf of the Pages tree.
+ * This object is described in the 'Portable Document Format Reference Manual version 1.3' + * section 6.4 (page 73-81) + * + * @see PDFPageNode + */ + +public class PDFPage { + COSDictionary dPage; + + /** + * Creates a new instance of PDPage with a size of 8.5x11. + */ + public PDFPage() { + dPage = new COSDictionary(); + dPage.setName(COSName.TYPE, COSName.PAGE); + //setMediaBox(PAGE_SIZE_LETTER); + } + + /** + * Creates a new instance of PDPage. + * + * @param size The MediaBox or the page. + */ + public PDFPage(PDFRectangle size) + { + dPage = new COSDictionary(); + dPage.setName(COSName.TYPE, COSName.PAGE); + //setMediaBox(size); + } + + /** + * Creates a new instance of PDPage. + * + * @param pageDic The existing page dictionary. + */ + public PDFPage(COSDictionary pageDic) + { + dPage = pageDic; + } + + + /** + * This will get the underlying dictionary that this class acts on. + * + * @return The underlying dictionary for this class. + */ + public COSDictionary getCOSDictionary() { + return dPage; + } + + /** + * A rectangle, expressed in default user space units, defining the boundaries of the physical medium on which the + * page is intended to be displayed or printed + * + * This will get the MediaBox at this page and not look up the hierarchy. This attribute is inheritable, and + * findMediaBox() should probably used. This will return null if no MediaBox are available at this level. + * + * @return The MediaBox at this level in the hierarchy. + */ + public PDFRectangle getMediaBox() { + return dPage.getRectangle(COSName.MEDIABOX); + } + + /** + * Set the mediaBox for this page. + * + * @param value The new mediaBox for this page. + */ + public void setMediaBox(PDFRectangle value) { + if (value == null) { + dPage.remove(COSName.MEDIABOX); + } else { + dPage.setRectangle(COSName.MEDIABOX, value); + } + } + + /** + * A rectangle, expressed in default user space units, defining the visible region of default user space. When the + * page is displayed or printed, its contents are to be clipped (cropped) to this rectangle and then imposed on the + * output medium in some implementation-defined manner + * + * This will get the CropBox at this page and not look up the hierarchy. This attribute is inheritable, and + * findCropBox() should probably used. This will return null if no CropBox is available at this level. + * + * @return The CropBox at this level in the hierarchy. + */ + public PDFRectangle getCropBox() { + return dPage.getRectangle(COSName.CROPBOX); + } + + /** + * Set the CropBox for this page. + * + * @param value The new CropBox for this page. + */ + public void setCropBox(PDFRectangle value) { + if (value == null) { + dPage.remove(COSName.CROPBOX); + } else { + dPage.setRectangle(COSName.CROPBOX, value); + } + } + + + + +} \ No newline at end of file diff --git a/src/main/java/org/pdfparse/model/PDFPageNode.java b/src/main/java/org/pdfparse/model/PDFPageNode.java new file mode 100755 index 0000000..e3f8f1a --- /dev/null +++ b/src/main/java/org/pdfparse/model/PDFPageNode.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.model; + +import org.pdfparse.cos.COSDictionary; +import org.pdfparse.cos.COSName; +import org.pdfparse.cos.COSReference; + +/** + * This represents a page node in a pdf document. + *

+ * The Pages of a document are accessible through a tree of nodes known as the Pages tree. + * This tree defines the ordering of the pages in the document.
+ * This object is described in the 'Portable Document Format Reference Manual version 1.3' + * section 6.3 (page 71-73) + * + * @see org.pdfparse.model.PDFPage + */ + +public class PDFPageNode { + + private COSDictionary dPageNode; + + /** + * Creates a new instance of PDPage. + */ + public PDFPageNode() { + dPageNode = new COSDictionary(); + dPageNode.setName( COSName.TYPE, COSName.PAGES); + //page.setName(COSName.KIDS, new COSArray()); + dPageNode.setInt(COSName.COUNT, 0); + } + + /** + * Creates a new instance of PDPage. + * + * @param pages The dictionary pages. + */ + public PDFPageNode( COSDictionary pages ) { + dPageNode = pages; + } + + /** + * Get the count of descendent page objects. + * + * @return The total number of descendent page objects. + */ + public int getCount() { + if(dPageNode == null) + return 0; + + return dPageNode.getInt(COSName.COUNT, 0); + } + + /** + * This will get the underlying dictionary that this class acts on. + * + * @return The underlying dictionary for this class. + */ + public COSDictionary getCOSDictionary() { + return dPageNode; + } + + /** + * The parent page node. + * + * @return The parent to this page. + */ + public COSReference getParent() { + return dPageNode.getReference(COSName.PARENT); + } + + /** + * Set the parent of this page. + * + * @param parent The parent to this page node. + */ + public void setParent( COSReference parent ) { + dPageNode.setReference( COSName.PARENT, parent ); + } +} diff --git a/src/main/java/org/pdfparse/parser/PDFParser.java b/src/main/java/org/pdfparse/parser/PDFParser.java new file mode 100755 index 0000000..c1ebf6f --- /dev/null +++ b/src/main/java/org/pdfparse/parser/PDFParser.java @@ -0,0 +1,871 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + + +import org.pdfparse.PDFDefines; +import org.pdfparse.cos.*; +import org.pdfparse.exception.EParseError; +import org.pdfparse.filter.StreamDecoder; +import org.pdfparse.utils.IntObjHashtable; + +import java.util.Arrays; + +public class PDFParser implements ParsingGetObject { + private static final byte[] OBJ = {0x6F, 0x62, 0x6A}; + private static final byte[] ENDOBJ = {0x65, 0x6E, 0x64, 0x6F, 0x62, 0x6A}; + + private static final byte[] STREAM = {0x73, 0x74, 0x72, 0x65, 0x61, 0x6D}; + private static final byte[] ENDSTREAM = {0x65, 0x6E, 0x64, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D}; + + private static final byte[] PDF_HEADER = {0x25, 0x50, 0x44, 0x46, 0x2D}; // "%PDF-"; + private static final byte[] FDF_HEADER = {0x25, 0x46, 0x44, 0x46, 0x2D}; // "%FDF-"; + + private static final byte[] EOF = {0x25, 0x25, 0x45, 0x4F, 0x46}; // "%%EOF" + private static final byte[] STARTXREF = {0x73, 0x74, 0x61, 0x72, 0x74, 0x78, 0x72, 0x65, 0x66}; // "startxref" + + private static final byte[] XREF = {0x78, 0x72, 0x65, 0x66}; + private static final byte[] TRAILER = {0x74, 0x72, 0x61, 0x69, 0x6C, 0x65, 0x72}; + + private ParsingContext pContext; + private PDFRawData pdfData; + private ParsingEvent parsingEvent; + + private IntObjHashtable by_id; + + private int max_id = 0; + private int max_gen = 0; + private int max_offset = 0; + + private int compressed_max_stream_id = 0; + private int compressed_max_stream_offs = 0; + + public PDFParser(PDFRawData pData, ParsingContext pContext, ParsingEvent evt) { + this.pContext = pContext; + this.pdfData = pData; + this.pContext.objectCache = this; + this.parsingEvent = evt; + by_id = new IntObjHashtable(); + parse(); + } + + public void done() { + pContext = null; + by_id.clear(); + } + + public void clear() { + by_id.clear(); + max_id = 0; + max_gen = 0; + max_offset = 0; + compressed_max_stream_id = 0; + compressed_max_stream_offs = 0; + } + + /** + * Parse a PDF file data + * @param src PDF file data to parse + * @param evt Listener to broadcast parsing events + * @return Trailer dictionary + */ + private COSDictionary parse() { + PDFRawData src = pdfData; + + if (src.length < 10) { + throw new EParseError("This is not a valid PDF file"); + } + + // Check the PDF header & version ----------------------- + src.pos = 0; + if ( !( src.checkSignature(PDF_HEADER) || src.checkSignature(FDF_HEADER) ) ) { + if (!pContext.allowScan) + throw new EParseError("This is not a PDF file"); + + while ( !(src.checkSignature(PDF_HEADER) || src.checkSignature(FDF_HEADER)) + && (src.pos < pContext.headerLookupRange) && (src.pos < src.length) ) src.pos++; + + if ( !(src.checkSignature(PDF_HEADER) || src.checkSignature(FDF_HEADER)) ) + throw new EParseError("This is not a PDF file (PDF header not found)"); + } + + if (src.length - src.pos < 10) + throw new EParseError("This is not a valid PDF file"); + + + if ((src.src[src.pos + 5] != '1') || (src.src[src.pos + 7] < '1') || (src.src[src.pos + 7] > '8')) { + throw new EParseError("PDF version is not supported"); + } + + double documentVersion = (src.src[src.pos + 5] - '0') + (src.src[src.pos + 7] - '0') / 10.0; + parsingEvent.onDocumentVersionFound((float)documentVersion); + + + // Scan for EOF ----------------------------------------- + if (src.reverseScan(src.length, EOF, pContext.eofLookupRange) < 0) + throw new EParseError("Missing end of file marker"); + + // Scan for 'startxref' marker -------------------------- + if (src.reverseScan(src.pos, STARTXREF, 100) < 0) + throw new EParseError("Missing 'startxref' marker"); + + + // Fetch XREF offset ------------------------------------ + src.pos += 10; + src.skipWS(); + + int xref_offset = COSNumber.readInteger(src); + + if ((xref_offset == 0) || (xref_offset >= src.length)) { + throw new EParseError("Invalid xref offset"); + } + + src.pos = xref_offset; + + src.skipWS(); + if (src.checkSignature(XREF)) + return parseTableAndTrailer(src, parsingEvent); + return parseXRefStream(src, false, 0, parsingEvent); + } + + public XRefEntry getXRefEntry(int id, int gen) { + return by_id.get(id); + } + + public XRefEntry getXRefEntry(int id) { + return by_id.get(id); + } + +// public Set getIdSet() { +// return by_id.keySet(); +// } + + public COSObject getCOSObject(int id, int gen) throws EParseError { + COSReference header; + + XRefEntry x = by_id.get(id); // TODO: what with GEN ? + + if (x == null) + return new COSNull(); + + if (x.cachedObject != null) + return x.cachedObject; + + if (x.gen != gen) { + if (PDFDefines.DEBUG) + System.out.printf("Object with generation %d not found. But there is %d generation number", gen, x.gen); + } + + if (!x.isCompressed) { + pdfData.pos = x.fileOffset; + //----- + + header = this.tryFetchIndirectObjHeader(pdfData, pContext.tmpReference); + if (header == null) + throw new EParseError(String.format("Invalid indirect object header (expected '%d %d obj' @ %d)", id, gen, pdfData.pos)); + if ((header.id != id)||(header.gen != gen)) + throw new EParseError(String.format("Object header not correspond data specified in reference (expected '%d %d obj' @ %d)", id, gen, pdfData.pos)); + pdfData.skipWS(); + //----- + x.cachedObject = this.parseObject(pdfData, pContext); + return x.cachedObject; + } + + // Compressed ---------------------------------------------------- + XRefEntry cx = by_id.get(x.containerObjId); + if (cx == null) + return new COSNull(); + + if (cx.cachedObject == null) { // Extract compressed block (stream object) + pdfData.pos = cx.fileOffset; + //----- + header = this.tryFetchIndirectObjHeader(pdfData, pContext.tmpReference); + if (header == null) + throw new EParseError("Invalid indirect object header"); + if ((header.id != x.containerObjId)||(header.gen != 0)) + throw new EParseError("Object header not correspond data specified in reference"); + pdfData.skipWS(); + //----- + cx.cachedObject = this.parseObject(pdfData, pContext); + + if (! (cx.cachedObject instanceof COSStream)) + throw new EParseError("Referenced object-container is not stream object"); + } + + COSStream streamObject = (COSStream)cx.cachedObject; + + // --- Ok, received streamObject + // next, decompress its data, and put in cache + if (cx.decompressedStreamData == null) { + cx.decompressedStreamData = StreamDecoder.decodeStream(streamObject.getData(), streamObject, pContext); + } + PDFRawData streamData = cx.decompressedStreamData; + + // -- OK, retrieved from cache decompressed data + // Parse stream index & content + + int n = streamObject.getInt(COSName.N, 0); + int first = streamObject.getInt(COSName.FIRST, 0); + int idxId, idxOffset, savepos; + XRefEntry idxXRefEntry; + COSObject obj = null; + for (int i=0; i= len) return null; + + // parse int #1 -------------------------------------------- + ch = src.src[pos]; + while ((ch >= 0x30)&&(ch <= 0x39)) { + obj_id = obj_id*10 + (ch - 0x30); + pos++; // 0..9 + if (pos >= len) return null; + ch = src.src[pos]; + } + + //check if not a whitespace or EOF + if ((pos >= len)||(!((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)||(ch==0x00)))) + return null; + pos++; // skip this space + if (pos >= len) return null; + + // skip succeeded spaces if any + ch = src.src[pos]; + while ((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)) { + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + // parse int #2 -------------------------------------------- + while ((ch >= 0x30)&&(ch <= 0x39)) { + obj_gen = obj_gen*10 + (ch - 0x30); + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + //check if not a whitespace or EOF + if (!((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)||(ch==0x00))) + return null; + pos++; // skip space + if (pos >= len) return null; + + // skip succeeded spaces if any + ch = src.src[pos]; + while ((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)) { + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + // check if next char is R --------------------------------- + if (src.src[pos] != 0x52) // 'R' + return null; + + src.pos = ++pos; // beyond the 'R' + + return new COSReference(obj_id, obj_gen); + } + + // if next token is not a object header, function return null (without position changes) + // else it fetches token and change stream position + private static COSReference tryFetchIndirectObjHeader(PDFRawData src, COSReference outHeader) { + int pos = src.pos; + int len = src.length; + int ch; + + int obj_id = 0, obj_gen = 0; + + if (pos >= len) return null; + + // parse int #1 -------------------------------------------- + ch = src.src[pos]; + while ((ch >= 0x30)&&(ch <= 0x39)) { + obj_id = obj_id*10 + (ch - 0x30); + pos++; // 0..9 + if (pos >= len) return null; + ch = src.src[pos]; + } + + //check if not a whitespace or EOF + if ((!((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)||(ch==0x00)))) + return null; + pos++; // skip this space + if (pos >= len) return null; + + // skip succeeded spaces if any + ch = src.src[pos]; + while ((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)) { + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + // parse int #2 -------------------------------------------- + while ((ch >= 0x30)&&(ch <= 0x39)) { + obj_gen = obj_gen*10 + (ch - 0x30); + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + //check if not a whitespace or EOF + if ((!((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)||(ch==0x00)))) + return null; + pos++; // skip space + if (pos >= len) return null; + + // skip succeeded spaces if any + ch = src.src[pos]; + while ((ch==0x20)||(ch==0x09)||(ch==0x0A)||(ch==0x0D)) { + pos++; + if (pos >= len) return null; + ch = src.src[pos]; + } + + // check if next char is obj --------------------------------- + if (!src.checkSignature(pos, OBJ)) // 'obj' + return null; + + src.pos = pos + 3; // beyond the 'obj' + + outHeader.set(obj_id, obj_gen); + + return outHeader; + } + + public static final byte[] fetchStream(PDFRawData src, int stream_len, boolean movePosBeyoundEndObj) throws EParseError { + src.skipWS(); + if (!src.checkSignature(STREAM)) + throw new EParseError("'stream' keyword not found"); + src.pos += STREAM.length; + src.skipCRLForLF(); + if (src.pos + stream_len > src.length) + throw new EParseError("Unexpected end of file (stream object too large)"); + + // TODO: Lazy parse (reference + start + len) + byte[] res = Arrays.copyOfRange(src.src, src.pos, src.pos + stream_len); + src.pos += stream_len; + + if (movePosBeyoundEndObj) { + byte firstbyte = ENDOBJ[0]; + int max_pos = src.length - ENDOBJ.length; + if (max_pos - src.pos > PDFDefines.MAX_SCAN_RANGE) + max_pos = src.pos + PDFDefines.MAX_SCAN_RANGE; + for (int i = src.pos; i < max_pos; i++) + if ((src.src[i] == firstbyte)&&src.checkSignature(i, ENDOBJ)) { + src.pos = i + ENDOBJ.length; + return res; + } + + throw new EParseError("'endobj' tag not found"); + } + + return res; + } + + private void addXref(int id, int gen, int offs) throws EParseError { + // Skip invalid or not-used objects (assumed that they are free objects) + if (offs == 0) { + if (PDFDefines.DEBUG) + System.out.printf("XREF: Got object with zero offset. Assumed that this was a free object(%d %d R)\r\n", id, gen); + return; + } + if (offs < 0) + throw new EParseError("Negative offset for object id=%d", id); + + XRefEntry obj = new XRefEntry(); + obj.id = id; + obj.gen = gen; + obj.fileOffset = offs; + obj.isCompressed = false; + + XRefEntry old_obj = by_id.get(id); + + if (old_obj == null) { + by_id.put(id, obj); + } else if (old_obj.gen < gen) { + by_id.put(id, obj); + } + + if (max_id < id) max_id = id; + if (max_offset < offs) max_offset = offs; + if (max_gen < gen) max_gen = gen; + } + + private void addXrefCompressed(int id, int containerId, int indexWithinContainer) throws EParseError { + // Skip invalid or not-used objects (assumed that they are free objects) + if (containerId == 0) { + if (PDFDefines.DEBUG) + System.out.printf("XREF: Got containerId which is zero. Assumed that this was a free object (%d 0 R)\r\n", id); + return; + } + if (indexWithinContainer < 0) + throw new EParseError(String.format("Negative indexWithinContainer for compressed object id=%d in stream #%d", id, containerId)); + + XRefEntry obj = new XRefEntry(); + obj.id = id; + obj.gen = 0; + obj.fileOffset = 0; + obj.isCompressed = true; + obj.containerObjId = containerId; + obj.indexWithinContainer = indexWithinContainer; + + by_id.put(id, obj); + + if (compressed_max_stream_id 0x39)) break; // not in [0..9] range + }// while(1)... + } + + private COSDictionary parseTableAndTrailer(PDFRawData src, ParsingEvent evt) throws EParseError { + int prev = src.pos; + int xrefstrm = 0; + int res, trailer_ordering = 0; + COSDictionary curr_trailer = null; + COSDictionary dic_trailer = null; + + while (prev != 0) { + src.pos = prev; + // Parse XREF --------------------- + if (!src.checkSignature(XREF)) + throw new EParseError("This is not an 'xref' table"); + src.pos += XREF.length; + + parseTableOnly(src, false); + // Parse Trailer ------------------ + src.skipWS(); + if (!src.checkSignature(TRAILER)) + throw new EParseError("Cannot find 'trailer' tag"); + src.pos += TRAILER.length; + src.skipWS(); + + curr_trailer = new COSDictionary(src, pContext); + prev = curr_trailer.getInt(COSName.PREV, 0); + if (trailer_ordering == 0) + dic_trailer = curr_trailer; + + res = evt.onTrailerFound(curr_trailer, trailer_ordering); + if ((res & ParsingEvent.ABORT_PARSING) != 0) + return dic_trailer; + + // TODO: mark encrypted objects for removing + //----------------------- + if (trailer_ordering == 0) { + xrefstrm = curr_trailer.getInt(COSName.XREFSTM, 0); + if (xrefstrm != 0) { // This is an a hybrid PDF-file + //res = parsingEvent.onNotSupported("Hybrid PDF-files not supported"); + //if ((res&ParsingEvent.CONTINUE) == 0) + // return dic_trailer; + + src.pos = xrefstrm; + parseXRefStream(src, true, trailer_ordering+1, evt); + } + } + trailer_ordering++; + } // while + return dic_trailer; + } + + private COSDictionary parseXRefStream(PDFRawData src, boolean override, int trailer_ordering, ParsingEvent evt) throws EParseError { + COSDictionary curr_trailer, dic_trailer = null; + int res, prev; + while (true) { + src.skipWS(); + + COSReference x = PDFParser.tryFetchIndirectObjHeader(src, pContext.tmpReference); + if (x == null) + throw new EParseError("Invalid indirect object header"); + + src.skipWS(); + + + //addXRef(65530, 0, trailerOffset); + curr_trailer = new COSDictionary(src, pContext); + if (trailer_ordering == 0) + dic_trailer = curr_trailer; + + res = evt.onTrailerFound(curr_trailer, trailer_ordering); + if ((res & ParsingEvent.ABORT_PARSING) != 0) + return dic_trailer; + + // TODO: Mark 'encrypt' objects for removing + + if (!curr_trailer.getName(COSName.TYPE, null).equals(COSName.XREF)) + throw new EParseError("This is not a XRef stream"); + + + COSArray oW = curr_trailer.getArray(COSName.W, null); + if ((oW == null) || (oW.size() != 3)) + throw new EParseError("Invalid PDF file"); + int[] w = {oW.getInt(0), oW.getInt(1), oW.getInt(2)}; + + int size = curr_trailer.getUInt(COSName.SIZE, 0); + COSArray index = curr_trailer.getArray(COSName.INDEX, null); + if (index == null) { + index = new COSArray(); + index.add(new COSNumber(0)); + index.add(new COSNumber(size)); + } + + //int row_len = w[0] + w[1] + w[2]; + + //byte[] bstream = // TODO: implement max verbosity mode + // src.fetchStream(curr_trailer.getUInt(COSName.LENGTH, 0), false); + + PDFRawData bstream; + bstream = StreamDecoder.decodeStream(src, curr_trailer, pContext); + + int start; + int count; + int index_idx = 0; + + int itype, i2, i3; + + while (index_idx < index.size()) { + start = index.getInt(index_idx++); + count = index.getInt(index_idx++); + + int i = 0; + while (i < count) { + if (w[0] != 0) itype = bstream.fetchBinaryUInt(w[0]); else itype = 1; // default value (see specs) + if (w[1] != 0) i2 = bstream.fetchBinaryUInt(w[1]); else i2 = 0; + if (w[2] != 0) i3 = bstream.fetchBinaryUInt(w[2]); else i3 = 0; + + switch(itype) { + case 0: // linked list of free objects (corresponding to f entries in a cross-reference table). + i++; //TODO: mark as free (delete if exist) + continue; + case 1: // objects that are in use but are not compressed (corresponding to n entries in a cross-reference table). + addXref((start+i), i3, i2); + i++; + continue; + case 2: // compressed objects. + addXrefCompressed(start+i, i2, i3); + i++; + continue; + default: + //throw new EParseError("Invalid iType entry in xref stream"); + if (PDFDefines.DEBUG) + System.out.printf("Invalid iType entry in xref stream: %d\r\n", itype ); + continue; + }// switch + }// for + } // while + + prev = curr_trailer.getInt(COSName.PREV, 0); + if (prev != 0) { + if ((prev < 0) || (prev > src.length)) + throw new EParseError("Invalid trailer offset (%d)", prev); + src.pos = prev; + trailer_ordering++; + continue; + } else break; + } // while (true) + + return dic_trailer; + + } + + public void dbgPrintAll() { + System.out.printf("Max id: %d\r\n", max_id); + System.out.printf("Max gen: %d\r\n", max_gen); + System.out.printf("Max offset: %d\r\n", max_offset); + System.out.printf("Compressed max stream id: %d\r\n", compressed_max_stream_id); + System.out.printf("Compressed max stream offs: %d\r\n", compressed_max_stream_offs); + + XRefEntry xref; + int[] keys = by_id.getKeys(); + for (int i = 0; i length)) + throw new EParseError("Out of range"); // TODO: special exception + + int r = 0; + int b; + + b = src[pos++]; + r = (b & 0xFF); + if (size == 1) return r; + + b = src[pos++] & 0xFF; + r = (r<<8) | b; + if (size == 2) return r; + + b = src[pos++] & 0xFF; + r = (r<<8) | b; + if (size == 3) return r; + + b = src[pos++] & 0xFF; + r = (r<<8) | b; + if (size == 4) return r; + + throw + new EParseError("Invalid bytes length"); + + } + + public final boolean checkSignature(byte[] sign) { + int _to = this.pos + sign.length; + if (_to > this.length) return false; + for (int i = this.pos, j=0; i<_to; i++, j++) + if (this.src[i] != sign[j]) + return false; + return true; + } + + public final boolean checkSignature(int from, byte[] sign) { + int _to = from + sign.length; + if (_to > this.length) return false; + for (int i = from, j=0; i<_to; i++, j++) + if (this.src[i] != sign[j]) + return false; + return true; + } + + public final int reverseScan(int from, byte[] sign, int limit) { + pos = from - sign.length; + if (pos < 0) { + pos = 0; + return -1; + } + + int scanto = pos - limit; + if (scanto < 0) scanto = 0; + + boolean found; + + while (pos >= scanto) { + found = true; + for (int i = 0; i < sign.length; i++) + if (this.src[pos + i] != sign[i]) { + found = false; + break; + } + if (found) + return pos; + pos--; + } + pos = scanto; + return -1; + } + + public String dbgPrintBytes() { + int len = 90; + + if (this.pos+len > this.length) + len = this.length - this.pos; + + byte[] chunk = new byte[len]; + + System.arraycopy(src, pos, chunk, 0, len); + String s = ""; + + for (int i=0; i 0x19) + s += (char)chunk[i]; + else + s += "x"+ String.format("%02X", chunk[i]&0xFF); + + return s + " @ " + String.valueOf(pos); + } + + public String dbgPrintBytesBefore() { + int l = 20; + int r = 20; + + if (this.pos+r > this.length) + r = this.length - this.pos; + if (this.pos-l < 0) + l = this.pos; + + int len = r + l; + byte[] chunk = new byte[len]; + + System.arraycopy(src, pos - l, chunk, 0, len); + String s = ""; + + for (int i=0; i 0x19) + s += (char)chunk[i]; + else + s += "x"+ String.format("%02X", chunk[i]&0xFF); + if (i == l) s += "] "; + } + + return s + " @ " + String.valueOf(pos); + } +} diff --git a/src/main/java/org/pdfparse/parser/ParsingContext.java b/src/main/java/org/pdfparse/parser/ParsingContext.java new file mode 100755 index 0000000..0732885 --- /dev/null +++ b/src/main/java/org/pdfparse/parser/ParsingContext.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + +import org.pdfparse.cos.COSReference; +import org.pdfparse.exception.EParseError; +import org.pdfparse.utils.ByteBuffer; + +public class ParsingContext { + private boolean checkSyntaxCompliance = false; + private boolean ignoreErrors = false; + private boolean ignoreBasicSyntaxErrors = false; + + + public boolean allowScan = true; + public int headerLookupRange = 100; + public int eofLookupRange = 100; + + public ByteBuffer tmpBuffer = new ByteBuffer(1024); + public COSReference tmpReference = new COSReference(0, 0); + + public ParsingGetObject objectCache; + public boolean useEncryption; + public byte[] encryptionKey; + + public ParsingContext() { + + } + + public void checkAndLog(boolean canContinue, String message) { + if (canContinue) + System.err.println(message); + else + throw new EParseError(message); + } + + public boolean softAssertSyntaxComliance(boolean condition, String message) { + if (!condition) + checkAndLog(checkSyntaxCompliance, message); + return condition; + } + + public boolean softAssertFormatError(boolean condition, String message) { + if (!condition) + checkAndLog(ignoreBasicSyntaxErrors, message); + return condition; + } + + public boolean softAssertStructure(boolean condition, String message) { + if (!condition) + checkAndLog(ignoreErrors, message); + return condition; + } +} diff --git a/src/main/java/org/pdfparse/parser/ParsingEvent.java b/src/main/java/org/pdfparse/parser/ParsingEvent.java new file mode 100755 index 0000000..4ebfa05 --- /dev/null +++ b/src/main/java/org/pdfparse/parser/ParsingEvent.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + +import org.pdfparse.cos.COSDictionary; + +public interface ParsingEvent { + public final static int CONTINUE = 1; + public final static int ABORT_PARSING = 2; + + public int onTrailerFound(COSDictionary trailer, int ordering); + public int onEncryptionDictFound(COSDictionary enc, int ordering); + public int onNotSupported(String msg); + public void onDocumentVersionFound(float version); + +} diff --git a/src/main/java/org/pdfparse/parser/ParsingGetObject.java b/src/main/java/org/pdfparse/parser/ParsingGetObject.java new file mode 100755 index 0000000..a530041 --- /dev/null +++ b/src/main/java/org/pdfparse/parser/ParsingGetObject.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + +import org.pdfparse.cos.*; + +public interface ParsingGetObject { + public COSObject getObject (COSReference ref); + public COSDictionary getDictionary(COSReference ref); + public COSStream getStream(COSReference ref); + +} diff --git a/src/main/java/org/pdfparse/parser/XRefEntry.java b/src/main/java/org/pdfparse/parser/XRefEntry.java new file mode 100755 index 0000000..29ce571 --- /dev/null +++ b/src/main/java/org/pdfparse/parser/XRefEntry.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.parser; + +import org.pdfparse.cos.COSObject; + +public class XRefEntry { + public int id; + public int gen; + public int fileOffset; + public int containerObjId; + public int indexWithinContainer; + + public boolean isCompressed; + + public COSObject cachedObject; + public PDFRawData decompressedStreamData; + + @Override + public String toString() { + String s, name = ""; + if (cachedObject != null) + name = cachedObject.getClass().getName(); + + if (isCompressed) { + s = String.format("(%d %d R)/%s @ [%d + %d]", id, gen, name, containerObjId, indexWithinContainer); + } else { + s = String.format("(%d %d R)/%s @ %d", id, gen, name, fileOffset); + }; + return s; + } + + public byte[] getTextRef() { + String s = String.format("%d %d R", id, gen); + return s.getBytes(); + } +} diff --git a/src/main/java/org/pdfparse/utils/ByteBuffer.java b/src/main/java/org/pdfparse/utils/ByteBuffer.java new file mode 100755 index 0000000..038ddd4 --- /dev/null +++ b/src/main/java/org/pdfparse/utils/ByteBuffer.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2013 Anton Golinko + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + */ + +package org.pdfparse.utils; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + + +/** + * Acts like a StringBuffer but works with byte arrays. + */ + +public class ByteBuffer extends OutputStream { + /** The count of bytes in the buffer. */ + protected int count; + + /** The buffer where the bytes are stored. */ + protected byte buf[]; + + /** Creates new ByteBuffer with capacity 128 */ + public ByteBuffer() { + this(128); + } + + /** + * Creates a byte buffer with a certain capacity. + * @param size the initial capacity + */ + public ByteBuffer(int size) { + if (size < 1) + size = 128; + buf = new byte[size]; + } + + + /** + * Appends an int. The size of the array will grow by one. + * @param b the int to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(int b) { + int newcount = count + 1; + if (newcount > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newcount)]; + System.arraycopy(buf, 0, newbuf, 0, count); + buf = newbuf; + } + buf[count] = (byte)b; + count = newcount; + return this; + } + + /** + * Appends the subarray of the byte array. The buffer will grow by + * len bytes. + * @param b the array to be appended + * @param off the offset to the start of the array + * @param len the length of bytes to append + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(byte b[], int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0) || len == 0) + return this; + int newcount = count + len; + if (newcount > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newcount)]; + System.arraycopy(buf, 0, newbuf, 0, count); + buf = newbuf; + } + System.arraycopy(b, off, buf, count, len); + count = newcount; + return this; + } + + /** + * Appends an array of bytes. + * @param b the array to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(byte b[]) { + return append(b, 0, b.length); + } + + /** + * Appends a String to the buffer. The String is + * converted platform's default charset. + * @param str the String to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(String str) { + if (str != null) + return append(str.getBytes()); + return this; + } + + /** + * Appends a char to the buffer. The char is + * converted according to the encoding ISO-8859-1. + * @param c the char to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(char c) { + return append((int)c); + } + + /** + * Appends another ByteBuffer to this buffer. + * @param buf the ByteBuffer to be appended + * @return a reference to this ByteBuffer object + */ + public ByteBuffer append(ByteBuffer buf) { + return append(buf.buf, 0, buf.count); + } + + /** + * Sets the size to zero. + */ + public void reset() { + count = 0; + } + + /** + * Clear memory + */ + public void clear() { + count = 0; + buf = new byte[128]; + } + + /** + * Creates a newly allocated byte array. Its size is the current + * size of this output stream and the valid contents of the buffer + * have been copied into it. + * + * @return the current contents of this output stream, as a byte array. + */ + public byte[] toByteArray() { + byte newbuf[] = new byte[count]; + System.arraycopy(buf, 0, newbuf, 0, count); + return newbuf; + } + + /** + * Returns the current size of the buffer. + * + * @return the value of the count field, which is the number of valid bytes in this byte buffer. + */ + public int size() { + return count; + } + + public void setSize(int size) { + if (size > count || size < 0) + throw new IndexOutOfBoundsException("The new size must be positive and less or equal of the current size"); + count = size; + } + + /** + * Converts the buffer's contents into a string, translating bytes into + * characters according to the platform's default character encoding. + * + * @return String translated from the buffer's contents. + */ + @Override + public String toString() { + return new String(buf, 0, count); + } + + /** + * Converts the buffer's contents into a string, translating bytes into + * characters according to the specified character encoding. + * + * @param enc a character-encoding name. + * @return String translated from the buffer's contents. + * @throws UnsupportedEncodingException + * If the named encoding is not supported. + */ + public String toString(String enc) throws UnsupportedEncodingException { + return new String(buf, 0, count, enc); + } + + /** + * Writes the complete contents of this byte buffer output to + * the specified output stream argument, as if by calling the output + * stream's write method using out.write(buf, 0, count). + * + * @param out the output stream to which to write the data. + * @exception IOException if an I/O error occurs. + */ + public void writeTo(OutputStream out) throws IOException { + out.write(buf, 0, count); + } + + @Override + public void write(int b) throws IOException { + append((byte)b); + } + + @Override + public void write(byte[] b, int off, int len) { + append(b, off, len); + } + + public byte[] getBuffer() { + return buf; + } + +} + diff --git a/src/main/java/org/pdfparse/utils/DateConverter.java b/src/main/java/org/pdfparse/utils/DateConverter.java new file mode 100755 index 0000000..1760848 --- /dev/null +++ b/src/main/java/org/pdfparse/utils/DateConverter.java @@ -0,0 +1,363 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Modified for pdfparse by Anton Golinko'2013 + +package org.pdfparse.utils; + +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.SimpleTimeZone; +import java.util.TimeZone; + +import org.pdfparse.cos.COSString; +import org.pdfparse.exception.*; + +/** + * This class is used to convert dates to strings and back using the PDF + * date standards. Date are described in PDFReference1.4 section 3.8.2 + * + * @author Ben Litchfield + * @version $Revision: 1.14 $ + */ + +// TODO: rewrite parsing method +public class DateConverter +{ + //The Date format is supposed to be the PDF_DATE_FORMAT, but not all PDF documents + //will use that date, so I have added a couple other potential formats + //to try if the original one does not work. + private static final SimpleDateFormat[] POTENTIAL_FORMATS = new SimpleDateFormat[] { + new SimpleDateFormat("EEEE, dd MMM yyyy hh:mm:ss a", Locale.ENGLISH), + new SimpleDateFormat("EEEE, MMM dd, yyyy hh:mm:ss a", Locale.ENGLISH), + new SimpleDateFormat("MM/dd/yyyy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("MM/dd/yyyy", Locale.ENGLISH), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH), + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz", Locale.ENGLISH), + new SimpleDateFormat("EEEE, MMM dd, yyyy", Locale.ENGLISH), // Acrobat Distiller 1.0.2 for Macintosh + new SimpleDateFormat("EEEE MMM dd, yyyy HH:mm:ss", Locale.ENGLISH), // ECMP5 + new SimpleDateFormat("EEEE MMM dd HH:mm:ss z yyyy", Locale.ENGLISH), // GNU Ghostscript 7.0.7 + new SimpleDateFormat("EEEE, MMM dd, yyyy 'at' hh:mma", Locale.ENGLISH), // Acrobat Net Distiller 1.0 for Windows + new SimpleDateFormat("d/MM/yyyy hh:mm:ss", Locale.ENGLISH), // PDFBOX-164 + new SimpleDateFormat("dd/MM/yyyy hh:mm:ss", Locale.ENGLISH), // PDFBOX-170 + new SimpleDateFormat("EEEEEEEEEE, MMMMMMMMMMMM dd, yyyy", Locale.ENGLISH), // PDFBOX-465 + new SimpleDateFormat("dd MMM yyyy hh:mm:ss", Locale.ENGLISH), // for 26 May 2000 11:25:00 + new SimpleDateFormat("dd MMM yyyy hh:mm", Locale.ENGLISH), // for 26 May 2000 11:25 + new SimpleDateFormat("M/dd/yyyy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("MM/d/yyyy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("M/dd/yyyy", Locale.ENGLISH), + new SimpleDateFormat("MM/d/yyyy", Locale.ENGLISH), + new SimpleDateFormat("M/d/yyyy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("M/d/yyyy", Locale.ENGLISH), + new SimpleDateFormat("M/d/yy hh:mm:ss", Locale.ENGLISH), + new SimpleDateFormat("M/d/yy", Locale.ENGLISH), + new SimpleDateFormat("yyyymmdd hh:mm:ss Z"), // + new SimpleDateFormat("yyyymmdd hh:mm:ss"), // + new SimpleDateFormat("yyyymmdd'+00''00'''"), // + new SimpleDateFormat("yyyymmdd'+01''00'''"), // + new SimpleDateFormat("yyyymmdd'+02''00'''"), // + new SimpleDateFormat("yyyymmdd'+03''00'''"), // + new SimpleDateFormat("yyyymmdd'+04''00'''"), // + new SimpleDateFormat("yyyymmdd'+05''00'''"), // + new SimpleDateFormat("yyyymmdd'+06''00'''"), // + new SimpleDateFormat("yyyymmdd'+07''00'''"), // + new SimpleDateFormat("yyyymmdd'+08''00'''"), // + new SimpleDateFormat("yyyymmdd'+09''00'''"), // + new SimpleDateFormat("yyyymmdd'+10''00'''"), // + new SimpleDateFormat("yyyymmdd'+11''00'''"), // + new SimpleDateFormat("yyyymmdd'+12''00'''"), // + new SimpleDateFormat("yyyymmdd'-01''00'''"), // + new SimpleDateFormat("yyyymmdd'-02''00'''"), // + new SimpleDateFormat("yyyymmdd'-03''00'''"), // + new SimpleDateFormat("yyyymmdd'-04''00'''"), // + new SimpleDateFormat("yyyymmdd'-05''00'''"), // + new SimpleDateFormat("yyyymmdd'-06''00'''"), // + new SimpleDateFormat("yyyymmdd'-07''00'''"), // + new SimpleDateFormat("yyyymmdd'-08''00'''"), // + new SimpleDateFormat("yyyymmdd'-09''00'''"), // + new SimpleDateFormat("yyyymmdd'-10''00'''"), // + new SimpleDateFormat("yyyymmdd'-11''00'''"), // + new SimpleDateFormat("yyyymmdd'-12''00'''"), // + new SimpleDateFormat("yyyymmdd"), // for 20090401+0200 + }; + + private DateConverter() + { + //utility class should not be constructed. + } + + /** + * This will convert the calendar to a string. + * + * @param date The date to convert to a string. + * + * @return The date as a String to be used in a PDF document. + */ + public static String toString( Calendar date ) + { + String retval = null; + if( date != null ) + { + StringBuffer buffer = new StringBuffer(); + TimeZone zone = date.getTimeZone(); + long offsetInMinutes = zone.getOffset( date.getTimeInMillis() )/1000/60; + long hours = Math.abs( offsetInMinutes/60 ); + long minutes = Math.abs( offsetInMinutes%60 ); + buffer.append( "D:" ); + // PDFBOX-402 , SimpleDateFormat is not thread safe, created it when you use it. + buffer.append( new SimpleDateFormat( "yyyyMMddHHmmss" , Locale.ENGLISH).format( date.getTime() ) ); + if( offsetInMinutes == 0 ) + { + buffer.append( "Z" ); + } + else if( offsetInMinutes < 0 ) + { + buffer.append( "-" ); + } + else + { + buffer.append( "+" ); + } + if( hours < 10 ) + { + buffer.append( "0" ); + } + buffer.append( hours ); + buffer.append( "'" ); + if( minutes < 10 ) + { + buffer.append( "0" ); + } + buffer.append( minutes ); + buffer.append( "'" ); + retval = buffer.toString(); + + } + return retval; + } + + /** + * This will convert a string to a calendar. + * + * @param date The string representation of the calendar. + * + * @return The calendar that this string represents. + * + * @throws EDateConvertError If the date string is not in the correct format. + */ + public static Calendar toCalendar( COSString date ) throws EDateConvertError + { + Calendar retval = null; + if( date != null ) + { + retval = toCalendar( date.getValue() ); + } + + return retval; + } + + /** + * This will convert a string to a calendar. + * + * @param date The string representation of the calendar. + * + * @return The calendar that this string represents. + * + * @throws EDateConvertError If the date string is not in the correct format. + */ + public static Calendar toCalendar( String date ) throws EDateConvertError + { + Calendar retval = null; + if( date != null && date.trim().length() > 0 ) + { + //these are the default values + int year = 0; + int month = 1; + int day = 1; + int hour = 0; + int minute = 0; + int second = 0; + //first string off the prefix if it exists + try + { + SimpleTimeZone zone = null; + if( date.startsWith( "D:" ) ) + { + date = date.substring( 2, date.length() ); + } + if( date.length() < 4 ) + { + throw new EDateConvertError( "Error: Invalid date format '" + date + "'" ); + } + year = Integer.parseInt( date.substring( 0, 4 ) ); + if( date.length() >= 6 ) + { + month = Integer.parseInt( date.substring( 4, 6 ) ); + } + if( date.length() >= 8 ) + { + day = Integer.parseInt( date.substring( 6, 8 ) ); + } + if( date.length() >= 10 ) + { + hour = Integer.parseInt( date.substring( 8, 10 ) ); + } + if( date.length() >= 12 ) + { + minute = Integer.parseInt( date.substring( 10, 12 ) ); + } + if( date.length() >= 14 ) + { + second = Integer.parseInt( date.substring( 12, 14 ) ); + } + + if( date.length() >= 15 ) + { + char sign = date.charAt( 14 ); + if( sign == 'Z' ) + { + zone = new SimpleTimeZone(0,"Unknown"); + } + else + { + int hours = 0; + int minutes = 0; + if( date.length() >= 17 ) + { + if( sign == '+' ) + { + //parseInt cannot handle the + sign + hours = Integer.parseInt( date.substring( 15, 17 ) ); + } + else if (sign == '-') + { + hours = -Integer.parseInt(date.substring(15,17)); + } + else + { + hours = -Integer.parseInt( date.substring( 14, 16 ) ); + } + } + if( date.length() > 20 ) + { + minutes = Integer.parseInt( date.substring( 18, 20 ) ); + } + zone = new SimpleTimeZone( hours*60*60*1000 + minutes*60*1000, "Unknown" ); + } + } + if( zone != null ) + { + retval = new GregorianCalendar( zone ); + } + else + { + retval = new GregorianCalendar(); + } + + retval.set(year, month-1, day, hour, minute, second ); + // PDF dates are only accurate up to a second + retval.set(Calendar.MILLISECOND, 0); + } + catch( NumberFormatException e ) + { + for( int i=0; retval == null && iA hash map that uses primitive ints for the key rather than objects.

+ * + *

Note that this class is for internal optimization purposes only, and may + * not be supported in future releases of Jakarta Commons Lang. Utilities of + * this sort may be included in future releases of Jakarta Commons Collections.

+ * + * @author Justin Couch + * @author Alex Chaffee (alex@apache.org) + * @author Stephen Colebourne + * @author Bruno Lowagie (change Objects as keys into int values) + * @author Paulo Soares (added extra methods) + */ +public class IntIntHashtable implements Cloneable { + + /*** + * The hash table data. + */ + private transient Entry table[]; + + /*** + * The total number of entries in the hash table. + */ + private transient int count; + + /*** + * The table is rehashed when its size exceeds this threshold. (The + * value of this field is (int)(capacity * loadFactor).) + * + * @serial + */ + private int threshold; + + /*** + * The load factor for the hashtable. + * + * @serial + */ + private float loadFactor; + + /*** + *

Constructs a new, empty hashtable with a default capacity and load + * factor, which is 20 and 0.75 respectively.

+ */ + public IntIntHashtable() { + this(150, 0.75f); + } + + /*** + *

Constructs a new, empty hashtable with the specified initial capacity + * and default load factor, which is 0.75.

+ * + * @param initialCapacity the initial capacity of the hashtable. + * @throws IllegalArgumentException if the initial capacity is less + * than zero. + */ + public IntIntHashtable(int initialCapacity) { + this(initialCapacity, 0.75f); + } + + /*** + *

Constructs a new, empty hashtable with the specified initial + * capacity and the specified load factor.

+ * + * @param initialCapacity the initial capacity of the hashtable. + * @param loadFactor the load factor of the hashtable. + * @throws IllegalArgumentException if the initial capacity is less + * than zero, or if the load factor is nonpositive. + */ + public IntIntHashtable(int initialCapacity, float loadFactor) { + super(); + if (initialCapacity < 0) { + throw new IllegalArgumentException(String.format("Illegal capacity %d", initialCapacity)); + } + if (loadFactor <= 0) { + throw new IllegalArgumentException(String.format("Illegal load %s", String.valueOf(loadFactor))); + } + if (initialCapacity == 0) { + initialCapacity = 1; + } + this.loadFactor = loadFactor; + table = new Entry[initialCapacity]; + threshold = (int) (initialCapacity * loadFactor); + } + + /*** + *

Returns the number of keys in this hashtable.

+ * + * @return the number of keys in this hashtable. + */ + public int size() { + return count; + } + + /*** + *

Tests if this hashtable maps no keys to values.

+ * + * @return true if this hashtable maps no keys to values; + * false otherwise. + */ + public boolean isEmpty() { + return count == 0; + } + + /*** + *

Tests if some key maps into the specified value in this hashtable. + * This operation is more expensive than the containsKey + * method.

+ * + *

Note that this method is identical in functionality to containsValue, + * (which is part of the Map interface in the collections framework).

+ * + * @param value a value to search for. + * @return true if and only if some key maps to the + * value argument in this hashtable as + * determined by the equals method; + * false otherwise. + * @throws NullPointerException if the value is null. + * @see #containsKey(int) + * @see #containsValue(int) + * @see java.util.Map + */ + public boolean contains(int value) { + + Entry tab[] = table; + for (int i = tab.length; i-- > 0;) { + for (Entry e = tab[i]; e != null; e = e.next) { + if (e.value == value) { + return true; + } + } + } + return false; + } + + /*** + *

Returns true if this HashMap maps one or more keys + * to this value.

+ * + *

Note that this method is identical in functionality to contains + * (which predates the Map interface).

+ * + * @param value value whose presence in this HashMap is to be tested. + * @return boolean true if the value is contained + * @see java.util.Map + * @since JDK1.2 + */ + public boolean containsValue(int value) { + return contains(value); + } + + /*** + *

Tests if the specified int is a key in this hashtable.

+ * + * @param key possible key. + * @return true if and only if the specified int is a + * key in this hashtable, as determined by the equals + * method; false otherwise. + * @see #contains(int) + */ + public boolean containsKey(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + return true; + } + } + return false; + } + + /*** + *

Returns the value to which the specified key is mapped in this map.

+ * + * @param key a key in the hashtable. + * @return the value to which the key is mapped in this hashtable; + * null if the key is not mapped to any value in + * this hashtable. + * @see #put(int, int) + */ + public int get(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + return e.value; + } + } + return 0; + } + + /*** + *

Increases the capacity of and internally reorganizes this + * hashtable, in order to accommodate and access its entries more + * efficiently.

+ * + *

This method is called automatically when the number of keys + * in the hashtable exceeds this hashtable's capacity and load + * factor.

+ */ + protected void rehash() { + int oldCapacity = table.length; + Entry oldMap[] = table; + + int newCapacity = oldCapacity * 2 + 1; + Entry newMap[] = new Entry[newCapacity]; + + threshold = (int) (newCapacity * loadFactor); + table = newMap; + + for (int i = oldCapacity; i-- > 0;) { + for (Entry old = oldMap[i]; old != null;) { + Entry e = old; + old = old.next; + + int index = (e.hash & 0x7FFFFFFF) % newCapacity; + e.next = newMap[index]; + newMap[index] = e; + } + } + } + + /*** + *

Maps the specified key to the specified + * value in this hashtable. The key cannot be + * null.

+ * + *

The value can be retrieved by calling the get method + * with a key that is equal to the original key.

+ * + * @param key the hashtable key. + * @param value the value. + * @return the previous value of the specified key in this hashtable, + * or null if it did not have one. + * @throws NullPointerException if the key is null. + * @see #get(int) + */ + public int put(int key, int value) { + // Makes sure the key is not already in the hashtable. + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + int old = e.value; + e.value = value; + return old; + } + } + + if (count >= threshold) { + // Rehash the table if the threshold is exceeded + rehash(); + + tab = table; + index = (hash & 0x7FFFFFFF) % tab.length; + } + + // Creates the new entry. + Entry e = new Entry(hash, key, value, tab[index]); + tab[index] = e; + count++; + return 0; + } + + /*** + *

Removes the key (and its corresponding value) from this + * hashtable.

+ * + *

This method does nothing if the key is not present in the + * hashtable.

+ * + * @param key the key that needs to be removed. + * @return the value to which the key had been mapped in this hashtable, + * or null if the key did not have a mapping. + */ + public int remove(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index], prev = null; e != null; prev = e, e = e.next) { + if (e.hash == hash && e.key == key) { + if (prev != null) { + prev.next = e.next; + } else { + tab[index] = e.next; + } + count--; + int oldValue = e.value; + e.value = 0; + return oldValue; + } + } + return 0; + } + + /*** + *

Clears this hashtable so that it contains no keys.

+ */ + public void clear() { + Entry tab[] = table; + for (int index = tab.length; --index >= 0;) { + tab[index] = null; + } + count = 0; + } + + /*** + *

Innerclass that acts as a datastructure to create a new entry in the + * table.

+ */ + static class Entry { + int hash; + int key; + int value; + Entry next; + + /*** + *

Create a new entry with the given values.

+ * + * @param hash The code used to hash the int with + * @param key The key used to enter this in the table + * @param value The value for this key + * @param next A reference to the next entry in the table + */ + protected Entry(int hash, int key, int value, Entry next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + // extra methods for inner class Entry by Paulo + public int getKey() { + return key; + } + public int getValue() { + return value; + } + @Override + protected Object clone() { + Entry entry = new Entry(hash, key, value, next != null ? (Entry)next.clone() : null); + return entry; + } + } + + // extra inner class by Paulo + static class IntHashtableIterator implements Iterator { + int index; + Entry table[]; + Entry entry; + + IntHashtableIterator(Entry table[]) { + this.table = table; + this.index = table.length; + } + public boolean hasNext() { + if (entry != null) { + return true; + } + while (index-- > 0) { + if ((entry = table[index]) != null) { + return true; + } + } + return false; + } + + @Override + public Entry next() { + if (entry == null) { + while (index-- > 0 && (entry = table[index]) == null); + } + if (entry != null) { + Entry e = entry; + entry = e.next; + return e; + } + throw new NoSuchElementException("inthashtableiterator"); + } + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + } + +// extra methods by Paulo Soares: + + public Iterator getEntryIterator() { + return new IntHashtableIterator(table); + } + + public int[] toOrderedKeys() { + int res[] = getKeys(); + Arrays.sort(res); + return res; + } + + public int[] getKeys() { + int res[] = new int[count]; + int ptr = 0; + int index = table.length; + Entry entry = null; + while (true) { + if (entry == null) + while (index-- > 0 && (entry = table[index]) == null); + if (entry == null) + break; + Entry e = entry; + entry = e.next; + res[ptr++] = e.key; + } + return res; + } + + public int getOneKey() { + if (count == 0) + return 0; + int index = table.length; + Entry entry = null; + while (index-- > 0 && (entry = table[index]) == null); + if (entry == null) + return 0; + return entry.key; + } + + @Override + public Object clone() { + try { + IntIntHashtable t = (IntIntHashtable)super.clone(); + t.table = new Entry[table.length]; + for (int i = table.length ; i-- > 0 ; ) { + t.table[i] = table[i] != null + ? (Entry)table[i].clone() : null; + } + return t; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(); + } + } +} diff --git a/src/main/java/org/pdfparse/utils/IntObjHashtable.java b/src/main/java/org/pdfparse/utils/IntObjHashtable.java new file mode 100755 index 0000000..c1e4a89 --- /dev/null +++ b/src/main/java/org/pdfparse/utils/IntObjHashtable.java @@ -0,0 +1,480 @@ +/* + * This class is based on org.apache.IntHashMap.commons.lang + * http://jakarta.apache.org/commons/lang/xref/org/apache/commons/lang/IntHashMap.html + * It was adapted by Bruno Lowagie for use in iText, + * reusing methods that were written by Paulo Soares. + * Instead of being a hashtable that stores objects with an int as key, + * it stores int values with an int as key. + * + * This is the original license of the original class IntHashMap: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Note: originally released under the GNU LGPL v2.1, + * but rereleased by the original author under the ASF license (above). + */ + +package org.pdfparse.utils; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.NoSuchElementException; + + +/*** + *

A hash map that uses primitive ints for the key rather than objects.

+ * + *

Note that this class is for internal optimization purposes only, and may + * not be supported in future releases of Jakarta Commons Lang. Utilities of + * this sort may be included in future releases of Jakarta Commons Collections.

+ * + * @author Justin Couch + * @author Alex Chaffee (alex@apache.org) + * @author Stephen Colebourne + * @author Bruno Lowagie (change Objects as keys into int values) + * @author Paulo Soares (added extra methods) + */ +public class IntObjHashtable implements Cloneable { + + /*** + * The hash table data. + */ + private transient Entry table[]; + + /*** + * The total number of entries in the hash table. + */ + private transient int count; + + /*** + * The table is rehashed when its size exceeds this threshold. (The + * value of this field is (int)(capacity * loadFactor).) + * + * @serial + */ + private int threshold; + + /*** + * The load factor for the hashtable. + * + * @serial + */ + private float loadFactor; + + /*** + *

Constructs a new, empty hashtable with a default capacity and load + * factor, which is 20 and 0.75 respectively.

+ */ + public IntObjHashtable() { + this(150, 0.75f); + } + + /*** + *

Constructs a new, empty hashtable with the specified initial capacity + * and default load factor, which is 0.75.

+ * + * @param initialCapacity the initial capacity of the hashtable. + * @throws IllegalArgumentException if the initial capacity is less + * than zero. + */ + public IntObjHashtable(int initialCapacity) { + this(initialCapacity, 0.75f); + } + + /*** + *

Constructs a new, empty hashtable with the specified initial + * capacity and the specified load factor.

+ * + * @param initialCapacity the initial capacity of the hashtable. + * @param loadFactor the load factor of the hashtable. + * @throws IllegalArgumentException if the initial capacity is less + * than zero, or if the load factor is nonpositive. + */ + public IntObjHashtable(int initialCapacity, float loadFactor) { + super(); + if (initialCapacity < 0) { + throw new IllegalArgumentException(String.format("Illegal capacity %d", initialCapacity)); + } + if (loadFactor <= 0) { + throw new IllegalArgumentException(String.format("Illegal load %s", String.valueOf(loadFactor))); + } + if (initialCapacity == 0) { + initialCapacity = 1; + } + this.loadFactor = loadFactor; + table = new Entry[initialCapacity]; + threshold = (int) (initialCapacity * loadFactor); + } + + /*** + *

Returns the number of keys in this hashtable.

+ * + * @return the number of keys in this hashtable. + */ + public int size() { + return count; + } + + /*** + *

Tests if this hashtable maps no keys to values.

+ * + * @return true if this hashtable maps no keys to values; + * false otherwise. + */ + public boolean isEmpty() { + return count == 0; + } + + /*** + *

Tests if some key maps into the specified value in this hashtable. + * This operation is more expensive than the containsKey + * method.

+ * + *

Note that this method is identical in functionality to containsValue, + * (which is part of the Map interface in the collections framework).

+ * + * @param value a value to search for. + * @return true if and only if some key maps to the + * value argument in this hashtable as + * determined by the equals method; + * false otherwise. + * @throws NullPointerException if the value is null. + * @see #containsKey(int) + * @see #containsValue(V) + * @see java.util.Map + */ + public boolean contains(V value) { + + Entry tab[] = table; + for (int i = tab.length; i-- > 0;) { + for (Entry e = tab[i]; e != null; e = e.next) { + if (e.value == value) { + return true; + } + } + } + return false; + } + + /*** + *

Returns true if this HashMap maps one or more keys + * to this value.

+ * + *

Note that this method is identical in functionality to contains + * (which predates the Map interface).

+ * + * @param value value whose presence in this HashMap is to be tested. + * @return boolean true if the value is contained + * @see java.util.Map + * @since JDK1.2 + */ + public boolean containsValue(V value) { + return contains(value); + } + + /*** + *

Tests if the specified int is a key in this hashtable.

+ * + * @param key possible key. + * @return true if and only if the specified int is a + * key in this hashtable, as determined by the equals + * method; false otherwise. + * @see #contains(V) + */ + public boolean containsKey(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + return true; + } + } + return false; + } + + /*** + *

Returns the value to which the specified key is mapped in this map.

+ * + * @param key a key in the hashtable. + * @return the value to which the key is mapped in this hashtable; + * null if the key is not mapped to any value in + * this hashtable. + * @see #put(int, V) + */ + public V get(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + return e.value; + } + } + return null; + } + + /*** + *

Increases the capacity of and internally reorganizes this + * hashtable, in order to accommodate and access its entries more + * efficiently.

+ * + *

This method is called automatically when the number of keys + * in the hashtable exceeds this hashtable's capacity and load + * factor.

+ */ + protected void rehash() { + int oldCapacity = table.length; + Entry oldMap[] = table; + + int newCapacity = oldCapacity * 2 + 1; + Entry newMap[] = new Entry[newCapacity]; + + threshold = (int) (newCapacity * loadFactor); + table = newMap; + + for (int i = oldCapacity; i-- > 0;) { + for (Entry old = oldMap[i]; old != null;) { + Entry e = old; + old = old.next; + + int index = (e.hash & 0x7FFFFFFF) % newCapacity; + e.next = newMap[index]; + newMap[index] = e; + } + } + } + + /*** + *

Maps the specified key to the specified + * value in this hashtable. The key cannot be + * null.

+ * + *

The value can be retrieved by calling the get method + * with a key that is equal to the original key.

+ * + * @param key the hashtable key. + * @param value the value. + * @return the previous value of the specified key in this hashtable, + * or null if it did not have one. + * @throws NullPointerException if the key is null. + * @see #get(int) + */ + public V put(int key, V value) { + // Makes sure the key is not already in the hashtable. + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index]; e != null; e = e.next) { + if (e.hash == hash && e.key == key) { + V old = e.value; + e.value = value; + return old; + } + } + + if (count >= threshold) { + // Rehash the table if the threshold is exceeded + rehash(); + + tab = table; + index = (hash & 0x7FFFFFFF) % tab.length; + } + + // Creates the new entry. + Entry e = new Entry(hash, key, value, tab[index]); + tab[index] = e; + count++; + return null; + } + + /*** + *

Removes the key (and its corresponding value) from this + * hashtable.

+ * + *

This method does nothing if the key is not present in the + * hashtable.

+ * + * @param key the key that needs to be removed. + * @return the value to which the key had been mapped in this hashtable, + * or null if the key did not have a mapping. + */ + public V remove(int key) { + Entry tab[] = table; + int hash = key; + int index = (hash & 0x7FFFFFFF) % tab.length; + for (Entry e = tab[index], prev = null; e != null; prev = e, e = e.next) { + if (e.hash == hash && e.key == key) { + if (prev != null) { + prev.next = e.next; + } else { + tab[index] = e.next; + } + count--; + V oldValue = e.value; + e.value = null; + return oldValue; + } + } + return null; + } + + /*** + *

Clears this hashtable so that it contains no keys.

+ */ + public void clear() { + Entry tab[] = table; + for (int index = tab.length; --index >= 0;) { + tab[index] = null; + } + count = 0; + } + + /*** + *

Innerclass that acts as a datastructure to create a new entry in the + * table.

+ */ + static class Entry { + int hash; + int key; + V value; + Entry next; + + /*** + *

Create a new entry with the given values.

+ * + * @param hash The code used to hash the int with + * @param key The key used to enter this in the table + * @param value The value for this key + * @param next A reference to the next entry in the table + */ + protected Entry(int hash, int key, V value, Entry next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + // extra methods for inner class Entry by Paulo + public int getKey() { + return key; + } + public V getValue() { + return value; + } + @Override + protected Object clone() { + Entry entry = new Entry(hash, key, value, next != null ? (Entry)next.clone() : null); + return entry; + } + } + + // extra inner class by Paulo + static class IntHashtableIterator implements Iterator> { + int index; + Entry table[]; + Entry entry; + + IntHashtableIterator(Entry table[]) { + this.table = table; + this.index = table.length; + } + public boolean hasNext() { + if (entry != null) { + return true; + } + while (index-- > 0) { + if ((entry = table[index]) != null) { + return true; + } + } + return false; + } + + @Override + public Entry next() { + if (entry == null) { + while (index-- > 0 && (entry = table[index]) == null); + } + if (entry != null) { + Entry e = entry; + entry = e.next; + return e; + } + throw new NoSuchElementException("inthashtableiterator"); + } + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + } + +// extra methods by Paulo Soares: + + public Iterator> getEntryIterator() { + return new IntHashtableIterator(table); + } + + public int[] toOrderedKeys() { + int res[] = getKeys(); + Arrays.sort(res); + return res; + } + + public int[] getKeys() { + int res[] = new int[count]; + int ptr = 0; + int index = table.length; + Entry entry = null; + while (true) { + if (entry == null) + while (index-- > 0 && (entry = table[index]) == null); + if (entry == null) + break; + Entry e = entry; + entry = e.next; + res[ptr++] = e.key; + } + return res; + } + + public int getOneKey() { + if (count == 0) + return 0; + int index = table.length; + Entry entry = null; + while (index-- > 0 && (entry = table[index]) == null); + if (entry == null) + return 0; + return entry.key; + } + + @Override + public Object clone() { + try { + IntObjHashtable t = (IntObjHashtable)super.clone(); + t.table = new Entry[table.length]; + for (int i = table.length ; i-- > 0 ; ) { + t.table[i] = table[i] != null + ? (Entry)table[i].clone() : null; + } + return t; + } catch (CloneNotSupportedException e) { + // this shouldn't happen, since we are Cloneable + throw new InternalError(); + } + } +} diff --git a/src/main/res/drawable-hdpi/ic_action_content_new.png b/src/main/res/drawable-hdpi/ic_action_content_new.png new file mode 100755 index 0000000..f002f19 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_action_content_new.png differ diff --git a/src/main/res/drawable-hdpi/ic_browse.png b/src/main/res/drawable-hdpi/ic_browse.png new file mode 100755 index 0000000..dd71ad5 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_browse.png differ diff --git a/src/main/res/drawable-hdpi/ic_collection_black.png b/src/main/res/drawable-hdpi/ic_collection_black.png new file mode 100755 index 0000000..70b9f50 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_collection_black.png differ diff --git a/src/main/res/drawable-hdpi/ic_collection_blue.png b/src/main/res/drawable-hdpi/ic_collection_blue.png new file mode 100755 index 0000000..1d8be26 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_collection_blue.png differ diff --git a/src/main/res/drawable-hdpi/ic_delete.png b/src/main/res/drawable-hdpi/ic_delete.png new file mode 100755 index 0000000..63ca42e Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_delete.png differ diff --git a/src/main/res/drawable-hdpi/ic_down.png b/src/main/res/drawable-hdpi/ic_down.png new file mode 100755 index 0000000..490d93a Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_down.png differ diff --git a/src/main/res/drawable-hdpi/ic_edit.png b/src/main/res/drawable-hdpi/ic_edit.png new file mode 100755 index 0000000..c8306b2 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_edit.png differ diff --git a/src/main/res/drawable-hdpi/ic_favorites_black.png b/src/main/res/drawable-hdpi/ic_favorites_black.png new file mode 100755 index 0000000..0951156 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_favorites_black.png differ diff --git a/src/main/res/drawable-hdpi/ic_favorites_blue.png b/src/main/res/drawable-hdpi/ic_favorites_blue.png new file mode 100755 index 0000000..92affe2 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_favorites_blue.png differ diff --git a/src/main/res/drawable-hdpi/ic_isbn.png b/src/main/res/drawable-hdpi/ic_isbn.png new file mode 100755 index 0000000..88ca7a1 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_isbn.png differ diff --git a/src/main/res/drawable-hdpi/ic_item_black.png b/src/main/res/drawable-hdpi/ic_item_black.png new file mode 100755 index 0000000..62c07ee Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_item_black.png differ diff --git a/src/main/res/drawable-hdpi/ic_item_blue.png b/src/main/res/drawable-hdpi/ic_item_blue.png new file mode 100755 index 0000000..6c85760 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_item_blue.png differ diff --git a/src/main/res/drawable-hdpi/ic_launcher.png b/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100755 index 0000000..32f2fbd Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-hdpi/ic_manual.png b/src/main/res/drawable-hdpi/ic_manual.png new file mode 100755 index 0000000..aad5c0a Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_manual.png differ diff --git a/src/main/res/drawable-hdpi/ic_plus.png b/src/main/res/drawable-hdpi/ic_plus.png new file mode 100755 index 0000000..82c4d36 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_plus.png differ diff --git a/src/main/res/drawable-hdpi/ic_scan.png b/src/main/res/drawable-hdpi/ic_scan.png new file mode 100755 index 0000000..5d3ccbb Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_scan.png differ diff --git a/src/main/res/drawable-hdpi/ic_settings.png b/src/main/res/drawable-hdpi/ic_settings.png new file mode 100755 index 0000000..4232905 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_settings.png differ diff --git a/src/main/res/drawable-hdpi/ic_star_empty.png b/src/main/res/drawable-hdpi/ic_star_empty.png new file mode 100755 index 0000000..1d1c2be Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_star_empty.png differ diff --git a/src/main/res/drawable-hdpi/ic_star_filled.png b/src/main/res/drawable-hdpi/ic_star_filled.png new file mode 100755 index 0000000..82a400b Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_star_filled.png differ diff --git a/src/main/res/drawable-hdpi/ic_tag_black.png b/src/main/res/drawable-hdpi/ic_tag_black.png new file mode 100755 index 0000000..5d300e7 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_tag_black.png differ diff --git a/src/main/res/drawable-hdpi/ic_tag_blue.png b/src/main/res/drawable-hdpi/ic_tag_blue.png new file mode 100755 index 0000000..c51c0d0 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_tag_blue.png differ diff --git a/src/main/res/drawable-hdpi/ic_upload.png b/src/main/res/drawable-hdpi/ic_upload.png new file mode 100755 index 0000000..5e5023a Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_upload.png differ diff --git a/src/main/res/drawable-hdpi/icon.png b/src/main/res/drawable-hdpi/icon.png deleted file mode 100644 index 8074c4c..0000000 Binary files a/src/main/res/drawable-hdpi/icon.png and /dev/null differ diff --git a/src/main/res/drawable-ldpi/icon.png b/src/main/res/drawable-ldpi/icon.png deleted file mode 100644 index 1095584..0000000 Binary files a/src/main/res/drawable-ldpi/icon.png and /dev/null differ diff --git a/src/main/res/drawable-mdpi/ic_browse.png b/src/main/res/drawable-mdpi/ic_browse.png new file mode 100755 index 0000000..9675403 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_browse.png differ diff --git a/src/main/res/drawable-mdpi/ic_collection_black.png b/src/main/res/drawable-mdpi/ic_collection_black.png new file mode 100755 index 0000000..32efcd7 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_collection_black.png differ diff --git a/src/main/res/drawable-mdpi/ic_collection_blue.png b/src/main/res/drawable-mdpi/ic_collection_blue.png new file mode 100755 index 0000000..005feca Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_collection_blue.png differ diff --git a/src/main/res/drawable-mdpi/ic_delete.png b/src/main/res/drawable-mdpi/ic_delete.png new file mode 100755 index 0000000..76ad113 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_delete.png differ diff --git a/src/main/res/drawable-mdpi/ic_down.png b/src/main/res/drawable-mdpi/ic_down.png new file mode 100755 index 0000000..aab7a16 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_down.png differ diff --git a/src/main/res/drawable-mdpi/ic_edit.png b/src/main/res/drawable-mdpi/ic_edit.png new file mode 100755 index 0000000..b65b2d2 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_edit.png differ diff --git a/src/main/res/drawable-mdpi/ic_favorites_black.png b/src/main/res/drawable-mdpi/ic_favorites_black.png new file mode 100755 index 0000000..98485d7 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_favorites_black.png differ diff --git a/src/main/res/drawable-mdpi/ic_favorites_blue.png b/src/main/res/drawable-mdpi/ic_favorites_blue.png new file mode 100755 index 0000000..2725298 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_favorites_blue.png differ diff --git a/src/main/res/drawable-mdpi/ic_item_black.png b/src/main/res/drawable-mdpi/ic_item_black.png new file mode 100755 index 0000000..43b240a Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_item_black.png differ diff --git a/src/main/res/drawable-mdpi/ic_item_blue.png b/src/main/res/drawable-mdpi/ic_item_blue.png new file mode 100755 index 0000000..94b9d21 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_item_blue.png differ diff --git a/src/main/res/drawable-mdpi/ic_launcher.png b/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100755 index 0000000..59ad414 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-mdpi/ic_plus.png b/src/main/res/drawable-mdpi/ic_plus.png new file mode 100755 index 0000000..1082664 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_plus.png differ diff --git a/src/main/res/drawable-mdpi/ic_scan.png b/src/main/res/drawable-mdpi/ic_scan.png new file mode 100755 index 0000000..4c9efa2 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_scan.png differ diff --git a/src/main/res/drawable-mdpi/ic_settings.png b/src/main/res/drawable-mdpi/ic_settings.png new file mode 100755 index 0000000..bf74b60 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_settings.png differ diff --git a/src/main/res/drawable-mdpi/ic_star_empty.png b/src/main/res/drawable-mdpi/ic_star_empty.png new file mode 100755 index 0000000..76aa95c Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_star_empty.png differ diff --git a/src/main/res/drawable-mdpi/ic_star_filled.png b/src/main/res/drawable-mdpi/ic_star_filled.png new file mode 100755 index 0000000..7b6e092 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_star_filled.png differ diff --git a/src/main/res/drawable-mdpi/ic_tag_black.png b/src/main/res/drawable-mdpi/ic_tag_black.png new file mode 100755 index 0000000..c8e8a23 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_tag_black.png differ diff --git a/src/main/res/drawable-mdpi/ic_tag_blue.png b/src/main/res/drawable-mdpi/ic_tag_blue.png new file mode 100755 index 0000000..1db6060 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_tag_blue.png differ diff --git a/src/main/res/drawable-mdpi/ic_upload.png b/src/main/res/drawable-mdpi/ic_upload.png new file mode 100755 index 0000000..4eeb9ba Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_upload.png differ diff --git a/src/main/res/drawable-mdpi/icon.png b/src/main/res/drawable-mdpi/icon.png deleted file mode 100644 index a07c69f..0000000 Binary files a/src/main/res/drawable-mdpi/icon.png and /dev/null differ diff --git a/src/main/res/drawable-xhdpi/ic_action_content_new.png b/src/main/res/drawable-xhdpi/ic_action_content_new.png new file mode 100755 index 0000000..e3ab979 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_action_content_new.png differ diff --git a/src/main/res/drawable-xhdpi/ic_browse.png b/src/main/res/drawable-xhdpi/ic_browse.png new file mode 100755 index 0000000..87f08c3 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_browse.png differ diff --git a/src/main/res/drawable-xhdpi/ic_collection_black.png b/src/main/res/drawable-xhdpi/ic_collection_black.png new file mode 100755 index 0000000..72c8818 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_collection_black.png differ diff --git a/src/main/res/drawable-xhdpi/ic_collection_blue.png b/src/main/res/drawable-xhdpi/ic_collection_blue.png new file mode 100755 index 0000000..4e47232 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_collection_blue.png differ diff --git a/src/main/res/drawable-xhdpi/ic_delete.png b/src/main/res/drawable-xhdpi/ic_delete.png new file mode 100755 index 0000000..8536661 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_delete.png differ diff --git a/src/main/res/drawable-xhdpi/ic_down.png b/src/main/res/drawable-xhdpi/ic_down.png new file mode 100755 index 0000000..f14f8df Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_down.png differ diff --git a/src/main/res/drawable-xhdpi/ic_edit.png b/src/main/res/drawable-xhdpi/ic_edit.png new file mode 100755 index 0000000..5ceeabf Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_edit.png differ diff --git a/src/main/res/drawable-xhdpi/ic_favorites_black.png b/src/main/res/drawable-xhdpi/ic_favorites_black.png new file mode 100755 index 0000000..229ab0b Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_favorites_black.png differ diff --git a/src/main/res/drawable-xhdpi/ic_favorites_blue.png b/src/main/res/drawable-xhdpi/ic_favorites_blue.png new file mode 100755 index 0000000..003211a Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_favorites_blue.png differ diff --git a/src/main/res/drawable-xhdpi/ic_isbn.png b/src/main/res/drawable-xhdpi/ic_isbn.png new file mode 100755 index 0000000..1a85694 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_isbn.png differ diff --git a/src/main/res/drawable-xhdpi/ic_item_black.png b/src/main/res/drawable-xhdpi/ic_item_black.png new file mode 100755 index 0000000..35ea6c8 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_item_black.png differ diff --git a/src/main/res/drawable-xhdpi/ic_item_blue.png b/src/main/res/drawable-xhdpi/ic_item_blue.png new file mode 100755 index 0000000..437418c Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_item_blue.png differ diff --git a/src/main/res/drawable-xhdpi/ic_launcher.png b/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100755 index 0000000..ad62055 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-xhdpi/ic_manual.png b/src/main/res/drawable-xhdpi/ic_manual.png new file mode 100755 index 0000000..0c72f32 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_manual.png differ diff --git a/src/main/res/drawable-xhdpi/ic_plus.png b/src/main/res/drawable-xhdpi/ic_plus.png new file mode 100755 index 0000000..8e358f9 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_plus.png differ diff --git a/src/main/res/drawable-xhdpi/ic_settings.png b/src/main/res/drawable-xhdpi/ic_settings.png new file mode 100755 index 0000000..096aed8 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_settings.png differ diff --git a/src/main/res/drawable-xhdpi/ic_star_empty.png b/src/main/res/drawable-xhdpi/ic_star_empty.png new file mode 100755 index 0000000..90e58e6 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_star_empty.png differ diff --git a/src/main/res/drawable-xhdpi/ic_star_filled.png b/src/main/res/drawable-xhdpi/ic_star_filled.png new file mode 100755 index 0000000..b8e143f Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_star_filled.png differ diff --git a/src/main/res/drawable-xhdpi/ic_tag_black.png b/src/main/res/drawable-xhdpi/ic_tag_black.png new file mode 100755 index 0000000..37fa47f Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_tag_black.png differ diff --git a/src/main/res/drawable-xhdpi/ic_tag_blue.png b/src/main/res/drawable-xhdpi/ic_tag_blue.png new file mode 100755 index 0000000..b5d987f Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_tag_blue.png differ diff --git a/src/main/res/drawable-xhdpi/ic_upload.png b/src/main/res/drawable-xhdpi/ic_upload.png new file mode 100755 index 0000000..0680633 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_upload.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_action_content_new.png b/src/main/res/drawable-xxhdpi/ic_action_content_new.png new file mode 100755 index 0000000..b8e9884 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_action_content_new.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_browse.png b/src/main/res/drawable-xxhdpi/ic_browse.png new file mode 100755 index 0000000..c0b93d9 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_browse.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_collection_black.png b/src/main/res/drawable-xxhdpi/ic_collection_black.png new file mode 100755 index 0000000..66347ae Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_collection_black.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_collection_blue.png b/src/main/res/drawable-xxhdpi/ic_collection_blue.png new file mode 100755 index 0000000..973112f Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_collection_blue.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_delete.png b/src/main/res/drawable-xxhdpi/ic_delete.png new file mode 100755 index 0000000..4ee26a5 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_delete.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_down.png b/src/main/res/drawable-xxhdpi/ic_down.png new file mode 100755 index 0000000..d925ffd Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_down.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_edit.png b/src/main/res/drawable-xxhdpi/ic_edit.png new file mode 100755 index 0000000..b149661 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_edit.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_favorites_black.png b/src/main/res/drawable-xxhdpi/ic_favorites_black.png new file mode 100755 index 0000000..6416df4 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_favorites_black.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_favorites_blue.png b/src/main/res/drawable-xxhdpi/ic_favorites_blue.png new file mode 100755 index 0000000..a762744 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_favorites_blue.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_isbn.png b/src/main/res/drawable-xxhdpi/ic_isbn.png new file mode 100755 index 0000000..d66a579 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_isbn.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_item_black.png b/src/main/res/drawable-xxhdpi/ic_item_black.png new file mode 100755 index 0000000..75da8f7 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_item_black.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_item_blue.png b/src/main/res/drawable-xxhdpi/ic_item_blue.png new file mode 100755 index 0000000..f91d3af Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_item_blue.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_launcher.png b/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100755 index 0000000..361d17f Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_manual.png b/src/main/res/drawable-xxhdpi/ic_manual.png new file mode 100755 index 0000000..22b5ffc Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_manual.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_plus.png b/src/main/res/drawable-xxhdpi/ic_plus.png new file mode 100755 index 0000000..2c8c167 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_plus.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_scan.png b/src/main/res/drawable-xxhdpi/ic_scan.png new file mode 100755 index 0000000..ec827dd Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_scan.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_settings.png b/src/main/res/drawable-xxhdpi/ic_settings.png new file mode 100755 index 0000000..39205bd Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_settings.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_star_empty.png b/src/main/res/drawable-xxhdpi/ic_star_empty.png new file mode 100755 index 0000000..36e5cb4 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_star_empty.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_star_filled.png b/src/main/res/drawable-xxhdpi/ic_star_filled.png new file mode 100755 index 0000000..a05553d Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_star_filled.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_tag_black.png b/src/main/res/drawable-xxhdpi/ic_tag_black.png new file mode 100755 index 0000000..c116f92 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_tag_black.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_tag_blue.png b/src/main/res/drawable-xxhdpi/ic_tag_blue.png new file mode 100755 index 0000000..57d0249 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_tag_blue.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_upload.png b/src/main/res/drawable-xxhdpi/ic_upload.png new file mode 100755 index 0000000..93e203a Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_upload.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_action_content_new.png b/src/main/res/drawable-xxxhdpi/ic_action_content_new.png new file mode 100755 index 0000000..d4cca96 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_action_content_new.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_browse.png b/src/main/res/drawable-xxxhdpi/ic_browse.png new file mode 100755 index 0000000..3a419f7 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_browse.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_collection_black.png b/src/main/res/drawable-xxxhdpi/ic_collection_black.png new file mode 100755 index 0000000..4af96cc Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_collection_black.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_collection_blue.png b/src/main/res/drawable-xxxhdpi/ic_collection_blue.png new file mode 100755 index 0000000..7fd5c9a Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_collection_blue.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_delete.png b/src/main/res/drawable-xxxhdpi/ic_delete.png new file mode 100755 index 0000000..181fbdd Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_delete.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_down.png b/src/main/res/drawable-xxxhdpi/ic_down.png new file mode 100755 index 0000000..91b8d82 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_down.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_edit.png b/src/main/res/drawable-xxxhdpi/ic_edit.png new file mode 100755 index 0000000..ae2ace9 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_edit.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_favorites_black.png b/src/main/res/drawable-xxxhdpi/ic_favorites_black.png new file mode 100755 index 0000000..12cbf80 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_favorites_black.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_favorites_blue.png b/src/main/res/drawable-xxxhdpi/ic_favorites_blue.png new file mode 100755 index 0000000..9a0e68b Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_favorites_blue.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_isbn.png b/src/main/res/drawable-xxxhdpi/ic_isbn.png new file mode 100755 index 0000000..7d2256e Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_isbn.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_item_black.png b/src/main/res/drawable-xxxhdpi/ic_item_black.png new file mode 100755 index 0000000..673c033 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_item_black.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_item_blue.png b/src/main/res/drawable-xxxhdpi/ic_item_blue.png new file mode 100755 index 0000000..c1e1a7d Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_item_blue.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_launcher.png b/src/main/res/drawable-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000..c002e89 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_manual.png b/src/main/res/drawable-xxxhdpi/ic_manual.png new file mode 100755 index 0000000..3507637 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_manual.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_plus.png b/src/main/res/drawable-xxxhdpi/ic_plus.png new file mode 100755 index 0000000..0a0c733 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_plus.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_scan.png b/src/main/res/drawable-xxxhdpi/ic_scan.png new file mode 100755 index 0000000..e6dbf41 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_scan.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_settings.png b/src/main/res/drawable-xxxhdpi/ic_settings.png new file mode 100755 index 0000000..3a9d01c Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_settings.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_star_empty.png b/src/main/res/drawable-xxxhdpi/ic_star_empty.png new file mode 100755 index 0000000..a691a20 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_star_empty.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_star_filled.png b/src/main/res/drawable-xxxhdpi/ic_star_filled.png new file mode 100755 index 0000000..963ef40 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_star_filled.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_tag_black.png b/src/main/res/drawable-xxxhdpi/ic_tag_black.png new file mode 100755 index 0000000..1d04cf0 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_tag_black.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_tag_blue.png b/src/main/res/drawable-xxxhdpi/ic_tag_blue.png new file mode 100755 index 0000000..ff41a9a Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_tag_blue.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_upload.png b/src/main/res/drawable-xxxhdpi/ic_upload.png new file mode 100755 index 0000000..3ab5d4f Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_upload.png differ diff --git a/src/main/res/drawable/bg_light.xml b/src/main/res/drawable/bg_light.xml new file mode 100755 index 0000000..f91b837 --- /dev/null +++ b/src/main/res/drawable/bg_light.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/bg_primary_light.xml b/src/main/res/drawable/bg_primary_light.xml new file mode 100755 index 0000000..ab6d13b --- /dev/null +++ b/src/main/res/drawable/bg_primary_light.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/res/drawable/book.png b/src/main/res/drawable/book.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/book_open.png b/src/main/res/drawable/book_open.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/comment.png b/src/main/res/drawable/comment.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/email.png b/src/main/res/drawable/email.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/fab_label_background.xml b/src/main/res/drawable/fab_label_background.xml new file mode 100755 index 0000000..3ba2a5b --- /dev/null +++ b/src/main/res/drawable/fab_label_background.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/film.png b/src/main/res/drawable/film.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/folder.png b/src/main/res/drawable/folder.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/glyphish_02_redo.png b/src/main/res/drawable/glyphish_02_redo.png deleted file mode 100644 index f8c8fb1..0000000 Binary files a/src/main/res/drawable/glyphish_02_redo.png and /dev/null differ diff --git a/src/main/res/drawable/glyphish_104_index_cards.png b/src/main/res/drawable/glyphish_104_index_cards.png deleted file mode 100644 index a1a5dfd..0000000 Binary files a/src/main/res/drawable/glyphish_104_index_cards.png and /dev/null differ diff --git a/src/main/res/drawable/glyphish_106_sliders.png b/src/main/res/drawable/glyphish_106_sliders.png deleted file mode 100644 index 00c5f5e..0000000 Binary files a/src/main/res/drawable/glyphish_106_sliders.png and /dev/null differ diff --git a/src/main/res/drawable/glyphish_10_medical.png b/src/main/res/drawable/glyphish_10_medical.png deleted file mode 100644 index ab78c78..0000000 Binary files a/src/main/res/drawable/glyphish_10_medical.png and /dev/null differ diff --git a/src/main/res/drawable/glyphish_151_telescope.png b/src/main/res/drawable/glyphish_151_telescope.png deleted file mode 100644 index be5852d..0000000 Binary files a/src/main/res/drawable/glyphish_151_telescope.png and /dev/null differ diff --git a/src/main/res/drawable/glyphish_195_barcode.png b/src/main/res/drawable/glyphish_195_barcode.png deleted file mode 100644 index a821cd2..0000000 Binary files a/src/main/res/drawable/glyphish_195_barcode.png and /dev/null differ diff --git a/src/main/res/drawable/glyphish_22_skull_n_bones.png b/src/main/res/drawable/glyphish_22_skull_n_bones.png deleted file mode 100644 index 462f549..0000000 Binary files a/src/main/res/drawable/glyphish_22_skull_n_bones.png and /dev/null differ diff --git a/src/main/res/drawable/glyphish_59_flag.png b/src/main/res/drawable/glyphish_59_flag.png deleted file mode 100644 index 3be3014..0000000 Binary files a/src/main/res/drawable/glyphish_59_flag.png and /dev/null differ diff --git a/src/main/res/drawable/ic_action_content_new.png b/src/main/res/drawable/ic_action_content_new.png new file mode 100755 index 0000000..f002f19 Binary files /dev/null and b/src/main/res/drawable/ic_action_content_new.png differ diff --git a/src/main/res/drawable/layout.png b/src/main/res/drawable/layout.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/list_child_indicator.xml b/src/main/res/drawable/list_child_indicator.xml old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/map.png b/src/main/res/drawable/map.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/newspaper.png b/src/main/res/drawable/newspaper.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/note.png b/src/main/res/drawable/note.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page.png b/src/main/res/drawable/page.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white.png b/src/main/res/drawable/page_white.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_acrobat.png b/src/main/res/drawable/page_white_acrobat.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_powerpoint.png b/src/main/res/drawable/page_white_powerpoint.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_text.png b/src/main/res/drawable/page_white_text.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_text_width.png b/src/main/res/drawable/page_white_text_width.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/page_white_width.png b/src/main/res/drawable/page_white_width.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/picture.png b/src/main/res/drawable/picture.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/report.png b/src/main/res/drawable/report.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/report_user.png b/src/main/res/drawable/report_user.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/script.png b/src/main/res/drawable/script.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/state_list_item.xml b/src/main/res/drawable/state_list_item.xml new file mode 100755 index 0000000..4c6dd1a --- /dev/null +++ b/src/main/res/drawable/state_list_item.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/television.png b/src/main/res/drawable/television.png old mode 100644 new mode 100755 diff --git a/src/main/res/drawable/zandy72.png b/src/main/res/drawable/zandy72.png deleted file mode 100644 index 773aae6..0000000 Binary files a/src/main/res/drawable/zandy72.png and /dev/null differ diff --git a/src/main/res/layout-sw600dp/collections.xml b/src/main/res/layout-sw600dp/collections.xml new file mode 100755 index 0000000..3505280 --- /dev/null +++ b/src/main/res/layout-sw600dp/collections.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/activity_main.xml b/src/main/res/layout/activity_main.xml new file mode 100755 index 0000000..5043b8e --- /dev/null +++ b/src/main/res/layout/activity_main.xml @@ -0,0 +1,214 @@ + + + + + + +