diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java index 095887433..e582ec559 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/provider/KeychainProvider.java @@ -87,7 +87,8 @@ public class KeychainProvider extends ContentProvider { private static final int CERTS = 401; private static final int CERTS_BY_KEY_ID = 402; private static final int CERTS_BY_ROW_ID = 403; - private static final int CERTS_BY_CERTIFIER_ID = 404; + private static final int CERTS_BY_KEY_ROW_ID = 404; + private static final int CERTS_BY_CERTIFIER_ID = 405; // private static final int DATA_STREAM = 401; @@ -253,6 +254,8 @@ public class KeychainProvider extends ContentProvider { */ matcher.addURI(authority, KeychainContract.BASE_CERTS, CERTS); matcher.addURI(authority, KeychainContract.BASE_CERTS + "/#", CERTS_BY_ROW_ID); + matcher.addURI(authority, KeychainContract.BASE_CERTS + "/" + + KeychainContract.PATH_BY_KEY_ROW_ID + "/#", CERTS_BY_KEY_ROW_ID); matcher.addURI(authority, KeychainContract.BASE_CERTS + "/" + KeychainContract.PATH_BY_KEY_ID + "/#", CERTS_BY_KEY_ID); matcher.addURI(authority, KeychainContract.BASE_CERTS + "/" @@ -713,6 +716,44 @@ public class KeychainProvider extends ContentProvider { break; + case CERTS_BY_KEY_ROW_ID: + qb.setTables(Tables.CERTS + + " JOIN " + Tables.USER_IDS + " ON (" + + Tables.CERTS + "." + Certs.KEY_RING_ROW_ID + " = " + + Tables.USER_IDS + "." + UserIds.KEY_RING_ROW_ID + + " AND " + + Tables.CERTS + "." + Certs.RANK + " = " + + Tables.USER_IDS + "." + UserIds.RANK + // noooooooot sure about this~ database design + + ") LEFT JOIN " + Tables.KEYS + " ON (" + + Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER + " = " + + Tables.KEYS + "." + Keys.KEY_ID + + ") LEFT JOIN " + Tables.USER_IDS + " AS signer ON (" + + Tables.KEYS + "." + Keys.KEY_RING_ROW_ID + " = " + + "signer." + UserIds.KEY_RING_ROW_ID + + " AND " + + Tables.KEYS + "." + Keys.RANK + " = " + + "signer." + UserIds.RANK + + ")"); + + // groupBy = Tables.USER_IDS + "." + UserIds.RANK; + + HashMap pmap2 = new HashMap(); + pmap2.put(Certs._ID, Tables.CERTS + "." + Certs._ID); + pmap2.put(Certs.RANK, Tables.CERTS + "." + Certs.RANK); + pmap2.put(Certs.KEY_ID_CERTIFIER, Tables.CERTS + "." + Certs.KEY_ID_CERTIFIER); + pmap2.put(Certs.VERIFIED, Tables.CERTS + "." + Certs.VERIFIED); + // verified key data + pmap2.put(UserIds.USER_ID, Tables.USER_IDS + "." + UserIds.USER_ID); + // verifying key data + pmap2.put("signer_uid", "signer." + UserIds.USER_ID + " AS signer_uid"); + qb.setProjectionMap(pmap2); + + qb.appendWhere(Tables.CERTS + "." + Certs.KEY_RING_ROW_ID + " = "); + qb.appendWhereEscapeString(uri.getPathSegments().get(2)); + + break; + case API_APPS: qb.setTables(Tables.API_APPS); @@ -825,7 +866,7 @@ public class KeychainProvider extends ContentProvider { break; case CERTS_BY_ROW_ID: rowId = db.insertOrThrow(Tables.CERTS, null, values); - // kinda useless :S + // kinda useless.. should this be buildCertsByKeyRowIdUri? rowUri = Certs.buildCertsUri(Long.toString(rowId)); break; diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index 8f177dc6b..055203c0f 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -84,13 +84,11 @@ public class ViewKeyActivity extends ActionBarActivity { selectedTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB); } - { - // normalize mDataUri to a "by row id" query, to ensure it works with any - // given valid /public/ query - long rowId = ProviderHelper.getRowId(this, getIntent().getData()); - // TODO: handle (rowId == 0) with something else than a crash - mDataUri = KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)) ; - } + // normalize mDataUri to a "by row id" query, to ensure it works with any + // given valid /public/ query + long rowId = ProviderHelper.getRowId(this, getIntent().getData()); + // TODO: handle (rowId == 0) with something else than a crash + mDataUri = KeychainContract.KeyRings.buildPublicKeyRingsUri(Long.toString(rowId)) ; Bundle mainBundle = new Bundle(); mainBundle.putParcelable(ViewKeyMainFragment.ARG_DATA_URI, mDataUri); @@ -98,7 +96,7 @@ public class ViewKeyActivity extends ActionBarActivity { ViewKeyMainFragment.class, mainBundle, (selectedTab == 0 ? true : false)); Bundle certBundle = new Bundle(); - certBundle.putParcelable(ViewKeyCertsFragment.ARG_DATA_URI, mDataUri); + certBundle.putLong(ViewKeyCertsFragment.ARG_KEYRING_ROW_ID, rowId); mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.key_view_tab_certs)), ViewKeyCertsFragment.class, certBundle, (selectedTab == 1 ? true : false)); } diff --git a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java index 36d3e6ace..abed097c1 100644 --- a/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java +++ b/OpenPGP-Keychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java @@ -17,26 +17,66 @@ package org.sufficientlysecure.keychain.ui; +import android.annotation.SuppressLint; +import android.content.Context; import android.content.Intent; +import android.database.Cursor; +import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v4.widget.CursorAdapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; import com.beardedhen.androidbootstrap.BootstrapButton; 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.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainDatabase; import org.sufficientlysecure.keychain.util.Log; +import java.nio.ByteBuffer; +import java.util.HashMap; -public class ViewKeyCertsFragment extends Fragment { +import se.emilsjolander.stickylistheaders.ApiLevelTooLowException; +import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; +import se.emilsjolander.stickylistheaders.StickyListHeadersListView; - public static final String ARG_DATA_URI = "uri"; - private BootstrapButton mActionCertify; +public class ViewKeyCertsFragment extends Fragment + implements LoaderManager.LoaderCallbacks { + + // These are the rows that we will retrieve. + static final String[] PROJECTION = new String[]{ + KeychainContract.Certs._ID, + KeychainContract.Certs.VERIFIED, + KeychainContract.Certs.RANK, + KeychainContract.Certs.KEY_ID_CERTIFIER, + KeychainContract.UserIds.USER_ID, + "signer_uid" + }; + + // sort by our user id, + static final String SORT_ORDER = + KeychainDatabase.Tables.USER_IDS + "." + KeychainContract.UserIds.USER_ID + " ASC, " + + KeychainDatabase.Tables.CERTS + "." + KeychainContract.Certs.VERIFIED + " DESC, " + + "signer_uid ASC"; + + public static final String ARG_KEYRING_ROW_ID = "row_id"; + + private StickyListHeadersListView mStickyList; + + private CertListAdapter mAdapter; private Uri mDataUri; @@ -44,8 +84,6 @@ public class ViewKeyCertsFragment extends Fragment { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.view_key_certs_fragment, container, false); - mActionCertify = (BootstrapButton) view.findViewById(R.id.action_certify); - return view; } @@ -53,40 +91,197 @@ public class ViewKeyCertsFragment extends Fragment { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); - if (dataUri == null) { + mStickyList = (StickyListHeadersListView) getActivity().findViewById(R.id.list); + + if (!getArguments().containsKey(ARG_KEYRING_ROW_ID)) { Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); getActivity().finish(); return; } - loadData(dataUri); - } + long rowId = getArguments().getLong(ARG_KEYRING_ROW_ID); + mDataUri = KeychainContract.Certs.buildCertsByKeyRowIdUri(Long.toString(rowId)); - private void loadData(Uri dataUri) { - if (dataUri.equals(mDataUri)) { - Log.d(Constants.TAG, "Same URI, no need to load the data again!"); - return; + mStickyList.setAreHeadersSticky(true); + mStickyList.setDrawingListUnderStickyHeader(false); + mStickyList.setFastScrollEnabled(true); + + try { + mStickyList.setFastScrollAlwaysVisible(true); + } catch (ApiLevelTooLowException e) { } - mDataUri = dataUri; + // TODO this view is made visible if no data is available + // mStickyList.setEmptyView(getActivity().findViewById(R.id.empty)); - Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); - mActionCertify.setOnClickListener(new View.OnClickListener() { + // Create an empty adapter we will use to display the loaded data. + mAdapter = new CertListAdapter(getActivity(), null); + mStickyList.setAdapter(mAdapter); - @Override - public void onClick(View v) { - certifyKey(mDataUri); - } - }); + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + getLoaderManager().initLoader(0, null, this); } - private void certifyKey(Uri dataUri) { - Intent signIntent = new Intent(getActivity(), CertifyKeyActivity.class); - signIntent.setData(dataUri); - startActivity(signIntent); + @Override + public Loader onCreateLoader(int id, Bundle args) { + // Now create and return a CursorLoader that will take care of + // creating a Cursor for the data being displayed. + return new CursorLoader(getActivity(), mDataUri, PROJECTION, null, null, SORT_ORDER); + } + + @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.swapCursor(data); + + mStickyList.setAdapter(mAdapter); + } + + @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.swapCursor(null); + } + + /** + * Implements StickyListHeadersAdapter from library + */ + private class CertListAdapter extends CursorAdapter implements StickyListHeadersAdapter { + private LayoutInflater mInflater; + private int mIndexUserId, mIndexRank; + private int mIndexSignerKeyId, mIndexSignerUserId; + private int mIndexVerified; + + public CertListAdapter(Context context, Cursor c) { + super(context, c, 0); + + mInflater = LayoutInflater.from(context); + initIndex(c); + } + + @Override + public Cursor swapCursor(Cursor newCursor) { + initIndex(newCursor); + + return super.swapCursor(newCursor); + } + + /** + * Get column indexes for performance reasons just once in constructor and swapCursor. For a + * performance comparison see http://stackoverflow.com/a/17999582 + * + * @param cursor + */ + private void initIndex(Cursor cursor) { + if (cursor != null) { + + mIndexUserId = cursor.getColumnIndexOrThrow(KeychainContract.UserIds.USER_ID); + mIndexRank = cursor.getColumnIndexOrThrow(KeychainContract.UserIds.RANK); + mIndexVerified = cursor.getColumnIndexOrThrow(KeychainContract.Certs.VERIFIED); + mIndexSignerKeyId = cursor.getColumnIndexOrThrow(KeychainContract.Certs.KEY_ID_CERTIFIER); + mIndexSignerUserId = cursor.getColumnIndexOrThrow("signer_uid"); + } + } + + /** + * Bind cursor data to the item list view + *

+ * NOTE: CursorAdapter already implements the ViewHolder pattern in its getView() method. + * Thus no ViewHolder is required here. + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + + // set name and stuff, common to both key types + TextView wSignerKeyId = (TextView) view.findViewById(R.id.signerKeyId); + TextView wSignerUserId = (TextView) view.findViewById(R.id.signerUserId); + TextView wSignStatus = (TextView) view.findViewById(R.id.signStatus); + + String signerKeyId = PgpKeyHelper.convertKeyIdToHex(cursor.getLong(mIndexSignerKeyId)); + String signerUserId = cursor.getString(mIndexSignerUserId); + String signStatus = cursor.getInt(mIndexVerified) > 0 ? "ok" : "unknown"; + + wSignerUserId.setText(signerUserId); + wSignerKeyId.setText(signerKeyId); + wSignStatus.setText(signStatus); + + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(R.layout.view_key_certs_item, parent, false); + } + + /** + * Creates a new header view and binds the section headers to it. It uses the ViewHolder + * pattern. Most functionality is similar to getView() from Android's CursorAdapter. + *

+ * NOTE: The variables mDataValid and mCursor are available due to the super class + * CursorAdapter. + */ + @Override + public View getHeaderView(int position, View convertView, ViewGroup parent) { + HeaderViewHolder holder; + if (convertView == null) { + holder = new HeaderViewHolder(); + convertView = mInflater.inflate(R.layout.view_key_certs_header, parent, false); + holder.text = (TextView) convertView.findViewById(R.id.stickylist_header_text); + holder.count = (TextView) convertView.findViewById(R.id.certs_num); + 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; + } + + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + // set header text as first char in user id + String userId = mCursor.getString(mIndexUserId); + holder.text.setText(userId); + holder.count.setVisibility(View.GONE); + return convertView; + } + + /** + * Header IDs should 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; + } + + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + + // otherwise, return the first character of the name as ID + return mCursor.getInt(mIndexRank); + + // sort by the first four characters (should be enough I guess?) + // return ByteBuffer.wrap(userId.getBytes()).asLongBuffer().get(0); + } + + class HeaderViewHolder { + TextView text; + TextView count; + } + } } \ No newline at end of file diff --git a/OpenPGP-Keychain/src/main/res/layout/view_key_certs_fragment.xml b/OpenPGP-Keychain/src/main/res/layout/view_key_certs_fragment.xml index 299471c66..faa26b525 100644 --- a/OpenPGP-Keychain/src/main/res/layout/view_key_certs_fragment.xml +++ b/OpenPGP-Keychain/src/main/res/layout/view_key_certs_fragment.xml @@ -1,7 +1,8 @@ + android:layout_height="match_parent" + android:fillViewport="true"> - - - - - + android:layout_height="match_parent" + class="se.emilsjolander.stickylistheaders.StickyListHeadersListView" + android:id="@+id/list" /> \ No newline at end of file