mirror of
https://github.com/emanuele-f/PCAPdroid.git
synced 2026-07-03 21:21:12 +08:00
Implement license activation via QR code
It's now possible to use a QR code to activate licenses on non-Play builds of the app. This comes handy on TV devices
This commit is contained in:
parent
2b57c40dfe
commit
c4d8a09e1d
@ -87,4 +87,5 @@ dependencies {
|
||||
implementation 'cat.ereza:customactivityoncrash:2.3.0'
|
||||
implementation 'com.github.KaKaVip:Android-Flag-Kit:v0.1'
|
||||
implementation 'com.github.AppIntro:AppIntro:6.2.0'
|
||||
implementation 'com.github.androidmads:QRGenerator:1.0.1'
|
||||
}
|
||||
|
||||
@ -115,13 +115,18 @@ public class Billing {
|
||||
public void connectBilling() {}
|
||||
public void disconnectBilling() {}
|
||||
|
||||
public void setLicense(String license) {
|
||||
if(!isValidLicense(license))
|
||||
public boolean setLicense(String license) {
|
||||
boolean valid = true;
|
||||
if(!isValidLicense(license)) {
|
||||
license = "";
|
||||
valid = false;
|
||||
}
|
||||
|
||||
mPrefs.edit()
|
||||
.putString("license", license)
|
||||
.apply();
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
public boolean isValidLicense(String license) {
|
||||
|
||||
@ -60,6 +60,7 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.provider.Settings;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannedString;
|
||||
import android.text.TextUtils;
|
||||
@ -139,6 +140,7 @@ import javax.net.ssl.HttpsURLConnection;
|
||||
public class Utils {
|
||||
static final String TAG = "Utils";
|
||||
public static final String INTERACT_ACROSS_USERS = "android.permission.INTERACT_ACROSS_USERS";
|
||||
public static final String PCAPDROID_WEBSITE = "https://pcapdroid.org";
|
||||
public static final int PER_USER_RANGE = 100000;
|
||||
public static final int UID_UNKNOWN = -1;
|
||||
public static final int UID_NO_FILTER = -2;
|
||||
@ -1410,6 +1412,10 @@ public class Utils {
|
||||
"OS version: " + getOsVersion() + "\n";
|
||||
}
|
||||
|
||||
public static String getDeviceName(Context ctx) {
|
||||
return Settings.Secure.getString(ctx.getContentResolver(), "bluetooth_name");
|
||||
}
|
||||
|
||||
public static String getAppVersionString() {
|
||||
return "PCAPdroid v" + BuildConfig.VERSION_NAME;
|
||||
}
|
||||
|
||||
@ -20,30 +20,66 @@
|
||||
package com.emanuelef.remote_capture.activities;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Point;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Display;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.core.view.MenuProvider;
|
||||
|
||||
import com.emanuelef.remote_capture.Billing;
|
||||
import com.emanuelef.remote_capture.Log;
|
||||
import com.emanuelef.remote_capture.R;
|
||||
import com.emanuelef.remote_capture.Utils;
|
||||
import com.emanuelef.remote_capture.model.Prefs;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
import androidmads.library.qrgenearator.QRGContents;
|
||||
import androidmads.library.qrgenearator.QRGEncoder;
|
||||
|
||||
public class AboutActivity extends BaseActivity implements MenuProvider {
|
||||
private static final String TAG = "AboutActivity";
|
||||
private ExecutorService mQrReqExecutor;
|
||||
private HttpsURLConnection mQrCon;
|
||||
private boolean mDialogClosing = false;
|
||||
private long mQrStartTime = 0;
|
||||
private long mQrDeadline = 0;
|
||||
private Handler mHandler;
|
||||
private AlertDialog mLicenseDialog;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@ -52,6 +88,7 @@ public class AboutActivity extends BaseActivity implements MenuProvider {
|
||||
setContentView(R.layout.about_activity);
|
||||
addMenuProvider(this);
|
||||
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
TextView appVersion = findViewById(R.id.app_version);
|
||||
appVersion.setText("PCAPdroid " + Utils.getAppVersion(this));
|
||||
|
||||
@ -64,6 +101,25 @@ public class AboutActivity extends BaseActivity implements MenuProvider {
|
||||
sourceLink.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
stopQrExecutor();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
private void stopQrExecutor() {
|
||||
// necessary to interrupt the executor thread
|
||||
if(mQrCon != null)
|
||||
mQrCon.disconnect();
|
||||
mQrCon = null;
|
||||
|
||||
if(mQrReqExecutor != null)
|
||||
mQrReqExecutor.shutdownNow();
|
||||
mQrReqExecutor = null;
|
||||
|
||||
mHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateMenu(@NonNull Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.about_menu, menu);
|
||||
@ -107,13 +163,20 @@ public class AboutActivity extends BaseActivity implements MenuProvider {
|
||||
private void showLicenseDialog() {
|
||||
Billing billing = Billing.newInstance(this);
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
View content = inflater.inflate(R.layout.license_dialog, null);
|
||||
final View content = inflater.inflate(R.layout.license_dialog, null);
|
||||
|
||||
String instId = billing.getInstallationId();
|
||||
TextView instIdText = content.findViewById(R.id.installation_id);
|
||||
instIdText.setText(instId);
|
||||
if(Utils.isTv(this))
|
||||
|
||||
mDialogClosing = false;
|
||||
final View showQr = content.findViewById(R.id.show_qr_code);
|
||||
showQr.setOnClickListener(v -> showQrCode(content, instId));
|
||||
|
||||
if(Utils.isTv(this) && !billing.isPurchased(Billing.SUPPORTER_SKU)) {
|
||||
instIdText.setOnClickListener(v -> Utils.shareText(this, getString(R.string.installation_id), instId));
|
||||
showQrCode(content, instId);
|
||||
}
|
||||
|
||||
TextView validationRc = content.findViewById(R.id.validation_rc);
|
||||
EditText licenseCode = content.findViewById(R.id.license_code);
|
||||
@ -122,25 +185,208 @@ public class AboutActivity extends BaseActivity implements MenuProvider {
|
||||
|
||||
content.findViewById(R.id.copy_id).setOnClickListener(v -> Utils.copyToClipboard(this, instId));
|
||||
|
||||
boolean was_valid = billing.isPurchased(Billing.SUPPORTER_SKU);
|
||||
|
||||
AlertDialog myDialog = new AlertDialog.Builder(this)
|
||||
mLicenseDialog = new AlertDialog.Builder(this)
|
||||
.setView(content)
|
||||
.setPositiveButton(R.string.ok, (dialog, whichButton) -> {
|
||||
boolean was_valid = billing.isPurchased(Billing.SUPPORTER_SKU);
|
||||
billing.setLicense(licenseCode.getText().toString());
|
||||
|
||||
if(!was_valid && billing.isPurchased(Billing.SUPPORTER_SKU))
|
||||
Utils.showToastLong(this, R.string.paid_features_unlocked);
|
||||
})
|
||||
.setOnDismissListener(dialog -> {
|
||||
mDialogClosing = true;
|
||||
mLicenseDialog = null;
|
||||
stopQrExecutor();
|
||||
})
|
||||
.setNeutralButton(R.string.validate, (dialog, which) -> {}) // see below
|
||||
.create();
|
||||
|
||||
myDialog.show();
|
||||
myDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
|
||||
mLicenseDialog.show();
|
||||
mLicenseDialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> {
|
||||
boolean valid = billing.isValidLicense(licenseCode.getText().toString());
|
||||
validationRc.setText(valid ? R.string.valid : R.string.invalid);
|
||||
validationRc.setTextColor(ContextCompat.getColor(this, valid ? R.color.ok : R.color.danger));
|
||||
});
|
||||
myDialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
mLicenseDialog.getWindow().setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
|
||||
private void showQrCode(View dialog, String instId) {
|
||||
View qrBox = dialog.findViewById(R.id.qr_box);
|
||||
View qrLoading = dialog.findViewById(R.id.qr_code_loading);
|
||||
View showQr = dialog.findViewById(R.id.show_qr_code);
|
||||
View qrInfo = dialog.findViewById(R.id.qr_info_text);
|
||||
|
||||
showQr.setVisibility(View.GONE);
|
||||
qrLoading.setVisibility(View.VISIBLE);
|
||||
qrBox.setVisibility(View.GONE);
|
||||
qrInfo.setVisibility(View.GONE);
|
||||
|
||||
mQrReqExecutor = Executors.newSingleThreadExecutor();
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
// start activation
|
||||
mQrReqExecutor.execute(() -> {
|
||||
try {
|
||||
URL url = new URL(Utils.PCAPDROID_WEBSITE + "/getlicense/qr_activation");
|
||||
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
|
||||
mQrCon = con;
|
||||
|
||||
try {
|
||||
con.setRequestProperty("User-Agent", Utils.getAppVersionString());
|
||||
con.setRequestMethod("POST");
|
||||
con.setUseCaches(false);
|
||||
con.setAllowUserInteraction(false);
|
||||
con.setDoInput(true);
|
||||
con.setDoOutput(true);
|
||||
con.setConnectTimeout(5000);
|
||||
|
||||
// Send POST request
|
||||
try (BufferedOutputStream os = new BufferedOutputStream(con.getOutputStream())) {
|
||||
os.write(("installation_id=" + instId).getBytes());
|
||||
}
|
||||
|
||||
int rc = con.getResponseCode();
|
||||
Log.d(TAG, "QR HTTP response: " + rc);
|
||||
if (rc != 200) {
|
||||
handler.post(() ->
|
||||
hideQrCode(dialog, "QR request failed with code " + rc));
|
||||
return;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(con.getInputStream()))) {
|
||||
// Step 1: get QR request ID
|
||||
String timeout_s = parseSseLine(reader.readLine());
|
||||
String qr_req_id = parseSseLine(reader.readLine());
|
||||
if ((qr_req_id == null) || (timeout_s == null)) {
|
||||
handler.post(() ->
|
||||
hideQrCode(dialog, "Invalid QR request ID"));
|
||||
return;
|
||||
}
|
||||
int timeout_ms = Integer.parseInt(timeout_s) * 1000;
|
||||
long deadline = SystemClock.uptimeMillis() + timeout_ms;
|
||||
Log.d(TAG, "QR request_id=" + qr_req_id + ", timeout=" + timeout_ms + " ms");
|
||||
|
||||
// Step 2: generate QR code
|
||||
Bitmap qrBitmap = genQrCode(instId, qr_req_id);
|
||||
handler.post(() -> onQrRequestReady(dialog, qrBitmap, deadline));
|
||||
|
||||
// Step 3: wait license
|
||||
String license = parseSseLine(reader.readLine());
|
||||
if(license == null) {
|
||||
handler.post(() ->
|
||||
hideQrCode(dialog, getString(R.string.qr_code_expired)));
|
||||
return;
|
||||
}
|
||||
handler.post(() -> onQrLicenseReceived(dialog, license));
|
||||
}
|
||||
} finally {
|
||||
con.disconnect();
|
||||
}
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
handler.post(() -> {
|
||||
if(e instanceof EOFException)
|
||||
hideQrCode(dialog, getString(R.string.qr_code_expired));
|
||||
else
|
||||
hideQrCode(dialog, getString(R.string.connection_error, e.getMessage()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String parseSseLine(String line) {
|
||||
if(line == null)
|
||||
return null;
|
||||
|
||||
if(line.startsWith("data: "))
|
||||
line = line.substring(6);
|
||||
return line;
|
||||
}
|
||||
|
||||
private Bitmap genQrCode(String instId, String qrReqId) {
|
||||
float maxDp = 180f;
|
||||
int maxPx = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
maxDp,
|
||||
getResources().getDisplayMetrics()
|
||||
);
|
||||
|
||||
WindowManager manager = (WindowManager) getSystemService(WINDOW_SERVICE);
|
||||
Display display = manager.getDefaultDisplay();
|
||||
Point point = new Point();
|
||||
display.getSize(point);
|
||||
int smallerDimension = Math.min(Math.min(point.x, point.y) / 2, maxPx);
|
||||
|
||||
String qrData = "pcapdroid://get_license?installation_id="+ instId +"&qr_request_id=" + qrReqId + "&device=" + Uri.encode(Utils.getDeviceName(this));
|
||||
Log.d(TAG, "QR activation URI: " + qrData);
|
||||
|
||||
QRGEncoder qrgEncoder = new QRGEncoder(qrData, null, QRGContents.Type.TEXT, smallerDimension);
|
||||
return qrgEncoder.getBitmap(0);
|
||||
}
|
||||
|
||||
private void onQrRequestReady(View dialog, Bitmap qrcode, long deadline) {
|
||||
View qrBox = dialog.findViewById(R.id.qr_box);
|
||||
ImageView qrImage = dialog.findViewById(R.id.qr_code);
|
||||
View qrLoading = dialog.findViewById(R.id.qr_code_loading);
|
||||
View qrInfo = dialog.findViewById(R.id.qr_info_text);
|
||||
|
||||
mQrStartTime = SystemClock.uptimeMillis();
|
||||
mQrDeadline = deadline;
|
||||
updateQrProgress(dialog);
|
||||
|
||||
qrImage.setImageBitmap(qrcode);
|
||||
qrBox.setVisibility(View.VISIBLE);
|
||||
qrInfo.setVisibility(View.VISIBLE);
|
||||
qrLoading.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void updateQrProgress(View dialog) {
|
||||
ProgressBar qrProgress = dialog.findViewById(R.id.qr_remaining_time);
|
||||
if(qrProgress == null)
|
||||
return;
|
||||
|
||||
long interval = mQrDeadline - mQrStartTime;
|
||||
int progress = Math.min((int)((SystemClock.uptimeMillis() - mQrStartTime) * 100 / interval), 100);
|
||||
qrProgress.setProgress(100 - progress);
|
||||
|
||||
mHandler.postDelayed(() -> updateQrProgress(dialog), 1000);
|
||||
}
|
||||
|
||||
private void onQrLicenseReceived(View dialog, String license) {
|
||||
EditText licenseCode = dialog.findViewById(R.id.license_code);
|
||||
Billing billing = Billing.newInstance(this);
|
||||
boolean was_valid = billing.isPurchased(Billing.SUPPORTER_SKU);
|
||||
|
||||
if(billing.setLicense(license)) {
|
||||
licenseCode.setText(license);
|
||||
|
||||
Utils.showToast(this, R.string.license_activation_ok);
|
||||
if(!was_valid)
|
||||
Utils.showToastLong(this, R.string.paid_features_unlocked);
|
||||
|
||||
hideQrCode(dialog, null);
|
||||
if(mLicenseDialog != null)
|
||||
mLicenseDialog.dismiss();
|
||||
} else
|
||||
hideQrCode(dialog, getString(R.string.invalid_license));
|
||||
}
|
||||
|
||||
private void hideQrCode(View dialog, @Nullable String error_msg) {
|
||||
View showQr = dialog.findViewById(R.id.show_qr_code);
|
||||
View qrLoading = dialog.findViewById(R.id.qr_code_loading);
|
||||
View qrBox = dialog.findViewById(R.id.qr_box);
|
||||
View qrInfo = dialog.findViewById(R.id.qr_info_text);
|
||||
|
||||
qrBox.setVisibility(View.GONE);
|
||||
qrInfo.setVisibility(View.GONE);
|
||||
qrLoading.setVisibility(View.GONE);
|
||||
showQr.setVisibility(View.VISIBLE);
|
||||
|
||||
if((error_msg != null) && !mDialogClosing)
|
||||
Toast.makeText(this, error_msg, Toast.LENGTH_LONG).show();
|
||||
|
||||
stopQrExecutor();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,70 +1,131 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/installation_id" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
android:padding="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/installation_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:focusable="true"
|
||||
tools:text="A127ADC1245" />
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/installation_id" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/copy_id"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/installation_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textIsSelectable="true"
|
||||
android:focusable="true"
|
||||
tools:text="A127ADC1245" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/copy_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:src="@drawable/ic_content_copy" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/show_qr_code"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:src="@drawable/ic_content_copy" />
|
||||
android:text="@string/activate_via_qr_code"
|
||||
android:layout_gravity="center"
|
||||
style="?attr/materialButtonOutlinedStyle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/qr_code_loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
android:text="@string/loading" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/qr_info_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textAlignment="center"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:layout_marginHorizontal="40dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/qr_info_text" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/qr_box"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qr_code"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="20dp"
|
||||
tools:layout_width="200dp"
|
||||
tools:layout_height="200dp"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/qr_remaining_time"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||
tools:progress="25"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/paid_features_msg"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:text="@string/access_paid_features_msg" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/license_code" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/license_code"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:gravity="top"
|
||||
android:lines="4" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/validation_rc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/ok"
|
||||
android:textAllCaps="true"
|
||||
android:textSize="13sp"
|
||||
tools:text="@string/valid" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/paid_features_msg"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/access_paid_features_msg" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/license_code" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/license_code"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:gravity="top"
|
||||
android:lines="4" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/validation_rc"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/ok"
|
||||
android:textAllCaps="true"
|
||||
android:textSize="13sp"
|
||||
tools:text="@string/valid" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@ -26,6 +26,7 @@
|
||||
- FlagKit: <a href='https://github.com/madebybowtie/FlagKit/blob/master/LICENSE'>MIT</a>\n\n
|
||||
- IP Geolocation by <a href='https://db-ip.com'>DB-IP</a>\n\n
|
||||
- AppIntro: <a href='https://github.com/AppIntro/AppIntro/blob/main/LICENSE'>Apache-2.0</a>\n\n
|
||||
- QrGenerator: <a href='https://github.com/androidmads/QRGenerator/blob/master/LICENSE.md'>MIT</a>\n\n
|
||||
- Font Awesome: <a href='https://fontawesome.com/license/free'>Licenses</a>\n\n
|
||||
- App icon by <a href="https://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">flaticon</a>\n\n
|
||||
- SourceCodePro font: <a href='https://github.com/adobe-fonts/source-code-pro/blob/release/LICENSE.md'>OFL-1.1</a>\n\n
|
||||
@ -461,4 +462,15 @@
|
||||
<string name="username">Username</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="mitm_addon_autostart_workaround">Connection to the mitm addon failed. As a workaround, you can try to open the mitm addon app and then go back to PCAPdroid without closing it. Do you want to open it now?</string>
|
||||
<string name="qr_license_confirm">Do you want to generate a license for the \"%1$s\" device using the following unlock token?</string>
|
||||
<string name="invalid_license">Invalid license</string>
|
||||
<string name="connection_error">Connection error: %1$s</string>
|
||||
<string name="activate_via_qr_code">Activate via QR code</string>
|
||||
<string name="qr_code_expired">QR code expired. Generate a new QR code and retry</string>
|
||||
<string name="qr_info_text">Install PCAPdroid from Google Play and scan this QR code</string>
|
||||
<string name="qr_purchase_required">Purchase an unlock token to proceed with the QR code activation</string>
|
||||
<string name="license_limit_reached">You have reached the licenses limit for this unlock token. Buy a new token to generate more licenses</string>
|
||||
<string name="license_error">License generation error [%1$d]: %2$s</string>
|
||||
<string name="requesting_license">Requesting a license code, please wait</string>
|
||||
<string name="license_activation_ok">License activation completed</string>
|
||||
</resources>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user