support for handling keyserver urls from browser, added documentation

This commit is contained in:
Adithya Abraham Philip 2015-04-19 13:31:37 +05:30
parent f2d3abd4c9
commit 91443c2f43
8 changed files with 167 additions and 37 deletions

View File

@ -624,6 +624,23 @@
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.bin" />
</intent-filter>
<!-- VIEW from keyserver urls opened in a browser -->
<intent-filter android:label="@string/intent_import_key">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https"/>
<data android:scheme="http"/>
<!-- if we don't specify a host, pathPattern will be ignored-->
<data android:host="*"/>
<!-- convention for keyserver paths specified by internet draft
draft-shaw-openpgp-hkp-00.txt
(http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00#section-3) -->
<data android:pathPattern="/pks/lookup.*"/>
</intent-filter>
<!-- Keychain's own Actions -->
<!-- IMPORT_KEY with files TODO: does this work? -->
<intent-filter android:label="@string/intent_import_key">

View File

@ -123,7 +123,8 @@ public class CreateKeyYubiImportFragment extends Fragment implements NfcListener
});
}
mListFragment = ImportKeysListFragment.newInstance(null, null, "0x" + mNfcFingerprint, true);
mListFragment = ImportKeysListFragment.newInstance(null, null,
"0x" + mNfcFingerprint, true, null);
view.findViewById(R.id.button_search).setOnClickListener(new OnClickListener() {
@Override

View File

@ -39,6 +39,7 @@ import org.sufficientlysecure.keychain.ui.base.BaseNfcActivity;
import org.sufficientlysecure.keychain.service.CloudImportService;
import org.sufficientlysecure.keychain.service.ServiceProgressHandler;
import org.sufficientlysecure.keychain.ui.dialog.ProgressDialogFragment;
import org.sufficientlysecure.keychain.ui.util.FormattingUtils;
import org.sufficientlysecure.keychain.ui.util.KeyFormattingUtils;
import org.sufficientlysecure.keychain.ui.util.Notify;
import org.sufficientlysecure.keychain.util.Log;
@ -62,6 +63,8 @@ public class ImportKeysActivity extends BaseNfcActivity {
// Actions for internal use only:
public static final String ACTION_IMPORT_KEY_FROM_FILE = Constants.INTENT_PREFIX
+ "IMPORT_KEY_FROM_FILE";
public static final String ACTION_SEARCH_KEYSERVER_FROM_URL = Constants.INTENT_PREFIX
+ "SEARCH_KEYSERVER_FROM_URL";
public static final String EXTRA_RESULT = "result";
// only used by ACTION_IMPORT_KEY
@ -112,16 +115,20 @@ public class ImportKeysActivity extends BaseNfcActivity {
}
if (action == null) {
startCloudFragment(savedInstanceState, null, false);
startListFragment(savedInstanceState, null, null, null);
startCloudFragment(savedInstanceState, null, false, null);
startListFragment(savedInstanceState, null, null, null, null);
return;
}
if (Intent.ACTION_VIEW.equals(action)) {
if (scheme.equals("http") || scheme.equals("https")) {
action = ACTION_SEARCH_KEYSERVER_FROM_URL;
} else {
// Android's Action when opening file associated to Keychain (see AndroidManifest.xml)
// delegate action to ACTION_IMPORT_KEY
action = ACTION_IMPORT_KEY;
}
}
switch (action) {
case ACTION_IMPORT_KEY: {
@ -130,12 +137,12 @@ public class ImportKeysActivity extends BaseNfcActivity {
if (dataUri != null) {
// action: directly load data
startListFragment(savedInstanceState, null, dataUri, null);
startListFragment(savedInstanceState, null, dataUri, null, null);
} else if (extras.containsKey(EXTRA_KEY_BYTES)) {
byte[] importData = extras.getByteArray(EXTRA_KEY_BYTES);
// action: directly load data
startListFragment(savedInstanceState, importData, null, null);
startListFragment(savedInstanceState, importData, null, null, null);
}
break;
}
@ -162,10 +169,10 @@ public class ImportKeysActivity extends BaseNfcActivity {
if (query != null && query.length() > 0) {
// display keyserver fragment with query
startCloudFragment(savedInstanceState, query, false);
startCloudFragment(savedInstanceState, query, false, null);
// action: search immediately
startListFragment(savedInstanceState, null, null, query);
startListFragment(savedInstanceState, null, null, query, null);
} else {
Log.e(Constants.TAG, "Query is empty!");
return;
@ -181,10 +188,10 @@ public class ImportKeysActivity extends BaseNfcActivity {
String query = "0x" + fingerprint;
// display keyserver fragment with query
startCloudFragment(savedInstanceState, query, true);
startCloudFragment(savedInstanceState, query, true, null);
// action: search immediately
startListFragment(savedInstanceState, null, null, query);
startListFragment(savedInstanceState, null, null, query, null);
}
} else {
Log.e(Constants.TAG,
@ -200,7 +207,29 @@ public class ImportKeysActivity extends BaseNfcActivity {
startFileFragment(savedInstanceState);
// no immediate actions!
startListFragment(savedInstanceState, null, null, null);
startListFragment(savedInstanceState, null, null, null, null);
break;
}
case ACTION_SEARCH_KEYSERVER_FROM_URL: {
// need to process URL to get search query and keyserver authority
String query = dataUri.getQueryParameter("search");
String keyserver = dataUri.getAuthority();
// if query not specified, we still allow users to search the keyserver in the link
if (query == null) {
Notify.create(this, R.string.import_url_warn_no_search_parameter, Notify.LENGTH_INDEFINITE,
Notify.Style.WARN).show(mTopFragment);
// we just set the keyserver
startCloudFragment(savedInstanceState, null, false, keyserver);
// it's not necessary to set the keyserver for ImportKeysListFragment since
// it'll be taken care of by ImportKeysCloudFragment when the user clicks
// the search button
startListFragment(savedInstanceState, null, null, null, null);
} else {
// we allow our users to edit the query if they wish
startCloudFragment(savedInstanceState, query, false, keyserver);
// search immediately
startListFragment(savedInstanceState, null, null, query, keyserver);
}
break;
}
case ACTION_IMPORT_KEY_FROM_FILE_AND_RETURN: {
@ -208,18 +237,31 @@ public class ImportKeysActivity extends BaseNfcActivity {
startFileFragment(savedInstanceState);
// no immediate actions!
startListFragment(savedInstanceState, null, null, null);
startListFragment(savedInstanceState, null, null, null, null);
break;
}
default: {
startCloudFragment(savedInstanceState, null, false);
startListFragment(savedInstanceState, null, null, null);
startCloudFragment(savedInstanceState, null, false, null);
startListFragment(savedInstanceState, null, null, null, null);
break;
}
}
}
private void startListFragment(Bundle savedInstanceState, byte[] bytes, Uri dataUri, String serverQuery) {
/**
* if the fragment is started with non-null bytes/dataUri/serverQuery, it will immediately
* load content
*
* @param savedInstanceState
* @param bytes bytes containing list of keyrings to import
* @param dataUri uri to file to import keyrings from
* @param serverQuery query to search for on the keyserver
* @param keyserver keyserver authority to search on. If null will use keyserver from
* user preferences
*/
private void startListFragment(Bundle savedInstanceState, byte[] bytes, Uri dataUri,
String serverQuery, String keyserver) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
@ -227,8 +269,8 @@ public class ImportKeysActivity extends BaseNfcActivity {
return;
}
// Create an instance of the fragment
mListFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery);
mListFragment = ImportKeysListFragment.newInstance(bytes, dataUri, serverQuery, false,
keyserver);
// Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!
@ -259,7 +301,18 @@ public class ImportKeysActivity extends BaseNfcActivity {
getSupportFragmentManager().executePendingTransactions();
}
private void startCloudFragment(Bundle savedInstanceState, String query, boolean disableQueryEdit) {
/**
* loads the CloudFragment, which consists of the search bar, search button and settings icon
* visually.
*
* @param savedInstanceState
* @param query search query
* @param disableQueryEdit if true, user will not be able to edit the search query
* @param keyserver keyserver authority to use for search, if null will use keyserver
* specified in user preferences
*/
private void startCloudFragment(Bundle savedInstanceState, String query, boolean disableQueryEdit, String keyserver) {
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
@ -268,7 +321,7 @@ public class ImportKeysActivity extends BaseNfcActivity {
}
// Create an instance of the fragment
mTopFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit);
mTopFragment = ImportKeysCloudFragment.newInstance(query, disableQueryEdit, keyserver);
// Add the fragment to the 'fragment_container' FrameLayout
// NOTE: We use commitAllowingStateLoss() to prevent weird crashes!

View File

@ -45,6 +45,7 @@ import java.util.List;
public class ImportKeysCloudFragment extends Fragment {
public static final String ARG_QUERY = "query";
public static final String ARG_DISABLE_QUERY_EDIT = "disable_query_edit";
public static final String ARG_KEYSERVER = "keyserver";
private ImportKeysActivity mImportActivity;
@ -54,13 +55,20 @@ public class ImportKeysCloudFragment extends Fragment {
/**
* Creates new instance of this fragment
*
* @param query query to search for
* @param disableQueryEdit if true, user cannot edit query
* @param keyserver specified keyserver authority to use. If null, will use keyserver
* specified in user preferences
*/
public static ImportKeysCloudFragment newInstance(String query, boolean disableQueryEdit) {
public static ImportKeysCloudFragment newInstance(String query, boolean disableQueryEdit,
String keyserver) {
ImportKeysCloudFragment frag = new ImportKeysCloudFragment();
Bundle args = new Bundle();
args.putString(ARG_QUERY, query);
args.putBoolean(ARG_DISABLE_QUERY_EDIT, disableQueryEdit);
args.putString(ARG_KEYSERVER, keyserver);
frag.setArguments(args);
@ -151,8 +159,17 @@ public class ImportKeysCloudFragment extends Fragment {
}
private void search(String query) {
Preferences prefs = Preferences.getPreferences(getActivity());
mImportActivity.loadCallback(new ImportKeysListFragment.CloudLoaderState(query, prefs.getCloudSearchPrefs()));
Preferences.CloudSearchPrefs cloudSearchPrefs;
String explicitKeyserver = getArguments().getString(ARG_KEYSERVER);
// no explicit keyserver passed
if (explicitKeyserver == null) {
cloudSearchPrefs = Preferences.getPreferences(getActivity()).getCloudSearchPrefs();
} else {
// assume we are also meant to search keybase.io
cloudSearchPrefs = new Preferences.CloudSearchPrefs(true, true, explicitKeyserver);
}
mImportActivity.loadCallback(
new ImportKeysListFragment.CloudLoaderState(query, cloudSearchPrefs));
toggleKeyboard(false);
}

View File

@ -58,6 +58,7 @@ public class ImportKeysListFragment extends ListFragment implements
private static final String ARG_BYTES = "bytes";
public static final String ARG_SERVER_QUERY = "query";
public static final String ARG_NON_INTERACTIVE = "non_interactive";
public static final String ARG_KEYSERVER_URL = "keyserver_url";
private Activity mActivity;
private ImportKeysAdapter mAdapter;
@ -78,7 +79,8 @@ public class ImportKeysListFragment extends ListFragment implements
return mAdapter.getData();
}
/** Returns an Iterator (with size) of the selected data items.
/**
* Returns an Iterator (with size) of the selected data items.
* This iterator is sort of a tradeoff, it's slightly more complex than an
* ArrayList would have been, but we save some memory by just returning
* relevant elements on demand.
@ -121,12 +123,36 @@ public class ImportKeysListFragment extends ListFragment implements
}
public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery) {
return newInstance(bytes, dataUri, serverQuery, false);
/**
* Creates an interactive ImportKeyListFragment which reads keyrings from bytes, or file specified
* by dataUri, or searches a keyserver for serverQuery, if parameter is not null, in that order
*
* @param bytes byte data containing list of keyrings to be imported
* @param dataUri file from which keyrings are to be imported
* @param serverQuery query to search for on keyserver
* @param keyserver if not null, will perform search on specified keyserver. Else, uses
* keyserver specified in user preferences
* @return fragment with arguments set based on passed parameters
*/
public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery,
String keyserver) {
return newInstance(bytes, dataUri, serverQuery, false, keyserver);
}
public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri,
String serverQuery, boolean nonInteractive) {
/**
* Visually consists of a list of keyrings with checkboxes to specify which are to be imported
* Can immediately load keyrings specified by any of its parameters
*
* @param bytes byte data containing list of keyrings to be imported
* @param dataUri file from which keyrings are to be imported
* @param serverQuery query to search for on keyserver
* @param nonInteractive if true, users will not be able to check/uncheck items in the list
* @param keyserver if set, will perform search on specified keyserver. If null, falls back
* to keyserver specified in user preferences
* @return fragment with arguments set based on passed parameters
*/
public static ImportKeysListFragment newInstance(byte[] bytes, Uri dataUri, String serverQuery,
boolean nonInteractive, String keyserver) {
ImportKeysListFragment frag = new ImportKeysListFragment();
Bundle args = new Bundle();
@ -134,6 +160,7 @@ public class ImportKeysListFragment extends ListFragment implements
args.putParcelable(ARG_DATA_URI, dataUri);
args.putString(ARG_SERVER_QUERY, serverQuery);
args.putBoolean(ARG_NON_INTERACTIVE, nonInteractive);
args.putString(ARG_KEYSERVER_URL, keyserver);
frag.setArguments(args);
@ -180,16 +207,23 @@ public class ImportKeysListFragment extends ListFragment implements
setListAdapter(mAdapter);
Bundle args = getArguments();
Uri dataUri = args.containsKey(ARG_DATA_URI) ? args.<Uri>getParcelable(ARG_DATA_URI) : null;
byte[] bytes = args.containsKey(ARG_BYTES) ? args.getByteArray(ARG_BYTES) : null;
String query = args.containsKey(ARG_SERVER_QUERY) ? args.getString(ARG_SERVER_QUERY) : null;
mNonInteractive = args.containsKey(ARG_NON_INTERACTIVE) ? args.getBoolean(ARG_NON_INTERACTIVE) : false;
Uri dataUri = args.getParcelable(ARG_DATA_URI);
byte[] bytes = args.getByteArray(ARG_BYTES);
String query = args.getString(ARG_SERVER_QUERY);
String keyserver = args.getString(ARG_KEYSERVER_URL);
mNonInteractive = args.getBoolean(ARG_NON_INTERACTIVE, false);
if (dataUri != null || bytes != null) {
mLoaderState = new BytesLoaderState(bytes, dataUri);
} else if (query != null) {
Preferences prefs = Preferences.getPreferences(getActivity());
mLoaderState = new CloudLoaderState(query, prefs.getCloudSearchPrefs());
Preferences.CloudSearchPrefs cloudSearchPrefs;
if (keyserver != null) {
cloudSearchPrefs = Preferences.getPreferences(getActivity()).getCloudSearchPrefs();
} else {
cloudSearchPrefs = new Preferences.CloudSearchPrefs(true, true, keyserver);
}
mLoaderState = new CloudLoaderState(query, cloudSearchPrefs);
}
getListView().setOnTouchListener(new OnTouchListener() {

View File

@ -82,12 +82,12 @@ public class ImportKeysListLoader
@Override
protected void onStartLoading() {
forceLoad();
super.forceLoad();
}
@Override
protected void onStopLoading() {
cancelLoad();
super.cancelLoad();
}
@Override

View File

@ -193,6 +193,11 @@ public class Preferences {
public final boolean searchKeybase;
public final String keyserver;
/**
* @param searchKeyserver should passed keyserver be searched
* @param searchKeybase should keybase.io be searched
* @param keyserver the keyserver url authority to search on
*/
public CloudSearchPrefs(boolean searchKeyserver, boolean searchKeybase, String keyserver) {
this.searchKeyserver = searchKeyserver;
this.searchKeybase = searchKeybase;

View File

@ -404,6 +404,9 @@
<string name="import_qr_code_button">"Scan QR Code"</string>
<string name="import_qr_code_text">"Place your camera over the QR Code!"</string>
<!-- Import from URL -->
<string name="import_url_warn_no_search_parameter">"No search parameter found. You may still attempt manually searching the keyserver."</string>
<!-- Generic result toast -->
<string name="view_log">"Details"</string>
<string name="with_warnings">", with warnings"</string>