@capgo/capacitor-native-biometric 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapgoCapacitorNativeBiometric.podspec +13 -0
- package/LICENSE +21 -0
- package/android/.gradle/8.0.2/checksums/checksums.lock +0 -0
- package/android/.gradle/8.0.2/dependencies-accessors/dependencies-accessors.lock +0 -0
- package/android/.gradle/8.0.2/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.0.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.0.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.0.2/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.0.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.0.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.0.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/8.0.2/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/android.iml +40 -0
- package/android/build.gradle +59 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +6 -0
- package/android/gradle.properties +20 -0
- package/android/gradlew +244 -0
- package/android/gradlew.bat +92 -0
- package/android/local.properties +8 -0
- package/android/proguard-rules.pro +21 -0
- package/android/settings.gradle +2 -0
- package/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +28 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/java/ee/forgr/biometric/AuthActivity.java +129 -0
- package/android/src/main/java/ee/forgr/biometric/NativeBiometric.java +457 -0
- package/android/src/main/res/layout/activity_auth_acitivy.xml +12 -0
- package/android/src/main/res/layout/bridge_layout_main.xml +15 -0
- package/android/src/main/res/navigation/nav_graph.xml +28 -0
- package/android/src/main/res/values/colors.xml +3 -0
- package/android/src/main/res/values/dimens.xml +3 -0
- package/android/src/main/res/values/strings.xml +12 -0
- package/android/src/main/res/values/styles.xml +16 -0
- package/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +18 -0
- package/dist/docs.json +426 -0
- package/dist/esm/definitions.d.ts +91 -0
- package/dist/esm/definitions.js +11 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +10 -0
- package/dist/esm/web.js +22 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +47 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +50 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/Info.plist +24 -0
- package/ios/Plugin/Plugin.h +10 -0
- package/ios/Plugin/Plugin.m +12 -0
- package/ios/Plugin/Plugin.swift +264 -0
- package/ios/Plugin.xcodeproj/project.pbxproj +554 -0
- package/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/Plugin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/Plugin.xcodeproj/project.xcworkspace/xcuserdata/pilito.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/Plugin.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/ios/Plugin.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/ios/Plugin.xcodeproj/xcuserdata/pilito.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
- package/ios/Plugin.xcworkspace/contents.xcworkspacedata +10 -0
- package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/Plugin.xcworkspace/xcuserdata/jmartinez.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/Plugin.xcworkspace/xcuserdata/josemartinez.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/Plugin.xcworkspace/xcuserdata/pilito.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
- package/ios/PluginTests/Info.plist +22 -0
- package/ios/PluginTests/PluginTests.swift +35 -0
- package/ios/Podfile +16 -0
- package/ios/Podfile.lock +22 -0
- package/ios/Pods/Local Podspecs/Capacitor.podspec.json +34 -0
- package/ios/Pods/Local Podspecs/CapacitorCordova.podspec.json +26 -0
- package/ios/Pods/Manifest.lock +22 -0
- package/ios/Pods/Pods.xcodeproj/project.pbxproj +1626 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/Capacitor.xcscheme +60 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/jmartinez.xcuserdatad/xcschemes/xcschememanagement.plist +39 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/Capacitor.xcscheme +60 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/josemartinez.xcuserdatad/xcschemes/xcschememanagement.plist +39 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/martindonadieu.xcuserdatad/xcschemes/Capacitor.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/martindonadieu.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/martindonadieu.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/martindonadieu.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +58 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/martindonadieu.xcuserdatad/xcschemes/xcschememanagement.plist +31 -0
- package/ios/Pods/Pods.xcodeproj/xcuserdata/pilito.xcuserdatad/xcschemes/xcschememanagement.plist +29 -0
- package/ios/Pods/Target Support Files/Capacitor/Capacitor-Info.plist +26 -0
- package/ios/Pods/Target Support Files/Capacitor/Capacitor-dummy.m +5 -0
- package/ios/Pods/Target Support Files/Capacitor/Capacitor-prefix.pch +12 -0
- package/ios/Pods/Target Support Files/Capacitor/Capacitor-umbrella.h +23 -0
- package/ios/Pods/Target Support Files/Capacitor/Capacitor.debug.xcconfig +16 -0
- package/ios/Pods/Target Support Files/Capacitor/Capacitor.modulemap +8 -0
- package/ios/Pods/Target Support Files/Capacitor/Capacitor.release.xcconfig +16 -0
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-Info.plist +26 -0
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-dummy.m +5 -0
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-prefix.pch +12 -0
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-umbrella.h +32 -0
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.debug.xcconfig +13 -0
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.modulemap +6 -0
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.release.xcconfig +13 -0
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-Info.plist +26 -0
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-acknowledgements.markdown +53 -0
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-acknowledgements.plist +91 -0
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-dummy.m +5 -0
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-umbrella.h +16 -0
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig +14 -0
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.modulemap +6 -0
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig +14 -0
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-Info.plist +26 -0
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-acknowledgements.markdown +53 -0
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-acknowledgements.plist +91 -0
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-dummy.m +5 -0
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh +188 -0
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-umbrella.h +16 -0
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig +15 -0
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.modulemap +6 -0
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig +15 -0
- package/package.json +80 -0
- package/readme.md +284 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
package ee.forgr.biometric;
|
|
2
|
+
|
|
3
|
+
import android.content.Intent;
|
|
4
|
+
import android.os.Build;
|
|
5
|
+
import android.os.Bundle;
|
|
6
|
+
import android.os.Handler;
|
|
7
|
+
import android.util.Log;
|
|
8
|
+
import android.view.View;
|
|
9
|
+
import androidx.annotation.NonNull;
|
|
10
|
+
import androidx.appcompat.app.AppCompatActivity;
|
|
11
|
+
import androidx.appcompat.widget.Toolbar;
|
|
12
|
+
import androidx.biometric.BiometricPrompt;
|
|
13
|
+
import ee.forgr.biometric.capacitornativebiometric.R;
|
|
14
|
+
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
15
|
+
import com.google.android.material.snackbar.Snackbar;
|
|
16
|
+
import java.util.concurrent.Executor;
|
|
17
|
+
|
|
18
|
+
public class AuthActivity extends AppCompatActivity {
|
|
19
|
+
|
|
20
|
+
private Executor executor;
|
|
21
|
+
private int maxAttempts;
|
|
22
|
+
private int counter = 0;
|
|
23
|
+
|
|
24
|
+
@Override
|
|
25
|
+
protected void onCreate(Bundle savedInstanceState) {
|
|
26
|
+
super.onCreate(savedInstanceState);
|
|
27
|
+
setContentView(R.layout.activity_auth_acitivy);
|
|
28
|
+
|
|
29
|
+
maxAttempts = getIntent().getIntExtra("maxAttempts", 1);
|
|
30
|
+
|
|
31
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
32
|
+
executor = this.getMainExecutor();
|
|
33
|
+
} else {
|
|
34
|
+
executor =
|
|
35
|
+
new Executor() {
|
|
36
|
+
@Override
|
|
37
|
+
public void execute(Runnable command) {
|
|
38
|
+
new Handler().post(command);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
BiometricPrompt.PromptInfo.Builder builder =
|
|
44
|
+
new BiometricPrompt.PromptInfo.Builder()
|
|
45
|
+
.setTitle(
|
|
46
|
+
getIntent().hasExtra("title")
|
|
47
|
+
? getIntent().getStringExtra("title")
|
|
48
|
+
: "Authenticate"
|
|
49
|
+
)
|
|
50
|
+
.setSubtitle(
|
|
51
|
+
getIntent().hasExtra("subtitle")
|
|
52
|
+
? getIntent().getStringExtra("subtitle")
|
|
53
|
+
: null
|
|
54
|
+
)
|
|
55
|
+
.setDescription(
|
|
56
|
+
getIntent().hasExtra("description")
|
|
57
|
+
? getIntent().getStringExtra("description")
|
|
58
|
+
: null
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
boolean useFallback = getIntent().getBooleanExtra("useFallback", false);
|
|
62
|
+
|
|
63
|
+
if (useFallback) {
|
|
64
|
+
builder.setDeviceCredentialAllowed(true);
|
|
65
|
+
} else {
|
|
66
|
+
builder.setNegativeButtonText(
|
|
67
|
+
getIntent().hasExtra("negativeButtonText")
|
|
68
|
+
? getIntent().getStringExtra("negativeButtonText")
|
|
69
|
+
: "Cancel"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
BiometricPrompt.PromptInfo promptInfo = builder.build();
|
|
74
|
+
|
|
75
|
+
BiometricPrompt biometricPrompt = new BiometricPrompt(
|
|
76
|
+
this,
|
|
77
|
+
executor,
|
|
78
|
+
new BiometricPrompt.AuthenticationCallback() {
|
|
79
|
+
@Override
|
|
80
|
+
public void onAuthenticationError(
|
|
81
|
+
int errorCode,
|
|
82
|
+
@NonNull CharSequence errString
|
|
83
|
+
) {
|
|
84
|
+
super.onAuthenticationError(errorCode, errString);
|
|
85
|
+
|
|
86
|
+
finishActivity(errString.toString(), errorCode);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@Override
|
|
90
|
+
public void onAuthenticationSucceeded(
|
|
91
|
+
@NonNull BiometricPrompt.AuthenticationResult result
|
|
92
|
+
) {
|
|
93
|
+
super.onAuthenticationSucceeded(result);
|
|
94
|
+
finishActivity("success", 0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Override
|
|
98
|
+
public void onAuthenticationFailed() {
|
|
99
|
+
super.onAuthenticationFailed();
|
|
100
|
+
counter++;
|
|
101
|
+
if (counter == maxAttempts) finishActivity("failed");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
biometricPrompt.authenticate(promptInfo);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
void finishActivity(String result) {
|
|
110
|
+
Intent intent = new Intent();
|
|
111
|
+
intent.putExtra("result", "failed");
|
|
112
|
+
intent.putExtra("errorDetails", "Authentication failed.");
|
|
113
|
+
setResult(RESULT_OK, intent);
|
|
114
|
+
finish();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
void finishActivity(String result, int errorCode) {
|
|
118
|
+
Intent intent = new Intent();
|
|
119
|
+
if (errorCode != 0) {
|
|
120
|
+
intent.putExtra("result", "error");
|
|
121
|
+
intent.putExtra("errorDetails", result);
|
|
122
|
+
intent.putExtra("errorCode", String.valueOf(errorCode));
|
|
123
|
+
} else {
|
|
124
|
+
intent.putExtra("result", result);
|
|
125
|
+
}
|
|
126
|
+
setResult(RESULT_OK, intent);
|
|
127
|
+
finish();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
package ee.forgr.biometric;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
import android.app.KeyguardManager;
|
|
5
|
+
import android.content.Context;
|
|
6
|
+
import android.content.Intent;
|
|
7
|
+
import android.content.SharedPreferences;
|
|
8
|
+
import android.content.pm.PackageManager;
|
|
9
|
+
import android.os.Build;
|
|
10
|
+
import android.security.KeyPairGeneratorSpec;
|
|
11
|
+
import android.security.keystore.KeyGenParameterSpec;
|
|
12
|
+
import android.security.keystore.KeyProperties;
|
|
13
|
+
import android.util.Base64;
|
|
14
|
+
import androidx.activity.result.ActivityResult;
|
|
15
|
+
import androidx.biometric.BiometricManager;
|
|
16
|
+
import com.getcapacitor.JSObject;
|
|
17
|
+
import com.getcapacitor.Plugin;
|
|
18
|
+
import com.getcapacitor.PluginCall;
|
|
19
|
+
import com.getcapacitor.PluginMethod;
|
|
20
|
+
import com.getcapacitor.annotation.ActivityCallback;
|
|
21
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
22
|
+
import java.io.ByteArrayInputStream;
|
|
23
|
+
import java.io.ByteArrayOutputStream;
|
|
24
|
+
import java.io.IOException;
|
|
25
|
+
import java.security.GeneralSecurityException;
|
|
26
|
+
import java.security.InvalidAlgorithmParameterException;
|
|
27
|
+
import java.security.InvalidKeyException;
|
|
28
|
+
import java.security.Key;
|
|
29
|
+
import java.security.KeyPairGenerator;
|
|
30
|
+
import java.security.KeyStore;
|
|
31
|
+
import java.security.KeyStoreException;
|
|
32
|
+
import java.security.NoSuchAlgorithmException;
|
|
33
|
+
import java.security.NoSuchProviderException;
|
|
34
|
+
import java.security.SecureRandom;
|
|
35
|
+
import java.security.UnrecoverableEntryException;
|
|
36
|
+
import java.security.cert.CertificateException;
|
|
37
|
+
import java.util.ArrayList;
|
|
38
|
+
import javax.crypto.Cipher;
|
|
39
|
+
import javax.crypto.CipherInputStream;
|
|
40
|
+
import javax.crypto.CipherOutputStream;
|
|
41
|
+
import javax.crypto.KeyGenerator;
|
|
42
|
+
import javax.crypto.NoSuchPaddingException;
|
|
43
|
+
import javax.crypto.spec.GCMParameterSpec;
|
|
44
|
+
import javax.crypto.spec.SecretKeySpec;
|
|
45
|
+
|
|
46
|
+
@CapacitorPlugin(name = "NativeBiometric")
|
|
47
|
+
public class NativeBiometric extends Plugin {
|
|
48
|
+
|
|
49
|
+
private BiometricManager biometricManager;
|
|
50
|
+
//protected final static int AUTH_CODE = 0102;
|
|
51
|
+
|
|
52
|
+
private static final int NONE = 0;
|
|
53
|
+
private static final int FINGERPRINT = 3;
|
|
54
|
+
private static final int FACE_AUTHENTICATION = 4;
|
|
55
|
+
private static final int IRIS_AUTHENTICATION = 5;
|
|
56
|
+
private static final int MULTIPLE = 6;
|
|
57
|
+
|
|
58
|
+
private KeyStore keyStore;
|
|
59
|
+
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
|
|
60
|
+
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
|
61
|
+
private static final String RSA_MODE = "RSA/ECB/PKCS1Padding";
|
|
62
|
+
private static final String AES_MODE = "AES/ECB/PKCS7Padding";
|
|
63
|
+
private static final byte[] FIXED_IV = new byte[12];
|
|
64
|
+
private static final String ENCRYPTED_KEY = "NativeBiometricKey";
|
|
65
|
+
private static final String NATIVE_BIOMETRIC_SHARED_PREFERENCES =
|
|
66
|
+
"NativeBiometricSharedPreferences";
|
|
67
|
+
|
|
68
|
+
private SharedPreferences encryptedSharedPreferences;
|
|
69
|
+
|
|
70
|
+
private int getAvailableFeature() {
|
|
71
|
+
// default to none
|
|
72
|
+
int type = NONE;
|
|
73
|
+
|
|
74
|
+
// if has fingerprint
|
|
75
|
+
if (
|
|
76
|
+
getContext()
|
|
77
|
+
.getPackageManager()
|
|
78
|
+
.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)
|
|
79
|
+
) {
|
|
80
|
+
type = FINGERPRINT;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// if has face auth
|
|
84
|
+
if (
|
|
85
|
+
getContext()
|
|
86
|
+
.getPackageManager()
|
|
87
|
+
.hasSystemFeature(PackageManager.FEATURE_FACE)
|
|
88
|
+
) {
|
|
89
|
+
// if also has fingerprint
|
|
90
|
+
if (type != NONE) return MULTIPLE;
|
|
91
|
+
|
|
92
|
+
type = FACE_AUTHENTICATION;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// if has iris auth
|
|
96
|
+
if (
|
|
97
|
+
getContext()
|
|
98
|
+
.getPackageManager()
|
|
99
|
+
.hasSystemFeature(PackageManager.FEATURE_IRIS)
|
|
100
|
+
) {
|
|
101
|
+
// if also has fingerprint or face auth
|
|
102
|
+
if (type != NONE) return MULTIPLE;
|
|
103
|
+
|
|
104
|
+
type = IRIS_AUTHENTICATION;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return type;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@PluginMethod
|
|
111
|
+
public void isAvailable(PluginCall call) {
|
|
112
|
+
JSObject ret = new JSObject();
|
|
113
|
+
|
|
114
|
+
biometricManager = BiometricManager.from(getContext());
|
|
115
|
+
int canAuthenticateResult = biometricManager.canAuthenticate();
|
|
116
|
+
|
|
117
|
+
switch (canAuthenticateResult) {
|
|
118
|
+
case BiometricManager.BIOMETRIC_SUCCESS:
|
|
119
|
+
ret.put("isAvailable", true);
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
ret.put("isAvailable", false);
|
|
123
|
+
ret.put("errorCode", canAuthenticateResult);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
ret.put("biometryType", getAvailableFeature());
|
|
128
|
+
call.resolve(ret);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@PluginMethod
|
|
132
|
+
public void verifyIdentity(final PluginCall call) {
|
|
133
|
+
Intent intent = new Intent(getContext(), AuthActivity.class);
|
|
134
|
+
|
|
135
|
+
intent.putExtra("title", call.getString("title", "Authenticate"));
|
|
136
|
+
|
|
137
|
+
if (call.hasOption("subtitle")) intent.putExtra(
|
|
138
|
+
"subtitle",
|
|
139
|
+
call.getString("subtitle")
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
if (call.hasOption("description")) intent.putExtra(
|
|
143
|
+
"description",
|
|
144
|
+
call.getString("description")
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (call.hasOption("negativeButtonText")) intent.putExtra(
|
|
148
|
+
"negativeButtonText",
|
|
149
|
+
call.getString("negativeButtonText")
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (call.hasOption("maxAttempts")) intent.putExtra(
|
|
153
|
+
"maxAttempts",
|
|
154
|
+
call.getInt("maxAttempts")
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
boolean useFallback = call.getBoolean("useFallback", false);
|
|
158
|
+
|
|
159
|
+
if (useFallback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
160
|
+
KeyguardManager keyguardManager = (KeyguardManager) getActivity()
|
|
161
|
+
.getSystemService(Context.KEYGUARD_SERVICE);
|
|
162
|
+
useFallback = keyguardManager.isDeviceSecure();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
intent.putExtra("useFallback", useFallback);
|
|
166
|
+
|
|
167
|
+
startActivityForResult(call, intent, "verifyResult");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@PluginMethod
|
|
171
|
+
public void setCredentials(final PluginCall call) {
|
|
172
|
+
String username = call.getString("username", null);
|
|
173
|
+
String password = call.getString("password", null);
|
|
174
|
+
String KEY_ALIAS = call.getString("server", null);
|
|
175
|
+
|
|
176
|
+
if (username != null && password != null && KEY_ALIAS != null) {
|
|
177
|
+
try {
|
|
178
|
+
SharedPreferences.Editor editor = getContext()
|
|
179
|
+
.getSharedPreferences(
|
|
180
|
+
NATIVE_BIOMETRIC_SHARED_PREFERENCES,
|
|
181
|
+
Context.MODE_PRIVATE
|
|
182
|
+
)
|
|
183
|
+
.edit();
|
|
184
|
+
editor.putString("username", encryptString(username, KEY_ALIAS));
|
|
185
|
+
editor.putString("password", encryptString(password, KEY_ALIAS));
|
|
186
|
+
editor.apply();
|
|
187
|
+
call.resolve();
|
|
188
|
+
} catch (GeneralSecurityException e) {
|
|
189
|
+
call.reject("Failed to save credentials", e);
|
|
190
|
+
e.printStackTrace();
|
|
191
|
+
} catch (IOException e) {
|
|
192
|
+
call.reject("Failed to save credentials", e);
|
|
193
|
+
e.printStackTrace();
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
call.reject("Missing properties");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@PluginMethod
|
|
201
|
+
public void getCredentials(final PluginCall call) {
|
|
202
|
+
String KEY_ALIAS = call.getString("server", null);
|
|
203
|
+
|
|
204
|
+
SharedPreferences sharedPreferences = getContext()
|
|
205
|
+
.getSharedPreferences(
|
|
206
|
+
NATIVE_BIOMETRIC_SHARED_PREFERENCES,
|
|
207
|
+
Context.MODE_PRIVATE
|
|
208
|
+
);
|
|
209
|
+
String username = sharedPreferences.getString("username", null);
|
|
210
|
+
String password = sharedPreferences.getString("password", null);
|
|
211
|
+
if (KEY_ALIAS != null) {
|
|
212
|
+
if (username != null && password != null) {
|
|
213
|
+
try {
|
|
214
|
+
JSObject jsObject = new JSObject();
|
|
215
|
+
jsObject.put("username", decryptString(username, KEY_ALIAS));
|
|
216
|
+
jsObject.put("password", decryptString(password, KEY_ALIAS));
|
|
217
|
+
call.resolve(jsObject);
|
|
218
|
+
} catch (GeneralSecurityException e) {
|
|
219
|
+
call.reject("Failed to get credentials", e);
|
|
220
|
+
} catch (IOException e) {
|
|
221
|
+
call.reject("Failed to get credentials", e);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
call.reject("No credentials found");
|
|
225
|
+
}
|
|
226
|
+
} else {
|
|
227
|
+
call.reject("No server name was provided");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@ActivityCallback
|
|
232
|
+
private void verifyResult(PluginCall call, ActivityResult result) {
|
|
233
|
+
if (result.getResultCode() == Activity.RESULT_OK) {
|
|
234
|
+
Intent data = result.getData();
|
|
235
|
+
if (data.hasExtra("result")) {
|
|
236
|
+
switch (data.getStringExtra("result")) {
|
|
237
|
+
case "success":
|
|
238
|
+
call.resolve();
|
|
239
|
+
break;
|
|
240
|
+
case "failed":
|
|
241
|
+
call.reject(
|
|
242
|
+
data.getStringExtra("errorDetails"),
|
|
243
|
+
data.getStringExtra("errorCode")
|
|
244
|
+
);
|
|
245
|
+
break;
|
|
246
|
+
default:
|
|
247
|
+
call.reject(
|
|
248
|
+
"Verification error: " + data.getStringExtra("result"),
|
|
249
|
+
data.getStringExtra("errorCode")
|
|
250
|
+
);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} else {
|
|
255
|
+
call.reject("Something went wrong.");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@PluginMethod
|
|
260
|
+
public void deleteCredentials(final PluginCall call) {
|
|
261
|
+
String KEY_ALIAS = call.getString("server", null);
|
|
262
|
+
|
|
263
|
+
if (KEY_ALIAS != null) {
|
|
264
|
+
try {
|
|
265
|
+
getKeyStore().deleteEntry(KEY_ALIAS);
|
|
266
|
+
SharedPreferences.Editor editor = getContext()
|
|
267
|
+
.getSharedPreferences(
|
|
268
|
+
NATIVE_BIOMETRIC_SHARED_PREFERENCES,
|
|
269
|
+
Context.MODE_PRIVATE
|
|
270
|
+
)
|
|
271
|
+
.edit();
|
|
272
|
+
editor.clear();
|
|
273
|
+
editor.apply();
|
|
274
|
+
call.resolve();
|
|
275
|
+
} catch (KeyStoreException e) {
|
|
276
|
+
call.reject("Failed to delete", e);
|
|
277
|
+
} catch (CertificateException e) {
|
|
278
|
+
call.reject("Failed to delete", e);
|
|
279
|
+
} catch (NoSuchAlgorithmException e) {
|
|
280
|
+
call.reject("Failed to delete", e);
|
|
281
|
+
} catch (IOException e) {
|
|
282
|
+
call.reject("Failed to delete", e);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
call.reject("No server name was provided");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private String encryptString(String stringToEncrypt, String KEY_ALIAS)
|
|
290
|
+
throws GeneralSecurityException, IOException {
|
|
291
|
+
Cipher cipher;
|
|
292
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
293
|
+
cipher = Cipher.getInstance(TRANSFORMATION);
|
|
294
|
+
cipher.init(
|
|
295
|
+
Cipher.ENCRYPT_MODE,
|
|
296
|
+
getKey(KEY_ALIAS),
|
|
297
|
+
new GCMParameterSpec(128, FIXED_IV)
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
cipher = Cipher.getInstance(AES_MODE, "BC");
|
|
301
|
+
cipher.init(Cipher.ENCRYPT_MODE, getKey(KEY_ALIAS));
|
|
302
|
+
}
|
|
303
|
+
byte[] encodedBytes = cipher.doFinal(stringToEncrypt.getBytes("UTF-8"));
|
|
304
|
+
return Base64.encodeToString(encodedBytes, Base64.DEFAULT);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private String decryptString(String stringToDecrypt, String KEY_ALIAS)
|
|
308
|
+
throws GeneralSecurityException, IOException {
|
|
309
|
+
byte[] encryptedData = Base64.decode(stringToDecrypt, Base64.DEFAULT);
|
|
310
|
+
|
|
311
|
+
Cipher cipher;
|
|
312
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
313
|
+
cipher = Cipher.getInstance(TRANSFORMATION);
|
|
314
|
+
cipher.init(
|
|
315
|
+
Cipher.DECRYPT_MODE,
|
|
316
|
+
getKey(KEY_ALIAS),
|
|
317
|
+
new GCMParameterSpec(128, FIXED_IV)
|
|
318
|
+
);
|
|
319
|
+
} else {
|
|
320
|
+
cipher = Cipher.getInstance(AES_MODE, "BC");
|
|
321
|
+
cipher.init(Cipher.DECRYPT_MODE, getKey(KEY_ALIAS));
|
|
322
|
+
}
|
|
323
|
+
byte[] decryptedData = cipher.doFinal(encryptedData);
|
|
324
|
+
return new String(decryptedData, "UTF-8");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private Key generateKey(String KEY_ALIAS)
|
|
328
|
+
throws GeneralSecurityException, IOException {
|
|
329
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
330
|
+
KeyGenerator generator = KeyGenerator.getInstance(
|
|
331
|
+
KeyProperties.KEY_ALGORITHM_AES,
|
|
332
|
+
ANDROID_KEY_STORE
|
|
333
|
+
);
|
|
334
|
+
generator.init(
|
|
335
|
+
new KeyGenParameterSpec.Builder(
|
|
336
|
+
KEY_ALIAS,
|
|
337
|
+
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
|
|
338
|
+
)
|
|
339
|
+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
340
|
+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
341
|
+
.setRandomizedEncryptionRequired(false)
|
|
342
|
+
.build()
|
|
343
|
+
);
|
|
344
|
+
return generator.generateKey();
|
|
345
|
+
} else {
|
|
346
|
+
return getAESKey(KEY_ALIAS);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private Key getKey(String KEY_ALIAS)
|
|
351
|
+
throws GeneralSecurityException, IOException {
|
|
352
|
+
KeyStore.SecretKeyEntry secretKeyEntry =
|
|
353
|
+
(KeyStore.SecretKeyEntry) getKeyStore().getEntry(KEY_ALIAS, null);
|
|
354
|
+
if (secretKeyEntry != null) {
|
|
355
|
+
return secretKeyEntry.getSecretKey();
|
|
356
|
+
}
|
|
357
|
+
return generateKey(KEY_ALIAS);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private KeyStore getKeyStore()
|
|
361
|
+
throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
|
|
362
|
+
if (keyStore != null) {
|
|
363
|
+
return keyStore;
|
|
364
|
+
} else {
|
|
365
|
+
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
|
|
366
|
+
keyStore.load(null);
|
|
367
|
+
return keyStore;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private Key getAESKey(String KEY_ALIAS)
|
|
372
|
+
throws CertificateException, NoSuchPaddingException, InvalidKeyException, NoSuchAlgorithmException, KeyStoreException, NoSuchProviderException, UnrecoverableEntryException, IOException, InvalidAlgorithmParameterException {
|
|
373
|
+
SharedPreferences sharedPreferences = getContext()
|
|
374
|
+
.getSharedPreferences("", Context.MODE_PRIVATE);
|
|
375
|
+
String encryptedKeyB64 = sharedPreferences.getString(ENCRYPTED_KEY, null);
|
|
376
|
+
if (encryptedKeyB64 == null) {
|
|
377
|
+
byte[] key = new byte[16];
|
|
378
|
+
SecureRandom secureRandom = new SecureRandom();
|
|
379
|
+
secureRandom.nextBytes(key);
|
|
380
|
+
byte[] encryptedKey = rsaEncrypt(key, KEY_ALIAS);
|
|
381
|
+
encryptedKeyB64 = Base64.encodeToString(encryptedKey, Base64.DEFAULT);
|
|
382
|
+
SharedPreferences.Editor edit = sharedPreferences.edit();
|
|
383
|
+
edit.putString(ENCRYPTED_KEY, encryptedKeyB64);
|
|
384
|
+
edit.apply();
|
|
385
|
+
return new SecretKeySpec(key, "AES");
|
|
386
|
+
} else {
|
|
387
|
+
byte[] encryptedKey = Base64.decode(encryptedKeyB64, Base64.DEFAULT);
|
|
388
|
+
byte[] key = rsaDecrypt(encryptedKey, KEY_ALIAS);
|
|
389
|
+
return new SecretKeySpec(key, "AES");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private KeyStore.PrivateKeyEntry getPrivateKeyEntry(String KEY_ALIAS)
|
|
394
|
+
throws NoSuchProviderException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, CertificateException, KeyStoreException, IOException, UnrecoverableEntryException {
|
|
395
|
+
KeyStore.PrivateKeyEntry privateKeyEntry =
|
|
396
|
+
(KeyStore.PrivateKeyEntry) getKeyStore().getEntry(KEY_ALIAS, null);
|
|
397
|
+
|
|
398
|
+
if (privateKeyEntry == null) {
|
|
399
|
+
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(
|
|
400
|
+
KeyProperties.KEY_ALGORITHM_RSA,
|
|
401
|
+
ANDROID_KEY_STORE
|
|
402
|
+
);
|
|
403
|
+
keyPairGenerator.initialize(
|
|
404
|
+
new KeyPairGeneratorSpec.Builder(getContext())
|
|
405
|
+
.setAlias(KEY_ALIAS)
|
|
406
|
+
.build()
|
|
407
|
+
);
|
|
408
|
+
keyPairGenerator.generateKeyPair();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return privateKeyEntry;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private byte[] rsaEncrypt(byte[] secret, String KEY_ALIAS)
|
|
415
|
+
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, UnrecoverableEntryException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
|
|
416
|
+
KeyStore.PrivateKeyEntry privateKeyEntry = getPrivateKeyEntry(KEY_ALIAS);
|
|
417
|
+
// Encrypt the text
|
|
418
|
+
Cipher inputCipher = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
|
|
419
|
+
inputCipher.init(
|
|
420
|
+
Cipher.ENCRYPT_MODE,
|
|
421
|
+
privateKeyEntry.getCertificate().getPublicKey()
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
425
|
+
CipherOutputStream cipherOutputStream = new CipherOutputStream(
|
|
426
|
+
outputStream,
|
|
427
|
+
inputCipher
|
|
428
|
+
);
|
|
429
|
+
cipherOutputStream.write(secret);
|
|
430
|
+
cipherOutputStream.close();
|
|
431
|
+
|
|
432
|
+
byte[] vals = outputStream.toByteArray();
|
|
433
|
+
return vals;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private byte[] rsaDecrypt(byte[] encrypted, String KEY_ALIAS)
|
|
437
|
+
throws UnrecoverableEntryException, NoSuchAlgorithmException, KeyStoreException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, IOException, CertificateException, InvalidAlgorithmParameterException {
|
|
438
|
+
KeyStore.PrivateKeyEntry privateKeyEntry = getPrivateKeyEntry(KEY_ALIAS);
|
|
439
|
+
Cipher output = Cipher.getInstance(RSA_MODE, "AndroidOpenSSL");
|
|
440
|
+
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.getPrivateKey());
|
|
441
|
+
CipherInputStream cipherInputStream = new CipherInputStream(
|
|
442
|
+
new ByteArrayInputStream(encrypted),
|
|
443
|
+
output
|
|
444
|
+
);
|
|
445
|
+
ArrayList<Byte> values = new ArrayList<>();
|
|
446
|
+
int nextByte;
|
|
447
|
+
while ((nextByte = cipherInputStream.read()) != -1) {
|
|
448
|
+
values.add((byte) nextByte);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
byte[] bytes = new byte[values.size()];
|
|
452
|
+
for (int i = 0; i < bytes.length; i++) {
|
|
453
|
+
bytes[i] = values.get(i).byteValue();
|
|
454
|
+
}
|
|
455
|
+
return bytes;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
4
|
+
xmlns:tools="http://schemas.android.com/tools"
|
|
5
|
+
android:layout_width="match_parent"
|
|
6
|
+
android:layout_height="match_parent"
|
|
7
|
+
tools:context="ee.forgr.biometric.AuthAcitivy">
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
4
|
+
xmlns:tools="http://schemas.android.com/tools"
|
|
5
|
+
android:layout_width="match_parent"
|
|
6
|
+
android:layout_height="match_parent"
|
|
7
|
+
tools:context="com.getcapacitor.BridgeActivity"
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
<WebView
|
|
11
|
+
android:id="@+id/webview"
|
|
12
|
+
android:layout_width="fill_parent"
|
|
13
|
+
android:layout_height="fill_parent" />
|
|
14
|
+
|
|
15
|
+
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
4
|
+
xmlns:tools="http://schemas.android.com/tools"
|
|
5
|
+
android:id="@+id/nav_graph"
|
|
6
|
+
app:startDestination="@id/FirstFragment">
|
|
7
|
+
|
|
8
|
+
<fragment
|
|
9
|
+
android:id="@+id/FirstFragment"
|
|
10
|
+
android:name="ee.forgr.biometric.FirstFragment"
|
|
11
|
+
android:label="@string/first_fragment_label"
|
|
12
|
+
tools:layout="@layout/fragment_first">
|
|
13
|
+
|
|
14
|
+
<action
|
|
15
|
+
android:id="@+id/action_FirstFragment_to_SecondFragment"
|
|
16
|
+
app:destination="@id/SecondFragment" />
|
|
17
|
+
</fragment>
|
|
18
|
+
<fragment
|
|
19
|
+
android:id="@+id/SecondFragment"
|
|
20
|
+
android:name="ee.forgr.biometric.SecondFragment"
|
|
21
|
+
android:label="@string/second_fragment_label"
|
|
22
|
+
tools:layout="@layout/fragment_second">
|
|
23
|
+
|
|
24
|
+
<action
|
|
25
|
+
android:id="@+id/action_SecondFragment_to_FirstFragment"
|
|
26
|
+
app:destination="@id/FirstFragment" />
|
|
27
|
+
</fragment>
|
|
28
|
+
</navigation>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<resources>
|
|
2
|
+
<string name="my_string">Just a simple string</string>
|
|
3
|
+
<string name="title_activity_auth_activity">AuthActivity</string>
|
|
4
|
+
<!-- Strings used for fragments for navigation -->
|
|
5
|
+
<string name="first_fragment_label">First Fragment</string>
|
|
6
|
+
<string name="second_fragment_label">Second Fragment</string>
|
|
7
|
+
<string name="next">Next</string>
|
|
8
|
+
<string name="previous">Previous</string>
|
|
9
|
+
|
|
10
|
+
<string name="hello_first_fragment">Hello first fragment</string>
|
|
11
|
+
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
|
|
12
|
+
</resources>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<resources>
|
|
2
|
+
|
|
3
|
+
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
|
4
|
+
|
|
5
|
+
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
|
6
|
+
|
|
7
|
+
<style name="AppTheme.Transparent" parent="Theme.AppCompat.NoActionBar">
|
|
8
|
+
<item name="android:windowIsTranslucent">true</item>
|
|
9
|
+
<item name="android:windowBackground">@android:color/transparent</item>
|
|
10
|
+
<item name="android:windowContentOverlay">@null</item>
|
|
11
|
+
<item name="android:windowNoTitle">true</item>
|
|
12
|
+
<item name="android:windowIsFloating">true</item>
|
|
13
|
+
<item name="android:backgroundDimEnabled">false</item>
|
|
14
|
+
</style>
|
|
15
|
+
|
|
16
|
+
</resources>
|