linked: redesign github resource creation, implement ouath flow (WIP)
This commit is contained in:
parent
9668abfe9e
commit
b52a0303ca
|
@ -113,7 +113,17 @@
|
|||
<activity
|
||||
android:name=".ui.linked.LinkedIdWizard"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|keyboard"
|
||||
android:label="@string/title_linked_create" />
|
||||
android:launchMode="singleInstance"
|
||||
android:label="@string/title_linked_create">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:host="linked"
|
||||
android:scheme="oauth-openkeychain" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.QrCodeViewActivity"
|
||||
android:label="@string/share_qr_code_dialog_title" />
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.graphics.PorterDuff;
|
|||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
@ -48,16 +49,19 @@ public abstract class LinkedIdCreateFinalFragment extends CryptoOperationFragmen
|
|||
|
||||
protected abstract View newView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
|
||||
|
||||
@Override
|
||||
@Override @NonNull
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
final View view = newView(inflater, container, savedInstanceState);
|
||||
|
||||
view.findViewById(R.id.next_button).setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
cryptoOperation();
|
||||
}
|
||||
});
|
||||
View nextButton = view.findViewById(R.id.next_button);
|
||||
if (nextButton != null) {
|
||||
nextButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
cryptoOperation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
view.findViewById(R.id.back_button).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,246 @@
|
|||
/*
|
||||
* Copyright (C) 2014 Dominik Schürmann <dominik@dominikschuermann.de>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.sufficientlysecure.keychain.ui.linked;
|
||||
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.URL;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ViewAnimator;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.sufficientlysecure.keychain.BuildConfig;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.ui.widget.StatusIndicator;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
|
||||
public class LinkedIdCreateGithubFragment extends Fragment {
|
||||
|
||||
ViewAnimator mProceedContainer;
|
||||
EditText mGithubUsername, mGithubPassword;
|
||||
|
||||
StatusIndicator mStatus1, mStatus2, mStatus3;
|
||||
|
||||
public static LinkedIdCreateGithubFragment newInstance() {
|
||||
return new LinkedIdCreateGithubFragment();
|
||||
}
|
||||
|
||||
@Override @NonNull
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.linked_create_github_fragment, container, false);
|
||||
|
||||
mProceedContainer = (ViewAnimator) view.findViewById(R.id.proceed_container);
|
||||
|
||||
mGithubUsername = (EditText) view.findViewById(R.id.username);
|
||||
mGithubPassword = (EditText) view.findViewById(R.id.password);
|
||||
|
||||
mStatus1 = (StatusIndicator) view.findViewById(R.id.linked_status_step1);
|
||||
mStatus2 = (StatusIndicator) view.findViewById(R.id.linked_status_step2);
|
||||
mStatus3 = (StatusIndicator) view.findViewById(R.id.linked_status_step3);
|
||||
|
||||
view.findViewById(R.id.button_send).setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
step1GetOAuthToken();
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
LinkedIdWizard wizard = (LinkedIdWizard) getActivity();
|
||||
final String oAuthCode = wizard.oAuthGetCode();
|
||||
final String oAuthState = wizard.oAuthGetState();
|
||||
if (oAuthCode == null) {
|
||||
Log.d(Constants.TAG, "no code");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(Constants.TAG, "got code: " + oAuthCode);
|
||||
|
||||
new AsyncTask<Void,Void,JSONObject>() {
|
||||
@Override
|
||||
protected JSONObject doInBackground(Void... dummy) {
|
||||
try {
|
||||
|
||||
JSONObject params = new JSONObject();
|
||||
params.put("client_id", "7a011b66275f244d3f21");
|
||||
params.put("client_secret", "eaced8a6655719d8c6848396de97b3f5d7a89fec");
|
||||
params.put("code", oAuthCode);
|
||||
params.put("state", oAuthState);
|
||||
|
||||
return jsonHttpRequest("https://github.com/login/oauth/access_token", params, null);
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "error in request", e);
|
||||
} catch (JSONException e) {
|
||||
throw new AssertionError("json error, this is a bug!");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(JSONObject result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
Log.d(Constants.TAG, "response: " + result);
|
||||
|
||||
if (result == null || !result.has("access_token")) {
|
||||
mStatus1.setDisplayedChild(3);
|
||||
return;
|
||||
}
|
||||
|
||||
mStatus1.setDisplayedChild(2);
|
||||
step2PostGist(result.optString("access_token"));
|
||||
|
||||
}
|
||||
}.execute();
|
||||
|
||||
}
|
||||
|
||||
private void step1GetOAuthToken() {
|
||||
|
||||
mStatus1.setDisplayedChild(1);
|
||||
mStatus2.setDisplayedChild(0);
|
||||
mStatus3.setDisplayedChild(0);
|
||||
|
||||
mProceedContainer.setDisplayedChild(1);
|
||||
|
||||
LinkedIdWizard wizard = (LinkedIdWizard) getActivity();
|
||||
wizard.oAuthRequest("github.com/login/oauth/authorize", "7a011b66275f244d3f21", "gist");
|
||||
|
||||
}
|
||||
|
||||
private void step2PostGist(final String accessToken) {
|
||||
|
||||
mStatus2.setDisplayedChild(1);
|
||||
|
||||
new AsyncTask<Void,Void,JSONObject>() {
|
||||
@Override
|
||||
protected JSONObject doInBackground(Void... dummy) {
|
||||
try {
|
||||
|
||||
JSONObject file = new JSONObject();
|
||||
file.put("content", "hello!");
|
||||
|
||||
JSONObject files = new JSONObject();
|
||||
files.put("file1.txt", file);
|
||||
|
||||
JSONObject params = new JSONObject();
|
||||
params.put("public", true);
|
||||
params.put("description", "OpenKeychain API Tests");
|
||||
params.put("files", files);
|
||||
|
||||
return jsonHttpRequest("https://api.github.com/gists", params, accessToken);
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "error in request", e);
|
||||
} catch (JSONException e) {
|
||||
throw new AssertionError("json error, this is a bug!");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(JSONObject result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
Log.d(Constants.TAG, "response: " + result);
|
||||
|
||||
if (result == null) {
|
||||
mStatus2.setDisplayedChild(3);
|
||||
return;
|
||||
}
|
||||
|
||||
mStatus2.setDisplayedChild(2);
|
||||
}
|
||||
}.execute();
|
||||
|
||||
}
|
||||
|
||||
private static JSONObject jsonHttpRequest(String url, JSONObject params, String accessToken)
|
||||
throws IOException {
|
||||
|
||||
HttpsURLConnection nection = (HttpsURLConnection) new URL(url).openConnection();
|
||||
nection.setDoInput(true);
|
||||
nection.setDoOutput(true);
|
||||
nection.setRequestProperty("Content-Type", "application/json");
|
||||
nection.setRequestProperty("Accept", "application/json");
|
||||
nection.setRequestProperty("User-Agent", "OpenKeychain " + BuildConfig.VERSION_NAME);
|
||||
if (accessToken != null) {
|
||||
nection.setRequestProperty("Authorization", "token " + accessToken);
|
||||
}
|
||||
|
||||
OutputStream os = nection.getOutputStream();
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
|
||||
writer.write(params.toString());
|
||||
writer.flush();
|
||||
writer.close();
|
||||
os.close();
|
||||
|
||||
try {
|
||||
|
||||
nection.connect();
|
||||
InputStream in = new BufferedInputStream(nection.getInputStream());
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
|
||||
StringBuilder response = new StringBuilder();
|
||||
while (true) {
|
||||
String line = reader.readLine();
|
||||
if (line == null) {
|
||||
break;
|
||||
}
|
||||
response.append(line);
|
||||
}
|
||||
|
||||
try {
|
||||
return new JSONObject(response.toString());
|
||||
} catch (JSONException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
} finally {
|
||||
nection.disconnect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -84,8 +84,8 @@ public class LinkedIdSelectFragment extends Fragment {
|
|||
.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LinkedIdCreateGithubStep1Fragment frag =
|
||||
LinkedIdCreateGithubStep1Fragment.newInstance();
|
||||
LinkedIdCreateGithubFragment frag =
|
||||
LinkedIdCreateGithubFragment.newInstance();
|
||||
|
||||
mLinkedIdWizard.loadFragment(null, frag, LinkedIdWizard.FRAG_ACTION_TO_RIGHT);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,10 @@
|
|||
|
||||
package org.sufficientlysecure.keychain.ui.linked;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
@ -25,6 +28,7 @@ import android.support.v4.app.FragmentTransaction;
|
|||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
import org.sufficientlysecure.keychain.Constants;
|
||||
import org.sufficientlysecure.keychain.R;
|
||||
import org.sufficientlysecure.keychain.pgp.exception.PgpKeyNotFoundException;
|
||||
|
@ -32,6 +36,8 @@ import org.sufficientlysecure.keychain.provider.CachedPublicKeyRing;
|
|||
import org.sufficientlysecure.keychain.provider.KeychainContract;
|
||||
import org.sufficientlysecure.keychain.provider.ProviderHelper;
|
||||
import org.sufficientlysecure.keychain.ui.base.BaseActivity;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify;
|
||||
import org.sufficientlysecure.keychain.ui.util.Notify.Style;
|
||||
import org.sufficientlysecure.keychain.util.Log;
|
||||
|
||||
public class LinkedIdWizard extends BaseActivity {
|
||||
|
@ -125,4 +131,55 @@ public class LinkedIdWizard extends BaseActivity {
|
|||
inputManager.hideSoftInputFromWindow(v.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
private String mOAuthCode, mOAuthState;
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
Uri uri = intent.getData();
|
||||
if (uri != null) {
|
||||
Log.d(Constants.TAG, "received oauth uri: " + uri);
|
||||
String state = uri.getQueryParameter("state");
|
||||
if (!mOAuthState.equalsIgnoreCase(state)) {
|
||||
Notify.create(this, "Authentication Error!", Style.ERROR).show();
|
||||
return;
|
||||
}
|
||||
mOAuthCode = uri.getQueryParameter("code");
|
||||
} else {
|
||||
Log.d(Constants.TAG, "received oauth uri: null");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public String oAuthGetCode() {
|
||||
try {
|
||||
return mOAuthCode;
|
||||
} finally {
|
||||
mOAuthCode = null;
|
||||
}
|
||||
}
|
||||
|
||||
public String oAuthGetState() {
|
||||
return mOAuthState;
|
||||
}
|
||||
|
||||
public void oAuthRequest(String hostAndPath, String clientId, String scope) {
|
||||
|
||||
byte[] buf = new byte[16];
|
||||
new Random().nextBytes(buf);
|
||||
mOAuthState = new String(Hex.encode(buf));
|
||||
|
||||
Intent intent = new Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse("https://" + hostAndPath +
|
||||
"?client_id=" + clientId +
|
||||
"&scope=" + scope +
|
||||
"&redirect_uri=oauth-openkeychain://linked/" +
|
||||
"&state=" + mOAuthState));
|
||||
|
||||
startActivity(intent);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:custom="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="false"
|
||||
android:layout_above="@+id/create_key_button_divider">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_margin="8dp"
|
||||
android:src="@drawable/linked_github" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="This login data will be used to post a gist, verifying this account and key belongs to you!"
|
||||
style="?android:textAppearanceSmall"/>
|
||||
|
||||
<org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/proceed_container"
|
||||
custom:initialView="1">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
style="?android:buttonBarButtonStyle"
|
||||
android:drawableLeft="@drawable/ic_mode_edit_grey_24dp"
|
||||
android:drawableStart="@drawable/ic_mode_edit_grey_24dp"
|
||||
android:drawablePadding="12dp"
|
||||
android:text="Post Gist & Link Key"
|
||||
android:id="@+id/button_send"
|
||||
/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<org.sufficientlysecure.keychain.ui.widget.StatusIndicator
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/linked_status_step1"
|
||||
android:layout_margin="4dp">
|
||||
</org.sufficientlysecure.keychain.ui.widget.StatusIndicator>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="Login at GitHub…"
|
||||
style="?android:textAppearanceMedium"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<org.sufficientlysecure.keychain.ui.widget.StatusIndicator
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/linked_status_step2"
|
||||
android:layout_margin="4dp">
|
||||
</org.sufficientlysecure.keychain.ui.widget.StatusIndicator>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="Post Gist…"
|
||||
style="?android:textAppearanceMedium"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<org.sufficientlysecure.keychain.ui.widget.StatusIndicator
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:id="@+id/linked_status_step3"
|
||||
android:layout_margin="4dp">
|
||||
</org.sufficientlysecure.keychain.ui.widget.StatusIndicator>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="Update Key…"
|
||||
style="?android:textAppearanceMedium"
|
||||
/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</org.sufficientlysecure.keychain.ui.widget.ToolableViewAnimator>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<View
|
||||
android:id="@+id/create_key_button_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dip"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:background="?android:attr/listDivider"
|
||||
android:layout_alignTop="@+id/create_key_buttons"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:id="@+id/create_key_buttons">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/back_button"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/btn_cancel"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:textAllCaps="true"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:drawableLeft="@drawable/ic_chevron_left_grey_24dp"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:layout_gravity="center_vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
|
@ -4,8 +4,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:showIn="@layout/linked_create_https_fragment_step2">
|
||||
tools:showIn="@layout/linked_create_github_fragment_step2">
|
||||
|
||||
<ViewAnimator
|
||||
android:id="@+id/verify_progress"
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
<string name="btn_export_to_server">"Upload To Keyserver"</string>
|
||||
<string name="btn_next">"Next"</string>
|
||||
<string name="btn_back">"Back"</string>
|
||||
<string name="btn_cancel">"Cancel"</string>
|
||||
<string name="btn_no">"No"</string>
|
||||
<string name="btn_match">"Fingerprints match"</string>
|
||||
<string name="btn_share_encrypted_signed">"Encrypt/sign and share text"</string>
|
||||
|
|
Loading…
Reference in a new issue