From ed01c37fe18f98c25b7515c459ae446efa8232ca Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Wed, 25 Dec 2013 18:14:38 +0100 Subject: Added shortifyFingerprint() to PGPHelper --- .../src/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java index e2d89bfab..5a3d332c4 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java @@ -40,6 +40,17 @@ import android.content.Context; public class PgpKeyHelper { + /** + * Returns the last 9 chars of a fingerprint + * + * @param fingerprint + * String containing short or long fingerprint + * @return + */ + public static String shortifyFingerprint(String fingerprint) { + return fingerprint.substring(41); + } + public static Date getCreationDate(PGPPublicKey key) { return key.getCreationTime(); } -- cgit v1.2.3 From 208ea19d5d56fe374bbf26e4ab4cce13dc99656b Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Wed, 25 Dec 2013 18:15:30 +0100 Subject: Added first draft of KeyDetailsActivity --- OpenPGP-Keychain/AndroidManifest.xml | 10 ++ OpenPGP-Keychain/res/layout/key_view.xml | 110 +++++++++++++++++++++ OpenPGP-Keychain/res/values/strings.xml | 7 +- .../keychain/ui/KeyDetailsActivity.java | 90 +++++++++++++++++ 4 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 OpenPGP-Keychain/res/layout/key_view.xml create mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java diff --git a/OpenPGP-Keychain/AndroidManifest.xml b/OpenPGP-Keychain/AndroidManifest.xml index ca203c1f2..36b835266 100644 --- a/OpenPGP-Keychain/AndroidManifest.xml +++ b/OpenPGP-Keychain/AndroidManifest.xml @@ -123,6 +123,16 @@ android:label="@string/title_edit_key" android:uiOptions="splitActionBarWhenNarrow" android:windowSoftInputMode="stateHidden" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/values/strings.xml b/OpenPGP-Keychain/res/values/strings.xml index 63f267277..9909292d3 100644 --- a/OpenPGP-Keychain/res/values/strings.xml +++ b/OpenPGP-Keychain/res/values/strings.xml @@ -44,6 +44,7 @@ Export to Key Server Unknown Signature Key Sign Key + Key Details Help Share key with NFC @@ -53,6 +54,7 @@ General Defaults Advanced + Master Key Sign (Clipboard) @@ -142,6 +144,8 @@ %s key server(s) Fingerprint: Secret Key: + not valid + Secret Keyring None @@ -340,5 +344,4 @@ Go through all QR Codes using \'Next\', and scan them one by one. QR Code %1$d of %2$d - - \ No newline at end of file + diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java new file mode 100644 index 000000000..652b8a89b --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java @@ -0,0 +1,90 @@ +package org.sufficientlysecure.keychain.ui; + +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.spongycastle.openpgp.PGPPublicKey; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; + +import android.os.Bundle; +import android.text.format.DateFormat; +import android.widget.TextView; + +import com.actionbarsherlock.app.SherlockActivity; + +public class KeyDetailsActivity extends SherlockActivity { + + private PGPPublicKey publicKey; + private TextView mAlgorithm; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + setContentView(R.layout.key_view); + if (extras == null) { + return; + } + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + long key = extras.getLong("key"); + + KeyRings.buildPublicKeyRingsByMasterKeyIdUri(key + ""); + String[] projection = new String[]{""}; + + this.publicKey = ProviderHelper.getPGPPublicKeyByKeyId( + getApplicationContext(), key); + + TextView fingerprint = (TextView) this.findViewById(R.id.fingerprint); + fingerprint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper.getFingerPrint(getApplicationContext(), key))); + String[] mainUserId = splitUserId(""); + + TextView expiry = (TextView) this.findViewById(R.id.expiry); + Date expiryDate = PgpKeyHelper.getExpiryDate(publicKey); + if (expiryDate == null) { + expiry.setText(""); + } else { + expiry.setText(DateFormat.getDateFormat(getApplicationContext()) + .format(expiryDate)); + } + + TextView creation = (TextView) this.findViewById(R.id.creation); + creation.setText(DateFormat.getDateFormat(getApplicationContext()) + .format(PgpKeyHelper.getCreationDate(publicKey))); + mAlgorithm = (TextView) this.findViewById(R.id.algorithm); + mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(publicKey)); + + } + + private String[] splitUserId(String userId) { + + String[] result = new String[]{"", "", ""}; + Log.v("UserID", userId); + + Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$"); + Matcher matcher = withComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(2); + result[2] = matcher.group(3); + return result; + } + + Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); + matcher = withoutComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(2); + return result; + } + return result; + } +} -- cgit v1.2.3 From e823ef6efce29fce8a8deb31b22a2d75416702c4 Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Wed, 25 Dec 2013 19:33:47 +0100 Subject: Renamed keyDetails to key_details --- OpenPGP-Keychain/AndroidManifest.xml | 2 +- OpenPGP-Keychain/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenPGP-Keychain/AndroidManifest.xml b/OpenPGP-Keychain/AndroidManifest.xml index 36b835266..e363047e6 100644 --- a/OpenPGP-Keychain/AndroidManifest.xml +++ b/OpenPGP-Keychain/AndroidManifest.xml @@ -126,7 +126,7 @@ Export to Key Server Unknown Signature Key Sign Key - Key Details + Key Details Help Share key with NFC -- cgit v1.2.3 From f265cd4d68a4bb398467da1a8d19b6ca0b125602 Mon Sep 17 00:00:00 2001 From: Bahtiar `kalkin-` Gadimov Date: Wed, 25 Dec 2013 19:33:53 +0100 Subject: Added context menu item KeyDetails to KeyListPublicFragment --- .../keychain/ui/KeyListPublicFragment.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java index 0fdcea809..790ec5ccf 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java @@ -73,9 +73,10 @@ public class KeyListPublicFragment extends KeyListFragment implements @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - menu.add(0, Id.menu.update, 1, R.string.menu_update_key); - menu.add(0, Id.menu.signKey, 2, R.string.menu_sign_key); - menu.add(0, Id.menu.exportToServer, 3, R.string.menu_export_key_to_server); + menu.add(0, 23, 1, R.string.title_key_details); // :TODO: Fix magic number + menu.add(0, Id.menu.update, 2, R.string.menu_update_key); + menu.add(0, Id.menu.signKey, 3, R.string.menu_sign_key); + menu.add(0, Id.menu.exportToServer, 4, R.string.menu_export_key_to_server); menu.add(0, Id.menu.share, 6, R.string.menu_share); menu.add(0, Id.menu.share_qr_code, 7, R.string.menu_share_qr_code); menu.add(0, Id.menu.share_nfc, 8, R.string.menu_share_nfc); @@ -112,7 +113,13 @@ public class KeyListPublicFragment extends KeyListFragment implements startActivityForResult(queryIntent, Id.request.look_up_key_id); return true; - + case 23: + + Intent detailsIntent = new Intent(mKeyListActivity, KeyDetailsActivity.class); + detailsIntent.putExtra("key", ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId)); + startActivity(detailsIntent); + return true; + case Id.menu.exportToServer: Intent uploadIntent = new Intent(mKeyListActivity, KeyServerUploadActivity.class); uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER); -- cgit v1.2.3 From daadc30044659bc08ba3fcbca16083b0e98d353e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 31 Dec 2013 01:34:06 +0100 Subject: readme wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 601d4710c..0bea8d377 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Translations are managed at Transifex, please contribute there at https://www.tr ## Code Contributions -Fork OpenPGP Keychain and do a pull request. I will help with occuring problems and merge your changes back into the main project. +Fork OpenPGP Keychain and create a pull request. I will help with occuring problems and merge your changes back into the main project. I am happy about every code contribution and appreciate your effort to help us developing OpenPGP Keychain :) ## Build with Gradle -- cgit v1.2.3 From cdb3e04b4701079cb77fb74e57de7545431a3b37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 31 Dec 2013 01:41:21 +0100 Subject: Add pinned listview lib --- libraries/pinned-section-listview/.gitignore | 6 + libraries/pinned-section-listview/README.md | 75 +++ .../example/AndroidManifest.xml | 27 ++ .../example/assets/.gitignore | 1 + .../example/ic_launcher-web.png | Bin 0 -> 45500 bytes .../example/libs/android-support-v4.jar | Bin 0 -> 393154 bytes .../example/proguard-project.txt | 20 + .../example/project.properties | 15 + .../example/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 5819 bytes .../example/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 3479 bytes .../example/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 7534 bytes .../example/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 11732 bytes .../example/res/layout/activity_main.xml | 8 + .../example/res/menu/main.xml | 27 ++ .../example/res/values-v11/styles.xml | 11 + .../example/res/values-v14/styles.xml | 12 + .../example/res/values/colors.xml | 10 + .../example/res/values/strings.xml | 10 + .../example/res/values/styles.xml | 20 + .../com/hb/examples/PinnedSectionListActivity.java | 285 ++++++++++++ .../library/AndroidManifest.xml | 13 + .../library/assets/.gitignore | 1 + .../pinned-section-listview/library/build.gradle | 24 + .../library/proguard-project.txt | 20 + .../library/project.properties | 15 + .../pinned-section-listview/library/res/.gitignore | 1 + .../src/com/hb/views/PinnedSectionListView.java | 513 +++++++++++++++++++++ libraries/pinned-section-listview/screen1.png | Bin 0 -> 23124 bytes libraries/pinned-section-listview/screen2.png | Bin 0 -> 22743 bytes libraries/pinned-section-listview/screen3.png | Bin 0 -> 22885 bytes 30 files changed, 1114 insertions(+) create mode 100644 libraries/pinned-section-listview/.gitignore create mode 100644 libraries/pinned-section-listview/README.md create mode 100644 libraries/pinned-section-listview/example/AndroidManifest.xml create mode 100644 libraries/pinned-section-listview/example/assets/.gitignore create mode 100644 libraries/pinned-section-listview/example/ic_launcher-web.png create mode 100644 libraries/pinned-section-listview/example/libs/android-support-v4.jar create mode 100644 libraries/pinned-section-listview/example/proguard-project.txt create mode 100644 libraries/pinned-section-listview/example/project.properties create mode 100644 libraries/pinned-section-listview/example/res/drawable-hdpi/ic_launcher.png create mode 100644 libraries/pinned-section-listview/example/res/drawable-mdpi/ic_launcher.png create mode 100644 libraries/pinned-section-listview/example/res/drawable-xhdpi/ic_launcher.png create mode 100644 libraries/pinned-section-listview/example/res/drawable-xxhdpi/ic_launcher.png create mode 100644 libraries/pinned-section-listview/example/res/layout/activity_main.xml create mode 100644 libraries/pinned-section-listview/example/res/menu/main.xml create mode 100644 libraries/pinned-section-listview/example/res/values-v11/styles.xml create mode 100644 libraries/pinned-section-listview/example/res/values-v14/styles.xml create mode 100644 libraries/pinned-section-listview/example/res/values/colors.xml create mode 100644 libraries/pinned-section-listview/example/res/values/strings.xml create mode 100644 libraries/pinned-section-listview/example/res/values/styles.xml create mode 100644 libraries/pinned-section-listview/example/src/com/hb/examples/PinnedSectionListActivity.java create mode 100644 libraries/pinned-section-listview/library/AndroidManifest.xml create mode 100644 libraries/pinned-section-listview/library/assets/.gitignore create mode 100644 libraries/pinned-section-listview/library/build.gradle create mode 100644 libraries/pinned-section-listview/library/proguard-project.txt create mode 100644 libraries/pinned-section-listview/library/project.properties create mode 100644 libraries/pinned-section-listview/library/res/.gitignore create mode 100644 libraries/pinned-section-listview/library/src/com/hb/views/PinnedSectionListView.java create mode 100644 libraries/pinned-section-listview/screen1.png create mode 100644 libraries/pinned-section-listview/screen2.png create mode 100644 libraries/pinned-section-listview/screen3.png diff --git a/libraries/pinned-section-listview/.gitignore b/libraries/pinned-section-listview/.gitignore new file mode 100644 index 000000000..ba354bcae --- /dev/null +++ b/libraries/pinned-section-listview/.gitignore @@ -0,0 +1,6 @@ +bin +gen +.project +.classpath +.settings +.DS_Store diff --git a/libraries/pinned-section-listview/README.md b/libraries/pinned-section-listview/README.md new file mode 100644 index 000000000..e89cde3bb --- /dev/null +++ b/libraries/pinned-section-listview/README.md @@ -0,0 +1,75 @@ +Introduction +============ + +Easy to use ListView with pinned sections for Android. Pinned section is a header view which sticks to the top +of the list until at least one item of that section is visible. + +![Alt text](screen1.png)  +![Alt text](screen2.png)  +![Alt text](screen3.png) + +Features +======== +This list properly implements many features which are missing from other implementations. These are + * Fast scroll + * Headers and footers + * Clickable pinned sections + +Besides this it doesn't create any unnecessary views, layouts etc. It's really lean. + +Watch [this video][1] to see `PinnedSectionListView` in action. + +Usage +===== + 1. Replace standard `ListView` with `com.hb.views.PinnedSectionListView` in your `layout.xml` file. + + + + 2. Extend your `ListAdapter` in a way that it implements `PinnedSectionListAdapter` interface, in addition to + what it already implements. Basically you need to add a single `isItemViewTypePinned(int viewType)` + method. This method must return `true` for all view types which have to be pinned. + + // Our adapter class implements 'PinnedSectionListAdapter' interface + class MyPinnedSectionListAdapter extends BaseAdapter implements PinnedSectionListAdapter { + + ... + + // We implement this method to return 'true' for all view types we want to pin + @Override + public boolean isItemViewTypePinned(int viewType) { + return viewType == ; + } + } + +That's all. You are done! A working example can also be found in `example` folder. + +Used by +======= +Let us know if you use this library in your application and we will mention it here. + +[Grocery Sum][2] + +License +======= + + Copyright 2013 Sergej Shafarenka, halfbit.de + + Licensed 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. + + +[1]: http://www.youtube.com/watch?v=mI3DpuoIIhQ +[2]: https://play.google.com/store/apps/details?id=org.codechimp.grocerysum diff --git a/libraries/pinned-section-listview/example/AndroidManifest.xml b/libraries/pinned-section-listview/example/AndroidManifest.xml new file mode 100644 index 000000000..56621893d --- /dev/null +++ b/libraries/pinned-section-listview/example/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/assets/.gitignore b/libraries/pinned-section-listview/example/assets/.gitignore new file mode 100644 index 000000000..fdffa2a0f --- /dev/null +++ b/libraries/pinned-section-listview/example/assets/.gitignore @@ -0,0 +1 @@ +# placeholder diff --git a/libraries/pinned-section-listview/example/ic_launcher-web.png b/libraries/pinned-section-listview/example/ic_launcher-web.png new file mode 100644 index 000000000..8bd40412e Binary files /dev/null and b/libraries/pinned-section-listview/example/ic_launcher-web.png differ diff --git a/libraries/pinned-section-listview/example/libs/android-support-v4.jar b/libraries/pinned-section-listview/example/libs/android-support-v4.jar new file mode 100644 index 000000000..65ebaf8dc Binary files /dev/null and b/libraries/pinned-section-listview/example/libs/android-support-v4.jar differ diff --git a/libraries/pinned-section-listview/example/proguard-project.txt b/libraries/pinned-section-listview/example/proguard-project.txt new file mode 100644 index 000000000..f2fe1559a --- /dev/null +++ b/libraries/pinned-section-listview/example/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/libraries/pinned-section-listview/example/project.properties b/libraries/pinned-section-listview/example/project.properties new file mode 100644 index 000000000..1561d7a9a --- /dev/null +++ b/libraries/pinned-section-listview/example/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library.reference.1=../library diff --git a/libraries/pinned-section-listview/example/res/drawable-hdpi/ic_launcher.png b/libraries/pinned-section-listview/example/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..3841c3408 Binary files /dev/null and b/libraries/pinned-section-listview/example/res/drawable-hdpi/ic_launcher.png differ diff --git a/libraries/pinned-section-listview/example/res/drawable-mdpi/ic_launcher.png b/libraries/pinned-section-listview/example/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..e65953cce Binary files /dev/null and b/libraries/pinned-section-listview/example/res/drawable-mdpi/ic_launcher.png differ diff --git a/libraries/pinned-section-listview/example/res/drawable-xhdpi/ic_launcher.png b/libraries/pinned-section-listview/example/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..4ea093d86 Binary files /dev/null and b/libraries/pinned-section-listview/example/res/drawable-xhdpi/ic_launcher.png differ diff --git a/libraries/pinned-section-listview/example/res/drawable-xxhdpi/ic_launcher.png b/libraries/pinned-section-listview/example/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..87e928a35 Binary files /dev/null and b/libraries/pinned-section-listview/example/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/libraries/pinned-section-listview/example/res/layout/activity_main.xml b/libraries/pinned-section-listview/example/res/layout/activity_main.xml new file mode 100644 index 000000000..b641f4c80 --- /dev/null +++ b/libraries/pinned-section-listview/example/res/layout/activity_main.xml @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/menu/main.xml b/libraries/pinned-section-listview/example/res/menu/main.xml new file mode 100644 index 000000000..091b6c770 --- /dev/null +++ b/libraries/pinned-section-listview/example/res/menu/main.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values-v11/styles.xml b/libraries/pinned-section-listview/example/res/values-v11/styles.xml new file mode 100644 index 000000000..541752f6e --- /dev/null +++ b/libraries/pinned-section-listview/example/res/values-v11/styles.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values-v14/styles.xml b/libraries/pinned-section-listview/example/res/values-v14/styles.xml new file mode 100644 index 000000000..f20e01501 --- /dev/null +++ b/libraries/pinned-section-listview/example/res/values-v14/styles.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values/colors.xml b/libraries/pinned-section-listview/example/res/values/colors.xml new file mode 100644 index 000000000..b4da27c93 --- /dev/null +++ b/libraries/pinned-section-listview/example/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #ffff4444 + #ff99cc00 + #ffffbb33 + #ff33b5e5 + + + + \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values/strings.xml b/libraries/pinned-section-listview/example/res/values/strings.xml new file mode 100644 index 000000000..e9d43a4aa --- /dev/null +++ b/libraries/pinned-section-listview/example/res/values/strings.xml @@ -0,0 +1,10 @@ + + + + Pinned Section Demo + Fast scroll + Add padding + Show shadow + Show Header & Footer + + \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values/styles.xml b/libraries/pinned-section-listview/example/res/values/styles.xml new file mode 100644 index 000000000..4a10ca492 --- /dev/null +++ b/libraries/pinned-section-listview/example/res/values/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/src/com/hb/examples/PinnedSectionListActivity.java b/libraries/pinned-section-listview/example/src/com/hb/examples/PinnedSectionListActivity.java new file mode 100644 index 000000000..223718a06 --- /dev/null +++ b/libraries/pinned-section-listview/example/src/com/hb/examples/PinnedSectionListActivity.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2013 Sergej Shafarenka, halfbit.de + * + * Licensed 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. + */ + +package com.hb.examples; + +import java.util.Locale; + +import android.annotation.SuppressLint; +import android.app.ListActivity; +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.SectionIndexer; +import android.widget.TextView; +import android.widget.Toast; + +import com.hb.examples.pinnedsection.R; +import com.hb.views.PinnedSectionListView; +import com.hb.views.PinnedSectionListView.PinnedSectionListAdapter; + +public class PinnedSectionListActivity extends ListActivity implements OnClickListener { + + static class SimpleAdapter extends ArrayAdapter implements PinnedSectionListAdapter { + + private static final int[] COLORS = new int[] { + R.color.green_light, R.color.orange_light, + R.color.blue_light, R.color.red_light }; + + public SimpleAdapter(Context context, int resource, int textViewResourceId) { + super(context, resource, textViewResourceId); + + final int sectionsNumber = 'Z' - 'A' + 1; + prepareSections(sectionsNumber); + + int sectionPosition = 0, listPosition = 0; + for (char i=0; i= sections.length) { + section = sections.length - 1; + } + return sections[section].listPosition; + } + + @Override public int getSectionForPosition(int position) { + if (position >= getCount()) { + position = getCount() - 1; + } + return getItem(position).sectionPosition; + } + + } + + static class Item { + + public static final int ITEM = 0; + public static final int SECTION = 1; + + public final int type; + public final String text; + + public int sectionPosition; + public int listPosition; + + public Item(int type, String text) { + this.type = type; + this.text = text; + } + + @Override public String toString() { + return text; + } + + } + + private boolean hasHeaderAndFooter; + private boolean isFastScroll; + private boolean addPadding; + private boolean isShadowVisible = true; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + if (savedInstanceState != null) { + isFastScroll = savedInstanceState.getBoolean("isFastScroll"); + addPadding = savedInstanceState.getBoolean("addPadding"); + isShadowVisible = savedInstanceState.getBoolean("isShadowVisible"); + hasHeaderAndFooter = savedInstanceState.getBoolean("hasHeaderAndFooter"); + } + initializeHeaderAndFooter(); + initializeAdapter(); + initializePadding(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean("isFastScroll", isFastScroll); + outState.putBoolean("addPadding", addPadding); + outState.putBoolean("isShadowVisible", isShadowVisible); + outState.putBoolean("hasHeaderAndFooter", hasHeaderAndFooter); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + Item item = (Item) getListView().getAdapter().getItem(position); + if (item != null) { + Toast.makeText(this, "Item " + position + ": " + item.text, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "Item " + position, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + menu.getItem(0).setChecked(isFastScroll); + menu.getItem(1).setChecked(addPadding); + menu.getItem(2).setChecked(isShadowVisible); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_fastscroll: + isFastScroll = !isFastScroll; + item.setChecked(isFastScroll); + initializeAdapter(); + break; + case R.id.action_addpadding: + addPadding = !addPadding; + item.setChecked(addPadding); + initializePadding(); + break; + case R.id.action_showShadow: + isShadowVisible = !isShadowVisible; + item.setChecked(isShadowVisible); + ((PinnedSectionListView)getListView()).setShadowVisible(isShadowVisible); + break; + case R.id.action_showHeaderAndFooter: + hasHeaderAndFooter = !hasHeaderAndFooter; + item.setChecked(hasHeaderAndFooter); + initializeHeaderAndFooter(); + break; + } + return true; + } + + private void initializePadding() { + float density = getResources().getDisplayMetrics().density; + int padding = addPadding ? (int) (16 * density) : 0; + getListView().setPadding(padding, padding, padding, padding); + } + + private void initializeHeaderAndFooter() { + setListAdapter(null); + if (hasHeaderAndFooter) { + ListView list = getListView(); + + LayoutInflater inflater = LayoutInflater.from(this); + TextView header1 = (TextView) inflater.inflate(android.R.layout.simple_list_item_1, list, false); + header1.setText("First header"); + list.addHeaderView(header1); + + TextView header2 = (TextView) inflater.inflate(android.R.layout.simple_list_item_1, list, false); + header2.setText("Second header"); + list.addHeaderView(header2); + + TextView footer = (TextView) inflater.inflate(android.R.layout.simple_list_item_1, list, false); + footer.setText("Single footer"); + list.addFooterView(footer); + } + initializeAdapter(); + } + + @SuppressLint("NewApi") + private void initializeAdapter() { + getListView().setFastScrollEnabled(isFastScroll); + if (isFastScroll) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + getListView().setFastScrollAlwaysVisible(true); + } + setListAdapter(new FastScrollAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1)); + } else { + setListAdapter(new SimpleAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1)); + } + } + + @Override + public void onClick(View v) { + Toast.makeText(this, "Item: " + v.getTag() , Toast.LENGTH_SHORT).show(); + } + +} \ No newline at end of file diff --git a/libraries/pinned-section-listview/library/AndroidManifest.xml b/libraries/pinned-section-listview/library/AndroidManifest.xml new file mode 100644 index 000000000..2e2ee9173 --- /dev/null +++ b/libraries/pinned-section-listview/library/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/libraries/pinned-section-listview/library/assets/.gitignore b/libraries/pinned-section-listview/library/assets/.gitignore new file mode 100644 index 000000000..fdffa2a0f --- /dev/null +++ b/libraries/pinned-section-listview/library/assets/.gitignore @@ -0,0 +1 @@ +# placeholder diff --git a/libraries/pinned-section-listview/library/build.gradle b/libraries/pinned-section-listview/library/build.gradle new file mode 100644 index 000000000..d77f4746f --- /dev/null +++ b/libraries/pinned-section-listview/library/build.gradle @@ -0,0 +1,24 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.6.3' + } +} + +apply plugin: 'android-library' + + +android { + compileSdkVersion 17 + buildToolsVersion '19' + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + res.srcDirs = ['res'] + } + } +} diff --git a/libraries/pinned-section-listview/library/proguard-project.txt b/libraries/pinned-section-listview/library/proguard-project.txt new file mode 100644 index 000000000..f2fe1559a --- /dev/null +++ b/libraries/pinned-section-listview/library/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/libraries/pinned-section-listview/library/project.properties b/libraries/pinned-section-listview/library/project.properties new file mode 100644 index 000000000..484dab075 --- /dev/null +++ b/libraries/pinned-section-listview/library/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library=true diff --git a/libraries/pinned-section-listview/library/res/.gitignore b/libraries/pinned-section-listview/library/res/.gitignore new file mode 100644 index 000000000..fdffa2a0f --- /dev/null +++ b/libraries/pinned-section-listview/library/res/.gitignore @@ -0,0 +1 @@ +# placeholder diff --git a/libraries/pinned-section-listview/library/src/com/hb/views/PinnedSectionListView.java b/libraries/pinned-section-listview/library/src/com/hb/views/PinnedSectionListView.java new file mode 100644 index 000000000..8100ad693 --- /dev/null +++ b/libraries/pinned-section-listview/library/src/com/hb/views/PinnedSectionListView.java @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2013 Sergej Shafarenka, halfbit.de + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file kt 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. + */ + +package com.hb.views; + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.GradientDrawable.Orientation; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.accessibility.AccessibilityEvent; +import android.widget.AbsListView; +import android.widget.HeaderViewListAdapter; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.SectionIndexer; + +import com.hb.views.pinnedsection.BuildConfig; + +/** + * ListView, which is capable to pin section views at its top while the rest is still scrolled. + */ +public class PinnedSectionListView extends ListView { + + //-- inner classes + + /** List adapter to be implemented for being used with PinnedSectionListView adapter. */ + public static interface PinnedSectionListAdapter extends ListAdapter { + /** This method shall return 'true' if views of given type has to be pinned. */ + boolean isItemViewTypePinned(int viewType); + } + + /** Wrapper class for pinned section view and its position in the list. */ + static class PinnedSection { + public View view; + public int position; + public long id; + } + + //-- class fields + + // fields used for handling touch events + private final Rect mTouchRect = new Rect(); + private final PointF mTouchPoint = new PointF(); + private int mTouchSlop; + private View mTouchTarget; + private MotionEvent mDownEvent; + + // fields used for drawing shadow under a pinned section + private GradientDrawable mShadowDrawable; + private int mSectionsDistanceY; + private int mShadowHeight; + + /** Delegating listener, can be null. */ + OnScrollListener mDelegateOnScrollListener; + + /** Shadow for being recycled, can be null. */ + PinnedSection mRecycleSection; + + /** shadow instance with a pinned view, can be null. */ + PinnedSection mPinnedSection; + + /** Pinned view Y-translation. We use it to stick pinned view to the next section. */ + int mTranslateY; + + /** Scroll listener which does the magic */ + private final OnScrollListener mOnScrollListener = new OnScrollListener() { + + @Override public void onScrollStateChanged(AbsListView view, int scrollState) { + if (mDelegateOnScrollListener != null) { // delegate + mDelegateOnScrollListener.onScrollStateChanged(view, scrollState); + } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + + if (mDelegateOnScrollListener != null) { // delegate + mDelegateOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); + } + + // get expected adapter or fail fast + ListAdapter adapter = getAdapter(); + if (adapter == null || visibleItemCount == 0) return; // nothing to do + + final boolean isFirstVisibleItemSection = + isItemViewTypePinned(adapter, adapter.getItemViewType(firstVisibleItem)); + + if (isFirstVisibleItemSection) { + View sectionView = getChildAt(0); + if (sectionView.getTop() == getPaddingTop()) { // view sticks to the top, no need for pinned shadow + destroyPinnedShadow(); + } else { // section doesn't stick to the top, make sure we have a pinned shadow + ensureShadowForPosition(firstVisibleItem, firstVisibleItem, visibleItemCount); + } + + } else { // section is not at the first visible position + int sectionPosition = findCurrentSectionPosition(firstVisibleItem); + if (sectionPosition > -1) { // we have section position + ensureShadowForPosition(sectionPosition, firstVisibleItem, visibleItemCount); + } else { // there is no section for the first visible item, destroy shadow + destroyPinnedShadow(); + } + } + }; + + }; + + /** Default change observer. */ + private final DataSetObserver mDataSetObserver = new DataSetObserver() { + @Override public void onChanged() { + recreatePinnedShadow(); + }; + @Override public void onInvalidated() { + recreatePinnedShadow(); + } + }; + + //-- constructors + + public PinnedSectionListView(Context context, AttributeSet attrs) { + super(context, attrs); + initView(); + } + + public PinnedSectionListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initView(); + } + + private void initView() { + setOnScrollListener(mOnScrollListener); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + initShadow(true); + } + + //-- public API methods + + public void setShadowVisible(boolean visible) { + initShadow(visible); + if (mPinnedSection != null) { + View v = mPinnedSection.view; + invalidate(v.getLeft(), v.getTop(), v.getRight(), v.getBottom() + mShadowHeight); + } + } + + //-- pinned section drawing methods + + public void initShadow(boolean visible) { + if (visible) { + if (mShadowDrawable == null) { + mShadowDrawable = new GradientDrawable(Orientation.TOP_BOTTOM, + new int[] { Color.parseColor("#ffa0a0a0"), Color.parseColor("#50a0a0a0"), Color.parseColor("#00a0a0a0")}); + mShadowHeight = (int) (8 * getResources().getDisplayMetrics().density); + } + } else { + if (mShadowDrawable != null) { + mShadowDrawable = null; + mShadowHeight = 0; + } + } + } + + /** Create shadow wrapper with a pinned view for a view at given position */ + void createPinnedShadow(int position) { + + // try to recycle shadow + PinnedSection pinnedShadow = mRecycleSection; + mRecycleSection = null; + + // create new shadow, if needed + if (pinnedShadow == null) pinnedShadow = new PinnedSection(); + // request new view using recycled view, if such + View pinnedView = getAdapter().getView(position, pinnedShadow.view, PinnedSectionListView.this); + + // read layout parameters + LayoutParams layoutParams = (LayoutParams) pinnedView.getLayoutParams(); + if (layoutParams == null) { // create default layout params + layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + int heightMode = MeasureSpec.getMode(layoutParams.height); + int heightSize = MeasureSpec.getSize(layoutParams.height); + + if (heightMode == MeasureSpec.UNSPECIFIED) heightMode = MeasureSpec.EXACTLY; + + int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom(); + if (heightSize > maxHeight) heightSize = maxHeight; + + // measure & layout + int ws = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() - getListPaddingRight(), MeasureSpec.EXACTLY); + int hs = MeasureSpec.makeMeasureSpec(heightSize, heightMode); + pinnedView.measure(ws, hs); + pinnedView.layout(0, 0, pinnedView.getMeasuredWidth(), pinnedView.getMeasuredHeight()); + mTranslateY = 0; + + // initialize pinned shadow + pinnedShadow.view = pinnedView; + pinnedShadow.position = position; + pinnedShadow.id = getAdapter().getItemId(position); + + // store pinned shadow + mPinnedSection = pinnedShadow; + } + + /** Destroy shadow wrapper for currently pinned view */ + void destroyPinnedShadow() { + if (mPinnedSection != null) { + // keep shadow for being recycled later + mRecycleSection = mPinnedSection; + mPinnedSection = null; + } + } + + /** Makes sure we have an actual pinned shadow for given position. */ + void ensureShadowForPosition(int sectionPosition, int firstVisibleItem, int visibleItemCount) { + if (visibleItemCount < 2) { // no need for creating shadow at all, we have a single visible item + destroyPinnedShadow(); + return; + } + + if (mPinnedSection != null + && mPinnedSection.position != sectionPosition) { // invalidate shadow, if required + destroyPinnedShadow(); + } + + if (mPinnedSection == null) { // create shadow, if empty + createPinnedShadow(sectionPosition); + } + + // align shadow according to next section position, if needed + int nextPosition = sectionPosition + 1; + if (nextPosition < getCount()) { + int nextSectionPosition = findFirstVisibleSectionPosition(nextPosition, + visibleItemCount - (nextPosition - firstVisibleItem)); + if (nextSectionPosition > -1) { + View nextSectionView = getChildAt(nextSectionPosition - firstVisibleItem); + final int bottom = mPinnedSection.view.getBottom() + getPaddingTop(); + mSectionsDistanceY = nextSectionView.getTop() - bottom; + if (mSectionsDistanceY < 0) { + // next section overlaps pinned shadow, move it up + mTranslateY = mSectionsDistanceY; + } else { + // next section does not overlap with pinned, stick to top + mTranslateY = 0; + } + } else { + // no other sections are visible, stick to top + mTranslateY = 0; + mSectionsDistanceY = Integer.MAX_VALUE; + } + } + + } + + int findFirstVisibleSectionPosition(int firstVisibleItem, int visibleItemCount) { + ListAdapter adapter = getAdapter(); + for (int childIndex = 0; childIndex < visibleItemCount; childIndex++) { + int position = firstVisibleItem + childIndex; + int viewType = adapter.getItemViewType(position); + if (isItemViewTypePinned(adapter, viewType)) return position; + } + return -1; + } + + int findCurrentSectionPosition(int fromPosition) { + ListAdapter adapter = getAdapter(); + + if (adapter instanceof SectionIndexer) { + // try fast way by asking section indexer + SectionIndexer indexer = (SectionIndexer) adapter; + int sectionPosition = indexer.getSectionForPosition(fromPosition); + int itemPosition = indexer.getPositionForSection(sectionPosition); + int typeView = adapter.getItemViewType(itemPosition); + if (isItemViewTypePinned(adapter, typeView)) { + return itemPosition; + } // else, no luck + } + + // try slow way by looking through to the next section item above + for (int position=fromPosition; position>=0; position--) { + int viewType = adapter.getItemViewType(position); + if (isItemViewTypePinned(adapter, viewType)) return position; + } + return -1; // no candidate found + } + + void recreatePinnedShadow() { + destroyPinnedShadow(); + ListAdapter adapter = getAdapter(); + if (adapter != null && adapter.getCount() > 0) { + int firstVisiblePosition = getFirstVisiblePosition(); + int sectionPosition = findCurrentSectionPosition(firstVisiblePosition); + if (sectionPosition == -1) return; // no views to pin, exit + ensureShadowForPosition(sectionPosition, + firstVisiblePosition, getLastVisiblePosition() - firstVisiblePosition); + } + } + + @Override + public void setOnScrollListener(OnScrollListener listener) { + if (listener == mOnScrollListener) { + super.setOnScrollListener(listener); + } else { + mDelegateOnScrollListener = listener; + } + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + super.onRestoreInstanceState(state); + post(new Runnable() { + @Override public void run() { // restore pinned view after configuration change + recreatePinnedShadow(); + } + }); + } + + @Override + public void setAdapter(ListAdapter adapter) { + + // assert adapter in debug mode + if (BuildConfig.DEBUG && adapter != null) { + if (!(adapter instanceof PinnedSectionListAdapter)) + throw new IllegalArgumentException("Does your adapter implement PinnedSectionListAdapter?"); + if (adapter.getViewTypeCount() < 2) + throw new IllegalArgumentException("Does your adapter handle at least two types" + + " of views in getViewTypeCount() method: items and sections?"); + } + + // unregister observer at old adapter and register on new one + ListAdapter oldAdapter = getAdapter(); + if (oldAdapter != null) oldAdapter.unregisterDataSetObserver(mDataSetObserver); + if (adapter != null) adapter.registerDataSetObserver(mDataSetObserver); + + // destroy pinned shadow, if new adapter is not same as old one + if (oldAdapter != adapter) destroyPinnedShadow(); + + super.setAdapter(adapter); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + if (mPinnedSection != null) { + int parentWidth = r - l - getPaddingLeft() - getPaddingRight(); + int shadowWidth = mPinnedSection.view.getWidth(); + if (parentWidth != shadowWidth) { + recreatePinnedShadow(); + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + + if (mPinnedSection != null) { + + // prepare variables + int pLeft = getListPaddingLeft(); + int pTop = getListPaddingTop(); + View view = mPinnedSection.view; + + // draw child + canvas.save(); + + int clipHeight = view.getHeight() + + (mShadowDrawable == null ? 0 : Math.min(mShadowHeight, mSectionsDistanceY)); + canvas.clipRect(pLeft, pTop, pLeft + view.getWidth(), pTop + clipHeight); + + canvas.translate(pLeft, pTop + mTranslateY); + drawChild(canvas, mPinnedSection.view, getDrawingTime()); + + if (mShadowDrawable != null && mSectionsDistanceY > 0) { + mShadowDrawable.setBounds(mPinnedSection.view.getLeft(), + mPinnedSection.view.getBottom(), + mPinnedSection.view.getRight(), + mPinnedSection.view.getBottom() + mShadowHeight); + mShadowDrawable.draw(canvas); + } + + canvas.restore(); + } + } + + //-- touch handling methods + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + + final float x = ev.getX(); + final float y = ev.getY(); + final int action = ev.getAction(); + + if (action == MotionEvent.ACTION_DOWN + && mTouchTarget == null + && mPinnedSection != null + && isPinnedViewTouched(mPinnedSection.view, x, y)) { // create touch target + + // user touched pinned view + mTouchTarget = mPinnedSection.view; + mTouchPoint.x = x; + mTouchPoint.y = y; + + // copy down event for eventually be used later + mDownEvent = MotionEvent.obtain(ev); + } + + if (mTouchTarget != null) { + if (isPinnedViewTouched(mTouchTarget, x, y)) { // forward event to pinned view + mTouchTarget.dispatchTouchEvent(ev); + } + + if (action == MotionEvent.ACTION_UP) { // perform onClick on pinned view + super.dispatchTouchEvent(ev); + performPinnedItemClick(); + clearTouchTarget(); + + } else if (action == MotionEvent.ACTION_CANCEL) { // cancel + clearTouchTarget(); + + } else if (action == MotionEvent.ACTION_MOVE) { + if (Math.abs(y - mTouchPoint.y) > mTouchSlop) { + + // cancel sequence on touch target + MotionEvent event = MotionEvent.obtain(ev); + event.setAction(MotionEvent.ACTION_CANCEL); + mTouchTarget.dispatchTouchEvent(event); + event.recycle(); + + // provide correct sequence to super class for further handling + super.dispatchTouchEvent(mDownEvent); + super.dispatchTouchEvent(ev); + clearTouchTarget(); + + } + } + + return true; + } + + // call super if this was not our pinned view + return super.dispatchTouchEvent(ev); + } + + private boolean isPinnedViewTouched(View view, float x, float y) { + view.getHitRect(mTouchRect); + + // by taping top or bottom padding, the list performs on click on a border item. + // we don't add top padding here to keep behavior consistent. + mTouchRect.top += mTranslateY; + + mTouchRect.bottom += mTranslateY + getPaddingTop(); + mTouchRect.left += getPaddingLeft(); + mTouchRect.right -= getPaddingRight(); + return mTouchRect.contains((int)x, (int)y); + } + + private void clearTouchTarget() { + mTouchTarget = null; + if (mDownEvent != null) { + mDownEvent.recycle(); + mDownEvent = null; + } + } + + private boolean performPinnedItemClick() { + if (mPinnedSection == null) return false; + + OnItemClickListener listener = getOnItemClickListener(); + if (listener != null) { + View view = mPinnedSection.view; + playSoundEffect(SoundEffectConstants.CLICK); + if (view != null) { + view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); + } + listener.onItemClick(this, view, mPinnedSection.position, mPinnedSection.id); + return true; + } + return false; + } + + public static boolean isItemViewTypePinned(ListAdapter adapter, int viewType) { + if (adapter instanceof HeaderViewListAdapter) { + adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter(); + } + return ((PinnedSectionListAdapter) adapter).isItemViewTypePinned(viewType); + } + +} diff --git a/libraries/pinned-section-listview/screen1.png b/libraries/pinned-section-listview/screen1.png new file mode 100644 index 000000000..9cca20109 Binary files /dev/null and b/libraries/pinned-section-listview/screen1.png differ diff --git a/libraries/pinned-section-listview/screen2.png b/libraries/pinned-section-listview/screen2.png new file mode 100644 index 000000000..ad59f6f2f Binary files /dev/null and b/libraries/pinned-section-listview/screen2.png differ diff --git a/libraries/pinned-section-listview/screen3.png b/libraries/pinned-section-listview/screen3.png new file mode 100644 index 000000000..0fd528fa9 Binary files /dev/null and b/libraries/pinned-section-listview/screen3.png differ -- cgit v1.2.3 From 52c55aaabeac7f35b4800e4cbf0ca098d3d6c54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 31 Dec 2013 01:41:37 +0100 Subject: code styling --- OpenPGP-Keychain/res/layout/key_view.xml | 8 +- .../keychain/pgp/PgpKeyHelper.java | 20 +-- .../keychain/ui/KeyDetailsActivity.java | 153 ++++++++++++--------- 3 files changed, 100 insertions(+), 81 deletions(-) diff --git a/OpenPGP-Keychain/res/layout/key_view.xml b/OpenPGP-Keychain/res/layout/key_view.xml index 5649fe338..88dc2a8eb 100644 --- a/OpenPGP-Keychain/res/layout/key_view.xml +++ b/OpenPGP-Keychain/res/layout/key_view.xml @@ -95,10 +95,12 @@ android:layout_height="wrap_content" android:padding="4dp" android:text="@string/section_user_ids" /> - - + android:layout_height="wrap_content" > + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + package org.sufficientlysecure.keychain.ui; import java.util.Date; @@ -19,72 +37,71 @@ import com.actionbarsherlock.app.SherlockActivity; public class KeyDetailsActivity extends SherlockActivity { - private PGPPublicKey publicKey; - private TextView mAlgorithm; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Bundle extras = getIntent().getExtras(); - setContentView(R.layout.key_view); - if (extras == null) { - return; - } - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeButtonEnabled(true); - - long key = extras.getLong("key"); - - KeyRings.buildPublicKeyRingsByMasterKeyIdUri(key + ""); - String[] projection = new String[]{""}; - - this.publicKey = ProviderHelper.getPGPPublicKeyByKeyId( - getApplicationContext(), key); - - TextView fingerprint = (TextView) this.findViewById(R.id.fingerprint); - fingerprint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper.getFingerPrint(getApplicationContext(), key))); - String[] mainUserId = splitUserId(""); - - TextView expiry = (TextView) this.findViewById(R.id.expiry); - Date expiryDate = PgpKeyHelper.getExpiryDate(publicKey); - if (expiryDate == null) { - expiry.setText(""); - } else { - expiry.setText(DateFormat.getDateFormat(getApplicationContext()) - .format(expiryDate)); - } - - TextView creation = (TextView) this.findViewById(R.id.creation); - creation.setText(DateFormat.getDateFormat(getApplicationContext()) - .format(PgpKeyHelper.getCreationDate(publicKey))); - mAlgorithm = (TextView) this.findViewById(R.id.algorithm); - mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(publicKey)); - - } - - private String[] splitUserId(String userId) { - - String[] result = new String[]{"", "", ""}; - Log.v("UserID", userId); - - Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$"); - Matcher matcher = withComment.matcher(userId); - if (matcher.matches()) { - result[0] = matcher.group(1); - result[1] = matcher.group(2); - result[2] = matcher.group(3); - return result; - } - - Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); - matcher = withoutComment.matcher(userId); - if (matcher.matches()) { - result[0] = matcher.group(1); - result[1] = matcher.group(2); - return result; - } - return result; - } + private PGPPublicKey publicKey; + private TextView mAlgorithm; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle extras = getIntent().getExtras(); + setContentView(R.layout.key_view); + if (extras == null) { + return; + } + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + long key = extras.getLong("key"); + + KeyRings.buildPublicKeyRingsByMasterKeyIdUri(key + ""); + String[] projection = new String[] { "" }; + + this.publicKey = ProviderHelper.getPGPPublicKeyByKeyId(getApplicationContext(), key); + + TextView fingerprint = (TextView) this.findViewById(R.id.fingerprint); + fingerprint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper.getFingerPrint( + getApplicationContext(), key))); + String[] mainUserId = splitUserId(""); + + TextView expiry = (TextView) this.findViewById(R.id.expiry); + Date expiryDate = PgpKeyHelper.getExpiryDate(publicKey); + if (expiryDate == null) { + expiry.setText(""); + } else { + expiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(expiryDate)); + } + + TextView creation = (TextView) this.findViewById(R.id.creation); + creation.setText(DateFormat.getDateFormat(getApplicationContext()).format( + PgpKeyHelper.getCreationDate(publicKey))); + mAlgorithm = (TextView) this.findViewById(R.id.algorithm); + mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(publicKey)); + + } + + private String[] splitUserId(String userId) { + + String[] result = new String[] { "", "", "" }; + Log.v("UserID", userId); + + Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$"); + Matcher matcher = withComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(2); + result[2] = matcher.group(3); + return result; + } + + Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); + matcher = withoutComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(2); + return result; + } + return result; + } } -- cgit v1.2.3 From d45462bb337270c3310b29cc4af392ed65df796a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Tue, 31 Dec 2013 01:45:49 +0100 Subject: key listview --- OpenPGP-Keychain/res/layout/key_view.xml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/OpenPGP-Keychain/res/layout/key_view.xml b/OpenPGP-Keychain/res/layout/key_view.xml index 88dc2a8eb..326959e97 100644 --- a/OpenPGP-Keychain/res/layout/key_view.xml +++ b/OpenPGP-Keychain/res/layout/key_view.xml @@ -99,8 +99,7 @@ - + android:layout_height="wrap_content" /> + + \ No newline at end of file -- cgit v1.2.3 From 6c1a58ef15e6b5d826326fa9636e44cc6f501d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Wed, 1 Jan 2014 16:54:55 +0100 Subject: Use data uri instead of extra for key details --- .../keychain/provider/ProviderHelper.java | 2 +- .../keychain/ui/KeyDetailsActivity.java | 63 +++++++++++++--------- .../keychain/ui/KeyListPublicFragment.java | 16 +++--- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java index f12048277..b07ac53f8 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/ProviderHelper.java @@ -60,7 +60,7 @@ public class ProviderHelper { * @param queryUri * @return */ - private static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) { + public static PGPKeyRing getPGPKeyRing(Context context, Uri queryUri) { Cursor cursor = context.getContentResolver().query(queryUri, new String[] { KeyRings._ID, KeyRings.KEY_RING_DATA }, null, null, null); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java index 82d66e2c0..5c8444d80 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java @@ -23,12 +23,15 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.util.Log; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.text.format.DateFormat; import android.widget.TextView; @@ -36,49 +39,59 @@ import android.widget.TextView; import com.actionbarsherlock.app.SherlockActivity; public class KeyDetailsActivity extends SherlockActivity { + private Uri mDataUri; + + private PGPPublicKey mPublicKey; - private PGPPublicKey publicKey; private TextView mAlgorithm; + private TextView mFingerint; + private TextView mExpiry; + private TextView mCreation; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - Bundle extras = getIntent().getExtras(); - setContentView(R.layout.key_view); - if (extras == null) { - return; - } - getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setHomeButtonEnabled(true); - long key = extras.getLong("key"); + setContentView(R.layout.key_view); + + mFingerint = (TextView) this.findViewById(R.id.fingerprint); + mExpiry = (TextView) this.findViewById(R.id.expiry); + mCreation = (TextView) this.findViewById(R.id.creation); + mAlgorithm = (TextView) this.findViewById(R.id.algorithm); - KeyRings.buildPublicKeyRingsByMasterKeyIdUri(key + ""); - String[] projection = new String[] { "" }; + Intent intent = getIntent(); + mDataUri = intent.getData(); + if (mDataUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!"); + finish(); + return; + } else { + Log.d(Constants.TAG, "uri: " + mDataUri); + loadData(mDataUri); + } + } - this.publicKey = ProviderHelper.getPGPPublicKeyByKeyId(getApplicationContext(), key); + private void loadData(Uri dataUri) { + PGPPublicKeyRing ring = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri); + mPublicKey = ring.getPublicKey(); - TextView fingerprint = (TextView) this.findViewById(R.id.fingerprint); - fingerprint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper.getFingerPrint( - getApplicationContext(), key))); + mFingerint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper + .convertFingerprintToHex(mPublicKey.getFingerprint()))); String[] mainUserId = splitUserId(""); - TextView expiry = (TextView) this.findViewById(R.id.expiry); - Date expiryDate = PgpKeyHelper.getExpiryDate(publicKey); + Date expiryDate = PgpKeyHelper.getExpiryDate(mPublicKey); if (expiryDate == null) { - expiry.setText(""); + mExpiry.setText(""); } else { - expiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(expiryDate)); + mExpiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(expiryDate)); } - TextView creation = (TextView) this.findViewById(R.id.creation); - creation.setText(DateFormat.getDateFormat(getApplicationContext()).format( - PgpKeyHelper.getCreationDate(publicKey))); - mAlgorithm = (TextView) this.findViewById(R.id.algorithm); - mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(publicKey)); - + mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format( + PgpKeyHelper.getCreationDate(mPublicKey))); + mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(mPublicKey)); } private String[] splitUserId(String userId) { diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java index 790ec5ccf..0cf15a451 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java @@ -20,12 +20,15 @@ package org.sufficientlysecure.keychain.ui; import org.spongycastle.openpgp.PGPPublicKeyRing; import org.sufficientlysecure.keychain.Id; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.service.remote.AppSettingsActivity; import org.sufficientlysecure.keychain.ui.adapter.KeyListAdapter; import org.sufficientlysecure.keychain.R; +import android.content.ContentUris; import android.content.Intent; import android.database.Cursor; import android.net.Uri; @@ -114,16 +117,17 @@ public class KeyListPublicFragment extends KeyListFragment implements return true; case 23: - - Intent detailsIntent = new Intent(mKeyListActivity, KeyDetailsActivity.class); - detailsIntent.putExtra("key", ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId)); - startActivity(detailsIntent); + + Intent detailsIntent = new Intent(mKeyListActivity, KeyDetailsActivity.class); + detailsIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long + .toString(keyRingRowId))); + startActivity(detailsIntent); return true; - + case Id.menu.exportToServer: Intent uploadIntent = new Intent(mKeyListActivity, KeyServerUploadActivity.class); uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER); - uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, (int)keyRingRowId); + uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, (int) keyRingRowId); startActivityForResult(uploadIntent, Id.request.export_to_server); return true; -- cgit v1.2.3 From c8d0ff77b10bc5e0103df473076fc7cacadf9014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Wed, 1 Jan 2014 20:29:56 +0100 Subject: show layout not before passphrase is entered --- .../keychain/ui/EditKeyActivity.java | 56 +++++++++++----------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java index 7abee78f3..2995a839a 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java @@ -119,12 +119,6 @@ public class EditKeyActivity extends SherlockFragmentActivity { } }); - setContentView(R.layout.edit_key); - - // find views - mChangePassPhrase = (Button) findViewById(R.id.edit_key_btn_change_pass_phrase); - mNoPassphrase = (CheckBox) findViewById(R.id.edit_key_no_passphrase); - mUserIds = new Vector(); mKeys = new Vector(); mKeysUsages = new Vector(); @@ -138,28 +132,6 @@ public class EditKeyActivity extends SherlockFragmentActivity { handleActionEditKey(intent); } - mChangePassPhrase.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - showSetPassphraseDialog(); - } - }); - - // disable passphrase when no passphrase checkobox is checked! - mNoPassphrase.setOnCheckedChangeListener(new OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (isChecked) { - // remove passphrase - mNewPassPhrase = null; - - mChangePassPhrase.setVisibility(View.GONE); - } else { - mChangePassPhrase.setVisibility(View.VISIBLE); - } - } - }); - if (mBuildLayout) { buildLayout(); } @@ -402,6 +374,12 @@ public class EditKeyActivity extends SherlockFragmentActivity { * id and key. */ private void buildLayout() { + setContentView(R.layout.edit_key); + + // find views + mChangePassPhrase = (Button) findViewById(R.id.edit_key_btn_change_pass_phrase); + mNoPassphrase = (CheckBox) findViewById(R.id.edit_key_no_passphrase); + // Build layout based on given userIds and keys LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); @@ -418,6 +396,28 @@ public class EditKeyActivity extends SherlockFragmentActivity { container.addView(mKeysView); updatePassPhraseButtonText(); + + mChangePassPhrase.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + showSetPassphraseDialog(); + } + }); + + // disable passphrase when no passphrase checkobox is checked! + mNoPassphrase.setOnCheckedChangeListener(new OnCheckedChangeListener() { + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked) { + // remove passphrase + mNewPassPhrase = null; + + mChangePassPhrase.setVisibility(View.GONE); + } else { + mChangePassPhrase.setVisibility(View.VISIBLE); + } + } + }); } private long getMasterKeyId() { -- cgit v1.2.3 From 1d91804dc7943e7149d02141a46c3eb0763e2b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Wed, 1 Jan 2014 22:26:19 +0100 Subject: Simple new list implementations, remove library, use simple adapter with headings --- OpenPGP-Keychain/AndroidManifest.xml | 5 +- OpenPGP-Keychain/project.properties | 1 - OpenPGP-Keychain/res/layout/key_view.xml | 116 ----- OpenPGP-Keychain/res/layout/key_view_activity.xml | 116 +++++ OpenPGP-Keychain/res/menu/key_view.xml | 38 ++ .../service/remote/RegisteredAppsListFragment.java | 8 +- .../keychain/ui/EditKeyActivity.java | 4 +- .../keychain/ui/KeyDetailsActivity.java | 120 ----- .../keychain/ui/KeyListActivity.java | 22 +- .../keychain/ui/KeyListFragment.java | 88 ---- .../keychain/ui/KeyListPublicFragment.java | 174 ++----- .../keychain/ui/KeyListSecretActivity.java | 22 +- .../keychain/ui/KeyListSecretFragment.java | 112 ++--- .../keychain/ui/KeyViewActivity.java | 228 +++++++++ .../keychain/ui/adapter/KeyListPublicAdapter.java | 77 ++++ .../keychain/ui/adapter/KeyListSecretAdapter.java | 82 ++++ .../keychain/util/SectionCursorAdapter.java | 266 +++++++++++ README.md | 11 +- libraries/pinned-section-listview/.gitignore | 6 - libraries/pinned-section-listview/README.md | 75 --- .../example/AndroidManifest.xml | 27 -- .../example/assets/.gitignore | 1 - .../example/ic_launcher-web.png | Bin 45500 -> 0 bytes .../example/libs/android-support-v4.jar | Bin 393154 -> 0 bytes .../example/proguard-project.txt | 20 - .../example/project.properties | 15 - .../example/res/drawable-hdpi/ic_launcher.png | Bin 5819 -> 0 bytes .../example/res/drawable-mdpi/ic_launcher.png | Bin 3479 -> 0 bytes .../example/res/drawable-xhdpi/ic_launcher.png | Bin 7534 -> 0 bytes .../example/res/drawable-xxhdpi/ic_launcher.png | Bin 11732 -> 0 bytes .../example/res/layout/activity_main.xml | 8 - .../example/res/menu/main.xml | 27 -- .../example/res/values-v11/styles.xml | 11 - .../example/res/values-v14/styles.xml | 12 - .../example/res/values/colors.xml | 10 - .../example/res/values/strings.xml | 10 - .../example/res/values/styles.xml | 20 - .../com/hb/examples/PinnedSectionListActivity.java | 285 ------------ .../library/AndroidManifest.xml | 13 - .../library/assets/.gitignore | 1 - .../pinned-section-listview/library/build.gradle | 24 - .../library/proguard-project.txt | 20 - .../library/project.properties | 15 - .../pinned-section-listview/library/res/.gitignore | 1 - .../src/com/hb/views/PinnedSectionListView.java | 513 --------------------- libraries/pinned-section-listview/screen1.png | Bin 23124 -> 0 bytes libraries/pinned-section-listview/screen2.png | Bin 22743 -> 0 bytes libraries/pinned-section-listview/screen3.png | Bin 22885 -> 0 bytes settings.gradle | 3 +- 49 files changed, 919 insertions(+), 1688 deletions(-) delete mode 100644 OpenPGP-Keychain/res/layout/key_view.xml create mode 100644 OpenPGP-Keychain/res/layout/key_view_activity.xml create mode 100644 OpenPGP-Keychain/res/menu/key_view.xml delete mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java delete mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java create mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyViewActivity.java create mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java create mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java create mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/SectionCursorAdapter.java delete mode 100644 libraries/pinned-section-listview/.gitignore delete mode 100644 libraries/pinned-section-listview/README.md delete mode 100644 libraries/pinned-section-listview/example/AndroidManifest.xml delete mode 100644 libraries/pinned-section-listview/example/assets/.gitignore delete mode 100644 libraries/pinned-section-listview/example/ic_launcher-web.png delete mode 100644 libraries/pinned-section-listview/example/libs/android-support-v4.jar delete mode 100644 libraries/pinned-section-listview/example/proguard-project.txt delete mode 100644 libraries/pinned-section-listview/example/project.properties delete mode 100644 libraries/pinned-section-listview/example/res/drawable-hdpi/ic_launcher.png delete mode 100644 libraries/pinned-section-listview/example/res/drawable-mdpi/ic_launcher.png delete mode 100644 libraries/pinned-section-listview/example/res/drawable-xhdpi/ic_launcher.png delete mode 100644 libraries/pinned-section-listview/example/res/drawable-xxhdpi/ic_launcher.png delete mode 100644 libraries/pinned-section-listview/example/res/layout/activity_main.xml delete mode 100644 libraries/pinned-section-listview/example/res/menu/main.xml delete mode 100644 libraries/pinned-section-listview/example/res/values-v11/styles.xml delete mode 100644 libraries/pinned-section-listview/example/res/values-v14/styles.xml delete mode 100644 libraries/pinned-section-listview/example/res/values/colors.xml delete mode 100644 libraries/pinned-section-listview/example/res/values/strings.xml delete mode 100644 libraries/pinned-section-listview/example/res/values/styles.xml delete mode 100644 libraries/pinned-section-listview/example/src/com/hb/examples/PinnedSectionListActivity.java delete mode 100644 libraries/pinned-section-listview/library/AndroidManifest.xml delete mode 100644 libraries/pinned-section-listview/library/assets/.gitignore delete mode 100644 libraries/pinned-section-listview/library/build.gradle delete mode 100644 libraries/pinned-section-listview/library/proguard-project.txt delete mode 100644 libraries/pinned-section-listview/library/project.properties delete mode 100644 libraries/pinned-section-listview/library/res/.gitignore delete mode 100644 libraries/pinned-section-listview/library/src/com/hb/views/PinnedSectionListView.java delete mode 100644 libraries/pinned-section-listview/screen1.png delete mode 100644 libraries/pinned-section-listview/screen2.png delete mode 100644 libraries/pinned-section-listview/screen3.png diff --git a/OpenPGP-Keychain/AndroidManifest.xml b/OpenPGP-Keychain/AndroidManifest.xml index 4b843bc01..be9fe222c 100644 --- a/OpenPGP-Keychain/AndroidManifest.xml +++ b/OpenPGP-Keychain/AndroidManifest.xml @@ -123,11 +123,10 @@ android:uiOptions="splitActionBarWhenNarrow" android:windowSoftInputMode="stateHidden" /> + android:parentActivityName=".ui.KeyListPublicActivity" > diff --git a/OpenPGP-Keychain/project.properties b/OpenPGP-Keychain/project.properties index 7347abfcd..7acfa6b58 100644 --- a/OpenPGP-Keychain/project.properties +++ b/OpenPGP-Keychain/project.properties @@ -11,4 +11,3 @@ target=android-19 android.library.reference.1=../libraries/ActionBarSherlock android.library.reference.2=../libraries/HtmlTextView -android.library.reference.3=../libraries/pinned-section-listview/library diff --git a/OpenPGP-Keychain/res/layout/key_view.xml b/OpenPGP-Keychain/res/layout/key_view.xml deleted file mode 100644 index 326959e97..000000000 --- a/OpenPGP-Keychain/res/layout/key_view.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/key_view_activity.xml b/OpenPGP-Keychain/res/layout/key_view_activity.xml new file mode 100644 index 000000000..326959e97 --- /dev/null +++ b/OpenPGP-Keychain/res/layout/key_view_activity.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/menu/key_view.xml b/OpenPGP-Keychain/res/menu/key_view.xml new file mode 100644 index 000000000..ac5c96c5d --- /dev/null +++ b/OpenPGP-Keychain/res/menu/key_view.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/remote/RegisteredAppsListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/remote/RegisteredAppsListFragment.java index 1b504a374..4c9d553ad 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/remote/RegisteredAppsListFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/service/remote/RegisteredAppsListFragment.java @@ -41,9 +41,6 @@ public class RegisteredAppsListFragment extends SherlockListFragment implements // This is the Adapter being used to display the list's data. RegisteredAppsAdapter mAdapter; - // If non-null, this is the current filter the user has provided. - String mCurFilter; - @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -75,8 +72,7 @@ public class RegisteredAppsListFragment extends SherlockListFragment implements } // These are the Contacts rows that we will retrieve. - static final String[] CONSUMERS_SUMMARY_PROJECTION = new String[] { ApiApps._ID, - ApiApps.PACKAGE_NAME }; + static final String[] PROJECTION = new String[] { ApiApps._ID, ApiApps.PACKAGE_NAME }; public Loader onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This @@ -87,7 +83,7 @@ public class RegisteredAppsListFragment extends SherlockListFragment implements // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), baseUri, CONSUMERS_SUMMARY_PROJECTION, null, null, + return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, ApiApps.PACKAGE_NAME + " COLLATE LOCALIZED ASC"); } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java index 2995a839a..7aeb51c8f 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 Dominik Schürmann + * Copyright (C) 2012-2013 Dominik Schürmann * Copyright (C) 2010 Thialfihar * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -268,7 +268,6 @@ public class EditKeyActivity extends SherlockFragmentActivity { * * @param intent */ - @SuppressWarnings("unchecked") private void handleActionEditKey(Intent intent) { Bundle extras = intent.getExtras(); @@ -291,7 +290,6 @@ public class EditKeyActivity extends SherlockFragmentActivity { finallyEdit(masterKeyId, masterCanSign); } - } } } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java deleted file mode 100644 index 5c8444d80..000000000 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyDetailsActivity.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (C) 2013 Bahtiar 'kalkin' Gadimov - * Copyright (C) 2013 Dominik Schürmann - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui; - -import java.util.Date; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.spongycastle.openpgp.PGPPublicKey; -import org.spongycastle.openpgp.PGPPublicKeyRing; -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; -import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.util.Log; - -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.text.format.DateFormat; -import android.widget.TextView; - -import com.actionbarsherlock.app.SherlockActivity; - -public class KeyDetailsActivity extends SherlockActivity { - private Uri mDataUri; - - private PGPPublicKey mPublicKey; - - private TextView mAlgorithm; - private TextView mFingerint; - private TextView mExpiry; - private TextView mCreation; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeButtonEnabled(true); - - setContentView(R.layout.key_view); - - mFingerint = (TextView) this.findViewById(R.id.fingerprint); - mExpiry = (TextView) this.findViewById(R.id.expiry); - mCreation = (TextView) this.findViewById(R.id.creation); - mAlgorithm = (TextView) this.findViewById(R.id.algorithm); - - Intent intent = getIntent(); - mDataUri = intent.getData(); - if (mDataUri == null) { - Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!"); - finish(); - return; - } else { - Log.d(Constants.TAG, "uri: " + mDataUri); - loadData(mDataUri); - } - } - - private void loadData(Uri dataUri) { - PGPPublicKeyRing ring = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri); - mPublicKey = ring.getPublicKey(); - - mFingerint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper - .convertFingerprintToHex(mPublicKey.getFingerprint()))); - String[] mainUserId = splitUserId(""); - - Date expiryDate = PgpKeyHelper.getExpiryDate(mPublicKey); - if (expiryDate == null) { - mExpiry.setText(""); - } else { - mExpiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(expiryDate)); - } - - mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format( - PgpKeyHelper.getCreationDate(mPublicKey))); - mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(mPublicKey)); - } - - private String[] splitUserId(String userId) { - - String[] result = new String[] { "", "", "" }; - Log.v("UserID", userId); - - Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$"); - Matcher matcher = withComment.matcher(userId); - if (matcher.matches()) { - result[0] = matcher.group(1); - result[1] = matcher.group(2); - result[2] = matcher.group(3); - return result; - } - - Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); - matcher = withoutComment.matcher(userId); - if (matcher.matches()) { - result[0] = matcher.group(1); - result[1] = matcher.group(2); - return result; - } - return result; - } -} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java index 7b844fe08..4d1849029 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListActivity.java @@ -1,18 +1,18 @@ /* - * Copyright (C) 2012 Dominik Schürmann - * Copyright (C) 2010 Thialfihar + * Copyright (C) 2013 Dominik Schürmann * - * Licensed 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 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ package org.sufficientlysecure.keychain.ui; diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java deleted file mode 100644 index 0d61b1108..000000000 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListFragment.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (C) 2012-2013 Dominik Schürmann - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui; - -import org.sufficientlysecure.keychain.Id; -import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.ui.widget.ExpandableListFragment; -import org.sufficientlysecure.keychain.R; - -import android.os.Bundle; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.View; -import android.widget.ExpandableListView; -import android.widget.ExpandableListView.ExpandableListContextMenuInfo; - -public class KeyListFragment extends ExpandableListFragment { - protected KeyListActivity mKeyListActivity; - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - mKeyListActivity = (KeyListActivity) getActivity(); - - // register long press context menu - registerForContextMenu(getListView()); - - // Give some text to display if there is no data. In a real - // application this would come from a resource. - setEmptyText(getString(R.string.list_empty)); - } - - /** - * Context Menu on Long Click - */ - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - menu.add(0, Id.menu.export, 5, R.string.menu_export_key); - menu.add(0, Id.menu.delete, 111, R.string.menu_delete_key); - } - - @Override - public boolean onContextItemSelected(android.view.MenuItem item) { - ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo(); - - // expInfo.id would also return row id of childs, but we always want to get the row id of - // the group item, thus we are using the following way - int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition); - long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition); - - switch (item.getItemId()) { - case Id.menu.export: - long masterKeyId = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId); - if (masterKeyId == -1) { - masterKeyId = ProviderHelper.getSecretMasterKeyId(mKeyListActivity, keyRingRowId); - } - - mKeyListActivity.showExportKeysDialog(masterKeyId); - return true; - - case Id.menu.delete: - mKeyListActivity.showDeleteKeyDialog(keyRingRowId); - return true; - - default: - return super.onContextItemSelected(item); - - } - } - -} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java index 0cf15a451..3dafc84ca 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2013 Dominik Schürmann + * Copyright (C) 2013 Dominik Schürmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,37 +17,32 @@ package org.sufficientlysecure.keychain.ui; -import org.spongycastle.openpgp.PGPPublicKeyRing; import org.sufficientlysecure.keychain.Id; -import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.provider.KeychainContract; -import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; -import org.sufficientlysecure.keychain.service.remote.AppSettingsActivity; -import org.sufficientlysecure.keychain.ui.adapter.KeyListAdapter; -import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.ui.adapter.KeyListPublicAdapter; -import android.content.ContentUris; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; +import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; -import android.support.v4.app.LoaderManager; -import android.view.ContextMenu; import android.view.View; -import android.view.ContextMenu.ContextMenuInfo; -import android.widget.ExpandableListView; -import android.widget.ExpandableListView.ExpandableListContextMenuInfo; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; + +import com.actionbarsherlock.app.SherlockListFragment; -public class KeyListPublicFragment extends KeyListFragment implements +public class KeyListPublicFragment extends SherlockListFragment implements LoaderManager.LoaderCallbacks { private KeyListPublicActivity mKeyListPublicActivity; - private KeyListAdapter mAdapter; + private KeyListPublicAdapter mAdapter; /** * Define Adapter and Loader on create of Activity @@ -58,135 +53,34 @@ public class KeyListPublicFragment extends KeyListFragment implements mKeyListPublicActivity = (KeyListPublicActivity) getActivity(); - mAdapter = new KeyListAdapter(mKeyListPublicActivity, null, Id.type.public_key); - setListAdapter(mAdapter); - - // Start out with a progress indicator. - setListShown(false); - - // Prepare the loader. Either re-connect with an existing one, - // or start a new one. - // id is -1 as the child cursors are numbered 0,...,n - getLoaderManager().initLoader(-1, null, this); - } - - /** - * Context Menu on Long Click - */ - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - menu.add(0, 23, 1, R.string.title_key_details); // :TODO: Fix magic number - menu.add(0, Id.menu.update, 2, R.string.menu_update_key); - menu.add(0, Id.menu.signKey, 3, R.string.menu_sign_key); - menu.add(0, Id.menu.exportToServer, 4, R.string.menu_export_key_to_server); - menu.add(0, Id.menu.share, 6, R.string.menu_share); - menu.add(0, Id.menu.share_qr_code, 7, R.string.menu_share_qr_code); - menu.add(0, Id.menu.share_nfc, 8, R.string.menu_share_nfc); - - } - - @Override - public boolean onContextItemSelected(android.view.MenuItem item) { - ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo(); - - // expInfo.id would also return row id of childs, but we always want to get the row id of - // the group item, thus we are using the following way - int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition); - long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition); - - switch (item.getItemId()) { - case Id.menu.update: - long updateKeyId = 0; - PGPPublicKeyRing updateKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId( - mKeyListActivity, keyRingRowId); - if (updateKeyRing != null) { - updateKeyId = PgpKeyHelper.getMasterKey(updateKeyRing).getKeyID(); + getListView().setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + // start key view on click + Intent detailsIntent = new Intent(mKeyListPublicActivity, KeyViewActivity.class); + detailsIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long + .toString(id))); + startActivity(detailsIntent); } - if (updateKeyId == 0) { - // this shouldn't happen - return true; - } - - Intent queryIntent = new Intent(mKeyListActivity, KeyServerQueryActivity.class); - queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID_AND_RETURN); - queryIntent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, updateKeyId); + }); - // TODO: lookup?? - startActivityForResult(queryIntent, Id.request.look_up_key_id); + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText(getString(R.string.list_empty)); - return true; - case 23: + // We have a menu item to show in action bar. + setHasOptionsMenu(true); - Intent detailsIntent = new Intent(mKeyListActivity, KeyDetailsActivity.class); - detailsIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long - .toString(keyRingRowId))); - startActivity(detailsIntent); - return true; - - case Id.menu.exportToServer: - Intent uploadIntent = new Intent(mKeyListActivity, KeyServerUploadActivity.class); - uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER); - uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, (int) keyRingRowId); - startActivityForResult(uploadIntent, Id.request.export_to_server); - - return true; - - case Id.menu.signKey: - long keyId = 0; - PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId( - mKeyListActivity, keyRingRowId); - if (signKeyRing != null) { - keyId = PgpKeyHelper.getMasterKey(signKeyRing).getKeyID(); - } - if (keyId == 0) { - // this shouldn't happen - return true; - } - - Intent signIntent = new Intent(mKeyListActivity, SignKeyActivity.class); - signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, keyId); - startActivity(signIntent); - - return true; - - case Id.menu.share_qr_code: - // get master key id using row id - long masterKeyId = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId); - - Intent qrCodeIntent = new Intent(mKeyListActivity, ShareActivity.class); - qrCodeIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING_WITH_QR_CODE); - qrCodeIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId); - startActivityForResult(qrCodeIntent, 0); - - return true; - - case Id.menu.share_nfc: - // get master key id using row id - long masterKeyId2 = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId); - - Intent nfcIntent = new Intent(mKeyListActivity, ShareNfcBeamActivity.class); - nfcIntent.setAction(ShareNfcBeamActivity.ACTION_SHARE_KEYRING_WITH_NFC); - nfcIntent.putExtra(ShareNfcBeamActivity.EXTRA_MASTER_KEY_ID, masterKeyId2); - startActivityForResult(nfcIntent, 0); - - return true; - - case Id.menu.share: - // get master key id using row id - long masterKeyId3 = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId); - - Intent shareIntent = new Intent(mKeyListActivity, ShareActivity.class); - shareIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING); - shareIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId3); - startActivityForResult(shareIntent, 0); - - return true; + // Start out with a progress indicator. + setListShown(false); - default: - return super.onContextItemSelected(item); + // Create an empty adapter we will use to display the loaded data. + mAdapter = new KeyListPublicAdapter(mKeyListPublicActivity, null, Id.type.public_key); + setListAdapter(mAdapter); - } + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); } // These are the rows that we will retrieve. @@ -210,7 +104,7 @@ public class KeyListPublicFragment extends KeyListFragment implements public void onLoadFinished(Loader loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) - mAdapter.setGroupCursor(data); + mAdapter.swapCursor(data); // The list should now be shown. if (isResumed()) { @@ -225,7 +119,7 @@ public class KeyListPublicFragment extends KeyListFragment implements // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. - mAdapter.setGroupCursor(null); + mAdapter.swapCursor(null); } } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java index 822c73872..d98b5ab37 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretActivity.java @@ -1,18 +1,18 @@ /* - * Copyright (C) 2012 Dominik Schürmann - * Copyright (C) 2010 Thialfihar + * Copyright (C) 2013 Dominik Schürmann * - * Licensed 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 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ package org.sufficientlysecure.keychain.ui; diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java index 4bbf3d6ef..e8a306063 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java @@ -1,18 +1,18 @@ /* - * Copyright (C) 2012 Dominik Schürmann - * Copyright (C) 2010 Thialfihar + * Copyright (C) 2013 Dominik Schürmann * - * Licensed 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 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program 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. * - * 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. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . */ package org.sufficientlysecure.keychain.ui; @@ -22,7 +22,7 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.ui.adapter.KeyListAdapter; +import org.sufficientlysecure.keychain.ui.adapter.KeyListSecretAdapter; import android.database.Cursor; import android.net.Uri; @@ -30,18 +30,18 @@ import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; import android.view.View; -import android.widget.ExpandableListView; -import android.widget.ExpandableListView.ExpandableListContextMenuInfo; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; -public class KeyListSecretFragment extends KeyListFragment implements +import com.actionbarsherlock.app.SherlockListFragment; + +public class KeyListSecretFragment extends SherlockListFragment implements LoaderManager.LoaderCallbacks { private KeyListSecretActivity mKeyListSecretActivity; - private KeyListAdapter mAdapter; + private KeyListSecretAdapter mAdapter; /** * Define Adapter and Loader on create of Activity @@ -52,53 +52,38 @@ public class KeyListSecretFragment extends KeyListFragment implements mKeyListSecretActivity = (KeyListSecretActivity) getActivity(); - mAdapter = new KeyListAdapter(mKeyListSecretActivity, null, Id.type.secret_key); - setListAdapter(mAdapter); + getListView().setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + // TODO: currently this is EDIT directly, later first show VIEW - // Start out with a progress indicator. - setListShown(false); + // get master key id using row id + long masterKeyId = ProviderHelper.getSecretMasterKeyId(mKeyListSecretActivity, id); - // Prepare the loader. Either re-connect with an existing one, - // or start a new one. - // id is -1 as the child cursors are numbered 0,...,n - getLoaderManager().initLoader(-1, null, this); - } + boolean masterCanSign = ProviderHelper.getSecretMasterKeyCanSign( + mKeyListSecretActivity, id); - /** - * Context Menu on Long Click - */ - @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { - super.onCreateContextMenu(menu, v, menuInfo); - menu.add(0, Id.menu.edit, 0, R.string.menu_edit_key); - } - - @Override - public boolean onContextItemSelected(android.view.MenuItem item) { - ExpandableListContextMenuInfo expInfo = (ExpandableListContextMenuInfo) item.getMenuInfo(); + mKeyListSecretActivity.editKey(masterKeyId, masterCanSign); + } + }); - // expInfo.id would also return row id of childs, but we always want to get the row id of - // the group item, thus we are using the following way - int groupPosition = ExpandableListView.getPackedPositionGroup(expInfo.packedPosition); - long keyRingRowId = getExpandableListAdapter().getGroupId(groupPosition); + // Give some text to display if there is no data. In a real + // application this would come from a resource. + setEmptyText(getString(R.string.list_empty)); - // get master key id using row id - long masterKeyId = ProviderHelper - .getSecretMasterKeyId(mKeyListSecretActivity, keyRingRowId); + // We have a menu item to show in action bar. + setHasOptionsMenu(true); - boolean masterCanSign = ProviderHelper.getSecretMasterKeyCanSign(mKeyListSecretActivity, - keyRingRowId); - - switch (item.getItemId()) { - case Id.menu.edit: - mKeyListSecretActivity.editKey(masterKeyId, masterCanSign); - - return true; + // Start out with a progress indicator. + setListShown(false); - default: - return super.onContextItemSelected(item); + // Create an empty adapter we will use to display the loaded data. + mAdapter = new KeyListSecretAdapter(mKeyListSecretActivity, null, Id.type.secret_key); + setListAdapter(mAdapter); - } + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); } // These are the rows that we will retrieve. @@ -107,22 +92,23 @@ public class KeyListSecretFragment extends KeyListFragment implements static final String SORT_ORDER = UserIds.USER_ID + " ASC"; - @Override public Loader onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This // sample only has one Loader, so we don't care about the ID. + // First, pick the base URI to use depending on whether we are + // currently filtering. Uri baseUri = KeyRings.buildSecretKeyRingsUri(); // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, SORT_ORDER); + return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, UserIds.USER_ID + + " COLLATE LOCALIZED ASC"); } - @Override public void onLoadFinished(Loader loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) - mAdapter.setGroupCursor(data); + mAdapter.swapCursor(data); // The list should now be shown. if (isResumed()) { @@ -132,12 +118,10 @@ public class KeyListSecretFragment extends KeyListFragment implements } } - @Override public void onLoaderReset(Loader loader) { // This is called when the last Cursor provided to onLoadFinished() // above is about to be closed. We need to make sure we are no // longer using it. - mAdapter.setGroupCursor(null); + mAdapter.swapCursor(null); } - } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyViewActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyViewActivity.java new file mode 100644 index 000000000..c2ef64d51 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyViewActivity.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2013 Bahtiar 'kalkin' Gadimov + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.util.Log; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.provider.ContactsContract.DataUsageFeedback; +import android.text.format.DateFormat; +import android.widget.TextView; + +import com.actionbarsherlock.app.SherlockActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; + +public class KeyViewActivity extends SherlockActivity { + private Uri mDataUri; + + private PGPPublicKey mPublicKey; + + private TextView mAlgorithm; + private TextView mFingerint; + private TextView mExpiry; + private TextView mCreation; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + setContentView(R.layout.key_view_activity); + + mFingerint = (TextView) this.findViewById(R.id.fingerprint); + mExpiry = (TextView) this.findViewById(R.id.expiry); + mCreation = (TextView) this.findViewById(R.id.creation); + mAlgorithm = (TextView) this.findViewById(R.id.algorithm); + + Intent intent = getIntent(); + mDataUri = intent.getData(); + if (mDataUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of app!"); + finish(); + return; + } else { + Log.d(Constants.TAG, "uri: " + mDataUri); + loadData(mDataUri); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getSupportMenuInflater().inflate(R.menu.key_view, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // TODO: use data uri in the other activities instead of givin key ring row id! + + long keyRingRowId = Long.valueOf(mDataUri.getLastPathSegment()); + + switch (item.getItemId()) { + case R.id.menu_key_view_update: + long updateKeyId = 0; + PGPPublicKeyRing updateKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId(this, + keyRingRowId); + if (updateKeyRing != null) { + updateKeyId = PgpKeyHelper.getMasterKey(updateKeyRing).getKeyID(); + } + if (updateKeyId == 0) { + // this shouldn't happen + return true; + } + + Intent queryIntent = new Intent(this, KeyServerQueryActivity.class); + queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID_AND_RETURN); + queryIntent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, updateKeyId); + + // TODO: lookup?? + startActivityForResult(queryIntent, Id.request.look_up_key_id); + + return true; + case R.id.menu_key_view_sign: + long keyId = 0; + PGPPublicKeyRing signKeyRing = ProviderHelper.getPGPPublicKeyRingByRowId(this, + keyRingRowId); + if (signKeyRing != null) { + keyId = PgpKeyHelper.getMasterKey(signKeyRing).getKeyID(); + } + if (keyId == 0) { + // this shouldn't happen + return true; + } + + Intent signIntent = new Intent(this, SignKeyActivity.class); + signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, keyId); + startActivity(signIntent); + return true; + case R.id.menu_key_view_export_keyserver: + Intent uploadIntent = new Intent(this, KeyServerUploadActivity.class); + uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER); + uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, (int) keyRingRowId); + startActivityForResult(uploadIntent, Id.request.export_to_server); + + return true; + case R.id.menu_key_view_export_file: +// long masterKeyId = ProviderHelper.getPublicMasterKeyId(mKeyListActivity, keyRingRowId); +// if (masterKeyId == -1) { +// masterKeyId = ProviderHelper.getSecretMasterKeyId(mKeyListActivity, keyRingRowId); +// } +// +// mKeyListActivity.showExportKeysDialog(masterKeyId); + return true; + case R.id.menu_key_view_share: + // get master key id using row id + long masterKeyId3 = ProviderHelper.getPublicMasterKeyId(this, keyRingRowId); + + Intent shareIntent = new Intent(this, ShareActivity.class); + shareIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING); + shareIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId3); + startActivityForResult(shareIntent, 0); + + return true; + case R.id.menu_key_view_share_qr_code: + // get master key id using row id + long masterKeyId = ProviderHelper.getPublicMasterKeyId(this, keyRingRowId); + + Intent qrCodeIntent = new Intent(this, ShareActivity.class); + qrCodeIntent.setAction(ShareActivity.ACTION_SHARE_KEYRING_WITH_QR_CODE); + qrCodeIntent.putExtra(ShareActivity.EXTRA_MASTER_KEY_ID, masterKeyId); + startActivityForResult(qrCodeIntent, 0); + + return true; + case R.id.menu_key_view_share_nfc: + // get master key id using row id + long masterKeyId2 = ProviderHelper.getPublicMasterKeyId(this, keyRingRowId); + + Intent nfcIntent = new Intent(this, ShareNfcBeamActivity.class); + nfcIntent.setAction(ShareNfcBeamActivity.ACTION_SHARE_KEYRING_WITH_NFC); + nfcIntent.putExtra(ShareNfcBeamActivity.EXTRA_MASTER_KEY_ID, masterKeyId2); + startActivityForResult(nfcIntent, 0); + + return true; + case R.id.menu_key_view_delete: +// mKeyListActivity.showDeleteKeyDialog(keyRingRowId); + + return true; + + } + return super.onOptionsItemSelected(item); + } + + private void loadData(Uri dataUri) { + PGPPublicKeyRing ring = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri); + mPublicKey = ring.getPublicKey(); + + mFingerint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper + .convertFingerprintToHex(mPublicKey.getFingerprint()))); + String[] mainUserId = splitUserId(""); + + Date expiryDate = PgpKeyHelper.getExpiryDate(mPublicKey); + if (expiryDate == null) { + mExpiry.setText(""); + } else { + mExpiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(expiryDate)); + } + + mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format( + PgpKeyHelper.getCreationDate(mPublicKey))); + mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(mPublicKey)); + } + + private String[] splitUserId(String userId) { + + String[] result = new String[] { "", "", "" }; + Log.v("UserID", userId); + + Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$"); + Matcher matcher = withComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(2); + result[2] = matcher.group(3); + return result; + } + + Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); + matcher = withoutComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(2); + return result; + } + return result; + } +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java new file mode 100644 index 000000000..d72c9d42a --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; +import org.sufficientlysecure.keychain.util.SectionCursorAdapter; + +import android.content.Context; +import android.database.Cursor; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class KeyListPublicAdapter extends SectionCursorAdapter { + + private LayoutInflater mInflater; + + public KeyListPublicAdapter(Context context, Cursor c, int flags) { + super(context, c, android.R.layout.preference_category, 2); // TODO: 2 is user id + + mInflater = LayoutInflater.from(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + int userIdIndex = cursor.getColumnIndex(UserIds.USER_ID); + + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + mainUserId.setText(R.string.unknown_user_id); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + mainUserIdRest.setText(""); + + String userId = cursor.getString(userIdIndex); + if (userId != null) { + String[] userIdSplit = OtherHelper.splitUserId(userId); + + if (userIdSplit[1] != null) { + mainUserIdRest.setText(userIdSplit[1]); + } + mainUserId.setText(userIdSplit[0]); + } + + if (mainUserId.getText().length() == 0) { + mainUserId.setText(R.string.unknown_user_id); + } + + if (mainUserIdRest.getText().length() == 0) { + mainUserIdRest.setVisibility(View.GONE); + } else { + mainUserIdRest.setVisibility(View.VISIBLE); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.key_list_group_item, null); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java new file mode 100644 index 000000000..6f3129e4f --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.ApiApps; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +public class KeyListSecretAdapter extends CursorAdapter { + + private LayoutInflater mInflater; + + public KeyListSecretAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + int userIdIndex = cursor.getColumnIndex(UserIds.USER_ID); + + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + mainUserId.setText(R.string.unknown_user_id); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + mainUserIdRest.setText(""); + + String userId = cursor.getString(userIdIndex); + if (userId != null) { + String[] userIdSplit = OtherHelper.splitUserId(userId); + + if (userIdSplit[1] != null) { + mainUserIdRest.setText(userIdSplit[1]); + } + mainUserId.setText(userIdSplit[0]); + } + + if (mainUserId.getText().length() == 0) { + mainUserId.setText(R.string.unknown_user_id); + } + + if (mainUserIdRest.getText().length() == 0) { + mainUserIdRest.setVisibility(View.GONE); + } else { + mainUserIdRest.setVisibility(View.VISIBLE); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.key_list_group_item, null); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/SectionCursorAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/SectionCursorAdapter.java new file mode 100644 index 000000000..0d4092d9e --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/util/SectionCursorAdapter.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * Copyright (C) 2011 Gonçalo Ferreira + * + * Licensed 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. + */ + +package org.sufficientlysecure.keychain.util; + +import java.util.LinkedHashMap; + +import android.content.Context; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.support.v4.widget.CursorAdapter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +/** + * Originally from https://github.com/monxalo/android-section-adapter + * + * getCustomGroup() has been modified + */ +public abstract class SectionCursorAdapter extends CursorAdapter { + + private static final String TAG = "SectionCursorAdapter"; + private static final boolean LOGV = false; + + private static final int TYPE_NORMAL = 1; + private static final int TYPE_HEADER = 0; + private static final int TYPE_COUNT = 2; + + private final int mHeaderRes; + private final int mGroupColumn; + private final LayoutInflater mLayoutInflater; + + private LinkedHashMap sectionsIndexer; + + public static class ViewHolder { + public TextView textView; + } + + public SectionCursorAdapter(Context context, Cursor c, int headerLayout, int groupColumn) { + super(context, c, 0); + + sectionsIndexer = new LinkedHashMap(); + + mHeaderRes = headerLayout; + mGroupColumn = groupColumn; + mLayoutInflater = LayoutInflater.from(context); + + if (c != null) { + calculateSectionHeaders(); + c.registerDataSetObserver(mDataSetObserver); + } + } + + private DataSetObserver mDataSetObserver = new DataSetObserver() { + public void onChanged() { + calculateSectionHeaders(); + }; + + public void onInvalidated() { + sectionsIndexer.clear(); + }; + }; + + /** + *

+ * This method serve as an intercepter before the sections are calculated so you can transform + * some computer data into human readable, e.g. format a unix timestamp, or a status. + *

+ * + *

+ * By default this method returns the original data for the group column. + *

+ * + * @param groupData + * @return + */ + protected String getCustomGroup(String groupData) { + return groupData.substring(0, 1); + } + + private void calculateSectionHeaders() { + int i = 0; + + String previous = ""; + int count = 0; + + final Cursor c = getCursor(); + + sectionsIndexer.clear(); + + if (c == null) { + return; + } + + c.moveToPosition(-1); + + while (c.moveToNext()) { + final String group = getCustomGroup(c.getString(mGroupColumn)); + + if (!previous.equals(group)) { + sectionsIndexer.put(i + count, group); + previous = group; + + if (LOGV) + Log.v(TAG, "Group " + group + "at position: " + (i + count)); + + count++; + } + + i++; + } + } + + public String getGroupCustomFormat(Object obj) { + return null; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + int viewType = getItemViewType(position); + + if (viewType == TYPE_NORMAL) { + Cursor c = (Cursor) getItem(position); + + if (c == null) { + if (LOGV) + Log.d(TAG, "getItem(" + position + ") = null"); + return mLayoutInflater.inflate(mHeaderRes, parent, false); + } + + final int mapCursorPos = getSectionForPosition(position); + c.moveToPosition(mapCursorPos); + + return super.getView(mapCursorPos, convertView, parent); + } else { + ViewHolder holder = null; + + if (convertView == null) { + if (LOGV) + Log.v(TAG, "Creating new view for section"); + + holder = new ViewHolder(); + convertView = mLayoutInflater.inflate(mHeaderRes, parent, false); + holder.textView = (TextView) convertView; + + convertView.setTag(holder); + } else { + if (LOGV) + Log.v(TAG, "Reusing view for section"); + + holder = (ViewHolder) convertView.getTag(); + } + + TextView sectionText = holder.textView; + + final String group = sectionsIndexer.get(position); + final String customFormat = getGroupCustomFormat(group); + + sectionText.setText(customFormat == null ? group : customFormat); + + return sectionText; + } + } + + @Override + public int getViewTypeCount() { + return TYPE_COUNT; + } + + @Override + public int getCount() { + return super.getCount() + sectionsIndexer.size(); + } + + @Override + public boolean isEnabled(int position) { + return getItemViewType(position) == TYPE_NORMAL; + } + + public int getPositionForSection(int section) { + if (sectionsIndexer.containsKey(section)) { + return section + 1; + } + return section; + } + + public int getSectionForPosition(int position) { + int offset = 0; + for (Integer key : sectionsIndexer.keySet()) { + if (position > key) { + offset++; + } else { + break; + } + } + + return position - offset; + } + + @Override + public Object getItem(int position) { + if (getItemViewType(position) == TYPE_NORMAL) { + return super.getItem(getSectionForPosition(position)); + } + return super.getItem(position); + } + + @Override + public long getItemId(int position) { + if (getItemViewType(position) == TYPE_NORMAL) { + return super.getItemId(getSectionForPosition(position)); + } + return super.getItemId(position); + } + + @Override + public int getItemViewType(int position) { + if (position == getPositionForSection(position)) { + return TYPE_NORMAL; + } + return TYPE_HEADER; + } + + @Override + public void changeCursor(Cursor cursor) { + final Cursor old = swapCursor(cursor); + + if (old != null) { + old.close(); + } + } + + @Override + public Cursor swapCursor(Cursor newCursor) { + if (getCursor() != null) { + getCursor().unregisterDataSetObserver(mDataSetObserver); + } + + final Cursor oldCursor = super.swapCursor(newCursor); + + calculateSectionHeaders(); + + if (newCursor != null) { + newCursor.registerDataSetObserver(mDataSetObserver); + } + + return oldCursor; + } +} \ No newline at end of file diff --git a/README.md b/README.md index 0bea8d377..80b34ab05 100644 --- a/README.md +++ b/README.md @@ -107,13 +107,6 @@ TODO # Libraries -All JAR-Libraries are provided in this repository under "libs", all Android Library projects are under "libraries". - -* ActionBarSherlock to provide an ActionBar for Android < 3.0 -* HtmlTextView for non-crashing TextViews with HTML content -* forked Spongy Castle Crypto Lib (Android version of Bouncy Castle) -* barcodescanner-android-integration-supportv4.jar: Barcode Scanner Integration - ## Build Barcode Scanner Integration 1. Checkout their SVN (see http://code.google.com/p/zxing/source/checkout) @@ -212,8 +205,8 @@ Some parts (older parts and some libraries are Apache License v2, MIT X11 Licens http://code.google.com/p/zxing/ Apache License v2 -* pinned-section-listview - https://github.com/beworker/pinned-section-listview +* android-section-adapter + https://github.com/monxalo/android-section-adapter Apache License v2 diff --git a/libraries/pinned-section-listview/.gitignore b/libraries/pinned-section-listview/.gitignore deleted file mode 100644 index ba354bcae..000000000 --- a/libraries/pinned-section-listview/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -bin -gen -.project -.classpath -.settings -.DS_Store diff --git a/libraries/pinned-section-listview/README.md b/libraries/pinned-section-listview/README.md deleted file mode 100644 index e89cde3bb..000000000 --- a/libraries/pinned-section-listview/README.md +++ /dev/null @@ -1,75 +0,0 @@ -Introduction -============ - -Easy to use ListView with pinned sections for Android. Pinned section is a header view which sticks to the top -of the list until at least one item of that section is visible. - -![Alt text](screen1.png)  -![Alt text](screen2.png)  -![Alt text](screen3.png) - -Features -======== -This list properly implements many features which are missing from other implementations. These are - * Fast scroll - * Headers and footers - * Clickable pinned sections - -Besides this it doesn't create any unnecessary views, layouts etc. It's really lean. - -Watch [this video][1] to see `PinnedSectionListView` in action. - -Usage -===== - 1. Replace standard `ListView` with `com.hb.views.PinnedSectionListView` in your `layout.xml` file. - - - - 2. Extend your `ListAdapter` in a way that it implements `PinnedSectionListAdapter` interface, in addition to - what it already implements. Basically you need to add a single `isItemViewTypePinned(int viewType)` - method. This method must return `true` for all view types which have to be pinned. - - // Our adapter class implements 'PinnedSectionListAdapter' interface - class MyPinnedSectionListAdapter extends BaseAdapter implements PinnedSectionListAdapter { - - ... - - // We implement this method to return 'true' for all view types we want to pin - @Override - public boolean isItemViewTypePinned(int viewType) { - return viewType == ; - } - } - -That's all. You are done! A working example can also be found in `example` folder. - -Used by -======= -Let us know if you use this library in your application and we will mention it here. - -[Grocery Sum][2] - -License -======= - - Copyright 2013 Sergej Shafarenka, halfbit.de - - Licensed 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. - - -[1]: http://www.youtube.com/watch?v=mI3DpuoIIhQ -[2]: https://play.google.com/store/apps/details?id=org.codechimp.grocerysum diff --git a/libraries/pinned-section-listview/example/AndroidManifest.xml b/libraries/pinned-section-listview/example/AndroidManifest.xml deleted file mode 100644 index 56621893d..000000000 --- a/libraries/pinned-section-listview/example/AndroidManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/assets/.gitignore b/libraries/pinned-section-listview/example/assets/.gitignore deleted file mode 100644 index fdffa2a0f..000000000 --- a/libraries/pinned-section-listview/example/assets/.gitignore +++ /dev/null @@ -1 +0,0 @@ -# placeholder diff --git a/libraries/pinned-section-listview/example/ic_launcher-web.png b/libraries/pinned-section-listview/example/ic_launcher-web.png deleted file mode 100644 index 8bd40412e..000000000 Binary files a/libraries/pinned-section-listview/example/ic_launcher-web.png and /dev/null differ diff --git a/libraries/pinned-section-listview/example/libs/android-support-v4.jar b/libraries/pinned-section-listview/example/libs/android-support-v4.jar deleted file mode 100644 index 65ebaf8dc..000000000 Binary files a/libraries/pinned-section-listview/example/libs/android-support-v4.jar and /dev/null differ diff --git a/libraries/pinned-section-listview/example/proguard-project.txt b/libraries/pinned-section-listview/example/proguard-project.txt deleted file mode 100644 index f2fe1559a..000000000 --- a/libraries/pinned-section-listview/example/proguard-project.txt +++ /dev/null @@ -1,20 +0,0 @@ -# To enable ProGuard in your project, edit project.properties -# to define the proguard.config property as described in that file. -# -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in ${sdk.dir}/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the ProGuard -# include property in project.properties. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/libraries/pinned-section-listview/example/project.properties b/libraries/pinned-section-listview/example/project.properties deleted file mode 100644 index 1561d7a9a..000000000 --- a/libraries/pinned-section-listview/example/project.properties +++ /dev/null @@ -1,15 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system edit -# "ant.properties", and override values to adapt the script to your -# project structure. -# -# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): -#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt - -# Project target. -target=android-17 -android.library.reference.1=../library diff --git a/libraries/pinned-section-listview/example/res/drawable-hdpi/ic_launcher.png b/libraries/pinned-section-listview/example/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index 3841c3408..000000000 Binary files a/libraries/pinned-section-listview/example/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/libraries/pinned-section-listview/example/res/drawable-mdpi/ic_launcher.png b/libraries/pinned-section-listview/example/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index e65953cce..000000000 Binary files a/libraries/pinned-section-listview/example/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/libraries/pinned-section-listview/example/res/drawable-xhdpi/ic_launcher.png b/libraries/pinned-section-listview/example/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 4ea093d86..000000000 Binary files a/libraries/pinned-section-listview/example/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/libraries/pinned-section-listview/example/res/drawable-xxhdpi/ic_launcher.png b/libraries/pinned-section-listview/example/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 87e928a35..000000000 Binary files a/libraries/pinned-section-listview/example/res/drawable-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/libraries/pinned-section-listview/example/res/layout/activity_main.xml b/libraries/pinned-section-listview/example/res/layout/activity_main.xml deleted file mode 100644 index b641f4c80..000000000 --- a/libraries/pinned-section-listview/example/res/layout/activity_main.xml +++ /dev/null @@ -1,8 +0,0 @@ - \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/menu/main.xml b/libraries/pinned-section-listview/example/res/menu/main.xml deleted file mode 100644 index 091b6c770..000000000 --- a/libraries/pinned-section-listview/example/res/menu/main.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values-v11/styles.xml b/libraries/pinned-section-listview/example/res/values-v11/styles.xml deleted file mode 100644 index 541752f6e..000000000 --- a/libraries/pinned-section-listview/example/res/values-v11/styles.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values-v14/styles.xml b/libraries/pinned-section-listview/example/res/values-v14/styles.xml deleted file mode 100644 index f20e01501..000000000 --- a/libraries/pinned-section-listview/example/res/values-v14/styles.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values/colors.xml b/libraries/pinned-section-listview/example/res/values/colors.xml deleted file mode 100644 index b4da27c93..000000000 --- a/libraries/pinned-section-listview/example/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #ffff4444 - #ff99cc00 - #ffffbb33 - #ff33b5e5 - - - - \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values/strings.xml b/libraries/pinned-section-listview/example/res/values/strings.xml deleted file mode 100644 index e9d43a4aa..000000000 --- a/libraries/pinned-section-listview/example/res/values/strings.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - Pinned Section Demo - Fast scroll - Add padding - Show shadow - Show Header & Footer - - \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/res/values/styles.xml b/libraries/pinned-section-listview/example/res/values/styles.xml deleted file mode 100644 index 4a10ca492..000000000 --- a/libraries/pinned-section-listview/example/res/values/styles.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/libraries/pinned-section-listview/example/src/com/hb/examples/PinnedSectionListActivity.java b/libraries/pinned-section-listview/example/src/com/hb/examples/PinnedSectionListActivity.java deleted file mode 100644 index 223718a06..000000000 --- a/libraries/pinned-section-listview/example/src/com/hb/examples/PinnedSectionListActivity.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * Copyright (C) 2013 Sergej Shafarenka, halfbit.de - * - * Licensed 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. - */ - -package com.hb.examples; - -import java.util.Locale; - -import android.annotation.SuppressLint; -import android.app.ListActivity; -import android.content.Context; -import android.graphics.Color; -import android.os.Build; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.SectionIndexer; -import android.widget.TextView; -import android.widget.Toast; - -import com.hb.examples.pinnedsection.R; -import com.hb.views.PinnedSectionListView; -import com.hb.views.PinnedSectionListView.PinnedSectionListAdapter; - -public class PinnedSectionListActivity extends ListActivity implements OnClickListener { - - static class SimpleAdapter extends ArrayAdapter implements PinnedSectionListAdapter { - - private static final int[] COLORS = new int[] { - R.color.green_light, R.color.orange_light, - R.color.blue_light, R.color.red_light }; - - public SimpleAdapter(Context context, int resource, int textViewResourceId) { - super(context, resource, textViewResourceId); - - final int sectionsNumber = 'Z' - 'A' + 1; - prepareSections(sectionsNumber); - - int sectionPosition = 0, listPosition = 0; - for (char i=0; i= sections.length) { - section = sections.length - 1; - } - return sections[section].listPosition; - } - - @Override public int getSectionForPosition(int position) { - if (position >= getCount()) { - position = getCount() - 1; - } - return getItem(position).sectionPosition; - } - - } - - static class Item { - - public static final int ITEM = 0; - public static final int SECTION = 1; - - public final int type; - public final String text; - - public int sectionPosition; - public int listPosition; - - public Item(int type, String text) { - this.type = type; - this.text = text; - } - - @Override public String toString() { - return text; - } - - } - - private boolean hasHeaderAndFooter; - private boolean isFastScroll; - private boolean addPadding; - private boolean isShadowVisible = true; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - if (savedInstanceState != null) { - isFastScroll = savedInstanceState.getBoolean("isFastScroll"); - addPadding = savedInstanceState.getBoolean("addPadding"); - isShadowVisible = savedInstanceState.getBoolean("isShadowVisible"); - hasHeaderAndFooter = savedInstanceState.getBoolean("hasHeaderAndFooter"); - } - initializeHeaderAndFooter(); - initializeAdapter(); - initializePadding(); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean("isFastScroll", isFastScroll); - outState.putBoolean("addPadding", addPadding); - outState.putBoolean("isShadowVisible", isShadowVisible); - outState.putBoolean("hasHeaderAndFooter", hasHeaderAndFooter); - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - Item item = (Item) getListView().getAdapter().getItem(position); - if (item != null) { - Toast.makeText(this, "Item " + position + ": " + item.text, Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(this, "Item " + position, Toast.LENGTH_SHORT).show(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.main, menu); - menu.getItem(0).setChecked(isFastScroll); - menu.getItem(1).setChecked(addPadding); - menu.getItem(2).setChecked(isShadowVisible); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_fastscroll: - isFastScroll = !isFastScroll; - item.setChecked(isFastScroll); - initializeAdapter(); - break; - case R.id.action_addpadding: - addPadding = !addPadding; - item.setChecked(addPadding); - initializePadding(); - break; - case R.id.action_showShadow: - isShadowVisible = !isShadowVisible; - item.setChecked(isShadowVisible); - ((PinnedSectionListView)getListView()).setShadowVisible(isShadowVisible); - break; - case R.id.action_showHeaderAndFooter: - hasHeaderAndFooter = !hasHeaderAndFooter; - item.setChecked(hasHeaderAndFooter); - initializeHeaderAndFooter(); - break; - } - return true; - } - - private void initializePadding() { - float density = getResources().getDisplayMetrics().density; - int padding = addPadding ? (int) (16 * density) : 0; - getListView().setPadding(padding, padding, padding, padding); - } - - private void initializeHeaderAndFooter() { - setListAdapter(null); - if (hasHeaderAndFooter) { - ListView list = getListView(); - - LayoutInflater inflater = LayoutInflater.from(this); - TextView header1 = (TextView) inflater.inflate(android.R.layout.simple_list_item_1, list, false); - header1.setText("First header"); - list.addHeaderView(header1); - - TextView header2 = (TextView) inflater.inflate(android.R.layout.simple_list_item_1, list, false); - header2.setText("Second header"); - list.addHeaderView(header2); - - TextView footer = (TextView) inflater.inflate(android.R.layout.simple_list_item_1, list, false); - footer.setText("Single footer"); - list.addFooterView(footer); - } - initializeAdapter(); - } - - @SuppressLint("NewApi") - private void initializeAdapter() { - getListView().setFastScrollEnabled(isFastScroll); - if (isFastScroll) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { - getListView().setFastScrollAlwaysVisible(true); - } - setListAdapter(new FastScrollAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1)); - } else { - setListAdapter(new SimpleAdapter(this, android.R.layout.simple_list_item_1, android.R.id.text1)); - } - } - - @Override - public void onClick(View v) { - Toast.makeText(this, "Item: " + v.getTag() , Toast.LENGTH_SHORT).show(); - } - -} \ No newline at end of file diff --git a/libraries/pinned-section-listview/library/AndroidManifest.xml b/libraries/pinned-section-listview/library/AndroidManifest.xml deleted file mode 100644 index 2e2ee9173..000000000 --- a/libraries/pinned-section-listview/library/AndroidManifest.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/libraries/pinned-section-listview/library/assets/.gitignore b/libraries/pinned-section-listview/library/assets/.gitignore deleted file mode 100644 index fdffa2a0f..000000000 --- a/libraries/pinned-section-listview/library/assets/.gitignore +++ /dev/null @@ -1 +0,0 @@ -# placeholder diff --git a/libraries/pinned-section-listview/library/build.gradle b/libraries/pinned-section-listview/library/build.gradle deleted file mode 100644 index d77f4746f..000000000 --- a/libraries/pinned-section-listview/library/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:0.6.3' - } -} - -apply plugin: 'android-library' - - -android { - compileSdkVersion 17 - buildToolsVersion '19' - - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src'] - res.srcDirs = ['res'] - } - } -} diff --git a/libraries/pinned-section-listview/library/proguard-project.txt b/libraries/pinned-section-listview/library/proguard-project.txt deleted file mode 100644 index f2fe1559a..000000000 --- a/libraries/pinned-section-listview/library/proguard-project.txt +++ /dev/null @@ -1,20 +0,0 @@ -# To enable ProGuard in your project, edit project.properties -# to define the proguard.config property as described in that file. -# -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in ${sdk.dir}/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the ProGuard -# include property in project.properties. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/libraries/pinned-section-listview/library/project.properties b/libraries/pinned-section-listview/library/project.properties deleted file mode 100644 index 484dab075..000000000 --- a/libraries/pinned-section-listview/library/project.properties +++ /dev/null @@ -1,15 +0,0 @@ -# This file is automatically generated by Android Tools. -# Do not modify this file -- YOUR CHANGES WILL BE ERASED! -# -# This file must be checked in Version Control Systems. -# -# To customize properties used by the Ant build system edit -# "ant.properties", and override values to adapt the script to your -# project structure. -# -# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): -#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt - -# Project target. -target=android-17 -android.library=true diff --git a/libraries/pinned-section-listview/library/res/.gitignore b/libraries/pinned-section-listview/library/res/.gitignore deleted file mode 100644 index fdffa2a0f..000000000 --- a/libraries/pinned-section-listview/library/res/.gitignore +++ /dev/null @@ -1 +0,0 @@ -# placeholder diff --git a/libraries/pinned-section-listview/library/src/com/hb/views/PinnedSectionListView.java b/libraries/pinned-section-listview/library/src/com/hb/views/PinnedSectionListView.java deleted file mode 100644 index 8100ad693..000000000 --- a/libraries/pinned-section-listview/library/src/com/hb/views/PinnedSectionListView.java +++ /dev/null @@ -1,513 +0,0 @@ -/* - * Copyright (C) 2013 Sergej Shafarenka, halfbit.de - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file kt 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. - */ - -package com.hb.views; - -import android.content.Context; -import android.database.DataSetObserver; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.GradientDrawable.Orientation; -import android.os.Parcelable; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.SoundEffectConstants; -import android.view.View; -import android.view.ViewConfiguration; -import android.view.accessibility.AccessibilityEvent; -import android.widget.AbsListView; -import android.widget.HeaderViewListAdapter; -import android.widget.ListAdapter; -import android.widget.ListView; -import android.widget.SectionIndexer; - -import com.hb.views.pinnedsection.BuildConfig; - -/** - * ListView, which is capable to pin section views at its top while the rest is still scrolled. - */ -public class PinnedSectionListView extends ListView { - - //-- inner classes - - /** List adapter to be implemented for being used with PinnedSectionListView adapter. */ - public static interface PinnedSectionListAdapter extends ListAdapter { - /** This method shall return 'true' if views of given type has to be pinned. */ - boolean isItemViewTypePinned(int viewType); - } - - /** Wrapper class for pinned section view and its position in the list. */ - static class PinnedSection { - public View view; - public int position; - public long id; - } - - //-- class fields - - // fields used for handling touch events - private final Rect mTouchRect = new Rect(); - private final PointF mTouchPoint = new PointF(); - private int mTouchSlop; - private View mTouchTarget; - private MotionEvent mDownEvent; - - // fields used for drawing shadow under a pinned section - private GradientDrawable mShadowDrawable; - private int mSectionsDistanceY; - private int mShadowHeight; - - /** Delegating listener, can be null. */ - OnScrollListener mDelegateOnScrollListener; - - /** Shadow for being recycled, can be null. */ - PinnedSection mRecycleSection; - - /** shadow instance with a pinned view, can be null. */ - PinnedSection mPinnedSection; - - /** Pinned view Y-translation. We use it to stick pinned view to the next section. */ - int mTranslateY; - - /** Scroll listener which does the magic */ - private final OnScrollListener mOnScrollListener = new OnScrollListener() { - - @Override public void onScrollStateChanged(AbsListView view, int scrollState) { - if (mDelegateOnScrollListener != null) { // delegate - mDelegateOnScrollListener.onScrollStateChanged(view, scrollState); - } - } - - @Override - public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - - if (mDelegateOnScrollListener != null) { // delegate - mDelegateOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); - } - - // get expected adapter or fail fast - ListAdapter adapter = getAdapter(); - if (adapter == null || visibleItemCount == 0) return; // nothing to do - - final boolean isFirstVisibleItemSection = - isItemViewTypePinned(adapter, adapter.getItemViewType(firstVisibleItem)); - - if (isFirstVisibleItemSection) { - View sectionView = getChildAt(0); - if (sectionView.getTop() == getPaddingTop()) { // view sticks to the top, no need for pinned shadow - destroyPinnedShadow(); - } else { // section doesn't stick to the top, make sure we have a pinned shadow - ensureShadowForPosition(firstVisibleItem, firstVisibleItem, visibleItemCount); - } - - } else { // section is not at the first visible position - int sectionPosition = findCurrentSectionPosition(firstVisibleItem); - if (sectionPosition > -1) { // we have section position - ensureShadowForPosition(sectionPosition, firstVisibleItem, visibleItemCount); - } else { // there is no section for the first visible item, destroy shadow - destroyPinnedShadow(); - } - } - }; - - }; - - /** Default change observer. */ - private final DataSetObserver mDataSetObserver = new DataSetObserver() { - @Override public void onChanged() { - recreatePinnedShadow(); - }; - @Override public void onInvalidated() { - recreatePinnedShadow(); - } - }; - - //-- constructors - - public PinnedSectionListView(Context context, AttributeSet attrs) { - super(context, attrs); - initView(); - } - - public PinnedSectionListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - initView(); - } - - private void initView() { - setOnScrollListener(mOnScrollListener); - mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); - initShadow(true); - } - - //-- public API methods - - public void setShadowVisible(boolean visible) { - initShadow(visible); - if (mPinnedSection != null) { - View v = mPinnedSection.view; - invalidate(v.getLeft(), v.getTop(), v.getRight(), v.getBottom() + mShadowHeight); - } - } - - //-- pinned section drawing methods - - public void initShadow(boolean visible) { - if (visible) { - if (mShadowDrawable == null) { - mShadowDrawable = new GradientDrawable(Orientation.TOP_BOTTOM, - new int[] { Color.parseColor("#ffa0a0a0"), Color.parseColor("#50a0a0a0"), Color.parseColor("#00a0a0a0")}); - mShadowHeight = (int) (8 * getResources().getDisplayMetrics().density); - } - } else { - if (mShadowDrawable != null) { - mShadowDrawable = null; - mShadowHeight = 0; - } - } - } - - /** Create shadow wrapper with a pinned view for a view at given position */ - void createPinnedShadow(int position) { - - // try to recycle shadow - PinnedSection pinnedShadow = mRecycleSection; - mRecycleSection = null; - - // create new shadow, if needed - if (pinnedShadow == null) pinnedShadow = new PinnedSection(); - // request new view using recycled view, if such - View pinnedView = getAdapter().getView(position, pinnedShadow.view, PinnedSectionListView.this); - - // read layout parameters - LayoutParams layoutParams = (LayoutParams) pinnedView.getLayoutParams(); - if (layoutParams == null) { // create default layout params - layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - } - - int heightMode = MeasureSpec.getMode(layoutParams.height); - int heightSize = MeasureSpec.getSize(layoutParams.height); - - if (heightMode == MeasureSpec.UNSPECIFIED) heightMode = MeasureSpec.EXACTLY; - - int maxHeight = getHeight() - getListPaddingTop() - getListPaddingBottom(); - if (heightSize > maxHeight) heightSize = maxHeight; - - // measure & layout - int ws = MeasureSpec.makeMeasureSpec(getWidth() - getListPaddingLeft() - getListPaddingRight(), MeasureSpec.EXACTLY); - int hs = MeasureSpec.makeMeasureSpec(heightSize, heightMode); - pinnedView.measure(ws, hs); - pinnedView.layout(0, 0, pinnedView.getMeasuredWidth(), pinnedView.getMeasuredHeight()); - mTranslateY = 0; - - // initialize pinned shadow - pinnedShadow.view = pinnedView; - pinnedShadow.position = position; - pinnedShadow.id = getAdapter().getItemId(position); - - // store pinned shadow - mPinnedSection = pinnedShadow; - } - - /** Destroy shadow wrapper for currently pinned view */ - void destroyPinnedShadow() { - if (mPinnedSection != null) { - // keep shadow for being recycled later - mRecycleSection = mPinnedSection; - mPinnedSection = null; - } - } - - /** Makes sure we have an actual pinned shadow for given position. */ - void ensureShadowForPosition(int sectionPosition, int firstVisibleItem, int visibleItemCount) { - if (visibleItemCount < 2) { // no need for creating shadow at all, we have a single visible item - destroyPinnedShadow(); - return; - } - - if (mPinnedSection != null - && mPinnedSection.position != sectionPosition) { // invalidate shadow, if required - destroyPinnedShadow(); - } - - if (mPinnedSection == null) { // create shadow, if empty - createPinnedShadow(sectionPosition); - } - - // align shadow according to next section position, if needed - int nextPosition = sectionPosition + 1; - if (nextPosition < getCount()) { - int nextSectionPosition = findFirstVisibleSectionPosition(nextPosition, - visibleItemCount - (nextPosition - firstVisibleItem)); - if (nextSectionPosition > -1) { - View nextSectionView = getChildAt(nextSectionPosition - firstVisibleItem); - final int bottom = mPinnedSection.view.getBottom() + getPaddingTop(); - mSectionsDistanceY = nextSectionView.getTop() - bottom; - if (mSectionsDistanceY < 0) { - // next section overlaps pinned shadow, move it up - mTranslateY = mSectionsDistanceY; - } else { - // next section does not overlap with pinned, stick to top - mTranslateY = 0; - } - } else { - // no other sections are visible, stick to top - mTranslateY = 0; - mSectionsDistanceY = Integer.MAX_VALUE; - } - } - - } - - int findFirstVisibleSectionPosition(int firstVisibleItem, int visibleItemCount) { - ListAdapter adapter = getAdapter(); - for (int childIndex = 0; childIndex < visibleItemCount; childIndex++) { - int position = firstVisibleItem + childIndex; - int viewType = adapter.getItemViewType(position); - if (isItemViewTypePinned(adapter, viewType)) return position; - } - return -1; - } - - int findCurrentSectionPosition(int fromPosition) { - ListAdapter adapter = getAdapter(); - - if (adapter instanceof SectionIndexer) { - // try fast way by asking section indexer - SectionIndexer indexer = (SectionIndexer) adapter; - int sectionPosition = indexer.getSectionForPosition(fromPosition); - int itemPosition = indexer.getPositionForSection(sectionPosition); - int typeView = adapter.getItemViewType(itemPosition); - if (isItemViewTypePinned(adapter, typeView)) { - return itemPosition; - } // else, no luck - } - - // try slow way by looking through to the next section item above - for (int position=fromPosition; position>=0; position--) { - int viewType = adapter.getItemViewType(position); - if (isItemViewTypePinned(adapter, viewType)) return position; - } - return -1; // no candidate found - } - - void recreatePinnedShadow() { - destroyPinnedShadow(); - ListAdapter adapter = getAdapter(); - if (adapter != null && adapter.getCount() > 0) { - int firstVisiblePosition = getFirstVisiblePosition(); - int sectionPosition = findCurrentSectionPosition(firstVisiblePosition); - if (sectionPosition == -1) return; // no views to pin, exit - ensureShadowForPosition(sectionPosition, - firstVisiblePosition, getLastVisiblePosition() - firstVisiblePosition); - } - } - - @Override - public void setOnScrollListener(OnScrollListener listener) { - if (listener == mOnScrollListener) { - super.setOnScrollListener(listener); - } else { - mDelegateOnScrollListener = listener; - } - } - - @Override - public void onRestoreInstanceState(Parcelable state) { - super.onRestoreInstanceState(state); - post(new Runnable() { - @Override public void run() { // restore pinned view after configuration change - recreatePinnedShadow(); - } - }); - } - - @Override - public void setAdapter(ListAdapter adapter) { - - // assert adapter in debug mode - if (BuildConfig.DEBUG && adapter != null) { - if (!(adapter instanceof PinnedSectionListAdapter)) - throw new IllegalArgumentException("Does your adapter implement PinnedSectionListAdapter?"); - if (adapter.getViewTypeCount() < 2) - throw new IllegalArgumentException("Does your adapter handle at least two types" + - " of views in getViewTypeCount() method: items and sections?"); - } - - // unregister observer at old adapter and register on new one - ListAdapter oldAdapter = getAdapter(); - if (oldAdapter != null) oldAdapter.unregisterDataSetObserver(mDataSetObserver); - if (adapter != null) adapter.registerDataSetObserver(mDataSetObserver); - - // destroy pinned shadow, if new adapter is not same as old one - if (oldAdapter != adapter) destroyPinnedShadow(); - - super.setAdapter(adapter); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - if (mPinnedSection != null) { - int parentWidth = r - l - getPaddingLeft() - getPaddingRight(); - int shadowWidth = mPinnedSection.view.getWidth(); - if (parentWidth != shadowWidth) { - recreatePinnedShadow(); - } - } - } - - @Override - protected void dispatchDraw(Canvas canvas) { - super.dispatchDraw(canvas); - - if (mPinnedSection != null) { - - // prepare variables - int pLeft = getListPaddingLeft(); - int pTop = getListPaddingTop(); - View view = mPinnedSection.view; - - // draw child - canvas.save(); - - int clipHeight = view.getHeight() + - (mShadowDrawable == null ? 0 : Math.min(mShadowHeight, mSectionsDistanceY)); - canvas.clipRect(pLeft, pTop, pLeft + view.getWidth(), pTop + clipHeight); - - canvas.translate(pLeft, pTop + mTranslateY); - drawChild(canvas, mPinnedSection.view, getDrawingTime()); - - if (mShadowDrawable != null && mSectionsDistanceY > 0) { - mShadowDrawable.setBounds(mPinnedSection.view.getLeft(), - mPinnedSection.view.getBottom(), - mPinnedSection.view.getRight(), - mPinnedSection.view.getBottom() + mShadowHeight); - mShadowDrawable.draw(canvas); - } - - canvas.restore(); - } - } - - //-- touch handling methods - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - - final float x = ev.getX(); - final float y = ev.getY(); - final int action = ev.getAction(); - - if (action == MotionEvent.ACTION_DOWN - && mTouchTarget == null - && mPinnedSection != null - && isPinnedViewTouched(mPinnedSection.view, x, y)) { // create touch target - - // user touched pinned view - mTouchTarget = mPinnedSection.view; - mTouchPoint.x = x; - mTouchPoint.y = y; - - // copy down event for eventually be used later - mDownEvent = MotionEvent.obtain(ev); - } - - if (mTouchTarget != null) { - if (isPinnedViewTouched(mTouchTarget, x, y)) { // forward event to pinned view - mTouchTarget.dispatchTouchEvent(ev); - } - - if (action == MotionEvent.ACTION_UP) { // perform onClick on pinned view - super.dispatchTouchEvent(ev); - performPinnedItemClick(); - clearTouchTarget(); - - } else if (action == MotionEvent.ACTION_CANCEL) { // cancel - clearTouchTarget(); - - } else if (action == MotionEvent.ACTION_MOVE) { - if (Math.abs(y - mTouchPoint.y) > mTouchSlop) { - - // cancel sequence on touch target - MotionEvent event = MotionEvent.obtain(ev); - event.setAction(MotionEvent.ACTION_CANCEL); - mTouchTarget.dispatchTouchEvent(event); - event.recycle(); - - // provide correct sequence to super class for further handling - super.dispatchTouchEvent(mDownEvent); - super.dispatchTouchEvent(ev); - clearTouchTarget(); - - } - } - - return true; - } - - // call super if this was not our pinned view - return super.dispatchTouchEvent(ev); - } - - private boolean isPinnedViewTouched(View view, float x, float y) { - view.getHitRect(mTouchRect); - - // by taping top or bottom padding, the list performs on click on a border item. - // we don't add top padding here to keep behavior consistent. - mTouchRect.top += mTranslateY; - - mTouchRect.bottom += mTranslateY + getPaddingTop(); - mTouchRect.left += getPaddingLeft(); - mTouchRect.right -= getPaddingRight(); - return mTouchRect.contains((int)x, (int)y); - } - - private void clearTouchTarget() { - mTouchTarget = null; - if (mDownEvent != null) { - mDownEvent.recycle(); - mDownEvent = null; - } - } - - private boolean performPinnedItemClick() { - if (mPinnedSection == null) return false; - - OnItemClickListener listener = getOnItemClickListener(); - if (listener != null) { - View view = mPinnedSection.view; - playSoundEffect(SoundEffectConstants.CLICK); - if (view != null) { - view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); - } - listener.onItemClick(this, view, mPinnedSection.position, mPinnedSection.id); - return true; - } - return false; - } - - public static boolean isItemViewTypePinned(ListAdapter adapter, int viewType) { - if (adapter instanceof HeaderViewListAdapter) { - adapter = ((HeaderViewListAdapter)adapter).getWrappedAdapter(); - } - return ((PinnedSectionListAdapter) adapter).isItemViewTypePinned(viewType); - } - -} diff --git a/libraries/pinned-section-listview/screen1.png b/libraries/pinned-section-listview/screen1.png deleted file mode 100644 index 9cca20109..000000000 Binary files a/libraries/pinned-section-listview/screen1.png and /dev/null differ diff --git a/libraries/pinned-section-listview/screen2.png b/libraries/pinned-section-listview/screen2.png deleted file mode 100644 index ad59f6f2f..000000000 Binary files a/libraries/pinned-section-listview/screen2.png and /dev/null differ diff --git a/libraries/pinned-section-listview/screen3.png b/libraries/pinned-section-listview/screen3.png deleted file mode 100644 index 0fd528fa9..000000000 Binary files a/libraries/pinned-section-listview/screen3.png and /dev/null differ diff --git a/settings.gradle b/settings.gradle index 2e582798c..08454c958 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,3 @@ include ':OpenPGP-Keychain' include ':libraries:ActionBarSherlock' -include ':libraries:HtmlTextView' -include ':libraries:pinned-section-listview:library' \ No newline at end of file +include ':libraries:HtmlTextView' \ No newline at end of file -- cgit v1.2.3 From f5da63f9882e1807c6bd2adb5205ad7482c45339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Thu, 2 Jan 2014 21:10:08 +0100 Subject: New list with sticky list headers library --- OpenPGP-Keychain/AndroidManifest.xml | 6 +- OpenPGP-Keychain/build.gradle | 3 +- OpenPGP-Keychain/project.properties | 1 + OpenPGP-Keychain/res/drawable/header_selector.xml | 7 + .../res/layout/key_list_public_activity.xml | 13 +- .../res/layout/key_list_public_fragment.xml | 14 + .../res/layout/key_list_secret_activity.xml | 13 +- OpenPGP-Keychain/res/layout/stickylist_header.xml | 17 + OpenPGP-Keychain/res/values/colors.xml | 5 +- OpenPGP-Keychain/res/values/strings.xml | 1 - .../keychain/ui/KeyListPublicFragment.java | 93 ++- .../keychain/ui/adapter/KeyListPublicAdapter.java | 80 +- .../stickylistheaders/views/UnderlineTextView.java | 50 ++ libraries/StickyListHeaders/.gitignore | 98 +++ libraries/StickyListHeaders/LICENSE | 191 +++++ libraries/StickyListHeaders/README.md | 203 +++++ libraries/StickyListHeaders/build.gradle | 24 + libraries/StickyListHeaders/demo.gif | Bin 0 -> 286171 bytes .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + libraries/StickyListHeaders/gradlew | 164 ++++ libraries/StickyListHeaders/gradlew.bat | 90 ++ libraries/StickyListHeaders/library/.gitignore | 2 + .../StickyListHeaders/library/AndroidManifest.xml | 11 + libraries/StickyListHeaders/library/build.gradle | 16 + libraries/StickyListHeaders/library/build.xml | 92 ++ .../StickyListHeaders/library/proguard-project.txt | 20 + .../StickyListHeaders/library/project.properties | 15 + .../StickyListHeaders/library/res/values/attrs.xml | 32 + .../stickylistheaders/AdapterWrapper.java | 225 +++++ .../stickylistheaders/ApiLevelTooLowException.java | 11 + .../stickylistheaders/CheckableWrapperView.java | 31 + .../SectionIndexerAdapterWrapper.java | 32 + .../StickyListHeadersAdapter.java | 38 + .../StickyListHeadersListView.java | 925 +++++++++++++++++++++ .../stickylistheaders/WrapperView.java | 150 ++++ .../stickylistheaders/WrapperViewList.java | 175 ++++ .../StickyListHeaders/sample/AndroidManifest.xml | 27 + libraries/StickyListHeaders/sample/build.gradle | 23 + .../sample/libs/android-support-v4.jar | Bin 0 -> 556198 bytes .../StickyListHeaders/sample/project.properties | 15 + .../sample/res/drawable-hdpi/ic_drawer.png | Bin 0 -> 2826 bytes .../sample/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 9397 bytes .../sample/res/drawable-ldpi/ic_launcher.png | Bin 0 -> 2729 bytes .../sample/res/drawable-mdpi/ic_drawer.png | Bin 0 -> 2816 bytes .../sample/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 5237 bytes .../sample/res/drawable-xhdpi/ic_drawer.png | Bin 0 -> 1038 bytes .../sample/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 14383 bytes .../sample/res/drawable/header_selector.xml | 7 + .../StickyListHeaders/sample/res/layout/header.xml | 17 + .../sample/res/layout/list_footer.xml | 13 + .../sample/res/layout/list_header.xml | 13 + .../StickyListHeaders/sample/res/layout/main.xml | 114 +++ .../sample/res/layout/test_list_item_layout.xml | 8 + .../StickyListHeaders/sample/res/values/arrays.xml | 230 +++++ .../StickyListHeaders/sample/res/values/colors.xml | 7 + .../sample/res/values/strings.xml | 20 + .../StickyListHeaders/sample/res/values/style.xml | 9 + .../stickylistheaders/TestActivity.java | 169 ++++ .../stickylistheaders/TestBaseAdapter.java | 169 ++++ .../stickylistheaders/views/UnderlineTextView.java | 50 ++ libraries/StickyListHeaders/settings.gradle | 2 + settings.gradle | 3 +- 63 files changed, 3688 insertions(+), 62 deletions(-) create mode 100644 OpenPGP-Keychain/res/drawable/header_selector.xml create mode 100644 OpenPGP-Keychain/res/layout/key_list_public_fragment.xml create mode 100644 OpenPGP-Keychain/res/layout/stickylist_header.xml create mode 100644 OpenPGP-Keychain/src/se/emilsjolander/stickylistheaders/views/UnderlineTextView.java create mode 100644 libraries/StickyListHeaders/.gitignore create mode 100644 libraries/StickyListHeaders/LICENSE create mode 100644 libraries/StickyListHeaders/README.md create mode 100644 libraries/StickyListHeaders/build.gradle create mode 100644 libraries/StickyListHeaders/demo.gif create mode 100644 libraries/StickyListHeaders/gradle/wrapper/gradle-wrapper.jar create mode 100644 libraries/StickyListHeaders/gradle/wrapper/gradle-wrapper.properties create mode 100644 libraries/StickyListHeaders/gradlew create mode 100644 libraries/StickyListHeaders/gradlew.bat create mode 100644 libraries/StickyListHeaders/library/.gitignore create mode 100644 libraries/StickyListHeaders/library/AndroidManifest.xml create mode 100644 libraries/StickyListHeaders/library/build.gradle create mode 100644 libraries/StickyListHeaders/library/build.xml create mode 100644 libraries/StickyListHeaders/library/proguard-project.txt create mode 100644 libraries/StickyListHeaders/library/project.properties create mode 100644 libraries/StickyListHeaders/library/res/values/attrs.xml create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/ApiLevelTooLowException.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/CheckableWrapperView.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperView.java create mode 100644 libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperViewList.java create mode 100644 libraries/StickyListHeaders/sample/AndroidManifest.xml create mode 100644 libraries/StickyListHeaders/sample/build.gradle create mode 100644 libraries/StickyListHeaders/sample/libs/android-support-v4.jar create mode 100644 libraries/StickyListHeaders/sample/project.properties create mode 100644 libraries/StickyListHeaders/sample/res/drawable-hdpi/ic_drawer.png create mode 100644 libraries/StickyListHeaders/sample/res/drawable-hdpi/ic_launcher.png create mode 100644 libraries/StickyListHeaders/sample/res/drawable-ldpi/ic_launcher.png create mode 100644 libraries/StickyListHeaders/sample/res/drawable-mdpi/ic_drawer.png create mode 100644 libraries/StickyListHeaders/sample/res/drawable-mdpi/ic_launcher.png create mode 100644 libraries/StickyListHeaders/sample/res/drawable-xhdpi/ic_drawer.png create mode 100644 libraries/StickyListHeaders/sample/res/drawable-xhdpi/ic_launcher.png create mode 100644 libraries/StickyListHeaders/sample/res/drawable/header_selector.xml create mode 100644 libraries/StickyListHeaders/sample/res/layout/header.xml create mode 100644 libraries/StickyListHeaders/sample/res/layout/list_footer.xml create mode 100644 libraries/StickyListHeaders/sample/res/layout/list_header.xml create mode 100644 libraries/StickyListHeaders/sample/res/layout/main.xml create mode 100644 libraries/StickyListHeaders/sample/res/layout/test_list_item_layout.xml create mode 100755 libraries/StickyListHeaders/sample/res/values/arrays.xml create mode 100644 libraries/StickyListHeaders/sample/res/values/colors.xml create mode 100644 libraries/StickyListHeaders/sample/res/values/strings.xml create mode 100644 libraries/StickyListHeaders/sample/res/values/style.xml create mode 100644 libraries/StickyListHeaders/sample/src/se/emilsjolander/stickylistheaders/TestActivity.java create mode 100644 libraries/StickyListHeaders/sample/src/se/emilsjolander/stickylistheaders/TestBaseAdapter.java create mode 100644 libraries/StickyListHeaders/sample/src/se/emilsjolander/stickylistheaders/views/UnderlineTextView.java create mode 100644 libraries/StickyListHeaders/settings.gradle diff --git a/OpenPGP-Keychain/AndroidManifest.xml b/OpenPGP-Keychain/AndroidManifest.xml index be9fe222c..8433323f4 100644 --- a/OpenPGP-Keychain/AndroidManifest.xml +++ b/OpenPGP-Keychain/AndroidManifest.xml @@ -88,8 +88,7 @@ android:name=".ui.KeyListPublicActivity" android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:label="@string/title_manage_public_keys" - android:launchMode="singleTop" - android:uiOptions="splitActionBarWhenNarrow" > + android:launchMode="singleTop" > @@ -104,8 +103,7 @@ android:name=".ui.KeyListSecretActivity" android:configChanges="orientation|screenSize|keyboardHidden|keyboard" android:label="@string/title_manage_secret_keys" - android:launchMode="singleTop" - android:uiOptions="splitActionBarWhenNarrow" > + android:launchMode="singleTop" > diff --git a/OpenPGP-Keychain/build.gradle b/OpenPGP-Keychain/build.gradle index 80c0f05cd..f31b6908c 100644 --- a/OpenPGP-Keychain/build.gradle +++ b/OpenPGP-Keychain/build.gradle @@ -4,6 +4,7 @@ buildscript { } dependencies { + // NOTE: Avoid using dynamic versions (+). This breaks offline builds! classpath 'com.android.tools.build:gradle:0.6.3' } } @@ -22,7 +23,7 @@ dependencies { compile 'com.android.support:support-v4:18.0.+' // already in actionbarsherlock compile project(':libraries:ActionBarSherlock') compile project(':libraries:HtmlTextView') - compile project(':libraries:pinned-section-listview:library') + compile project(':libraries:StickyListHeaders:library') } android { diff --git a/OpenPGP-Keychain/project.properties b/OpenPGP-Keychain/project.properties index 7acfa6b58..8e240f3c3 100644 --- a/OpenPGP-Keychain/project.properties +++ b/OpenPGP-Keychain/project.properties @@ -11,3 +11,4 @@ target=android-19 android.library.reference.1=../libraries/ActionBarSherlock android.library.reference.2=../libraries/HtmlTextView +android.library.reference.3=../libraries/StickyListHeaders/library diff --git a/OpenPGP-Keychain/res/drawable/header_selector.xml b/OpenPGP-Keychain/res/drawable/header_selector.xml new file mode 100644 index 000000000..5dfb8265c --- /dev/null +++ b/OpenPGP-Keychain/res/drawable/header_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/key_list_public_activity.xml b/OpenPGP-Keychain/res/layout/key_list_public_activity.xml index a35e23038..07ec253cc 100644 --- a/OpenPGP-Keychain/res/layout/key_list_public_activity.xml +++ b/OpenPGP-Keychain/res/layout/key_list_public_activity.xml @@ -8,18 +8,7 @@ android:id="@+id/key_list_public_fragment" android:name="org.sufficientlysecure.keychain.ui.KeyListPublicFragment" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="0dip" android:layout_weight="1" /> - - \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/key_list_public_fragment.xml b/OpenPGP-Keychain/res/layout/key_list_public_fragment.xml new file mode 100644 index 000000000..052dd4249 --- /dev/null +++ b/OpenPGP-Keychain/res/layout/key_list_public_fragment.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/key_list_secret_activity.xml b/OpenPGP-Keychain/res/layout/key_list_secret_activity.xml index d4397c444..b8df9faa7 100644 --- a/OpenPGP-Keychain/res/layout/key_list_secret_activity.xml +++ b/OpenPGP-Keychain/res/layout/key_list_secret_activity.xml @@ -8,18 +8,7 @@ android:id="@+id/key_list_secret_fragment" android:name="org.sufficientlysecure.keychain.ui.KeyListSecretFragment" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="0dip" android:layout_weight="1" /> - - \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/stickylist_header.xml b/OpenPGP-Keychain/res/layout/stickylist_header.xml new file mode 100644 index 000000000..475d1c4db --- /dev/null +++ b/OpenPGP-Keychain/res/layout/stickylist_header.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/values/colors.xml b/OpenPGP-Keychain/res/values/colors.xml index d1dc6c1f4..7831a63a0 100644 --- a/OpenPGP-Keychain/res/values/colors.xml +++ b/OpenPGP-Keychain/res/values/colors.xml @@ -3,5 +3,8 @@ #31b6e7 #cecbce - + #ffe74c3c + #ffc0392b + #FFDDDDDD + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/values/strings.xml b/OpenPGP-Keychain/res/values/strings.xml index c3736815c..d900f5a71 100644 --- a/OpenPGP-Keychain/res/values/strings.xml +++ b/OpenPGP-Keychain/res/values/strings.xml @@ -210,7 +210,6 @@ Unknown key %s, do you want to try finding it on a keyserver? Successfully sent key to server Successfully signed key - Long press one entry in this list to show more options! This list is empty! Successfully sent key with NFC Beam! diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java index 3dafc84ca..8167ff439 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java @@ -24,6 +24,9 @@ import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; import org.sufficientlysecure.keychain.ui.adapter.KeyListPublicAdapter; +import se.emilsjolander.stickylistheaders.ApiLevelTooLowException; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; +import android.annotation.SuppressLint; import android.content.Intent; import android.database.Cursor; import android.net.Uri; @@ -31,58 +34,84 @@ import android.os.Bundle; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import com.actionbarsherlock.app.SherlockListFragment; - -public class KeyListPublicFragment extends SherlockListFragment implements - LoaderManager.LoaderCallbacks { +import com.actionbarsherlock.app.SherlockFragment; + +/** + * Public key list with sticky list headers. + * + * - uses StickyListHeaders library + * - custom adapter: KeyListPublicAdapter + * + * TODO: + * - fix loader with spinning animation + * - fix design + * - fix view holder in adapter + * + */ +public class KeyListPublicFragment extends SherlockFragment implements + AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks { private KeyListPublicActivity mKeyListPublicActivity; private KeyListPublicAdapter mAdapter; + StickyListHeadersListView stickyList; + /** * Define Adapter and Loader on create of Activity */ + @SuppressLint("NewApi") @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mKeyListPublicActivity = (KeyListPublicActivity) getActivity(); - getListView().setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int position, long id) { - // start key view on click - Intent detailsIntent = new Intent(mKeyListPublicActivity, KeyViewActivity.class); - detailsIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long - .toString(id))); - startActivity(detailsIntent); - } - }); + stickyList = (StickyListHeadersListView) getActivity().findViewById( + R.id.key_list_public_fragment_stickylist); + + stickyList.setOnItemClickListener(this); + // stickyList.setOnHeaderClickListener(this); + // stickyList.setOnStickyHeaderOffsetChangedListener(this); + // mStickyList.addHeaderView(inflater.inflate(R.layout.list_header, null)); + // mStickyList.addFooterView(inflater.inflate(R.layout.list_footer, null)); + // stickyList.setEmptyView(findViewById(R.id.empty)); + stickyList.setAreHeadersSticky(true); + stickyList.setDrawingListUnderStickyHeader(true); + stickyList.setFastScrollEnabled(true); + try { + stickyList.setFastScrollAlwaysVisible(true); + } catch (ApiLevelTooLowException e) { + } // Give some text to display if there is no data. In a real // application this would come from a resource. - setEmptyText(getString(R.string.list_empty)); - - // We have a menu item to show in action bar. - setHasOptionsMenu(true); + // setEmptyText(getString(R.string.list_empty)); // Start out with a progress indicator. - setListShown(false); + // setListShown(false); // Create an empty adapter we will use to display the loaded data. - mAdapter = new KeyListPublicAdapter(mKeyListPublicActivity, null, Id.type.public_key); - setListAdapter(mAdapter); + // mAdapter = new KeyListPublicAdapter(mKeyListPublicActivity, null, Id.type.public_key); + // setListAdapter(mAdapter); + // stickyList.setAdapter(mAdapter); // Prepare the loader. Either re-connect with an existing one, // or start a new one. getLoaderManager().initLoader(0, null, this); } + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.key_list_public_fragment, container, false); + return view; + } + // These are the rows that we will retrieve. static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID, UserIds.USER_ID }; @@ -104,13 +133,19 @@ public class KeyListPublicFragment extends SherlockListFragment implements public void onLoadFinished(Loader loader, Cursor data) { // Swap the new cursor in. (The framework will take care of closing the // old cursor once we return.) - mAdapter.swapCursor(data); + // mAdapter.swapCursor(data); + int userIdIndex = data.getColumnIndex(UserIds.USER_ID); + + mAdapter = new KeyListPublicAdapter(mKeyListPublicActivity, data, Id.type.public_key, + userIdIndex); + + stickyList.setAdapter(mAdapter); // The list should now be shown. if (isResumed()) { - setListShown(true); + // setListShown(true); } else { - setListShownNoAnimation(true); + // setListShownNoAnimation(true); } } @@ -122,4 +157,12 @@ public class KeyListPublicFragment extends SherlockListFragment implements mAdapter.swapCursor(null); } + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + // start key view on click + Intent detailsIntent = new Intent(mKeyListPublicActivity, KeyViewActivity.class); + detailsIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(id))); + startActivity(detailsIntent); + } + } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java index d72c9d42a..86a47d4d7 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java @@ -17,30 +17,40 @@ package org.sufficientlysecure.keychain.ui.adapter; +import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.helper.OtherHelper; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; -import org.sufficientlysecure.keychain.util.SectionCursorAdapter; +import org.sufficientlysecure.keychain.util.Log; +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; import android.content.Context; import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -public class KeyListPublicAdapter extends SectionCursorAdapter { - +/** + * - implements StickyListHeadersAdapter from library - uses view holder pattern for performance + * + */ +public class KeyListPublicAdapter extends CursorAdapter implements StickyListHeadersAdapter { private LayoutInflater mInflater; - public KeyListPublicAdapter(Context context, Cursor c, int flags) { - super(context, c, android.R.layout.preference_category, 2); // TODO: 2 is user id + int mSectionColumnIndex; + + public KeyListPublicAdapter(Context context, Cursor c, int flags, int sectionColumnIndex) { + super(context, c, flags); mInflater = LayoutInflater.from(context); + mSectionColumnIndex = sectionColumnIndex; } @Override public void bindView(View view, Context context, Cursor cursor) { + // TODO: view holder pattern? int userIdIndex = cursor.getColumnIndex(UserIds.USER_ID); TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); @@ -74,4 +84,64 @@ public class KeyListPublicAdapter extends SectionCursorAdapter { return mInflater.inflate(R.layout.key_list_group_item, null); } + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + + HeaderViewHolder holder; + if (convertView == null) { + holder = new HeaderViewHolder(); + convertView = mInflater.inflate(R.layout.stickylist_header, parent, false); + holder.text = (TextView) convertView.findViewById(R.id.stickylist_header_text); + convertView.setTag(holder); + } else { + holder = (HeaderViewHolder) convertView.getTag(); + } + + if (!mDataValid) { + // no data available at this point + Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); + return convertView; + } + + // similar to getView in CursorAdapter + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + // set header text as first char in name + String headerText = "" + mCursor.getString(mSectionColumnIndex).subSequence(0, 1).charAt(0); + holder.text.setText(headerText); + return convertView; + } + + /** + * Remember that these have to be static, position=1 should always return the same Id that is. + */ + @Override + public long getHeaderId(int position) { + if (!mDataValid) { + // no data available at this point + Log.d(Constants.TAG, "getHeaderView: No data available at this point!"); + return -1; + } + + // similar to getView in CursorAdapter + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + // return the first character of the name as ID because this is what + // headers are based upon + return mCursor.getString(mSectionColumnIndex).subSequence(0, 1).charAt(0); + } + + class HeaderViewHolder { + TextView text; + } + + class ViewHolder { + TextView mainUserId; + TextView mainUserIdRest; + } + } diff --git a/OpenPGP-Keychain/src/se/emilsjolander/stickylistheaders/views/UnderlineTextView.java b/OpenPGP-Keychain/src/se/emilsjolander/stickylistheaders/views/UnderlineTextView.java new file mode 100644 index 000000000..c202c00b8 --- /dev/null +++ b/OpenPGP-Keychain/src/se/emilsjolander/stickylistheaders/views/UnderlineTextView.java @@ -0,0 +1,50 @@ +package se.emilsjolander.stickylistheaders.views; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.TextView; + +/** + * @author Eric Frohnhoefer + */ +public class UnderlineTextView extends TextView { + private final Paint mPaint = new Paint(); + private int mUnderlineHeight = 0; + + public UnderlineTextView(Context context) { + this(context, null); + } + + public UnderlineTextView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public UnderlineTextView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + init(context, attrs); + } + + private void init(Context context, AttributeSet attrs) { + Resources r = getResources(); + mUnderlineHeight = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, r.getDisplayMetrics()); + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + super.setPadding(left, top, right, bottom + mUnderlineHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the underline the same color as the text + mPaint.setColor(getTextColors().getDefaultColor()); + canvas.drawRect(0, getHeight() - mUnderlineHeight, getWidth(), getHeight(), mPaint); + } +} diff --git a/libraries/StickyListHeaders/.gitignore b/libraries/StickyListHeaders/.gitignore new file mode 100644 index 000000000..71d84cca6 --- /dev/null +++ b/libraries/StickyListHeaders/.gitignore @@ -0,0 +1,98 @@ +###Android### + +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ + +# Local configuration file (sdk path, etc) +local.properties + +# Eclipse project files +.classpath +.project + + +###Eclipse### + +*.pydevproject +.project +.metadata +bin/** +tmp/** +tmp/**/* +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + + +###Maven### + +target/ + + +###Gradle### + +.gradle/ +build/ + + +###IntelliJ### + +*.iml +*.ipr +*.iws +.idea/ + + +###Actionscript### + +# Build and Release Folders +bin/ +bin-debug/ +bin-release/ + +# Project property files +.actionScriptProperties +.flexProperties +.settings/ +.project + +###OSX### + +.DS_Store + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + + diff --git a/libraries/StickyListHeaders/LICENSE b/libraries/StickyListHeaders/LICENSE new file mode 100644 index 000000000..37ec93a14 --- /dev/null +++ b/libraries/StickyListHeaders/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/libraries/StickyListHeaders/README.md b/libraries/StickyListHeaders/README.md new file mode 100644 index 000000000..2cfff2991 --- /dev/null +++ b/libraries/StickyListHeaders/README.md @@ -0,0 +1,203 @@ +StickyListHeaders +================= +StickyListHeaders is an Android library that makes it easy to integrate section headers in your `ListView`. These section headers stick to the top like in the new People app of Android 4.0 Ice Cream Sandwich. This behavior is also found in lists with sections on iOS devices. This library can also be used without the sticky functionality if you just want section headers. + +StickyListHeaders actively supports android versions 2.3 (gingerbread) and above. +That said, it works all the way down to 2.1 but is not actively tested or working perfectly. + +Here is a short gif showing the functionality you get with this library: + +![alt text](https://github.com/emilsjolander/StickyListHeaders/raw/master/demo.gif "Demo gif") + + +Goal +---- +The goal of this project is to deliver a high performance replacement to `ListView`. You should with minimal effort and time be able to add section headers to a list. This should be done via a simple to use API without any special features. This library will always priorities general use cases over special ones. This means that the library will add very few public methods to the standard `ListView` and will not try to work for every use case. While I will want to support even narrow use cases I will not do so if it compromises the API or any other feature. + + +Installing +--------------- +###Gradle +Add the following gradle dependency exchanging `x.x.x` for the latest release. +```groovy +dependencies { + compile 'se.emilsjolander:stickylistheaders:x.x.x' +} +``` + +###Cloning +First of all you will have to clone the library. +```shell +git clone https://github.com/emilsjolander/StickyListHeaders.git +``` + +Now that you have the library you will have to import it into Android Studio. +In Android Studio navigate the menus like this. +``` +File -> Import Project ... +``` +In the following dialog navigate to StickyListHeaders which you cloned to your computer in the previous steps and select the `build.gradle`. + +Getting Started +--------------- +Ok lets start with your activities or fragments xml file. It might look something like this. +```xml + +``` + +Now in your activities `onCreate()` or your fragments `onCreateView()` you would want to do something like this +```java +StickyListHeadersListView stickyList = (StickyListHeadersListView) findViewById(R.id.list); +MyAdapter adapter = new MyAdapter(this); +stickyList.setAdapter(adapter); +``` + +`MyAdapter` in the above example would look something like this if your list was a list of countries where each header was for a letter in the alphabet. +```java +public class MyAdapter extends BaseAdapter implements StickyListHeadersAdapter { + + private String[] countries; + private LayoutInflater inflater; + + public TestBaseAdapter(Context context) { + inflater = LayoutInflater.from(context); + countries = context.getResources().getStringArray(R.array.countries); + } + + @Override + public int getCount() { + return countries.length; + } + + @Override + public Object getItem(int position) { + return countries[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + if (convertView == null) { + holder = new ViewHolder(); + convertView = inflater.inflate(R.layout.test_list_item_layout, parent, false); + holder.text = (TextView) convertView.findViewById(R.id.text); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + holder.text.setText(countries[position]); + + return convertView; + } + + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + HeaderViewHolder holder; + if (convertView == null) { + holder = new HeaderViewHolder(); + convertView = inflater.inflate(R.layout.header, parent, false); + holder.text = (TextView) convertView.findViewById(R.id.text); + convertView.setTag(holder); + } else { + holder = (HeaderViewHolder) convertView.getTag(); + } + //set header text as first char in name + String headerText = "" + countries[position].subSequence(0, 1).charAt(0); + holder.text.setText(headerText); + return convertView; + } + + @Override + public long getHeaderId(int position) { + //return the first character of the country as ID because this is what headers are based upon + return countries[position].subSequence(0, 1).charAt(0); + } + + class HeaderViewHolder { + TextView text; + } + + class ViewHolder { + TextView text; + } + +} +``` + +That's it! Look through the API docs below to get know about things to customize and if you have any problems getting started please open an issue as it probably means the getting started guide need some improvement! + +Upgrading from 1.x versions +--------------------------- +First of all the package name has changed from `com.emilsjolander.components.stickylistheaders` -> `se.emilsjolander.stickylistheaders` so update all your imports and xml files using StickyListHeaders! + +If you are Upgrading from a version prior to 2.x you might run into the following problems. +1. `StickyListHeadersListView` is no longer a `ListView` subclass. This means that it cannot be passed into a method expecting a ListView. You can retrieve an instance of the `ListView` via `getWrappedList()` but use this with caution as things will probably break if you start setting things directly on that list. +2. Because `StickyListHeadersListView` is no longer a `ListView` it does not support all the methods. I have implemented delegate methods for all the usual methods and gladly accept pull requests for more. + +API +--- +###StickyListHeadersAdapter +```java +public interface StickyListHeadersAdapter extends ListAdapter { + View getHeaderView(int position, View convertView, ViewGroup parent); + long getHeaderId(int position); +} +``` +Your adapter must implement this interface to function with `StickyListHeadersListView`. +`getHeaderId()` must return a unique integer for every section. A valid implementation for a list with alphabetical sections is the return the char value of the section that `position` is a part of. + +`getHeaderView()` works exactly like `getView()` in a regular `ListAdapter`. + + +###StickyListHeadersListView +Headers are sticky by default but that can easily be changed with this setter. There is of course also a matching getter for the sticky property. +```java +public void setAreHeadersSticky(boolean areHeadersSticky); +public boolean areHeadersSticky(); +``` + +A `OnHeaderClickListener` is the header version of OnItemClickListener. This is the setter for it and the interface of the listener. The currentlySticky boolean flag indicated if the header that was clicked was sticking to the top at the time it was clicked. +```java +public void setOnHeaderClickListener(OnHeaderClickListener listener); + +public interface OnHeaderClickListener { + public void onHeaderClick(StickyListHeadersListView l, View header, int itemPosition, long headerId, boolean currentlySticky); +} +``` + +A `OnStickyHeaderOffsetChangedListener` is a Listener used for listening to when the sticky header slides out of the screen. The offset parameter will slowly grow to be the same size as the headers height. Use the listeners callback to transform the header in any way you see fit, the standard android contacts app dims the text for example. +```java +public void setOnStickyHeaderOffsetChangedListener(OnStickyHeaderOffsetChangedListener listener); + +public interface OnStickyHeaderOffsetChangedListener { + public void onStickyHeaderOffsetChanged(StickyListHeadersListView l, View header, int offset); +} +``` + +Here are two methods added to the API for inspecting the children of the underlying `ListView`. I could not override the normal `getChildAt()` and `getChildCount()` methods as that would mess up the underlying measurement system of the `FrameLayout` wrapping the `ListView`. +```java +public View getListChildAt(int index); +public int getListChildCount(); +``` + +This is a setter and getter for an internal attribute that controls if the list should be drawn under the stuck header. The default value is true. If you do not want to see the list scroll under your header you will want to set this attribute to `false`. +```java +public void setDrawingListUnderStickyHeader(boolean drawingListUnderStickyHeader); +public boolean isDrawingListUnderStickyHeader(); +``` + +Contributing +------------ +Contributions are very welcome. Now that this library has grown in popularity i have a hard time keeping upp with all the issues while tending to a multitude of other projects as well as school. So if you find a bug in the library or want a feature and think you can fix it yourself, fork + pull request and i will greatly appreciate it! + +I love getting pull requests for new features as well as bugs. However, when it comes to new features please also explain the use case and way you think the library should include it. If you don't want to start coding a feature without knowing if the feature will have chance of being included, open an issue and we can discuss the feature! diff --git a/libraries/StickyListHeaders/build.gradle b/libraries/StickyListHeaders/build.gradle new file mode 100644 index 000000000..849b1323c --- /dev/null +++ b/libraries/StickyListHeaders/build.gradle @@ -0,0 +1,24 @@ +buildscript { + repositories { + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:0.6.+' + } +} + +def isReleaseBuild() { + return version.contains("SNAPSHOT") == false +} + +allprojects { + version = VERSION_NAME + group = GROUP + + repositories { + mavenCentral() + } +} + +apply plugin: 'android-reporting' diff --git a/libraries/StickyListHeaders/demo.gif b/libraries/StickyListHeaders/demo.gif new file mode 100644 index 000000000..d73e3297b Binary files /dev/null and b/libraries/StickyListHeaders/demo.gif differ diff --git a/libraries/StickyListHeaders/gradle/wrapper/gradle-wrapper.jar b/libraries/StickyListHeaders/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..8c0fb64a8 Binary files /dev/null and b/libraries/StickyListHeaders/gradle/wrapper/gradle-wrapper.jar differ diff --git a/libraries/StickyListHeaders/gradle/wrapper/gradle-wrapper.properties b/libraries/StickyListHeaders/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..0a7effc64 --- /dev/null +++ b/libraries/StickyListHeaders/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Nov 19 08:36:06 CET 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.8-all.zip diff --git a/libraries/StickyListHeaders/gradlew b/libraries/StickyListHeaders/gradlew new file mode 100644 index 000000000..91a7e269e --- /dev/null +++ b/libraries/StickyListHeaders/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/libraries/StickyListHeaders/gradlew.bat b/libraries/StickyListHeaders/gradlew.bat new file mode 100644 index 000000000..aec99730b --- /dev/null +++ b/libraries/StickyListHeaders/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/libraries/StickyListHeaders/library/.gitignore b/libraries/StickyListHeaders/library/.gitignore new file mode 100644 index 000000000..f8b92c3aa --- /dev/null +++ b/libraries/StickyListHeaders/library/.gitignore @@ -0,0 +1,2 @@ +.gradle +build diff --git a/libraries/StickyListHeaders/library/AndroidManifest.xml b/libraries/StickyListHeaders/library/AndroidManifest.xml new file mode 100644 index 000000000..328dcc65c --- /dev/null +++ b/libraries/StickyListHeaders/library/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/libraries/StickyListHeaders/library/build.gradle b/libraries/StickyListHeaders/library/build.gradle new file mode 100644 index 000000000..a15de94c2 --- /dev/null +++ b/libraries/StickyListHeaders/library/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'android-library' + +android { + compileSdkVersion 19 + buildToolsVersion '19.0.0' + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + res.srcDirs = ['res'] + } + } +} + +apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/eaa6b5404b7594e6c23b884fdc5795f545db55dd/gradle-mvn-push.gradle' diff --git a/libraries/StickyListHeaders/library/build.xml b/libraries/StickyListHeaders/library/build.xml new file mode 100644 index 000000000..2f6f323a2 --- /dev/null +++ b/libraries/StickyListHeaders/library/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libraries/StickyListHeaders/library/proguard-project.txt b/libraries/StickyListHeaders/library/proguard-project.txt new file mode 100644 index 000000000..f2fe1559a --- /dev/null +++ b/libraries/StickyListHeaders/library/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/libraries/StickyListHeaders/library/project.properties b/libraries/StickyListHeaders/library/project.properties new file mode 100644 index 000000000..91d2b0246 --- /dev/null +++ b/libraries/StickyListHeaders/library/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-19 +android.library=true diff --git a/libraries/StickyListHeaders/library/res/values/attrs.xml b/libraries/StickyListHeaders/library/res/values/attrs.xml new file mode 100644 index 000000000..8d09a9c1b --- /dev/null +++ b/libraries/StickyListHeaders/library/res/values/attrs.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java new file mode 100644 index 000000000..e67de0dbf --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/AdapterWrapper.java @@ -0,0 +1,225 @@ +package se.emilsjolander.stickylistheaders; + +import java.util.LinkedList; +import java.util.List; + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Checkable; +import android.widget.ListAdapter; + +/** + * A {@link ListAdapter} which wraps a {@link StickyListHeadersAdapter} and + * automatically handles wrapping the result of + * {@link StickyListHeadersAdapter#getView(int, android.view.View, android.view.ViewGroup)} + * and + * {@link StickyListHeadersAdapter#getHeaderView(int, android.view.View, android.view.ViewGroup)} + * appropriately. + * + * @author Jake Wharton (jakewharton@gmail.com) + */ +class AdapterWrapper extends BaseAdapter implements StickyListHeadersAdapter { + + interface OnHeaderClickListener { + public void onHeaderClick(View header, int itemPosition, long headerId); + } + + final StickyListHeadersAdapter mDelegate; + private final List mHeaderCache = new LinkedList(); + private final Context mContext; + private Drawable mDivider; + private int mDividerHeight; + private OnHeaderClickListener mOnHeaderClickListener; + private DataSetObserver mDataSetObserver = new DataSetObserver() { + + @Override + public void onInvalidated() { + mHeaderCache.clear(); + AdapterWrapper.super.notifyDataSetInvalidated(); + } + + @Override + public void onChanged() { + AdapterWrapper.super.notifyDataSetChanged(); + } + }; + + AdapterWrapper(Context context, + StickyListHeadersAdapter delegate) { + this.mContext = context; + this.mDelegate = delegate; + delegate.registerDataSetObserver(mDataSetObserver); + } + + void setDivider(Drawable divider, int dividerHeight) { + this.mDivider = divider; + this.mDividerHeight = dividerHeight; + notifyDataSetChanged(); + } + + @Override + public boolean areAllItemsEnabled() { + return mDelegate.areAllItemsEnabled(); + } + + @Override + public boolean isEnabled(int position) { + return mDelegate.isEnabled(position); + } + + @Override + public int getCount() { + return mDelegate.getCount(); + } + + @Override + public Object getItem(int position) { + return mDelegate.getItem(position); + } + + @Override + public long getItemId(int position) { + return mDelegate.getItemId(position); + } + + @Override + public boolean hasStableIds() { + return mDelegate.hasStableIds(); + } + + @Override + public int getItemViewType(int position) { + return mDelegate.getItemViewType(position); + } + + @Override + public int getViewTypeCount() { + return mDelegate.getViewTypeCount(); + } + + @Override + public boolean isEmpty() { + return mDelegate.isEmpty(); + } + + /** + * Will recycle header from {@link WrapperView} if it exists + */ + private void recycleHeaderIfExists(WrapperView wv) { + View header = wv.mHeader; + if (header != null) { + // reset the headers visibility when adding it to the cache + header.setVisibility(View.VISIBLE); + mHeaderCache.add(header); + } + } + + /** + * Get a header view. This optionally pulls a header from the supplied + * {@link WrapperView} and will also recycle the divider if it exists. + */ + private View configureHeader(WrapperView wv, final int position) { + View header = wv.mHeader == null ? popHeader() : wv.mHeader; + header = mDelegate.getHeaderView(position, header, wv); + if (header == null) { + throw new NullPointerException("Header view must not be null."); + } + //if the header isn't clickable, the listselector will be drawn on top of the header + header.setClickable(true); + header.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if(mOnHeaderClickListener != null){ + long headerId = mDelegate.getHeaderId(position); + mOnHeaderClickListener.onHeaderClick(v, position, headerId); + } + } + }); + return header; + } + + private View popHeader() { + if(mHeaderCache.size() > 0) { + return mHeaderCache.remove(0); + } + return null; + } + + /** Returns {@code true} if the previous position has the same header ID. */ + private boolean previousPositionHasSameHeader(int position) { + return position != 0 + && mDelegate.getHeaderId(position) == mDelegate + .getHeaderId(position - 1); + } + + @Override + public WrapperView getView(int position, View convertView, ViewGroup parent) { + WrapperView wv = (convertView == null) ? new WrapperView(mContext) : (WrapperView) convertView; + View item = mDelegate.getView(position, wv.mItem, parent); + View header = null; + if (previousPositionHasSameHeader(position)) { + recycleHeaderIfExists(wv); + } else { + header = configureHeader(wv, position); + } + if((item instanceof Checkable) && !(wv instanceof CheckableWrapperView)) { + // Need to create Checkable subclass of WrapperView for ListView to work correctly + wv = new CheckableWrapperView(mContext); + } else if(!(item instanceof Checkable) && (wv instanceof CheckableWrapperView)) { + wv = new WrapperView(mContext); + } + wv.update(item, header, mDivider, mDividerHeight); + return wv; + } + + public void setOnHeaderClickListener(OnHeaderClickListener onHeaderClickListener){ + this.mOnHeaderClickListener = onHeaderClickListener; + } + + @Override + public boolean equals(Object o) { + return mDelegate.equals(o); + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return ((BaseAdapter) mDelegate).getDropDownView(position, convertView, parent); + } + + @Override + public int hashCode() { + return mDelegate.hashCode(); + } + + @Override + public void notifyDataSetChanged() { + ((BaseAdapter) mDelegate).notifyDataSetChanged(); + } + + @Override + public void notifyDataSetInvalidated() { + ((BaseAdapter) mDelegate).notifyDataSetInvalidated(); + } + + @Override + public String toString() { + return mDelegate.toString(); + } + + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + return mDelegate.getHeaderView(position, convertView, parent); + } + + @Override + public long getHeaderId(int position) { + return mDelegate.getHeaderId(position); + } + +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/ApiLevelTooLowException.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/ApiLevelTooLowException.java new file mode 100644 index 000000000..5b0a83827 --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/ApiLevelTooLowException.java @@ -0,0 +1,11 @@ +package se.emilsjolander.stickylistheaders; + +public class ApiLevelTooLowException extends RuntimeException { + + private static final long serialVersionUID = -5480068364264456757L; + + public ApiLevelTooLowException(int versionCode) { + super("Requires API level " + versionCode); + } + +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/CheckableWrapperView.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/CheckableWrapperView.java new file mode 100644 index 000000000..9039c3f5c --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/CheckableWrapperView.java @@ -0,0 +1,31 @@ +package se.emilsjolander.stickylistheaders; + +import android.content.Context; +import android.widget.Checkable; + +/** + * A WrapperView that implements the checkable interface + * + * @author Emil Sjölander + */ +class CheckableWrapperView extends WrapperView implements Checkable { + + public CheckableWrapperView(final Context context) { + super(context); + } + + @Override + public boolean isChecked() { + return ((Checkable) mItem).isChecked(); + } + + @Override + public void setChecked(final boolean checked) { + ((Checkable) mItem).setChecked(checked); + } + + @Override + public void toggle() { + setChecked(!isChecked()); + } +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java new file mode 100644 index 000000000..ff7e293af --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/SectionIndexerAdapterWrapper.java @@ -0,0 +1,32 @@ +package se.emilsjolander.stickylistheaders; + +import android.content.Context; +import android.widget.SectionIndexer; + +class SectionIndexerAdapterWrapper extends + AdapterWrapper implements SectionIndexer { + + final SectionIndexer mSectionIndexerDelegate; + + SectionIndexerAdapterWrapper(Context context, + StickyListHeadersAdapter delegate) { + super(context, delegate); + mSectionIndexerDelegate = (SectionIndexer) delegate; + } + + @Override + public int getPositionForSection(int section) { + return mSectionIndexerDelegate.getPositionForSection(section); + } + + @Override + public int getSectionForPosition(int position) { + return mSectionIndexerDelegate.getSectionForPosition(position); + } + + @Override + public Object[] getSections() { + return mSectionIndexerDelegate.getSections(); + } + +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java new file mode 100644 index 000000000..8b80b71f1 --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersAdapter.java @@ -0,0 +1,38 @@ +package se.emilsjolander.stickylistheaders; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; + +public interface StickyListHeadersAdapter extends ListAdapter { + /** + * Get a View that displays the header data at the specified position in the + * set. You can either create a View manually or inflate it from an XML layout + * file. + * + * @param position + * The position of the item within the adapter's data set of the item whose + * header view we want. + * @param convertView + * The old view to reuse, if possible. Note: You should check that this view is + * non-null and of an appropriate type before using. If it is not possible to + * convert this view to display the correct data, this method can create a new + * view. + * @param parent + * The parent that this view will eventually be attached to. + * @return + * A View corresponding to the data at the specified position. + */ + View getHeaderView(int position, View convertView, ViewGroup parent); + + /** + * Get the header id associated with the specified position in the list. + * + * @param position + * The position of the item within the adapter's data set whose header id we + * want. + * @return + * The id of the header at the specified position. + */ + long getHeaderId(int position); +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java new file mode 100644 index 000000000..476f6cfad --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/StickyListHeadersListView.java @@ -0,0 +1,925 @@ +package se.emilsjolander.stickylistheaders; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; +import android.widget.FrameLayout; +import android.widget.ListView; +import android.widget.SectionIndexer; + +import se.emilsjolander.stickylistheaders.WrapperViewList.LifeCycleListener; + +/** + * Even though this is a FrameLayout subclass we it is called a ListView. This + * is because of 2 reasons. 1. It acts like as ListView 2. It used to be a + * ListView subclass and i did not was to change to name causing compatibility + * errors. + * + * @author Emil Sjölander + */ +public class StickyListHeadersListView extends FrameLayout { + + public interface OnHeaderClickListener { + public void onHeaderClick(StickyListHeadersListView l, View header, + int itemPosition, long headerId, boolean currentlySticky); + } + + /** + * Notifies the listener when the sticky headers top offset has changed. + */ + public interface OnStickyHeaderOffsetChangedListener { + /** + * @param l The view parent + * @param header The currently sticky header being offset. + * This header is not guaranteed to have it's measurements set. + * It is however guaranteed that this view has been measured, + * therefor you should user getMeasured* methods instead of + * get* methods for determining the view's size. + * @param offset The amount the sticky header is offset by towards to top of the screen. + */ + public void onStickyHeaderOffsetChanged(StickyListHeadersListView l, View header, int offset); + } + + /* --- Children --- */ + private WrapperViewList mList; + private View mHeader; + + /* --- Header state --- */ + private Long mHeaderId; + // used to not have to call getHeaderId() all the time + private Integer mHeaderPosition; + private Integer mHeaderOffset; + + /* --- Delegates --- */ + private OnScrollListener mOnScrollListenerDelegate; + private AdapterWrapper mAdapter; + + /* --- Settings --- */ + private boolean mAreHeadersSticky = true; + private boolean mClippingToPadding = true; + private boolean mIsDrawingListUnderStickyHeader = true; + private int mPaddingLeft = 0; + private int mPaddingTop = 0; + private int mPaddingRight = 0; + private int mPaddingBottom = 0; + + /* --- Other --- */ + private OnHeaderClickListener mOnHeaderClickListener; + private OnStickyHeaderOffsetChangedListener mOnStickyHeaderOffsetChangedListener; + private AdapterWrapperDataSetObserver mDataSetObserver; + private Drawable mDivider; + private int mDividerHeight; + + public StickyListHeadersListView(Context context) { + this(context, null); + } + + public StickyListHeadersListView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public StickyListHeadersListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Initialize the wrapped list + mList = new WrapperViewList(context); + + // null out divider, dividers are handled by adapter so they look good with headers + mDivider = mList.getDivider(); + mDividerHeight = mList.getDividerHeight(); + mList.setDivider(null); + mList.setDividerHeight(0); + + mList.setVerticalScrollBarEnabled(isVerticalScrollBarEnabled()); + mList.setHorizontalScrollBarEnabled(isHorizontalScrollBarEnabled()); + + if (attrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(attrs,R.styleable.StickyListHeadersListView, 0, 0); + + try { + // -- View attributes -- + int padding = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_padding, 0); + mPaddingLeft = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingLeft, padding); + mPaddingTop = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingTop, padding); + mPaddingRight = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingRight, padding); + mPaddingBottom = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingBottom, padding); + + setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, + mPaddingBottom); + + // Set clip to padding on the list and reset value to default on + // wrapper + mClippingToPadding = a.getBoolean(R.styleable.StickyListHeadersListView_android_clipToPadding, true); + super.setClipToPadding(true); + mList.setClipToPadding(mClippingToPadding); + + // -- ListView attributes -- + mList.setFadingEdgeLength(a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_fadingEdgeLength, + mList.getVerticalFadingEdgeLength())); + final int fadingEdge = a.getInt(R.styleable.StickyListHeadersListView_android_requiresFadingEdge, 0); + if (fadingEdge == 0x00001000) { + mList.setVerticalFadingEdgeEnabled(false); + mList.setHorizontalFadingEdgeEnabled(true); + } else if (fadingEdge == 0x00002000) { + mList.setVerticalFadingEdgeEnabled(true); + mList.setHorizontalFadingEdgeEnabled(false); + } else { + mList.setVerticalFadingEdgeEnabled(false); + mList.setHorizontalFadingEdgeEnabled(false); + } + mList.setCacheColorHint(a + .getColor(R.styleable.StickyListHeadersListView_android_cacheColorHint, mList.getCacheColorHint())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mList.setChoiceMode(a.getInt(R.styleable.StickyListHeadersListView_android_choiceMode, + mList.getChoiceMode())); + } + mList.setDrawSelectorOnTop(a.getBoolean(R.styleable.StickyListHeadersListView_android_drawSelectorOnTop, false)); + mList.setFastScrollEnabled(a.getBoolean(R.styleable.StickyListHeadersListView_android_fastScrollEnabled, + mList.isFastScrollEnabled())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mList.setFastScrollAlwaysVisible(a.getBoolean( + R.styleable.StickyListHeadersListView_android_fastScrollAlwaysVisible, + mList.isFastScrollAlwaysVisible())); + } + + mList.setScrollBarStyle(a.getInt(R.styleable.StickyListHeadersListView_android_scrollbarStyle, 0)); + + if (a.hasValue(R.styleable.StickyListHeadersListView_android_listSelector)) { + mList.setSelector(a.getDrawable(R.styleable.StickyListHeadersListView_android_listSelector)); + } + + mList.setScrollingCacheEnabled(a.getBoolean(R.styleable.StickyListHeadersListView_android_scrollingCache, + mList.isScrollingCacheEnabled())); + + if (a.hasValue(R.styleable.StickyListHeadersListView_android_divider)) { + mDivider = a.getDrawable(R.styleable.StickyListHeadersListView_android_divider); + } + + mDividerHeight = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_dividerHeight, + mDividerHeight); + + // -- StickyListHeaders attributes -- + mAreHeadersSticky = a.getBoolean(R.styleable.StickyListHeadersListView_hasStickyHeaders, true); + mIsDrawingListUnderStickyHeader = a.getBoolean( + R.styleable.StickyListHeadersListView_isDrawingListUnderStickyHeader, + true); + } finally { + a.recycle(); + } + } + + // attach some listeners to the wrapped list + mList.setLifeCycleListener(new WrapperViewListLifeCycleListener()); + mList.setOnScrollListener(new WrapperListScrollListener()); + + addView(mList); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + measureHeader(mHeader); + } + + private void ensureHeaderHasCorrectLayoutParams(View header) { + ViewGroup.LayoutParams lp = header.getLayoutParams(); + if (lp == null) { + lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } else if (lp.height == LayoutParams.MATCH_PARENT) { + lp.height = LayoutParams.WRAP_CONTENT; + } + header.setLayoutParams(lp); + } + + private void measureHeader(View header) { + if (header != null) { + final int width = getMeasuredWidth() - mPaddingLeft - mPaddingRight; + final int parentWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + width, MeasureSpec.EXACTLY); + final int parentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, + MeasureSpec.UNSPECIFIED); + measureChild(header, parentWidthMeasureSpec, + parentHeightMeasureSpec); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, + int bottom) { + mList.layout(0, 0, mList.getMeasuredWidth(), getHeight()); + if (mHeader != null) { + MarginLayoutParams lp = (MarginLayoutParams) mHeader + .getLayoutParams(); + int headerTop = lp.topMargin + + (mClippingToPadding ? mPaddingTop : 0); + // The left parameter must for some reason be set to 0. + // I think it should be set to mPaddingLeft but apparently not + mHeader.layout(mPaddingLeft, headerTop, mHeader.getMeasuredWidth() + + mPaddingLeft, headerTop + mHeader.getMeasuredHeight()); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + // Only draw the list here. + // The header should be drawn right after the lists children are drawn. + // This is done so that the header is above the list items + // but below the list decorators (scroll bars etc). + drawChild(canvas, mList, 0); + } + + // Reset values tied the header. also remove header form layout + // This is called in response to the data set or the adapter changing + private void clearHeader() { + if (mHeader != null) { + removeView(mHeader); + mHeader = null; + mHeaderId = null; + mHeaderPosition = null; + mHeaderOffset = null; + + // reset the top clipping length + mList.setTopClippingLength(0); + updateHeaderVisibilities(); + } + } + + private void updateOrClearHeader(int firstVisiblePosition) { + final int adapterCount = mAdapter == null ? 0 : mAdapter.getCount(); + if (adapterCount == 0 || !mAreHeadersSticky) { + return; + } + + final int headerViewCount = mList.getHeaderViewsCount(); + final int realFirstVisibleItem = firstVisiblePosition - headerViewCount; + + // It is not a mistake to call getFirstVisiblePosition() here. + // Most of the time getFixedFirstVisibleItem() should be called + // but that does not work great together with getChildAt() + final boolean doesListHaveChildren = mList.getChildCount() != 0; + final boolean isFirstViewBelowTop = doesListHaveChildren && mList + .getFirstVisiblePosition() == 0 + && mList.getChildAt(0).getTop() > (mClippingToPadding ? mPaddingTop : 0); + final boolean isFirstVisibleItemOutsideAdapterRange = realFirstVisibleItem > adapterCount - 1 + || realFirstVisibleItem < 0; + if (!doesListHaveChildren || isFirstVisibleItemOutsideAdapterRange + || isFirstViewBelowTop) { + clearHeader(); + return; + } + + updateHeader(realFirstVisibleItem); + } + + private void updateHeader(int firstVisiblePosition) { + + // check if there is a new header should be sticky + if (mHeaderPosition == null || mHeaderPosition != firstVisiblePosition) { + mHeaderPosition = firstVisiblePosition; + final long headerId = mAdapter.getHeaderId(firstVisiblePosition); + if (mHeaderId == null || mHeaderId != headerId) { + mHeaderId = headerId; + final View header = mAdapter.getHeaderView(mHeaderPosition, + mHeader, this); + if (mHeader != header) { + if (header == null) { + throw new NullPointerException("header may not be null"); + } + swapHeader(header); + } + + ensureHeaderHasCorrectLayoutParams(mHeader); + measureHeader(mHeader); + + // Reset mHeaderOffset to null ensuring + // that it will be set on the header and + // not skipped for performance reasons. + mHeaderOffset = null; + } + } + + int headerOffset = 0; + + // Calculate new header offset + // Skip looking at the first view. it never matters because it always + // results in a headerOffset = 0 + int headerBottom = mHeader.getMeasuredHeight() + + (mClippingToPadding ? mPaddingTop : 0); + for (int i = 0; i < mList.getChildCount(); i++) { + final View child = mList.getChildAt(i); + final boolean doesChildHaveHeader = child instanceof WrapperView + && ((WrapperView) child).hasHeader(); + final boolean isChildFooter = mList.containsFooterView(child); + if (child.getTop() >= (mClippingToPadding ? mPaddingTop : 0) + && (doesChildHaveHeader || isChildFooter)) { + headerOffset = Math.min(child.getTop() - headerBottom, 0); + break; + } + } + + setHeaderOffet(headerOffset); + + if (!mIsDrawingListUnderStickyHeader) { + mList.setTopClippingLength(mHeader.getMeasuredHeight() + + mHeaderOffset); + } + + updateHeaderVisibilities(); + } + + private void swapHeader(View newHeader) { + if (mHeader != null) { + removeView(mHeader); + } + mHeader = newHeader; + addView(mHeader); + mHeader.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + if (mOnHeaderClickListener != null) { + mOnHeaderClickListener.onHeaderClick( + StickyListHeadersListView.this, mHeader, + mHeaderPosition, mHeaderId, true); + } + } + + }); + } + + // hides the headers in the list under the sticky header. + // Makes sure the other ones are showing + private void updateHeaderVisibilities() { + int top; + if (mHeader != null) { + top = mHeader.getMeasuredHeight() + + (mHeaderOffset != null ? mHeaderOffset : 0); + } else { + top = mClippingToPadding ? mPaddingTop : 0; + } + int childCount = mList.getChildCount(); + for (int i = 0; i < childCount; i++) { + + // ensure child is a wrapper view + View child = mList.getChildAt(i); + if (!(child instanceof WrapperView)) { + continue; + } + + // ensure wrapper view child has a header + WrapperView wrapperViewChild = (WrapperView) child; + if (!wrapperViewChild.hasHeader()) { + continue; + } + + // update header views visibility + View childHeader = wrapperViewChild.mHeader; + if (wrapperViewChild.getTop() < top) { + if (childHeader.getVisibility() != View.INVISIBLE) { + childHeader.setVisibility(View.INVISIBLE); + } + } else { + if (childHeader.getVisibility() != View.VISIBLE) { + childHeader.setVisibility(View.VISIBLE); + } + } + } + } + + // Wrapper around setting the header offset in different ways depending on + // the API version + @SuppressLint("NewApi") + private void setHeaderOffet(int offset) { + if (mHeaderOffset == null || mHeaderOffset != offset) { + mHeaderOffset = offset; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + mHeader.setTranslationY(mHeaderOffset); + } else { + MarginLayoutParams params = (MarginLayoutParams) mHeader + .getLayoutParams(); + params.topMargin = mHeaderOffset; + mHeader.setLayoutParams(params); + } + if (mOnStickyHeaderOffsetChangedListener != null) { + mOnStickyHeaderOffsetChangedListener + .onStickyHeaderOffsetChanged(this, mHeader, -mHeaderOffset); + } + } + } + + private class AdapterWrapperDataSetObserver extends DataSetObserver { + + @Override + public void onChanged() { + clearHeader(); + } + + @Override + public void onInvalidated() { + clearHeader(); + } + + } + + private class WrapperListScrollListener implements OnScrollListener { + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + if (mOnScrollListenerDelegate != null) { + mOnScrollListenerDelegate.onScroll(view, firstVisibleItem, + visibleItemCount, totalItemCount); + } + updateOrClearHeader(mList.getFixedFirstVisibleItem()); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (mOnScrollListenerDelegate != null) { + mOnScrollListenerDelegate.onScrollStateChanged(view, + scrollState); + } + } + + } + + private class WrapperViewListLifeCycleListener implements LifeCycleListener { + + @Override + public void onDispatchDrawOccurred(Canvas canvas) { + // onScroll is not called often at all before froyo + // therefor we need to update the header here as well. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { + updateOrClearHeader(mList.getFixedFirstVisibleItem()); + } + if (mHeader != null) { + if (mClippingToPadding) { + canvas.save(); + canvas.clipRect(0, mPaddingTop, getRight(), getBottom()); + drawChild(canvas, mHeader, 0); + canvas.restore(); + } else { + drawChild(canvas, mHeader, 0); + } + } + } + + } + + private class AdapterWrapperHeaderClickHandler implements + AdapterWrapper.OnHeaderClickListener { + + @Override + public void onHeaderClick(View header, int itemPosition, long headerId) { + mOnHeaderClickListener.onHeaderClick( + StickyListHeadersListView.this, header, itemPosition, + headerId, false); + } + + } + + private boolean isStartOfSection(int position) { + return position == 0 + || mAdapter.getHeaderId(position) != mAdapter + .getHeaderId(position - 1); + } + + private int getHeaderOverlap(int position) { + boolean isStartOfSection = isStartOfSection(position); + if (!isStartOfSection) { + View header = mAdapter.getHeaderView(position, null, mList); + if (header == null) { + throw new NullPointerException("header may not be null"); + } + ensureHeaderHasCorrectLayoutParams(header); + measureHeader(header); + return header.getMeasuredHeight(); + } + return 0; + } + + /* ---------- StickyListHeaders specific API ---------- */ + + public void setAreHeadersSticky(boolean areHeadersSticky) { + mAreHeadersSticky = areHeadersSticky; + if (!areHeadersSticky) { + clearHeader(); + } else { + updateOrClearHeader(mList.getFixedFirstVisibleItem()); + } + // invalidating the list will trigger dispatchDraw() + mList.invalidate(); + } + + public boolean areHeadersSticky() { + return mAreHeadersSticky; + } + + /** + * Use areHeadersSticky() method instead + */ + @Deprecated + public boolean getAreHeadersSticky() { + return areHeadersSticky(); + } + + public void setDrawingListUnderStickyHeader( + boolean drawingListUnderStickyHeader) { + mIsDrawingListUnderStickyHeader = drawingListUnderStickyHeader; + // reset the top clipping length + mList.setTopClippingLength(0); + } + + public boolean isDrawingListUnderStickyHeader() { + return mIsDrawingListUnderStickyHeader; + } + + public void setOnHeaderClickListener(OnHeaderClickListener listener) { + mOnHeaderClickListener = listener; + if (mAdapter != null) { + if (mOnHeaderClickListener != null) { + mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler()); + } else { + mAdapter.setOnHeaderClickListener(null); + } + } + } + + public void setOnStickyHeaderOffsetChangedListener(OnStickyHeaderOffsetChangedListener listener) { + mOnStickyHeaderOffsetChangedListener = listener; + } + + public View getListChildAt(int index) { + return mList.getChildAt(index); + } + + public int getListChildCount() { + return mList.getChildCount(); + } + + /** + * Use the method with extreme caution!! Changing any values on the + * underlying ListView might break everything. + * + * @return the ListView backing this view. + */ + public ListView getWrappedList() { + return mList; + } + + /* ---------- ListView delegate methods ---------- */ + + public void setAdapter(StickyListHeadersAdapter adapter) { + if (adapter == null) { + mList.setAdapter(null); + clearHeader(); + return; + } + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mDataSetObserver); + } + + if (adapter instanceof SectionIndexer) { + mAdapter = new SectionIndexerAdapterWrapper(getContext(), adapter); + } else { + mAdapter = new AdapterWrapper(getContext(), adapter); + } + mDataSetObserver = new AdapterWrapperDataSetObserver(); + mAdapter.registerDataSetObserver(mDataSetObserver); + + if (mOnHeaderClickListener != null) { + mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler()); + } else { + mAdapter.setOnHeaderClickListener(null); + } + + mAdapter.setDivider(mDivider, mDividerHeight); + + mList.setAdapter(mAdapter); + clearHeader(); + } + + public StickyListHeadersAdapter getAdapter() { + return mAdapter == null ? null : mAdapter.mDelegate; + } + + public void setDivider(Drawable divider) { + mDivider = divider; + if (mAdapter != null) { + mAdapter.setDivider(mDivider, mDividerHeight); + } + } + + public void setDividerHeight(int dividerHeight) { + mDividerHeight = dividerHeight; + if (mAdapter != null) { + mAdapter.setDivider(mDivider, mDividerHeight); + } + } + + public Drawable getDivider() { + return mDivider; + } + + public int getDividerHeight() { + return mDividerHeight; + } + + public void setOnScrollListener(OnScrollListener onScrollListener) { + mOnScrollListenerDelegate = onScrollListener; + } + + public void setOnItemClickListener(OnItemClickListener listener) { + mList.setOnItemClickListener(listener); + } + + public void setOnItemLongClickListener(OnItemLongClickListener listener) { + mList.setOnItemLongClickListener(listener); + } + + public void addHeaderView(View v, Object data, boolean isSelectable) { + mList.addHeaderView(v, data, isSelectable); + } + + public void addHeaderView(View v) { + mList.addHeaderView(v); + } + + public void removeHeaderView(View v) { + mList.removeHeaderView(v); + } + + public int getHeaderViewsCount() { + return mList.getHeaderViewsCount(); + } + + public void addFooterView(View v) { + mList.addFooterView(v); + } + + public void removeFooterView(View v) { + mList.removeFooterView(v); + } + + public int getFooterViewsCount() { + return mList.getFooterViewsCount(); + } + + public void setEmptyView(View v) { + mList.setEmptyView(v); + } + + public View getEmptyView() { + return mList.getEmptyView(); + } + + @Override + public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) { + mList.setVerticalScrollBarEnabled(verticalScrollBarEnabled); + } + + @Override + public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) { + mList.setHorizontalScrollBarEnabled(horizontalScrollBarEnabled); + } + + @TargetApi(Build.VERSION_CODES.FROYO) + public void smoothScrollBy(int distance, int duration) { + requireSdkVersion(Build.VERSION_CODES.FROYO); + mList.smoothScrollBy(distance, duration); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void smoothScrollByOffset(int offset) { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + mList.smoothScrollByOffset(offset); + } + + @SuppressLint("NewApi") + @TargetApi(Build.VERSION_CODES.FROYO) + public void smoothScrollToPosition(int position) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + mList.smoothScrollToPosition(position); + } else { + int offset = mAdapter == null ? 0 : getHeaderOverlap(position); + offset -= mClippingToPadding ? 0 : mPaddingTop; + mList.smoothScrollToPositionFromTop(position, offset); + } + } + + @TargetApi(Build.VERSION_CODES.FROYO) + public void smoothScrollToPosition(int position, int boundPosition) { + requireSdkVersion(Build.VERSION_CODES.FROYO); + mList.smoothScrollToPosition(position, boundPosition); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void smoothScrollToPositionFromTop(int position, int offset) { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + offset += mAdapter == null ? 0 : getHeaderOverlap(position); + offset -= mClippingToPadding ? 0 : mPaddingTop; + mList.smoothScrollToPositionFromTop(position, offset); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void smoothScrollToPositionFromTop(int position, int offset, + int duration) { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + offset += mAdapter == null ? 0 : getHeaderOverlap(position); + offset -= mClippingToPadding ? 0 : mPaddingTop; + mList.smoothScrollToPositionFromTop(position, offset, duration); + } + + public void setSelection(int position) { + setSelectionFromTop(position, 0); + } + + public void setSelectionAfterHeaderView() { + mList.setSelectionAfterHeaderView(); + } + + public void setSelectionFromTop(int position, int y) { + y += mAdapter == null ? 0 : getHeaderOverlap(position); + y -= mClippingToPadding ? 0 : mPaddingTop; + mList.setSelectionFromTop(position, y); + } + + public void setSelector(Drawable sel) { + mList.setSelector(sel); + } + + public void setSelector(int resID) { + mList.setSelector(resID); + } + + public int getFirstVisiblePosition() { + return mList.getFirstVisiblePosition(); + } + + public int getLastVisiblePosition() { + return mList.getLastVisiblePosition(); + } + + public void setChoiceMode(int choiceMode) { + mList.setChoiceMode(choiceMode); + } + + public void setItemChecked(int position, boolean value) { + mList.setItemChecked(position, value); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public int getCheckedItemCount() { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + return mList.getCheckedItemCount(); + } + + @TargetApi(Build.VERSION_CODES.FROYO) + public long[] getCheckedItemIds() { + requireSdkVersion(Build.VERSION_CODES.FROYO); + return mList.getCheckedItemIds(); + } + + public int getCheckedItemPosition() { + return mList.getCheckedItemPosition(); + } + + public SparseBooleanArray getCheckedItemPositions() { + return mList.getCheckedItemPositions(); + } + + public int getCount() { + return mList.getCount(); + } + + public Object getItemAtPosition(int position) { + return mList.getItemAtPosition(position); + } + + public long getItemIdAtPosition(int position) { + return mList.getItemIdAtPosition(position); + } + + @Override + public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) { + mList.setOnCreateContextMenuListener(l); + } + + @Override + public boolean showContextMenu() { + return mList.showContextMenu(); + } + + public void invalidateViews() { + mList.invalidateViews(); + } + + @Override + public void setClipToPadding(boolean clipToPadding) { + if (mList != null) { + mList.setClipToPadding(clipToPadding); + } + mClippingToPadding = clipToPadding; + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + mPaddingLeft = left; + mPaddingTop = top; + mPaddingRight = right; + mPaddingBottom = bottom; + + if (mList != null) { + mList.setPadding(left, top, right, bottom); + } + super.setPadding(0, 0, 0, 0); + requestLayout(); + } + + /* + * Overrides an @hide method in View + */ + protected void recomputePadding() { + setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom); + } + + @Override + public int getPaddingLeft() { + return mPaddingLeft; + } + + @Override + public int getPaddingTop() { + return mPaddingTop; + } + + @Override + public int getPaddingRight() { + return mPaddingRight; + } + + @Override + public int getPaddingBottom() { + return mPaddingBottom; + } + + public void setFastScrollEnabled(boolean fastScrollEnabled) { + mList.setFastScrollEnabled(fastScrollEnabled); + } + + /** + * @throws ApiLevelTooLowException on pre-Honeycomb device. + * @see android.widget.AbsListView#setFastScrollAlwaysVisible(boolean) + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void setFastScrollAlwaysVisible(boolean alwaysVisible) { + requireSdkVersion(Build.VERSION_CODES.HONEYCOMB); + mList.setFastScrollAlwaysVisible(alwaysVisible); + } + + /** + * @return true if the fast scroller will always show. False on pre-Honeycomb devices. + * @see android.widget.AbsListView#isFastScrollAlwaysVisible() + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public boolean isFastScrollAlwaysVisible() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + return false; + } + return mList.isFastScrollAlwaysVisible(); + } + + public void setScrollBarStyle(int style) { + mList.setScrollBarStyle(style); + } + + public int getScrollBarStyle() { + return mList.getScrollBarStyle(); + } + + private void requireSdkVersion(int versionCode) { + if (Build.VERSION.SDK_INT < versionCode) { + throw new ApiLevelTooLowException(versionCode); + } + } + + public int getPositionForView(View view) { + return mList.getPositionForView(view); + } + +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperView.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperView.java new file mode 100644 index 000000000..f51416c1c --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperView.java @@ -0,0 +1,150 @@ +package se.emilsjolander.stickylistheaders; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +/** + * + * the view that wrapps a divider header and a normal list item. The listview sees this as 1 item + * + * @author Emil Sjölander + */ +public class WrapperView extends ViewGroup { + + View mItem; + Drawable mDivider; + int mDividerHeight; + View mHeader; + int mItemTop; + + WrapperView(Context c) { + super(c); + } + + public boolean hasHeader() { + return mHeader != null; + } + + public View getItem() { + return mItem; + } + + public View getHeader() { + return mHeader; + } + + void update(View item, View header, Drawable divider, int dividerHeight) { + + //every wrapperview must have a list item + if (item == null) { + throw new NullPointerException("List view item must not be null."); + } + + //only remove the current item if it is not the same as the new item. this can happen if wrapping a recycled view + if (this.mItem != item) { + removeView(this.mItem); + this.mItem = item; + final ViewParent parent = item.getParent(); + if(parent != null && parent != this) { + if(parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(item); + } + } + addView(item); + } + + //same logik as above but for the header + if (this.mHeader != header) { + if (this.mHeader != null) { + removeView(this.mHeader); + } + this.mHeader = header; + if (header != null) { + addView(header); + } + } + + if (this.mDivider != divider) { + this.mDivider = divider; + this.mDividerHeight = dividerHeight; + invalidate(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, + MeasureSpec.EXACTLY); + int measuredHeight = 0; + + //measure header or divider. when there is a header visible it acts as the divider + if (mHeader != null) { + ViewGroup.LayoutParams params = mHeader.getLayoutParams(); + if (params != null && params.height > 0) { + mHeader.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY)); + } else { + mHeader.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + } + measuredHeight += mHeader.getMeasuredHeight(); + } else if (mDivider != null) { + measuredHeight += mDividerHeight; + } + + //measure item + ViewGroup.LayoutParams params = mItem.getLayoutParams(); + if (params != null && params.height > 0) { + mItem.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY)); + } else { + mItem.measure(childWidthMeasureSpec, + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + } + measuredHeight += mItem.getMeasuredHeight(); + + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + + l = 0; + t = 0; + r = getWidth(); + b = getHeight(); + + if (mHeader != null) { + int headerHeight = mHeader.getMeasuredHeight(); + mHeader.layout(l, t, r, headerHeight); + mItemTop = headerHeight; + mItem.layout(l, headerHeight, r, b); + } else if (mDivider != null) { + mDivider.setBounds(l, t, r, mDividerHeight); + mItemTop = mDividerHeight; + mItem.layout(l, mDividerHeight, r, b); + } else { + mItemTop = t; + mItem.layout(l, t, r, b); + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + super.dispatchDraw(canvas); + if (mHeader == null && mDivider != null) { + // Drawable.setBounds() does not seem to work pre-honeycomb. So have + // to do this instead + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { + canvas.clipRect(0, 0, getWidth(), mDividerHeight); + } + mDivider.draw(canvas); + } + } +} diff --git a/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperViewList.java b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperViewList.java new file mode 100644 index 000000000..3d68e98db --- /dev/null +++ b/libraries/StickyListHeaders/library/src/se/emilsjolander/stickylistheaders/WrapperViewList.java @@ -0,0 +1,175 @@ +package se.emilsjolander.stickylistheaders; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; +import android.widget.AbsListView; +import android.widget.ListView; + +class WrapperViewList extends ListView { + + interface LifeCycleListener { + void onDispatchDrawOccurred(Canvas canvas); + } + + private LifeCycleListener mLifeCycleListener; + private List mFooterViews; + private int mTopClippingLength; + private Rect mSelectorRect = new Rect();// for if reflection fails + private Field mSelectorPositionField; + private boolean mClippingToPadding = true; + + public WrapperViewList(Context context) { + super(context); + + // Use reflection to be able to change the size/position of the list + // selector so it does not come under/over the header + try { + Field selectorRectField = AbsListView.class.getDeclaredField("mSelectorRect"); + selectorRectField.setAccessible(true); + mSelectorRect = (Rect) selectorRectField.get(this); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + mSelectorPositionField = AbsListView.class.getDeclaredField("mSelectorPosition"); + mSelectorPositionField.setAccessible(true); + } + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + + @Override + public boolean performItemClick(View view, int position, long id) { + if (view instanceof WrapperView) { + view = ((WrapperView) view).mItem; + } + return super.performItemClick(view, position, id); + } + + private void positionSelectorRect() { + if (!mSelectorRect.isEmpty()) { + int selectorPosition = getSelectorPosition(); + if (selectorPosition >= 0) { + int firstVisibleItem = getFixedFirstVisibleItem(); + View v = getChildAt(selectorPosition - firstVisibleItem); + if (v instanceof WrapperView) { + WrapperView wrapper = ((WrapperView) v); + mSelectorRect.top = wrapper.getTop() + wrapper.mItemTop; + } + } + } + } + + private int getSelectorPosition() { + if (mSelectorPositionField == null) { // not all supported andorid + // version have this variable + for (int i = 0; i < getChildCount(); i++) { + if (getChildAt(i).getBottom() == mSelectorRect.bottom) { + return i + getFixedFirstVisibleItem(); + } + } + } else { + try { + return mSelectorPositionField.getInt(this); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + return -1; + } + + @Override + protected void dispatchDraw(Canvas canvas) { + positionSelectorRect(); + if (mTopClippingLength != 0) { + canvas.save(); + Rect clipping = canvas.getClipBounds(); + clipping.top = mTopClippingLength; + canvas.clipRect(clipping); + super.dispatchDraw(canvas); + canvas.restore(); + } else { + super.dispatchDraw(canvas); + } + mLifeCycleListener.onDispatchDrawOccurred(canvas); + } + + void setLifeCycleListener(LifeCycleListener lifeCycleListener) { + mLifeCycleListener = lifeCycleListener; + } + + @Override + public void addFooterView(View v) { + super.addFooterView(v); + if (mFooterViews == null) { + mFooterViews = new ArrayList(); + } + mFooterViews.add(v); + } + + @Override + public boolean removeFooterView(View v) { + if (super.removeFooterView(v)) { + mFooterViews.remove(v); + return true; + } + return false; + } + + boolean containsFooterView(View v) { + if (mFooterViews == null) { + return false; + } + return mFooterViews.contains(v); + } + + void setTopClippingLength(int topClipping) { + mTopClippingLength = topClipping; + } + + int getFixedFirstVisibleItem() { + int firstVisibleItem = getFirstVisiblePosition(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + return firstVisibleItem; + } + + // first getFirstVisiblePosition() reports items + // outside the view sometimes on old versions of android + for (int i = 0; i < getChildCount(); i++) { + if (getChildAt(i).getBottom() >= 0) { + firstVisibleItem += i; + break; + } + } + + // work around to fix bug with firstVisibleItem being to high + // because list view does not take clipToPadding=false into account + // on old versions of android + if (!mClippingToPadding && getPaddingTop() > 0 && firstVisibleItem > 0) { + if (getChildAt(0).getTop() > 0) { + firstVisibleItem -= 1; + } + } + + return firstVisibleItem; + } + + @Override + public void setClipToPadding(boolean clipToPadding) { + mClippingToPadding = clipToPadding; + super.setClipToPadding(clipToPadding); + } + +} diff --git a/libraries/StickyListHeaders/sample/AndroidManifest.xml b/libraries/StickyListHeaders/sample/AndroidManifest.xml new file mode 100644 index 000000000..4bf61a441 --- /dev/null +++ b/libraries/StickyListHeaders/sample/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libraries/StickyListHeaders/sample/build.gradle b/libraries/StickyListHeaders/sample/build.gradle new file mode 100644 index 000000000..b620008e3 --- /dev/null +++ b/libraries/StickyListHeaders/sample/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'android' + +repositories { + mavenCentral() +} +dependencies { + compile project(':library') + compile 'com.android.support:appcompat-v7:19.0.+' + compile 'com.android.support:support-v4:19.0.0' +} + +android { + compileSdkVersion 19 + buildToolsVersion '19.0.0' + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src'] + res.srcDirs = ['res'] + } + } +} diff --git a/libraries/StickyListHeaders/sample/libs/android-support-v4.jar b/libraries/StickyListHeaders/sample/libs/android-support-v4.jar new file mode 100644 index 000000000..cf12d2839 Binary files /dev/null and b/libraries/StickyListHeaders/sample/libs/android-support-v4.jar differ diff --git a/libraries/StickyListHeaders/sample/project.properties b/libraries/StickyListHeaders/sample/project.properties new file mode 100644 index 000000000..a6cf15dae --- /dev/null +++ b/libraries/StickyListHeaders/sample/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-19 +android.library.reference.1=../library diff --git a/libraries/StickyListHeaders/sample/res/drawable-hdpi/ic_drawer.png b/libraries/StickyListHeaders/sample/res/drawable-hdpi/ic_drawer.png new file mode 100644 index 000000000..6614ea4f4 Binary files /dev/null and b/libraries/StickyListHeaders/sample/res/drawable-hdpi/ic_drawer.png differ diff --git a/libraries/StickyListHeaders/sample/res/drawable-hdpi/ic_launcher.png b/libraries/StickyListHeaders/sample/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 000000000..96a442e5b Binary files /dev/null and b/libraries/StickyListHeaders/sample/res/drawable-hdpi/ic_launcher.png differ diff --git a/libraries/StickyListHeaders/sample/res/drawable-ldpi/ic_launcher.png b/libraries/StickyListHeaders/sample/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 000000000..99238729d Binary files /dev/null and b/libraries/StickyListHeaders/sample/res/drawable-ldpi/ic_launcher.png differ diff --git a/libraries/StickyListHeaders/sample/res/drawable-mdpi/ic_drawer.png b/libraries/StickyListHeaders/sample/res/drawable-mdpi/ic_drawer.png new file mode 100644 index 000000000..b05c026c1 Binary files /dev/null and b/libraries/StickyListHeaders/sample/res/drawable-mdpi/ic_drawer.png differ diff --git a/libraries/StickyListHeaders/sample/res/drawable-mdpi/ic_launcher.png b/libraries/StickyListHeaders/sample/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 000000000..359047dfa Binary files /dev/null and b/libraries/StickyListHeaders/sample/res/drawable-mdpi/ic_launcher.png differ diff --git a/libraries/StickyListHeaders/sample/res/drawable-xhdpi/ic_drawer.png b/libraries/StickyListHeaders/sample/res/drawable-xhdpi/ic_drawer.png new file mode 100644 index 000000000..bcf49dd73 Binary files /dev/null and b/libraries/StickyListHeaders/sample/res/drawable-xhdpi/ic_drawer.png differ diff --git a/libraries/StickyListHeaders/sample/res/drawable-xhdpi/ic_launcher.png b/libraries/StickyListHeaders/sample/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 000000000..71c6d760f Binary files /dev/null and b/libraries/StickyListHeaders/sample/res/drawable-xhdpi/ic_launcher.png differ diff --git a/libraries/StickyListHeaders/sample/res/drawable/header_selector.xml b/libraries/StickyListHeaders/sample/res/drawable/header_selector.xml new file mode 100644 index 000000000..5dfb8265c --- /dev/null +++ b/libraries/StickyListHeaders/sample/res/drawable/header_selector.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/libraries/StickyListHeaders/sample/res/layout/header.xml b/libraries/StickyListHeaders/sample/res/layout/header.xml new file mode 100644 index 000000000..177e40c4e --- /dev/null +++ b/libraries/StickyListHeaders/sample/res/layout/header.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/libraries/StickyListHeaders/sample/res/layout/list_footer.xml b/libraries/StickyListHeaders/sample/res/layout/list_footer.xml new file mode 100644 index 000000000..4fa22c1f3 --- /dev/null +++ b/libraries/StickyListHeaders/sample/res/layout/list_footer.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/libraries/StickyListHeaders/sample/res/layout/list_header.xml b/libraries/StickyListHeaders/sample/res/layout/list_header.xml new file mode 100644 index 000000000..97d46c69b --- /dev/null +++ b/libraries/StickyListHeaders/sample/res/layout/list_header.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/libraries/StickyListHeaders/sample/res/layout/main.xml b/libraries/StickyListHeaders/sample/res/layout/main.xml new file mode 100644 index 000000000..2d22c3266 --- /dev/null +++ b/libraries/StickyListHeaders/sample/res/layout/main.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/sign_key_layout.xml b/OpenPGP-Keychain/res/layout/sign_key_layout.xml deleted file mode 100644 index 4530831ee..000000000 --- a/OpenPGP-Keychain/res/layout/sign_key_layout.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/OpenPGP-Keychain/res/layout/stickylist_header.xml b/OpenPGP-Keychain/res/layout/stickylist_header.xml deleted file mode 100644 index 5768e4153..000000000 --- a/OpenPGP-Keychain/res/layout/stickylist_header.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/view_key_activity.xml b/OpenPGP-Keychain/res/layout/view_key_activity.xml new file mode 100644 index 000000000..ffb2400f1 --- /dev/null +++ b/OpenPGP-Keychain/res/layout/view_key_activity.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/view_key_keys_item.xml b/OpenPGP-Keychain/res/layout/view_key_keys_item.xml new file mode 100644 index 000000000..b50253980 --- /dev/null +++ b/OpenPGP-Keychain/res/layout/view_key_keys_item.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/view_key_userids_item.xml b/OpenPGP-Keychain/res/layout/view_key_userids_item.xml new file mode 100644 index 000000000..cbc85189f --- /dev/null +++ b/OpenPGP-Keychain/res/layout/view_key_userids_item.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java index 87229f6c3..6cc0b3b5a 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/DecryptActivity.java @@ -230,7 +230,7 @@ public class DecryptActivity extends DrawerActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.decrypt); + setContentView(R.layout.decrypt_activity); // set actionbar without home button if called from another app ActionBarHelper.setBackButton(this); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java index b79200ba1..be2e4115b 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EditKeyActivity.java @@ -429,7 +429,7 @@ public class EditKeyActivity extends SherlockFragmentActivity { * id and key. */ private void buildLayout() { - setContentView(R.layout.edit_key); + setContentView(R.layout.edit_key_activity); // find views mChangePassPhrase = (BootstrapButton) findViewById(R.id.edit_key_btn_change_pass_phrase); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java index 24caebb3a..c974dfd46 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/EncryptActivity.java @@ -172,7 +172,7 @@ public class EncryptActivity extends DrawerActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.encrypt); + setContentView(R.layout.encrypt_activity); // set actionbar without home button if called from another app ActionBarHelper.setBackButton(this); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java index e7a977707..840ebb650 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/HelpFragmentAbout.java @@ -48,7 +48,7 @@ public class HelpFragmentAbout extends SherlockFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.help_fragment_about, container, false); + View view = inflater.inflate(R.layout.help_about_fragment, container, false); TextView versionText = (TextView) view.findViewById(R.id.help_about_version); versionText.setText(getString(R.string.help_about_version) + " " + getVersion()); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java index 1842364e7..7d8f4154f 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -83,7 +83,7 @@ public class ImportKeysActivity extends DrawerActivity implements OnNavigationLi protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.import_keys); + setContentView(R.layout.import_keys_activity); mImportButton = (BootstrapButton) findViewById(R.id.import_import); mImportButton.setOnClickListener(new OnClickListener() { diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java index 88503a5c4..ea088efca 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListPublicFragment.java @@ -259,7 +259,7 @@ public class KeyListPublicFragment extends Fragment implements AdapterView.OnIte */ @Override public void onItemClick(AdapterView adapterView, View view, int position, long id) { - Intent detailsIntent = new Intent(getActivity(), KeyViewActivity.class); + Intent detailsIntent = new Intent(getActivity(), ViewKeyActivity.class); detailsIntent.setData(KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(id))); startActivity(detailsIntent); } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java index b4679f9d5..6073e6b80 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerQueryActivity.java @@ -111,7 +111,7 @@ public class KeyServerQueryActivity extends SherlockFragmentActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.key_server_query_layout); + setContentView(R.layout.key_server_query); mQuery = (EditText)findViewById(R.id.query); mSearch = (Button)findViewById(R.id.btn_search); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java index 996637c7a..8a32ea513 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyServerUploadActivity.java @@ -76,7 +76,7 @@ public class KeyServerUploadActivity extends SherlockFragmentActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.key_server_export_layout); + setContentView(R.layout.key_server_export); export = (Button) findViewById(R.id.btn_export_to_server); keyServer = (Spinner) findViewById(R.id.keyServer); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyViewActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyViewActivity.java deleted file mode 100644 index 0e1e20cce..000000000 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyViewActivity.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright (C) 2013 Dominik Schürmann - * Copyright (C) 2013 Bahtiar 'kalkin' Gadimov - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program 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. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.sufficientlysecure.keychain.ui; - -import java.util.ArrayList; -import java.util.Date; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.spongycastle.openpgp.PGPPublicKey; -import org.spongycastle.openpgp.PGPPublicKeyRing; -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.Id; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; -import org.sufficientlysecure.keychain.helper.ExportHelper; -import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; -import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; -import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment; -import org.sufficientlysecure.keychain.ui.dialog.ShareQrCodeDialogFragment; -import org.sufficientlysecure.keychain.util.Log; - -import android.annotation.SuppressLint; -import android.content.Intent; -import android.net.Uri; -import android.nfc.NdefMessage; -import android.nfc.NdefRecord; -import android.nfc.NfcAdapter; -import android.nfc.NfcAdapter.CreateNdefMessageCallback; -import android.nfc.NfcAdapter.OnNdefPushCompleteCallback; -import android.nfc.NfcEvent; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.text.format.DateFormat; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.TextView; -import android.widget.Toast; - -import com.actionbarsherlock.app.SherlockFragmentActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; -import com.beardedhen.androidbootstrap.BootstrapButton; - -@SuppressLint("NewApi") -public class KeyViewActivity extends SherlockFragmentActivity implements CreateNdefMessageCallback, - OnNdefPushCompleteCallback { - - ExportHelper mExportHelper; - - private Uri mDataUri; - - private PGPPublicKey mPublicKey; - - private TextView mAlgorithm; - private TextView mFingerint; - private TextView mExpiry; - private TextView mCreation; - private BootstrapButton mActionEncrypt; - - // NFC - private NfcAdapter mNfcAdapter; - private byte[] mSharedKeyringBytes; - private static final int NFC_SENT = 1; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mExportHelper = new ExportHelper(this); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeButtonEnabled(true); - - setContentView(R.layout.key_view_activity); - - mFingerint = (TextView) this.findViewById(R.id.fingerprint); - mExpiry = (TextView) this.findViewById(R.id.expiry); - mCreation = (TextView) this.findViewById(R.id.creation); - mAlgorithm = (TextView) this.findViewById(R.id.algorithm); - mActionEncrypt = (BootstrapButton) this.findViewById(R.id.action_encrypt); - - Intent intent = getIntent(); - mDataUri = intent.getData(); - if (mDataUri == null) { - Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!"); - finish(); - return; - } else { - Log.d(Constants.TAG, "uri: " + mDataUri); - loadData(mDataUri); - initNfc(mDataUri); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - super.onCreateOptionsMenu(menu); - getSupportMenuInflater().inflate(R.menu.key_view, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_key_view_update: - updateFromKeyserver(mDataUri); - return true; - case R.id.menu_key_view_sign: - signKey(mDataUri); - return true; - case R.id.menu_key_view_export_keyserver: - uploadToKeyserver(mDataUri); - return true; - case R.id.menu_key_view_export_file: - mExportHelper.showExportKeysDialog(mDataUri, Id.type.public_key, Constants.path.APP_DIR - + "/pubexport.asc"); - return true; - case R.id.menu_key_view_share_default: - shareKey(mDataUri); - return true; - case R.id.menu_key_view_share_qr_code: - shareKeyQrCode(mDataUri); - return true; - case R.id.menu_key_view_share_nfc: - shareNfc(); - return true; - case R.id.menu_key_view_share_clipboard: - copyToClipboard(mDataUri); - return true; - case R.id.menu_key_view_delete: { - // Message is received after key is deleted - Handler returnHandler = new Handler() { - @Override - public void handleMessage(Message message) { - if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) { - setResult(RESULT_CANCELED); - finish(); - } - } - }; - - mExportHelper.deleteKey(mDataUri, Id.type.public_key, returnHandler); - return true; - } - } - return super.onOptionsItemSelected(item); - } - - private void loadData(Uri dataUri) { - PGPPublicKeyRing ring = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri); - mPublicKey = ring.getPublicKey(); - - mFingerint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper - .convertFingerprintToHex(mPublicKey.getFingerprint()))); - String[] mainUserId = splitUserId(""); - - Date expiryDate = PgpKeyHelper.getExpiryDate(mPublicKey); - if (expiryDate == null) { - mExpiry.setText(""); - } else { - mExpiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(expiryDate)); - } - - mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format( - PgpKeyHelper.getCreationDate(mPublicKey))); - mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(mPublicKey)); - - mActionEncrypt.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - long[] encryptionKeyIds = new long[] { mPublicKey.getKeyID() }; - Intent intent = new Intent(KeyViewActivity.this, EncryptActivity.class); - intent.setAction(EncryptActivity.ACTION_ENCRYPT); - intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); - // used instead of startActivity set actionbar based on callingPackage - startActivityForResult(intent, 0); - } - }); - } - - /** - * TODO: does this duplicate functionality from elsewhere? put in helper! - */ - private String[] splitUserId(String userId) { - String[] result = new String[] { "", "", "" }; - Log.v("UserID", userId); - - Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$"); - Matcher matcher = withComment.matcher(userId); - if (matcher.matches()) { - result[0] = matcher.group(1); - result[1] = matcher.group(2); - result[2] = matcher.group(3); - return result; - } - - Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); - matcher = withoutComment.matcher(userId); - if (matcher.matches()) { - result[0] = matcher.group(1); - result[1] = matcher.group(2); - return result; - } - return result; - } - - private void uploadToKeyserver(Uri dataUri) { - long keyRingRowId = Long.valueOf(dataUri.getLastPathSegment()); - - Intent uploadIntent = new Intent(this, KeyServerUploadActivity.class); - uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER); - uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, (int) keyRingRowId); - startActivityForResult(uploadIntent, Id.request.export_to_server); - } - - private void updateFromKeyserver(Uri dataUri) { - long updateKeyId = 0; - PGPPublicKeyRing updateRing = (PGPPublicKeyRing) ProviderHelper - .getPGPKeyRing(this, dataUri); - - if (updateRing != null) { - updateKeyId = PgpKeyHelper.getMasterKey(updateRing).getKeyID(); - } - if (updateKeyId == 0) { - Log.e(Constants.TAG, "this shouldn't happen. KeyId == 0!"); - return; - } - - Intent signIntent = new Intent(this, SignKeyActivity.class); - signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, updateKeyId); - startActivity(signIntent); - - Intent queryIntent = new Intent(this, KeyServerQueryActivity.class); - queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID_AND_RETURN); - queryIntent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, updateKeyId); - - // TODO: lookup?? - startActivityForResult(queryIntent, Id.request.look_up_key_id); - } - - private void signKey(Uri dataUri) { - long keyId = 0; - PGPPublicKeyRing signKey = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri); - - if (signKey != null) { - keyId = PgpKeyHelper.getMasterKey(signKey).getKeyID(); - } - if (keyId == 0) { - Log.e(Constants.TAG, "this shouldn't happen. KeyId == 0!"); - return; - } - - Intent signIntent = new Intent(this, SignKeyActivity.class); - signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, keyId); - startActivity(signIntent); - } - - private void shareKey(Uri dataUri) { - // get public keyring as ascii armored string - long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); - ArrayList keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this, dataUri, - new long[] { masterKeyId }); - - // let user choose application - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, keyringArmored.get(0)); - sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent, - getResources().getText(R.string.action_share_key_with))); - } - - private void shareKeyQrCode(Uri dataUri) { - // get public keyring as ascii armored string - long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); - ArrayList keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this, dataUri, - new long[] { masterKeyId }); - - ShareQrCodeDialogFragment dialog = ShareQrCodeDialogFragment.newInstance(keyringArmored - .get(0)); - dialog.show(getSupportFragmentManager(), "shareQrCodeDialog"); - } - - private void copyToClipboard(Uri dataUri) { - // get public keyring as ascii armored string - long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); - ArrayList keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this, dataUri, - new long[] { masterKeyId }); - - ClipboardReflection.copyToClipboard(this, keyringArmored.get(0)); - } - - private void shareNfc() { - ShareNfcDialogFragment dialog = ShareNfcDialogFragment.newInstance(); - dialog.show(getSupportFragmentManager(), "shareNfcDialog"); - } - - /** - * NFC: Initialize NFC sharing if OS and device supports it - */ - private void initNfc(Uri dataUri) { - // check if NFC Beam is supported (>= Android 4.1) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - // Check for available NFC Adapter - mNfcAdapter = NfcAdapter.getDefaultAdapter(this); - if (mNfcAdapter != null) { - // init nfc - - // get public keyring as byte array - long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); - mSharedKeyringBytes = ProviderHelper.getKeyRingsAsByteArray(this, dataUri, - new long[] { masterKeyId }); - - // Register callback to set NDEF message - mNfcAdapter.setNdefPushMessageCallback(this, this); - // Register callback to listen for message-sent success - mNfcAdapter.setOnNdefPushCompleteCallback(this, this); - } - } - } - - /** - * NFC: Implementation for the CreateNdefMessageCallback interface - */ - @Override - public NdefMessage createNdefMessage(NfcEvent event) { - /** - * When a device receives a push with an AAR in it, the application specified in the AAR is - * guaranteed to run. The AAR overrides the tag dispatch system. You can add it back in to - * guarantee that this activity starts when receiving a beamed message. For now, this code - * uses the tag dispatch system. - */ - NdefMessage msg = new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME, - mSharedKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME)); - return msg; - } - - /** - * NFC: Implementation for the OnNdefPushCompleteCallback interface - */ - @Override - public void onNdefPushComplete(NfcEvent arg0) { - // A handler is needed to send messages to the activity when this - // callback occurs, because it happens from a binder thread - mNfcHandler.obtainMessage(NFC_SENT).sendToTarget(); - } - - /** - * NFC: This handler receives a message from onNdefPushComplete - */ - private final Handler mNfcHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case NFC_SENT: - Toast.makeText(getApplicationContext(), R.string.nfc_successfull, Toast.LENGTH_LONG) - .show(); - break; - } - } - }; - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (!mExportHelper.handleActivityResult(requestCode, resultCode, data)) { - super.onActivityResult(requestCode, resultCode, data); - } - } - -} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java index c2fe1315b..6abdddee6 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/SignKeyActivity.java @@ -68,7 +68,7 @@ public class SignKeyActivity extends SherlockFragmentActivity { super.onCreate(savedInstanceState); // check we havent already signed it - setContentView(R.layout.sign_key_layout); + setContentView(R.layout.sign_key_activity); final ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayShowTitleEnabled(true); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java new file mode 100644 index 000000000..5bc30ccc5 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -0,0 +1,396 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * Copyright (C) 2013 Bahtiar 'kalkin' Gadimov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui; + +import java.util.ArrayList; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.spongycastle.openpgp.PGPPublicKey; +import org.spongycastle.openpgp.PGPPublicKeyRing; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.helper.ExportHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.ShareQrCodeDialogFragment; +import org.sufficientlysecure.keychain.util.Log; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.net.Uri; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.NfcAdapter.CreateNdefMessageCallback; +import android.nfc.NfcAdapter.OnNdefPushCompleteCallback; +import android.nfc.NfcEvent; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.text.format.DateFormat; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; +import com.beardedhen.androidbootstrap.BootstrapButton; + +@SuppressLint("NewApi") +public class ViewKeyActivity extends SherlockFragmentActivity implements CreateNdefMessageCallback, + OnNdefPushCompleteCallback { + + ExportHelper mExportHelper; + + private Uri mDataUri; + + private PGPPublicKey mPublicKey; + + private TextView mAlgorithm; + private TextView mFingerint; + private TextView mExpiry; + private TextView mCreation; + private BootstrapButton mActionEncrypt; + + private ListView mUserIds; + private ListView mKeys; + + // NFC + private NfcAdapter mNfcAdapter; + private byte[] mSharedKeyringBytes; + private static final int NFC_SENT = 1; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mExportHelper = new ExportHelper(this); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setHomeButtonEnabled(true); + + setContentView(R.layout.view_key_activity); + + mFingerint = (TextView) findViewById(R.id.fingerprint); + mExpiry = (TextView) findViewById(R.id.expiry); + mCreation = (TextView) findViewById(R.id.creation); + mAlgorithm = (TextView) findViewById(R.id.algorithm); + mActionEncrypt = (BootstrapButton) findViewById(R.id.action_encrypt); + mUserIds = (ListView) findViewById(R.id.user_ids); + mKeys = (ListView) findViewById(R.id.keys); + + Intent intent = getIntent(); + mDataUri = intent.getData(); + if (mDataUri == null) { + Log.e(Constants.TAG, "Intent data missing. Should be Uri of key!"); + finish(); + return; + } else { + Log.d(Constants.TAG, "uri: " + mDataUri); + loadData(mDataUri); + initNfc(mDataUri); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getSupportMenuInflater().inflate(R.menu.key_view, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_key_view_update: + updateFromKeyserver(mDataUri); + return true; + case R.id.menu_key_view_sign: + signKey(mDataUri); + return true; + case R.id.menu_key_view_export_keyserver: + uploadToKeyserver(mDataUri); + return true; + case R.id.menu_key_view_export_file: + mExportHelper.showExportKeysDialog(mDataUri, Id.type.public_key, Constants.path.APP_DIR + + "/pubexport.asc"); + return true; + case R.id.menu_key_view_share_default: + shareKey(mDataUri); + return true; + case R.id.menu_key_view_share_qr_code: + shareKeyQrCode(mDataUri); + return true; + case R.id.menu_key_view_share_nfc: + shareNfc(); + return true; + case R.id.menu_key_view_share_clipboard: + copyToClipboard(mDataUri); + return true; + case R.id.menu_key_view_delete: { + // Message is received after key is deleted + Handler returnHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == DeleteKeyDialogFragment.MESSAGE_OKAY) { + setResult(RESULT_CANCELED); + finish(); + } + } + }; + + mExportHelper.deleteKey(mDataUri, Id.type.public_key, returnHandler); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + private void loadData(Uri dataUri) { + PGPPublicKeyRing ring = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri); + mPublicKey = ring.getPublicKey(); + + mFingerint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper + .convertFingerprintToHex(mPublicKey.getFingerprint()))); + String[] mainUserId = splitUserId(""); + + Date expiryDate = PgpKeyHelper.getExpiryDate(mPublicKey); + if (expiryDate == null) { + mExpiry.setText(""); + } else { + mExpiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(expiryDate)); + } + + mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format( + PgpKeyHelper.getCreationDate(mPublicKey))); + mAlgorithm.setText(PgpKeyHelper.getAlgorithmInfo(mPublicKey)); + + mActionEncrypt.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + long[] encryptionKeyIds = new long[] { mPublicKey.getKeyID() }; + Intent intent = new Intent(ViewKeyActivity.this, EncryptActivity.class); + intent.setAction(EncryptActivity.ACTION_ENCRYPT); + intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); + // used instead of startActivity set actionbar based on callingPackage + startActivityForResult(intent, 0); + } + }); + } + + /** + * TODO: does this duplicate functionality from elsewhere? put in helper! + */ + private String[] splitUserId(String userId) { + String[] result = new String[] { "", "", "" }; + Log.v("UserID", userId); + + Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$"); + Matcher matcher = withComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(2); + result[2] = matcher.group(3); + return result; + } + + Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); + matcher = withoutComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(2); + return result; + } + return result; + } + + private void uploadToKeyserver(Uri dataUri) { + long keyRingRowId = Long.valueOf(dataUri.getLastPathSegment()); + + Intent uploadIntent = new Intent(this, KeyServerUploadActivity.class); + uploadIntent.setAction(KeyServerUploadActivity.ACTION_EXPORT_KEY_TO_SERVER); + uploadIntent.putExtra(KeyServerUploadActivity.EXTRA_KEYRING_ROW_ID, (int) keyRingRowId); + startActivityForResult(uploadIntent, Id.request.export_to_server); + } + + private void updateFromKeyserver(Uri dataUri) { + long updateKeyId = 0; + PGPPublicKeyRing updateRing = (PGPPublicKeyRing) ProviderHelper + .getPGPKeyRing(this, dataUri); + + if (updateRing != null) { + updateKeyId = PgpKeyHelper.getMasterKey(updateRing).getKeyID(); + } + if (updateKeyId == 0) { + Log.e(Constants.TAG, "this shouldn't happen. KeyId == 0!"); + return; + } + + Intent signIntent = new Intent(this, SignKeyActivity.class); + signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, updateKeyId); + startActivity(signIntent); + + Intent queryIntent = new Intent(this, KeyServerQueryActivity.class); + queryIntent.setAction(KeyServerQueryActivity.ACTION_LOOK_UP_KEY_ID_AND_RETURN); + queryIntent.putExtra(KeyServerQueryActivity.EXTRA_KEY_ID, updateKeyId); + + // TODO: lookup?? + startActivityForResult(queryIntent, Id.request.look_up_key_id); + } + + private void signKey(Uri dataUri) { + long keyId = 0; + PGPPublicKeyRing signKey = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri); + + if (signKey != null) { + keyId = PgpKeyHelper.getMasterKey(signKey).getKeyID(); + } + if (keyId == 0) { + Log.e(Constants.TAG, "this shouldn't happen. KeyId == 0!"); + return; + } + + Intent signIntent = new Intent(this, SignKeyActivity.class); + signIntent.putExtra(SignKeyActivity.EXTRA_KEY_ID, keyId); + startActivity(signIntent); + } + + private void shareKey(Uri dataUri) { + // get public keyring as ascii armored string + long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); + ArrayList keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this, dataUri, + new long[] { masterKeyId }); + + // let user choose application + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, keyringArmored.get(0)); + sendIntent.setType("text/plain"); + startActivity(Intent.createChooser(sendIntent, + getResources().getText(R.string.action_share_key_with))); + } + + private void shareKeyQrCode(Uri dataUri) { + // get public keyring as ascii armored string + long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); + ArrayList keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this, dataUri, + new long[] { masterKeyId }); + + ShareQrCodeDialogFragment dialog = ShareQrCodeDialogFragment.newInstance(keyringArmored + .get(0)); + dialog.show(getSupportFragmentManager(), "shareQrCodeDialog"); + } + + private void copyToClipboard(Uri dataUri) { + // get public keyring as ascii armored string + long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); + ArrayList keyringArmored = ProviderHelper.getKeyRingsAsArmoredString(this, dataUri, + new long[] { masterKeyId }); + + ClipboardReflection.copyToClipboard(this, keyringArmored.get(0)); + } + + private void shareNfc() { + ShareNfcDialogFragment dialog = ShareNfcDialogFragment.newInstance(); + dialog.show(getSupportFragmentManager(), "shareNfcDialog"); + } + + /** + * NFC: Initialize NFC sharing if OS and device supports it + */ + private void initNfc(Uri dataUri) { + // check if NFC Beam is supported (>= Android 4.1) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // Check for available NFC Adapter + mNfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (mNfcAdapter != null) { + // init nfc + + // get public keyring as byte array + long masterKeyId = ProviderHelper.getMasterKeyId(this, dataUri); + mSharedKeyringBytes = ProviderHelper.getKeyRingsAsByteArray(this, dataUri, + new long[] { masterKeyId }); + + // Register callback to set NDEF message + mNfcAdapter.setNdefPushMessageCallback(this, this); + // Register callback to listen for message-sent success + mNfcAdapter.setOnNdefPushCompleteCallback(this, this); + } + } + } + + /** + * NFC: Implementation for the CreateNdefMessageCallback interface + */ + @Override + public NdefMessage createNdefMessage(NfcEvent event) { + /** + * When a device receives a push with an AAR in it, the application specified in the AAR is + * guaranteed to run. The AAR overrides the tag dispatch system. You can add it back in to + * guarantee that this activity starts when receiving a beamed message. For now, this code + * uses the tag dispatch system. + */ + NdefMessage msg = new NdefMessage(NdefRecord.createMime(Constants.NFC_MIME, + mSharedKeyringBytes), NdefRecord.createApplicationRecord(Constants.PACKAGE_NAME)); + return msg; + } + + /** + * NFC: Implementation for the OnNdefPushCompleteCallback interface + */ + @Override + public void onNdefPushComplete(NfcEvent arg0) { + // A handler is needed to send messages to the activity when this + // callback occurs, because it happens from a binder thread + mNfcHandler.obtainMessage(NFC_SENT).sendToTarget(); + } + + /** + * NFC: This handler receives a message from onNdefPushComplete + */ + private final Handler mNfcHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case NFC_SENT: + Toast.makeText(getApplicationContext(), R.string.nfc_successfull, Toast.LENGTH_LONG) + .show(); + break; + } + } + }; + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (!mExportHelper.handleActivityResult(requestCode, resultCode, data)) { + super.onActivityResult(requestCode, resultCode, data); + } + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java index 5f71c9805..140781b4f 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java @@ -105,7 +105,7 @@ public class KeyListPublicAdapter extends CursorAdapter implements StickyListHea HeaderViewHolder holder; if (convertView == null) { holder = new HeaderViewHolder(); - convertView = mInflater.inflate(R.layout.stickylist_header, parent, false); + convertView = mInflater.inflate(R.layout.key_list_public_header, parent, false); holder.text = (TextView) convertView.findViewById(R.id.stickylist_header_text); convertView.setTag(holder); } else { diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java new file mode 100644 index 000000000..55821b11e --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2013 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import java.util.HashMap; +import java.util.Set; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Color; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class ViewKeyUserIdsAdapter extends CursorAdapter { + private LayoutInflater mInflater; + + public ViewKeyUserIdsAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + int userIdIndex = cursor.getColumnIndex(UserIds.USER_ID); + + TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); + mainUserId.setText(R.string.unknown_user_id); + TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); + mainUserIdRest.setText(""); + + String userId = cursor.getString(userIdIndex); + if (userId != null) { + String[] userIdSplit = OtherHelper.splitUserId(userId); + + if (userIdSplit[1] != null) { + mainUserIdRest.setText(userIdSplit[1]); + } + mainUserId.setText(userIdSplit[0]); + } + + if (mainUserId.getText().length() == 0) { + mainUserId.setText(R.string.unknown_user_id); + } + + if (mainUserIdRest.getText().length() == 0) { + mainUserIdRest.setVisibility(View.GONE); + } else { + mainUserIdRest.setVisibility(View.VISIBLE); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.key_list_item, null); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java index aba7e974e..e88271240 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/PassphraseDialogFragment.java @@ -141,7 +141,7 @@ public class PassphraseDialogFragment extends DialogFragment implements OnEditor } LayoutInflater inflater = activity.getLayoutInflater(); - View view = inflater.inflate(R.layout.passphrase, null); + View view = inflater.inflate(R.layout.passphrase_dialog, null); alert.setView(view); mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java index 797b28829..79d577c58 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java @@ -90,7 +90,7 @@ public class SetPassphraseDialogFragment extends DialogFragment implements OnEdi alert.setMessage(R.string.enter_passphrase_twice); LayoutInflater inflater = activity.getLayoutInflater(); - View view = inflater.inflate(R.layout.passphrase_repeat, null); + View view = inflater.inflate(R.layout.passphrase_repeat_dialogf, null); alert.setView(view); mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java index 91e3831b7..b33dbe4c5 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/SectionView.java @@ -153,7 +153,7 @@ public class SectionView extends LinearLayout implements OnClickListener, Editor case Id.type.key: { AlertDialog.Builder dialog = new AlertDialog.Builder(getContext()); - View view = mInflater.inflate(R.layout.create_key, null); + View view = mInflater.inflate(R.layout.create_key_dialog, null); dialog.setView(view); dialog.setTitle(R.string.title_create_key); -- cgit v1.2.3 From c740d409c4f8977da22738f98f16f6b3f624ea73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Sun, 12 Jan 2014 22:26:55 +0100 Subject: fix compile --- OpenPGP-Keychain/AndroidManifest.xml | 2 +- .../keychain/ui/adapter/KeyListAdapterOLD.java | 40 +++++++++++++--------- .../ui/dialog/SetPassphraseDialogFragment.java | 2 +- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/OpenPGP-Keychain/AndroidManifest.xml b/OpenPGP-Keychain/AndroidManifest.xml index 3b2e423df..9e4f3ca49 100644 --- a/OpenPGP-Keychain/AndroidManifest.xml +++ b/OpenPGP-Keychain/AndroidManifest.xml @@ -115,7 +115,7 @@ android:label="@string/title_edit_key" android:windowSoftInputMode="stateHidden" /> diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapterOLD.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapterOLD.java index 0c709d25a..8f745bdd3 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapterOLD.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapterOLD.java @@ -18,12 +18,12 @@ package org.sufficientlysecure.keychain.ui.adapter; import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.Id; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.helper.OtherHelper; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; import org.sufficientlysecure.keychain.util.Log; -import org.sufficientlysecure.keychain.R; import android.content.Context; import android.database.Cursor; @@ -36,7 +36,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.CursorTreeAdapter; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; public class KeyListAdapterOLD extends CursorTreeAdapter { @@ -97,27 +96,27 @@ public class KeyListAdapterOLD extends CursorTreeAdapter { } } - /** - * Inflate new view for child items - */ - @Override - public View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) { - return mInflater.inflate(R.layout.key_list_child_item, null); - } +// /** +// * Inflate new view for child items +// */ +// @Override +// public View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) { +// return mInflater.inflate(R.layout.key_list_child_item, null); +// } /** * Bind TextViews from view of childs based on query results */ @Override protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) { - LinearLayout keyLayout = (LinearLayout) view.findViewById(R.id.keyLayout); - LinearLayout userIdLayout = (LinearLayout) view.findViewById(R.id.userIdLayout); +// LinearLayout keyLayout = (LinearLayout) view.findViewById(R.id.keyLayout); +// LinearLayout userIdLayout = (LinearLayout) view.findViewById(R.id.userIdLayout); // first entry is fingerprint if (cursor.getPosition() == 0) { // show only userId layout - keyLayout.setVisibility(View.GONE); - userIdLayout.setVisibility(View.VISIBLE); +// keyLayout.setVisibility(View.GONE); +// userIdLayout.setVisibility(View.VISIBLE); String fingerprint = PgpKeyHelper.getFingerPrint(context, cursor.getLong(cursor.getColumnIndex(Keys.KEY_ID))); @@ -131,8 +130,8 @@ public class KeyListAdapterOLD extends CursorTreeAdapter { } else { // differentiate between keys and userIds in MergeCursor if (cursor.getColumnIndex(Keys.KEY_ID) != -1) { - keyLayout.setVisibility(View.VISIBLE); - userIdLayout.setVisibility(View.GONE); +// keyLayout.setVisibility(View.VISIBLE); +// userIdLayout.setVisibility(View.GONE); String keyIdStr = PgpKeyHelper.convertKeyIdToHex(cursor.getLong(cursor .getColumnIndex(Keys.KEY_ID))); @@ -174,8 +173,8 @@ public class KeyListAdapterOLD extends CursorTreeAdapter { signIcon.setVisibility(View.VISIBLE); } } else { - keyLayout.setVisibility(View.GONE); - userIdLayout.setVisibility(View.VISIBLE); +// keyLayout.setVisibility(View.GONE); +// userIdLayout.setVisibility(View.VISIBLE); String userIdStr = cursor.getString(cursor.getColumnIndex(UserIds.USER_ID)); @@ -270,4 +269,11 @@ public class KeyListAdapterOLD extends CursorTreeAdapter { return mContext.getContentResolver().query(uri, projection, selection, null, sortOrder); } + @Override + protected View newChildView(Context context, Cursor cursor, boolean isLastChild, + ViewGroup parent) { + // TODO Auto-generated method stub + return null; + } + } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java index 79d577c58..d5c366bed 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/dialog/SetPassphraseDialogFragment.java @@ -90,7 +90,7 @@ public class SetPassphraseDialogFragment extends DialogFragment implements OnEdi alert.setMessage(R.string.enter_passphrase_twice); LayoutInflater inflater = activity.getLayoutInflater(); - View view = inflater.inflate(R.layout.passphrase_repeat_dialogf, null); + View view = inflater.inflate(R.layout.passphrase_repeat_dialog, null); alert.setView(view); mPassphraseEditText = (EditText) view.findViewById(R.id.passphrase_passphrase); -- cgit v1.2.3 From face67d64b06f14913fff9ce61ae4a23421cec28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 13 Jan 2014 00:39:51 +0100 Subject: key view is working --- OpenPGP-Keychain/res/layout/select_key_item.xml | 20 +- .../res/layout/share_qr_code_dialog.xml | 16 - OpenPGP-Keychain/res/layout/view_key_activity.xml | 343 +++++++++++++-------- .../res/layout/view_key_userids_item.xml | 1 - OpenPGP-Keychain/res/values/strings.xml | 2 + .../keychain/helper/OtherHelper.java | 19 -- .../keychain/pgp/PgpKeyHelper.java | 30 ++ .../keychain/provider/KeychainContract.java | 20 +- .../keychain/ui/KeyListSecretFragment.java | 9 +- .../keychain/ui/ViewKeyActivity.java | 183 +++++++++-- .../keychain/ui/adapter/KeyListAdapterOLD.java | 279 ----------------- .../keychain/ui/adapter/KeyListPublicAdapter.java | 6 +- .../keychain/ui/adapter/KeyListSecretAdapter.java | 4 +- .../ui/adapter/SelectKeyCursorAdapter.java | 5 +- .../keychain/ui/adapter/ViewKeyKeysAdapter.java | 90 ++++++ .../keychain/ui/adapter/ViewKeyUserIdsAdapter.java | 36 +-- .../keychain/ui/widget/FixedListView.java | 55 ++++ 17 files changed, 577 insertions(+), 541 deletions(-) delete mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapterOLD.java create mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java create mode 100644 OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/FixedListView.java diff --git a/OpenPGP-Keychain/res/layout/select_key_item.xml b/OpenPGP-Keychain/res/layout/select_key_item.xml index d5b1655ed..bbfe17c44 100644 --- a/OpenPGP-Keychain/res/layout/select_key_item.xml +++ b/OpenPGP-Keychain/res/layout/select_key_item.xml @@ -1,22 +1,6 @@ - - diff --git a/OpenPGP-Keychain/res/layout/share_qr_code_dialog.xml b/OpenPGP-Keychain/res/layout/share_qr_code_dialog.xml index 88b06b698..66102d3e5 100644 --- a/OpenPGP-Keychain/res/layout/share_qr_code_dialog.xml +++ b/OpenPGP-Keychain/res/layout/share_qr_code_dialog.xml @@ -1,20 +1,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_width="match_parent" + android:layout_height="match_parent" > - - - - - - - \ No newline at end of file + android:layout_height="wrap_content" + android:layout_marginRight="?android:attr/scrollbarSize" + android:orientation="vertical" + android:paddingLeft="16dp" + android:paddingRight="16dp" > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenPGP-Keychain/res/layout/view_key_userids_item.xml b/OpenPGP-Keychain/res/layout/view_key_userids_item.xml index cbc85189f..2d022ba13 100644 --- a/OpenPGP-Keychain/res/layout/view_key_userids_item.xml +++ b/OpenPGP-Keychain/res/layout/view_key_userids_item.xml @@ -4,7 +4,6 @@ android:layout_height="?android:attr/listPreferredItemHeight" android:layout_marginRight="?android:attr/scrollbarSize" android:orientation="vertical" - android:paddingLeft="36dip" android:singleLine="true" > Defaults
Advanced Master Key + Master User ID Actions @@ -137,6 +138,7 @@ Comment Email Send Key to Server? + Fingerprint Select 1 Selected Selected diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java index 8fa2df1f5..9f3cd8e88 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/helper/OtherHelper.java @@ -79,23 +79,4 @@ public class OtherHelper { } } - /** - * Splits userId string into naming part and email part - * - * @param userId - * @return array with naming (0) and email (1) - */ - public static String[] splitUserId(String userId) { - String[] output = new String[2]; - - String chunks[] = userId.split(" <", 2); - userId = chunks[0]; - if (chunks.length > 1) { - output[1] = "<" + chunks[1]; - } - output[0] = userId; - - return output; - } - } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java index daba94b99..edb30496a 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/pgp/PgpKeyHelper.java @@ -22,6 +22,8 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.Locale; import java.util.Vector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.spongycastle.bcpg.sig.KeyFlags; import org.spongycastle.openpgp.PGPPublicKey; @@ -524,4 +526,32 @@ public class PgpKeyHelper { return (Long.parseLong(s1, 16) << 32) | Long.parseLong(s2, 16); } + /** + * Splits userId string into naming part, email part, and comment part + * + * @param userId + * @return array with naming (0), email (1), comment (2) + */ + public static String[] splitUserId(String userId) { + String[] result = new String[] { "", "", "" }; + + Pattern withComment = Pattern.compile("^(.*) \\((.*)\\) <(.*)>$"); + Matcher matcher = withComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(3); + result[2] = matcher.group(2); + return result; + } + + Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); + matcher = withoutComment.matcher(userId); + if (matcher.matches()) { + result[0] = matcher.group(1); + result[1] = matcher.group(2); + return result; + } + return result; + } + } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java index 82bb473f6..d2381f6f0 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/provider/KeychainContract.java @@ -69,8 +69,8 @@ public class KeychainContract { public static final String CONTENT_AUTHORITY = Constants.PACKAGE_NAME + ".provider"; - private static final Uri BASE_CONTENT_URI_INTERNAL = Uri.parse("content://" - + CONTENT_AUTHORITY); + private static final Uri BASE_CONTENT_URI_INTERNAL = Uri + .parse("content://" + CONTENT_AUTHORITY); public static final String BASE_KEY_RINGS = "key_rings"; public static final String BASE_DATA = "data"; @@ -185,6 +185,14 @@ public class KeychainContract { return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId) .appendPath(PATH_KEYS).appendPath(keyRowId).build(); } + + public static Uri buildKeysUri(Uri keyRingUri) { + return keyRingUri.buildUpon().appendPath(PATH_KEYS).build(); + } + + public static Uri buildKeysUri(Uri keyRingUri, String keyRowId) { + return keyRingUri.buildUpon().appendPath(PATH_KEYS).appendPath(keyRowId).build(); + } } public static class UserIds implements UserIdsColumns, BaseColumns { @@ -216,6 +224,14 @@ public class KeychainContract { return CONTENT_URI.buildUpon().appendPath(PATH_SECRET).appendPath(keyRingRowId) .appendPath(PATH_USER_IDS).appendPath(userIdRowId).build(); } + + public static Uri buildUserIdsUri(Uri keyRingUri) { + return keyRingUri.buildUpon().appendPath(PATH_USER_IDS).build(); + } + + public static Uri buildUserIdsUri(Uri keyRingUri, String userIdRowId) { + return keyRingUri.buildUpon().appendPath(PATH_USER_IDS).appendPath(userIdRowId).build(); + } } public static class ApiApps implements ApiAppsColumns, BaseColumns { diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java index c0b67719e..0e2d22e1e 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/KeyListSecretFragment.java @@ -51,7 +51,6 @@ public class KeyListSecretFragment extends SherlockListFragment implements LoaderManager.LoaderCallbacks, OnItemClickListener { private KeyListSecretActivity mKeyListSecretActivity; - private KeyListSecretAdapter mAdapter; /** @@ -146,7 +145,7 @@ public class KeyListSecretFragment extends SherlockListFragment implements setListShown(false); // Create an empty adapter we will use to display the loaded data. - mAdapter = new KeyListSecretAdapter(mKeyListSecretActivity, null, Id.type.secret_key); + mAdapter = new KeyListSecretAdapter(mKeyListSecretActivity, null, 0); setListAdapter(mAdapter); // Prepare the loader. Either re-connect with an existing one, @@ -157,8 +156,7 @@ public class KeyListSecretFragment extends SherlockListFragment implements // These are the rows that we will retrieve. static final String[] PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID, UserIds.USER_ID }; - - static final String SORT_ORDER = UserIds.USER_ID + " ASC"; + static final String SORT_ORDER = UserIds.USER_ID + " COLLATE LOCALIZED ASC"; public Loader onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. This @@ -169,8 +167,7 @@ public class KeyListSecretFragment extends SherlockListFragment implements // Now create and return a CursorLoader that will take care of // creating a Cursor for the data being displayed. - return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, UserIds.USER_ID - + " COLLATE LOCALIZED ASC"); + return new CursorLoader(getActivity(), baseUri, PROJECTION, null, null, SORT_ORDER); } public void onLoadFinished(Loader loader, Cursor data) { diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 5bc30ccc5..e2f90e87c 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Dominik Schürmann + * Copyright (C) 2013-2014 Dominik Schürmann * Copyright (C) 2013 Bahtiar 'kalkin' Gadimov * * This program is free software: you can redistribute it and/or modify @@ -20,8 +20,6 @@ package org.sufficientlysecure.keychain.ui; import java.util.ArrayList; import java.util.Date; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPPublicKeyRing; @@ -31,7 +29,12 @@ import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; import org.sufficientlysecure.keychain.helper.ExportHelper; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.adapter.ViewKeyKeysAdapter; +import org.sufficientlysecure.keychain.ui.adapter.ViewKeyUserIdsAdapter; import org.sufficientlysecure.keychain.ui.dialog.DeleteKeyDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment; import org.sufficientlysecure.keychain.ui.dialog.ShareQrCodeDialogFragment; @@ -39,6 +42,7 @@ import org.sufficientlysecure.keychain.util.Log; import android.annotation.SuppressLint; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.nfc.NdefMessage; import android.nfc.NdefRecord; @@ -50,6 +54,9 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; import android.text.format.DateFormat; import android.view.View; import android.view.View.OnClickListener; @@ -64,7 +71,7 @@ import com.beardedhen.androidbootstrap.BootstrapButton; @SuppressLint("NewApi") public class ViewKeyActivity extends SherlockFragmentActivity implements CreateNdefMessageCallback, - OnNdefPushCompleteCallback { + OnNdefPushCompleteCallback, LoaderManager.LoaderCallbacks { ExportHelper mExportHelper; @@ -72,10 +79,14 @@ public class ViewKeyActivity extends SherlockFragmentActivity implements CreateN private PGPPublicKey mPublicKey; + private TextView mName; + private TextView mEmail; + private TextView mComment; private TextView mAlgorithm; - private TextView mFingerint; + private TextView mKeyId; private TextView mExpiry; private TextView mCreation; + private TextView mFingerprint; private BootstrapButton mActionEncrypt; private ListView mUserIds; @@ -86,6 +97,12 @@ public class ViewKeyActivity extends SherlockFragmentActivity implements CreateN private byte[] mSharedKeyringBytes; private static final int NFC_SENT = 1; + private static final int LOADER_ID_KEYRING = 0; + private static final int LOADER_ID_USER_IDS = 1; + private static final int LOADER_ID_KEYS = 2; + private ViewKeyUserIdsAdapter mUserIdsAdapter; + private ViewKeyKeysAdapter mKeysAdapter; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -93,14 +110,19 @@ public class ViewKeyActivity extends SherlockFragmentActivity implements CreateN mExportHelper = new ExportHelper(this); getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setIcon(android.R.color.transparent); getSupportActionBar().setHomeButtonEnabled(true); setContentView(R.layout.view_key_activity); - mFingerint = (TextView) findViewById(R.id.fingerprint); - mExpiry = (TextView) findViewById(R.id.expiry); - mCreation = (TextView) findViewById(R.id.creation); + mName = (TextView) findViewById(R.id.name); + mEmail = (TextView) findViewById(R.id.email); + mComment = (TextView) findViewById(R.id.comment); + mKeyId = (TextView) findViewById(R.id.key_id); mAlgorithm = (TextView) findViewById(R.id.algorithm); + mCreation = (TextView) findViewById(R.id.creation); + mExpiry = (TextView) findViewById(R.id.expiry); + mFingerprint = (TextView) findViewById(R.id.fingerprint); mActionEncrypt = (BootstrapButton) findViewById(R.id.action_encrypt); mUserIds = (ListView) findViewById(R.id.user_ids); mKeys = (ListView) findViewById(R.id.keys); @@ -128,6 +150,11 @@ public class ViewKeyActivity extends SherlockFragmentActivity implements CreateN @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { + case android.R.id.home: + Intent homeIntent = new Intent(this, KeyListPublicActivity.class); + homeIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(homeIntent); + return true; case R.id.menu_key_view_update: updateFromKeyserver(mDataUri); return true; @@ -173,16 +200,23 @@ public class ViewKeyActivity extends SherlockFragmentActivity implements CreateN } private void loadData(Uri dataUri) { + // TODO: don't use pubkey object, use database!!! + PGPPublicKeyRing ring = (PGPPublicKeyRing) ProviderHelper.getPGPKeyRing(this, dataUri); mPublicKey = ring.getPublicKey(); - mFingerint.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper + mKeyId.setText(PgpKeyHelper.shortifyFingerprint(PgpKeyHelper .convertFingerprintToHex(mPublicKey.getFingerprint()))); - String[] mainUserId = splitUserId(""); + + String fingerprint = PgpKeyHelper.convertFingerprintToHex(mPublicKey.getFingerprint()); + fingerprint = fingerprint.replace(" ", "\n"); + mFingerprint.setText(fingerprint); + + // TODO: get image with getUserAttributes() on key and then PGPUserAttributeSubpacketVector Date expiryDate = PgpKeyHelper.getExpiryDate(mPublicKey); if (expiryDate == null) { - mExpiry.setText(""); + mExpiry.setText(R.string.none); } else { mExpiry.setText(DateFormat.getDateFormat(getApplicationContext()).format(expiryDate)); } @@ -203,32 +237,117 @@ public class ViewKeyActivity extends SherlockFragmentActivity implements CreateN startActivityForResult(intent, 0); } }); + + mUserIdsAdapter = new ViewKeyUserIdsAdapter(this, null, 0); + + mUserIds.setAdapter(mUserIdsAdapter); + // mUserIds.setEmptyView(findViewById(android.R.id.empty)); + // mUserIds.setClickable(true); + // mUserIds.setOnItemClickListener(new AdapterView.OnItemClickListener() { + // @Override + // public void onItemClick(AdapterView arg0, View arg1, int position, long id) { + // } + // }); + + mKeysAdapter = new ViewKeyKeysAdapter(this, null, 0); + + mKeys.setAdapter(mKeysAdapter); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getSupportLoaderManager().initLoader(LOADER_ID_KEYRING, null, this); + getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); + getSupportLoaderManager().initLoader(LOADER_ID_KEYS, null, this); } - /** - * TODO: does this duplicate functionality from elsewhere? put in helper! - */ - private String[] splitUserId(String userId) { - String[] result = new String[] { "", "", "" }; - Log.v("UserID", userId); - - Pattern withComment = Pattern.compile("^(.*) [(](.*)[)] <(.*)>$"); - Matcher matcher = withComment.matcher(userId); - if (matcher.matches()) { - result[0] = matcher.group(1); - result[1] = matcher.group(2); - result[2] = matcher.group(3); - return result; + static final String[] KEYRING_PROJECTION = new String[] { KeyRings._ID, KeyRings.MASTER_KEY_ID, + UserIds.USER_ID }; + + static final String[] USER_IDS_PROJECTION = new String[] { UserIds._ID, UserIds.USER_ID, + UserIds.RANK, }; + // not the main user id + static final String USER_IDS_SELECTION = UserIds.RANK + " > 0 "; + static final String USER_IDS_SORT_ORDER = UserIds.USER_ID + " COLLATE LOCALIZED ASC"; + + static final String[] KEYS_PROJECTION = new String[] { Keys._ID, Keys.KEY_ID, + Keys.IS_MASTER_KEY, Keys.ALGORITHM, Keys.KEY_SIZE, Keys.CAN_CERTIFY, Keys.CAN_SIGN, + Keys.CAN_ENCRYPT, }; + static final String KEYS_SORT_ORDER = Keys.RANK + " ASC"; + + public Loader onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_ID_KEYRING: { + Uri baseUri = mDataUri; + + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(this, baseUri, KEYRING_PROJECTION, null, null, null); } + case LOADER_ID_USER_IDS: { + Uri baseUri = UserIds.buildUserIdsUri(mDataUri); + + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(this, baseUri, USER_IDS_PROJECTION, USER_IDS_SELECTION, null, + USER_IDS_SORT_ORDER); + } + case LOADER_ID_KEYS: { + Uri baseUri = Keys.buildKeysUri(mDataUri); + + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(this, baseUri, KEYS_PROJECTION, null, null, KEYS_SORT_ORDER); + } + + default: + return null; + } + } - Pattern withoutComment = Pattern.compile("^(.*) <(.*)>$"); - matcher = withoutComment.matcher(userId); - if (matcher.matches()) { - result[0] = matcher.group(1); - result[1] = matcher.group(2); - return result; + public void onLoadFinished(Loader loader, Cursor data) { + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + switch (loader.getId()) { + case LOADER_ID_KEYRING: + if (data.moveToFirst()) { + String[] mainUserId = PgpKeyHelper.splitUserId(data.getString(2)); + setTitle(mainUserId[0]); + mName.setText(mainUserId[0]); + mEmail.setText(mainUserId[1]); + mComment.setText(mainUserId[2]); + } + + break; + case LOADER_ID_USER_IDS: + mUserIdsAdapter.swapCursor(data); + break; + case LOADER_ID_KEYS: + mKeysAdapter.swapCursor(data); + break; + + default: + break; + } + } + + /** + * This is called when the last Cursor provided to onLoadFinished() above is about to be closed. + * We need to make sure we are no longer using it. + */ + public void onLoaderReset(Loader loader) { + switch (loader.getId()) { + case LOADER_ID_KEYRING: + // TODO? + break; + case LOADER_ID_USER_IDS: + mUserIdsAdapter.swapCursor(null); + break; + case LOADER_ID_KEYS: + mKeysAdapter.swapCursor(null); + break; + default: + break; } - return result; } private void uploadToKeyserver(Uri dataUri) { diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapterOLD.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapterOLD.java deleted file mode 100644 index 8f745bdd3..000000000 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListAdapterOLD.java +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright (C) 2012-2013 Dominik Schürmann - * - * Licensed 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. - */ - -package org.sufficientlysecure.keychain.ui.adapter; - -import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.Id; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.helper.OtherHelper; -import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; -import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; -import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; -import org.sufficientlysecure.keychain.util.Log; - -import android.content.Context; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.MergeCursor; -import android.net.Uri; -import android.provider.BaseColumns; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CursorTreeAdapter; -import android.widget.ImageView; -import android.widget.TextView; - -public class KeyListAdapterOLD extends CursorTreeAdapter { - private Context mContext; - private LayoutInflater mInflater; - - protected int mKeyType; - - private static final int CHILD_KEY = 0; - private static final int CHILD_USER_ID = 1; - private static final int CHILD_FINGERPRINT = 2; - - public KeyListAdapterOLD(Context context, Cursor groupCursor, int keyType) { - super(groupCursor, context); - mContext = context; - mInflater = LayoutInflater.from(context); - mKeyType = keyType; - } - - /** - * Inflate new view for group items - */ - @Override - public View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) { - return mInflater.inflate(R.layout.key_list_item, null); - } - - /** - * Binds TextViews from group view to results from database group cursor. - */ - @Override - protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) { - int userIdIndex = cursor.getColumnIndex(UserIds.USER_ID); - - TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); - mainUserId.setText(R.string.unknown_user_id); - TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); - mainUserIdRest.setText(""); - - String userId = cursor.getString(userIdIndex); - if (userId != null) { - String[] userIdSplit = OtherHelper.splitUserId(userId); - - if (userIdSplit[1] != null) { - mainUserIdRest.setText(userIdSplit[1]); - } - mainUserId.setText(userIdSplit[0]); - } - - if (mainUserId.getText().length() == 0) { - mainUserId.setText(R.string.unknown_user_id); - } - - if (mainUserIdRest.getText().length() == 0) { - mainUserIdRest.setVisibility(View.GONE); - } else { - mainUserIdRest.setVisibility(View.VISIBLE); - } - } - -// /** -// * Inflate new view for child items -// */ -// @Override -// public View newChildView(Context context, Cursor cursor, boolean isLastChild, ViewGroup parent) { -// return mInflater.inflate(R.layout.key_list_child_item, null); -// } - - /** - * Bind TextViews from view of childs based on query results - */ - @Override - protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) { -// LinearLayout keyLayout = (LinearLayout) view.findViewById(R.id.keyLayout); -// LinearLayout userIdLayout = (LinearLayout) view.findViewById(R.id.userIdLayout); - - // first entry is fingerprint - if (cursor.getPosition() == 0) { - // show only userId layout -// keyLayout.setVisibility(View.GONE); -// userIdLayout.setVisibility(View.VISIBLE); - - String fingerprint = PgpKeyHelper.getFingerPrint(context, - cursor.getLong(cursor.getColumnIndex(Keys.KEY_ID))); - fingerprint = fingerprint.replace(" ", "\n"); - - TextView userId = (TextView) view.findViewById(R.id.userId); - if (userId == null) { - Log.d(Constants.TAG, "userId is null!"); - } - userId.setText(context.getString(R.string.fingerprint) + "\n" + fingerprint); - } else { - // differentiate between keys and userIds in MergeCursor - if (cursor.getColumnIndex(Keys.KEY_ID) != -1) { -// keyLayout.setVisibility(View.VISIBLE); -// userIdLayout.setVisibility(View.GONE); - - String keyIdStr = PgpKeyHelper.convertKeyIdToHex(cursor.getLong(cursor - .getColumnIndex(Keys.KEY_ID))); - String algorithmStr = PgpKeyHelper.getAlgorithmInfo( - cursor.getInt(cursor.getColumnIndex(Keys.ALGORITHM)), - cursor.getInt(cursor.getColumnIndex(Keys.KEY_SIZE))); - - TextView keyId = (TextView) view.findViewById(R.id.keyId); - keyId.setText(keyIdStr); - - TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails); - keyDetails.setText("(" + algorithmStr + ")"); - - ImageView masterKeyIcon = (ImageView) view.findViewById(R.id.ic_masterKey); - if (cursor.getInt(cursor.getColumnIndex(Keys.IS_MASTER_KEY)) != 1) { - masterKeyIcon.setVisibility(View.INVISIBLE); - } else { - masterKeyIcon.setVisibility(View.VISIBLE); - } - - ImageView certifyIcon = (ImageView) view.findViewById(R.id.ic_certifyKey); - if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_CERTIFY)) != 1) { - certifyIcon.setVisibility(View.GONE); - } else { - certifyIcon.setVisibility(View.VISIBLE); - } - - ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey); - if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_ENCRYPT)) != 1) { - encryptIcon.setVisibility(View.GONE); - } else { - encryptIcon.setVisibility(View.VISIBLE); - } - - ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey); - if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_SIGN)) != 1) { - signIcon.setVisibility(View.GONE); - } else { - signIcon.setVisibility(View.VISIBLE); - } - } else { -// keyLayout.setVisibility(View.GONE); -// userIdLayout.setVisibility(View.VISIBLE); - - String userIdStr = cursor.getString(cursor.getColumnIndex(UserIds.USER_ID)); - - TextView userId = (TextView) view.findViewById(R.id.userId); - userId.setText(userIdStr); - } - } - } - - /** - * Given the group cursor, we start cursors for a fingerprint, keys, and userIds, which are - * merged together and build the child cursor - */ - @Override - protected Cursor getChildrenCursor(Cursor groupCursor) { - final long keyRingRowId = groupCursor.getLong(groupCursor.getColumnIndex(BaseColumns._ID)); - - Cursor fingerprintCursor = getChildCursor(keyRingRowId, CHILD_FINGERPRINT); - Cursor keyCursor = getChildCursor(keyRingRowId, CHILD_KEY); - Cursor userIdCursor = getChildCursor(keyRingRowId, CHILD_USER_ID); - - MergeCursor mergeCursor = new MergeCursor(new Cursor[] { fingerprintCursor, keyCursor, - userIdCursor }); - Log.d(Constants.TAG, "mergeCursor:" + DatabaseUtils.dumpCursorToString(mergeCursor)); - - return mergeCursor; - } - - /** - * This builds a cursor for a specific type of children - * - * @param keyRingRowId - * foreign row id of the keyRing - * @param type - * @return - */ - private Cursor getChildCursor(long keyRingRowId, int type) { - Uri uri = null; - String[] projection = null; - String sortOrder = null; - String selection = null; - - switch (type) { - case CHILD_FINGERPRINT: - projection = new String[] { Keys._ID, Keys.KEY_ID, Keys.IS_MASTER_KEY, Keys.ALGORITHM, - Keys.KEY_SIZE, Keys.CAN_CERTIFY, Keys.CAN_SIGN, Keys.CAN_ENCRYPT, }; - sortOrder = Keys.RANK + " ASC"; - - // use only master key for fingerprint - selection = Keys.IS_MASTER_KEY + " = 1 "; - - if (mKeyType == Id.type.public_key) { - uri = Keys.buildPublicKeysUri(String.valueOf(keyRingRowId)); - } else { - uri = Keys.buildSecretKeysUri(String.valueOf(keyRingRowId)); - } - break; - - case CHILD_KEY: - projection = new String[] { Keys._ID, Keys.KEY_ID, Keys.IS_MASTER_KEY, Keys.ALGORITHM, - Keys.KEY_SIZE, Keys.CAN_CERTIFY, Keys.CAN_SIGN, Keys.CAN_ENCRYPT, }; - sortOrder = Keys.RANK + " ASC"; - - if (mKeyType == Id.type.public_key) { - uri = Keys.buildPublicKeysUri(String.valueOf(keyRingRowId)); - } else { - uri = Keys.buildSecretKeysUri(String.valueOf(keyRingRowId)); - } - - break; - - case CHILD_USER_ID: - projection = new String[] { UserIds._ID, UserIds.USER_ID, UserIds.RANK, }; - sortOrder = UserIds.RANK + " ASC"; - - // not the main user id - selection = UserIds.RANK + " > 0 "; - - if (mKeyType == Id.type.public_key) { - uri = UserIds.buildPublicUserIdsUri(String.valueOf(keyRingRowId)); - } else { - uri = UserIds.buildSecretUserIdsUri(String.valueOf(keyRingRowId)); - } - - break; - - default: - return null; - - } - - return mContext.getContentResolver().query(uri, projection, selection, null, sortOrder); - } - - @Override - protected View newChildView(Context context, Cursor cursor, boolean isLastChild, - ViewGroup parent) { - // TODO Auto-generated method stub - return null; - } - -} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java index 140781b4f..f1e58a5d3 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListPublicAdapter.java @@ -19,11 +19,13 @@ package org.sufficientlysecure.keychain.ui.adapter; import java.util.HashMap; import java.util.Set; + import org.sufficientlysecure.keychain.Constants; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; import org.sufficientlysecure.keychain.util.Log; + import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; import android.annotation.SuppressLint; import android.content.Context; @@ -69,7 +71,7 @@ public class KeyListPublicAdapter extends CursorAdapter implements StickyListHea String userId = cursor.getString(userIdIndex); if (userId != null) { - String[] userIdSplit = OtherHelper.splitUserId(userId); + String[] userIdSplit = PgpKeyHelper.splitUserId(userId); if (userIdSplit[1] != null) { mainUserIdRest.setText(userIdSplit[1]); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java index f78eaf627..d06c0287c 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/KeyListSecretAdapter.java @@ -21,7 +21,7 @@ import java.util.HashMap; import java.util.Set; import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; import android.annotation.SuppressLint; @@ -57,7 +57,7 @@ public class KeyListSecretAdapter extends CursorAdapter { String userId = cursor.getString(userIdIndex); if (userId != null) { - String[] userIdSplit = OtherHelper.splitUserId(userId); + String[] userIdSplit = PgpKeyHelper.splitUserId(userId); if (userIdSplit[1] != null) { mainUserIdRest.setText(userIdSplit[1]); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java index ebb7261be..c6eca0296 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/SelectKeyCursorAdapter.java @@ -18,11 +18,10 @@ package org.sufficientlysecure.keychain.ui.adapter; import org.sufficientlysecure.keychain.Id; -import org.sufficientlysecure.keychain.helper.OtherHelper; +import org.sufficientlysecure.keychain.R; import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; -import org.sufficientlysecure.keychain.R; import android.content.Context; import android.database.Cursor; @@ -78,7 +77,7 @@ public class SelectKeyCursorAdapter extends CursorAdapter { String userId = cursor.getString(cursor.getColumnIndex(UserIds.USER_ID)); if (userId != null) { - String[] userIdSplit = OtherHelper.splitUserId(userId); + String[] userIdSplit = PgpKeyHelper.splitUserId(userId); if (userIdSplit[1] != null) { mainUserIdRest.setText(userIdSplit[1]); diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java new file mode 100644 index 000000000..51286af66 --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyKeysAdapter.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.sufficientlysecure.keychain.ui.adapter; + +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; + +import android.content.Context; +import android.database.Cursor; +import android.support.v4.widget.CursorAdapter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +public class ViewKeyKeysAdapter extends CursorAdapter { + private LayoutInflater mInflater; + + public ViewKeyKeysAdapter(Context context, Cursor c, int flags) { + super(context, c, flags); + + mInflater = LayoutInflater.from(context); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + String keyIdStr = PgpKeyHelper.convertKeyIdToHex(cursor.getLong(cursor + .getColumnIndex(Keys.KEY_ID))); + String algorithmStr = PgpKeyHelper.getAlgorithmInfo( + cursor.getInt(cursor.getColumnIndex(Keys.ALGORITHM)), + cursor.getInt(cursor.getColumnIndex(Keys.KEY_SIZE))); + + TextView keyId = (TextView) view.findViewById(R.id.keyId); + keyId.setText(keyIdStr); + + TextView keyDetails = (TextView) view.findViewById(R.id.keyDetails); + keyDetails.setText("(" + algorithmStr + ")"); + + ImageView masterKeyIcon = (ImageView) view.findViewById(R.id.ic_masterKey); + if (cursor.getInt(cursor.getColumnIndex(Keys.IS_MASTER_KEY)) != 1) { + masterKeyIcon.setVisibility(View.INVISIBLE); + } else { + masterKeyIcon.setVisibility(View.VISIBLE); + } + + ImageView certifyIcon = (ImageView) view.findViewById(R.id.ic_certifyKey); + if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_CERTIFY)) != 1) { + certifyIcon.setVisibility(View.GONE); + } else { + certifyIcon.setVisibility(View.VISIBLE); + } + + ImageView encryptIcon = (ImageView) view.findViewById(R.id.ic_encryptKey); + if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_ENCRYPT)) != 1) { + encryptIcon.setVisibility(View.GONE); + } else { + encryptIcon.setVisibility(View.VISIBLE); + } + + ImageView signIcon = (ImageView) view.findViewById(R.id.ic_signKey); + if (cursor.getInt(cursor.getColumnIndex(Keys.CAN_SIGN)) != 1) { + signIcon.setVisibility(View.GONE); + } else { + signIcon.setVisibility(View.VISIBLE); + } + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.view_key_keys_item, null); + } + +} diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java index 55821b11e..2e2606fd0 100644 --- a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Dominik Schürmann + * Copyright (C) 2014 Dominik Schürmann * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,17 +17,11 @@ package org.sufficientlysecure.keychain.ui.adapter; -import java.util.HashMap; -import java.util.Set; - import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.helper.OtherHelper; import org.sufficientlysecure.keychain.provider.KeychainContract.UserIds; -import android.annotation.SuppressLint; import android.content.Context; import android.database.Cursor; -import android.graphics.Color; import android.support.v4.widget.CursorAdapter; import android.view.LayoutInflater; import android.view.View; @@ -47,35 +41,15 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter { public void bindView(View view, Context context, Cursor cursor) { int userIdIndex = cursor.getColumnIndex(UserIds.USER_ID); - TextView mainUserId = (TextView) view.findViewById(R.id.mainUserId); - mainUserId.setText(R.string.unknown_user_id); - TextView mainUserIdRest = (TextView) view.findViewById(R.id.mainUserIdRest); - mainUserIdRest.setText(""); - - String userId = cursor.getString(userIdIndex); - if (userId != null) { - String[] userIdSplit = OtherHelper.splitUserId(userId); - - if (userIdSplit[1] != null) { - mainUserIdRest.setText(userIdSplit[1]); - } - mainUserId.setText(userIdSplit[0]); - } - - if (mainUserId.getText().length() == 0) { - mainUserId.setText(R.string.unknown_user_id); - } + String userIdStr = cursor.getString(userIdIndex); - if (mainUserIdRest.getText().length() == 0) { - mainUserIdRest.setVisibility(View.GONE); - } else { - mainUserIdRest.setVisibility(View.VISIBLE); - } + TextView userId = (TextView) view.findViewById(R.id.userId); + userId.setText(userIdStr); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - return mInflater.inflate(R.layout.key_list_item, null); + return mInflater.inflate(R.layout.view_key_userids_item, null); } } diff --git a/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/FixedListView.java b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/FixedListView.java new file mode 100644 index 000000000..277f14b6f --- /dev/null +++ b/OpenPGP-Keychain/src/org/sufficientlysecure/keychain/ui/widget/FixedListView.java @@ -0,0 +1,55 @@ +package org.sufficientlysecure.keychain.ui.widget; + +/* + * Copyright (C) 2014 Dominik Schürmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ListView; + +/** + * Automatically calculate height of listview based on contained items. This enables to put this + * ListView into a ScrollView without messing up. + * + * from + * http://stackoverflow.com/questions/2419246/how-do-i-create-a-listview-thats-not-in-a-scrollview- + * or-has-the-scrollview-dis + */ +public class FixedListView extends ListView { + + public FixedListView(Context context) { + super(context); + } + + public FixedListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public FixedListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Calculate height of the entire list by providing a very large + // height hint. But do not use the highest 2 bits of this integer; + // those are reserved for the MeasureSpec mode. + int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, expandSpec); + } + +} \ No newline at end of file -- cgit v1.2.3 From e3bcf64d9e63e293c51db921572a9b87533d26ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 13 Jan 2014 00:48:20 +0100 Subject: bump version code to 21106 for beta testers --- OpenPGP-Keychain/AndroidManifest.xml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/OpenPGP-Keychain/AndroidManifest.xml b/OpenPGP-Keychain/AndroidManifest.xml index 9e4f3ca49..da655ba02 100644 --- a/OpenPGP-Keychain/AndroidManifest.xml +++ b/OpenPGP-Keychain/AndroidManifest.xml @@ -1,24 +1,8 @@ -