From e663dadc32633dc12f846539196276265ccc3534 Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Fri, 18 Apr 2014 12:44:42 -0700 Subject: [PATCH 01/72] can search openkeychain, retrieve & install & use keys from there --- .gitignore | 3 +- .../service/KeychainIntentService.java | 53 ++++++ .../keychain/ui/ImportKeysActivity.java | 41 ++++- .../ui/ImportKeysClipboardFragment.java | 2 +- .../keychain/ui/ImportKeysFileFragment.java | 2 +- .../ui/ImportKeysKeybaseFragment.java | 108 +++++++++++ .../keychain/ui/ImportKeysListFragment.java | 52 +++++- .../keychain/ui/ImportKeysQrCodeFragment.java | 4 +- .../keychain/ui/ImportKeysServerFragment.java | 2 +- .../ui/adapter/ImportKeysListEntry.java | 2 +- .../adapter/ImportKeysListKeybaseLoader.java | 109 +++++++++++ .../keychain/util/JWalk.java | 56 ++++++ .../keychain/util/KeybaseKeyServer.java | 173 ++++++++++++++++++ .../layout/import_keys_keybase_fragment.xml | 51 ++++++ OpenKeychain/src/main/res/values/arrays.xml | 1 + OpenKeychain/src/main/res/values/strings.xml | 3 + 16 files changed, 649 insertions(+), 13 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysKeybaseFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListKeybaseLoader.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/JWalk.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java create mode 100644 OpenKeychain/src/main/res/layout/import_keys_keybase_fragment.xml diff --git a/.gitignore b/.gitignore index 11b413fd9..1dfe84d5a 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ pom.xml.* *.iml #OS Specific -[Tt]humbs.db \ No newline at end of file +[Tt]humbs.db +.DS_Store diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index 5615b59c4..fcd31b2fe 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -56,6 +56,8 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry; import org.sufficientlysecure.keychain.util.HkpKeyServer; import org.sufficientlysecure.keychain.util.InputData; +import org.sufficientlysecure.keychain.util.KeyServer; +import org.sufficientlysecure.keychain.util.KeybaseKeyServer; import org.sufficientlysecure.keychain.util.KeychainServiceListener; import org.sufficientlysecure.keychain.util.Log; import org.sufficientlysecure.keychain.util.ProgressScaler; @@ -103,6 +105,7 @@ public class KeychainIntentService extends IntentService public static final String ACTION_UPLOAD_KEYRING = Constants.INTENT_PREFIX + "UPLOAD_KEYRING"; public static final String ACTION_DOWNLOAD_AND_IMPORT_KEYS = Constants.INTENT_PREFIX + "QUERY_KEYRING"; + public static final String ACTION_IMPORT_KEYBASE_KEYS = Constants.INTENT_PREFIX + "DOWNLOAD_KEYBASE"; public static final String ACTION_CERTIFY_KEYRING = Constants.INTENT_PREFIX + "SIGN_KEYRING"; @@ -739,6 +742,56 @@ public class KeychainIntentService extends IntentService } catch (Exception e) { sendErrorToHandler(e); } + } else if (ACTION_IMPORT_KEYBASE_KEYS.equals(action)) { + ArrayList entries = data.getParcelableArrayList(DOWNLOAD_KEY_LIST); + + try { + KeybaseKeyServer server = new KeybaseKeyServer(); + for (ImportKeysListEntry entry : entries) { + // the keybase handle is in userId(1) + String username = entry.getUserIds().get(1); + byte[] downloadedKeyBytes = server.get(username).getBytes(); + + // create PGPKeyRing object based on downloaded armored key + PGPKeyRing downloadedKey = null; + BufferedInputStream bufferedInput = + new BufferedInputStream(new ByteArrayInputStream(downloadedKeyBytes)); + if (bufferedInput.available() > 0) { + InputStream in = PGPUtil.getDecoderStream(bufferedInput); + PGPObjectFactory objectFactory = new PGPObjectFactory(in); + + // get first object in block + Object obj; + if ((obj = objectFactory.nextObject()) != null) { + Log.d(Constants.TAG, "Found class: " + obj.getClass()); + + if (obj instanceof PGPKeyRing) { + downloadedKey = (PGPKeyRing) obj; + } else { + throw new PgpGeneralException("Object not recognized as PGPKeyRing!"); + } + } + } + + // save key bytes in entry object for doing the + // actual import afterwards + entry.setBytes(downloadedKey.getEncoded()); + } + + Intent importIntent = new Intent(this, KeychainIntentService.class); + importIntent.setAction(ACTION_IMPORT_KEYRING); + Bundle importData = new Bundle(); + importData.putParcelableArrayList(IMPORT_KEY_LIST, entries); + importIntent.putExtra(EXTRA_DATA, importData); + importIntent.putExtra(EXTRA_MESSENGER, mMessenger); + + // now import it with this service + onHandleIntent(importIntent); + + // result is handled in ACTION_IMPORT_KEYRING + } catch (Exception e) { + sendErrorToHandler(e); + } } else if (ACTION_DOWNLOAD_AND_IMPORT_KEYS.equals(action)) { try { ArrayList entries = data.getParcelableArrayList(DOWNLOAD_KEY_LIST); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java index 0fccd668f..650e51069 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysActivity.java @@ -62,6 +62,8 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O + "IMPORT_KEY_FROM_KEYSERVER"; public static final String ACTION_IMPORT_KEY_FROM_KEYSERVER_AND_RETURN = Constants.INTENT_PREFIX + "IMPORT_KEY_FROM_KEY_SERVER_AND_RETURN"; + public static final String ACTION_IMPORT_KEY_FROM_KEYBASE = Constants.INTENT_PREFIX + + "IMPORT_KEY_FROM_KEYBASE"; // Actions for internal use only: public static final String ACTION_IMPORT_KEY_FROM_FILE = Constants.INTENT_PREFIX @@ -92,13 +94,15 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O ImportKeysFileFragment.class, ImportKeysQrCodeFragment.class, ImportKeysClipboardFragment.class, - ImportKeysNFCFragment.class + ImportKeysNFCFragment.class, + ImportKeysKeybaseFragment.class }; private static final int NAV_SERVER = 0; private static final int NAV_FILE = 1; private static final int NAV_QR_CODE = 2; private static final int NAV_CLIPBOARD = 3; private static final int NAV_NFC = 4; + private static final int NAV_KEYBASE = 5; private int mCurrentNavPosition = -1; @@ -236,6 +240,12 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O // NOTE: this only displays the appropriate fragment, no actions are taken loadNavFragment(NAV_NFC, null); + // no immediate actions! + startListFragment(savedInstanceState, null, null, null); + } else if (ACTION_IMPORT_KEY_FROM_KEYBASE.equals(action)) { + // NOTE: this only displays the appropriate fragment, no actions are taken + loadNavFragment(NAV_KEYBASE, null); + // no immediate actions! startListFragment(savedInstanceState, null, null, null); } else { @@ -340,8 +350,8 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O startListFragment(savedInstanceState, null, null, query); } - public void loadCallback(byte[] importData, Uri dataUri, String serverQuery, String keyServer) { - mListFragment.loadNew(importData, dataUri, serverQuery, keyServer); + public void loadCallback(byte[] importData, Uri dataUri, String serverQuery, String keyServer, String keybaseQuery) { + mListFragment.loadNew(importData, dataUri, serverQuery, keyServer, keybaseQuery); } /** @@ -449,6 +459,31 @@ public class ImportKeysActivity extends ActionBarActivity implements ActionBar.O // start service with intent startService(intent); + } else if (mListFragment.getKeybaseQuery() != null) { + // Send all information needed to service to query keys in other thread + Intent intent = new Intent(this, KeychainIntentService.class); + + intent.setAction(KeychainIntentService.ACTION_IMPORT_KEYBASE_KEYS); + + // fill values for this action + Bundle data = new Bundle(); + + // get selected key entries + ArrayList selectedEntries = mListFragment.getSelectedData(); + data.putParcelableArrayList(KeychainIntentService.DOWNLOAD_KEY_LIST, selectedEntries); + + intent.putExtra(KeychainIntentService.EXTRA_DATA, data); + + // Create a new Messenger for the communication back + Messenger messenger = new Messenger(saveHandler); + intent.putExtra(KeychainIntentService.EXTRA_MESSENGER, messenger); + + // show progress dialog + saveHandler.showProgressDialog(this); + + // start service with intent + startService(intent); + } else { AppMsg.makeText(this, R.string.error_nothing_import, AppMsg.STYLE_ALERT).show(); } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java index 412fbddd8..f331358fa 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysClipboardFragment.java @@ -71,7 +71,7 @@ public class ImportKeysClipboardFragment extends Fragment { return; } } - mImportActivity.loadCallback(sendText.getBytes(), null, null, null); + mImportActivity.loadCallback(sendText.getBytes(), null, null, null, null); } }); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java index dc5333a8f..51f961aab 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysFileFragment.java @@ -85,7 +85,7 @@ public class ImportKeysFileFragment extends Fragment { if (resultCode == Activity.RESULT_OK && data != null) { // load data - mImportActivity.loadCallback(null, data.getData(), null, null); + mImportActivity.loadCallback(null, data.getData(), null, null, null); } break; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysKeybaseFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysKeybaseFragment.java new file mode 100644 index 000000000..7ae57cec3 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysKeybaseFragment.java @@ -0,0 +1,108 @@ +package org.sufficientlysecure.keychain.ui; + +import android.content.Context; +import android.support.v4.app.Fragment; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.TextView; + +import com.beardedhen.androidbootstrap.BootstrapButton; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.util.Log; + +/** + * Import public keys from the Keybase.io directory. First cut: just raw search. + * TODO: make a pick list of the people you’re following on keybase + */ +public class ImportKeysKeybaseFragment extends Fragment { + + private ImportKeysActivity mImportActivity; + private BootstrapButton mSearchButton; + private EditText mQueryEditText; + + public static final String ARG_QUERY = "query"; + public static final String ARG_DISABLE_QUERY_EDIT = "disable_query_edit"; + + /** + * Creates new instance of this fragment + */ + public static ImportKeysKeybaseFragment newInstance() { + ImportKeysKeybaseFragment frag = new ImportKeysKeybaseFragment(); + + Bundle args = new Bundle(); + frag.setArguments(args); + + return frag; + } + + /** + * Inflate the layout for this fragment + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.import_keys_keybase_fragment, container, false); + + mQueryEditText = (EditText) view.findViewById(R.id.import_keybase_query); + + mSearchButton = (BootstrapButton) view.findViewById(R.id.import_keybase_search); + mSearchButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String query = mQueryEditText.getText().toString(); + search(query); + + // close keyboard after pressing search + InputMethodManager imm = + (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mQueryEditText.getWindowToken(), 0); + } + }); + + mQueryEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_SEARCH) { + String query = mQueryEditText.getText().toString(); + search(query); + + // Don't return true to let the keyboard close itself after pressing search + return false; + } + return false; + } + }); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + mImportActivity = (ImportKeysActivity) getActivity(); + + // set displayed values + if (getArguments() != null) { + if (getArguments().containsKey(ARG_QUERY)) { + String query = getArguments().getString(ARG_QUERY); + mQueryEditText.setText(query, TextView.BufferType.EDITABLE); + } + + if (getArguments().getBoolean(ARG_DISABLE_QUERY_EDIT, false)) { + mQueryEditText.setEnabled(false); + } + } + } + + private void search(String query) { + mImportActivity.loadCallback(null, null, null, null, query); + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java index 3a6c384e8..0580db080 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java @@ -34,6 +34,7 @@ import org.sufficientlysecure.keychain.helper.Preferences; import org.sufficientlysecure.keychain.ui.adapter.AsyncTaskResultWrapper; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysAdapter; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry; +import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListKeybaseLoader; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListLoader; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListServerLoader; import org.sufficientlysecure.keychain.util.InputData; @@ -60,9 +61,11 @@ public class ImportKeysListFragment extends ListFragment implements private Uri mDataUri; private String mServerQuery; private String mKeyServer; + private String mKeybaseQuery; private static final int LOADER_ID_BYTES = 0; private static final int LOADER_ID_SERVER_QUERY = 1; + private static final int LOADER_ID_KEYBASE = 2; public byte[] getKeyBytes() { return mKeyBytes; @@ -76,6 +79,10 @@ public class ImportKeysListFragment extends ListFragment implements return mServerQuery; } + public String getKeybaseQuery() { + return mKeybaseQuery; + } + public String getKeyServer() { return mKeyServer; } @@ -148,6 +155,16 @@ public class ImportKeysListFragment extends ListFragment implements // give arguments to onCreateLoader() getLoaderManager().initLoader(LOADER_ID_SERVER_QUERY, null, this); } + + if (mKeybaseQuery != null) { + // Start out with a progress indicator. + setListShown(false); + + // Prepare the loader. Either re-connect with an existing one, + // or start a new one. + // give arguments to onCreateLoader() + getLoaderManager().initLoader(LOADER_ID_KEYBASE, null, this); + } } @Override @@ -157,16 +174,18 @@ public class ImportKeysListFragment extends ListFragment implements // Select checkbox! // Update underlying data and notify adapter of change. The adapter will // update the view automatically. + ImportKeysListEntry entry = mAdapter.getItem(position); entry.setSelected(!entry.isSelected()); mAdapter.notifyDataSetChanged(); } - public void loadNew(byte[] keyBytes, Uri dataUri, String serverQuery, String keyServer) { + public void loadNew(byte[] keyBytes, Uri dataUri, String serverQuery, String keyServer, String keybaseQuery) { mKeyBytes = keyBytes; mDataUri = dataUri; mServerQuery = serverQuery; mKeyServer = keyServer; + mKeybaseQuery = keybaseQuery; if (mKeyBytes != null || mDataUri != null) { // Start out with a progress indicator. @@ -181,11 +200,18 @@ public class ImportKeysListFragment extends ListFragment implements getLoaderManager().restartLoader(LOADER_ID_SERVER_QUERY, null, this); } + + if (mKeybaseQuery != null) { + // Start out with a progress indicator. + setListShown(false); + + getLoaderManager().restartLoader(LOADER_ID_KEYBASE, null, this); + } } @Override public Loader>> - onCreateLoader(int id, Bundle args) { + onCreateLoader(int id, Bundle args) { switch (id) { case LOADER_ID_BYTES: { InputData inputData = getInputData(mKeyBytes, mDataUri); @@ -194,6 +220,9 @@ public class ImportKeysListFragment extends ListFragment implements case LOADER_ID_SERVER_QUERY: { return new ImportKeysListServerLoader(getActivity(), mServerQuery, mKeyServer); } + case LOADER_ID_KEYBASE: { + return new ImportKeysListKeybaseLoader(getActivity(), mKeybaseQuery); + } default: return null; @@ -248,7 +277,7 @@ public class ImportKeysListFragment extends ListFragment implements if (error == null) { AppMsg.makeText( getActivity(), getResources().getQuantityString(R.plurals.keys_found, - mAdapter.getCount(), mAdapter.getCount()), + mAdapter.getCount(), mAdapter.getCount()), AppMsg.STYLE_INFO ).show(); } else if (error instanceof KeyServer.InsufficientQuery) { @@ -263,6 +292,19 @@ public class ImportKeysListFragment extends ListFragment implements } break; + case LOADER_ID_KEYBASE: + + if (error == null) { + AppMsg.makeText( + getActivity(), getResources().getQuantityString(R.plurals.keys_found, + mAdapter.getCount(), mAdapter.getCount()), + AppMsg.STYLE_INFO + ).show(); + } else if (error instanceof KeyServer.QueryException) { + AppMsg.makeText(getActivity(), R.string.error_keyserver_query, + AppMsg.STYLE_ALERT).show(); + } + default: break; } @@ -279,6 +321,10 @@ public class ImportKeysListFragment extends ListFragment implements // Clear the data in the adapter. mAdapter.clear(); break; + case LOADER_ID_KEYBASE: + // Clear the data in the adapter. + mAdapter.clear(); + break; default: break; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java index 65d463456..22b56e1ab 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysQrCodeFragment.java @@ -117,7 +117,7 @@ public class ImportKeysQrCodeFragment extends Fragment { // is this a full key encoded as qr code? if (scannedContent.startsWith("-----BEGIN PGP")) { - mImportActivity.loadCallback(scannedContent.getBytes(), null, null, null); + mImportActivity.loadCallback(scannedContent.getBytes(), null, null, null, null); return; } @@ -197,7 +197,7 @@ public class ImportKeysQrCodeFragment extends Fragment { for (String in : mScannedContent) { result += in; } - mImportActivity.loadCallback(result.getBytes(), null, null, null); + mImportActivity.loadCallback(result.getBytes(), null, null, null, null); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java index 82fe2fc4c..9e3d88ff5 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysServerFragment.java @@ -151,7 +151,7 @@ public class ImportKeysServerFragment extends Fragment { } private void search(String query, String keyServer) { - mImportActivity.loadCallback(null, null, query, keyServer); + mImportActivity.loadCallback(null, null, query, keyServer, null); } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java index b06852af4..1610bfeab 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListEntry.java @@ -203,7 +203,7 @@ public class ImportKeysListEntry implements Serializable, Parcelable { * Constructor for later querying from keyserver */ public ImportKeysListEntry() { - // keys from keyserver are always public keys + // keys from keyserver are always public keys; from keybase too secretKey = false; // do not select by default mSelected = false; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListKeybaseLoader.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListKeybaseLoader.java new file mode 100644 index 000000000..73ff9a8f8 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysListKeybaseLoader.java @@ -0,0 +1,109 @@ +/* + * 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 android.content.Context; +import android.support.v4.content.AsyncTaskLoader; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.util.KeyServer; +import org.sufficientlysecure.keychain.util.KeybaseKeyServer; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.ArrayList; + +public class ImportKeysListKeybaseLoader + extends AsyncTaskLoader>> { + Context mContext; + + String mKeybaseQuery; + + private ArrayList mEntryList = new ArrayList(); + private AsyncTaskResultWrapper> mEntryListWrapper; + + public ImportKeysListKeybaseLoader(Context context, String keybaseQuery) { + super(context); + mContext = context; + mKeybaseQuery = keybaseQuery; + } + + @Override + public AsyncTaskResultWrapper> loadInBackground() { + + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, null); + + if (mKeybaseQuery == null) { + Log.e(Constants.TAG, "mKeybaseQery is null!"); + return mEntryListWrapper; + } + + queryServer(mKeybaseQuery); + + return mEntryListWrapper; + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + } + + @Override + protected void onStartLoading() { + forceLoad(); + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + public void deliverResult(AsyncTaskResultWrapper> data) { + super.deliverResult(data); + } + + /** + * Query keybase + */ + private void queryServer(String query) { + + KeybaseKeyServer server = new KeybaseKeyServer(); + try { + ArrayList searchResult = server.search(query); + + mEntryList.clear(); + + mEntryList.addAll(searchResult); + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, null); + } catch (KeyServer.InsufficientQuery e) { + Log.e(Constants.TAG, "InsufficientQuery", e); + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, e); + } catch (KeyServer.QueryException e) { + Log.e(Constants.TAG, "QueryException", e); + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, e); + } catch (KeyServer.TooManyResponses e) { + Log.e(Constants.TAG, "TooManyResponses", e); + mEntryListWrapper = new AsyncTaskResultWrapper>(mEntryList, e); + } + + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/JWalk.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/JWalk.java new file mode 100644 index 000000000..6f9c4cfa5 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/JWalk.java @@ -0,0 +1,56 @@ +package org.sufficientlysecure.keychain.util; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Minimal hierarchy selector + */ +public class JWalk { + + public static int getInt(JSONObject json, String... path) throws JSONException { + json = walk(json, path); + return json.getInt(path[path.length - 1]); + } + + public static long getLong(JSONObject json, String... path) throws JSONException { + json = walk(json, path); + return json.getLong(path[path.length - 1]); + } + + public static String getString(JSONObject json, String... path) throws JSONException { + json = walk(json, path); + return json.getString(path[path.length - 1]); + } + + public static JSONArray getArray(JSONObject json, String... path) throws JSONException { + json = walk(json, path); + return json.getJSONArray(path[path.length - 1]); + } + + public static JSONObject optObject(JSONObject json, String... path) throws JSONException { + json = walk(json, path); + return json.optJSONObject(path[path.length - 1]); + } + + private static JSONObject walk(JSONObject json, String... path) throws JSONException { + int len = path.length - 1; + int pathIndex = 0; + try { + while (pathIndex < len) { + json = json.getJSONObject(path[pathIndex]); + pathIndex++; + } + } catch (JSONException e) { + // try to give ’em a nice-looking error + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < len; i++) { + sb.append(path[i]).append('.'); + } + sb.append(path[len]); + throw new JSONException("JWalk error at step " + pathIndex + " of " + sb); + } + return json; + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java new file mode 100644 index 000000000..4b802c0e1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2012-2014 Dominik Schürmann + * Copyright (C) 2011-2014 Thialfihar + * Copyright (C) 2011 Senecaso + * + * 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 org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.TimeZone; +import java.util.WeakHashMap; + +public class KeybaseKeyServer extends KeyServer { + + private WeakHashMap mKeyCache = new WeakHashMap(); + + private static String readAll(InputStream in, String encoding) throws IOException { + ByteArrayOutputStream raw = new ByteArrayOutputStream(); + + byte buffer[] = new byte[1 << 16]; + int n = 0; + while ((n = in.read(buffer)) != -1) { + raw.write(buffer, 0, n); + } + + if (encoding == null) { + encoding = "utf8"; + } + return raw.toString(encoding); + } + + @Override + public ArrayList search(String query) throws QueryException, TooManyResponses, + InsufficientQuery { + ArrayList results = new ArrayList(); + + JSONObject fromQuery = getFromKeybase("_/api/1.0/user/autocomplete.json?q=", query); + try { + + JSONArray matches = JWalk.getArray(fromQuery, "completions"); + for (int i = 0; i < matches.length(); i++) { + JSONObject match = matches.getJSONObject(i); + + // only list them if they have a key + if (JWalk.optObject(match, "components", "key_fingerprint") != null) { + results.add(makeEntry(match)); + } + } + } catch (Exception e) { + throw new QueryException("Unexpected structure in keybase search result: " + e.getMessage()); + } + + return results; + } + + private JSONObject getUser(String keybaseID) throws QueryException { + try { + return getFromKeybase("_/api/1.0/user/lookup.json?username=", keybaseID); + } catch (Exception e) { + String detail = ""; + if (keybaseID != null) { + detail = ". Query was for user '" + keybaseID + "'"; + } + throw new QueryException(e.getMessage() + detail); + } + } + + private ImportKeysListEntry makeEntry(JSONObject match) throws QueryException, JSONException { + + String keybaseID = JWalk.getString(match, "components", "username", "val"); + String key_fingerprint = JWalk.getString(match, "components", "key_fingerprint", "val"); + key_fingerprint = key_fingerprint.replace(" ", "").toUpperCase(); + match = getUser(keybaseID); + + final ImportKeysListEntry entry = new ImportKeysListEntry(); + + entry.setBitStrength(4096); + entry.setAlgorithm("RSA"); + entry.setKeyIdHex("0x" + key_fingerprint); + + final long creationDate = JWalk.getLong(match, "them", "public_keys", "primary", "ctime"); + final GregorianCalendar tmpGreg = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + tmpGreg.setTimeInMillis(creationDate * 1000); + entry.setDate(tmpGreg.getTime()); + entry.setRevoked(false); + mKeyCache.put(keybaseID, JWalk.getString(match,"them", "public_keys", "primary", "bundle")); + String name = JWalk.getString(match, "them", "profile", "full_name"); + ArrayList userIds = new ArrayList(); + userIds.add(name); + userIds.add("keybase.io/" + keybaseID); // TODO: Maybe should be keybaseID@keybase.io ? + entry.setUserIds(userIds); + entry.setPrimaryUserId(name); + return entry; + } + + private JSONObject getFromKeybase(String path, String query) throws QueryException { + try { + String url = "https://keybase.io/" + path + URLEncoder.encode(query, "utf8"); + Log.d(Constants.TAG, "keybase query: " + url); + + URL realUrl = new URL(url); + HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection(); + conn.setConnectTimeout(5000); // TODO: Reasonable values for keybase + conn.setReadTimeout(25000); + conn.connect(); + int response = conn.getResponseCode(); + if (response >= 200 && response < 300) { + String text = readAll(conn.getInputStream(), conn.getContentEncoding()); + try { + JSONObject json = new JSONObject(text); + if (JWalk.getInt(json, "status", "code") != 0) { + throw new QueryException("Keybase autocomplete search failed"); + } + return json; + } catch (JSONException e) { + throw new QueryException("Keybase.io query returned broken JSON"); + } + } else { + String message = readAll(conn.getErrorStream(), conn.getContentEncoding()); + throw new QueryException("Keybase.io query error (status=" + response + + "): " + message); + } + } catch (Exception e) { + throw new QueryException("Keybase.io query error"); + } + } + + @Override + public String get(String id) throws QueryException { + // id is like "keybase/username" + String keybaseID = id.substring(id.indexOf('/') + 1); + String key = mKeyCache.get(keybaseID); + if (key == null) { + try { + JSONObject user = getUser(keybaseID); + key = JWalk.getString(user, "them", "public_keys", "primary", "bundle"); + } catch (Exception e) { + throw new QueryException(e.getMessage()); + } + } + return key; + } + + @Override + public void add(String armoredKey) throws AddKeyException { + throw new AddKeyException(); + } +} \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/import_keys_keybase_fragment.xml b/OpenKeychain/src/main/res/layout/import_keys_keybase_fragment.xml new file mode 100644 index 000000000..248581342 --- /dev/null +++ b/OpenKeychain/src/main/res/layout/import_keys_keybase_fragment.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/values/arrays.xml b/OpenKeychain/src/main/res/values/arrays.xml index 4173d49e4..699c02aff 100644 --- a/OpenKeychain/src/main/res/values/arrays.xml +++ b/OpenKeychain/src/main/res/values/arrays.xml @@ -54,6 +54,7 @@ @string/menu_import_from_qr_code @string/import_from_clipboard @string/menu_import_from_nfc + @string/menu_import_from_keybase diff --git a/OpenKeychain/src/main/res/values/strings.xml b/OpenKeychain/src/main/res/values/strings.xml index 330bc349d..21d008bb9 100644 --- a/OpenKeychain/src/main/res/values/strings.xml +++ b/OpenKeychain/src/main/res/values/strings.xml @@ -75,6 +75,7 @@ Create key (expert) Search Keyserver + Import from Keybase.io Keyserver… Update from keyserver Upload to key server @@ -348,6 +349,7 @@ Search Public Keys Search Secret Keys Share Key with… + Search Keybase.io 512 @@ -393,6 +395,7 @@ To receive keys via NFC, the device needs to be unlocked. Help Get key from clipboard + Get key from Keybase.io Decrypt File with OpenKeychain From ea1032dbfdbb42f39ef37af510e38b64f9854d5a Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Mon, 28 Apr 2014 10:47:39 -0700 Subject: [PATCH 02/72] Clean up debug crud --- .../keychain/service/KeychainIntentService.java | 3 --- .../org/sufficientlysecure/keychain/util/KeybaseKeyServer.java | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index fcd31b2fe..e085b5b72 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -56,7 +56,6 @@ import org.sufficientlysecure.keychain.provider.ProviderHelper; import org.sufficientlysecure.keychain.ui.adapter.ImportKeysListEntry; import org.sufficientlysecure.keychain.util.HkpKeyServer; import org.sufficientlysecure.keychain.util.InputData; -import org.sufficientlysecure.keychain.util.KeyServer; import org.sufficientlysecure.keychain.util.KeybaseKeyServer; import org.sufficientlysecure.keychain.util.KeychainServiceListener; import org.sufficientlysecure.keychain.util.Log; @@ -763,7 +762,6 @@ public class KeychainIntentService extends IntentService // get first object in block Object obj; if ((obj = objectFactory.nextObject()) != null) { - Log.d(Constants.TAG, "Found class: " + obj.getClass()); if (obj instanceof PGPKeyRing) { downloadedKey = (PGPKeyRing) obj; @@ -820,7 +818,6 @@ public class KeychainIntentService extends IntentService // get first object in block Object obj; if ((obj = objectFactory.nextObject()) != null) { - Log.d(Constants.TAG, "Found class: " + obj.getClass()); if (obj instanceof PGPKeyRing) { downloadedKey = (PGPKeyRing) obj; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java index 4b802c0e1..c5f97f0dd 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java @@ -111,8 +111,8 @@ public class KeybaseKeyServer extends KeyServer { mKeyCache.put(keybaseID, JWalk.getString(match,"them", "public_keys", "primary", "bundle")); String name = JWalk.getString(match, "them", "profile", "full_name"); ArrayList userIds = new ArrayList(); + name = "keybase.io/" + keybaseID + " " + name; userIds.add(name); - userIds.add("keybase.io/" + keybaseID); // TODO: Maybe should be keybaseID@keybase.io ? entry.setUserIds(userIds); entry.setPrimaryUserId(name); return entry; From b5fb311c6152af36254a803aabe726ed8b03a447 Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Tue, 29 Apr 2014 15:03:37 -0700 Subject: [PATCH 03/72] keybase ID stuff --- .../keychain/util/KeybaseKeyServer.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java index c5f97f0dd..ae4c44e09 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java @@ -99,19 +99,24 @@ public class KeybaseKeyServer extends KeyServer { final ImportKeysListEntry entry = new ImportKeysListEntry(); + // TODO: Fix; have suggested keybase provide this value to avoid search-time crypto calls entry.setBitStrength(4096); entry.setAlgorithm("RSA"); entry.setKeyIdHex("0x" + key_fingerprint); + entry.setRevoked(false); + // ctime final long creationDate = JWalk.getLong(match, "them", "public_keys", "primary", "ctime"); final GregorianCalendar tmpGreg = new GregorianCalendar(TimeZone.getTimeZone("UTC")); tmpGreg.setTimeInMillis(creationDate * 1000); entry.setDate(tmpGreg.getTime()); - entry.setRevoked(false); + + // key bits mKeyCache.put(keybaseID, JWalk.getString(match,"them", "public_keys", "primary", "bundle")); - String name = JWalk.getString(match, "them", "profile", "full_name"); + + // String displayName = JWalk.getString(match, "them", "profile", "full_name"); ArrayList userIds = new ArrayList(); - name = "keybase.io/" + keybaseID + " " + name; + String name = "keybase.io/" + keybaseID + " <" + keybaseID + "@keybase.io>"; userIds.add(name); entry.setUserIds(userIds); entry.setPrimaryUserId(name); From 4c693b45097d08d0c4285dccd5d1679a2e4551d6 Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Wed, 30 Apr 2014 13:46:35 -0700 Subject: [PATCH 04/72] Changes should now be in sync with dominik --- .../keychain/util/KeybaseKeyServer.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java index ae4c44e09..dee3899a8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/KeybaseKeyServer.java @@ -112,12 +112,15 @@ public class KeybaseKeyServer extends KeyServer { entry.setDate(tmpGreg.getTime()); // key bits + // we have to fetch the user object to construct the search-result list, so we might as + // well (weakly) remember the key, in case they try to import it mKeyCache.put(keybaseID, JWalk.getString(match,"them", "public_keys", "primary", "bundle")); // String displayName = JWalk.getString(match, "them", "profile", "full_name"); ArrayList userIds = new ArrayList(); String name = "keybase.io/" + keybaseID + " <" + keybaseID + "@keybase.io>"; userIds.add(name); + userIds.add(keybaseID); entry.setUserIds(userIds); entry.setPrimaryUserId(name); return entry; @@ -157,12 +160,10 @@ public class KeybaseKeyServer extends KeyServer { @Override public String get(String id) throws QueryException { - // id is like "keybase/username" - String keybaseID = id.substring(id.indexOf('/') + 1); - String key = mKeyCache.get(keybaseID); + String key = mKeyCache.get(id); if (key == null) { try { - JSONObject user = getUser(keybaseID); + JSONObject user = getUser(id); key = JWalk.getString(user, "them", "public_keys", "primary", "bundle"); } catch (Exception e) { throw new QueryException(e.getMessage()); From 18ed8286080c70daabfedcd258bfa20c1099c268 Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Wed, 30 Apr 2014 13:49:48 -0700 Subject: [PATCH 05/72] tidy up IntentService --- .../keychain/service/KeychainIntentService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java index e085b5b72..eabcfadee 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/service/KeychainIntentService.java @@ -748,8 +748,8 @@ public class KeychainIntentService extends IntentService KeybaseKeyServer server = new KeybaseKeyServer(); for (ImportKeysListEntry entry : entries) { // the keybase handle is in userId(1) - String username = entry.getUserIds().get(1); - byte[] downloadedKeyBytes = server.get(username).getBytes(); + String keybaseID = entry.getUserIds().get(1); + byte[] downloadedKeyBytes = server.get(keybaseID).getBytes(); // create PGPKeyRing object based on downloaded armored key PGPKeyRing downloadedKey = null; From 5b0f19fcebd586235be7c895769ccb111c97e78e Mon Sep 17 00:00:00 2001 From: Tim Bray Date: Wed, 30 Apr 2014 13:56:56 -0700 Subject: [PATCH 06/72] stupid identation stuff --- .../sufficientlysecure/keychain/ui/ImportKeysListFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java index 0580db080..c1aa8a1f2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ImportKeysListFragment.java @@ -211,7 +211,7 @@ public class ImportKeysListFragment extends ListFragment implements @Override public Loader>> - onCreateLoader(int id, Bundle args) { + onCreateLoader(int id, Bundle args) { switch (id) { case LOADER_ID_BYTES: { InputData inputData = getInputData(mKeyBytes, mDataUri); From 9726ac7a964dacc593cdb94a494182f61fd59c5b Mon Sep 17 00:00:00 2001 From: Vadim Lebedev Date: Fri, 2 May 2014 19:05:43 +0200 Subject: [PATCH 07/72] Avoid displaying double 0x before keyid in ViewCertActivity --- .../org/sufficientlysecure/keychain/ui/ViewCertActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java index f740cfc22..645766287 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewCertActivity.java @@ -124,7 +124,7 @@ public class ViewCertActivity extends ActionBarActivity @Override public void onLoadFinished(Loader loader, Cursor data) { if (data.moveToFirst()) { - String signeeKey = "0x" + PgpKeyHelper.convertKeyIdToHex(data.getLong(INDEX_MASTER_KEY_ID)); + String signeeKey = PgpKeyHelper.convertKeyIdToHex(data.getLong(INDEX_MASTER_KEY_ID)); mSigneeKey.setText(signeeKey); String signeeUid = data.getString(INDEX_USER_ID); @@ -134,7 +134,7 @@ public class ViewCertActivity extends ActionBarActivity mCreation.setText(DateFormat.getDateFormat(getApplicationContext()).format(creationDate)); mSignerKeyId = data.getLong(INDEX_KEY_ID_CERTIFIER); - String signerKey = "0x" + PgpKeyHelper.convertKeyIdToHex(mSignerKeyId); + String signerKey = PgpKeyHelper.convertKeyIdToHex(mSignerKeyId); mSignerKey.setText(signerKey); String signerUid = data.getString(INDEX_SIGNER_UID); From 6055b0b0da6ca3f6fdae3b7b1602a38d3a05bb3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Sch=C3=BCrmann?= Date: Mon, 5 May 2014 00:58:22 +0200 Subject: [PATCH 08/72] New key view design, using Android flat buttons and Android icons --- .../keychain/ui/CertifyKeyActivity.java | 4 +- .../keychain/ui/ViewKeyActivity.java | 241 ++++++------- .../keychain/ui/ViewKeyCertsFragment.java | 18 +- .../keychain/ui/ViewKeyKeysFragment.java | 238 +++++++++++++ .../keychain/ui/ViewKeyMainFragment.java | 179 +++------- .../keychain/ui/ViewKeyShareFragment.java | 313 +++++++++++++++++ .../ui/adapter/ImportKeysAdapter.java | 3 +- .../ui/adapter/PagerTabStripAdapter.java | 10 +- .../ui/adapter/ViewKeyUserIdsAdapter.java | 48 ++- .../keychain/util/SlidingTabLayout.java | 318 ++++++++++++++++++ .../keychain/util/SlidingTabStrip.java | 211 ++++++++++++ .../main/res/drawable-hdpi/ic_action_copy.png | Bin 0 -> 381 bytes .../main/res/drawable-hdpi/ic_action_edit.png | Bin 0 -> 884 bytes .../main/res/drawable-hdpi/ic_action_good.png | Bin 0 -> 485 bytes .../main/res/drawable-hdpi/ic_action_help.png | Bin 0 -> 497 bytes .../ic_action_important_small.png | Bin 0 -> 473 bytes .../res/drawable-hdpi/ic_action_settings.png | Bin 0 -> 953 bytes .../main/res/drawable-mdpi/ic_action_copy.png | Bin 0 -> 288 bytes .../main/res/drawable-mdpi/ic_action_edit.png | Bin 0 -> 587 bytes .../main/res/drawable-mdpi/ic_action_good.png | Bin 0 -> 343 bytes .../main/res/drawable-mdpi/ic_action_help.png | Bin 0 -> 404 bytes .../ic_action_important_small.png | Bin 0 -> 361 bytes .../res/drawable-mdpi/ic_action_settings.png | Bin 0 -> 594 bytes .../res/drawable-xhdpi/ic_action_copy.png | Bin 0 -> 353 bytes .../res/drawable-xhdpi/ic_action_edit.png | Bin 0 -> 1179 bytes .../res/drawable-xhdpi/ic_action_good.png | Bin 0 -> 566 bytes .../res/drawable-xhdpi/ic_action_help.png | Bin 0 -> 648 bytes .../ic_action_important_small.png | Bin 0 -> 623 bytes .../res/drawable-xhdpi/ic_action_settings.png | Bin 0 -> 1231 bytes .../res/drawable-xxhdpi/ic_action_copy.png | Bin 0 -> 470 bytes .../res/drawable-xxhdpi/ic_action_edit.png | Bin 0 -> 1670 bytes .../res/drawable-xxhdpi/ic_action_good.png | Bin 0 -> 823 bytes .../res/drawable-xxhdpi/ic_action_help.png | Bin 0 -> 925 bytes .../ic_action_important_small.png | Bin 0 -> 890 bytes .../drawable-xxhdpi/ic_action_settings.png | Bin 0 -> 1863 bytes .../drawable/selector_transparent_button.xml | 4 +- .../main/res/layout/certify_key_activity.xml | 4 +- .../res/layout/import_keys_list_entry.xml | 2 +- .../src/main/res/layout/key_list_item.xml | 2 +- .../src/main/res/layout/view_key_activity.xml | 13 +- .../main/res/layout/view_key_certs_item.xml | 10 +- .../res/layout/view_key_keys_fragment.xml | 149 ++++++++ .../res/layout/view_key_main_fragment.xml | 292 ++++------------ .../res/layout/view_key_share_fragment.xml | 179 ++++++++++ .../main/res/layout/view_key_userids_item.xml | 47 ++- OpenKeychain/src/main/res/menu/key_view.xml | 51 +-- .../src/main/res/values-v14/styles.xml | 4 + OpenKeychain/src/main/res/values/colors.xml | 12 +- OpenKeychain/src/main/res/values/strings.xml | 36 +- OpenKeychain/src/main/res/values/styles.xml | 4 +- 50 files changed, 1793 insertions(+), 599 deletions(-) create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeysFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyShareFragment.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SlidingTabLayout.java create mode 100644 OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SlidingTabStrip.java create mode 100644 OpenKeychain/src/main/res/drawable-hdpi/ic_action_copy.png create mode 100644 OpenKeychain/src/main/res/drawable-hdpi/ic_action_edit.png create mode 100644 OpenKeychain/src/main/res/drawable-hdpi/ic_action_good.png create mode 100644 OpenKeychain/src/main/res/drawable-hdpi/ic_action_help.png create mode 100644 OpenKeychain/src/main/res/drawable-hdpi/ic_action_important_small.png create mode 100644 OpenKeychain/src/main/res/drawable-hdpi/ic_action_settings.png create mode 100644 OpenKeychain/src/main/res/drawable-mdpi/ic_action_copy.png create mode 100644 OpenKeychain/src/main/res/drawable-mdpi/ic_action_edit.png create mode 100644 OpenKeychain/src/main/res/drawable-mdpi/ic_action_good.png create mode 100644 OpenKeychain/src/main/res/drawable-mdpi/ic_action_help.png create mode 100644 OpenKeychain/src/main/res/drawable-mdpi/ic_action_important_small.png create mode 100644 OpenKeychain/src/main/res/drawable-mdpi/ic_action_settings.png create mode 100644 OpenKeychain/src/main/res/drawable-xhdpi/ic_action_copy.png create mode 100644 OpenKeychain/src/main/res/drawable-xhdpi/ic_action_edit.png create mode 100644 OpenKeychain/src/main/res/drawable-xhdpi/ic_action_good.png create mode 100644 OpenKeychain/src/main/res/drawable-xhdpi/ic_action_help.png create mode 100644 OpenKeychain/src/main/res/drawable-xhdpi/ic_action_important_small.png create mode 100644 OpenKeychain/src/main/res/drawable-xhdpi/ic_action_settings.png create mode 100644 OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_copy.png create mode 100644 OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_edit.png create mode 100644 OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_good.png create mode 100644 OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_help.png create mode 100644 OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_important_small.png create mode 100644 OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_settings.png create mode 100644 OpenKeychain/src/main/res/layout/view_key_keys_fragment.xml create mode 100644 OpenKeychain/src/main/res/layout/view_key_share_fragment.xml diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java index fbcbbb0c3..2d31e0de8 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/CertifyKeyActivity.java @@ -147,7 +147,7 @@ public class CertifyKeyActivity extends ActionBarActivity implements } Log.e(Constants.TAG, "uri: " + mDataUri); - mUserIds = (ListView) findViewById(R.id.user_ids); + mUserIds = (ListView) findViewById(R.id.view_key_user_ids); mUserIdsAdapter = new ViewKeyUserIdsAdapter(this, null, 0, true); mUserIds.setAdapter(mUserIdsAdapter); @@ -203,7 +203,7 @@ public class CertifyKeyActivity extends ActionBarActivity implements byte[] fingerprintBlob = data.getBlob(INDEX_FINGERPRINT); String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob); - ((TextView) findViewById(R.id.fingerprint)) + ((TextView) findViewById(R.id.view_key_fingerprint)) .setText(PgpKeyHelper.colorizeFingerprint(fingerprint)); } break; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java index e595c1889..56aaba57b 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyActivity.java @@ -21,6 +21,7 @@ package org.sufficientlysecure.keychain.ui; import android.annotation.TargetApi; import android.app.Activity; import android.content.Intent; +import android.database.Cursor; import android.net.Uri; import android.nfc.NdefMessage; import android.nfc.NdefRecord; @@ -31,6 +32,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.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; @@ -42,20 +46,19 @@ import com.devspark.appmsg.AppMsg; import org.sufficientlysecure.keychain.Constants; 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; import org.sufficientlysecure.keychain.provider.ProviderHelper; -import org.sufficientlysecure.keychain.ui.adapter.TabsAdapter; -import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment; -import org.sufficientlysecure.keychain.ui.dialog.ShareQrCodeDialogFragment; +import org.sufficientlysecure.keychain.ui.adapter.PagerTabStripAdapter; import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.SlidingTabLayout; import java.io.IOException; import java.util.HashMap; -public class ViewKeyActivity extends ActionBarActivity { +public class ViewKeyActivity extends ActionBarActivity implements + LoaderManager.LoaderCallbacks { ExportHelper mExportHelper; ProviderHelper mProviderHelper; @@ -63,9 +66,15 @@ public class ViewKeyActivity extends ActionBarActivity { protected Uri mDataUri; public static final String EXTRA_SELECTED_TAB = "selectedTab"; + public static final int TAB_MAIN = 0; + public static final int TAB_SHARE = 1; + public static final int TAB_KEYS = 2; + public static final int TAB_CERTS = 3; - ViewPager mViewPager; - TabsAdapter mTabsAdapter; + // view + private ViewPager mViewPager; + private SlidingTabLayout mSlidingTabLayout; + private PagerTabStripAdapter mTabsAdapter; public static final int REQUEST_CODE_LOOKUP_KEY = 0x00007006; @@ -76,6 +85,9 @@ public class ViewKeyActivity extends ActionBarActivity { private byte[] mNfcKeyringBytes; private static final int NFC_SENT = 1; + private static final int LOADER_ID_UNIFIED = 0; + + @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); @@ -89,33 +101,67 @@ public class ViewKeyActivity extends ActionBarActivity { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setIcon(android.R.color.transparent); actionBar.setHomeButtonEnabled(true); - actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); setContentView(R.layout.view_key_activity); - mViewPager = (ViewPager) findViewById(R.id.pager); + mViewPager = (ViewPager) findViewById(R.id.view_key_pager); + mSlidingTabLayout = (SlidingTabLayout) findViewById(R.id.view_key_sliding_tab_layout); - mTabsAdapter = new TabsAdapter(this, mViewPager); + mTabsAdapter = new PagerTabStripAdapter(this); + mViewPager.setAdapter(mTabsAdapter); - int selectedTab = 0; + int switchToTab = TAB_MAIN; Intent intent = getIntent(); if (intent.getExtras() != null && intent.getExtras().containsKey(EXTRA_SELECTED_TAB)) { - selectedTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB); + switchToTab = intent.getExtras().getInt(EXTRA_SELECTED_TAB); } - mDataUri = getIntent().getData(); + Uri dataUri = getIntent().getData(); + if (dataUri == null) { + Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); + finish(); + return; + } - initNfc(mDataUri); + loadData(dataUri); + + initNfc(dataUri); Bundle mainBundle = new Bundle(); - mainBundle.putParcelable(ViewKeyMainFragment.ARG_DATA_URI, mDataUri); - mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.key_view_tab_main)), - ViewKeyMainFragment.class, mainBundle, (selectedTab == 0)); + mainBundle.putParcelable(ViewKeyMainFragment.ARG_DATA_URI, dataUri); + mTabsAdapter.addTab(ViewKeyMainFragment.class, + mainBundle, getString(R.string.key_view_tab_main)); + + Bundle shareBundle = new Bundle(); + shareBundle.putParcelable(ViewKeyMainFragment.ARG_DATA_URI, dataUri); + mTabsAdapter.addTab(ViewKeyShareFragment.class, + mainBundle, getString(R.string.key_view_tab_share)); + + Bundle keyDetailsBundle = new Bundle(); + keyDetailsBundle.putParcelable(ViewKeyKeysFragment.ARG_DATA_URI, dataUri); + mTabsAdapter.addTab(ViewKeyKeysFragment.class, + keyDetailsBundle, getString(R.string.key_view_tab_keys_details)); Bundle certBundle = new Bundle(); - certBundle.putParcelable(ViewKeyCertsFragment.ARG_DATA_URI, mDataUri); - mTabsAdapter.addTab(actionBar.newTab().setText(getString(R.string.key_view_tab_certs)), - ViewKeyCertsFragment.class, certBundle, (selectedTab == 1)); + certBundle.putParcelable(ViewKeyCertsFragment.ARG_DATA_URI, dataUri); + mTabsAdapter.addTab(ViewKeyCertsFragment.class, + certBundle, getString(R.string.key_view_tab_certs)); + + // NOTE: must be after adding the tabs! + mSlidingTabLayout.setViewPager(mViewPager); + + // switch to tab selected by extra + mViewPager.setCurrentItem(switchToTab); + } + + private void loadData(Uri dataUri) { + mDataUri = dataUri; + + Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); + + // Prepare the loaders. Either re-connect with an existing ones, + // or start new ones. + getSupportLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); } @Override @@ -143,24 +189,6 @@ public class ViewKeyActivity extends ActionBarActivity { case R.id.menu_key_view_export_file: exportToFile(mDataUri, mExportHelper, mProviderHelper); return true; - case R.id.menu_key_view_share_default_fingerprint: - shareKey(mDataUri, true, mProviderHelper); - return true; - case R.id.menu_key_view_share_default: - shareKey(mDataUri, false, mProviderHelper); - return true; - case R.id.menu_key_view_share_qr_code_fingerprint: - shareKeyQrCode(mDataUri, true); - return true; - case R.id.menu_key_view_share_qr_code: - shareKeyQrCode(mDataUri, false); - return true; - case R.id.menu_key_view_share_nfc: - shareNfc(); - return true; - case R.id.menu_key_view_share_clipboard: - copyToClipboard(mDataUri, mProviderHelper); - return true; case R.id.menu_key_view_delete: { deleteKey(mDataUri, mExportHelper); return true; @@ -209,84 +237,6 @@ public class ViewKeyActivity extends ActionBarActivity { startActivityForResult(queryIntent, REQUEST_CODE_LOOKUP_KEY); } - private void shareKey(Uri dataUri, boolean fingerprintOnly, ProviderHelper providerHelper) - throws ProviderHelper.NotFoundException { - String content = null; - if (fingerprintOnly) { - byte[] data = (byte[]) providerHelper.getGenericData( - KeychainContract.KeyRings.buildUnifiedKeyRingUri(dataUri), - KeychainContract.Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); - if (data != null) { - String fingerprint = PgpKeyHelper.convertFingerprintToHex(data); - content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; - } else { - AppMsg.makeText(this, "Bad key selected!", - AppMsg.STYLE_ALERT).show(); - return; - } - } else { - // get public keyring as ascii armored string - try { - Uri uri = KeychainContract.KeyRingData.buildPublicKeyRingUri(dataUri); - content = providerHelper.getKeyRingAsArmoredString(uri); - - // Android will fail with android.os.TransactionTooLargeException if key is too big - // see http://www.lonestarprod.com/?p=34 - if (content.length() >= 86389) { - AppMsg.makeText(this, R.string.key_too_big_for_sharing, - AppMsg.STYLE_ALERT).show(); - return; - } - } catch (IOException e) { - Log.e(Constants.TAG, "error processing key!", e); - AppMsg.makeText(this, R.string.error_invalid_data, AppMsg.STYLE_ALERT).show(); - } catch (ProviderHelper.NotFoundException e) { - Log.e(Constants.TAG, "key not found!", e); - AppMsg.makeText(this, R.string.error_key_not_found, AppMsg.STYLE_ALERT).show(); - } - } - - if (content != null) { - // let user choose application - Intent sendIntent = new Intent(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_TEXT, content); - sendIntent.setType("text/plain"); - startActivity(Intent.createChooser(sendIntent, - getResources().getText(R.string.action_share_key_with))); - } else { - Log.e(Constants.TAG, "content is null!"); - } - } - - private void shareKeyQrCode(Uri dataUri, boolean fingerprintOnly) { - ShareQrCodeDialogFragment dialog = ShareQrCodeDialogFragment.newInstance(dataUri, - fingerprintOnly); - dialog.show(getSupportFragmentManager(), "shareQrCodeDialog"); - } - - private void copyToClipboard(Uri dataUri, ProviderHelper providerHelper) { - // get public keyring as ascii armored string - try { - Uri uri = KeychainContract.KeyRingData.buildPublicKeyRingUri(dataUri); - String keyringArmored = providerHelper.getKeyRingAsArmoredString(uri); - - ClipboardReflection.copyToClipboard(this, keyringArmored); - AppMsg.makeText(this, R.string.key_copied_to_clipboard, AppMsg.STYLE_INFO) - .show(); - } catch (IOException e) { - Log.e(Constants.TAG, "error processing key!", e); - AppMsg.makeText(this, R.string.error_key_processing, AppMsg.STYLE_ALERT).show(); - } catch (ProviderHelper.NotFoundException e) { - Log.e(Constants.TAG, "key not found!", e); - AppMsg.makeText(this, R.string.error_key_not_found, AppMsg.STYLE_ALERT).show(); - } - } - - private void shareNfc() { - ShareNfcDialogFragment dialog = ShareNfcDialogFragment.newInstance(); - dialog.show(getSupportFragmentManager(), "shareNfcDialog"); - } - private void deleteKey(Uri dataUri, ExportHelper exportHelper) { // Message is received after key is deleted Handler returnHandler = new Handler() { @@ -409,4 +359,63 @@ public class ViewKeyActivity extends ActionBarActivity { } }; + static final String[] UNIFIED_PROJECTION = new String[]{ + KeychainContract.KeyRings._ID, + KeychainContract.KeyRings.MASTER_KEY_ID, + KeychainContract.KeyRings.USER_ID, + + }; + static final int INDEX_UNIFIED_MASTER_KEY_ID = 1; + static final int INDEX_UNIFIED_USER_ID = 2; + + @Override + public Loader onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_ID_UNIFIED: { + Uri baseUri = KeychainContract.KeyRings.buildUnifiedKeyRingUri(mDataUri); + return new CursorLoader(this, baseUri, UNIFIED_PROJECTION, null, null, null); + } + + default: + return null; + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + /* TODO better error handling? May cause problems when a key is deleted, + * because the notification triggers faster than the activity closes. + */ + // Avoid NullPointerExceptions... + if (data.getCount() == 0) { + return; + } + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + switch (loader.getId()) { + case LOADER_ID_UNIFIED: { + if (data.moveToFirst()) { + // get name, email, and comment from USER_ID + String[] mainUserId = PgpKeyHelper.splitUserId(data.getString(INDEX_UNIFIED_USER_ID)); + if (mainUserId[0] != null) { + setTitle(mainUserId[0]); + } else { + setTitle(R.string.user_id_no_name); + } + + // get key id from MASTER_KEY_ID + long masterKeyId = data.getLong(INDEX_UNIFIED_MASTER_KEY_ID); + String keyIdStr = PgpKeyHelper.convertKeyIdToHex(masterKeyId); + getSupportActionBar().setSubtitle(keyIdStr); + + break; + } + } + } + } + + @Override + public void onLoaderReset(Loader loader) { + + } } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java index 3c4135715..e1c2013ea 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyCertsFragment.java @@ -75,6 +75,9 @@ public class ViewKeyCertsFragment extends Fragment private Uri mDataUri; + // starting with 4 for this fragment + private static final int LOADER_ID = 4; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.view_key_certs_fragment, container, false); @@ -112,7 +115,7 @@ public class ViewKeyCertsFragment extends Fragment mAdapter = new CertListAdapter(getActivity(), null); mStickyList.setAdapter(mAdapter); - getLoaderManager().initLoader(0, null, this); + getLoaderManager().initLoader(LOADER_ID, null, this); } @Override @@ -208,11 +211,18 @@ public class ViewKeyCertsFragment extends Fragment // 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 wSignerName = (TextView) view.findViewById(R.id.signerName); TextView wSignStatus = (TextView) view.findViewById(R.id.signStatus); String signerKeyId = PgpKeyHelper.convertKeyIdToHex(cursor.getLong(mIndexSignerKeyId)); - String signerUserId = cursor.getString(mIndexSignerUserId); + String[] userId = PgpKeyHelper.splitUserId(cursor.getString(mIndexSignerUserId)); + if (userId[0] != null) { + wSignerName.setText(userId[0]); + } else { + wSignerName.setText(R.string.user_id_no_name); + } + wSignerKeyId.setText(signerKeyId); + switch (cursor.getInt(mIndexType)) { case PGPSignature.DEFAULT_CERTIFICATION: // 0x10 wSignStatus.setText(R.string.cert_default); @@ -231,8 +241,6 @@ public class ViewKeyCertsFragment extends Fragment break; } - wSignerUserId.setText(signerUserId); - wSignerKeyId.setText(signerKeyId); view.setTag(R.id.tag_mki, cursor.getLong(mIndexMasterKeyId)); view.setTag(R.id.tag_rank, cursor.getLong(mIndexRank)); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeysFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeysFragment.java new file mode 100644 index 000000000..bb0e4b23a --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyKeysFragment.java @@ -0,0 +1,238 @@ +/* + * 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; + +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.text.format.DateFormat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.TextView; + +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.KeychainContract.Keys; +import org.sufficientlysecure.keychain.ui.adapter.ViewKeyKeysAdapter; +import org.sufficientlysecure.keychain.util.Log; + +import java.util.Date; + + +public class ViewKeyKeysFragment extends Fragment implements + LoaderManager.LoaderCallbacks { + + public static final String ARG_DATA_URI = "uri"; + + private LinearLayout mContainer; + private TextView mAlgorithm; + private TextView mKeyId; + private TextView mExpiry; + private TextView mCreation; + private TextView mFingerprint; + private TextView mSecretKey; + + private ListView mKeys; + + private static final int LOADER_ID_UNIFIED = 0; + private static final int LOADER_ID_KEYS = 1; + + private ViewKeyKeysAdapter mKeysAdapter; + + private Uri mDataUri; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.view_key_keys_fragment, container, false); + + mContainer = (LinearLayout) view.findViewById(R.id.container); + mKeyId = (TextView) view.findViewById(R.id.key_id); + mAlgorithm = (TextView) view.findViewById(R.id.algorithm); + mCreation = (TextView) view.findViewById(R.id.creation); + mExpiry = (TextView) view.findViewById(R.id.expiry); + mFingerprint = (TextView) view.findViewById(R.id.view_key_fingerprint); + mSecretKey = (TextView) view.findViewById(R.id.secret_key); + mKeys = (ListView) view.findViewById(R.id.keys); + + return view; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); + if (dataUri == null) { + Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); + getActivity().finish(); + return; + } + + loadData(dataUri); + } + + private void loadData(Uri dataUri) { + getActivity().setProgressBarIndeterminateVisibility(true); + mContainer.setVisibility(View.GONE); + + mDataUri = dataUri; + + Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); + + mKeysAdapter = new ViewKeyKeysAdapter(getActivity(), null, 0); + mKeys.setAdapter(mKeysAdapter); + + // Prepare the loaders. Either re-connect with an existing ones, + // or start new ones. + getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); + getLoaderManager().initLoader(LOADER_ID_KEYS, null, this); + } + + static final String[] UNIFIED_PROJECTION = new String[] { + KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.HAS_ANY_SECRET, + KeyRings.USER_ID, KeyRings.FINGERPRINT, + KeyRings.ALGORITHM, KeyRings.KEY_SIZE, KeyRings.CREATION, KeyRings.EXPIRY, + + }; + static final int INDEX_UNIFIED_MKI = 1; + static final int INDEX_UNIFIED_HAS_ANY_SECRET = 2; + static final int INDEX_UNIFIED_UID = 3; + static final int INDEX_UNIFIED_FINGERPRINT = 4; + static final int INDEX_UNIFIED_ALGORITHM = 5; + static final int INDEX_UNIFIED_KEY_SIZE = 6; + static final int INDEX_UNIFIED_CREATION = 7; + static final int INDEX_UNIFIED_EXPIRY = 8; + + static final String[] KEYS_PROJECTION = new String[] { + Keys._ID, + Keys.KEY_ID, Keys.RANK, Keys.ALGORITHM, Keys.KEY_SIZE, Keys.HAS_SECRET, + Keys.CAN_CERTIFY, Keys.CAN_ENCRYPT, Keys.CAN_SIGN, Keys.IS_REVOKED, + Keys.CREATION, Keys.EXPIRY, Keys.FINGERPRINT + }; + static final int KEYS_INDEX_CAN_ENCRYPT = 7; + + public Loader onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_ID_UNIFIED: { + Uri baseUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); + } + case LOADER_ID_KEYS: { + Uri baseUri = Keys.buildKeysUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, KEYS_PROJECTION, null, null, null); + } + + default: + return null; + } + } + + public void onLoadFinished(Loader loader, Cursor data) { + /* TODO better error handling? May cause problems when a key is deleted, + * because the notification triggers faster than the activity closes. + */ + // Avoid NullPointerExceptions... + if(data.getCount() == 0) { + return; + } + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + switch (loader.getId()) { + case LOADER_ID_UNIFIED: { + if (data.moveToFirst()) { + if (data.getInt(INDEX_UNIFIED_HAS_ANY_SECRET) != 0) { + mSecretKey.setTextColor(getResources().getColor(R.color.emphasis)); + mSecretKey.setText(R.string.secret_key_yes); + } else { + mSecretKey.setTextColor(Color.BLACK); + mSecretKey.setText(getResources().getString(R.string.secret_key_no)); + } + + // get key id from MASTER_KEY_ID + long masterKeyId = data.getLong(INDEX_UNIFIED_MKI); + String keyIdStr = PgpKeyHelper.convertKeyIdToHex(masterKeyId); + mKeyId.setText(keyIdStr); + + // get creation date from CREATION + if (data.isNull(INDEX_UNIFIED_CREATION)) { + mCreation.setText(R.string.none); + } else { + Date creationDate = new Date(data.getLong(INDEX_UNIFIED_CREATION) * 1000); + + mCreation.setText( + DateFormat.getDateFormat(getActivity().getApplicationContext()).format( + creationDate)); + } + + // get expiry date from EXPIRY + if (data.isNull(INDEX_UNIFIED_EXPIRY)) { + mExpiry.setText(R.string.none); + } else { + Date expiryDate = new Date(data.getLong(INDEX_UNIFIED_EXPIRY) * 1000); + + mExpiry.setText( + DateFormat.getDateFormat(getActivity().getApplicationContext()).format( + expiryDate)); + } + + String algorithmStr = PgpKeyHelper.getAlgorithmInfo( + getActivity(), + data.getInt(INDEX_UNIFIED_ALGORITHM), + data.getInt(INDEX_UNIFIED_KEY_SIZE) + ); + mAlgorithm.setText(algorithmStr); + + byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); + String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob); + mFingerprint.setText(PgpKeyHelper.colorizeFingerprint(fingerprint)); + + break; + } + } + + case LOADER_ID_KEYS: + mKeysAdapter.swapCursor(data); + break; + } + getActivity().setProgressBarIndeterminateVisibility(false); + mContainer.setVisibility(View.VISIBLE); + } + + /** + * 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_KEYS: + mKeysAdapter.swapCursor(null); + break; + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java index ef4da3010..43e484ffe 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyMainFragment.java @@ -19,66 +19,46 @@ package org.sufficientlysecure.keychain.ui; 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.text.format.DateFormat; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.ListView; -import android.widget.TextView; - -import com.beardedhen.androidbootstrap.BootstrapButton; import org.sufficientlysecure.keychain.Constants; -import org.sufficientlysecure.keychain.R; -import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; -import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRingData; +import org.sufficientlysecure.keychain.R;import org.sufficientlysecure.keychain.provider.KeychainContract; 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.util.Log; -import java.util.Date; - - public class ViewKeyMainFragment extends Fragment implements LoaderManager.LoaderCallbacks { public static final String ARG_DATA_URI = "uri"; private LinearLayout mContainer; - private TextView mName; - private TextView mEmail; - private TextView mComment; - private TextView mAlgorithm; - private TextView mKeyId; - private TextView mExpiry; - private TextView mCreation; - private TextView mFingerprint; - private TextView mSecretKey; - private BootstrapButton mActionEdit; - private BootstrapButton mActionEncrypt; - private BootstrapButton mActionCertify; + private View mActionEdit; + private View mActionEditDivider; + private View mActionEncrypt; + private View mActionCertify; + private View mActionCertifyDivider; private ListView mUserIds; - private ListView mKeys; private static final int LOADER_ID_UNIFIED = 0; private static final int LOADER_ID_USER_IDS = 1; private static final int LOADER_ID_KEYS = 2; private ViewKeyUserIdsAdapter mUserIdsAdapter; - private ViewKeyKeysAdapter mKeysAdapter; private Uri mDataUri; @@ -87,20 +67,12 @@ public class ViewKeyMainFragment extends Fragment implements View view = inflater.inflate(R.layout.view_key_main_fragment, container, false); mContainer = (LinearLayout) view.findViewById(R.id.container); - mName = (TextView) view.findViewById(R.id.name); - mEmail = (TextView) view.findViewById(R.id.email); - mComment = (TextView) view.findViewById(R.id.comment); - mKeyId = (TextView) view.findViewById(R.id.key_id); - mAlgorithm = (TextView) view.findViewById(R.id.algorithm); - mCreation = (TextView) view.findViewById(R.id.creation); - mExpiry = (TextView) view.findViewById(R.id.expiry); - mFingerprint = (TextView) view.findViewById(R.id.fingerprint); - mSecretKey = (TextView) view.findViewById(R.id.secret_key); - mUserIds = (ListView) view.findViewById(R.id.user_ids); - mKeys = (ListView) view.findViewById(R.id.keys); - mActionEdit = (BootstrapButton) view.findViewById(R.id.action_edit); - mActionEncrypt = (BootstrapButton) view.findViewById(R.id.action_encrypt); - mActionCertify = (BootstrapButton) view.findViewById(R.id.action_certify); + mUserIds = (ListView) view.findViewById(R.id.view_key_user_ids); + mActionEdit = view.findViewById(R.id.view_key_action_edit); + mActionEditDivider = view.findViewById(R.id.view_key_action_edit_divider); + mActionEncrypt = view.findViewById(R.id.view_key_action_encrypt); + mActionCertify = view.findViewById(R.id.view_key_action_certify); + mActionCertifyDivider = view.findViewById(R.id.view_key_action_certify_divider); return view; } @@ -120,11 +92,6 @@ public class ViewKeyMainFragment extends Fragment implements } private void loadData(Uri dataUri) { - if (dataUri.equals(mDataUri)) { - Log.d(Constants.TAG, "Same URI, no need to load the data again!"); - return; - } - getActivity().setProgressBarIndeterminateVisibility(true); mContainer.setVisibility(View.GONE); @@ -135,44 +102,46 @@ public class ViewKeyMainFragment extends Fragment implements mActionEncrypt.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - encryptToContact(mDataUri); + encrypt(mDataUri); } }); mActionCertify.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { - certifyKey(mDataUri); + certify(mDataUri); + } + }); + mActionEdit.setOnClickListener(new View.OnClickListener() { + public void onClick(View view) { + editKey(mDataUri); } }); mUserIdsAdapter = new ViewKeyUserIdsAdapter(getActivity(), null, 0); mUserIds.setAdapter(mUserIdsAdapter); - mKeysAdapter = new ViewKeyKeysAdapter(getActivity(), null, 0); - mKeys.setAdapter(mKeysAdapter); - // Prepare the loaders. Either re-connect with an existing ones, // or start new ones. - getActivity().getSupportLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); - getActivity().getSupportLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); - getActivity().getSupportLoaderManager().initLoader(LOADER_ID_KEYS, null, this); + getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); + getLoaderManager().initLoader(LOADER_ID_USER_IDS, null, this); + getLoaderManager().initLoader(LOADER_ID_KEYS, null, this); } - static final String[] UNIFIED_PROJECTION = new String[] { - KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.HAS_ANY_SECRET, + static final String[] UNIFIED_PROJECTION = new String[]{ + KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.HAS_ANY_SECRET, KeyRings.USER_ID, KeyRings.FINGERPRINT, KeyRings.ALGORITHM, KeyRings.KEY_SIZE, KeyRings.CREATION, KeyRings.EXPIRY, }; - static final int INDEX_UNIFIED_MKI = 1; + static final int INDEX_UNIFIED_MASTER_KEY_ID = 1; static final int INDEX_UNIFIED_HAS_ANY_SECRET = 2; - static final int INDEX_UNIFIED_UID = 3; + static final int INDEX_UNIFIED_USER_ID = 3; static final int INDEX_UNIFIED_FINGERPRINT = 4; static final int INDEX_UNIFIED_ALGORITHM = 5; static final int INDEX_UNIFIED_KEY_SIZE = 6; static final int INDEX_UNIFIED_CREATION = 7; static final int INDEX_UNIFIED_EXPIRY = 8; - static final String[] KEYS_PROJECTION = new String[] { + static final String[] KEYS_PROJECTION = new String[]{ Keys._ID, Keys.KEY_ID, Keys.RANK, Keys.ALGORITHM, Keys.KEY_SIZE, Keys.HAS_SECRET, Keys.CAN_CERTIFY, Keys.CAN_ENCRYPT, Keys.CAN_SIGN, Keys.IS_REVOKED, @@ -205,7 +174,7 @@ public class ViewKeyMainFragment extends Fragment implements * because the notification triggers faster than the activity closes. */ // Avoid NullPointerExceptions... - if(data.getCount() == 0) { + if (data.getCount() == 0) { return; } // Swap the new cursor in. (The framework will take care of closing the @@ -213,81 +182,24 @@ public class ViewKeyMainFragment extends Fragment implements switch (loader.getId()) { case LOADER_ID_UNIFIED: { if (data.moveToFirst()) { - // get name, email, and comment from USER_ID - String[] mainUserId = PgpKeyHelper.splitUserId(data.getString(INDEX_UNIFIED_UID)); - if (mainUserId[0] != null) { - getActivity().setTitle(mainUserId[0]); - mName.setText(mainUserId[0]); - } else { - getActivity().setTitle(R.string.user_id_no_name); - mName.setText(R.string.user_id_no_name); - } - mEmail.setText(mainUserId[1]); - mComment.setText(mainUserId[2]); - if (data.getInt(INDEX_UNIFIED_HAS_ANY_SECRET) != 0) { - mSecretKey.setTextColor(getResources().getColor(R.color.emphasis)); - mSecretKey.setText(R.string.secret_key_yes); + // certify button + mActionCertify.setVisibility(View.GONE); + mActionCertifyDivider.setVisibility(View.GONE); // edit button mActionEdit.setVisibility(View.VISIBLE); - mActionEdit.setOnClickListener(new View.OnClickListener() { - public void onClick(View view) { - Intent editIntent = new Intent(getActivity(), EditKeyActivity.class); - editIntent.setData( - KeyRingData.buildSecretKeyRingUri(mDataUri)); - editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY); - startActivityForResult(editIntent, 0); - } - }); + mActionEditDivider.setVisibility(View.VISIBLE); } else { - mSecretKey.setTextColor(Color.BLACK); - mSecretKey.setText(getResources().getString(R.string.secret_key_no)); - // certify button mActionCertify.setVisibility(View.VISIBLE); + mActionCertifyDivider.setVisibility(View.VISIBLE); + // edit button mActionEdit.setVisibility(View.GONE); + mActionEditDivider.setVisibility(View.GONE); } - // get key id from MASTER_KEY_ID - long masterKeyId = data.getLong(INDEX_UNIFIED_MKI); - String keyIdStr = PgpKeyHelper.convertKeyIdToHex(masterKeyId); - mKeyId.setText(keyIdStr); - - // get creation date from CREATION - if (data.isNull(INDEX_UNIFIED_CREATION)) { - mCreation.setText(R.string.none); - } else { - Date creationDate = new Date(data.getLong(INDEX_UNIFIED_CREATION) * 1000); - - mCreation.setText( - DateFormat.getDateFormat(getActivity().getApplicationContext()).format( - creationDate)); - } - - // get expiry date from EXPIRY - if (data.isNull(INDEX_UNIFIED_EXPIRY)) { - mExpiry.setText(R.string.none); - } else { - Date expiryDate = new Date(data.getLong(INDEX_UNIFIED_EXPIRY) * 1000); - - mExpiry.setText( - DateFormat.getDateFormat(getActivity().getApplicationContext()).format( - expiryDate)); - } - - String algorithmStr = PgpKeyHelper.getAlgorithmInfo( - getActivity(), - data.getInt(INDEX_UNIFIED_ALGORITHM), - data.getInt(INDEX_UNIFIED_KEY_SIZE) - ); - mAlgorithm.setText(algorithmStr); - - byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); - String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob); - mFingerprint.setText(PgpKeyHelper.colorizeFingerprint(fingerprint)); - break; } } @@ -307,11 +219,12 @@ public class ViewKeyMainFragment extends Fragment implements break; } } while (data.moveToNext()); - if (!canEncrypt) { + if (canEncrypt) { + mActionEncrypt.setVisibility(View.VISIBLE); + } else { mActionEncrypt.setVisibility(View.GONE); } - mKeysAdapter.swapCursor(data); break; } getActivity().setProgressBarIndeterminateVisibility(false); @@ -327,16 +240,13 @@ public class ViewKeyMainFragment extends Fragment implements case LOADER_ID_USER_IDS: mUserIdsAdapter.swapCursor(null); break; - case LOADER_ID_KEYS: - mKeysAdapter.swapCursor(null); - break; } } - private void encryptToContact(Uri dataUri) { + private void encrypt(Uri dataUri) { try { long keyId = new ProviderHelper(getActivity()).extractOrGetMasterKeyId(dataUri); - long[] encryptionKeyIds = new long[]{ keyId }; + long[] encryptionKeyIds = new long[]{keyId}; Intent intent = new Intent(getActivity(), EncryptActivity.class); intent.setAction(EncryptActivity.ACTION_ENCRYPT); intent.putExtra(EncryptActivity.EXTRA_ENCRYPTION_KEY_IDS, encryptionKeyIds); @@ -347,10 +257,17 @@ public class ViewKeyMainFragment extends Fragment implements } } - private void certifyKey(Uri dataUri) { + private void certify(Uri dataUri) { Intent signIntent = new Intent(getActivity(), CertifyKeyActivity.class); signIntent.setData(dataUri); startActivity(signIntent); } + private void editKey(Uri dataUri) { + Intent editIntent = new Intent(getActivity(), EditKeyActivity.class); + editIntent.setData(KeychainContract.KeyRingData.buildSecretKeyRingUri(dataUri)); + editIntent.setAction(EditKeyActivity.ACTION_EDIT_KEY); + startActivityForResult(editIntent, 0); + } + } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyShareFragment.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyShareFragment.java new file mode 100644 index 000000000..aacf30429 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/ViewKeyShareFragment.java @@ -0,0 +1,313 @@ +/* + * 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; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +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.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.devspark.appmsg.AppMsg; + +import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.R; +import org.sufficientlysecure.keychain.compatibility.ClipboardReflection; +import org.sufficientlysecure.keychain.pgp.PgpKeyHelper; +import org.sufficientlysecure.keychain.provider.KeychainContract; +import org.sufficientlysecure.keychain.provider.KeychainContract.KeyRings; +import org.sufficientlysecure.keychain.provider.KeychainContract.Keys; +import org.sufficientlysecure.keychain.provider.ProviderHelper; +import org.sufficientlysecure.keychain.ui.dialog.ShareNfcDialogFragment; +import org.sufficientlysecure.keychain.ui.dialog.ShareQrCodeDialogFragment; +import org.sufficientlysecure.keychain.util.Log; +import org.sufficientlysecure.keychain.util.QrCodeUtils; + +import java.io.IOException; + + +public class ViewKeyShareFragment extends Fragment implements + LoaderManager.LoaderCallbacks { + + public static final String ARG_DATA_URI = "uri"; + + private LinearLayout mContainer; + private TextView mFingerprint; + private ImageView mFingerprintQrCode; + private View mFingerprintShareButton; + private View mFingerprintClipboardButton; + private View mKeyShareButton; + private View mKeyClipboardButton; + private View mNfcHelpButton; + private View mNfcPrefsButton; + + ProviderHelper mProviderHelper; + + private static final int QR_CODE_SIZE = 1000; + + private static final int LOADER_ID_UNIFIED = 0; + + private Uri mDataUri; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.view_key_share_fragment, container, false); + + mProviderHelper = new ProviderHelper(ViewKeyShareFragment.this.getActivity()); + + mContainer = (LinearLayout) view.findViewById(R.id.container); + mFingerprint = (TextView) view.findViewById(R.id.view_key_fingerprint); + mFingerprintQrCode = (ImageView) view.findViewById(R.id.view_key_fingerprint_qr_code_image); + mFingerprintShareButton = view.findViewById(R.id.view_key_action_fingerprint_share); + mFingerprintClipboardButton = view.findViewById(R.id.view_key_action_fingerprint_clipboard); + mKeyShareButton = view.findViewById(R.id.view_key_action_key_share); + mKeyClipboardButton = view.findViewById(R.id.view_key_action_key_clipboard); + mNfcHelpButton = view.findViewById(R.id.view_key_action_nfc_help); + mNfcPrefsButton = view.findViewById(R.id.view_key_action_nfc_prefs); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + mNfcPrefsButton.setVisibility(View.VISIBLE); + } else { + mNfcPrefsButton.setVisibility(View.GONE); + } + + mFingerprintQrCode.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showQrCodeDialog(); + } + }); + + mFingerprintShareButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + share(mDataUri, mProviderHelper, true, false); + } + }); + mFingerprintClipboardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + share(mDataUri, mProviderHelper, true, true); + } + }); + mKeyShareButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + share(mDataUri, mProviderHelper, false, false); + } + }); + mKeyClipboardButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + share(mDataUri, mProviderHelper, false, true); + } + }); + mNfcHelpButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showNfcHelpDialog(); + } + }); + mNfcPrefsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + showNfcPrefs(); + } + }); + + return view; + } + + private void share(Uri dataUri, ProviderHelper providerHelper, boolean fingerprintOnly, + boolean toClipboard) { + try { + String content; + if (fingerprintOnly) { + byte[] data = (byte[]) providerHelper.getGenericData( + KeyRings.buildUnifiedKeyRingUri(dataUri), + Keys.FINGERPRINT, ProviderHelper.FIELD_TYPE_BLOB); + String fingerprint = PgpKeyHelper.convertFingerprintToHex(data); + content = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; + } else { + // get public keyring as ascii armored string + Uri uri = KeychainContract.KeyRingData.buildPublicKeyRingUri(dataUri); + content = providerHelper.getKeyRingAsArmoredString(uri); + } + + if (toClipboard) { + ClipboardReflection.copyToClipboard(getActivity(), content); + String message; + if (fingerprintOnly) { + message = getResources().getString(R.string.fingerprint_copied_to_clipboard); + } else { + message = getResources().getString(R.string.key_copied_to_clipboard); + } + AppMsg.makeText(getActivity(), message, AppMsg.STYLE_INFO).show(); + } else { + // Android will fail with android.os.TransactionTooLargeException if key is too big + // see http://www.lonestarprod.com/?p=34 + if (content.length() >= 86389) { + AppMsg.makeText(getActivity(), R.string.key_too_big_for_sharing, + AppMsg.STYLE_ALERT).show(); + return; + } + + // let user choose application + Intent sendIntent = new Intent(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, content); + sendIntent.setType("text/plain"); + String title; + if (fingerprintOnly) { + title = getResources().getString(R.string.title_share_fingerprint_with); + } else { + title = getResources().getString(R.string.title_share_key_with); + } + startActivity(Intent.createChooser(sendIntent, title)); + } + } catch (IOException e) { + Log.e(Constants.TAG, "error processing key!", e); + AppMsg.makeText(getActivity(), R.string.error_key_processing, AppMsg.STYLE_ALERT).show(); + } catch (ProviderHelper.NotFoundException e) { + Log.e(Constants.TAG, "key not found!", e); + AppMsg.makeText(getActivity(), R.string.error_key_not_found, AppMsg.STYLE_ALERT).show(); + } + } + + private void showQrCodeDialog() { + ShareQrCodeDialogFragment dialog = ShareQrCodeDialogFragment.newInstance(mDataUri, + true); + dialog.show(ViewKeyShareFragment.this.getActivity().getSupportFragmentManager(), "shareQrCodeDialog"); + } + + private void showNfcHelpDialog() { + ShareNfcDialogFragment dialog = ShareNfcDialogFragment.newInstance(); + dialog.show(getActivity().getSupportFragmentManager(), "shareNfcDialog"); + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private void showNfcPrefs() { + Intent intentSettings = new Intent( + Settings.ACTION_NFCSHARING_SETTINGS); + startActivity(intentSettings); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + Uri dataUri = getArguments().getParcelable(ARG_DATA_URI); + if (dataUri == null) { + Log.e(Constants.TAG, "Data missing. Should be Uri of key!"); + getActivity().finish(); + return; + } + + loadData(dataUri); + } + + private void loadData(Uri dataUri) { + getActivity().setProgressBarIndeterminateVisibility(true); + mContainer.setVisibility(View.GONE); + + mDataUri = dataUri; + + Log.i(Constants.TAG, "mDataUri: " + mDataUri.toString()); + + // Prepare the loaders. Either re-connect with an existing ones, + // or start new ones. + getLoaderManager().initLoader(LOADER_ID_UNIFIED, null, this); + } + + static final String[] UNIFIED_PROJECTION = new String[]{ + KeyRings._ID, KeyRings.MASTER_KEY_ID, KeyRings.HAS_ANY_SECRET, + KeyRings.USER_ID, KeyRings.FINGERPRINT, + KeyRings.ALGORITHM, KeyRings.KEY_SIZE, KeyRings.CREATION, KeyRings.EXPIRY, + + }; + static final int INDEX_UNIFIED_MASTER_KEY_ID = 1; + static final int INDEX_UNIFIED_HAS_ANY_SECRET = 2; + static final int INDEX_UNIFIED_USER_ID = 3; + static final int INDEX_UNIFIED_FINGERPRINT = 4; + static final int INDEX_UNIFIED_ALGORITHM = 5; + static final int INDEX_UNIFIED_KEY_SIZE = 6; + static final int INDEX_UNIFIED_CREATION = 7; + static final int INDEX_UNIFIED_EXPIRY = 8; + + public Loader onCreateLoader(int id, Bundle args) { + switch (id) { + case LOADER_ID_UNIFIED: { + Uri baseUri = KeyRings.buildUnifiedKeyRingUri(mDataUri); + return new CursorLoader(getActivity(), baseUri, UNIFIED_PROJECTION, null, null, null); + } + + default: + return null; + } + } + + public void onLoadFinished(Loader loader, Cursor data) { + /* TODO better error handling? May cause problems when a key is deleted, + * because the notification triggers faster than the activity closes. + */ + // Avoid NullPointerExceptions... + if (data.getCount() == 0) { + return; + } + // Swap the new cursor in. (The framework will take care of closing the + // old cursor once we return.) + switch (loader.getId()) { + case LOADER_ID_UNIFIED: { + if (data.moveToFirst()) { + + byte[] fingerprintBlob = data.getBlob(INDEX_UNIFIED_FINGERPRINT); + String fingerprint = PgpKeyHelper.convertFingerprintToHex(fingerprintBlob); + mFingerprint.setText(PgpKeyHelper.colorizeFingerprint(fingerprint)); + + String qrCodeContent = Constants.FINGERPRINT_SCHEME + ":" + fingerprint; + mFingerprintQrCode.setImageBitmap( + QrCodeUtils.getQRCodeBitmap(qrCodeContent, QR_CODE_SIZE) + ); + + break; + } + } + + } + getActivity().setProgressBarIndeterminateVisibility(false); + mContainer.setVisibility(View.VISIBLE); + } + + /** + * 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) { + } +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java index f4fa7f3bf..c9070c897 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ImportKeysAdapter.java @@ -28,7 +28,6 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.LinearLayout; -import android.widget.LinearLayout.LayoutParams; import android.widget.TextView; import org.sufficientlysecure.keychain.R; @@ -106,7 +105,7 @@ public class ImportKeysAdapter extends ArrayAdapter { holder.mainUserId = (TextView) convertView.findViewById(R.id.mainUserId); holder.mainUserIdRest = (TextView) convertView.findViewById(R.id.mainUserIdRest); holder.keyId = (TextView) convertView.findViewById(R.id.keyId); - holder.fingerprint = (TextView) convertView.findViewById(R.id.fingerprint); + holder.fingerprint = (TextView) convertView.findViewById(R.id.view_key_fingerprint); holder.algorithm = (TextView) convertView.findViewById(R.id.algorithm); holder.status = (TextView) convertView.findViewById(R.id.status); holder.userIdsList = (LinearLayout) convertView.findViewById(R.id.user_ids_list); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java index fd864eb09..977740567 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/PagerTabStripAdapter.java @@ -17,7 +17,7 @@ package org.sufficientlysecure.keychain.ui.adapter; -import android.content.Context; +import android.app.Activity; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentPagerAdapter; @@ -26,8 +26,8 @@ import android.support.v7.app.ActionBarActivity; import java.util.ArrayList; public class PagerTabStripAdapter extends FragmentPagerAdapter { - private final Context mContext; - private final ArrayList mTabs = new ArrayList(); + protected final Activity mActivity; + protected final ArrayList mTabs = new ArrayList(); static final class TabInfo { public final Class clss; @@ -43,7 +43,7 @@ public class PagerTabStripAdapter extends FragmentPagerAdapter { public PagerTabStripAdapter(ActionBarActivity activity) { super(activity.getSupportFragmentManager()); - mContext = activity; + mActivity = activity; } public void addTab(Class clss, Bundle args, String title) { @@ -60,7 +60,7 @@ public class PagerTabStripAdapter extends FragmentPagerAdapter { @Override public Fragment getItem(int position) { TabInfo info = mTabs.get(position); - return Fragment.instantiate(mContext, info.clss.getName(), info.args); + return Fragment.instantiate(mActivity, info.clss.getName(), info.args); } @Override diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java index 52e6dec92..05f8f8860 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/ui/adapter/ViewKeyUserIdsAdapter.java @@ -27,6 +27,7 @@ import android.widget.AdapterView; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.TextView; import org.sufficientlysecure.keychain.R; @@ -106,40 +107,55 @@ public class ViewKeyUserIdsAdapter extends CursorAdapter implements AdapterView. @Override public void bindView(View view, Context context, Cursor cursor) { - TextView vRank = (TextView) view.findViewById(R.id.rank); - TextView vUserId = (TextView) view.findViewById(R.id.userId); + TextView vName = (TextView) view.findViewById(R.id.userId); TextView vAddress = (TextView) view.findViewById(R.id.address); + TextView vComment = (TextView) view.findViewById(R.id.comment); ImageView vVerified = (ImageView) view.findViewById(R.id.certified); - - if (cursor.getInt(mIsPrimary) > 0) { - vRank.setText("+"); - } else { - vRank.setText(Integer.toString(cursor.getInt(mIndexRank))); - } + ImageView vPrimaryUserIdIcon = (ImageView) view.findViewById(R.id.primary_user_id_icon); String[] userId = PgpKeyHelper.splitUserId(cursor.getString(mIndexUserId)); if (userId[0] != null) { - vUserId.setText(userId[0]); + vName.setText(userId[0]); } else { - vUserId.setText(R.string.user_id_no_name); + vName.setText(R.string.user_id_no_name); + } + if (userId[1] != null) { + vAddress.setText(userId[1]); + vAddress.setVisibility(View.VISIBLE); + } else { + vAddress.setVisibility(View.GONE); + } + if (userId[2] != null) { + vComment.setText(userId[2]); + vComment.setVisibility(View.VISIBLE); + } else { + vComment.setVisibility(View.GONE); + } + + // show small star icon for primary user ids + if (cursor.getInt(mIsPrimary) > 0) { + vPrimaryUserIdIcon.setVisibility(View.VISIBLE); + } else { + vPrimaryUserIdIcon.setVisibility(View.GONE); } - vAddress.setText(userId[1]); if (cursor.getInt(mIsRevoked) > 0) { - vRank.setText(" "); + // no star icon for revoked user ids! + vPrimaryUserIdIcon.setVisibility(View.GONE); + + // set revocation icon vVerified.setImageResource(R.drawable.key_certify_revoke); // disable and strike through text for revoked user ids - vUserId.setEnabled(false); + vName.setEnabled(false); vAddress.setEnabled(false); - vUserId.setText(OtherHelper.strikeOutText(vUserId.getText())); + vName.setText(OtherHelper.strikeOutText(vName.getText())); vAddress.setText(OtherHelper.strikeOutText(vAddress.getText())); } else { - vUserId.setEnabled(true); + vName.setEnabled(true); vAddress.setEnabled(true); int verified = cursor.getInt(mVerifiedId); - // TODO introduce own resources for this :) switch (verified) { case Certs.VERIFIED_SECRET: vVerified.setImageResource(R.drawable.key_certify_ok_depth0); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SlidingTabLayout.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SlidingTabLayout.java new file mode 100644 index 000000000..065034be1 --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SlidingTabLayout.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.HorizontalScrollView; +import android.widget.TextView; + +/** + * Copied from http://developer.android.com/samples/SlidingTabsColors/index.html + */ + +/** + * To be used with ViewPager to provide a tab indicator component which give constant feedback as to + * the user's scroll progress. + *

+ * To use the component, simply add it to your view hierarchy. Then in your + * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call + * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for. + *

+ * The colors can be customized in two ways. The first and simplest is to provide an array of colors + * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The + * alternative is via the {@link TabColorizer} interface which provides you complete control over + * which color is used for any individual position. + *

+ * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, + * providing the layout ID of your custom layout. + */ +public class SlidingTabLayout extends HorizontalScrollView { + + /** + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link #setCustomTabColorizer(TabColorizer)}. + */ + public interface TabColorizer { + + /** + * @return return the color of the indicator used when {@code position} is selected. + */ + int getIndicatorColor(int position); + + /** + * @return return the color of the divider drawn to the right of {@code position}. + */ + int getDividerColor(int position); + + } + + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + + private int mTitleOffset; + + private int mTabViewLayoutId; + private int mTabViewTextViewId; + + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; + + private final SlidingTabStrip mTabStrip; + + public SlidingTabLayout(Context context) { + this(context, null); + } + + public SlidingTabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + // Make sure that the Tab Strips fills this View + setFillViewport(true); + + mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); + + mTabStrip = new SlidingTabStrip(context); + addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + /** + * Set the custom {@link TabColorizer} to be used. + *

+ * If you only require simple custmisation then you can use + * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve + * similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + mTabStrip.setCustomTabColorizer(tabColorizer); + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors are treated as a + * circular array. Providing one color will mean that all tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + mTabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Sets the colors to be used for tab dividers. These colors are treated as a circular array. + * Providing one color will mean that all tabs are indicated with the same color. + */ + public void setDividerColors(int... colors) { + mTabStrip.setDividerColors(colors); + } + + /** + * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are + * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mViewPagerPageChangeListener = listener; + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param layoutResId Layout id to be inflated + * @param textViewId id of the {@link TextView} in the inflated view + */ + public void setCustomTabView(int layoutResId, int textViewId) { + mTabViewLayoutId = layoutResId; + mTabViewTextViewId = textViewId; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the pager content + * (number of tabs and tab titles) does not change after this call has been made. + */ + public void setViewPager(ViewPager viewPager) { + mTabStrip.removeAllViews(); + + mViewPager = viewPager; + if (viewPager != null) { + viewPager.setOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Create a default view to be used for tabs. This is called if a custom tab view is not set via + * {@link #setCustomTabView(int, int)}. + */ + protected TextView createDefaultTabView(Context context) { + TextView textView = new TextView(context); + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); + textView.setTypeface(Typeface.DEFAULT_BOLD); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + // If we're running on Honeycomb or newer, then we can use the Theme's + // selectableItemBackground to ensure that the View has a pressed state + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + textView.setBackgroundResource(outValue.resourceId); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style + textView.setAllCaps(true); + } + + int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); + textView.setPadding(padding, padding, padding, padding); + + return textView; + } + + private void populateTabStrip() { + final PagerAdapter adapter = mViewPager.getAdapter(); + final View.OnClickListener tabClickListener = new TabClickListener(); + + for (int i = 0; i < adapter.getCount(); i++) { + View tabView = null; + TextView tabTitleView = null; + + if (mTabViewLayoutId != 0) { + // If there is a custom tab view layout id set, try and inflate it + tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, + false); + tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId); + } + + if (tabView == null) { + tabView = createDefaultTabView(getContext()); + } + + if (tabTitleView == null && TextView.class.isInstance(tabView)) { + tabTitleView = (TextView) tabView; + } + + tabTitleView.setText(adapter.getPageTitle(i)); + tabView.setOnClickListener(tabClickListener); + + mTabStrip.addView(tabView); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mViewPager != null) { + scrollToTab(mViewPager.getCurrentItem(), 0); + } + } + + private void scrollToTab(int tabIndex, int positionOffset) { + final int tabStripChildCount = mTabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + View selectedChild = mTabStrip.getChildAt(tabIndex); + if (selectedChild != null) { + int targetScrollX = selectedChild.getLeft() + positionOffset; + + if (tabIndex > 0 || positionOffset > 0) { + // If we're not at the first child and are mid-scroll, make sure we obey the offset + targetScrollX -= mTitleOffset; + } + + scrollTo(targetScrollX, 0); + } + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onViewPagerPageChanged(position, positionOffset); + + View selectedTitle = mTabStrip.getChildAt(position); + int extraOffset = (selectedTitle != null) + ? (int) (positionOffset * selectedTitle.getWidth()) + : 0; + scrollToTab(position, extraOffset); + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, + positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mTabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class TabClickListener implements View.OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + if (v == mTabStrip.getChildAt(i)) { + mViewPager.setCurrentItem(i); + return; + } + } + } + } + +} diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SlidingTabStrip.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SlidingTabStrip.java new file mode 100644 index 000000000..4b8c7e75d --- /dev/null +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/util/SlidingTabStrip.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * 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 android.R; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +/** + * Copied from http://developer.android.com/samples/SlidingTabsColors/index.html + */ +class SlidingTabStrip extends LinearLayout { + + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + + private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1; + private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20; + private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f; + + private final int mBottomBorderThickness; + private final Paint mBottomBorderPaint; + + private final int mSelectedIndicatorThickness; + private final Paint mSelectedIndicatorPaint; + + private final int mDefaultBottomBorderColor; + + private final Paint mDividerPaint; + private final float mDividerHeight; + + private int mSelectedPosition; + private float mSelectionOffset; + + private SlidingTabLayout.TabColorizer mCustomTabColorizer; + private final SimpleTabColorizer mDefaultTabColorizer; + + SlidingTabStrip(Context context) { + this(context, null); + } + + SlidingTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, + DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + + mDefaultTabColorizer = new SimpleTabColorizer(); + mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); + mDefaultTabColorizer.setDividerColors(setColorAlpha(themeForegroundColor, + DEFAULT_DIVIDER_COLOR_ALPHA)); + + mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + mBottomBorderPaint = new Paint(); + mBottomBorderPaint.setColor(mDefaultBottomBorderColor); + + mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + mSelectedIndicatorPaint = new Paint(); + + mDividerHeight = DEFAULT_DIVIDER_HEIGHT; + mDividerPaint = new Paint(); + mDividerPaint.setStrokeWidth((int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density)); + } + + void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { + mCustomTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void setDividerColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setDividerColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + mSelectedPosition = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int height = getHeight(); + final int childCount = getChildCount(); + final int dividerHeightPx = (int) (Math.min(Math.max(0f, mDividerHeight), 1f) * height); + final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null + ? mCustomTabColorizer + : mDefaultTabColorizer; + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mSelectedPosition); + int left = selectedTitle.getLeft(); + int right = selectedTitle.getRight(); + int color = tabColorizer.getIndicatorColor(mSelectedPosition); + + if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, mSelectionOffset); + } + + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mSelectedPosition + 1); + left = (int) (mSelectionOffset * nextTitle.getLeft() + + (1.0f - mSelectionOffset) * left); + right = (int) (mSelectionOffset * nextTitle.getRight() + + (1.0f - mSelectionOffset) * right); + } + + mSelectedIndicatorPaint.setColor(color); + + canvas.drawRect(left, height - mSelectedIndicatorThickness, right, + height, mSelectedIndicatorPaint); + } + + // Thin underline along the entire bottom edge + canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); + + // Vertical separators between the titles + int separatorTop = (height - dividerHeightPx) / 2; + for (int i = 0; i < childCount - 1; i++) { + View child = getChildAt(i); + mDividerPaint.setColor(tabColorizer.getDividerColor(i)); + canvas.drawLine(child.getRight(), separatorTop, child.getRight(), + separatorTop + dividerHeightPx, mDividerPaint); + } + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { + private int[] mIndicatorColors; + private int[] mDividerColors; + + @Override + public final int getIndicatorColor(int position) { + return mIndicatorColors[position % mIndicatorColors.length]; + } + + @Override + public final int getDividerColor(int position) { + return mDividerColors[position % mDividerColors.length]; + } + + void setIndicatorColors(int... colors) { + mIndicatorColors = colors; + } + + void setDividerColors(int... colors) { + mDividerColors = colors; + } + } +} \ No newline at end of file diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_action_copy.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_action_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..22327391e9d1820b2dedfe3df093c1bcf581e153 GIT binary patch literal 381 zcmV-@0fPRCP)TZ%@CcWlz>SV-&cbcz5(=I`I&}eQ^}=yu%B}k<1Q=1Y&R^aMFu2pR zb*^YXHOHNPiGT=qf(r%bLeks`5&{7vF*7I$4>X*6E^A8mTr>efJ@E0d;v+zB*!|Z$ zVwF%Iaw4unkkHT?NXVyz*xYC>30bibh+RVXkU>HtB;<TUu!n(oHu3^#E4{%!fDX5>*93py`ZNJ;{oaQjCpqb(_(Z_$LV&4}NbF0082~ bTYv!o)Zbu6Ia}sl00000NkvXXu0mjfJR_Q8 literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_action_edit.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_action_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..5f7c6eff38c2a503c1cfc2c4794850ad2c06d23f GIT binary patch literal 884 zcmV-)1B?8LP)%qHdNgg7?rJr+8F6bBgDqJXES!0PfUkwTBqa zF{?w2eS)kfkhongmouvbIE$k9=h6VaDms^7#dw18QA{lSH4yM^OXw~GQw62Oz&VH= zQ{xwCi!B{{^ zFyE0F5J}F^dHbf`%g3y0Z2?3A1bg1af{kP z#xpOg>jEzyYce{~fXR&ZIaZ6=iV~C6`J5|qKkgls48ix9=bwsfII}az;G;IX4l~aU zHQu2oZYd>^*E@XYw^0HF-wx$*AyO6H6P$Rz!F!KzZJ^6WQYJo6asB7IcU264*G<43 z0oYdnn`(ev@eq1ntnS)Oxz~D`j_Z{wceUsqRcwm!7 zOC3(VHU)m41Y{@mjaex$S&Y#b|*cY{w zOJo2BU;qYS00v+Ho|2|%7aFSsfZ;t1W6~xdqQJ)AV|e4AbUoCz0)38n4=?Pc+4^H4&0Jm zZ4&ctGd^NG34MvXP0V*A5bvWuKE8o2I5&QR0dTmGw_j?!?j!Gjm^YpSFqX=I>e4%* z{HJ=USpbgNK`V?bC6f3tknyy|P9*>y0s%PQ$y9e_Puc;NngOO#IS@)dwVz7FQULP+ z#%~ZlUQLeAmv|*?Jk>v6z7Mrey|FCIb&@1oT+X-^EVuw$L+_xsX99e{`!msg025?K zN{t@ZFwcBs;wy`bm(x3SljFiKt||Ix6(>Ghr#L`Ovs0Drsb%xO6+4|a00S@p126#9 b@+-gqQEOP8ri4a)00000NkvXXu0mjf@b<~p literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_action_help.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_action_help.png new file mode 100644 index 0000000000000000000000000000000000000000..382d314ca1f6c7013aea23a51f0cd57e823db22d GIT binary patch literal 497 zcmVA<0lsNaQTnA3faz~B92|MxBN`gHG`aHOa2}wJEXixS zwhq&NcN%i1jibn`)B;)}wgd5lW2Q!kcy{D`kyHS=$o540433$Jhf+dV6aiENYsc+A zYXL_ETrwt*w1_$KwzzMY7GsYZ>a!F;vn~*P)aG^yk@=^kRvdck&e9V>jUFyLxTL}?J6!Cq`8BnB#)I#=oHe~Te zThIIjFk8o9t%I}+tsI@_oL1=UZInBitNwz3=m;%fn0PN60gYpmKKgDQW7=qGzMfRu nFW`61rVkE)7#>=@9m>EC? zSl7G)T)X>xQlvmN;6+-?5KtGuGW&2ff!7j_ds`9)HtN@!D z*CuS{4Y&uMfLo2vz#Z`5?lzLt2z=Bt4y!q7G089tDb(k$mfl^_4(f zF>v>j5D+09`3}Mk%ZG?=w~LwK?pJA=ZUNgFPLu!GpP$M1+agm)JWbQ0a{QCD-2Gxw z@TqMY0JHRZGUgL?4@wybN;6$FGb;ka3HWiJp|H^>Cj4dSKi4m!ukVJ|a>7Ov>;g@9KL*;Lg$#<>)>>28=%+D>z$dX!>EDre3*~?V$dqz6 P00000NkvXXu0mjfKR>~n literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-hdpi/ic_action_settings.png b/OpenKeychain/src/main/res/drawable-hdpi/ic_action_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..0eb78f7c7341b7a4e8f2fa63bb42068ab72b1ae7 GIT binary patch literal 953 zcmV;q14jIbP))n4Ni}Vxl$aDGpk&`o z-$|#_`F!rq-hiL<35!N;xSu1DtsT=t+36NWyh zR;%H1xm@R7h2Z2aF0Y_x&^a^|RfYF=46j^Ta({w%zbId>!LJv|`(6WJb@`AmOq9s) z3@(B`mgN%~(7_IWmpEo+2=F6<2z{5)Uc&PU^e40n9Vkwg6lXje z3}E%es=c%f8FD3s<4plp6M&(0=ria!^eY3oZyF1nM8L_yR~6b#aK6b^t2o#aW8Ofw z`894cr+WabE*mkA<*wR^0q}%s%I6Mqe-dLma4EUv6aapwt*}U$0|L9EIX(giFAWPZ z&;ToJ92#s3nZm}<3kHyj*T~6V`1wM)eFA+1-O`*t#Ke)o#u_$GIewG3MgHHgO4ZXk zpK2%OimFEE25K<2u}wZ3X}1NCr>b37=TlP&7@jJ>LoFRHU1~QMLQStgOSeg8Tu22K zb6S-!g{d=xU72HB(Qe7nudn(>}YJyET`(&62EQC*nOR z+nLa5Od@vL=h_}}u9eXyO63$K^TyII43sijBKdowl+lJOjYz|V`i9*$V2|5T_>6FH z3l3btfye;Vw}y@En0Pw%WW^|GOFk%{_m>K)I7dI{%361-zyXQbW^ zw&;)xlN|dz;PrYk$FgV3-O%^xbQWi$b6WuanvJ@MZS%SCJz1`QaJZhf6;encg%pyT b{1#vU%aSg@hL7wY00000NkvXXu0mjfhD literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_action_copy.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_action_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..7134820209a2219c193c8e08a48c0e1a52aa3368 GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1Ghdf;zLn>}1B}lL`^GIyul#-S%X4I7AI$Kxt?aj^n7Lkn) z1lS|vd=@fSHU1D&boF7DWz}gBVQ4m-E5h7((R}hMJ_TjpFG??im8YmhbeK#@%S`&P zoH1&7CnJC3LUGm$38HL4VHt~%p_a@b zW+u2~K@5Rlh>7VL%q$lE1uV0(rw~J8Fo(dL*^(IqGZP~jW`cRZI-b0sc zlU7z=axKk1V6_WHT<&X@J+1rHssn%vNDFF1SIX=i2BO%CSb@jZf)H>4IDqb;7w8xY z2#r{;(jl=$U7Ui26$c2fGtf?<2_30Y;@G{jE@nLf{7oOn-4ZU%=W-u04wl@ONOk~x zY#OUz0j^rWSr3>6N6-`C)1>iSi*RXeF28`zSHUx-;*@Y{eSo7h9o-z@4v(T^#eogr zA!a|a;sD@fLq1;zkF&&8VNHe;=lc=5hYp!qt-)71fSc_cMU%(r8+za|^Am#Zd6S&t zuDRW$wYU{GYI32)X(K+h1H9ezF6XU=$26V5X*F#>U3Q zQc_at&!0cPK&&1nDq8Lgq||`e63CXNs{?=*3Ii!0Bq2_Ehd=@lbHO-g&z`+SHwPdE zJq&CiML!D#7K3~o28wee;m1JVZzjbYCNeDtC9)zk%R%4?G(8Xv7|F2Q07%6n$2lJG z4T$&Q3l$b3EQ4h!P&^AmF|mLPnmaa=n+C#2v=AG3OGz3Kra~6|MwgNh50D*b2V59v z2W%N=2T+s-E>M{IrvuA?w^S*Jprs!uSAfa}eIg2?TR_Wusa_HyIYFaS8|G<$=w6W;&;002ovPDHLkV1honb`<~s literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-mdpi/ic_action_help.png b/OpenKeychain/src/main/res/drawable-mdpi/ic_action_help.png new file mode 100644 index 0000000000000000000000000000000000000000..5876cdea48bb0746735a5da18ffb9e12da857e4c GIT binary patch literal 404 zcmV;F0c-w=P)F6XV1NN5Ihu@(jn#mZ zI+D;gAij0>?AgaOa{$m%VIbuT#QH$Yg+tjbAYMg^LzqZ#Yz+`w;;|eAKp|5Ebu85# zaGXeWAP0sKYbXl|4%iFC&Pa~A0K}l67KU0Vi=@_oCW#OjvoMUWpJrpRXdva;oG4D0oY*MslOz7_{P{*a1Vf zJ-`L6`9N9m0y*sf7+@wxla!QHIkaK|Il>OeXMFzrIeii#v;dOD(hi_oUBF4rG(fTg yp!xkVmZlh8vtb`n^Xv_&#qTH>1%nCz1Q-Bfy-#kMN(7r(rc zy&_3dX~}H8q}-{&72MYT4_LpLblsIv=!-Dw7}aVB?2|*JtLK2`U;dpOM_Mjm0cXC^tN(r83P=`H`@vOHX0fbJ50BQRkS{ zXKycA{y6bs*SmeWx2N@uc4+U<6{Uax;_Qov!pg~0@dGS8KwLAcfmQXM0 z^@7F>#_J%}i$PzJ{wX5ChCe~XT;n!$3yhIT>#jNlmY1A*%BanB`sRE z=TfQtu@7)wm&k2{a^0ewFAO9M1qm5UgUge-WU7GNDnJE{3~lyOcqY4HPcS}fR@Yhz z$3(`&rVJn8xjf_z2m1wFi4X~?laTRyTMkULxewzr7||ty=b-`MWZTO~iAK~!X|cg? zLDDP@3DT4OGbr-@ahvPd)2>&W>o`F8-LB&TU?V}g&231Ap8xBQC@$%Odk3)4T5RG9 gppr@sF24c{01P0jsq^4BjQ{`u07*qoM6N<$f<^ZV0{{R3 literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_action_copy.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_action_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..5ddf15139b80b2996782448b3453268197a0cb48 GIT binary patch literal 353 zcmV-n0iOPeP)0=5Nvp#iT_!=86cC_vUhLEcY{7ee&=_%o=+AcPP?&FYad*g4nB>3Q)r2k-q? zYciWSe!KbcVgn{Q7Oly;)|UVVv~GYsI)MbzJLd&T@O~hmWCC(4H(^#8*XsHNZ33$6 z6DWfiKozJv#gFz0ECCF_01UtY%HTTId->eo@W}u&00W9y0--d7PcYzs0ki6E&g_H zZtn8-_O=&yV#N~KFN*h5UOEUDUOzTD0AbK%0@O86LSf%30+eJ|6Gv9>2nbJUN7uKR zkPIL5fn58^nj8?y^oZqIaaZDbP2885z7zLl|CDXBN%=s|_2inq6$HqGKF9-K$?gNe zO)D|IDs403U37DD?kRq2D4}f)0VJU(ej(saNWi?(46sX30LT*W$bLOW2?&9oPyN@F zf&F}+asQiD-J!IhxY<^^!8$Yi4hxy?ZYoS~F;(KT}ngAp=0dP(NYD(Ng)(vU&0NGw1_>Fj{(`2yxDf`oY_MG}DUS>^FPoq6>LM=YRvH0^tQ1Cqy+yI2_6fi zz+7&RIQemJBmo)0t0a6-#`ODP$qVpKR`4SyASZaWhiDP3&pAJbvd?W5!@&pEM9gy^ zVX%)kKa3ksSO8vYX2%!>j0ikzc@!_JVhQ+>S;Z3Y_bi5nZWFeE*IDE!gaDht=MDh@ zzhN`@+@nDmd38B%hdn5h6jp%G6#^(vqe@V!qGbd4%mgSixTwl*co?&|KI`^x8HcCO z3O-K`XcC~yx7`Y6XQ+x@8^C8GfNwaeDr+f2S19AVGKryM2hR;Ys{)o|(#hLC05?MxkgT)Hd-uS2qK;?H%;Ma}?Hl-o| zhDP%9ndH1Yixj3RU@fLC(-+#-5WvNVg$SAd3WmRq<(8xXwd)JM-13|;vLpgrj^?aM tYC|)WzDZwNmsVP7rIl8i-S$_20RXOO5G<352)Y0O002ovPDHLkV1kOuAU*&9 literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_action_good.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_action_good.png new file mode 100644 index 0000000000000000000000000000000000000000..0bb45d2c03ea82246a8ce30dc0f93f96796aade1 GIT binary patch literal 566 zcmV-60?GY}P)SVE-#ddabe+_oJy0*4eB-PmI+Scw}_5@i5437K3-S?ARNGRp*eec`5cdtPJ00000 z0002MKNhwQEX%T(E(Nt)RaJXc{e%`z&O5aYwVcFw^-2H=SOmUm+}g4u+!vr>{U+kx zgD(!)all##Et~x{UDi7L3@x+%uI+Q$32N6J@X}%Zi_;FE*%vE*-tox6Spg{cl=u8L zj%5EYRsBa6?Ds{N^;Kqp#sp|AczrAWLG@T7gO*SFo>ggmBZncj{M&%9lzbSfXDLlDlmQ@c_-$q2O25O~6~zEaptNg`aVSOfSsE`k=p zMhid#5L?hk%P~1=)C4vPM4Ie;c?%m_F*;N5 zpR;nnKSf!x81WwmoSO6fT1TlI000000001h>wF6^0FD}J4#4+HeE4I;Jd%`7wJD|a2Td&y;=nVpz??Yn!s zcb6z72!bF8f*=U3q{H=~;c(c8!x35tc8EvKX2)K@p0X?}8ZQ7koq)w)(Vmwt!4@FI zrf~v*<2uSG814Tu2NBZ72msDsR^<4XYyc5v6asV!?n#ld2?!fEMSulSwvIuBu=WCA z(OMA*pq(EyAp^>?pup-r1-k{Y&fs_hHnSW$l@9~5Hu$X=>WoOi;k@L09pP(3yWGG% zuBv>`fUw1;7*(rYt|vYqg1%-{a#~dNOOgqI(a!h0?3H^e+*K{7ZUUH!+{#-4um~pH zL!?_H=P!0vhh+;SzXaHEzHdr@D6ExdK;YQOM4&(D|xQmeH<>f)iC0vuaZ{noDr+2JgLZ-(XURtbV2Fw^0D7_J2d*b*ab zeY!B_)Stnf3xJy+=j!*p{Q~FzOKt%;=K&-2_lFIg86yxW@=^eF%=FKI79sXE18gH3 z`aA?`36}udRttdpdNytD!RH|`H7t2k7juCFt_Q8x>-S!-_lSk=)3hMZVE2^2!U=*P i2!bF8g7AY60R{lV{h*XWBt!`S0000mB3( literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_action_important_small.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_action_important_small.png new file mode 100644 index 0000000000000000000000000000000000000000..40ca1572cf0232686725f96b541b8efbbd1a24fd GIT binary patch literal 623 zcmV-#0+9WQP)x0sKis zK~z}7?Uz4mQ(+Xwe>c(8P#r8%B#7uxun2WHn}bWYd=9~(L$%IM3QobX2!4TXU0pi5 zJ%hz>pb!U1hbpF~NnD=5B~A3+`-Y;>126Zz=Y7vT&-u@V3;LfaBAOA=O!-DF5yK;| z%5*6aK-Xy%d%fLrToiiaYst&O4 z%R^w4pnPKBC}+gJq)t9G*_^mP1RmR^Sz@;wJ7n)9eV9J1&ywhNyPsRF)+hJ*(E>v7 zTGCI@#2RDNvPUQ#P|!Yl!Afs=v&mVhU~$=|wT ztzhe2^Ux_ur@&f~)?Bm|->U%N{vv0=ddVcr?6rjfxHE^}pPGt!y1S@hbbHGBNn<&X zv@K~HI500?DVb%HJ+Pje4RsHj9Z7q+MBM{+d{+0}fn}2cOkz0%b|t;?w40_QpY8fC zMe?8sfUvu_0Od+^R=IksEr%sYp@?Wi#A<^4YD6?He&POU`VCTEuj={Yi#h-R002ov JPDHLkV1kF84le)z literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xhdpi/ic_action_settings.png b/OpenKeychain/src/main/res/drawable-xhdpi/ic_action_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..999d0f0d8c1571d3f90be129c8e4c95e895ee7b9 GIT binary patch literal 1231 zcmV;=1Tg!FP)hghH17Mrn&ZDP&C4ukZN0TjEU&^6R3zQYP-+@eq8oMeYFz?O9Lg#v&({fS28uqHIV zBZTqbhuah8N1lBfaT@o1NvAnU}e*7 z6Mu~c1GYh9DTcoSaJFgO(b})HZc5)@8tk=tb92)x+yZcl4FlFTPIAw}3G#_{5JHXi zB+&Xrhrt~Ko*tAxv`{|E2M!^to71yp2LNFwDL#+r6uLP1AdU}rewa}Fu#A0<0^p6K z3jEW(UV$b+I!F`Sg=DE7bEO!7&k3NOknBlePLLeUm>g56@#F(ODVqPFi2wXhr!;>_ zGNuZ-QyM>L?GUo(6!pP3INu--`A z8UU$r5qa`|a{nwnJ~+vdCK8Et#Qc4h!z;q~siuStE#yx%+2W&Zi9I;71>i4J)Rd$v z7V_tFwy4|zs7M)`>H*6rakF%h z@{3ZLJVvszAuUQETcbrpCXJa|44`SPQ@&=h+WP3qX+Y08u&I#7oe)R(L2_ zN>Z84sdSrU9vQUAnrRZRR6c<2*&VmH653%4#IuNY)?$pclm*4h2_^0+$=X%;7S%O5 zD>@2X81V3mjU7HUO;KY?y!Hh3g~^MR^TMM5S-~xx#+pG&T7@OF5gk?&PFlVm9qt|A z={)M99!4(~cvP)Kkc;{%mH!%A@PLg{Gg+>9% zA;vkmP+=jQ#Pr0zK-9kzU#lSu)=KG`sSI$d@Gz(diN{cMgig)RNEsoXt6F1YMaEb~ zhNt51V2=QNH349r|CqHWVEb~EA7ZN893Zikyh(BYWhdBG$;aLd#L(tHMNa-CrFfB= zM66KD=B@98>IdKi(}csm3*}umLzyOgz tRnh7415chjdGh4RlP6D}3ZUNt3;-0k%aSxwCl>$!002ovPDHLkV1nmLGUWgO literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_copy.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..a0508df8c408c743e6933b6d5d581c289e89495a GIT binary patch literal 470 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7OGok|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+7%Mzo978H@y}7f$@Q{H>L*eAbNo`jm4>%h#6dO1^^SJhb zY2DpRYPz2;UU(wZk!tU`|NS}lcK-T3_LrPXWPz42FdWEwGp%CszW4kupKkq-ZpeH1 z!`5%U-?{gm|MS7WmD^%@cFgZ(`jZZ96L4T)R9F!EZD;<3sNKm+_~%4DD;H#7n&s(u zv16K$t3$V7!uyQhA}Q}ZxPq90YFR2+#F7?@d+q$*=h7hJ!oZ=xz#_oF#NqH-`=NZ; z;`4k3dYP%UiVyzVSReT5bG==pWs|bP(utK{%GU%oa40x%t=h?5)v~Bx!}V>G+(AKA zO%L%U9Iu-cSX19L9eT^V^%`sIZRtr(E)9AdEFO+q%N*5`q|1TC1y;>N3JnZX?zZhK z{i&9eyvp!-7VDk)8@tN>d9(bPyK?v0oZ=6@2j1tt_>!u9F(3 zbuWRq^z53J#ecVNhPt`Lu{-GSy^Bv?GlK$*f$Oo>0fu?9F(>4X3aNqwJYD@<);T3K F0RTJkyr%#F literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_edit.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..f2b2078b0787cf6f5722033b5b794cb5fdcba000 GIT binary patch literal 1670 zcmV;126_33P)mMdBG5BtRs>(%|z-Jayzec#NEWNGgrec!$x`*v3e0)apv5C{YUfj}S-2z)|KC|Je8!NEQ~ zbg6atrAuzp^Yimi1SpsI=hRvW-6qsVB*at%uuJ}c$mTUeA4!Cz#DMkF&w+(l0E{Uf3`Ky|VB6!#)GRJ_ zq<#?KC}kdy1uwEr*lM1T|ClQSEVI`;37A5?A|Mt0;-vs^B}CXy z$n~ksiHMsBA>g0)Sd7$JEsKCT!+yq(@h0nBpza#^9CV*@gAjYv0)x-|Gn++w&O7Qx4t_@1e6QjMfCs05K8y)W3L3TKz^DC z7~$r^NtgQRZfC-eLtZ2BLja59CyEf`iu#5Cxi?-_G6fOajm zvH(5nX|Uf{ZD3ddb|jqnO1^ag*zF{pqw1Nii4Qc}s>g&?BEJj^=x6A291#y-1;e1& zS}d-8RVDHPtJ_lA?ePdpN0I>BEMW!c>y^19%k&`i4j5V z<)5+;O_#~HDgcr8ELc6%kSYQnj<_*|2>H-m*Jbjp#efXKHz{L!Y3xYWvl*w!w;6={ z1+Im7C;WpM9fM$}$+szhjf!X2xX;XW@~sHK!Piz%t)r!;iwHO56#2F+z-GO_Yiu7M zxlDdx0SW})B>bp7zu?)eljPg5fPM*|X<)dap{wK$m41kip6)y8N?pdGpDkE%=5S@#1DSXaOL9C_z;q5C{YUfj}S-2m}H_etZ*P04TpsYf3?Y QLjV8(07*qoM6N<$f`u#-@c;k- literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_good.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_good.png new file mode 100644 index 0000000000000000000000000000000000000000..fda51ad86d135ee0d20eb877e5043a3e97b3a41d GIT binary patch literal 823 zcmV-71IYY|P)oS~|G(U+8FQ@n}Xj*q}%$Y-uWmtYp9iBfF=ZB`aFpIL$lXoxe}AT-8-& zzu&v}-rc=R5&!@I000000000000000P@&b751mXVJ#!nID_blUa|Cd2{7Z9n?XTuW zgc~2;t=*6T@Tp*Y;navSMgTY7e`;={|2+DD0Q$xkuRnj`anC#-B7iPaF?7lE4g%p`0mTQw<0nA=L(_chw=Uy0vyKpL+kmwmYm+Kg65=wC){a;u#@=| zdV&?ehMbSdW^<(qcgFiO1bF)-t4gvh^5o+;r2!fLyh52746OhRzi-IdMwT7&>gUmy zaMTLG_*F{Gx3DtT^VZKYVOus*fIUh~EhKkb8zEMW5Wsr4{!qr!eu$6k`?E@R< z#~u#I+$m;331G{{ZQk4az<$^$kb*_!JyS*FUwG=xUeQ)EelJ4fXTb;%P*|zLG9&?x zBQyS7YXe71fI&pYf6%CYv<2u!B!Gm|OPkk%5g<0odQ1@@IEJd~E0V%e9bq;W=*+r){0#=a-(9v#T)Dqx15&?$V=g7zbHI<(T;0fe_ax(2x zG~|GCeuX*$?jtb%PZw?MTJr#zw#yb?Xv@-h2eq?C-Xo2Fx?ZpU_WS)m1pz`4;$5;i zz~QWsx3!G> z`JHUq&Y$WqIsgCw000000000000000IN+ZE0|08nuo&Z9Uc3MR002ovPDHLkV1jet BaMu6; literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_help.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_help.png new file mode 100644 index 0000000000000000000000000000000000000000..c5a34319b7e31e12a08c7b429195a569be0a5c22 GIT binary patch literal 925 zcmV;O17iG%P)qKorKWsWo6SkRod% z11Vgjlz3oZp(`C89WB{EP`V7377c{9G=nqPA}uhciR2NJf|TyP>7qqbTlp^6-Fx=; zlTM9PJ8{16?w;Sfi;V#Q000000000000000K-)RccEO|3Xz;+l>-l_M!+>yN_vz1& z8jJp%TEF3`CAAf`pJYf01KdPsp>r#IKfqgVsC}h2BQrKI;C|s*^s(*fkHH@!$b>5x z@K^NIik=7R?M>jRX(DAb97OK%@2I5_K37Q1SgMSt*!D+Rs(Wv7Dh zE))o#*j~8X+o#mNkTBmVNZvFI>^B@#>E&z3OSq+Ti>^*V%#7L_o>q8D+NXfVBZWpn z;W!<2ItCa!m!AhLB`>uY&=WEOOyD}cnl*u*X(z5Fh7tyZFEd~SMC7I8@hcb*E~glH zi~txANc^;6R{bvv7!XGMGtVq|0|UZ{?-P8Zu1h2>5Z|}{!(7MSN0cZRf1h>bm4qD< ziDL0tc;Ceo?)4K1-y=~fKHus1*8Vx*UUYt2N+j_`?Fmw6K-I^&S_~LC9)F=l zd@TmJ6xa&sbG}9cT#9TW!;0AK(N0IkY|KRFz`hNS=i0000QP_ZU#cfRqC zXD&_eJ~xO<58LMg6)@m&#BaLujm=P1ie?Uli_fBer1rGI;XF!CvMl>iRn@H)1EiNC zT(=$av8S3wKu>V*PmTL>B$G>c3{4wGz_Uo_!T?L(bO6Z)9KnFNEMY)g>PY9p0LK1w z$VbRh)8mV!=K)iXbFPdcL}$X3(f*>$wsyz|E~)n~U_e+C*7hMxM-SYZmKw!pwEaRR zy(7t5OMECd000000000000000006+2-vSH(Y!L)N;PR}@00000NkvXXu0mjf<`|u1 literal 0 HcmV?d00001 diff --git a/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_important_small.png b/OpenKeychain/src/main/res/drawable-xxhdpi/ic_action_important_small.png new file mode 100644 index 0000000000000000000000000000000000000000..44754152fb72f046521ccc44eee8a0188d2bc232 GIT binary patch literal 890 zcmV-=1BLvFP)jR^e$Y7y5i zMR4V+D^XmWO?Bf!+D(n9P^wi)n}-X}=op)1GMO0@oIhN0lQ}t$d!Bb7MT!(D8bL&| zB9e_$MqkV}oUzrj2AsCldj^QeEZ_i+?R(Ax1)yrH5SNht7)~Bw92f<*?B`Jt83*2~ zYIl3U6f6LnzMnUNLe$E~Vt|P30;VngJvhL$J?r`$I9GT2elJ|?1q04Dd3LI+4G@u; z7KLHJXu8$|7l2BW=M|&rJ_AJLAdss|-g-Dd&S)cn)25Pxz;0j zAAl9$6YvGtu>EIXLsd(m4G@vzz$7qa&1e|d3GBBJsQEf<%~q#}=vXxL***h&1iqVr z?}4wt8&zFu7(ya~5sUz{z?hxx5bzBsSvf5G4CoZzfs~DR$z%@m}2L{*op)Bi%o z|D{+gdPb=0x(;y6nEy)*08do)q3ISp2SnrqaK%o>Nm%h3umiZSsxKQ6Wtwp)7K^K{ z>%Im~nkSqDmCpe4s`{$qDkLI%fq7q&#jSi5xS^`+QT2g!;F^8LjkaV3GQbk2s;bw4 z<(RzS0Lvz$8g%r|1m*6lP|sY~%>suaDx;nr&sFu#c9kZwPF$B4=YdNRl-WMuk*YpT zdKP*S(Ur8{cqKQ?SvQ=t?%65Iz+`=gjnx2)(R_>1IVdVXKJ0zDlMp%@Q8jXmSK&1RymfmMJ7RlNNHaz<2#8664H95#fe=$06vQx7mXHD!1W44Rz@`casE9pt2I)*i3X~bU;7EZ4 zLi(QiF5ReiyQ}><-P!M(*^%ws-tE2b?R#(EzEy-^Fc=I5gTY`h7z_r3!C){L323-oP94y z0c_SSn9^|e1>UM6NSMNqgqirSV!=dUGg=pn6wjZMIT?|OInbE1G}0Q-y5q?T=I_FX z@V!St17UNj-HE^=KOJgc*4G@F?iG;itpQ3Q2JdM^#JB$PS_Q{0hp@TD# zu8lQjXY5;6A$P=HlLr6K`O;@oV{xM z%+Ms5j%A#q3_?-{d2Gx1pFt=&1>FInH%qa+e5DIax*l1k7zT ze9h&A%{xOtMigW!Snr1`bT(e$Bj%MQfL6PC1WXJTMn0iicVtFAJ{E)PG(krSO;o*D zJA*t|bajrGOkZtjyxmF?K>Gt()4pXe15gNi9>=n;NqfW!`+uN|+DbOo9v}tO` znrLX(M{G{Qd!o7QT4AP;xT63wG7JGUWmVc6NgDN5XTn^xPbxuC32#ihVyd$U_;1G{ zNO%xH_&tM5V~UP#NRWzyb$!z;r>>$S3PFO;vJ~9jduAGDHX04oyuS^0qOhC< zYkD@IVL8c1oz}y3KZt+JTXe$0y3T`OQ3b?!W2zT3b<0zB>()?#DcB^B76toGv zP)F!%ftp0bchIm1>?8D@OF~D%JSLO#$l&?77<_|*gnmzj`B`MIB>38tx5=O2PE?1S zai7eEPGf82+`A-rmsChv64R7SqAAjHcIUj27au^W1)ISfqOF$pZ;Q$iEl$ah={d6K zpZKbh_UVLew(M#|LXB3Op-I4dFE-Xm_{YMo63^TAq&FuF_Amjv*b=K($bGV@I1}LW zcliB#;lDl~lmyT$ur2#W61vd;FML6Yj>cpn>INZV<|2!n?IL0!hx;S6hql26-0N4LG7IsA zP3J;=I8NsiaP=FLqr@3CkZOopLVcJn4F>)Y(#F2W10Mt5!YcD2y;OtGvo>&+4(cU} zYj(tHb)R{-2vHlfj$^8*l^;k`r}X=dMmwiO#>C*TcUmRg6zV;I^!bs2uR(HOR_2(h zB#1Q3!xzPjW1J^I!-{6z!q3EMXI5HE#AThtd>LwECiGVo)TM=*$@RVu=NX%{jB&Jy zj5tJ1XdwhR61VS>2qVF-1Ev1nP{%XJL7k^Bl>PgHek;j6?kxe=j{T=)G&_|cz~In? ziR~9mfE!#0m@onEuodDgbWzp4Ab@Omau>C~Dl;HWMKA#sGegrVyja!#0|!hI==K^g z6+N$l{bw*33 - - + + \ No newline at end of file diff --git a/OpenKeychain/src/main/res/layout/certify_key_activity.xml b/OpenKeychain/src/main/res/layout/certify_key_activity.xml index 0ae46a261..cba73a3c7 100644 --- a/OpenKeychain/src/main/res/layout/certify_key_activity.xml +++ b/OpenKeychain/src/main/res/layout/certify_key_activity.xml @@ -93,7 +93,7 @@ android:text="@string/label_fingerprint" /> @@ -111,7 +111,7 @@ android:text="@string/section_uids_to_sign" /> diff --git a/OpenKeychain/src/main/res/layout/import_keys_list_entry.xml b/OpenKeychain/src/main/res/layout/import_keys_list_entry.xml index ba8ff91ca..f5ec71abe 100644 --- a/OpenKeychain/src/main/res/layout/import_keys_list_entry.xml +++ b/OpenKeychain/src/main/res/layout/import_keys_list_entry.xml @@ -106,7 +106,7 @@ android:typeface="monospace" />