diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 44a71234d..19d6c35ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Download Libdigidocpp Android (androidarm) artifact uses: dawidd6/action-download-artifact@v2 with: @@ -62,11 +62,11 @@ jobs: run: | pwd ./gradlew common-lib:updateLibdigidocpp --dir=libdigidocpp-files/ - - name: Setup Gradle and Build + - name: Setup Gradle, Test and Build uses: gradle/gradle-build-action@v2 with: gradle-version: wrapper - arguments: clean --no-daemon fetchAndPackageDefaultConfiguration -PappVersionName=${{ env.APP_VERSION_NAME }}.${{ env.BUILD_NUMBER }} assembleRelease --info + arguments: clean test --no-daemon fetchAndPackageDefaultConfiguration -PappVersionName=${{ env.APP_VERSION_NAME }}.${{ env.BUILD_NUMBER }} assembleRelease --info - name: Sign app APK uses: r0adkll/sign-android-release@v1 id: signed_apk diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dadf2e6d9..4776e2322 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -5,7 +5,6 @@ on: [push, pull_request] jobs: analyze: name: CodeQL Analyze - if: contains(github.repository, 'open-eid/MOPP-Android') && contains(github.ref, 'master') runs-on: ubuntu-latest permissions: actions: read @@ -27,7 +26,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: zulu - java-version: 11 + java-version: 17 - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/app/build.gradle b/app/build.gradle index 6d01b95ad..e58b84ccf 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,8 +60,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } lint { @@ -82,6 +82,7 @@ dependencies { implementation "androidx.recyclerview:recyclerview:${androidxRecyclerviewVersion}" implementation "androidx.cardview:cardview:${androidxCardviewVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" + implementation "androidx.work:work-runtime:${androidxWorkRuntime}" implementation "com.takisoft.preferencex:preferencex:${preferencexVersion}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index afa8c1fda..9d738aa9d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,8 +14,8 @@ + android:screenOrientation="user"> diff --git a/app/src/main/java/ee/ria/DigiDoc/android/Activity.java b/app/src/main/java/ee/ria/DigiDoc/android/Activity.java index b4ee6f7d1..ea8b9d123 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/Activity.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/Activity.java @@ -1,10 +1,10 @@ package ee.ria.DigiDoc.android; import static ee.ria.DigiDoc.android.Constants.DIR_EXTERNALLY_OPENED_FILES; +import static ee.ria.DigiDoc.android.utils.IntentUtils.setIntentData; import android.app.Dialog; import android.content.ActivityNotFoundException; -import android.content.ClipData; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -20,8 +20,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.FileProvider; import androidx.preference.PreferenceManager; +import androidx.work.WorkManager; import com.google.android.gms.common.util.CollectionUtils; import com.google.android.gms.tasks.Task; @@ -32,6 +32,7 @@ import java.io.File; import java.lang.ref.WeakReference; import java.nio.file.Path; +import java.util.Optional; import java.util.concurrent.Callable; import javax.inject.Inject; @@ -80,6 +81,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { handleCrashOnPreviousExecution(); + WorkManager.getInstance(this).cancelAllWork(); + Intent intent = sanitizeIntent(getIntent()); if ((Intent.ACTION_SEND.equals(intent.getAction()) || Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction()) || Intent.ACTION_VIEW.equals(intent.getAction())) && intent.getType() != null) { @@ -189,8 +192,10 @@ private Intent sanitizeIntent(Intent intent) { Uri normalizedUri = FileUtil.normalizeUri(Uri.parse(intent.getDataString())); intent.setDataAndNormalize(normalizedUri); } - if (intent.getExtras() != null && !(intent.getExtras().containsKey(Intent.EXTRA_REFERRER) && - intent.getExtras().get(Intent.EXTRA_REFERRER).equals(R.string.application_name))) { + if (intent.getExtras() != null) { + if (intent.getExtras().containsKey(Intent.EXTRA_REFERRER)) { + intent.getExtras().getString(Intent.EXTRA_REFERRER); + } intent.replaceExtras(new Bundle()); } return intent; @@ -215,7 +220,7 @@ private void initializeApplicationFileTypesAssociation() { @Override protected void attachBaseContext(Context newBase) { - Application.ApplicationComponent component = Application.component(newBase); + ApplicationApp.ApplicationComponent component = ApplicationApp.component(newBase); navigator = component.navigator(); rootScreenFactory = component.rootScreenFactory(); settingsDataStore = component.settingsDataStore(); @@ -232,6 +237,14 @@ public void onBackPressed() { @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); + // If user selects a file from provider menu (Open from -> RIA DigiDoc), it starts a new activity + // Replace the main activity after the new file has been selected + if (Optional.ofNullable(data) + .map(Intent::getAction) + .filter(action -> action.equals(Intent.ACTION_GET_CONTENT)) + .isPresent()) { + navigator.onCreate(this, findViewById(android.R.id.content), null); + } navigator.onActivityResult(requestCode, resultCode, data); } @@ -345,17 +358,6 @@ private static String getFileName(File file) { } } } - - private static Intent setIntentData(Intent intent, Path filePath, android.app.Activity activity) { - intent.setData(Uri.parse(filePath.toUri().toString())); - intent.setClipData(ClipData.newRawUri(filePath.getFileName().toString(), FileProvider.getUriForFile( - activity, - activity.getString(R.string.file_provider_authority), - filePath.toFile()))); - return intent; - } - - } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/Application.java b/app/src/main/java/ee/ria/DigiDoc/android/ApplicationApp.java similarity index 94% rename from app/src/main/java/ee/ria/DigiDoc/android/Application.java rename to app/src/main/java/ee/ria/DigiDoc/android/ApplicationApp.java index 9b756afe9..b73b6adf7 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/Application.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/ApplicationApp.java @@ -26,8 +26,10 @@ import android.content.Intent; import android.content.SharedPreferences; import android.hardware.usb.UsbManager; +import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.ResultReceiver; import android.os.StrictMode; import android.util.Log; @@ -72,6 +74,7 @@ import ee.ria.DigiDoc.android.main.diagnostics.source.FileSystemDiagnosticsDataSource; import ee.ria.DigiDoc.android.main.home.HomeViewModel; import ee.ria.DigiDoc.android.main.settings.SettingsDataStore; +import ee.ria.DigiDoc.android.main.settings.create.TSACertificateAddViewModel; import ee.ria.DigiDoc.android.signature.create.SignatureCreateViewModel; import ee.ria.DigiDoc.android.signature.data.SignatureContainerDataSource; import ee.ria.DigiDoc.android.signature.data.source.FileSystemSignatureContainerDataSource; @@ -98,7 +101,7 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins; import timber.log.Timber; -public class Application extends android.app.Application { +public class ApplicationApp extends android.app.Application { private static ConfigurationProvider configurationProvider; @@ -223,11 +226,11 @@ private void setupConfiguration() { Bundle bundle = new Bundle(); bundle.putParcelable(ConfigurationConstants.CONFIGURATION_PROVIDER, configurationProvider); - ConfigurationProviderReceiver confProviderReceiver = new ConfigurationProviderReceiver(new Handler()); + ConfigurationProviderReceiver confProviderReceiver = new ConfigurationProviderReceiver(new Handler(Looper.getMainLooper())); confProviderReceiver.send(ConfigurationManagerService.NEW_CONFIGURATION_LOADED, bundle); // Load configuration again in asynchronous manner, from central if needed or cache if present. - initAsyncConfigurationLoad(new ConfigurationProviderReceiver(new Handler()), false); + initAsyncConfigurationLoad(new ConfigurationProviderReceiver(new Handler(Looper.getMainLooper())), false); } private boolean isDefaultConfNewerThanCachedConf(ConfigurationProperties confProperties, CachedConfigurationHandler cachedConfHandler) { @@ -238,7 +241,7 @@ private boolean isDefaultConfNewerThanCachedConf(ConfigurationProperties confPro // Following configuration updating should be asynchronous public void updateConfiguration(DiagnosticsView diagnosticsView) { - ConfigurationProviderReceiver confProviderReceiver = new ConfigurationProviderReceiver(new Handler()); + ConfigurationProviderReceiver confProviderReceiver = new ConfigurationProviderReceiver(new Handler(Looper.getMainLooper())); confProviderReceiver.setDiagnosticView(diagnosticsView); initAsyncConfigurationLoad(confProviderReceiver, true); } @@ -301,7 +304,11 @@ void setDiagnosticView(DiagnosticsView diagnosticView) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { - configurationProvider = resultData.getParcelable(ConfigurationConstants.CONFIGURATION_PROVIDER); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + configurationProvider = resultData.getParcelable(ConfigurationConstants.CONFIGURATION_PROVIDER, ConfigurationProvider.class); + } else { + configurationProvider = resultData.getParcelable(ConfigurationConstants.CONFIGURATION_PROVIDER); + } if (resultCode == ConfigurationManagerService.NEW_CONFIGURATION_LOADED) { setupSignLib(); } @@ -316,13 +323,13 @@ protected void onReceiveResult(int resultCode, Bundle resultData) { private ApplicationComponent component; private void setupDagger() { - component = DaggerApplication_ApplicationComponent.builder() + component = DaggerApplicationApp_ApplicationComponent.builder() .application(this) .build(); } public static ApplicationComponent component(Context context) { - return ((Application) context.getApplicationContext()).component; + return ((ApplicationApp) context.getApplicationContext()).component; } @Singleton @@ -417,6 +424,10 @@ static ViewModelProvider.Factory viewModelFactory( @SuppressWarnings("unused") @Binds @IntoMap @ClassKey(EIDHomeViewModel.class) abstract ViewModel eidHomeViewModel(EIDHomeViewModel viewModel); + + @SuppressWarnings("unused") + @Binds @IntoMap @ClassKey(TSACertificateAddViewModel.class) + abstract ViewModel tsaCertificateAddViewModel(TSACertificateAddViewModel viewModel); } @Module diff --git a/app/src/main/java/ee/ria/DigiDoc/android/Constants.java b/app/src/main/java/ee/ria/DigiDoc/android/Constants.java index 9bfb3b375..ff012479d 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/Constants.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/Constants.java @@ -13,6 +13,7 @@ public final class Constants { public static final int RC_CRYPTO_CREATE_INITIAL = 2; public static final int RC_CRYPTO_CREATE_DATA_FILE_ADD = 3; public static final int SAVE_FILE = 4; + public static final int RC_TSA_CERT_ADD = 5; /** * Sub-directory name in {@link android.content.Context#getFilesDir() files dir} for signature diff --git a/app/src/main/java/ee/ria/DigiDoc/android/accessibility/AccessibilityUtils.java b/app/src/main/java/ee/ria/DigiDoc/android/accessibility/AccessibilityUtils.java index 842e04eed..1ebe4dac7 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/accessibility/AccessibilityUtils.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/accessibility/AccessibilityUtils.java @@ -11,6 +11,7 @@ import android.widget.EditText; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.view.AccessibilityDelegateCompat; @@ -19,6 +20,8 @@ import com.google.android.material.textfield.TextInputLayout; +import net.cachapa.expandablelayout.ExpandableLayout; + import ee.ria.DigiDoc.android.Activity; import ee.ria.DigiDoc.common.TextUtil; @@ -42,7 +45,7 @@ public static void interrupt(Context context) { public static void sendAccessibilityEvent(Context context, int eventType, CharSequence message) { AccessibilityManager accessibilityManager = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE); if (accessibilityManager.isEnabled()) { - AccessibilityEvent event = AccessibilityEvent.obtain(); + AccessibilityEvent event = getAccessibilityEvent(); event.setEventType(eventType); event.getText().add(message); accessibilityManager.sendAccessibilityEvent(event); @@ -52,7 +55,7 @@ public static void sendAccessibilityEvent(Context context, int eventType, CharSe public static void sendAccessibilityEvent(Context context, int eventType, CharSequence... messages) { AccessibilityManager accessibilityManager = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE); if (accessibilityManager.isEnabled()) { - AccessibilityEvent event = AccessibilityEvent.obtain(); + AccessibilityEvent event = getAccessibilityEvent(); event.setEventType(eventType); event.getText().add(combineMessages(messages)); accessibilityManager.sendAccessibilityEvent(event); @@ -62,7 +65,7 @@ public static void sendAccessibilityEvent(Context context, int eventType, CharSe public static void sendDelayedAccessibilityEvent(Context context, int eventType, CharSequence message) { AccessibilityManager accessibilityManager = (AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE); if (accessibilityManager.isEnabled()) { - AccessibilityEvent event = AccessibilityEvent.obtain(); + AccessibilityEvent event = getAccessibilityEvent(); event.setEventType(eventType); event.getText().add(message); accessibilityManager.sendAccessibilityEvent(event); @@ -100,7 +103,7 @@ public static void disableContentDescription(View view) { public static void disableDoubleTapToActivateFeedback(View view) { ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() { @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); info.addAction(AccessibilityNodeInfoCompat.ACTION_FOCUS); info.removeAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK); @@ -111,7 +114,7 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo public static void setSingleCharactersContentDescription(TextView textView) { ViewCompat.setAccessibilityDelegate(textView, new AccessibilityDelegateCompat() { @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); StringBuilder textViewAccessibility = new StringBuilder(); String[] personalCodeTextSplit = textView.getText().toString().split(","); @@ -142,6 +145,29 @@ public static boolean isLargeFontEnabled(Resources resources) { return configuration.fontScale > 1; } + public static boolean isSmallFontEnabled(Resources resources) { + Configuration configuration = resources.getConfiguration(); + return configuration.fontScale < 1; + } + + public static void setCustomClickAccessibilityFeedBack(TextView titleView, ExpandableLayout containerView) { + ViewCompat.setAccessibilityDelegate(titleView, new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + String message; + if (containerView.isExpanded()) { + message = "deactivate"; + } else { + message = "activate"; + } + AccessibilityNodeInfoCompat.AccessibilityActionCompat customClick = new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + AccessibilityNodeInfoCompat.ACTION_CLICK, message); + info.addAction(customClick); + } + }); + } + private static String combineMessages(CharSequence... messages) { StringBuilder combinedMessage = new StringBuilder(); for (CharSequence message : messages) { @@ -149,4 +175,12 @@ private static String combineMessages(CharSequence... messages) { } return combinedMessage.toString(); } + + private static AccessibilityEvent getAccessibilityEvent() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + return new AccessibilityEvent(); + } else { + return AccessibilityEvent.obtain(); + } + } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoCreateAdapter.java b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoCreateAdapter.java index 854e0fa1d..99153b8ec 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoCreateAdapter.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoCreateAdapter.java @@ -25,7 +25,7 @@ import java.util.List; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.Formatter; import ee.ria.DigiDoc.android.utils.display.DisplayUtil; @@ -388,7 +388,7 @@ static final class RecipientViewHolder extends CreateViewHolder { RecipientViewHolder(View itemView) { super(itemView); - formatter = Application.component(itemView.getContext()).formatter(); + formatter = ApplicationApp.component(itemView.getContext()).formatter(); AccessibilityUtils.disableDoubleTapToActivateFeedback(itemView.findViewById(R.id.cryptoRecipient)); nameView = itemView.findViewById(R.id.cryptoRecipientName); infoView = itemView.findViewById(R.id.cryptoRecipientInfo); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoCreateScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoCreateScreen.java index 728c1f69f..c51e2c82c 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoCreateScreen.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoCreateScreen.java @@ -1,9 +1,11 @@ package ee.ria.DigiDoc.android.crypto.create; +import static android.view.View.GONE; import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; import static com.jakewharton.rxbinding4.view.RxView.clicks; import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; import static ee.ria.DigiDoc.android.utils.BundleUtils.getFile; +import static ee.ria.DigiDoc.android.utils.BundleUtils.putBoolean; import static ee.ria.DigiDoc.android.utils.BundleUtils.putFile; import static ee.ria.DigiDoc.android.utils.Predicates.duplicates; import static ee.ria.DigiDoc.android.utils.TintUtils.tintCompoundDrawables; @@ -33,7 +35,7 @@ import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.Activity; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.model.idcard.IdCardData; import ee.ria.DigiDoc.android.model.idcard.IdCardDataResponse; @@ -42,6 +44,7 @@ import ee.ria.DigiDoc.android.utils.files.EmptyFileException; import ee.ria.DigiDoc.android.utils.mvi.MviView; import ee.ria.DigiDoc.android.utils.mvi.State; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.navigator.Screen; import ee.ria.DigiDoc.android.utils.widget.ConfirmationDialog; import ee.ria.DigiDoc.android.utils.widget.ErrorDialog; @@ -54,19 +57,21 @@ import io.reactivex.rxjava3.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.Subject; -public final class CryptoCreateScreen extends Controller implements Screen, +public final class CryptoCreateScreen extends Controller implements Screen, ContentView, MviView { private static final String KEY_CONTAINER_FILE = "containerFile"; private static final String KEY_INTENT = "intent"; + private static final String KEY_IS_FROM_SIGNATURE_VIEW = "isFromSignatureView"; public static CryptoCreateScreen create() { return new CryptoCreateScreen(Bundle.EMPTY); } - public static CryptoCreateScreen open(File containerFile) { + public static CryptoCreateScreen open(File containerFile, boolean isFromSignatureView) { Bundle args = new Bundle(); putFile(args, KEY_CONTAINER_FILE, containerFile); + putBoolean(args, KEY_IS_FROM_SIGNATURE_VIEW, isFromSignatureView); return new CryptoCreateScreen(args); } @@ -78,6 +83,7 @@ public static CryptoCreateScreen open(android.content.Intent intent) { @Nullable private File containerFile; @Nullable private final android.content.Intent intent; + private final boolean isFromSignatureView; private final Subject idCardTokenAvailableSubject = PublishSubject.create(); @@ -95,8 +101,10 @@ public static CryptoCreateScreen open(android.content.Intent intent) { private Button encryptButton; private RecyclerView listView; private TextView decryptButton; + private TextView signButton; private TextView sendButton; - private View buttonSpaceView; + private View cryptoButtonSpaceView; + private View signatureButtonSpaceView; private DecryptDialog decryptDialog; private ErrorDialog errorDialog; @@ -116,11 +124,12 @@ public CryptoCreateScreen(Bundle args) { containerFile = args.containsKey(KEY_CONTAINER_FILE) ? getFile(args, KEY_CONTAINER_FILE) : null; - intent = args.getParcelable(KEY_INTENT); + intent = getIntent(args); + isFromSignatureView = args.getBoolean(KEY_IS_FROM_SIGNATURE_VIEW); } private Observable initialIntent() { - return Observable.just(Intent.InitialIntent.create(containerFile, intent)); + return Observable.just(Intent.InitialIntent.create(containerFile, intent, isFromSignatureView)); } @SuppressWarnings("unchecked") @@ -216,6 +225,10 @@ private Observable sendIntent() { return clicks(sendButton).map(ignored -> Intent.SendIntent.create(containerFile)); } + private Observable signIntent() { + return clicks(signButton).map(ignored -> Intent.SignIntent.create(containerFile)); + } + private Observable errorIntents() { return cancels(errorDialog) .map(ignored -> { @@ -236,7 +249,7 @@ public Observable intents() { return Observable.mergeArray(initialIntent(), nameUpdateIntent(), upButtonClickIntent(), dataFilesAddIntent(), dataFileRemoveIntent(), dataFileSaveIntent(), dataFileViewIntent(), recipientsAddButtonClickIntent(), recipientRemoveIntent(), encryptIntent(), decryptionIntent(), decryptIntent(), - sendIntent(), errorIntents()); + signIntent(), sendIntent(), errorIntents()); } @Override @@ -295,10 +308,14 @@ public void render(ViewState state) { encryptButton.setVisibility(state.encryptButtonVisible() ? View.VISIBLE : View.GONE); decryptButton.setVisibility(state.decryptButtonVisible() ? View.VISIBLE : View.GONE); + signButton.setVisibility(state.decryptButtonVisible() && !isFromSignatureView ? View.VISIBLE : View.GONE); sendButton.setVisibility(state.sendButtonVisible() ? View.VISIBLE : View.GONE); - buttonSpaceView.setVisibility(state.sendButtonVisible() && + cryptoButtonSpaceView.setVisibility(state.sendButtonVisible() && (state.encryptButtonVisible() || state.decryptButtonVisible()) ? View.VISIBLE : View.GONE); + signatureButtonSpaceView.setVisibility( + state.sendButtonVisible() && + state.decryptButtonVisible() ? View.VISIBLE : View.GONE); decryptionIdCardDataResponse = state.decryptionIdCardDataResponse(); boolean decryptionPin1Locked = false; @@ -362,17 +379,18 @@ private void showSuccessNotification() { } private void setActivity(boolean activity) { - activityOverlayView.setVisibility(activity ? View.VISIBLE : View.GONE); - activityIndicatorView.setVisibility(activity ? View.VISIBLE : View.GONE); + activityOverlayView.setVisibility(activity ? View.VISIBLE : GONE); + activityIndicatorView.setVisibility(activity ? View.VISIBLE : GONE); encryptButton.setEnabled(!activity); decryptButton.setEnabled(!activity); + signButton.setEnabled(!activity); sendButton.setEnabled(!activity); } @Override protected void onContextAvailable(@NonNull Context context) { super.onContextAvailable(context); - viewModel = Application.component(context).navigator() + viewModel = ApplicationApp.component(context).navigator() .viewModel(getInstanceId(), CryptoCreateViewModel.class); } @@ -397,9 +415,11 @@ protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup activityIndicatorView = view.findViewById(R.id.activityIndicator); encryptButton = view.findViewById(R.id.cryptoCreateEncryptButton); decryptButton = view.findViewById(R.id.cryptoCreateDecryptButton); + signButton = view.findViewById(R.id.cryptoCreateSignatureButton); sendButton = view.findViewById(R.id.cryptoCreateSendButton); sendButton.setContentDescription(getResources().getString(R.string.share_container)); - buttonSpaceView = view.findViewById(R.id.cryptoCreateButtonSpace); + cryptoButtonSpaceView = view.findViewById(R.id.cryptoCreateCryptoButtonSpace); + signatureButtonSpaceView = view.findViewById(R.id.cryptoCreateSignatureButtonSpace); decryptDialog = new DecryptDialog(inflater.getContext()); this.errorDialog = new ErrorDialog(inflater.getContext()); @@ -411,15 +431,23 @@ protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup tintCompoundDrawables(encryptButton, true); tintCompoundDrawables(decryptButton, true); + tintCompoundDrawables(signButton, true); tintCompoundDrawables(sendButton, true); - decryptButton.setContentDescription(getResources().getString(R.string.decrypt_content_description, 1, 2)); - sendButton.setContentDescription(getResources().getString(R.string.decrypt_send_content_description, 2, 2)); + decryptButton.setContentDescription(getResources().getString(R.string.decrypt_content_description, 1, 3)); + signButton.setContentDescription(getResources().getString(R.string.sign_send_content_description, 2, 3)); + sendButton.setContentDescription(getResources().getString(R.string.decrypt_send_content_description, 3, 3)); disposables.attach(); disposables.add(viewModel.viewStates().subscribe(this::render)); viewModel.process(intents()); + ContentView.addInvisibleElement(getApplicationContext(), view); + + View lastElementView = view.findViewById(R.id.lastInvisibleElement); + + ContentView.addInvisibleElementScrollListener(listView, lastElementView); + return view; } @@ -428,7 +456,16 @@ protected void onDestroyView(@NonNull View view) { decryptDialog.dismiss(); errorDialog.dismiss(); sivaConfirmationDialog.dismiss(); + ContentView.removeInvisibleElementScrollListener(listView); disposables.detach(); super.onDestroyView(view); } + + private android.content.Intent getIntent(Bundle bundle) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + return bundle.getParcelable(KEY_INTENT, android.content.Intent.class); + } else { + return bundle.getParcelable(KEY_INTENT); + } + } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoRecipientsScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoRecipientsScreen.java index 62db32eaa..73040bed8 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoRecipientsScreen.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/CryptoRecipientsScreen.java @@ -8,6 +8,7 @@ import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; import static ee.ria.DigiDoc.android.Constants.MAXIMUM_PERSONAL_CODE_LENGTH; import static ee.ria.DigiDoc.android.Constants.VOID; +import static ee.ria.DigiDoc.android.utils.ViewUtil.moveView; import android.content.Context; import android.graphics.Color; @@ -42,12 +43,13 @@ import org.apache.commons.lang3.StringUtils; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.ViewDisposables; import ee.ria.DigiDoc.android.utils.display.DisplayUtil; import ee.ria.DigiDoc.android.utils.mvi.MviView; import ee.ria.DigiDoc.android.utils.mvi.State; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.android.utils.navigator.Screen; import ee.ria.DigiDoc.android.utils.validator.PersonalCodeValidator; @@ -57,7 +59,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.Subject; -public final class CryptoRecipientsScreen extends Controller implements Screen, +public final class CryptoRecipientsScreen extends Controller implements Screen, ContentView, MviView, Navigator.BackButtonClickListener { private static final String KEY_CRYPTO_CREATE_SCREEN_ID = "cryptoCreateScreenId"; @@ -79,6 +81,7 @@ public static CryptoRecipientsScreen create(String cryptoCreateScreenId) { private Toolbar toolbarView; private SearchView searchView; private EditText searchViewInnerText; + private RecyclerView listView; private CryptoCreateAdapter adapter; private View doneButton; private View activityOverlayView; @@ -177,9 +180,9 @@ public boolean onBackButtonClick() { @Override protected void onContextAvailable(@NonNull Context context) { super.onContextAvailable(context); - viewModel = Application.component(context).navigator() + viewModel = ApplicationApp.component(context).navigator() .viewModel(cryptoCreateScreenId, CryptoCreateViewModel.class); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); } @Override @@ -333,13 +336,20 @@ public void afterTextChanged(Editable s) { searchView.setSubmitButtonEnabled(true); searchView.setEnabled(true); - RecyclerView listView = view.findViewById(R.id.cryptoRecipientsList); + listView = view.findViewById(R.id.cryptoRecipientsList); listView.setLayoutManager(new LinearLayoutManager(container.getContext())); listView.setAdapter(adapter = new CryptoCreateAdapter()); listView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); doneButton = view.findViewById(R.id.cryptoRecipientsDoneButton); activityOverlayView = view.findViewById(R.id.activityOverlay); activityIndicatorView = view.findViewById(R.id.activityIndicator); + + ContentView.addInvisibleElement(getApplicationContext(), view); + + View lastElementView = view.findViewById(R.id.lastInvisibleElement); + + ContentView.addInvisibleElementScrollListener(listView, lastElementView); + return view; } @@ -372,6 +382,7 @@ protected void onDetach(@NonNull View view) { disposables.detach(); navigator.removeBackButtonClickListener(this); searchView.setOnCloseListener(null); + ContentView.removeInvisibleElementScrollListener(listView); ee.ria.DigiDoc.android.utils.TextUtil.removeTextWatcher(searchViewInnerText,searchViewTextWatcher); super.onDetach(view); } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/Intent.java b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/Intent.java index a402324e2..addd713b2 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/Intent.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/Intent.java @@ -26,9 +26,12 @@ abstract class InitialIntent implements Intent { @Nullable abstract android.content.Intent intent(); + abstract boolean isFromSignatureView(); + static InitialIntent create(@Nullable File containerFile, - @Nullable android.content.Intent intent) { - return new AutoValue_Intent_InitialIntent(containerFile, intent); + @Nullable android.content.Intent intent, + boolean isFromSignatureView) { + return new AutoValue_Intent_InitialIntent(containerFile, intent, isFromSignatureView); } } @@ -301,4 +304,14 @@ static SendIntent create(File containerFile) { return new AutoValue_Intent_SendIntent(containerFile); } } + + @AutoValue + abstract class SignIntent implements Intent { + + abstract File containerFile(); + + static SignIntent create(File containerFile) { + return new AutoValue_Intent_SignIntent(containerFile); + } + } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/Processor.java b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/Processor.java index 88eb4d34f..3d8e4bede 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/Processor.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/crypto/create/Processor.java @@ -13,6 +13,7 @@ import static ee.ria.DigiDoc.android.utils.IntentUtils.createGetContentIntent; import static ee.ria.DigiDoc.android.utils.IntentUtils.createSaveIntent; import static ee.ria.DigiDoc.android.utils.IntentUtils.parseGetContentIntent; +import static ee.ria.DigiDoc.android.utils.IntentUtils.setIntentData; import static ee.ria.DigiDoc.crypto.CryptoContainer.createContainerFileName; import static ee.ria.DigiDoc.crypto.CryptoContainer.isContainerFileName; @@ -40,6 +41,7 @@ import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.model.idcard.IdCardDataResponse; import ee.ria.DigiDoc.android.model.idcard.IdCardService; +import ee.ria.DigiDoc.android.signature.create.SignatureCreateScreen; import ee.ria.DigiDoc.android.signature.update.SignatureUpdateScreen; import ee.ria.DigiDoc.android.utils.ToastUtil; import ee.ria.DigiDoc.android.utils.files.EmptyFileException; @@ -118,6 +120,8 @@ final class Processor implements ObservableTransformer { private final ObservableTransformer send; + private final ObservableTransformer sign; + @Inject Processor(Navigator navigator, RecipientRepository recipientRepository, ContentResolver contentResolver, FileSystem fileSystem, Application application, IdCardService idCardService) { @@ -139,7 +143,7 @@ final class Processor implements ObservableTransformer { return parseIntent(androidIntent, application, fileSystem.getExternallyOpenedFilesDir()); } else { navigator.execute(Transaction.activityForResult(RC_CRYPTO_CREATE_INITIAL, - createGetContentIntent(), null)); + createGetContentIntent(true), null)); return navigator.activityResults() .filter(activityResult -> activityResult.requestCode() == RC_CRYPTO_CREATE_INITIAL) @@ -149,7 +153,7 @@ final class Processor implements ObservableTransformer { return parseIntent(data, application, fileSystem.getExternallyOpenedFilesDir()) .onErrorReturn(throwable -> { if (throwable instanceof EmptyFileException) { - ToastUtil.showEmptyFileError(navigator.activity(), application); + ToastUtil.showEmptyFileError(navigator.activity()); } navigator.execute(Transaction.pop()); return Result.InitialResult.failure(throwable); @@ -201,7 +205,7 @@ final class Processor implements ObservableTransformer { } navigator.execute(Transaction.activityForResult(RC_CRYPTO_CREATE_DATA_FILE_ADD, - createGetContentIntent(), null)); + createGetContentIntent(true), null)); return navigator.activityResults() .filter(activityResult -> activityResult.requestCode() == RC_CRYPTO_CREATE_DATA_FILE_ADD) @@ -215,7 +219,7 @@ final class Processor implements ObservableTransformer { ImmutableList.Builder builder = ImmutableList.builder().addAll(dataFiles); ImmutableList validFiles = FileSystem.getFilesWithValidSize(fileStreams); - ToastUtil.handleEmptyFileError(validFiles, application, navigator.activity()); + ToastUtil.handleEmptyFileError(validFiles, navigator.activity()); for (FileStream fileStream : validFiles) { File dataFile = fileSystem.cache(fileStream); if (dataFiles.contains(dataFile)) { @@ -311,7 +315,7 @@ final class Processor implements ObservableTransformer { .fromCallable(() -> { File file = intent.dataFile(); if (CryptoContainer.isContainerFileName(file.getName())) { - return Transaction.push(CryptoCreateScreen.open(file)); + return Transaction.push(CryptoCreateScreen.open(file, false)); } else if (SignedContainer.isContainer(navigator.activity(), file)) { return Transaction.push( SignatureUpdateScreen.create(true, true, file, false, false, null, true)); @@ -491,6 +495,13 @@ final class Processor implements ObservableTransformer { .activity(createActionIntent(application, intent.containerFile(), android.content.Intent.ACTION_SEND), null)); return Observable.empty(); }); + + sign = upstream -> upstream.switchMap(signIntent -> { + android.content.Intent intent = new android.content.Intent(); + android.content.Intent intentWithData = setIntentData(intent, signIntent.containerFile().toPath(), navigator.activity()); + navigator.execute(Transaction.push(SignatureCreateScreen.create(intentWithData))); + return Observable.empty(); + }); } @SuppressWarnings("unchecked") @@ -517,6 +528,7 @@ public ObservableSource apply(Observable upstream) { shared.ofType(Intent.EncryptIntent.class).compose(encrypt), shared.ofType(Intent.DecryptionIntent.class).compose(decryption), shared.ofType(Intent.DecryptIntent.class).compose(decrypt), + shared.ofType(Intent.SignIntent.class).compose(sign), shared.ofType(Intent.SendIntent.class).compose(send))); } @@ -525,7 +537,7 @@ private Observable parseIntent(android.content.Intent inte .fromCallable(() -> { ImmutableList validFiles = FileSystem.getFilesWithValidSize( parseGetContentIntent(application.getApplicationContext(), contentResolver, intent, externallyOpenedFileDir)); - ToastUtil.handleEmptyFileError(validFiles, application, application.getApplicationContext()); + ToastUtil.handleEmptyFileError(validFiles, application.getApplicationContext()); if (validFiles.size() == 1 && isContainerFileName(validFiles.get(0).displayName())) { File file = fileSystem.addSignatureContainer(validFiles.get(0)); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/crypto/home/CryptoHomeView.java b/app/src/main/java/ee/ria/DigiDoc/android/crypto/home/CryptoHomeView.java index 648d1792c..512bc35a5 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/crypto/home/CryptoHomeView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/crypto/home/CryptoHomeView.java @@ -9,7 +9,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.crypto.create.CryptoCreateScreen; import ee.ria.DigiDoc.android.main.home.HomeToolbar; @@ -43,7 +43,7 @@ public CryptoHomeView(Context context, @Nullable AttributeSet attrs, int defStyl inflate(context, R.layout.crypto_home, this); toolbarView = findViewById(R.id.toolbar); createButton = findViewById(R.id.cryptoHomeCreateButton); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); AccessibilityUtils.setViewAccessibilityPaneTitle(this, R.string.main_home_navigation_crypto); createButton.postDelayed(() -> { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/eid/CertificateDataView.java b/app/src/main/java/ee/ria/DigiDoc/android/eid/CertificateDataView.java index fd1d05110..86f468f5c 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/eid/CertificateDataView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/eid/CertificateDataView.java @@ -10,7 +10,7 @@ import androidx.annotation.Nullable; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.utils.Formatter; import ee.ria.DigiDoc.common.Certificate; import ee.ria.DigiDoc.idcard.CertificateType; @@ -45,7 +45,7 @@ public CertificateDataView(Context context, @Nullable AttributeSet attrs, int de public CertificateDataView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - formatter = Application.component(context).formatter(); + formatter = ApplicationApp.component(context).formatter(); setOrientation(VERTICAL); inflate(context, R.layout.eid_home_certificate_data, this); titleView = findViewById(R.id.eidHomeCertificateDataTitle); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/eid/CodeUpdateView.java b/app/src/main/java/ee/ria/DigiDoc/android/eid/CodeUpdateView.java index 5d4736e04..f6490cb74 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/eid/CodeUpdateView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/eid/CodeUpdateView.java @@ -31,6 +31,7 @@ import ee.ria.DigiDoc.android.eid.CodeUpdateError.CodeSameAsCurrentError; import ee.ria.DigiDoc.android.eid.CodeUpdateError.CodeTooEasyError; import ee.ria.DigiDoc.android.utils.mvi.State; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import io.reactivex.rxjava3.core.Observable; import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; @@ -38,7 +39,7 @@ import static ee.ria.DigiDoc.android.utils.ErrorMessageUtil.setTextViewError; import static ee.ria.DigiDoc.android.utils.InputMethodUtils.hideSoftKeyboard; -public final class CodeUpdateView extends CoordinatorLayout { +public final class CodeUpdateView extends CoordinatorLayout implements ContentView { private final Toolbar toolbarView; private final TextView successMessageView; @@ -88,6 +89,8 @@ public CodeUpdateView(Context context, @Nullable AttributeSet attrs, int defStyl activityOverlayView = findViewById(R.id.activityOverlay); activityIndicatorView = findViewById(R.id.activityIndicator); scrollView = findViewById(R.id.eidHomeCodeUpdateScroll); + + ContentView.addInvisibleElement(context, this); } public void render(@State String state, CodeUpdateAction action, @@ -219,7 +222,7 @@ public void render(@State String state, CodeUpdateAction action, setTextViewError(getContext(), getResources().getString(action.newDateOfBirthErrorRes()), repeatLabelViewText, repeatLabelView, repeatView); setViewContentDescription(repeatLabelViewText, getResources().getString(action.newDateOfBirthErrorRes())); } else if (newError instanceof CodeTooEasyError) { - setTextViewError(getContext(), getResources().getString(action.newDateOfBirthErrorRes()), repeatLabelViewText, repeatLabelView, repeatView); + setTextViewError(getContext(), getResources().getString(action.newTooEasyErrorRes()), repeatLabelViewText, repeatLabelView, repeatView); setViewContentDescription(repeatLabelViewText, getResources().getString(action.newTooEasyErrorRes())); } else if (newError instanceof CodeSameAsCurrentError) { setTextViewError(getContext(), getResources().getString(action.newSameAsCurrentErrorRes()), repeatLabelViewText, repeatLabelView, repeatView); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/eid/EIDDataView.java b/app/src/main/java/ee/ria/DigiDoc/android/eid/EIDDataView.java index e69d74ac2..dbbb5c6ed 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/eid/EIDDataView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/eid/EIDDataView.java @@ -19,8 +19,7 @@ import net.cachapa.expandablelayout.ExpandableLayout; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; -import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.model.idcard.IdCardData; import ee.ria.DigiDoc.android.utils.Formatter; import ee.ria.DigiDoc.common.TextUtil; @@ -29,10 +28,10 @@ import io.reactivex.rxjava3.core.Observable; import static com.jakewharton.rxbinding4.view.RxView.clicks; +import static ee.ria.DigiDoc.android.accessibility.AccessibilityUtils.setCustomClickAccessibilityFeedBack; import static ee.ria.DigiDoc.android.utils.TintUtils.tintCompoundDrawables; import java.time.LocalDate; -import java.util.Date; public final class EIDDataView extends LinearLayout { @@ -74,7 +73,7 @@ public EIDDataView(Context context, @Nullable AttributeSet attrs, int defStyleAt public EIDDataView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - formatter = Application.component(context).formatter(); + formatter = ApplicationApp.component(context).formatter(); setOrientation(VERTICAL); inflate(context, R.layout.eid_home_data, this); typeView = findViewById(R.id.eidHomeDataType); @@ -126,7 +125,7 @@ public void render(@NonNull IdCardData data, boolean certificateContainerExpande : R.drawable.ic_icon_accordion_collapsed; certificatesTitleView.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, 0, 0, 0); tintCompoundDrawables(certificatesTitleView); - setCustomClickAccessibilityFeedBack(certificatesTitleView); + setCustomClickAccessibilityFeedBack(certificatesTitleView, certificatesContainerView); certificatesContainerView.setExpanded(certificateContainerExpanded); @@ -178,22 +177,4 @@ public Observable actions() { CodeUpdateAction.create(CodeType.PUK, CodeUpdateType.UNBLOCK)) ); } - - private void setCustomClickAccessibilityFeedBack(TextView certificatesTitleView) { - ViewCompat.setAccessibilityDelegate(certificatesTitleView, new AccessibilityDelegateCompat() { - @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { - super.onInitializeAccessibilityNodeInfo(host, info); - String message; - if (certificatesContainerView.isExpanded()) { - message = "deactivate"; - } else { - message = "activate"; - } - AccessibilityNodeInfoCompat.AccessibilityActionCompat customClick = new AccessibilityNodeInfoCompat.AccessibilityActionCompat( - AccessibilityNodeInfoCompat.ACTION_CLICK, message); - info.addAction(customClick); - } - }); - } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/eid/EIDHomeView.java b/app/src/main/java/ee/ria/DigiDoc/android/eid/EIDHomeView.java index ce92f2c4c..e2edacd16 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/eid/EIDHomeView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/eid/EIDHomeView.java @@ -3,16 +3,18 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; -import androidx.annotation.Nullable; -import androidx.coordinatorlayout.widget.CoordinatorLayout; import android.view.accessibility.AccessibilityEvent; import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.Nullable; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + import com.google.common.collect.ImmutableMap; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.main.home.HomeToolbar; import ee.ria.DigiDoc.android.main.home.HomeView; @@ -49,6 +51,7 @@ public final class EIDHomeView extends FrameLayout implements MviView dialog.cancel()); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); } private Observable initialIntent() { @@ -133,7 +137,7 @@ public void render(ViewState state) { if (data != null) { dataView.render(data, state.certificatesContainerExpanded()); } - progressMessageView.setVisibility(data == null ? VISIBLE : GONE); + progressLayout.setVisibility(data == null ? VISIBLE : GONE); dataView.setVisibility(data == null ? GONE : VISIBLE); codeUpdateAction = state.codeUpdateAction(); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/eid/Processor.java b/app/src/main/java/ee/ria/DigiDoc/android/eid/Processor.java index f7f74adda..060dfeb28 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/eid/Processor.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/eid/Processor.java @@ -170,6 +170,8 @@ private static CodeUpdateResponse validate(CodeUpdateAction action, CodeUpdateRe .add(dateOfBirth.format(DateTimeFormatter.ofPattern("yyyy"))) .add(dateOfBirth.format(DateTimeFormatter.ofPattern("MMdd"))) .add(dateOfBirth.format(DateTimeFormatter.ofPattern("ddMM"))) + .add(dateOfBirth.format(DateTimeFormatter.ofPattern("ddMMyyyy"))) + .add(dateOfBirth.format(DateTimeFormatter.ofPattern("yyyyMM"))) .add(dateOfBirth.format(DateTimeFormatter.ofPattern("yyyyMMdd"))); } ImmutableSet dateOfBirthValues = dateOfBirthValuesBuilder.build(); @@ -221,7 +223,6 @@ private static boolean isCodeTooEasy(String code) { // Reset sequence if (Math.abs(d) == 9) { - delta = null; if ((currentNumber == 9 && nextNumber == 0)) { d = -1; } else if ((currentNumber == 0 && nextNumber == 9)) { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/about/AboutAdapter.java b/app/src/main/java/ee/ria/DigiDoc/android/main/about/AboutAdapter.java index d54abaea6..cc7b24be7 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/about/AboutAdapter.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/about/AboutAdapter.java @@ -15,9 +15,10 @@ import ee.ria.DigiDoc.BuildConfig; import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.common.TextUtil; -final class AboutAdapter extends RecyclerView.Adapter { +final class AboutAdapter extends RecyclerView.Adapter implements ContentView { private final ImmutableList components = ImmutableList.builder() .add(Component.create( diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/about/AboutScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/main/about/AboutScreen.java index 148833c11..753454d62 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/about/AboutScreen.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/about/AboutScreen.java @@ -1,5 +1,10 @@ package ee.ria.DigiDoc.android.main.about; +import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; +import static ee.ria.DigiDoc.android.utils.navigator.ContentView.addInvisibleElement; +import static ee.ria.DigiDoc.android.utils.navigator.ContentView.addInvisibleElementScrollListener; +import static ee.ria.DigiDoc.android.utils.navigator.ContentView.removeInvisibleElementScrollListener; + import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -14,20 +19,20 @@ import com.bluelinelabs.conductor.Controller; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.ViewDisposables; import ee.ria.DigiDoc.android.utils.navigator.Screen; import ee.ria.DigiDoc.android.utils.navigator.Transaction; -import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; - public final class AboutScreen extends Controller implements Screen { public static AboutScreen create() { return new AboutScreen(); } + private RecyclerView listView; + private final ViewDisposables disposables = new ViewDisposables(); @SuppressWarnings("WeakerAccess") @@ -45,13 +50,17 @@ protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup toolbarView.setTitle(R.string.main_about_title); toolbarView.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material); toolbarView.setNavigationContentDescription(R.string.back); - RecyclerView listView = view.findViewById(R.id.mainAboutList); + listView = view.findViewById(R.id.mainAboutList); listView.setLayoutManager(new LinearLayoutManager(container.getContext())); listView.setAdapter(new AboutAdapter()); + addInvisibleElement(getApplicationContext(), view); + View lastElementView = view.findViewById(R.id.lastInvisibleElement); + addInvisibleElementScrollListener(listView, lastElementView); + disposables.attach(); disposables.add(navigationClicks(toolbarView).subscribe(ignored -> - Application.component(container.getContext()).navigator().execute(Transaction.pop()))); + ApplicationApp.component(container.getContext()).navigator().execute(Transaction.pop()))); return view; } @@ -59,6 +68,7 @@ protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup @Override protected void onDestroyView(@NonNull View view) { disposables.detach(); + removeInvisibleElementScrollListener(listView); super.onDestroyView(view); } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/accessibility/AccessibilityView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/accessibility/AccessibilityView.java index 8d161d97d..70cfeb762 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/accessibility/AccessibilityView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/accessibility/AccessibilityView.java @@ -1,20 +1,25 @@ package ee.ria.DigiDoc.android.main.accessibility; import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; +import static ee.ria.DigiDoc.android.utils.TextUtil.getInvisibleElementTextView; +import static ee.ria.DigiDoc.android.utils.ViewUtil.findLastElement; import android.content.Context; import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; import android.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.utils.ViewDisposables; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.android.utils.navigator.Transaction; -public class AccessibilityView extends CoordinatorLayout { +public class AccessibilityView extends CoordinatorLayout implements ContentView { private final Toolbar toolbarView; @@ -34,12 +39,14 @@ public AccessibilityView(Context context, AttributeSet attrs, int defStyleAttr) super(context, attrs, defStyleAttr); inflate(context, R.layout.main_accessibility, this); toolbarView = findViewById(R.id.toolbar); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); disposables = new ViewDisposables(); toolbarView.setTitle(R.string.main_accessibility_title); toolbarView.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material); toolbarView.setNavigationContentDescription(R.string.back); + + ContentView.addInvisibleElement(getContext(), this); } @Override diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsScreen.java index 8d430a5b0..07997d962 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsScreen.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsScreen.java @@ -10,10 +10,9 @@ import androidx.annotation.Nullable; import java.io.File; -import java.util.List; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.utils.ViewDisposables; import ee.ria.DigiDoc.android.utils.mvi.MviView; import ee.ria.DigiDoc.android.utils.navigator.Screen; @@ -43,7 +42,7 @@ protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup @Override protected void onContextAvailable(@NonNull Context context) { super.onContextAvailable(context); - viewModel = Application.component(context).navigator() + viewModel = ApplicationApp.component(context).navigator() .viewModel(getInstanceId(), DiagnosticsViewModel.class); } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsView.java index 0ffe7d156..e3b4e9009 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/diagnostics/DiagnosticsView.java @@ -7,6 +7,7 @@ import static ee.ria.DigiDoc.android.main.diagnostics.DiagnosticsScreen.diagnosticsFileSaveClicksSubject; import android.content.Context; +import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; import android.os.Build; @@ -46,12 +47,14 @@ import ee.ria.DigiDoc.BuildConfig; import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.Activity; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.ClickableDialogUtil; import ee.ria.DigiDoc.android.utils.TSLException; import ee.ria.DigiDoc.android.utils.TSLUtil; import ee.ria.DigiDoc.android.utils.ViewDisposables; +import ee.ria.DigiDoc.android.utils.ViewUtil; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.android.utils.navigator.Transaction; import ee.ria.DigiDoc.android.utils.widget.ConfirmationDialog; @@ -69,7 +72,7 @@ import okhttp3.Response; import timber.log.Timber; -public final class DiagnosticsView extends CoordinatorLayout { +public final class DiagnosticsView extends CoordinatorLayout implements ContentView { private final Navigator navigator; private final SimpleDateFormat dateFormat; @@ -92,7 +95,9 @@ public DiagnosticsView(Context context) { AccessibilityUtils.setViewAccessibilityPaneTitle(this, R.string.main_diagnostics_title); toolbarView = findViewById(R.id.toolbar); View saveDiagnosticsButton = findViewById(R.id.configurationSaveButton); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); + + ContentView.addInvisibleElement(getContext(), this); diagnosticsRestartConfirmationDialog = new ConfirmationDialog(navigator.activity(), R.string.main_diagnostics_restart_message, R.id.mainDiagnosticsRestartConfirmationDialog); @@ -104,7 +109,7 @@ public DiagnosticsView(Context context) { (activateLogFileGenerating.isChecked() && FileUtil.logsExist(FileUtil.getLogsDirectory(getContext()))) ? VISIBLE : GONE); - ConfigurationProvider configurationProvider = ((Application) context.getApplicationContext()).getConfigurationProvider(); + ConfigurationProvider configurationProvider = ((ApplicationApp) context.getApplicationContext()).getConfigurationProvider(); disposables = new ViewDisposables(); toolbarView.setTitle(R.string.main_diagnostics_title); @@ -273,8 +278,8 @@ private String getTSLFileVersion(InputStream tslInputStream, String tslFileName) } private void updateConfiguration() { - Application application = (Application) getContext().getApplicationContext(); - application.updateConfiguration(this); + ApplicationApp applicationApp = (ApplicationApp) getContext().getApplicationContext(); + applicationApp.updateConfiguration(this); } private void setData(ConfigurationProvider configurationProvider) { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeMenuView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeMenuView.java index 7472b0816..53116e8b6 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeMenuView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeMenuView.java @@ -28,6 +28,7 @@ import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.utils.display.DisplayUtil; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.common.TextUtil; import io.reactivex.rxjava3.core.Observable; @@ -35,7 +36,7 @@ import static com.jakewharton.rxbinding4.widget.RxRadioGroup.checkedChanges; import static ee.ria.DigiDoc.android.utils.TintUtils.tintCompoundDrawables; -public final class HomeMenuView extends NestedScrollView { +public final class HomeMenuView extends NestedScrollView implements ContentView { private final View closeButton; @@ -139,6 +140,8 @@ public HomeMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int }, 2000); setFontSize(); + + ContentView.addInvisibleElement(getContext(), this); } @Override diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeScreen.java index 0aee2b018..f3468e22d 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeScreen.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeScreen.java @@ -23,11 +23,19 @@ public static HomeScreen create(Intent intent) { @SuppressWarnings("WeakerAccess") public HomeScreen(Bundle args) { super(R.id.mainHomeScreen, args); - intent = args.getParcelable(KEY_INTENT); + intent = getIntent(args); } @Override protected View view(Context context) { return new HomeView(context, intent, getInstanceId()); } + + private android.content.Intent getIntent(Bundle bundle) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + return bundle.getParcelable(KEY_INTENT, android.content.Intent.class); + } else { + return bundle.getParcelable(KEY_INTENT); + } + } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeView.java index 72a08caf1..920433634 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/home/HomeView.java @@ -17,23 +17,25 @@ import com.google.android.material.internal.BaselineLayout; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.crypto.home.CryptoHomeView; import ee.ria.DigiDoc.android.eid.EIDHomeView; import ee.ria.DigiDoc.android.signature.home.SignatureHomeView; import ee.ria.DigiDoc.android.utils.TextUtil; import ee.ria.DigiDoc.android.utils.ViewDisposables; import ee.ria.DigiDoc.android.utils.mvi.MviView; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.Subject; import static com.jakewharton.rxbinding4.material.RxBottomNavigationView.itemSelections; import static ee.ria.DigiDoc.android.utils.Predicates.duplicates; +import static ee.ria.DigiDoc.android.utils.ViewUtil.moveView; import static ee.ria.DigiDoc.android.utils.rxbinding.app.RxDialog.cancels; @SuppressLint("ViewConstructor") -public final class HomeView extends LinearLayout implements MviView { +public final class HomeView extends LinearLayout implements ContentView, MviView { public interface HomeViewChild { @@ -90,9 +92,14 @@ public HomeView(Context context, android.content.Intent intent, String screenId) menuDialog = new HomeMenuDialog(context); menuView = menuDialog.getMenuView(); - viewModel = Application.component(context).navigator().viewModel(screenId, + viewModel = ApplicationApp.component(context).navigator().viewModel(screenId, HomeViewModel.class); viewModel.eidScreenId(eidScreenId); + + ContentView.addInvisibleElement(getContext(), navigationContainerView); + + View lastElementView = findViewById(R.id.lastInvisibleElement); + moveView(lastElementView); } private void setCustomAccessibilityFeedback(BottomNavigationItemView bottomNavigationItemView) { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/home/Processor.java b/app/src/main/java/ee/ria/DigiDoc/android/main/home/Processor.java index 07c675bc1..253cc6107 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/home/Processor.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/home/Processor.java @@ -1,5 +1,9 @@ package ee.ria.DigiDoc.android.main.home; +import static android.content.Intent.ACTION_VIEW; +import static ee.ria.DigiDoc.android.utils.IntentUtils.createBrowserIntent; +import static ee.ria.DigiDoc.android.utils.IntentUtils.parseGetContentIntent; + import android.app.Application; import android.content.ContentResolver; import android.content.Context; @@ -40,10 +44,6 @@ import io.reactivex.rxjava3.core.ObservableSource; import io.reactivex.rxjava3.core.ObservableTransformer; -import static android.content.Intent.ACTION_VIEW; -import static ee.ria.DigiDoc.android.utils.IntentUtils.createBrowserIntent; -import static ee.ria.DigiDoc.android.utils.IntentUtils.parseGetContentIntent; - final class Processor implements ObservableTransformer { private static final SparseIntArray NAVIGATION_ITEM_VIEWS = new SparseIntArray(); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsDataStore.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsDataStore.java index 4a347359f..ebf0e7fa0 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsDataStore.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsDataStore.java @@ -3,6 +3,7 @@ import android.app.Application; import android.content.SharedPreferences; import android.content.res.Resources; + import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; @@ -182,4 +183,23 @@ public void setIsLogFileGenerationRunning(boolean isRunning) { editor.commit(); } + public void setTSACertName(String cert) { + SharedPreferences.Editor editor = preferences.edit(); + editor.putString(resources.getString(R.string.main_settings_tsa_cert_key), cert); + editor.commit(); + } + + public String getTSACertName() { + return preferences.getString(resources.getString(R.string.main_settings_tsa_cert_key), ""); + } + + public void setIsTsaCertificateViewVisible(boolean isVisible) { + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(resources.getString(R.string.main_settings_tsa_cert_view), isVisible); + editor.commit(); + } + + public boolean getIsTsaCertificateViewVisible() { + return preferences.getBoolean(resources.getString(R.string.main_settings_tsa_cert_view), false); + } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsScreen.java index b4be7a102..75f83dfe2 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsScreen.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsScreen.java @@ -25,19 +25,4 @@ public SettingsScreen() { protected View view(Context context) { return new SettingsView(context); } - - @Override - protected void onDestroyView(@NonNull View view) { - FragmentActivity activity = (FragmentActivity) getActivity(); - if (activity != null) { - FragmentManager fragmentManager = activity.getSupportFragmentManager(); - Fragment fragment = fragmentManager.findFragmentById(R.id.mainSettingsFragment); - if (fragment != null) { - fragmentManager.beginTransaction() - .remove(fragment) - .commitAllowingStateLoss(); - } - } - super.onDestroyView(view); - } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsView.java index 9f824b882..d8b1c4fa3 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsView.java @@ -1,30 +1,29 @@ package ee.ria.DigiDoc.android.main.settings; -import android.content.ComponentName; +import static com.jakewharton.rxbinding4.view.RxView.clicks; +import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; +import static ee.ria.DigiDoc.android.main.settings.util.SettingsUtil.getToolbarViewTitle; + import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; import android.util.AttributeSet; -import android.view.View; +import android.widget.Button; import android.widget.TextView; import android.widget.Toolbar; -import androidx.appcompat.widget.SwitchCompat; import androidx.coordinatorlayout.widget.CoordinatorLayout; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Activity; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; +import ee.ria.DigiDoc.android.main.settings.access.SettingsAccessScreen; import ee.ria.DigiDoc.android.utils.ViewDisposables; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.android.utils.navigator.Transaction; -import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; - -public final class SettingsView extends CoordinatorLayout { +public final class SettingsView extends CoordinatorLayout implements ContentView { private final Toolbar toolbarView; - private final TextView toolbarTitleView; + private final Button accessCategory; private final Navigator navigator; @@ -42,10 +41,12 @@ public SettingsView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); inflate(context, R.layout.main_settings, this); toolbarView = findViewById(R.id.toolbar); - toolbarTitleView = getToolbarViewTitle(); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); + TextView toolbarTitleView = getToolbarViewTitle(toolbarView); disposables = new ViewDisposables(); + accessCategory = findViewById(R.id.mainSettingsAccessCategory); + toolbarView.setTitle(R.string.main_settings_title); toolbarView.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material); toolbarView.setNavigationContentDescription(R.string.back); @@ -53,47 +54,6 @@ public SettingsView(Context context, AttributeSet attrs, int defStyleAttr) { if (toolbarTitleView != null) { toolbarTitleView.setContentDescription("\u202F"); } - - Activity activityContext = (Activity) this.getContext(); - SwitchCompat openAllFileTypesSwitch = findViewById(R.id.mainSettingsOpenAllFileTypes); - if (openAllFileTypesSwitch != null && activityContext != null) { - openAllFileTypesSwitch.setChecked(activityContext.getSettingsDataStore().getIsOpenAllFileTypesEnabled()); - - openAllFileTypesSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { - activityContext.getSettingsDataStore().setIsOpenAllFileTypesEnabled(isChecked); - restartIntent(); - }); - } - - SwitchCompat allowScreenshotsSwitch = findViewById(R.id.mainSettingsAllowScreenshots); - if (allowScreenshotsSwitch != null && activityContext != null) { - allowScreenshotsSwitch.setChecked(activityContext.getSettingsDataStore().getIsScreenshotAllowed()); - - allowScreenshotsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { - activityContext.getSettingsDataStore().setIsScreenshotAllowed(isChecked); - restartIntent(); - }); - } - } - - private TextView getToolbarViewTitle() { - for (int i = 0; i < toolbarView.getChildCount(); i++) { - View childView = toolbarView.getChildAt(i); - if (childView instanceof TextView) { - return (TextView) childView; - } - } - - return null; - } - - private void restartIntent() { - PackageManager packageManager = getContext().getPackageManager(); - Intent intent = packageManager.getLaunchIntentForPackage(getContext().getPackageName()); - ComponentName componentName = intent.getComponent(); - Intent restartIntent = Intent.makeRestartActivityTask(componentName); - restartIntent.setAction(Intent.ACTION_CONFIGURATION_CHANGED); - getContext().startActivity(restartIntent); } @Override @@ -102,6 +62,10 @@ public void onAttachedToWindow() { disposables.attach(); disposables.add(navigationClicks(toolbarView).subscribe(o -> navigator.execute(Transaction.pop()))); + disposables.add(clicks(accessCategory).subscribe(o -> + navigator.execute( + Transaction.push(SettingsAccessScreen.create()))) + ); } @Override diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/SettingsAccessScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/SettingsAccessScreen.java new file mode 100644 index 000000000..6577a7b28 --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/SettingsAccessScreen.java @@ -0,0 +1,44 @@ +package ee.ria.DigiDoc.android.main.settings.access; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; + +import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.android.utils.navigator.conductor.ConductorScreen; + +public final class SettingsAccessScreen extends ConductorScreen { + + public static SettingsAccessScreen create() { + return new SettingsAccessScreen(); + } + + @SuppressWarnings("WeakerAccess") + public SettingsAccessScreen() { + super(R.id.mainSettingsAccessScreen); + } + + @Override + protected View view(Context context) { + return new SettingsAccessView(context); + } + + @Override + protected void onDestroyView(@NonNull View view) { + FragmentActivity activity = (FragmentActivity) getActivity(); + if (activity != null) { + FragmentManager fragmentManager = activity.getSupportFragmentManager(); + Fragment fragment = fragmentManager.findFragmentById(R.id.mainSettingsFragment); + if (fragment != null) { + fragmentManager.beginTransaction() + .remove(fragment) + .commitAllowingStateLoss(); + } + } + super.onDestroyView(view); + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/SettingsAccessView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/SettingsAccessView.java new file mode 100644 index 000000000..3e3bc94f8 --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/SettingsAccessView.java @@ -0,0 +1,242 @@ +package ee.ria.DigiDoc.android.main.settings.access; + +import static com.jakewharton.rxbinding4.view.RxView.clicks; +import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; +import static ee.ria.DigiDoc.android.main.settings.util.SettingsUtil.getToolbarViewTitle; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toolbar; + +import androidx.appcompat.widget.SwitchCompat; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; + +import java.io.File; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.android.Activity; +import ee.ria.DigiDoc.android.ApplicationApp; +import ee.ria.DigiDoc.android.main.settings.SettingsDataStore; +import ee.ria.DigiDoc.android.main.settings.create.ChooseFileScreen; +import ee.ria.DigiDoc.android.main.settings.create.TSACertificateAddViewModel; +import ee.ria.DigiDoc.android.main.settings.create.ViewState; +import ee.ria.DigiDoc.android.signature.detail.CertificateDetailScreen; +import ee.ria.DigiDoc.android.utils.ViewDisposables; +import ee.ria.DigiDoc.android.utils.navigator.Navigator; +import ee.ria.DigiDoc.android.utils.navigator.Transaction; +import ee.ria.DigiDoc.common.CertificateUtil; +import ee.ria.DigiDoc.common.FileUtil; +import ee.ria.DigiDoc.configuration.util.FileUtils; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.subjects.Subject; +import timber.log.Timber; + +public final class SettingsAccessView extends CoordinatorLayout { + + private final Toolbar toolbarView; + private final LinearLayout tsaCertContainer; + private final TextView tsaCertIssuedTo; + private final TextView tsaCertValidTo; + private final Button addCertificateButton; + private final Button showCertificateButton; + private X509Certificate tsaCertificate; + + private final Navigator navigator; + private final SettingsDataStore settingsDataStore; + + private final ViewDisposables disposables; + private final TSACertificateAddViewModel viewModel; + + private static boolean isTsaCertificateViewVisible; + private static final Subject isTsaCertificateViewVisibleSubject = PublishSubject.create(); + + String viewId = String.valueOf(View.generateViewId()); + + public SettingsAccessView(Context context) { + this(context, null); + } + + public SettingsAccessView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SettingsAccessView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + viewModel = ApplicationApp.component(context).navigator() + .viewModel(viewId, TSACertificateAddViewModel.class); + inflate(context, R.layout.main_settings_access, this); + toolbarView = findViewById(R.id.toolbar); + TextView toolbarTitleView = getToolbarViewTitle(toolbarView); + navigator = ApplicationApp.component(context).navigator(); + settingsDataStore = ApplicationApp.component(context).settingsDataStore(); + disposables = new ViewDisposables(); + + toolbarView.setTitle(R.string.main_settings_access_button); + toolbarView.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material); + toolbarView.setNavigationContentDescription(R.string.back); + + if (toolbarTitleView != null) { + toolbarTitleView.setContentDescription("\u202F"); + } + + tsaCertContainer = findViewById(R.id.mainSettingsTsaCertificateContainer); + tsaCertIssuedTo = findViewById(R.id.mainSettingsTsaCertificateIssuedTo); + tsaCertValidTo = findViewById(R.id.mainSettingsTsaCertificateValidTo); + addCertificateButton = findViewById(R.id.mainSettingsTsaCertificateAddCertificateButton); + showCertificateButton = findViewById(R.id.mainSettingsTsaCertificateShowCertificateButton); + + if (settingsDataStore != null) { + isTsaCertificateViewVisible = settingsDataStore.getIsTsaCertificateViewVisible(); + setTSAContainerViewVisibility(isTsaCertificateViewVisible); + } + + Activity activityContext = (Activity) this.getContext(); + SwitchCompat openAllFileTypesSwitch = findViewById(R.id.mainSettingsOpenAllFileTypes); + if (openAllFileTypesSwitch != null && activityContext != null) { + openAllFileTypesSwitch.setChecked(activityContext.getSettingsDataStore().getIsOpenAllFileTypesEnabled()); + + openAllFileTypesSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + activityContext.getSettingsDataStore().setIsOpenAllFileTypesEnabled(isChecked); + restartIntent(); + }); + } + + SwitchCompat allowScreenshotsSwitch = findViewById(R.id.mainSettingsAllowScreenshots); + if (allowScreenshotsSwitch != null && activityContext != null) { + allowScreenshotsSwitch.setChecked(activityContext.getSettingsDataStore().getIsScreenshotAllowed()); + + allowScreenshotsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + activityContext.getSettingsDataStore().setIsScreenshotAllowed(isChecked); + restartIntent(); + }); + } + } + + private void restartIntent() { + PackageManager packageManager = getContext().getPackageManager(); + Intent intent = packageManager.getLaunchIntentForPackage(getContext().getPackageName()); + ComponentName componentName = intent.getComponent(); + Intent restartIntent = Intent.makeRestartActivityTask(componentName); + restartIntent.setAction(Intent.ACTION_CONFIGURATION_CHANGED); + getContext().startActivity(restartIntent); + } + + public void render(ViewState state) { + if (settingsDataStore != null) { + String tsaCertName = settingsDataStore.getTSACertName(); + + File tsaFile = FileUtil.getTSAFile(getContext(), tsaCertName); + + if (tsaFile != null) { + String fileContents = FileUtils.readFileContent(tsaFile.getPath()); + try { + tsaCertificate = CertificateUtil.x509Certificate(fileContents); + X509CertificateHolder certificateHolder = new JcaX509CertificateHolder(tsaCertificate); + String issuer = getIssuer(certificateHolder); + tsaCertIssuedTo.setText(String.format("%s %s", + getResources().getText(R.string.main_settings_timestamp_cert_issued_to_title), + issuer)); + tsaCertValidTo.setText(String.format("%s %s", + getResources().getText(R.string.main_settings_timestamp_cert_valid_to_title), + getFormattedDateTime(certificateHolder.getNotAfter()))); + } catch (CertificateException e) { + Timber.log(Log.ERROR, e, "Unable to get TSA certificate"); + + // Remove invalid files + FileUtils.removeFile(tsaFile.getPath()); + settingsDataStore.setTSACertName(null); + + tsaCertIssuedTo.setText(getResources().getText(R.string.main_settings_timestamp_cert_issued_to_title)); + tsaCertValidTo.setText(getResources().getText(R.string.main_settings_timestamp_cert_valid_to_title)); + } + } + } + } + + private String getIssuer(X509CertificateHolder certificateHolder) { + RDN[] organizationRDNs = certificateHolder.getIssuer().getRDNs(BCStyle.O); + if (organizationRDNs.length > 0) { + return organizationRDNs[0].getFirst().getValue().toString(); + } + RDN[] organizationUnitRDNs = certificateHolder.getIssuer().getRDNs(BCStyle.OU); + if (organizationUnitRDNs.length > 0) { + return organizationUnitRDNs[0].getFirst().getValue().toString(); + } + RDN[] commonNameRDNs = certificateHolder.getIssuer().getRDNs(BCStyle.CN); + if (commonNameRDNs.length > 0) { + return commonNameRDNs[0].getFirst().getValue().toString(); + } + + return "-"; + } + + private static String getFormattedDateTime(Date date) { + try { + SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()); + if (date != null) { + return dateFormat.format(date); + } + } catch (IllegalStateException e) { + Timber.log(Log.ERROR, e, "Unable to format date"); + } + return "-"; + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + disposables.attach(); + disposables.add(navigationClicks(toolbarView).subscribe(o -> + navigator.execute(Transaction.pop()))); + disposables.add(clicks(showCertificateButton).subscribe(o -> { + if (tsaCertificate != null) { + navigator.execute(Transaction.push(CertificateDetailScreen.create(tsaCertificate))); + } + })); + disposables.add(clicks(addCertificateButton).subscribe(o -> + navigator.execute(Transaction.push(ChooseFileScreen.create())))); + disposables.add(observeTsaCertificateViewVisibleChanges().subscribe(isVisible -> { + settingsDataStore.setIsTsaCertificateViewVisible(isVisible); + setTSAContainerViewVisibility(isVisible); + })); + disposables.add(viewModel.viewStates().subscribe(this::render)); + } + + @Override + public void onDetachedFromWindow() { + disposables.detach(); + super.onDetachedFromWindow(); + } + + public static void setTsaCertificateViewVisibleValue(boolean value) { + isTsaCertificateViewVisible = value; + isTsaCertificateViewVisibleSubject.onNext(value); + } + + public static Observable observeTsaCertificateViewVisibleChanges() { + return isTsaCertificateViewVisibleSubject; + } + + private void setTSAContainerViewVisibility(boolean isVisible) { + tsaCertContainer.setVisibility(!isVisible ? GONE : VISIBLE); + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsFragment.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/SettingsFragment.java similarity index 97% rename from app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsFragment.java rename to app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/SettingsFragment.java index 9760e169d..83c039858 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/SettingsFragment.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/SettingsFragment.java @@ -1,4 +1,4 @@ -package ee.ria.DigiDoc.android.main.settings; +package ee.ria.DigiDoc.android.main.settings.access; import android.os.Bundle; import android.view.View; @@ -24,7 +24,7 @@ public final class SettingsFragment extends PreferenceFragmentCompat { @Override public void onCreatePreferencesFix(@Nullable Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.main_settings, null); + setPreferencesFromResource(R.xml.main_settings_access, null); } @Override diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/TsaUrlPreference.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/TsaUrlPreference.java similarity index 86% rename from app/src/main/java/ee/ria/DigiDoc/android/main/settings/TsaUrlPreference.java rename to app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/TsaUrlPreference.java index 8916ff5bc..b8b90b442 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/TsaUrlPreference.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/TsaUrlPreference.java @@ -1,4 +1,4 @@ -package ee.ria.DigiDoc.android.main.settings; +package ee.ria.DigiDoc.android.main.settings.access; import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; @@ -6,10 +6,7 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; -import android.view.View; -import android.view.ViewGroup; import android.widget.CheckBox; -import android.widget.LinearLayout; import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatCheckBox; @@ -17,8 +14,9 @@ import com.takisoft.preferencex.EditTextPreference; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; +import ee.ria.DigiDoc.android.main.settings.access.SettingsAccessView; import ee.ria.DigiDoc.configuration.ConfigurationProvider; public class TsaUrlPreference extends EditTextPreference { @@ -41,7 +39,7 @@ public TsaUrlPreference(Context context, @Nullable AttributeSet attrs, int defSt public TsaUrlPreference(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - configurationProvider = ((Application) context.getApplicationContext()).getConfigurationProvider(); + configurationProvider = ((ApplicationApp) context.getApplicationContext()).getConfigurationProvider(); checkBox = new AppCompatCheckBox(context); checkBox.setId(android.R.id.checkbox); checkBox.setText(R.string.main_settings_tsa_url_use_default); @@ -52,6 +50,7 @@ public TsaUrlPreference(Context context, @Nullable AttributeSet attrs, int defSt setViewId(R.id.mainSettingsAccessToTimeStampingService); setOnPreferenceChangeListener((preference, newValue) -> { + SettingsAccessView.setTsaCertificateViewVisibleValue(!checkBox.isChecked()); AccessibilityUtils.sendAccessibilityEvent(context, TYPE_ANNOUNCEMENT, R.string.setting_value_changed); return true; }); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/TsaUrlPreferenceDialogFragment.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/TsaUrlPreferenceDialogFragment.java similarity index 93% rename from app/src/main/java/ee/ria/DigiDoc/android/main/settings/TsaUrlPreferenceDialogFragment.java rename to app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/TsaUrlPreferenceDialogFragment.java index 1438022e8..baa9fa5b8 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/TsaUrlPreferenceDialogFragment.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/TsaUrlPreferenceDialogFragment.java @@ -1,4 +1,4 @@ -package ee.ria.DigiDoc.android.main.settings; +package ee.ria.DigiDoc.android.main.settings.access; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; @@ -16,7 +16,7 @@ import androidx.preference.EditTextPreferenceDialogFragmentCompat; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.SecureUtil; import ee.ria.DigiDoc.android.utils.TextUtil; @@ -30,7 +30,7 @@ protected void onBindDialogView(View view) { super.onBindDialogView(view); TsaUrlPreference tsaUrlPreference = getTsaUrlPreference(); if (tsaUrlPreference != null) { - ConfigurationProvider configurationProvider = ((Application) getContext().getApplicationContext()).getConfigurationProvider(); + ConfigurationProvider configurationProvider = ((ApplicationApp) getContext().getApplicationContext()).getConfigurationProvider(); CheckBox checkBox = tsaUrlPreference.getCheckBox(); AppCompatEditText appCompatEditText = TextUtil.getTextView(view); @@ -50,6 +50,8 @@ protected void onBindDialogView(View view) { checkBox.setChecked(TextUtils.isEmpty(tsaUrlPreference.getText())); + SettingsAccessView.setTsaCertificateViewVisibleValue(!checkBox.isChecked()); + ViewGroup parent = ((ViewGroup) appCompatEditText.getParent()); View oldCheckBox = appCompatEditText.findViewById(checkBox.getId()); if (oldCheckBox != null) { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/UUIDPreference.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/UUIDPreference.java similarity index 98% rename from app/src/main/java/ee/ria/DigiDoc/android/main/settings/UUIDPreference.java rename to app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/UUIDPreference.java index 797fb46fb..7fffc7c1a 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/UUIDPreference.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/UUIDPreference.java @@ -18,7 +18,7 @@ * */ -package ee.ria.DigiDoc.android.main.settings; +package ee.ria.DigiDoc.android.main.settings.access; import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/UUIDPreferenceDialogFragment.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/UUIDPreferenceDialogFragment.java similarity index 98% rename from app/src/main/java/ee/ria/DigiDoc/android/main/settings/UUIDPreferenceDialogFragment.java rename to app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/UUIDPreferenceDialogFragment.java index e86ba7bee..8293dee6d 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/UUIDPreferenceDialogFragment.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/access/UUIDPreferenceDialogFragment.java @@ -18,7 +18,7 @@ * */ -package ee.ria.DigiDoc.android.main.settings; +package ee.ria.DigiDoc.android.main.settings.access; import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Action.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Action.java new file mode 100644 index 000000000..0747d89cd --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Action.java @@ -0,0 +1,35 @@ +package ee.ria.DigiDoc.android.main.settings.create; + +import static ee.ria.DigiDoc.android.Constants.RC_TSA_CERT_ADD; +import static ee.ria.DigiDoc.android.utils.IntentUtils.createGetContentIntent; + +import androidx.annotation.Nullable; + +import com.google.auto.value.AutoValue; + +import ee.ria.DigiDoc.android.utils.mvi.MviAction; +import ee.ria.DigiDoc.android.utils.navigator.Transaction; +import ee.ria.DigiDoc.android.utils.navigator.TransactionAction; + +interface Action extends MviAction { + + @AutoValue + abstract class InitialAction implements Action { + + static InitialAction create() { + return new AutoValue_Action_InitialAction(); + } + } + + @AutoValue + abstract class ChooseFileAction implements Action, + TransactionAction { + + @Nullable abstract android.content.Intent intent(); + + static ChooseFileAction create() { + return new AutoValue_Action_ChooseFileAction(Transaction.activityForResult( + RC_TSA_CERT_ADD, createGetContentIntent(false), null), null); + } + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/ChooseFileScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/ChooseFileScreen.java new file mode 100644 index 000000000..c12b38bae --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/ChooseFileScreen.java @@ -0,0 +1,86 @@ +package ee.ria.DigiDoc.android.main.settings.create; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.android.ApplicationApp; +import ee.ria.DigiDoc.android.utils.ViewDisposables; +import ee.ria.DigiDoc.android.utils.mvi.MviView; +import ee.ria.DigiDoc.android.utils.navigator.Navigator; +import ee.ria.DigiDoc.android.utils.navigator.Screen; +import ee.ria.DigiDoc.android.utils.navigator.Transaction; +import ee.ria.DigiDoc.android.utils.navigator.conductor.ConductorScreen; +import io.reactivex.rxjava3.core.Observable; + +public final class ChooseFileScreen extends ConductorScreen implements Screen, MviView { + + public static ChooseFileScreen create() { + return new ChooseFileScreen(); + } + + @SuppressWarnings("WeakerAccess") + public ChooseFileScreen() { + super(R.id.mainSettingsChooseFileScreen); + } + + @Override + protected View view(Context context) { + return new View(context); + } + + @Override + protected void onDestroyView(@NonNull View view) { + super.onDestroyView(view); + } + + @Override + public Observable intents() { + return Observable.mergeArray(chooseFileIntent()); + } + + private final ViewDisposables disposables = new ViewDisposables(); + private TSACertificateAddViewModel viewModel; + + @NonNull + @Override + protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container, @Nullable Bundle savedViewState) { + disposables.attach(); + disposables.add(viewModel.viewStates().subscribe(this::render)); + viewModel.process(intents()); + + return super.onCreateView(inflater, container, savedViewState); + } + + @Override + protected void onContextAvailable(@NonNull Context context) { + super.onContextAvailable(context); + viewModel = ApplicationApp.component(context).navigator() + .viewModel(getInstanceId(), TSACertificateAddViewModel.class); + } + + @Override + protected void onContextUnavailable() { + viewModel = null; + super.onContextUnavailable(); + } + + @Override + public void render(ViewState state) { + + if (state.context() != null) { + Navigator navigator = ApplicationApp.component(state.context()).navigator(); + navigator.execute(Transaction.pop()); + } + } + + private Observable chooseFileIntent() { + return Observable.just(new Intent.ChooseFileIntent().create()); + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Intent.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Intent.java new file mode 100644 index 000000000..145a1ee63 --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Intent.java @@ -0,0 +1,32 @@ +package ee.ria.DigiDoc.android.main.settings.create; + +import com.google.auto.value.AutoValue; + +import ee.ria.DigiDoc.android.utils.mvi.MviIntent; + +public interface Intent extends MviIntent { + + @AutoValue + class ChooseFileIntent implements Intent { + + public static ChooseFileIntent clear() { + return null; + } + + public ChooseFileIntent create() { + return new AutoValue_Intent_ChooseFileIntent(); + } + } + + @AutoValue + class OpenAccessIntent implements Intent { + + public static OpenAccessIntent clear() { + return null; + } + + public OpenAccessIntent create() { + return new AutoValue_Intent_OpenAccessIntent(); + } + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Processor.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Processor.java new file mode 100644 index 000000000..dc6a3d967 --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Processor.java @@ -0,0 +1,141 @@ +package ee.ria.DigiDoc.android.main.settings.create; + +import static android.app.Activity.RESULT_OK; +import static ee.ria.DigiDoc.android.utils.IntentUtils.parseGetContentIntent; +import static ee.ria.DigiDoc.common.CommonConstants.DIR_TSA_CERT; + +import android.app.Application; +import android.util.Log; + +import androidx.documentfile.provider.DocumentFile; + +import com.google.common.collect.ImmutableList; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.InputStream; +import java.util.List; + +import javax.inject.Inject; + +import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.android.main.settings.SettingsDataStore; +import ee.ria.DigiDoc.android.utils.ToastUtil; +import ee.ria.DigiDoc.android.utils.files.EmptyFileException; +import ee.ria.DigiDoc.android.utils.files.FileStream; +import ee.ria.DigiDoc.android.utils.files.FileSystem; +import ee.ria.DigiDoc.android.utils.navigator.ActivityResult; +import ee.ria.DigiDoc.android.utils.navigator.ActivityResultException; +import ee.ria.DigiDoc.android.utils.navigator.Navigator; +import ee.ria.DigiDoc.android.utils.navigator.Transaction; +import ee.ria.DigiDoc.common.ActivityUtil; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.ObservableSource; +import io.reactivex.rxjava3.core.ObservableTransformer; +import io.reactivex.rxjava3.exceptions.CompositeException; +import timber.log.Timber; + +final class Processor implements ObservableTransformer { + + private final ObservableTransformer + chooseFile; + + @Inject Processor(Navigator navigator, + Application application, + SettingsDataStore settingsDataStore, + FileSystem fileSystem) { + + chooseFile = upstream -> upstream + .switchMap(action -> { + if (action.intent() != null) { + throw new ActivityResultException(ActivityResult.create( + action.transaction().getRequestCode(), RESULT_OK, action.intent())); + } + navigator.execute(action.transaction()); + return navigator.activityResults() + .filter(activityResult -> + activityResult.requestCode() + == action.transaction().getRequestCode()) + .doOnNext(activityResult -> { + throw new ActivityResultException(activityResult); + }) + .map(activityResult -> Result.ChooseFileResult.create(navigator.activity())); + }) + .onErrorResumeNext(throwable -> { + if (!(throwable instanceof ActivityResultException)) { + return Observable.error(throwable); + } + ActivityResult activityResult = ((ActivityResultException) throwable) + .activityResult; + if (activityResult.resultCode() == RESULT_OK) { + if (activityResult.data() != null) { + ImmutableList validFiles = FileSystem.getFilesWithValidSize( + parseGetContentIntent(navigator.activity(), application.getContentResolver(), activityResult.data(), fileSystem.getExternallyOpenedFilesDir())); + ToastUtil.handleEmptyFileError(validFiles, navigator.activity()); + + try (InputStream initialStream = application.getContentResolver().openInputStream(activityResult.data().getData())) { + DocumentFile documentFile = DocumentFile.fromSingleUri(navigator.activity(), activityResult.data().getData()); + if (documentFile != null) { + File tsaCertFolder = new File(navigator.activity().getFilesDir(), DIR_TSA_CERT); + if (!tsaCertFolder.exists()) { + boolean isFolderCreated = tsaCertFolder.mkdirs(); + Timber.log(Log.DEBUG, String.format("TSA cert folder created: %s", isFolderCreated)); + } + + String fileName = documentFile.getName(); + if (fileName == null || fileName.isEmpty()) { + fileName = "tsaCert"; + } + File tsaFile = new File(tsaCertFolder, fileName); + + FileUtils.copyInputStreamToFile(initialStream, tsaFile); + + settingsDataStore.setTSACertName(tsaFile.getName()); + + return Observable.just(Result.ChooseFileResult.create(navigator.activity())); + } + } catch (Exception e) { + Timber.log(Log.ERROR, e, "Unable to read TSA certificate file data"); + } + + return Observable.just(Result.ChooseFileResult.create(navigator.activity())); + } else { + Timber.log(Log.ERROR, "Data from file chooser is empty"); + ToastUtil.showError(navigator.activity(), R.string.signature_create_error); + + navigator.execute(Transaction.pop()); + } + } else { + if (ActivityUtil.isExternalFileOpened(navigator.activity())) { + ActivityUtil.restartActivity(application.getApplicationContext(), navigator.activity()); + } else { + navigator.execute(Transaction.pop()); + } + return Observable.just(Result.ChooseFileResult.create(navigator.activity())); + } + + return Observable.empty(); + }) + .onErrorReturn(throwable -> { + List exceptions = ((CompositeException) throwable).getExceptions(); + if (!exceptions.isEmpty()) { + boolean isEmptyFileException = exceptions.stream().anyMatch(exception -> + (exception instanceof EmptyFileException)); + if (isEmptyFileException) { + ToastUtil.showEmptyFileError(navigator.activity()); + } else { + ToastUtil.showError(navigator.activity(), R.string.signature_create_error); + } + } + navigator.execute(Transaction.pop()); + return Result.ChooseFileResult.create(navigator.activity()); + }); + } + + @Override + public ObservableSource apply(Observable upstream) { + return upstream.publish(shared -> Observable.mergeArray( + shared.ofType(Action.ChooseFileAction.class).compose(chooseFile))); + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Result.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Result.java new file mode 100644 index 000000000..b0a91225b --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/Result.java @@ -0,0 +1,43 @@ +package ee.ria.DigiDoc.android.main.settings.create; + +import android.content.Context; + +import androidx.annotation.Nullable; + +import com.google.auto.value.AutoValue; + +import ee.ria.DigiDoc.android.utils.mvi.MviResult; + +interface Result extends MviResult { + + @AutoValue + abstract class ChooseFileResult implements Result { + + @Nullable abstract Context context(); + + @Override + public ViewState reduce(ViewState state) { + return state.buildWith() + .context(context()) + .build(); + } + + static ChooseFileResult create(@Nullable Context context) { + return new AutoValue_Result_ChooseFileResult(context); + } + } + + @AutoValue + abstract class OpenAccessResult implements Result { + + @Override + public ViewState reduce(ViewState state) { + return state.buildWith() + .build(); + } + + static OpenAccessResult create() { + return new AutoValue_Result_OpenAccessResult(); + } + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/TSACertificateAddViewModel.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/TSACertificateAddViewModel.java new file mode 100644 index 000000000..e149ff80c --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/TSACertificateAddViewModel.java @@ -0,0 +1,31 @@ +package ee.ria.DigiDoc.android.main.settings.create; + +import javax.inject.Inject; + +import ee.ria.DigiDoc.android.utils.mvi.BaseMviViewModel; + +public final class TSACertificateAddViewModel extends BaseMviViewModel { + + @Inject TSACertificateAddViewModel(Processor processor) { + super(processor); + } + + @Override + protected Class initialIntentType() { + return Intent.ChooseFileIntent.class; + } + + @Override + protected Action action(Intent intent) { + if (intent instanceof Intent.ChooseFileIntent) { + return Action.ChooseFileAction.create(); + } else { + throw new IllegalArgumentException("Unknown intent " + intent); + } + } + + @Override + protected ViewState initialViewState() { + return ViewState.initial(); + } +} \ No newline at end of file diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/ViewState.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/ViewState.java new file mode 100644 index 000000000..26dfe2cf7 --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/create/ViewState.java @@ -0,0 +1,29 @@ +package ee.ria.DigiDoc.android.main.settings.create; + +import android.content.Context; + +import androidx.annotation.Nullable; + +import com.google.auto.value.AutoValue; + +import ee.ria.DigiDoc.android.utils.mvi.MviViewState; + +@AutoValue +public abstract class ViewState implements MviViewState { + + @Nullable abstract Context context(); + + abstract Builder buildWith(); + + static ViewState initial() { + return new AutoValue_ViewState.Builder() + .context(null) + .build(); + } + + @AutoValue.Builder + interface Builder { + Builder context(Context context); + ViewState build(); + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/settings/util/SettingsUtil.java b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/util/SettingsUtil.java new file mode 100644 index 000000000..a5ce405e7 --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/settings/util/SettingsUtil.java @@ -0,0 +1,19 @@ +package ee.ria.DigiDoc.android.main.settings.util; + +import android.view.View; +import android.widget.TextView; +import android.widget.Toolbar; + +public class SettingsUtil { + + public static TextView getToolbarViewTitle(Toolbar toolbarView) { + for (int i = 0; i < toolbarView.getChildCount(); i++) { + View childView = toolbarView.getChildAt(i); + if (childView instanceof TextView) { + return (TextView) childView; + } + } + + return null; + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/main/sharing/SharingScreenView.java b/app/src/main/java/ee/ria/DigiDoc/android/main/sharing/SharingScreenView.java index a82eac2fb..d061d83d5 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/main/sharing/SharingScreenView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/main/sharing/SharingScreenView.java @@ -1,26 +1,28 @@ package ee.ria.DigiDoc.android.main.sharing; +import static android.app.Activity.RESULT_OK; + import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import androidx.core.content.FileProvider; - import android.util.Log; import android.webkit.MimeTypeMap; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.Toolbar; -import android.widget.Toast; + +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.content.FileProvider; import java.io.File; import java.util.ArrayList; +import java.util.Optional; import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.Activity; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.Constants; import ee.ria.DigiDoc.android.utils.ToastUtil; import ee.ria.DigiDoc.android.utils.ViewDisposables; @@ -43,7 +45,7 @@ public SharingScreenView(Context context) { super(context); inflate(context, R.layout.sharing_choose_container_file, this); toolbarView = findViewById(R.id.toolbar); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); disposables = new ViewDisposables(); @@ -116,24 +118,21 @@ private void showFilesListener(ListView listView, ArrayList containerFiles File requestFile = containerFilesList.get(position); try { - Activity activity = (Activity) getContext(); - Uri fileProviderUri = FileProvider.getUriForFile(getContext(), - activity.getString(R.string.file_provider_authority), + navigator.activity().getString(R.string.file_provider_authority), requestFile); - Intent returnIntent = new Intent(Intent.ACTION_VIEW); + Intent returnIntent = navigator.activity().getIntent(); if (fileProviderUri != null) { returnIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); returnIntent.setDataAndType(fileProviderUri, getFileExtensionFromFileUri(fileProviderUri)); - activity.setResult(Activity.RESULT_OK, returnIntent); - - returnToParentApplication(activity, returnIntent); + navigator.activity().setResult(RESULT_OK, returnIntent); + navigator.activity().finish(); } else { returnIntent.setDataAndType(null, ""); - activity.setResult(Activity.RESULT_CANCELED, returnIntent); - activity.finish(); + navigator.activity().setResult(Activity.RESULT_CANCELED, returnIntent); + navigator.activity().finish(); restartToMainApp(); } } catch (IllegalArgumentException e) { @@ -153,9 +152,12 @@ private void returnToParentApplication(Activity activity, Intent intent) { } private boolean isIntentWithExtraReferrer(Activity activity) { - return activity.getIntent() != null && activity.getIntent().getExtras() != null && - activity.getIntent().getExtras().get(Intent.EXTRA_REFERRER) != null && - activity.getIntent().getExtras().get(Intent.EXTRA_REFERRER).equals(R.string.application_name); + return Optional + .ofNullable(activity.getIntent()) + .map(Intent::getExtras) + .map(extras -> extras.get(Intent.EXTRA_REFERRER)) + .filter(extraReferrer -> extraReferrer.equals(R.string.application_name)) + .isPresent(); } private String getFileExtensionFromFileUri(Uri uri) { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/create/Action.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/create/Action.java index f3cda0fa2..87ce9159d 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/create/Action.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/create/Action.java @@ -1,6 +1,10 @@ package ee.ria.DigiDoc.android.signature.create; +import static ee.ria.DigiDoc.android.Constants.RC_SIGNATURE_CREATE_DOCUMENTS_ADD; +import static ee.ria.DigiDoc.android.utils.IntentUtils.createGetContentIntent; + import android.content.Intent; + import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; @@ -9,9 +13,6 @@ import ee.ria.DigiDoc.android.utils.navigator.Transaction; import ee.ria.DigiDoc.android.utils.navigator.TransactionAction; -import static ee.ria.DigiDoc.android.Constants.RC_SIGNATURE_CREATE_DOCUMENTS_ADD; -import static ee.ria.DigiDoc.android.utils.IntentUtils.createGetContentIntent; - interface Action extends MviAction { @AutoValue @@ -22,7 +23,7 @@ abstract class ChooseFilesAction implements Action, static ChooseFilesAction create(@Nullable Intent intent) { return new AutoValue_Action_ChooseFilesAction(Transaction.activityForResult( - RC_SIGNATURE_CREATE_DOCUMENTS_ADD, createGetContentIntent(), null), intent); + RC_SIGNATURE_CREATE_DOCUMENTS_ADD, createGetContentIntent(true), null), intent); } } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/create/Processor.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/create/Processor.java index 78b3fa97e..67535df88 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/create/Processor.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/create/Processor.java @@ -14,7 +14,6 @@ import javax.inject.Inject; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Activity; import ee.ria.DigiDoc.android.signature.data.SignatureContainerDataSource; import ee.ria.DigiDoc.android.signature.update.SignatureUpdateScreen; import ee.ria.DigiDoc.android.utils.ClickableDialogUtil; @@ -37,7 +36,6 @@ import io.reactivex.rxjava3.core.ObservableSource; import io.reactivex.rxjava3.core.ObservableTransformer; import io.reactivex.rxjava3.exceptions.CompositeException; -import io.reactivex.rxjava3.schedulers.Schedulers; import timber.log.Timber; final class Processor implements ObservableTransformer { @@ -60,13 +58,13 @@ final class Processor implements ObservableTransformer { .switchMap(action -> { if (action.intent() != null) { throw new ActivityResultException(ActivityResult.create( - action.transaction().requestCode(), RESULT_OK, action.intent())); + action.transaction().getRequestCode(), RESULT_OK, action.intent())); } navigator.execute(action.transaction()); return navigator.activityResults() .filter(activityResult -> activityResult.requestCode() - == action.transaction().requestCode()) + == action.transaction().getRequestCode()) .doOnNext(activityResult -> { throw new ActivityResultException(activityResult); }) @@ -82,11 +80,11 @@ final class Processor implements ObservableTransformer { if (activityResult.data() != null) { ImmutableList validFiles = FileSystem.getFilesWithValidSize( parseGetContentIntent(navigator.activity(), application.getContentResolver(), activityResult.data(), fileSystem.getExternallyOpenedFilesDir())); - ToastUtil.handleEmptyFileError(validFiles, application, navigator.activity()); + ToastUtil.handleEmptyFileError(validFiles, navigator.activity()); return handleFiles(navigator, signatureContainerDataSource, validFiles) - .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(AndroidSchedulers.mainThread()) .doOnError(throwable1 -> { Timber.log(Log.ERROR, throwable1, String.format("Unable to add file to container. Error: %s", @@ -118,7 +116,7 @@ final class Processor implements ObservableTransformer { boolean isEmptyFileException = exceptions.stream().anyMatch(exception -> (exception instanceof EmptyFileException)); if (isEmptyFileException) { - ToastUtil.showEmptyFileError(navigator.activity(), application); + ToastUtil.showEmptyFileError(navigator.activity()); } else { ToastUtil.showError(navigator.activity(), R.string.signature_create_error); } @@ -142,8 +140,8 @@ private Observable addFilesToContainer(Navigator navig return signatureContainerDataSource .addContainer(navigator.activity(), validFiles, false) .toObservable() - .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(AndroidSchedulers.mainThread()) .doOnNext(containerAdd -> navigator.execute(Transaction.replace(SignatureUpdateScreen .create(containerAdd.isExistingContainer(), false, @@ -170,6 +168,7 @@ private Observable handleFiles(Navigator navigator, ImmutableList validFiles) { return SivaUtil.isSivaConfirmationNeeded(validFiles, navigator.activity()) .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(AndroidSchedulers.mainThread()) .flatMap(isSivaConfirmationNeeded -> { if (isSivaConfirmationNeeded) { sivaConfirmationDialog.show(); @@ -197,6 +196,15 @@ private Observable handleFiles(Navigator navigator, return addFilesToContainer(navigator, signatureContainerDataSource, validFiles, true); } return Observable.just(Result.ChooseFilesResult.create()); + }) + .onErrorResumeNext(throwable -> { + if (throwable instanceof NoInternetConnectionException) { + ToastUtil.showError(navigator.activity(), R.string.no_internet_connection); + } else { + ToastUtil.showError(navigator.activity(), R.string.signature_create_error); + } + navigator.execute(Transaction.pop()); + return Observable.just(Result.ChooseFilesResult.create()); }); } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/create/SignatureCreateScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/create/SignatureCreateScreen.java index f4167c660..54f9a6257 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/create/SignatureCreateScreen.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/create/SignatureCreateScreen.java @@ -24,11 +24,19 @@ public static SignatureCreateScreen create(@Nullable android.content.Intent inte @SuppressWarnings("WeakerAccess") public SignatureCreateScreen(Bundle args) { super(R.id.signatureCreateScreen); - intent = args.getParcelable(KEY_INTENT); + intent = getIntent(args); } @Override protected View view(Context context) { return new SignatureCreateView(context, getInstanceId(), intent); } + + private android.content.Intent getIntent(Bundle bundle) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + return bundle.getParcelable(KEY_INTENT, android.content.Intent.class); + } else { + return bundle.getParcelable(KEY_INTENT); + } + } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/create/SignatureCreateView.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/create/SignatureCreateView.java index 847d3a761..e4afc6bdd 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/create/SignatureCreateView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/create/SignatureCreateView.java @@ -7,13 +7,14 @@ import android.widget.FrameLayout; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.utils.ViewDisposables; import ee.ria.DigiDoc.android.utils.mvi.MviView; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import io.reactivex.rxjava3.core.Observable; @SuppressLint("ViewConstructor") -public final class SignatureCreateView extends FrameLayout implements MviView { +public final class SignatureCreateView extends FrameLayout implements ContentView, MviView { @Nullable private final android.content.Intent intent; @@ -24,9 +25,11 @@ public SignatureCreateView(@NonNull Context context, String screenId, @Nullable android.content.Intent intent) { super(context); this.intent = intent; - viewModel = Application.component(context).navigator() + viewModel = ApplicationApp.component(context).navigator() .viewModel(screenId, SignatureCreateViewModel.class); inflate(context, R.layout.signature_create, this); + + ContentView.addInvisibleElement(getContext(), this); } private Observable initialIntent() { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/detail/CertificateDetailView.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/detail/CertificateDetailView.java index 38a1ffb24..d9e8dbbfd 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/detail/CertificateDetailView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/detail/CertificateDetailView.java @@ -8,7 +8,6 @@ import android.text.TextUtils; import android.util.Log; import android.webkit.URLUtil; -import android.widget.TextView; import android.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; @@ -46,17 +45,18 @@ import java.util.Map; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.TextUtil; import ee.ria.DigiDoc.android.utils.ViewDisposables; import ee.ria.DigiDoc.android.utils.ViewSavedState; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.android.utils.navigator.Transaction; import timber.log.Timber; @SuppressLint("ViewConstructor") -public final class CertificateDetailView extends CoordinatorLayout { +public final class CertificateDetailView extends CoordinatorLayout implements ContentView { private final Navigator navigator; private final Toolbar toolbarView; @@ -69,7 +69,7 @@ public CertificateDetailView(Context context, X509Certificate certificate) { inflate(context, R.layout.certificate_details_screen, this); AccessibilityUtils.setViewAccessibilityPaneTitle(this, R.string.certificate_details_title); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); toolbarView = findViewById(R.id.toolbar); toolbarView.setTitle(R.string.certificate_details_title); @@ -77,6 +77,8 @@ public CertificateDetailView(Context context, X509Certificate certificate) { toolbarView.setNavigationContentDescription(R.string.back); setData(certificate); + + ContentView.addInvisibleElement(context, this); } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/detail/SignatureDetailView.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/detail/SignatureDetailView.java index f4cc0a30f..d85a42f23 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/detail/SignatureDetailView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/detail/SignatureDetailView.java @@ -1,14 +1,27 @@ package ee.ria.DigiDoc.android.signature.detail; +import static com.jakewharton.rxbinding4.view.RxView.clicks; import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; +import static ee.ria.DigiDoc.android.accessibility.AccessibilityUtils.setCustomClickAccessibilityFeedBack; +import static ee.ria.DigiDoc.android.utils.TintUtils.tintCompoundDrawables; import android.annotation.SuppressLint; import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.os.Build; import android.os.Parcelable; +import android.text.util.Linkify; import android.util.Log; +import android.widget.LinearLayout; +import android.widget.TextView; import android.widget.Toolbar; +import androidx.annotation.ColorInt; import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.content.ContextCompat; + +import net.cachapa.expandablelayout.ExpandableLayout; import org.bouncycastle.asn1.x500.RDN; import org.bouncycastle.asn1.x500.X500Name; @@ -20,24 +33,36 @@ import java.security.cert.X509Certificate; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.TextUtil; import ee.ria.DigiDoc.android.utils.ViewDisposables; import ee.ria.DigiDoc.android.utils.ViewSavedState; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.android.utils.navigator.Transaction; import ee.ria.DigiDoc.sign.Signature; +import ee.ria.DigiDoc.sign.SignatureStatus; import ee.ria.DigiDoc.sign.SignedContainer; import ee.ria.libdigidocpp.Container; +import io.reactivex.rxjava3.core.Observable; import timber.log.Timber; @SuppressLint("ViewConstructor") -public final class SignatureDetailView extends CoordinatorLayout { +public final class SignatureDetailView extends CoordinatorLayout implements ContentView { private final Navigator navigator; private final Toolbar toolbarView; + private final LinearLayout errorContainer; + private final TextView errorTitle; + private final TextView errorDetails; + private final TextView technicalInformationButtonTitle; + private final ExpandableLayout technicalInformationContainerView; + private final TextView technicalInformationText; + + @ColorInt private final int titleColor; + private final ViewDisposables disposables = new ViewDisposables(); public SignatureDetailView(Context context, Signature signature, SignedContainer signedContainer) { @@ -46,13 +71,32 @@ public SignatureDetailView(Context context, Signature signature, SignedContainer inflate(context, R.layout.signature_detail_screen, this); AccessibilityUtils.setViewAccessibilityPaneTitle(this, R.string.signature_details_title); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); toolbarView = findViewById(R.id.toolbar); + errorContainer = findViewById(R.id.signersCertificateErrorContainer); + errorTitle = findViewById(R.id.signersCertificateErrorTitle); + errorDetails = findViewById(R.id.signersCertificateErrorDetails); + technicalInformationButtonTitle = findViewById(R.id.signersCertificateTechnicalInformationButtonTitle); + technicalInformationContainerView = findViewById(R.id.signersCertificateTechnicalInformationContainerView); + technicalInformationText = findViewById(R.id.signersCertificateTechnicalInformationText); + toolbarView.setTitle(R.string.signature_details_title); toolbarView.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material); toolbarView.setNavigationContentDescription(R.string.back); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try (TypedArray a = getContext().obtainStyledAttributes(new int[]{ + android.R.attr.textColorSecondary, R.attr.colorAccent})) { + titleColor = a.getColor(1, Color.BLACK); + } + } else { + TypedArray a = getContext().obtainStyledAttributes(new int[] { + android.R.attr.textColorSecondary, R.attr.colorAccent}); + titleColor = a.getColor(1, Color.BLACK); + a.recycle(); + } + if (isCertificateInExistence(signature.signingCertificate())) { findViewById(R.id.signersCertificateButton).setOnClickListener(view -> navigator.execute(Transaction.push(CertificateDetailScreen.create(signature.signingCertificate())))); } @@ -65,7 +109,11 @@ public SignatureDetailView(Context context, Signature signature, SignedContainer findViewById(R.id.signatureDetailOCSPCertificateButton).setOnClickListener(view -> navigator.execute(Transaction.push(CertificateDetailScreen.create(signature.ocspCertificate())))); } + setExpandedState(false); + setWarningsData(signature); setData(signature, signedContainer); + + ContentView.addInvisibleElement(context, this); } private String getContainerMimeType(SignedContainer signedContainer) { @@ -112,12 +160,52 @@ private static String getX509CertificateSubject(X509Certificate x509Certificate) } } + private void setExpandedState(boolean isExpanded) { + technicalInformationButtonTitle.setTextColor(titleColor); + int drawable = isExpanded + ? R.drawable.ic_icon_accordion_expanded + : R.drawable.ic_icon_accordion_collapsed; + technicalInformationButtonTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(drawable, 0, 0, 0); + tintCompoundDrawables(technicalInformationButtonTitle); + setCustomClickAccessibilityFeedBack(technicalInformationButtonTitle, technicalInformationContainerView); + + technicalInformationContainerView.setExpanded(isExpanded); + } + + private void setWarningsData(Signature signature) { + SignatureStatus status = signature.status(); + if (status != SignatureStatus.VALID) { + errorContainer.setVisibility(VISIBLE); + String diagnosticsInfo = signature.diagnosticsInfo(); + if (status == SignatureStatus.WARNING) { + if (diagnosticsInfo.contains("Signature digest weak")) { + errorDetails.setText(getResources().getString(R.string.signature_error_details_reason_weak)); + } else { + errorDetails.setText(getResources().getString(R.string.signature_error_details_reason_warning)); + } + errorTitle.setTextColor(ContextCompat.getColor(getContext(), R.color.warningText)); + } else if (status == SignatureStatus.NON_QSCD) { + errorDetails.setText(getResources().getString(R.string.signature_error_details_reason_nonqscd)); + errorTitle.setTextColor(ContextCompat.getColor(getContext(), R.color.warningText)); + } else if (status == SignatureStatus.UNKNOWN) { + errorDetails.setText(getResources().getString(R.string.signature_error_details_reason_unknown)); + errorTitle.setTextColor(ContextCompat.getColor(getContext(), R.color.error)); + } else if (status == SignatureStatus.INVALID) { + errorDetails.setText(getResources().getString(R.string.signature_error_details_invalid_reason)); + errorTitle.setTextColor(ContextCompat.getColor(getContext(), R.color.error)); + } + Linkify.addLinks(errorDetails, Linkify.WEB_URLS); + technicalInformationText.setText(diagnosticsInfo); + } + } + @Override public void onAttachedToWindow() { super.onAttachedToWindow(); disposables.attach(); disposables.add(navigationClicks(toolbarView).subscribe(o -> navigator.execute(Transaction.pop()))); + disposables.add(certificateContainerStates().subscribe(this::setExpandedState)); } @Override @@ -126,6 +214,11 @@ public void onDetachedFromWindow() { super.onDetachedFromWindow(); } + public Observable certificateContainerStates() { + return clicks(technicalInformationButtonTitle) + .map(ignored -> !technicalInformationContainerView.isExpanded()); + } + private boolean isCertificateInExistence(X509Certificate certificate) { return certificate != null; } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/home/SignatureHomeView.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/home/SignatureHomeView.java index 4d58cf916..75524ea1f 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/home/SignatureHomeView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/home/SignatureHomeView.java @@ -9,7 +9,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.main.home.HomeToolbar; import ee.ria.DigiDoc.android.main.home.HomeView; @@ -43,7 +43,7 @@ public SignatureHomeView(Context context, @Nullable AttributeSet attrs, int defS inflate(context, R.layout.signature_home, this); toolbarView = findViewById(R.id.toolbar); createButton = findViewById(R.id.signatureHomeCreateButton); - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); disposables = new ViewDisposables(); AccessibilityUtils.setViewAccessibilityPaneTitle(this, R.string.main_home_navigation_signature); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/list/Intent.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/list/Intent.java index f925c0065..4849db4b2 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/list/Intent.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/list/Intent.java @@ -2,9 +2,6 @@ import android.content.Context; -import androidx.annotation.Nullable; - -import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import java.io.File; @@ -16,83 +13,104 @@ import io.reactivex.rxjava3.core.Observable; interface Intent extends MviIntent { + Action action(); +} - @AutoValue - abstract class InitialIntent implements Intent { +class InitialIntent implements Intent { + private InitialIntent() {} - static InitialIntent create() { - return new AutoValue_Intent_InitialIntent(); - } + static InitialIntent create() { + return new InitialIntent(); } - @AutoValue - abstract class UpButtonIntent implements Intent { - - static UpButtonIntent create() { - return new AutoValue_Intent_UpButtonIntent(); - } + @Override + public Action action() { + return Action.ContainersLoadAction.create(true); } +} - @AutoValue - abstract class ContainerOpenIntent implements Intent, Action { +class UpButtonIntent implements Intent { + private UpButtonIntent() {} - @Nullable abstract File containerFile(); + static UpButtonIntent create() { + return new UpButtonIntent(); + } - abstract boolean confirmation(); + @Override + public Action action() { + return Action.NavigateUpAction.create(); + } +} - abstract boolean isSivaConfirmed(); +class ContainerOpenIntent implements Intent { + private final File containerFile; + private final boolean confirmation; + private final boolean isSivaConfirmed; - static Observable confirmation(File containerFile, Context context) { - return SivaUtil.isSivaConfirmationNeeded( - ImmutableList.of(FileStream.create(containerFile)), context) - .observeOn(AndroidSchedulers.mainThread()) - .map(isSivaConfirmationNeeded -> create(containerFile, isSivaConfirmationNeeded, false)) - .subscribeOn(AndroidSchedulers.mainThread()); - } + private ContainerOpenIntent(File containerFile, boolean confirmation, boolean isSivaConfirmed) { + this.containerFile = containerFile; + this.confirmation = confirmation; + this.isSivaConfirmed = isSivaConfirmed; + } - static ContainerOpenIntent open(File containerFile, boolean isSivaConfirmed) { - return create(containerFile, false, isSivaConfirmed); - } + static Observable confirmation(File containerFile, Context context) { + return SivaUtil.isSivaConfirmationNeeded( + ImmutableList.of(FileStream.create(containerFile)), context) + .observeOn(AndroidSchedulers.mainThread()) + .map(isSivaConfirmationNeeded -> new ContainerOpenIntent(containerFile, isSivaConfirmationNeeded, false)) + .subscribeOn(AndroidSchedulers.mainThread()); + } - static ContainerOpenIntent cancel() { - return create(null, false, true); - } + static ContainerOpenIntent open(File containerFile, boolean isSivaConfirmed) { + return new ContainerOpenIntent(containerFile, false, isSivaConfirmed); + } - private static ContainerOpenIntent create(@Nullable File containerFile, boolean confirmation, boolean isSivaConfirmed) { - return new AutoValue_Intent_ContainerOpenIntent(containerFile, confirmation, isSivaConfirmed); - } + static ContainerOpenIntent cancel() { + return new ContainerOpenIntent(null, false, true); } - @AutoValue - abstract class ContainerRemoveIntent implements Intent { + @Override + public Action action() { + return Action.ContainerOpenAction.create(containerFile, confirmation, isSivaConfirmed); + } +} - @Nullable abstract File containerFile(); +class ContainerRemoveIntent implements Intent { + private final File containerFile; + private final boolean confirmation; - abstract boolean confirmation(); + private ContainerRemoveIntent(File containerFile, boolean confirmation) { + this.containerFile = containerFile; + this.confirmation = confirmation; + } - static ContainerRemoveIntent confirmation(File containerFile) { - return create(containerFile, true); - } + static ContainerRemoveIntent confirmation(File containerFile) { + return new ContainerRemoveIntent(containerFile, true); + } - static ContainerRemoveIntent remove(File containerFile) { - return create(containerFile, false); - } + static ContainerRemoveIntent remove(File containerFile) { + return new ContainerRemoveIntent(containerFile, false); + } - static ContainerRemoveIntent cancel() { - return create(null, false); - } + static ContainerRemoveIntent cancel() { + return new ContainerRemoveIntent(null, false); + } - private static ContainerRemoveIntent create(@Nullable File containerFile, - boolean confirmation) { - return new AutoValue_Intent_ContainerRemoveIntent(containerFile, confirmation); - } + @Override + public Action action() { + return Action.ContainerRemoveAction.create(containerFile, confirmation); } +} - @AutoValue - abstract class RefreshIntent implements Intent { +class RefreshIntent implements Intent { + private RefreshIntent() {} + + static RefreshIntent create() { + return new RefreshIntent(); + } - static RefreshIntent create() { - return new AutoValue_Intent_RefreshIntent(); - } + @Override + public Action action() { + return Action.ContainersLoadAction.create(false); } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/list/Processor.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/list/Processor.java index f73fc1d9a..7ac695257 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/list/Processor.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/list/Processor.java @@ -67,7 +67,7 @@ final class Processor implements ObservableTransformer { } else { File containerFile = action.containerFile(); if (CryptoContainer.isContainerFileName(containerFile.getName())) { - navigator.execute(Transaction.push(CryptoCreateScreen.open(containerFile))); + navigator.execute(Transaction.push(CryptoCreateScreen.open(containerFile, false))); } else { navigator.execute(Transaction.push(SignatureUpdateScreen .create(true, false, containerFile, false, false, diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/list/SignatureListScreen.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/list/SignatureListScreen.java index 9bd8b7f0a..ec13af666 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/list/SignatureListScreen.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/list/SignatureListScreen.java @@ -1,17 +1,26 @@ package ee.ria.DigiDoc.android.signature.list; -import android.content.Context; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; +import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; +import static ee.ria.DigiDoc.android.utils.navigator.ContentView.addInvisibleElementScrollListener; +import static ee.ria.DigiDoc.android.utils.navigator.ContentView.addInvisibleElementToObject; +import static ee.ria.DigiDoc.android.utils.navigator.ContentView.removeInvisibleElementScrollListener; +import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; import android.widget.Toolbar; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.bluelinelabs.conductor.Controller; import com.google.common.io.Files; @@ -19,7 +28,7 @@ import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.Activity; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.ViewDisposables; import ee.ria.DigiDoc.android.utils.mvi.MviView; @@ -28,11 +37,6 @@ import ee.ria.DigiDoc.sign.SignedContainer; import io.reactivex.rxjava3.core.Observable; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; -import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; - public final class SignatureListScreen extends Controller implements Screen, MviView { @@ -59,20 +63,20 @@ public static SignatureListScreen create() { public SignatureListScreen() { } - private Observable initialIntent() { - return Observable.just(Intent.InitialIntent.create()); + private Observable initialIntent() { + return Observable.just(InitialIntent.create()); } - private Observable upButtonIntent() { + private Observable upButtonIntent() { return navigationClicks(toolbarView) - .map(ignored -> Intent.UpButtonIntent.create()); + .map(ignored -> UpButtonIntent.create()); } - private Observable containerOpenIntent() { + private Observable containerOpenIntent() { return Observable.merge(adapter.itemClicks() - .flatMap(file -> Intent.ContainerOpenIntent.confirmation(file, getApplicationContext())), + .flatMap(file -> ContainerOpenIntent.confirmation(file, getApplicationContext())), sivaConfirmationDialog.positiveButtonClicks() - .map(ignored -> Intent.ContainerOpenIntent.open(sivaConfirmationContainerFile, true)), + .map(ignored -> ContainerOpenIntent.open(sivaConfirmationContainerFile, true)), sivaConfirmationDialog.cancels() .map(ignored -> { if (sivaConfirmationContainerFile != null && @@ -81,31 +85,31 @@ private Observable containerOpenIntent() { SignedContainer signedContainer = SignedContainer.open(sivaConfirmationContainerFile); if (signedContainer.dataFiles().size() == 1 && Files.getFileExtension(signedContainer.dataFiles().get(0).name()).equalsIgnoreCase("ddoc")) { - return Intent.ContainerOpenIntent.open(sivaConfirmationContainerFile, false); + return ContainerOpenIntent.open(sivaConfirmationContainerFile, false); } } - return Intent.ContainerOpenIntent.cancel(); + return ContainerOpenIntent.cancel(); })); } - private Observable containerRemoveIntent() { + private Observable containerRemoveIntent() { return Observable.merge( adapter.removeButtonClicks() - .map(Intent.ContainerRemoveIntent::confirmation), + .map(ContainerRemoveIntent::confirmation), removeConfirmationDialog.positiveButtonClicks() - .map(ignored -> Intent.ContainerRemoveIntent + .map(ignored -> ContainerRemoveIntent .remove(removeConfirmationContainerFile)), removeConfirmationDialog.cancels() .map(ignored -> { if (getApplicationContext() != null) { AccessibilityUtils.sendAccessibilityEvent(getApplicationContext(), TYPE_ANNOUNCEMENT, R.string.document_removal_cancelled); } - return Intent.ContainerRemoveIntent.cancel(); + return ContainerRemoveIntent.cancel(); })); } - private Observable refreshIntent() { - return Observable.just(Intent.RefreshIntent.create()); + private Observable refreshIntent() { + return Observable.just(RefreshIntent.create()); } @SuppressWarnings("unchecked") @@ -153,7 +157,7 @@ private void setEmpty(boolean empty) { @Override protected void onContextAvailable(@NonNull Context context) { super.onContextAvailable(context); - viewModel = Application.component(context).navigator() + viewModel = ApplicationApp.component(context).navigator() .viewModel(getInstanceId(), SignatureListViewModel.class); } @@ -183,6 +187,12 @@ protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup emptyView = view.findViewById(R.id.listEmpty); activityIndicatorView = view.findViewById(R.id.activityIndicator); activityOverlayView = view.findViewById(R.id.activityOverlay); + + LinearLayout signatureLayout = view.findViewById(R.id.signatureListLayout); + addInvisibleElementToObject(getApplicationContext(), signatureLayout); + View lastElementView = view.findViewById(R.id.lastInvisibleElement); + addInvisibleElementScrollListener(listView, lastElementView); + return view; } @@ -190,6 +200,7 @@ protected View onCreateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup protected void onDestroyView(@NonNull View view) { removeConfirmationDialog.dismiss(); sivaConfirmationDialog.dismiss(); + removeInvisibleElementScrollListener(listView); super.onDestroyView(view); } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/list/SignatureListViewModel.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/list/SignatureListViewModel.java index 1e528367c..0a949223b 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/list/SignatureListViewModel.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/list/SignatureListViewModel.java @@ -13,34 +13,15 @@ public final class SignatureListViewModel extends @Override protected Class initialIntentType() { - return Intent.InitialIntent.class; + return InitialIntent.class; } @Override protected Action action(Intent intent) { - if (intent instanceof Intent.InitialIntent) { - return Action.ContainersLoadAction.create(true); - } else if (intent instanceof Intent.UpButtonIntent) { - return Action.NavigateUpAction.create(); - } else if (intent instanceof Intent.ContainerRemoveIntent) { - Intent.ContainerRemoveIntent containerRemoveIntent = - (Intent.ContainerRemoveIntent) intent; - return Action.ContainerRemoveAction.create(containerRemoveIntent.containerFile(), - containerRemoveIntent.confirmation()); - } else if (intent instanceof Intent.ContainerOpenIntent) { - Intent.ContainerOpenIntent containerOpenIntent = - (Intent.ContainerOpenIntent) intent; - return Action.ContainerOpenAction.create(containerOpenIntent.containerFile(), - containerOpenIntent.confirmation(), containerOpenIntent.isSivaConfirmed()); - } else if (intent instanceof Intent.RefreshIntent) { - return Action.ContainersLoadAction.create(false); - } else if (intent instanceof Action) { - return (Action) intent; - } else { - throw new IllegalArgumentException("Unknown intent " + intent); - } + return intent.action(); } + @Override protected ViewState initialViewState() { return ViewState.initial(); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Action.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Action.java index 66edf6312..285ad4966 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Action.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Action.java @@ -45,7 +45,7 @@ abstract class DocumentsAddAction implements Action, static DocumentsAddAction create(@Nullable File containerFile) { return new AutoValue_Action_DocumentsAddAction( Transaction.activityForResult(RC_SIGNATURE_UPDATE_DOCUMENTS_ADD, - createGetContentIntent(), null), + createGetContentIntent(true), null), containerFile); } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Intent.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Intent.java index d0af083fb..56c82a751 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Intent.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Intent.java @@ -7,7 +7,6 @@ import androidx.annotation.Nullable; -import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import java.io.File; @@ -22,219 +21,307 @@ interface Intent extends MviIntent { - @AutoValue - abstract class InitialIntent implements Intent { + Action action(); +} - abstract boolean isExistingContainer(); +class InitialIntent implements Intent { - abstract File containerFile(); + private final boolean isExistingContainer; + private final File containerFile; + @Nullable private final Integer signatureAddMethod; + private final boolean signatureAddSuccessMessageVisible; - @Nullable abstract Integer signatureAddMethod(); + private InitialIntent(boolean isExistingContainer, File containerFile, + @Nullable Integer signatureAddMethod, + boolean signatureAddSuccessMessageVisible) { + this.isExistingContainer = isExistingContainer; + this.containerFile = containerFile; + this.signatureAddMethod = signatureAddMethod; + this.signatureAddSuccessMessageVisible = signatureAddSuccessMessageVisible; + } - abstract boolean signatureAddSuccessMessageVisible(); + public static InitialIntent create(boolean isExistingContainer, File containerFile, + Integer signatureAddMethod, + boolean signatureAddSuccessMessageVisible) { + return new InitialIntent(isExistingContainer, containerFile, signatureAddMethod, signatureAddSuccessMessageVisible); + } - static InitialIntent create(boolean isExistingContainer, File containerFile, - @Nullable Integer signatureAddMethod, - boolean signatureAddSuccessMessageVisible) { - return new AutoValue_Intent_InitialIntent(isExistingContainer, containerFile, - signatureAddMethod, signatureAddSuccessMessageVisible); - } + @Override + public Action action() { + return Action.ContainerLoadAction.create(containerFile, signatureAddMethod, + signatureAddSuccessMessageVisible, isExistingContainer); } +} - @AutoValue - abstract class NameUpdateIntent implements Intent, Action { +class NameUpdateIntent implements Intent, Action { - @Nullable abstract File containerFile(); + @Nullable File containerFile; - @Nullable abstract String name(); + @Nullable String name; - static NameUpdateIntent show(File file) { - return create(file, null); - } + public NameUpdateIntent(@Nullable File containerFile, @Nullable String name) { + this.containerFile = containerFile; + this.name = name; + } - static NameUpdateIntent update(File file, String name) { - return create(file, name); - } + static NameUpdateIntent show(File file) { + return create(file, null); + } - static NameUpdateIntent clear() { - return create(null, null); - } + static NameUpdateIntent update(File file, String name) { + return create(file, name); + } - private static NameUpdateIntent create(@Nullable File containerFile, - @Nullable String name) { - return new AutoValue_Intent_NameUpdateIntent(containerFile, name); - } + static NameUpdateIntent clear() { + return create(null, null); } - @AutoValue - abstract class DocumentsAddIntent implements Intent { + private static NameUpdateIntent create(@Nullable File containerFile, + @Nullable String name) { + return new NameUpdateIntent(containerFile, name); + } + + @Override + public Action action() { + return null; + } +} - @Nullable abstract File containerFile(); +class DocumentsAddIntent implements Intent { + @Nullable private final File containerFile; - static DocumentsAddIntent create(File containerFile) { - return new AutoValue_Intent_DocumentsAddIntent(containerFile); - } + public DocumentsAddIntent(@Nullable File containerFile) { + this.containerFile = containerFile; + } - static DocumentsAddIntent clear() { - return new AutoValue_Intent_DocumentsAddIntent(null); - } + static DocumentsAddIntent create(File containerFile) { + return new DocumentsAddIntent(containerFile); + } + + static DocumentsAddIntent clear() { + return new DocumentsAddIntent(null); } - @AutoValue - abstract class DocumentViewIntent implements Intent, Action { + @Override + public Action action() { + return Action.DocumentsAddAction.create(containerFile); + } +} - @Nullable abstract File containerFile(); +class DocumentViewIntent implements Intent, Action { - @Nullable abstract DataFile document(); + @Nullable File containerFile; + @Nullable DataFile document; + boolean confirmation; - abstract boolean confirmation(); + public DocumentViewIntent(@Nullable File containerFile, @Nullable DataFile document, boolean confirmation) { + this.containerFile = containerFile; + this.document = document; + this.confirmation = confirmation; + } - static DocumentViewIntent confirmation(Context context, File containerFile, DataFile document) throws Exception { - String containerFileExtension = getFileExtension(containerFile.getName()).toLowerCase(Locale.US); - String documentFileExtension = getFileExtension(document.name()).toLowerCase(Locale.US); - if (!containerFileExtension.equals("pdf") && SignedContainer.isContainer(context, containerFile)) { - try { - boolean isConfirmationNeeded = SivaUtil.isSivaConfirmationNeeded(containerFile, document); - return create(containerFile, document, isConfirmationNeeded); - } catch (Exception e) { - Timber.log(Log.ERROR, e, "Unable to get data file from container"); - return create(containerFile, document, false); - } - } else if (containerFileExtension.equals("pdf") && documentFileExtension.equals("pdf")) { - return create(containerFile, document, false); - } else { + static DocumentViewIntent confirmation(Context context, File containerFile, DataFile document) throws Exception { + String containerFileExtension = getFileExtension(containerFile.getName()).toLowerCase(Locale.US); + String documentFileExtension = getFileExtension(document.name()).toLowerCase(Locale.US); + if (!containerFileExtension.equals("pdf") && SignedContainer.isContainer(context, containerFile)) { + try { boolean isConfirmationNeeded = SivaUtil.isSivaConfirmationNeeded(containerFile, document); return create(containerFile, document, isConfirmationNeeded); + } catch (Exception e) { + Timber.log(Log.ERROR, e, "Unable to get data file from container"); + return create(containerFile, document, false); } - - } - - static DocumentViewIntent cancel() { - return create(null, null, false); - } - - static DocumentViewIntent open(File containerFile, DataFile document) { + } else if (containerFileExtension.equals("pdf") && documentFileExtension.equals("pdf")) { return create(containerFile, document, false); + } else { + boolean isConfirmationNeeded = SivaUtil.isSivaConfirmationNeeded(containerFile, document); + return create(containerFile, document, isConfirmationNeeded); } - static DocumentViewIntent create(@Nullable File containerFile, @Nullable DataFile document, boolean confirmation) { - return new AutoValue_Intent_DocumentViewIntent(containerFile, document, confirmation); - } } - @AutoValue - abstract class DocumentSaveIntent implements Intent, Action { + static DocumentViewIntent cancel() { + return create(null, null, false); + } - abstract File containerFile(); + static DocumentViewIntent open(File containerFile, DataFile document) { + return create(containerFile, document, false); + } - abstract DataFile document(); + static DocumentViewIntent create(@Nullable File containerFile, @Nullable DataFile document, boolean confirmation) { + return new DocumentViewIntent(containerFile, document, confirmation); + } - static DocumentSaveIntent create(File containerFile, DataFile document) { - return new AutoValue_Intent_DocumentSaveIntent(containerFile, document); - } + @Override + public Action action() { + return null; } +} - @AutoValue - abstract class SignatureViewIntent implements Intent { +class DocumentSaveIntent implements Intent, Action { - abstract File containerFile(); + File containerFile; - abstract Signature signature(); + DataFile document; - static SignatureViewIntent create(File containerFile, Signature signature) { - return new AutoValue_Intent_SignatureViewIntent(containerFile, signature); - } + public DocumentSaveIntent(File containerFile, DataFile document) { + this.containerFile = containerFile; + this.document = document; + } + + static DocumentSaveIntent create(File containerFile, DataFile document) { + return new DocumentSaveIntent(containerFile, document); } - @AutoValue - abstract class DocumentRemoveIntent implements Intent { + @Override + public Action action() { + return null; + } +} - abstract boolean showConfirmation(); +class SignatureViewIntent implements Intent { - @Nullable abstract File containerFile(); + File containerFile; - abstract ImmutableList documents(); + Signature signature; - @Nullable abstract DataFile document(); + public SignatureViewIntent(File containerFile, Signature signature) { + this.containerFile = containerFile; + this.signature = signature; + } - static DocumentRemoveIntent showConfirmation(File containerFile, ImmutableList documents, DataFile document) { - return new AutoValue_Intent_DocumentRemoveIntent(true, containerFile, documents, document); - } + static SignatureViewIntent create(File containerFile, Signature signature) { + return new SignatureViewIntent(containerFile, signature); + } - static DocumentRemoveIntent remove(File containerFile, ImmutableList documents, DataFile document) { - return new AutoValue_Intent_DocumentRemoveIntent(false, containerFile, documents, document); - } + @Override + public Action action() { + return Action.SignatureViewAction.create(containerFile, signature); + } +} - static DocumentRemoveIntent clear() { - return new AutoValue_Intent_DocumentRemoveIntent(false, null, ImmutableList.of(), null); - } +class DocumentRemoveIntent implements Intent { + + boolean showConfirmation; + @Nullable File containerFile; + ImmutableList documents; + + @Nullable DataFile document; + + public DocumentRemoveIntent(boolean showConfirmation, File containerFile, ImmutableList documents, DataFile document) { + this.showConfirmation = showConfirmation; + this.containerFile = containerFile; + this.documents = documents; + this.document = document; } - @AutoValue - abstract class SignatureRemoveIntent implements Intent { + static DocumentRemoveIntent showConfirmation(File containerFile, ImmutableList documents, DataFile document) { + return new DocumentRemoveIntent(true, containerFile, documents, document); + } - abstract boolean showConfirmation(); + static DocumentRemoveIntent remove(File containerFile, ImmutableList documents, DataFile document) { + return new DocumentRemoveIntent(false, containerFile, documents, document); + } - @Nullable abstract File containerFile(); + static DocumentRemoveIntent clear() { + return new DocumentRemoveIntent(false, null, ImmutableList.of(), null); + } - @Nullable abstract Signature signature(); + @Override + public Action action() { + return Action.DocumentRemoveAction.create(showConfirmation, containerFile, documents, document); + } +} - static SignatureRemoveIntent showConfirmation(File containerFile, Signature signature) { - return new AutoValue_Intent_SignatureRemoveIntent(true, containerFile, signature); - } +class SignatureRemoveIntent implements Intent { - static SignatureRemoveIntent remove(File containerFile, Signature signature) { - return new AutoValue_Intent_SignatureRemoveIntent(false, containerFile, signature); - } + boolean showConfirmation; + @Nullable File containerFile; + @Nullable Signature signature; - static SignatureRemoveIntent clear() { - return new AutoValue_Intent_SignatureRemoveIntent(false, null, null); - } + public SignatureRemoveIntent(boolean showConfirmation, @Nullable File containerFile, @Nullable Signature signature) { + this.showConfirmation = showConfirmation; + this.containerFile = containerFile; + this.signature = signature; } - @AutoValue - abstract class SignatureAddIntent implements Intent { + static SignatureRemoveIntent showConfirmation(File containerFile, Signature signature) { + return new SignatureRemoveIntent(true, containerFile, signature); + } - @Nullable abstract Integer method(); + static SignatureRemoveIntent remove(File containerFile, Signature signature) { + return new SignatureRemoveIntent(false, containerFile, signature); + } - @Nullable abstract Boolean existingContainer(); + static SignatureRemoveIntent clear() { + return new SignatureRemoveIntent(false, null, null); + } - @Nullable abstract File containerFile(); + @Override + public Action action() { + return Action.SignatureRemoveAction.create(showConfirmation, containerFile, signature); + } +} - @Nullable abstract SignatureAddRequest request(); +class SignatureAddIntent implements Intent { + + @Nullable Integer method; + @Nullable Boolean existingContainer; + @Nullable File containerFile; + @Nullable SignatureAddRequest request; + boolean isCancelled; + + public SignatureAddIntent(@Nullable Integer method, @Nullable Boolean existingContainer, + @Nullable File containerFile, @Nullable SignatureAddRequest request, + boolean isCancelled) { + this.method = method; + this.existingContainer = existingContainer; + this.containerFile = containerFile; + this.request = request; + this.isCancelled = isCancelled; + } - abstract boolean isCancelled(); + static SignatureAddIntent show(int method, boolean existingContainer, File containerFile) { + return create(method, existingContainer, containerFile, null, false); + } - static SignatureAddIntent show(int method, boolean existingContainer, File containerFile) { - return create(method, existingContainer, containerFile, null, false); - } + static SignatureAddIntent sign(int method, boolean existingContainer, File containerFile, + SignatureAddRequest request) { + return create(method, existingContainer, containerFile, request, false); + } - static SignatureAddIntent sign(int method, boolean existingContainer, File containerFile, - SignatureAddRequest request) { - return create(method, existingContainer, containerFile, request, false); - } + static SignatureAddIntent clear() { + return create(null, null, null, null, true); + } - static SignatureAddIntent clear() { - return create(null, null, null, null, true); - } + private static SignatureAddIntent create(@Nullable Integer method, + @Nullable Boolean existingContainer, + @Nullable File containerFile, + @Nullable SignatureAddRequest request, + boolean isCancelled) { + return new SignatureAddIntent(method, existingContainer, containerFile, request, isCancelled); + } - private static SignatureAddIntent create(@Nullable Integer method, - @Nullable Boolean existingContainer, - @Nullable File containerFile, - @Nullable SignatureAddRequest request, - boolean isCancelled) { - return new AutoValue_Intent_SignatureAddIntent(method, existingContainer, containerFile, - request, isCancelled); - } + @Override + public Action action() { + return Action.SignatureAddAction.create(method, existingContainer, containerFile, request, isCancelled); } +} - @AutoValue - abstract class SendIntent implements Intent { +class SendIntent implements Intent { - abstract File containerFile(); + File containerFile; - static SendIntent create(File containerFile) { - return new AutoValue_Intent_SendIntent(containerFile); - } + public SendIntent(File containerFile) { + this.containerFile = containerFile; + } + + static SendIntent create(File containerFile) { + return new SendIntent(containerFile); + } + + @Override + public Action action() { + return Action.SendAction.create(containerFile); } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Processor.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Processor.java index fcaefaf2a..0582d98c0 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Processor.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/Processor.java @@ -14,7 +14,6 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; -import android.os.Handler; import android.util.Log; import android.view.accessibility.AccessibilityEvent; @@ -50,11 +49,9 @@ import ee.ria.DigiDoc.android.utils.navigator.Transaction; import ee.ria.DigiDoc.common.FileUtil; import ee.ria.DigiDoc.crypto.CryptoContainer; -import ee.ria.DigiDoc.mobileid.service.MobileSignService; import ee.ria.DigiDoc.sign.DataFile; import ee.ria.DigiDoc.sign.NoInternetConnectionException; import ee.ria.DigiDoc.sign.SignedContainer; -import ee.ria.DigiDoc.smartid.service.SmartSignService; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.ObservableSource; @@ -68,16 +65,16 @@ final class Processor implements ObservableTransformer { private final ObservableTransformer containerLoad; - private final ObservableTransformer + private final ObservableTransformer nameUpdate; private final ObservableTransformer documentsAdd; - private final ObservableTransformer documentView; - private final ObservableTransformer documentSave; private final ObservableTransformer { private final PublishSubject notificationsPermissionSubject = PublishSubject.create(); - private android.content.Intent intent; - @Inject Processor(SignatureContainerDataSource signatureContainerDataSource, SignatureAddSource signatureAddSource, Application application, Navigator navigator, @@ -131,7 +126,7 @@ final class Processor implements ObservableTransformer { .startWithItem(Result.ContainerLoadResult.progress())); nameUpdate = upstream -> upstream.switchMap(action -> { - File containerFile = action.containerFile(); + File containerFile = action.containerFile; String name = containerFile != null ? assignName(action, containerFile) : null; if (containerFile == null) { @@ -193,14 +188,14 @@ final class Processor implements ObservableTransformer { return navigator.activityResults() .filter(activityResult -> activityResult.requestCode() - == action.transaction().requestCode()) + == action.transaction().getRequestCode()) .switchMap(activityResult -> { android.content.Intent data = activityResult.data(); if (activityResult.resultCode() == RESULT_OK && data != null) { ImmutableList validFiles = FileSystem.getFilesWithValidSize( parseGetContentIntent(navigator.activity(), application.getContentResolver(), data, fileSystem.getExternallyOpenedFilesDir())); - ToastUtil.handleEmptyFileError(validFiles, application, navigator.activity()); + ToastUtil.handleEmptyFileError(validFiles, navigator.activity()); ImmutableList filesNotInContainer = getFilesNotInContainer(navigator.activity(), validFiles, action.containerFile()); if (filesNotInContainer.isEmpty()) { throw new FileAlreadyExistsException(navigator.activity() @@ -225,15 +220,15 @@ final class Processor implements ObservableTransformer { }); documentView = upstream -> upstream.switchMap(action -> { - if (action.containerFile() == null) { + if (action.containerFile == null) { return Observable.just(Result.DocumentViewResult.idle()); - } else if (action.confirmation()) { + } else if (action.confirmation) { return Observable - .just(Result.DocumentViewResult.confirmation(action.document())); + .just(Result.DocumentViewResult.confirmation(action.document)); } else { - File containerFile = action.containerFile(); + File containerFile = action.containerFile; return signatureContainerDataSource - .getDocumentFile(containerFile, action.document()) + .getDocumentFile(containerFile, action.document) .toObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -242,16 +237,16 @@ final class Processor implements ObservableTransformer { String containerFileExtension = getFileExtension(containerFile.getName()).toLowerCase(Locale.US); String documentFileExtension = getFileExtension(documentFile.getName()).toLowerCase(Locale.US); boolean isPdfInSignedPdfContainer = false; - if (action.document() != null) { + if (action.document != null) { isPdfInSignedPdfContainer = containerFileExtension.equals("pdf") && - (SivaUtil.isSivaConfirmationNeeded(containerFile, action.document()) && + (SivaUtil.isSivaConfirmationNeeded(containerFile, action.document) && documentFileExtension.equals("pdf")); } if (!isPdfInSignedPdfContainer && SignedContainer.isContainer(navigator.activity(), documentFile)) { transaction = Transaction.push(SignatureUpdateScreen .create(true, true, documentFile, false, false, null, true)); } else if (CryptoContainer.isContainerFileName(documentFile.getName())) { - transaction = Transaction.push(CryptoCreateScreen.open(documentFile)); + transaction = Transaction.push(CryptoCreateScreen.open(documentFile, true)); } else { transaction = Transaction.activity(IntentUtils .createActionIntent(application, documentFile, @@ -262,7 +257,7 @@ final class Processor implements ObservableTransformer { }) .onErrorReturn(throwable -> { if (throwable instanceof NoInternetConnectionException) { - ToastUtil.showError(navigator.activity(),R.string.no_internet_connection); + ToastUtil.showError(navigator.activity(), R.string.no_internet_connection); } else { ToastUtil.showError(navigator.activity(), R.string.signature_update_container_load_error); } @@ -274,12 +269,12 @@ final class Processor implements ObservableTransformer { documentSave = upstream -> upstream.switchMap(action -> { navigator.execute(Transaction.activityForResult(SAVE_FILE, - createSaveIntent(action.document()), null)); + createSaveIntent(action.document), null)); return navigator.activityResults() .filter(activityResult -> activityResult.requestCode() == SAVE_FILE) .switchMap(activityResult -> signatureContainerDataSource - .getDocumentFile(action.containerFile(), action.document()) + .getDocumentFile(action.containerFile, action.document) .toObservable() .map(documentFile -> { if (activityResult.resultCode() == RESULT_OK) { @@ -372,14 +367,7 @@ final class Processor implements ObservableTransformer { File containerFile = action.containerFile(); SignatureAddRequest request = action.request(); boolean isCancelled = action.isCancelled(); - if (method != null) { - intent = getSigningIntent(navigator, method); - } if (method == null || isCancelled) { - if (intent != null) { - Handler handler = new Handler(navigator.activity().getMainLooper()); - handler.post(() -> stopSigningService(navigator, intent)); - } return Observable.just(Result.SignatureAddResult.clear()); } else if (request == null && existingContainer != null && containerFile != null) { if (SignedContainer.isLegacyContainer(containerFile)) { @@ -400,7 +388,7 @@ final class Processor implements ObservableTransformer { } } else if (existingContainer != null && containerFile != null) { return askNotificationPermission(navigator, method) - .flatMap(ign -> signatureAddSource.sign(containerFile, request, navigator, intent) + .flatMap(ign -> signatureAddSource.sign(containerFile, request, navigator) .switchMap(response -> { if (response.container() != null) { return Observable.fromCallable(() -> { @@ -417,7 +405,6 @@ final class Processor implements ObservableTransformer { .observeOn(AndroidSchedulers.mainThread()) .onErrorReturn(Result.SignatureAddResult::failure) .startWithItem(Result.SignatureAddResult.activity(method))); - } else { throw new IllegalArgumentException("Can't handle action " + action); } @@ -436,10 +423,10 @@ final class Processor implements ObservableTransformer { public ObservableSource apply(Observable upstream) { return upstream.publish(shared -> Observable.mergeArray( shared.ofType(Action.ContainerLoadAction.class).compose(containerLoad), - shared.ofType(Intent.NameUpdateIntent.class).compose(nameUpdate), + shared.ofType(NameUpdateIntent.class).compose(nameUpdate), shared.ofType(Action.DocumentsAddAction.class).compose(documentsAdd), - shared.ofType(Intent.DocumentViewIntent.class).compose(documentView), - shared.ofType(Intent.DocumentSaveIntent.class).compose(documentSave), + shared.ofType(DocumentViewIntent.class).compose(documentView), + shared.ofType(DocumentSaveIntent.class).compose(documentSave), shared.ofType(Action.DocumentRemoveAction.class).compose(documentRemove), shared.ofType(Action.SignatureRemoveAction.class).compose(signatureRemove), shared.ofType(Action.SignatureViewAction.class).compose(signatureView), @@ -460,8 +447,8 @@ private String addContainerExtension(File oldContainerFileName, String newName) return newName.concat(".").concat(oldContainerNamePart); } - private String assignName(Intent.NameUpdateIntent action, File containerFile) { - String name = FileUtil.sanitizeString(action.name(), ""); + private String assignName(NameUpdateIntent action, File containerFile) { + String name = FileUtil.sanitizeString(action.name, ""); if (name != null && !name.isEmpty()) { return addContainerExtension(containerFile, name); } @@ -499,21 +486,6 @@ private void announceAccessibilityFilesAddedEvent(Context context, int addedData } } - private android.content.Intent getSigningIntent(Navigator navigator, Integer method) { - if (method != null) { - if (method == R.id.signatureUpdateSignatureAddMethodMobileId) { - return new android.content.Intent(navigator.activity(), MobileSignService.class); - } else if (method == R.id.signatureUpdateSignatureAddMethodSmartId) { - return new android.content.Intent(navigator.activity(), SmartSignService.class); - } - } - return null; - } - - private void stopSigningService(Navigator navigator, android.content.Intent intent) { - navigator.activity().stopService(intent); - } - private Observable askNotificationPermission(Navigator navigator, int method) { if (method == R.id.signatureUpdateSignatureAddMethodMobileId || method == R.id.signatureUpdateSignatureAddMethodIdCard) { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureAddSource.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureAddSource.java index df23bcbc0..dba70d598 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureAddSource.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureAddSource.java @@ -76,8 +76,7 @@ Observable show(int method) { Observable sign(File containerFile, SignatureAddRequest request, - Navigator navigator, - android.content.Intent intent) { + Navigator navigator) { if (request instanceof MobileIdRequest) { MobileIdRequest mobileIdRequest = (MobileIdRequest) request; if (mobileIdRequest.rememberMe()) { @@ -90,7 +89,7 @@ Observable sign(File containerFile, return signatureContainerDataSource .get(containerFile) .flatMapObservable(container -> - Observable.create(new MobileIdOnSubscribe(navigator, intent, container, + Observable.create(new MobileIdOnSubscribe(navigator, container, localeService.applicationLocale(), settingsDataStore.getUuid(), mobileIdRequest.personalCode(), mobileIdRequest.phoneNo()))) @@ -126,9 +125,9 @@ Observable sign(File containerFile, return signatureContainerDataSource .get(containerFile) .flatMapObservable(container -> - Observable.create(new SmartIdOnSubscribe(navigator, intent, container, - settingsDataStore.getUuid(), smartIdRequest.personalCode(), - smartIdRequest.country()))) + Observable.create(new SmartIdOnSubscribe(navigator, container, + localeService.applicationLocale(), settingsDataStore.getUuid(), + smartIdRequest.personalCode(), smartIdRequest.country()))) .switchMap(response -> { SessionStatusResponse.ProcessStatus processStatus = response.status(); if (SessionStatusResponse.ProcessStatus.OK.equals(processStatus)) { diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateAdapter.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateAdapter.java index 1f4ef5d5a..68bb1cab1 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateAdapter.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateAdapter.java @@ -1,15 +1,21 @@ package ee.ria.DigiDoc.android.signature.update; +import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; +import static androidx.core.content.res.ResourcesCompat.getColor; +import static com.jakewharton.rxbinding4.view.RxView.clicks; +import static ee.ria.DigiDoc.android.Constants.VOID; +import static ee.ria.DigiDoc.android.signature.update.SignatureUpdateAdapter.SubheadItemType.DOCUMENT; +import static ee.ria.DigiDoc.android.signature.update.SignatureUpdateAdapter.SubheadItemType.SIGNATURE; +import static ee.ria.DigiDoc.android.signature.update.SignatureUpdateAdapter.SubheadItemType.TIMESTAMP; +import static ee.ria.DigiDoc.android.utils.Immutables.containsType; + import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; -import android.os.Handler; -import android.os.Looper; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; import android.widget.Button; import android.widget.ImageButton; import android.widget.TextView; @@ -30,13 +36,10 @@ import java.lang.annotation.RetentionPolicy; import java.time.Instant; import java.time.Month; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.Activity; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.DateUtil; import ee.ria.DigiDoc.android.utils.Formatter; @@ -51,16 +54,6 @@ import io.reactivex.rxjava3.subjects.Subject; import timber.log.Timber; -import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; -import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED; -import static androidx.core.content.res.ResourcesCompat.getColor; -import static com.jakewharton.rxbinding4.view.RxView.clicks; -import static ee.ria.DigiDoc.android.Constants.VOID; -import static ee.ria.DigiDoc.android.signature.update.SignatureUpdateAdapter.SubheadItemType.DOCUMENT; -import static ee.ria.DigiDoc.android.signature.update.SignatureUpdateAdapter.SubheadItemType.SIGNATURE; -import static ee.ria.DigiDoc.android.signature.update.SignatureUpdateAdapter.SubheadItemType.TIMESTAMP; -import static ee.ria.DigiDoc.android.utils.Immutables.containsType; - final class SignatureUpdateAdapter extends RecyclerView.Adapter> { @@ -367,7 +360,7 @@ static final class NameViewHolder extends UpdateViewHolder { @Override void bind(SignatureUpdateAdapter adapter, NameItem item) { if (item.name().startsWith(".")) { - nameView.setText("newFile" + FileUtil.sanitizeString(item.name(), "")); + nameView.setText(FileUtil.DEFAULT_FILENAME + FileUtil.sanitizeString(item.name(), "")); } else { nameView.setText(FileUtil.sanitizeString(item.name(), "")); } @@ -454,6 +447,7 @@ static final class SignatureViewHolder extends UpdateViewHolder { private final Formatter formatter; private final ColorStateList colorValid; + private final ColorStateList colorWarning; private final ColorStateList colorInvalid; private final TextView nameView; @@ -466,9 +460,10 @@ static final class SignatureViewHolder extends UpdateViewHolder { SignatureViewHolder(View itemView) { super(itemView); - formatter = Application.component(itemView.getContext()).formatter(); + formatter = ApplicationApp.component(itemView.getContext()).formatter(); Resources resources = itemView.getResources(); colorValid = ColorStateList.valueOf(getColor(resources, R.color.success, null)); + colorWarning = ColorStateList.valueOf(getColor(resources, R.color.warningText, null)); colorInvalid = ColorStateList.valueOf(getColor(resources, R.color.error, null)); nameView = itemView.findViewById(R.id.signatureUpdateListSignatureName); statusView = itemView.findViewById(R.id.signatureUpdateListSignatureStatus); @@ -515,6 +510,7 @@ void bind(SignatureUpdateAdapter adapter, SignatureItem item) { case NON_QSCD: statusCautionView.setVisibility(View.VISIBLE); statusCautionView.setText(R.string.signature_update_signature_status_non_qscd); + statusCautionView.setTextColor(colorWarning); break; default: statusCautionView.setVisibility(View.GONE); @@ -569,6 +565,7 @@ static final class TimestampViewHolder extends UpdateViewHolder { private final Formatter formatter; private final ColorStateList colorValid; + private final ColorStateList colorWarning; private final ColorStateList colorInvalid; private final TextView nameView; @@ -576,13 +573,12 @@ static final class TimestampViewHolder extends UpdateViewHolder { private final TextView statusCautionView; private final TextView createdAtView; - private final Activity activityContext = (Activity)Activity.getContext().get(); - TimestampViewHolder(View itemView) { super(itemView); - formatter = Application.component(itemView.getContext()).formatter(); + formatter = ApplicationApp.component(itemView.getContext()).formatter(); Resources resources = itemView.getResources(); colorValid = ColorStateList.valueOf(getColor(resources, R.color.success, null)); + colorWarning = ColorStateList.valueOf(getColor(resources, R.color.warningText, null)); colorInvalid = ColorStateList.valueOf(getColor(resources, R.color.error, null)); nameView = itemView.findViewById(R.id.signatureUpdateListSignatureName); statusView = itemView.findViewById(R.id.signatureUpdateListSignatureStatus); @@ -617,6 +613,7 @@ void bind(SignatureUpdateAdapter adapter, TimestampItem item) { case NON_QSCD: statusCautionView.setVisibility(View.VISIBLE); statusCautionView.setText(R.string.signature_update_signature_status_non_qscd); + statusCautionView.setTextColor(colorWarning); break; default: statusCautionView.setVisibility(View.GONE); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateErrorDialog.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateErrorDialog.java index 0d2157702..237f7cc7a 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateErrorDialog.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateErrorDialog.java @@ -2,8 +2,8 @@ import android.content.Context; import android.content.DialogInterface; -import android.content.res.Configuration; import android.text.Html; +import android.text.Spanned; import android.text.TextUtils; import android.view.View; import android.view.Window; @@ -16,20 +16,23 @@ import java.lang.annotation.RetentionPolicy; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.model.mobileid.MobileIdMessageException; -import ee.ria.DigiDoc.android.model.smartid.SmartIdMessageException; +import ee.ria.DigiDoc.android.signature.update.exception.DocumentExistsException; +import ee.ria.DigiDoc.android.signature.update.exception.DocumentRemoveException; +import ee.ria.DigiDoc.android.signature.update.exception.GeneralSignatureUpdateException; import ee.ria.DigiDoc.android.utils.ClickableDialogUtil; import ee.ria.DigiDoc.android.utils.ErrorMessageUtil; import ee.ria.DigiDoc.android.utils.files.EmptyFileException; import ee.ria.DigiDoc.android.utils.widget.ErrorDialog; +import ee.ria.DigiDoc.common.DetailMessageException; import ee.ria.DigiDoc.common.DetailMessageSource; +import ee.ria.DigiDoc.common.exception.SignatureUpdateDetailError; +import ee.ria.DigiDoc.common.exception.SignatureUpdateError; import ee.ria.DigiDoc.idcard.CodeVerificationException; import ee.ria.DigiDoc.sign.CertificateRevokedException; import ee.ria.DigiDoc.sign.NoInternetConnectionException; import ee.ria.DigiDoc.sign.OcspInvalidTimeSlotException; import ee.ria.DigiDoc.sign.TooManyRequestsException; import ee.ria.DigiDoc.sign.utils.UrlMessage; - import io.reactivex.rxjava3.subjects.Subject; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; @@ -38,9 +41,6 @@ import static ee.ria.DigiDoc.android.signature.update.SignatureUpdateErrorDialog.Type.SIGNATURE_ADD; import static ee.ria.DigiDoc.android.signature.update.SignatureUpdateErrorDialog.Type.SIGNATURE_REMOVE; import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDeviceLayoutWidth; -import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDeviceOrientation; -import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDialogLandscapeWidth; -import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDialogPortraitWidth; public final class SignatureUpdateErrorDialog extends ErrorDialog implements DialogInterface.OnDismissListener { @@ -53,10 +53,10 @@ public final class SignatureUpdateErrorDialog extends ErrorDialog implements Dia String SIGNATURE_REMOVE = "SIGNATURE_REMOVE"; } - private final Subject documentsAddIntentSubject; - private final Subject documentRemoveIntentSubject; - private final Subject signatureAddIntentSubject; - private final Subject signatureRemoveIntentSubject; + private final Subject documentsAddIntentSubject; + private final Subject documentRemoveIntentSubject; + private final Subject signatureAddIntentSubject; + private final Subject signatureRemoveIntentSubject; private final SignatureUpdateSignatureAddDialog signatureAddDialog; private String type; @@ -65,10 +65,10 @@ public final class SignatureUpdateErrorDialog extends ErrorDialog implements Dia private View.OnLayoutChangeListener layoutChangeListener; SignatureUpdateErrorDialog(@NonNull Context context, - Subject documentsAddIntentSubject, - Subject documentRemoveIntentSubject, - Subject signatureAddIntentSubject, - Subject signatureRemoveIntentSubject, + Subject documentsAddIntentSubject, + Subject documentRemoveIntentSubject, + Subject signatureAddIntentSubject, + Subject signatureRemoveIntentSubject, SignatureUpdateSignatureAddDialog signatureAddDialog, View view) { super(context); @@ -90,33 +90,36 @@ void show(@Nullable Throwable documentsAddError, @Nullable Throwable documentRem setCustomLayoutChangeListener(window); view.addOnLayoutChangeListener(getCustomLayoutChangeListener()); + SignatureUpdateError updateError = null; + SignatureUpdateDetailError detailError = null; + if (documentsAddError != null) { type = DOCUMENTS_ADD; if (documentsAddError instanceof EmptyFileException) { - setMessage(getContext().getString(R.string.empty_file_error)); + updateError = new EmptyFileException(); } else { - setMessage(getContext().getString( - R.string.signature_update_documents_add_error_exists)); + updateError = new DocumentExistsException(); } } else if (documentRemoveError != null) { type = DOCUMENT_REMOVE; - setMessage(getContext().getString(R.string.signature_update_document_remove_error)); + updateError = new DocumentRemoveException(); } else if (signatureAddError != null) { type = SIGNATURE_ADD; if (signatureAddError instanceof CodeVerificationException) { - setMessage(getContext().getString( - R.string.signature_update_id_card_sign_pin2_locked)); + updateError = new CodeVerificationException( + ((CodeVerificationException) signatureAddError).getType(), + getContext().getString( + R.string.signature_update_id_card_sign_pin2_locked) + ); } else if (signatureAddError instanceof NoInternetConnectionException) { - setMessage(getContext().getString(R.string.no_internet_connection)); + updateError = new NoInternetConnectionException(); } else if (signatureAddError instanceof DetailMessageSource) { String link = ErrorMessageUtil.extractLink(signatureAddError.getMessage()); if (!link.isEmpty()) { - setMessage( - Html.fromHtml("" + + detailError = new DetailMessageException(Html.fromHtml("" + ErrorMessageUtil.removeLink(signatureAddError.getMessage()) + "" + getTextFromTranslation(R.string.signature_update_signature_error_message_additional_information) + "", - Html.FROM_HTML_MODE_LEGACY) - ); + Html.FROM_HTML_MODE_LEGACY)); } else { setTitle(R.string.signature_update_signature_add_error_title); if (((DetailMessageSource) signatureAddError).getDetailMessage() != null && @@ -124,32 +127,48 @@ void show(@Nullable Throwable documentsAddError, @Nullable Throwable documentRem String errorMessage = getContext().getString(R.string.signature_update_signature_error_message_details) + ":\n" + ((DetailMessageSource) signatureAddError).getDetailMessage(); - setMessage(errorMessage); + detailError = new DetailMessageException(errorMessage); } else { - setMessage(signatureAddError.getMessage()); + detailError = new DetailMessageException(signatureAddError.getMessage()); } } } else if (signatureAddError instanceof TooManyRequestsException) { - setMessage(Html.fromHtml(UrlMessage.withURL( - getContext(), - R.string.signature_update_signature_error_message_too_many_requests, - R.string.signature_update_signature_error_message_additional_information - ), Html.FROM_HTML_MODE_LEGACY)); + detailError = new TooManyRequestsException( + Html.fromHtml(UrlMessage.withURL( + getContext(), + R.string.signature_update_signature_error_message_too_many_requests, + R.string.signature_update_signature_error_message_additional_information + ), Html.FROM_HTML_MODE_LEGACY)); } else if (signatureAddError instanceof OcspInvalidTimeSlotException) { - setMessage(Html.fromHtml(UrlMessage.withURL( + detailError = new OcspInvalidTimeSlotException(Html.fromHtml(UrlMessage.withURL( getContext(), R.string.signature_update_signature_error_message_invalid_time_slot, R.string.signature_update_signature_error_message_additional_information ), Html.FROM_HTML_MODE_LEGACY)); } else if (signatureAddError instanceof CertificateRevokedException) { - setMessage(getContext().getString(R.string.signature_update_signature_error_message_certificate_revoked)); + updateError = new CertificateRevokedException( + getContext().getString(R.string.signature_update_signature_error_message_certificate_revoked) + ); } else { setTitle(R.string.signature_update_signature_add_error); - setMessage(signatureAddError.getMessage()); + updateError = new GeneralSignatureUpdateException(signatureAddError.getMessage()); } } else if (signatureRemoveError != null) { type = SIGNATURE_REMOVE; - setMessage(getContext().getString(R.string.signature_update_signature_remove_error)); + updateError = new GeneralSignatureUpdateException( + getContext().getString(R.string.signature_update_signature_remove_error)); + } + + if (updateError != null) { + String message = updateError.getMessage(getContext()); + setMessage(message); + } else if (detailError != null) { + Spanned detailMessage = detailError.getDetailMessage(getContext()); + if (detailMessage == null) { + setMessage(detailError.getMessage(getContext())); + } else { + setMessage(detailMessage); + } } else { dismiss(); return; @@ -168,13 +187,13 @@ public void onDismiss(DialogInterface dialog) { setTitle(null); removeListeners(); if (TextUtils.equals(type, DOCUMENTS_ADD)) { - documentsAddIntentSubject.onNext(Intent.DocumentsAddIntent.clear()); + documentsAddIntentSubject.onNext(DocumentsAddIntent.clear()); } else if (TextUtils.equals(type, DOCUMENT_REMOVE)) { - documentRemoveIntentSubject.onNext(Intent.DocumentRemoveIntent.clear()); + documentRemoveIntentSubject.onNext(DocumentRemoveIntent.clear()); } else if (TextUtils.equals(type, SIGNATURE_ADD)) { - signatureAddIntentSubject.onNext(Intent.SignatureAddIntent.clear()); + signatureAddIntentSubject.onNext(SignatureAddIntent.clear()); } else if (TextUtils.equals(type, SIGNATURE_REMOVE)) { - signatureRemoveIntentSubject.onNext(Intent.SignatureRemoveIntent.clear()); + signatureRemoveIntentSubject.onNext(SignatureRemoveIntent.clear()); } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateProgressBar.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateProgressBar.java index dc317b1ad..a1c9fefe3 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateProgressBar.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateProgressBar.java @@ -16,10 +16,12 @@ static void startProgressBar(ProgressBar progressBar) { progressBar.setMax((int) (isMobileIdProgressBar(progressBar) ? (MOBILE_ID_PROGRESS_BAR_TIMEOUT_CANCEL / 1000) : (SMART_ID_PROGRESS_BAR_TIMEOUT_CANCEL / 1000))); timeoutTimer = new CountDownTimer(isMobileIdProgressBar(progressBar) ? MOBILE_ID_PROGRESS_BAR_TIMEOUT_CANCEL : SMART_ID_PROGRESS_BAR_TIMEOUT_CANCEL, 1000) { + @Override public void onTick(long millisUntilFinished) { progressBar.incrementProgressBy(1); } + @Override public void onFinish() { stopProgressBar(progressBar); } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateSignatureAddDialog.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateSignatureAddDialog.java index bf25c476f..a5ab6c66e 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateSignatureAddDialog.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateSignatureAddDialog.java @@ -1,8 +1,16 @@ package ee.ria.DigiDoc.android.signature.update; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static ee.ria.DigiDoc.android.Constants.VOID; +import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDeviceLayoutWidth; + import android.content.Context; +import android.content.DialogInterface; import android.content.res.Configuration; import android.content.res.TypedArray; +import android.graphics.Color; import android.view.View; import android.view.Window; import android.view.WindowManager; @@ -11,23 +19,17 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.utils.SecureUtil; import ee.ria.DigiDoc.android.utils.ViewDisposables; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.rxbinding.app.ObservableDialogClickListener; import io.reactivex.rxjava3.core.Observable; -import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; -import static ee.ria.DigiDoc.android.Constants.VOID; -import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDeviceLayoutWidth; -import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDeviceOrientation; -import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDeviceWidth; -import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDialogLandscapeWidth; -import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDialogPortraitWidth; - -public final class SignatureUpdateSignatureAddDialog extends AlertDialog { +public final class SignatureUpdateSignatureAddDialog extends AlertDialog implements ContentView { private final SignatureUpdateSignatureAddView view; private final ObservableDialogClickListener positiveButtonClicks; @@ -35,6 +37,13 @@ public final class SignatureUpdateSignatureAddDialog extends AlertDialog { private final ViewDisposables disposables = new ViewDisposables(); + private final Button mobileIdPositiveButton; + private final Button mobileIdCancelButton; + private final Button smartIdPositiveButton; + private final Button smartIdCancelButton; + private final Button idCardPositiveButton; + private final Button idCardCancelButton; + SignatureUpdateSignatureAddDialog(@NonNull Context context) { super(context); SecureUtil.markAsSecure(context, getWindow()); @@ -45,15 +54,32 @@ public final class SignatureUpdateSignatureAddDialog extends AlertDialog { view = new SignatureUpdateSignatureAddView(getContext()); view.setId(R.id.signatureUpdateSignatureAdd); setView(view, padding, padding, padding, padding); + + positiveButtonClicks = new ObservableDialogClickListener(); + + // Buttons to show when in portrait mode setButton(BUTTON_POSITIVE, getContext().getString(R.string.signature_update_signature_add_positive_button), - positiveButtonClicks = new ObservableDialogClickListener()); + positiveButtonClicks); setButton(BUTTON_NEGATIVE, getContext().getString(android.R.string.cancel), (dialog, which) -> { cancel(); AccessibilityUtils.sendAccessibilityEvent(context, AccessibilityEvent.TYPE_ANNOUNCEMENT, R.string.signing_cancelled); } ); + + // Buttons to show when in landscape mode (to give more room for TextViews) + mobileIdPositiveButton = view.findViewById(R.id.signatureUpdateMobileIdSignButton); + mobileIdCancelButton = view.findViewById(R.id.signatureUpdateMobileIdCancelSigningButton); + setLandscapeButtons(getContext(), mobileIdPositiveButton, mobileIdCancelButton, positiveButtonClicks); + + smartIdPositiveButton = view.findViewById(R.id.signatureUpdateSmartIdSignButton); + smartIdCancelButton = view.findViewById(R.id.signatureUpdateSmartIdCancelSigningButton); + setLandscapeButtons(getContext(), smartIdPositiveButton, smartIdCancelButton, positiveButtonClicks); + + idCardPositiveButton = view.findViewById(R.id.signatureUpdateIdCardSignButton); + idCardCancelButton = view.findViewById(R.id.signatureUpdateIdCardCancelButton); + setLandscapeButtons(getContext(), idCardPositiveButton, idCardCancelButton, positiveButtonClicks); } public SignatureUpdateSignatureAddView view() { @@ -74,6 +100,12 @@ public void show() { WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); setCustomLayoutChangeListener(window); view.addOnLayoutChangeListener(getCustomLayoutChangeListener()); + + View parentPanel = findViewById(R.id.parentPanel); + int lastInvisibleElement = R.id.lastInvisibleElement; + if (parentPanel != null && parentPanel.findViewById(lastInvisibleElement) == null) { + ContentView.addInvisibleElementToObject(getContext(), findViewById(R.id.parentPanel)); + } } } @@ -84,9 +116,16 @@ public void onAttachedToWindow() { confirmButton.setContentDescription(getContext().getString(R.string.sign_container)); Button cancelButton = getButton(BUTTON_NEGATIVE); cancelButton.setContentDescription(getContext().getString(R.string.cancel_signing_process)); + setButtonsBasedOnOrientation(getContext().getResources().getConfiguration().orientation); + mobileIdCancelButton.setTextColor(ContextCompat.getColor(getContext(), R.color.accent)); + smartIdCancelButton.setTextColor(ContextCompat.getColor(getContext(), R.color.accent)); + idCardCancelButton.setTextColor(ContextCompat.getColor(getContext(), R.color.accent)); disposables.attach(); - disposables.add(view.positiveButtonEnabled().subscribe(enabled -> - getButton(BUTTON_POSITIVE).setEnabled(enabled))); + disposables.add(view.positiveButtonEnabled().subscribe(enabled -> { + Button positiveButton = getButton(BUTTON_POSITIVE); + Button[] additionalPositiveButtons = { mobileIdPositiveButton, smartIdPositiveButton, idCardPositiveButton }; + updateButtonStateAndColor(positiveButton, additionalPositiveButtons, enabled); + })); } @Override @@ -111,4 +150,53 @@ private void removeListeners() { view.removeOnLayoutChangeListener(layoutChangeListener); layoutChangeListener = null; } + + void setButtonsBasedOnOrientation(int orientation) { + Button confirmButton = getButton(BUTTON_POSITIVE); + Button cancelButton = getButton(BUTTON_NEGATIVE); + + if (confirmButton != null && cancelButton != null) { + confirmButton.setVisibility(orientation == Configuration.ORIENTATION_PORTRAIT ? VISIBLE : GONE); + cancelButton.setVisibility(orientation == Configuration.ORIENTATION_PORTRAIT ? VISIBLE : GONE); + } + + if (mobileIdPositiveButton != null && mobileIdCancelButton != null && + smartIdPositiveButton != null && smartIdCancelButton != null && + idCardPositiveButton != null && idCardCancelButton != null) { + int visibility = orientation == Configuration.ORIENTATION_LANDSCAPE ? VISIBLE : GONE; + mobileIdPositiveButton.setVisibility(visibility); + mobileIdCancelButton.setVisibility(visibility); + smartIdPositiveButton.setVisibility(visibility); + smartIdCancelButton.setVisibility(visibility); + idCardPositiveButton.setVisibility(visibility); + idCardCancelButton.setVisibility(visibility); + } + } + + private void setLandscapeButtons(Context context, Button positiveButton, Button cancelButton, ObservableDialogClickListener clickListener) { + positiveButton.setText(getContext().getString(R.string.signature_update_signature_add_positive_button)); + positiveButton.setOnClickListener(v -> clickListener.onClick(this, DialogInterface.BUTTON_POSITIVE)); + + cancelButton.setText(getContext().getString(android.R.string.cancel)); + cancelButton.setOnClickListener(v -> { + cancel(); + AccessibilityUtils.sendAccessibilityEvent(context, AccessibilityEvent.TYPE_ANNOUNCEMENT, R.string.signing_cancelled); + }); + } + + private void updateButtonStateAndColor(Button positiveButton, Button[] additionalPositiveButtons, boolean enabled) { + int textColor = enabled ? ContextCompat.getColor(getContext(), R.color.accent) : Color.GRAY; + + for (Button button : additionalPositiveButtons) { + if (button != null) { + button.setEnabled(enabled); + button.setTextColor(textColor); + } + } + + if (positiveButton != null) { + positiveButton.setEnabled(enabled); + positiveButton.setTextColor(textColor); + } + } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateSignatureAddView.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateSignatureAddView.java index b04b6d8f5..b477a1e06 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateSignatureAddView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateSignatureAddView.java @@ -1,16 +1,13 @@ package ee.ria.DigiDoc.android.signature.update; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static com.jakewharton.rxbinding4.widget.RxRadioGroup.checkedChanges; + import android.content.Context; import android.content.res.Configuration; -import android.text.Layout; -import android.text.SpannableString; -import android.text.style.AlignmentSpan; import android.util.AttributeSet; -import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityEvent; -import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -20,7 +17,6 @@ import androidx.annotation.Nullable; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.signature.update.idcard.IdCardResponse; import ee.ria.DigiDoc.android.signature.update.idcard.IdCardView; import ee.ria.DigiDoc.android.signature.update.mobileid.MobileIdResponse; @@ -30,10 +26,6 @@ import ee.ria.DigiDoc.android.utils.TextUtil; import io.reactivex.rxjava3.core.Observable; -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; -import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; -import static com.jakewharton.rxbinding4.widget.RxRadioGroup.checkedChanges; - public final class SignatureUpdateSignatureAddView extends LinearLayout { private final RadioGroup methodView; diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateView.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateView.java index ea0ce5ccb..1a2a1623d 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateView.java @@ -3,8 +3,13 @@ import static android.util.TypedValue.COMPLEX_UNIT_SP; import static android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT; import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED; +import static com.jakewharton.rxbinding4.view.RxView.clicks; +import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; import static ee.ria.DigiDoc.android.accessibility.AccessibilityUtils.isLargeFontEnabled; +import static ee.ria.DigiDoc.android.accessibility.AccessibilityUtils.isSmallFontEnabled; import static ee.ria.DigiDoc.android.utils.TextUtil.convertPxToDp; +import static com.jakewharton.rxbinding4.view.RxView.clicks; +import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; import static ee.ria.DigiDoc.android.utils.TintUtils.tintCompoundDrawables; import static ee.ria.DigiDoc.android.utils.display.DisplayUtil.getDisplayMetricsDpToInt; import static ee.ria.DigiDoc.android.utils.rxbinding.app.RxDialog.cancels; @@ -39,7 +44,7 @@ import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.Activity; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.accessibility.AccessibilityUtils; import ee.ria.DigiDoc.android.signature.update.mobileid.MobileIdResponse; import ee.ria.DigiDoc.android.signature.update.smartid.SmartIdResponse; @@ -51,11 +56,13 @@ import ee.ria.DigiDoc.android.utils.files.FileSystem; import ee.ria.DigiDoc.android.utils.mvi.MviView; import ee.ria.DigiDoc.android.utils.mvi.State; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.android.utils.navigator.Transaction; import ee.ria.DigiDoc.android.utils.widget.ConfirmationDialog; import ee.ria.DigiDoc.android.utils.widget.NotificationDialog; import ee.ria.DigiDoc.common.ActivityUtil; +import ee.ria.DigiDoc.mobileid.service.MobileSignService; import ee.ria.DigiDoc.sign.DataFile; import ee.ria.DigiDoc.sign.NoInternetConnectionException; import ee.ria.DigiDoc.sign.Signature; @@ -64,11 +71,8 @@ import io.reactivex.rxjava3.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.Subject; -import static com.jakewharton.rxbinding4.view.RxView.clicks; -import static com.jakewharton.rxbinding4.widget.RxToolbar.navigationClicks; - @SuppressLint("ViewConstructor") -public final class SignatureUpdateView extends LinearLayout implements MviView { +public final class SignatureUpdateView extends LinearLayout implements ContentView, MviView { private final ImmutableList ASICS_TIMESTAMP_CONTAINERS = ImmutableList.of("asics", "scs"); private static final ImmutableSet UNSIGNABLE_CONTAINER_EXTENSIONS = ImmutableSet.builder().add("adoc", "asics", "scs", "ddoc").build(); @@ -115,17 +119,17 @@ public final class SignatureUpdateView extends LinearLayout implements MviView documentsAddIntentSubject = + private final Subject documentsAddIntentSubject = PublishSubject.create(); - private final Subject documentSaveIntentSubject = + private final Subject documentSaveIntentSubject = PublishSubject.create(); - private final Subject documentRemoveIntentSubject = + private final Subject documentRemoveIntentSubject = PublishSubject.create(); - private final Subject signatureViewIntentSubject = + private final Subject signatureViewIntentSubject = PublishSubject.create(); - private final Subject signatureRemoveIntentSubject = + private final Subject signatureRemoveIntentSubject = PublishSubject.create(); - private final Subject signatureAddIntentSubject = + private final Subject signatureAddIntentSubject = PublishSubject.create(); @Nullable private DataFile sivaConfirmation; @@ -154,7 +158,7 @@ public SignatureUpdateView(Context context, String screenId, boolean isExistingC this.nestedFile = nestedFile; this.isSivaConfirmed = isSivaConfirmed; - navigator = Application.component(context).navigator(); + navigator = ApplicationApp.component(context).navigator(); viewModel = navigator.viewModel(screenId, SignatureUpdateViewModel.class); setOrientation(VERTICAL); @@ -205,6 +209,13 @@ public SignatureUpdateView(Context context, String screenId, boolean isExistingC setupAccessibilityTabs(); setActionButtonsTextSize(); + + ContentView.addInvisibleElement(getContext(), this); + + View lastElementView = findViewById(R.id.lastInvisibleElement); + lastElementView.setVisibility(VISIBLE); + + ContentView.addInvisibleElementScrollListener(listView, lastElementView); } @Override @@ -212,25 +223,27 @@ protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); setActionButtonsTextSize(); + + signatureAddDialog.setButtonsBasedOnOrientation(newConfig.orientation); } private void setActionButtonsTextSize() { signatureAddButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f); sendButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f); - mobileIdCancelButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f); - smartIdCancelButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f); + mobileIdCancelButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12.0f); + smartIdCancelButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12.0f); - if (isLargeFontEnabled(getResources())) { + if (isLargeFontEnabled(getResources()) || isSmallFontEnabled(getResources())) { signatureAddButton.setAutoSizeTextTypeUniformWithConfiguration(7, 12, 1, COMPLEX_UNIT_SP); sendButton.setAutoSizeTextTypeUniformWithConfiguration(7, 12, 1, COMPLEX_UNIT_SP); - mobileIdCancelButton.setAutoSizeTextTypeUniformWithConfiguration(7, 12, 1, COMPLEX_UNIT_SP); - smartIdCancelButton.setAutoSizeTextTypeUniformWithConfiguration(7, 12, 1, COMPLEX_UNIT_SP); + mobileIdCancelButton.setAutoSizeTextTypeUniformWithConfiguration(7, 20, 1, COMPLEX_UNIT_SP); + smartIdCancelButton.setAutoSizeTextTypeUniformWithConfiguration(7, 20, 1, COMPLEX_UNIT_SP); } else { signatureAddButton.setAutoSizeTextTypeUniformWithConfiguration(11, 20, 1, COMPLEX_UNIT_SP); sendButton.setAutoSizeTextTypeUniformWithConfiguration(11, 20, 1, COMPLEX_UNIT_SP); - mobileIdCancelButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f); - smartIdCancelButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16.0f); + mobileIdCancelButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12.0f); + smartIdCancelButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12.0f); } } @@ -521,88 +534,88 @@ private void resetSignatureAddDialog() { signatureAddView.reset(viewModel); } - private Observable initialIntent() { - return Observable.just(Intent.InitialIntent.create(isExistingContainer, containerFile, + private Observable initialIntent() { + return Observable.just(InitialIntent.create(isExistingContainer, containerFile, signatureAddVisible ? viewModel.signatureAddMethod() : null, signatureAddSuccessMessageVisible)); } @SuppressWarnings("unchecked") - private Observable nameUpdateIntent() { + private Observable nameUpdateIntent() { return Observable.mergeArray( - adapter.nameUpdateClicks().map(ignored -> Intent.NameUpdateIntent.show(containerFile)), + adapter.nameUpdateClicks().map(ignored -> NameUpdateIntent.show(containerFile)), cancels(nameUpdateDialog).map(ignored -> { AccessibilityUtils.sendAccessibilityEvent(getContext(), TYPE_ANNOUNCEMENT, R.string.container_name_change_cancelled); - return Intent.NameUpdateIntent.clear(); + return NameUpdateIntent.clear(); }), - nameUpdateDialog.updates().map(name -> Intent.NameUpdateIntent.update(containerFile, name))); + nameUpdateDialog.updates().map(name -> NameUpdateIntent.update(containerFile, name))); } - private Observable addDocumentsIntent() { + private Observable addDocumentsIntent() { return adapter.documentAddClicks() - .map(ignored -> Intent.DocumentsAddIntent.create(containerFile)) + .map(ignored -> DocumentsAddIntent.create(containerFile)) .mergeWith(documentsAddIntentSubject); } - private Observable documentViewIntent() { + private Observable documentViewIntent() { return Observable.mergeArray(adapter.documentClicks() - .map(document -> Intent.DocumentViewIntent.confirmation(getContext(), + .map(document -> DocumentViewIntent.confirmation(getContext(), (nestedFile != null && isSivaConfirmed) ? nestedFile : containerFile, document)), sivaConfirmationDialog.positiveButtonClicks() - .map(ignored -> Intent.DocumentViewIntent.open( + .map(ignored -> DocumentViewIntent.open( (nestedFile != null && isSivaConfirmed) ? nestedFile : containerFile, sivaConfirmation)), sivaConfirmationDialog.cancels() - .map(ignored -> Intent.DocumentViewIntent.cancel())); + .map(ignored -> DocumentViewIntent.cancel())); } - private Observable documentSaveIntent() { + private Observable documentSaveIntent() { return documentSaveIntentSubject; } - private Observable documentRemoveIntent() { + private Observable documentRemoveIntent() { return documentRemoveIntentSubject; } - private Observable signatureViewIntent() { + private Observable signatureViewIntent() { return adapter.signatureClicks() - .map(document -> Intent.SignatureViewIntent.create(containerFile, document)); + .map(document -> SignatureViewIntent.create(containerFile, document)); } - private Observable signatureRemoveIntent() { + private Observable signatureRemoveIntent() { return signatureRemoveIntentSubject; } @SuppressWarnings("unchecked") - private Observable signatureAddIntent() { + private Observable signatureAddIntent() { return Observable.mergeArray( clicks(signatureAddButton) .doOnNext(ignored -> resetSignatureAddDialog()) .map(ignored -> { int method = viewModel.signatureAddMethod(); - return Intent.SignatureAddIntent.show(method, isExistingContainer, containerFile); + return SignatureAddIntent.show(method, isExistingContainer, containerFile); }), cancels(signatureAddDialog) .doOnNext(ignored -> resetSignatureAddDialog()) - .map(ignored -> Intent.SignatureAddIntent.clear()), + .map(ignored -> SignatureAddIntent.clear()), signatureAddView.methodChanges().map(method -> { viewModel.setSignatureAddMethod(method); sendMethodSelectionAccessibilityEvent(method); - return Intent.SignatureAddIntent.show(method, isExistingContainer, containerFile); + return SignatureAddIntent.show(method, isExistingContainer, containerFile); }), signatureAddDialog.positiveButtonClicks().map(ignored -> { SignatureUpdateProgressBar.stopProgressBar(mobileIdProgressBar); SignatureUpdateProgressBar.stopProgressBar(smartIdProgressBar); - return Intent.SignatureAddIntent.sign(signatureAddView.method(), + return SignatureAddIntent.sign(signatureAddView.method(), isExistingContainer, containerFile, signatureAddView.request()); }), signatureAddIntentSubject ); } - private Observable sendIntent() { + private Observable sendIntent() { return clicks(sendButton) - .map(ignored -> Intent.SendIntent.create(containerFile)); + .map(ignored -> SendIntent.create(containerFile)); } private void sendMethodSelectionAccessibilityEvent(int method) { @@ -656,40 +669,41 @@ public void onAttachedToWindow() { })); disposables.add(adapter.scrollToTop().subscribe(ignored -> listView.scrollToPosition(0))); disposables.add(adapter.documentSaveClicks().subscribe(document -> - documentSaveIntentSubject.onNext(Intent.DocumentSaveIntent + documentSaveIntentSubject.onNext(DocumentSaveIntent .create((nestedFile != null) ? nestedFile : containerFile, document)))); disposables.add(adapter.signatureClicks().subscribe(signature -> - signatureViewIntentSubject.onNext(Intent.SignatureViewIntent + signatureViewIntentSubject.onNext(SignatureViewIntent .create(containerFile, signature)))); disposables.add(adapter.documentRemoveClicks().subscribe(document -> - documentRemoveIntentSubject.onNext(Intent.DocumentRemoveIntent + documentRemoveIntentSubject.onNext(DocumentRemoveIntent .showConfirmation(containerFile, dataFiles, document)))); disposables.add(documentRemoveConfirmationDialog.positiveButtonClicks().subscribe(ignored -> - documentRemoveIntentSubject.onNext(Intent.DocumentRemoveIntent + documentRemoveIntentSubject.onNext(DocumentRemoveIntent .remove(containerFile, dataFiles, documentRemoveConfirmation)))); disposables.add(documentRemoveConfirmationDialog.cancels().subscribe(ignored -> { AccessibilityUtils.sendAccessibilityEvent(getContext(), TYPE_ANNOUNCEMENT, R.string.document_removal_cancelled); - documentRemoveIntentSubject.onNext(Intent.DocumentRemoveIntent.clear()); + documentRemoveIntentSubject.onNext(DocumentRemoveIntent.clear()); })); disposables.add(adapter.signatureRemoveClicks().subscribe(signature -> - signatureRemoveIntentSubject.onNext(Intent.SignatureRemoveIntent + signatureRemoveIntentSubject.onNext(SignatureRemoveIntent .showConfirmation(containerFile, signature)))); disposables.add(signatureRemoveConfirmationDialog.positiveButtonClicks() .subscribe(ignored -> signatureRemoveIntentSubject - .onNext(Intent.SignatureRemoveIntent + .onNext(SignatureRemoveIntent .remove(containerFile, signatureRemoveConfirmation)))); disposables.add(signatureRemoveConfirmationDialog.cancels().subscribe(ignored -> { AccessibilityUtils.sendAccessibilityEvent(getContext(), TYPE_ANNOUNCEMENT, R.string.signature_removal_cancelled); - signatureRemoveIntentSubject.onNext(Intent.SignatureRemoveIntent.clear()); + signatureRemoveIntentSubject.onNext(SignatureRemoveIntent.clear()); })); disposables.add(clicks(mobileIdCancelButton).subscribe(ignored -> { + MobileSignService.setIsCancelled(true); resetSignatureAddDialog(); - signatureAddIntentSubject.onNext(Intent.SignatureAddIntent.clear()); + signatureAddIntentSubject.onNext(SignatureAddIntent.clear()); })); disposables.add(clicks(smartIdCancelButton).subscribe(ignored -> { SmartSignService.setIsCancelled(true); resetSignatureAddDialog(); - signatureAddIntentSubject.onNext(Intent.SignatureAddIntent.clear()); + signatureAddIntentSubject.onNext(SignatureAddIntent.clear()); })); } @@ -706,6 +720,7 @@ public void onDetachedFromWindow() { isTitleViewFocused = false; SignatureUpdateProgressBar.stopProgressBar(mobileIdProgressBar); SignatureUpdateProgressBar.stopProgressBar(smartIdProgressBar); + ContentView.removeInvisibleElementScrollListener(listView); super.onDetachedFromWindow(); } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateViewModel.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateViewModel.java index f856a86d3..58ab0afc6 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateViewModel.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/SignatureUpdateViewModel.java @@ -37,46 +37,15 @@ public String sidPersonalCode() { @Override protected Class initialIntentType() { - return Intent.InitialIntent.class; + return InitialIntent.class; } @Override protected Action action(Intent intent) { - if (intent instanceof Intent.InitialIntent) { - Intent.InitialIntent initialIntent = (Intent.InitialIntent) intent; - return Action.ContainerLoadAction - .create(initialIntent.containerFile(), initialIntent.signatureAddMethod(), - initialIntent.signatureAddSuccessMessageVisible(), initialIntent.isExistingContainer()); - } else if (intent instanceof Intent.DocumentsAddIntent) { - return Action.DocumentsAddAction - .create(((Intent.DocumentsAddIntent) intent).containerFile()); - } else if (intent instanceof Intent.DocumentRemoveIntent) { - Intent.DocumentRemoveIntent documentRemoveIntent = (Intent.DocumentRemoveIntent) intent; - return Action.DocumentRemoveAction.create(documentRemoveIntent.showConfirmation(), - documentRemoveIntent.containerFile(), documentRemoveIntent.documents(), documentRemoveIntent.document()); - } else if (intent instanceof Intent.SignatureViewIntent) { - Intent.SignatureViewIntent signatureViewIntent = (Intent.SignatureViewIntent) intent; - return Action.SignatureViewAction.create(signatureViewIntent.containerFile(), signatureViewIntent.signature()); - } else if (intent instanceof Intent.SignatureRemoveIntent) { - Intent.SignatureRemoveIntent signatureRemoveIntent = - (Intent.SignatureRemoveIntent) intent; - return Action.SignatureRemoveAction.create(signatureRemoveIntent.showConfirmation(), - signatureRemoveIntent.containerFile(), signatureRemoveIntent.signature()); - } else if (intent instanceof Intent.SignatureAddIntent) { - Intent.SignatureAddIntent signatureAddIntent = (Intent.SignatureAddIntent) intent; - if (signatureAddIntent.isCancelled()) { - return Action.SignatureAddAction.create(signatureAddIntent.method(), - null, null, null, true); - } - return Action.SignatureAddAction.create(signatureAddIntent.method(), - signatureAddIntent.existingContainer(), signatureAddIntent.containerFile(), - signatureAddIntent.request(), false); - } else if (intent instanceof Intent.SendIntent) { - return Action.SendAction.create(((Intent.SendIntent) intent).containerFile()); - } else if (intent instanceof Action) { + if (intent instanceof Action) { return (Action) intent; } else { - throw new IllegalArgumentException("Unknown intent " + intent); + return intent.action(); } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/exception/DocumentExistsException.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/exception/DocumentExistsException.java new file mode 100644 index 000000000..083fe9cbf --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/exception/DocumentExistsException.java @@ -0,0 +1,18 @@ +package ee.ria.DigiDoc.android.signature.update.exception; + +import android.content.Context; + +import java.io.IOException; + +import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.common.exception.SignatureUpdateError; + +public class DocumentExistsException extends IOException implements SignatureUpdateError { + + public DocumentExistsException() {} + + @Override + public String getMessage(Context context) { + return context.getString(R.string.signature_update_documents_add_error_exists); + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/exception/DocumentRemoveException.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/exception/DocumentRemoveException.java new file mode 100644 index 000000000..5b1666435 --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/exception/DocumentRemoveException.java @@ -0,0 +1,18 @@ +package ee.ria.DigiDoc.android.signature.update.exception; + +import android.content.Context; + +import java.io.IOException; + +import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.common.exception.SignatureUpdateError; + +public final class DocumentRemoveException extends IOException implements SignatureUpdateError { + + public DocumentRemoveException() {} + + @Override + public String getMessage(Context context) { + return context.getString(R.string.signature_update_document_remove_error); + } +} \ No newline at end of file diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/exception/GeneralSignatureUpdateException.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/exception/GeneralSignatureUpdateException.java new file mode 100644 index 000000000..842b77363 --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/exception/GeneralSignatureUpdateException.java @@ -0,0 +1,19 @@ +package ee.ria.DigiDoc.android.signature.update.exception; + +import android.content.Context; + +import ee.ria.DigiDoc.common.exception.SignatureUpdateError; + +public class GeneralSignatureUpdateException extends Exception implements SignatureUpdateError { + + private final String message; + + public GeneralSignatureUpdateException(String message) { + this.message = message; + } + + @Override + public String getMessage(Context context) { + return message; + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/mobileid/MobileIdOnSubscribe.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/mobileid/MobileIdOnSubscribe.java index 2455a808a..cd7c4193c 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/mobileid/MobileIdOnSubscribe.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/mobileid/MobileIdOnSubscribe.java @@ -1,40 +1,48 @@ package ee.ria.DigiDoc.android.signature.update.mobileid; +import static ee.ria.DigiDoc.mobileid.dto.request.MobileCreateSignatureRequest.toJson; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.ACCESS_TOKEN_PASS; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.ACCESS_TOKEN_PATH; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CERTIFICATE_CERT_BUNDLE; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CREATE_SIGNATURE_CHALLENGE; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CREATE_SIGNATURE_REQUEST; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CREATE_SIGNATURE_STATUS; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MID_BROADCAST_ACTION; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MID_BROADCAST_TYPE_KEY; +import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.SERVICE_FAULT; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.util.Log; +import android.content.SharedPreferences; +import android.content.res.Configuration; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; +import androidx.work.Data; +import androidx.work.ExistingWorkPolicy; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; + +import com.google.gson.Gson; -import java.util.ArrayList; import java.util.Locale; +import java.util.UUID; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.model.mobileid.MobileIdMessageException; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.configuration.ConfigurationProvider; import ee.ria.DigiDoc.mobileid.dto.request.MobileCreateSignatureRequest; import ee.ria.DigiDoc.mobileid.dto.response.MobileIdServiceResponse; import ee.ria.DigiDoc.mobileid.dto.response.RESTServiceFault; +import ee.ria.DigiDoc.mobileid.service.MobileSignService; import ee.ria.DigiDoc.sign.SignLib; import ee.ria.DigiDoc.sign.SignedContainer; import io.reactivex.rxjava3.core.ObservableEmitter; import io.reactivex.rxjava3.core.ObservableOnSubscribe; -import timber.log.Timber; - -import static ee.ria.DigiDoc.mobileid.dto.request.MobileCreateSignatureRequest.toJson; -import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.ACCESS_TOKEN_PASS; -import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.ACCESS_TOKEN_PATH; -import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CERTIFICATE_CERT_BUNDLE; -import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CREATE_SIGNATURE_CHALLENGE; -import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CREATE_SIGNATURE_REQUEST; -import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.CREATE_SIGNATURE_STATUS; -import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MID_BROADCAST_ACTION; -import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.MID_BROADCAST_TYPE_KEY; -import static ee.ria.DigiDoc.mobileid.service.MobileSignConstants.SERVICE_FAULT; public final class MobileIdOnSubscribe implements ObservableOnSubscribe { @@ -45,13 +53,11 @@ public final class MobileIdOnSubscribe implements ObservableOnSubscribe emitter) { BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (navigator.activity() == null) { - Timber.log(Log.ERROR,"Activity is null"); - IllegalStateException ise = new IllegalStateException("Activity not found. Please try again after restarting application"); - emitter.onError(ise); - } switch (intent.getStringExtra(MID_BROADCAST_TYPE_KEY)) { case SERVICE_FAULT: RESTServiceFault fault = RESTServiceFault .fromJson(intent.getStringExtra(SERVICE_FAULT)); + Configuration configuration = context.getResources().getConfiguration(); + configuration.setLocale(locale); + Context configuredContext = context.createConfigurationContext(configuration); if (fault.getStatus() != null) { emitter.onError(MobileIdMessageException - .create(navigator.activity(), fault.getStatus(), fault.getDetailMessage())); + .create(configuredContext, fault.getStatus(), fault.getDetailMessage())); } else { emitter.onError(MobileIdMessageException - .create(navigator.activity(), fault.getResult(), fault.getDetailMessage())); + .create(configuredContext, fault.getResult(), fault.getDetailMessage())); } break; case CREATE_SIGNATURE_CHALLENGE: @@ -102,7 +106,7 @@ public void onReceive(Context context, Intent intent) { break; default: emitter.onError(MobileIdMessageException - .create(navigator.activity(), status.getStatus(), null)); + .create(context, status.getStatus(), null)); break; } break; @@ -114,18 +118,34 @@ public void onReceive(Context context, Intent intent) { emitter.setCancellable(() -> broadcastManager.unregisterReceiver(receiver)); ConfigurationProvider configurationProvider = - ((Application) navigator.activity().getApplication()).getConfigurationProvider(); + ((ApplicationApp) navigator.activity().getApplication()).getConfigurationProvider(); String displayMessage = navigator.activity() .getString(R.string.signature_update_mobile_id_display_message); MobileCreateSignatureRequest request = MobileCreateSignatureRequestHelper .create(container, uuid, configurationProvider.getMidRestUrl(), configurationProvider.getMidSkRestUrl(), locale, personalCode, phoneNo, displayMessage); - intent.putExtra(CREATE_SIGNATURE_REQUEST, toJson(request)); - intent.putExtra(ACCESS_TOKEN_PASS, SignLib.accessTokenPass()); - intent.putExtra(ACCESS_TOKEN_PATH, SignLib.accessTokenPath()); - intent.putStringArrayListExtra(CERTIFICATE_CERT_BUNDLE, - new ArrayList<>(configurationProvider.getCertBundle())); - navigator.activity().startService(intent); + Gson gson = new Gson(); + String certBundleList = gson.toJson(configurationProvider.getCertBundle()); + + // WorkManager has 10KB data limit. Saving certs to SharedPreferences + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(navigator.activity()); + sharedPreferences.edit().putString(CERTIFICATE_CERT_BUNDLE, certBundleList).commit(); + + UUID uuid = UUID.randomUUID(); + Data inputData = new Data.Builder() + .putString(CREATE_SIGNATURE_REQUEST, toJson(request)) + .putString(ACCESS_TOKEN_PASS, SignLib.accessTokenPass()) + .putString(ACCESS_TOKEN_PATH, SignLib.accessTokenPath()) + .build(); + + + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(MobileSignService.class) + .addTag(SIGNING_TAG + uuid) + .setId(uuid) + .setInputData(inputData) + .build(); + + WorkManager.getInstance(navigator.activity()).enqueueUniqueWork(uuid.toString(), ExistingWorkPolicy.REPLACE, workRequest); } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/smartid/SmartIdOnSubscribe.java b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/smartid/SmartIdOnSubscribe.java index 7b13fe0eb..85d140b95 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/signature/update/smartid/SmartIdOnSubscribe.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/signature/update/smartid/SmartIdOnSubscribe.java @@ -20,6 +20,7 @@ package ee.ria.DigiDoc.android.signature.update.smartid; +import static ee.ria.DigiDoc.smartid.dto.request.SmartIDSignatureRequest.toJson; import static ee.ria.DigiDoc.smartid.dto.response.SessionStatusResponse.ProcessStatus.NO_RESPONSE; import static ee.ria.DigiDoc.smartid.service.SmartSignConstants.CERTIFICATE_CERT_BUNDLE; import static ee.ria.DigiDoc.smartid.service.SmartSignConstants.CREATE_SIGNATURE_CHALLENGE; @@ -36,7 +37,9 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.os.Build; import android.util.Log; @@ -44,11 +47,19 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; +import androidx.work.Data; +import androidx.work.ExistingWorkPolicy; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; -import java.util.ArrayList; +import com.google.gson.Gson; + +import java.util.Locale; +import java.util.UUID; import ee.ria.DigiDoc.R; -import ee.ria.DigiDoc.android.Application; +import ee.ria.DigiDoc.android.ApplicationApp; import ee.ria.DigiDoc.android.model.smartid.SmartIdMessageException; import ee.ria.DigiDoc.android.utils.navigator.Navigator; import ee.ria.DigiDoc.common.NotificationUtil; @@ -59,6 +70,7 @@ import ee.ria.DigiDoc.smartid.dto.response.ServiceFault; import ee.ria.DigiDoc.smartid.dto.response.SessionStatusResponse; import ee.ria.DigiDoc.smartid.dto.response.SmartIDServiceResponse; +import ee.ria.DigiDoc.smartid.service.SmartSignService; import io.reactivex.rxjava3.core.ObservableEmitter; import io.reactivex.rxjava3.core.ObservableOnSubscribe; import timber.log.Timber; @@ -69,22 +81,21 @@ public final class SmartIdOnSubscribe implements ObservableOnSubscribe emitter) { BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (navigator.activity() == null) { - Timber.log(Log.ERROR, "Activity is null"); - IllegalStateException ise = new IllegalStateException("Activity not found. Please try again after restarting application"); - emitter.onError(ise); - } switch (intent.getStringExtra(SID_BROADCAST_TYPE_KEY)) { case SERVICE_FAULT: { NotificationManagerCompat.from(navigator.activity()).cancelAll(); ServiceFault serviceFault = ServiceFault.fromJson(intent.getStringExtra(SERVICE_FAULT)); Timber.log(Log.DEBUG, "Got SERVICE_FAULT status: %s", serviceFault.getStatus()); + Configuration configuration = context.getResources().getConfiguration(); + configuration.setLocale(locale); + Context configuredContext = context.createConfigurationContext(configuration); if (serviceFault.getStatus() == NO_RESPONSE) { emitter.onError(SmartIdMessageException - .create(navigator.activity(), serviceFault.getStatus())); + .create(configuredContext, serviceFault.getStatus())); } else { emitter.onError(SmartIdMessageException - .create(navigator.activity(), serviceFault.getStatus(), serviceFault.getDetailMessage())); + .create(configuredContext, serviceFault.getStatus(), serviceFault.getDetailMessage())); } break; } @@ -122,9 +131,9 @@ public void onReceive(Context context, Intent intent) { intent.getStringExtra(CREATE_SIGNATURE_CHALLENGE); emitter.onNext(SmartIdResponse.challenge(challenge)); - if (!PowerUtil.isPowerSavingMode(navigator.activity())) { + if (!PowerUtil.isPowerSavingMode(context)) { Timber.log(Log.DEBUG, "Creating notification channel"); - NotificationUtil.createNotificationChannel(navigator.activity(), + NotificationUtil.createNotificationChannel(context, NOTIFICATION_CHANNEL, navigator.activity() .getResources() .getString(R.string.signature_update_signature_add_method_smart_id)); @@ -132,15 +141,19 @@ public void onReceive(Context context, Intent intent) { String challengeTitle = navigator.activity() .getResources().getString(R.string.smart_id_challenge); - Notification notification = NotificationUtil.createNotification(navigator.activity(), NOTIFICATION_CHANNEL, + Notification notification = NotificationUtil.createNotification(context, NOTIFICATION_CHANNEL, R.mipmap.ic_launcher, challengeTitle, challenge, NotificationCompat.PRIORITY_HIGH, false); - sendNotification(navigator.activity(), challenge, notification); + try { + sendNotification(context, challenge, notification); + } catch (NumberFormatException nfe) { + Timber.log(Log.ERROR, nfe, "Unable to send notification"); + } break; case CREATE_SIGNATURE_STATUS: - NotificationManagerCompat.from(navigator.activity()).cancelAll(); + NotificationManagerCompat.from(context).cancelAll(); SmartIDServiceResponse status = SmartIDServiceResponse.fromJson( intent.getStringExtra(CREATE_SIGNATURE_STATUS)); @@ -151,7 +164,7 @@ public void onReceive(Context context, Intent intent) { } else { Timber.log(Log.DEBUG, "Got CREATE_SIGNATURE_STATUS error status: %s", status.getStatus()); emitter.onError(SmartIdMessageException - .create(navigator.activity(), status.getStatus())); + .create(context, status.getStatus())); } break; } @@ -162,7 +175,7 @@ public void onReceive(Context context, Intent intent) { emitter.setCancellable(() -> broadcastManager.unregisterReceiver(receiver)); ConfigurationProvider configurationProvider = - ((Application) navigator.activity().getApplication()).getConfigurationProvider(); + ((ApplicationApp) navigator.activity().getApplication()).getConfigurationProvider(); String displayMessage = navigator.activity() .getString(R.string.signature_update_mobile_id_display_message); SmartIDSignatureRequest request = SmartCreateSignatureRequestHelper @@ -170,13 +183,28 @@ public void onReceive(Context context, Intent intent) { configurationProvider.getSidV2SkRestUrl(), country, personalCode, displayMessage); - intent.putExtra(CREATE_SIGNATURE_REQUEST, request); - intent.putStringArrayListExtra(CERTIFICATE_CERT_BUNDLE, - new ArrayList<>(configurationProvider.getCertBundle())); - navigator.activity().startService(intent); + Gson gson = new Gson(); + String certBundleList = gson.toJson(configurationProvider.getCertBundle()); + + // WorkManager has 10KB data limit. Saving certs to SharedPreferences + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(navigator.activity()); + sharedPreferences.edit().putString(CERTIFICATE_CERT_BUNDLE, certBundleList).commit(); + + UUID uuid = UUID.randomUUID(); + Data inputData = new Data.Builder() + .putString(CREATE_SIGNATURE_REQUEST, toJson(request)) + .build(); + + OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(SmartSignService.class) + .addTag(SIGNING_TAG + uuid) + .setId(uuid) + .setInputData(inputData) + .build(); + + WorkManager.getInstance(navigator.activity()).enqueueUniqueWork(uuid.toString(), ExistingWorkPolicy.REPLACE, workRequest); } - private void sendNotification(Context context, String challenge, Notification notification) { + private void sendNotification(Context context, String challenge, Notification notification) throws NumberFormatException { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { NotificationManagerCompat.from(navigator.activity()) .notify(Integer.parseInt(challenge), notification); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/BundleUtils.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/BundleUtils.java index cbbb67564..bfe5d8027 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/BundleUtils.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/BundleUtils.java @@ -1,30 +1,13 @@ package ee.ria.DigiDoc.android.utils; import android.os.Bundle; -import android.os.Parcelable; - -import com.google.common.collect.ImmutableList; import java.io.File; -import java.util.ArrayList; public final class BundleUtils { private BundleUtils() {} - public static void putParcelableImmutableList(Bundle bundle, String key, - ImmutableList value) { - ArrayList arrayList = new ArrayList<>(value.size()); - arrayList.addAll(value); - bundle.putParcelableArrayList(key, arrayList); - } - - @SuppressWarnings("ConstantConditions") - public static ImmutableList getParcelableImmutableList(Bundle bundle, - String key) { - return ImmutableList.copyOf(bundle.getParcelableArrayList(key)); - } - public static void putFile(Bundle bundle, String key, File value) { bundle.putString(key, value.getAbsolutePath()); } @@ -33,4 +16,9 @@ public static void putFile(Bundle bundle, String key, File value) { public static File getFile(Bundle bundle, String key) { return new File(bundle.getString(key)); } + + public static void putBoolean(Bundle bundle, String key, boolean value) { + bundle.putBoolean(key, value); + } + } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/IntentUtils.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/IntentUtils.java index 05b44a93c..50cb6fb05 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/IntentUtils.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/IntentUtils.java @@ -61,13 +61,13 @@ public final class IntentUtils { * * @return Intent to use with {@link android.app.Activity#startActivityForResult(Intent, int)}. */ - public static Intent createGetContentIntent() { + public static Intent createGetContentIntent(Boolean allowMultiple) { return Intent .createChooser(new Intent(Intent.ACTION_GET_CONTENT) .setType("*/*") .addCategory(Intent.CATEGORY_OPENABLE) .putExtra(Intent.EXTRA_REFERRER, R.string.application_name) - .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true), null); + .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple), null); } /** @@ -272,6 +272,15 @@ public static Intent createBrowserIntent(Context context, int stringRes, Configu return new Intent(Intent.ACTION_VIEW, Uri.parse(localizedUrl)); } + public static Intent setIntentData(Intent intent, Path filePath, android.app.Activity activity) { + intent.setData(Uri.parse(filePath.toUri().toString())); + intent.setClipData(ClipData.newRawUri(filePath.getFileName().toString(), FileProvider.getUriForFile( + activity, + activity.getString(R.string.file_provider_authority), + filePath.toFile()))); + return intent; + } + private static String getDataFileMimetype(DataFile dataFile) { int extensionIndex = dataFile.name().lastIndexOf("."); String extension = extensionIndex != -1 ? dataFile.name().substring(extensionIndex + 1) : ""; diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/TextUtil.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/TextUtil.java index f4e06db34..defc84103 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/TextUtil.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/TextUtil.java @@ -2,9 +2,11 @@ import static android.util.TypedValue.COMPLEX_UNIT_PX; import static android.view.View.GONE; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO; import static android.view.View.VISIBLE; import android.content.Context; +import android.graphics.Color; import android.text.Editable; import android.text.Layout; import android.text.SpannableString; @@ -26,6 +28,8 @@ import com.google.android.material.textfield.TextInputLayout; +import ee.ria.DigiDoc.R; + public class TextUtil { public static AppCompatEditText getTextView(View view) { @@ -102,6 +106,22 @@ public static int convertPxToDp(float size, Context context) { size, context.getResources().getDisplayMetrics()); } + public static TextView getInvisibleElementTextView(Context context) { + TextView textView = new TextView(context); + textView.setText(R.string.last_invisible_element_name); + textView.setTextColor(Color.GRAY); + textView.setId(R.id.lastInvisibleElement); + textView.setTag(R.string.last_invisible_element_tag); + textView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + textView.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + textView.setEnabled(false); + textView.setAlpha(0.001f); + + return textView; + } + public static TextWatcher addTextWatcher(EditText editText) { float defaultTextSize = editText.getTextSize(); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/ToastUtil.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/ToastUtil.java index 7d757f657..28f97696b 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/ToastUtil.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/ToastUtil.java @@ -20,18 +20,17 @@ public final class ToastUtil { public static void handleEmptyFileError(ImmutableList validFiles, - Application application, Context context) throws EmptyFileException { if (validFiles.isEmpty()) { Timber.log(Log.ERROR, "Add file to container failed"); throw new EmptyFileException(); } if (FileSystem.isEmptyFileInList(validFiles)) { - showEmptyFileError(context, application); + showEmptyFileError(context); } } - public static void showEmptyFileError(Context context, Application application) { + public static void showEmptyFileError(Context context) { Timber.log(Log.DEBUG, "Excluded empty files in list"); new Handler(Looper.getMainLooper()).post(() -> showError(context, R.string.empty_file_error)); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/ViewSavedState.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/ViewSavedState.java index 27abf7e8c..d1fa5d970 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/ViewSavedState.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/ViewSavedState.java @@ -1,5 +1,6 @@ package ee.ria.DigiDoc.android.utils; +import android.os.Build; import android.os.Parcel; import android.os.Parcelable; @@ -37,7 +38,7 @@ private ViewSavedState(Parcelable superState, byte[] state) { } private ViewSavedState(Parcel source, ClassLoader loader) { - superState = source.readParcelable(loader); + superState = getParcelable(source, loader); state = source.createByteArray(); } @@ -66,4 +67,12 @@ public ViewSavedState[] newArray(int size) { return new ViewSavedState[size]; } }; + + private Parcelable getParcelable(Parcel source, ClassLoader loader) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return source.readParcelable(loader, Parcelable.class); + } else { + return source.readParcelable(loader); + } + } } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/ViewUtil.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/ViewUtil.java new file mode 100644 index 000000000..8fb4ce0cb --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/ViewUtil.java @@ -0,0 +1,54 @@ +package ee.ria.DigiDoc.android.utils; + +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import androidx.recyclerview.widget.RecyclerView; + +public class ViewUtil { + + public static View findLastElement(View view) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + int subviews = viewGroup.getChildCount(); + if (subviews > 0) { + return findLastElement(viewGroup.getChildAt(subviews - 1)); + } + } + + return view; + } + + public static View findMainLayoutElement(View view) { + View mainView = findLastElement(view); + + if (mainView != null) { + ViewParent parent = mainView.getParent(); + if (parent != null) { + while ((parent instanceof LinearLayout || + parent instanceof RelativeLayout || + parent instanceof RecyclerView)) { + mainView = (View) parent; + parent = mainView.getParent(); + } + } + + } + return mainView; + } + + public static void moveView(View view) { + if (view != null) { + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + + if (layoutParams instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) layoutParams; + marginLayoutParams.setMargins(marginLayoutParams.leftMargin, -100, marginLayoutParams.rightMargin, marginLayoutParams.bottomMargin); + view.setLayoutParams(layoutParams); + } + } + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/container/NameUpdateView.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/container/NameUpdateView.java index b6650c8a8..b128bdc31 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/container/NameUpdateView.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/container/NameUpdateView.java @@ -12,6 +12,7 @@ import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.utils.display.DisplayUtil; +import ee.ria.DigiDoc.common.FileUtil; public final class NameUpdateView extends TextInputLayout { @@ -43,7 +44,7 @@ public String name() { public void name(String name) { if (name.startsWith(".")) { - editText.setText(nameWithoutExtension("newFile" + name)); + editText.setText(nameWithoutExtension(FileUtil.DEFAULT_FILENAME + name)); } else { editText.setText(nameWithoutExtension(name)); } diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/files/EmptyFileException.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/files/EmptyFileException.java index 35b903143..667d4eaa8 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/files/EmptyFileException.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/files/EmptyFileException.java @@ -1,8 +1,18 @@ package ee.ria.DigiDoc.android.utils.files; +import android.content.Context; + import java.io.IOException; -public final class EmptyFileException extends IOException { +import ee.ria.DigiDoc.R; +import ee.ria.DigiDoc.common.exception.SignatureUpdateError; + +public final class EmptyFileException extends IOException implements SignatureUpdateError { public EmptyFileException() {} + + @Override + public String getMessage(Context context) { + return context.getString(R.string.empty_file_error); + } } \ No newline at end of file diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/files/FileStream.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/files/FileStream.java index 6ac8e61d4..c4285eb94 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/files/FileStream.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/files/FileStream.java @@ -29,7 +29,7 @@ public abstract class FileStream { * Create FileStream from {@link ContentResolver} and content {@link Uri}. */ public static FileStream create(ContentResolver contentResolver, Uri uri, long fileSize) { - String displayName = uri.getLastPathSegment() == null ? "newFile" : FilenameUtils.getName(uri.getLastPathSegment()); + String displayName = uri.getLastPathSegment() == null ? FileUtil.DEFAULT_FILENAME : FilenameUtils.getName(uri.getLastPathSegment()); Uri sanitizedUri = FileUtil.normalizeUri(uri); Cursor cursor = contentResolver.query(sanitizedUri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null); diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/ContentView.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/ContentView.java new file mode 100644 index 000000000..5f555ef96 --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/ContentView.java @@ -0,0 +1,40 @@ +package ee.ria.DigiDoc.android.utils.navigator; + +import static ee.ria.DigiDoc.android.utils.ViewUtil.findMainLayoutElement; + +import android.content.Context; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +import ee.ria.DigiDoc.BuildConfig; + +public interface ContentView { + + // Adds an invisible label element to the bottom of the view. + // Used for autotests by testers + static void addInvisibleElement(Context context, View view) { + if (BuildConfig.DEBUG) { + View mainLayoutElement = findMainLayoutElement(view); + InvisibleView.addInvisibleElement(context, mainLayoutElement); + } + } + + static void addInvisibleElementToObject(Context context, View view) { + if (BuildConfig.DEBUG) { + InvisibleView.addInvisibleElementToObject(context, view); + } + } + + static void addInvisibleElementScrollListener(RecyclerView recyclerView, View lastElementView) { + if (BuildConfig.DEBUG) { + InvisibleView.addInvisibleElementScrollListener(recyclerView, lastElementView); + } + } + + static void removeInvisibleElementScrollListener(RecyclerView recyclerView) { + if (BuildConfig.DEBUG) { + InvisibleView.removeInvisibleElementScrollListener(recyclerView); + } + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/InvisibleView.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/InvisibleView.java new file mode 100644 index 000000000..6a789f35d --- /dev/null +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/InvisibleView.java @@ -0,0 +1,70 @@ +package ee.ria.DigiDoc.android.utils.navigator; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static ee.ria.DigiDoc.android.utils.TextUtil.getInvisibleElementTextView; +import static ee.ria.DigiDoc.android.utils.ViewUtil.moveView; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public class InvisibleView implements ContentView { + + private static boolean hasInvisibleElementMoved = false; + private static RecyclerView.OnScrollListener recyclerViewOnScrollListener; + + protected static void addInvisibleElementToObject(Context context, View view) { + if (view instanceof ViewGroup) { + ((ViewGroup) view).addView(getInvisibleElementTextView(context)); + } else if (view != null && view.getParent() instanceof ViewGroup) { + ((ViewGroup) view.getParent()).addView(getInvisibleElementTextView(context)); + } + } + + protected static void addInvisibleElement(Context context, View mainLayout) { + if (mainLayout instanceof ViewGroup) { + ((ViewGroup) mainLayout).addView(getInvisibleElementTextView(context)); + } else if (mainLayout != null && mainLayout.getParent() instanceof ViewGroup) { + ((ViewGroup) mainLayout.getParent()).addView(getInvisibleElementTextView(context)); + } + } + + protected static void addInvisibleElementScrollListener(RecyclerView recyclerView, View lastElementView) { + if (!hasInvisibleElementMoved) { + moveView(lastElementView); + hasInvisibleElementMoved = true; + } + + recyclerView.addOnScrollListener( + recyclerViewOnScrollListener = new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); + int totalItemCount; + if (layoutManager != null) { + totalItemCount = layoutManager.getItemCount(); + + int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition(); + + if (lastVisibleItemPosition == totalItemCount - 1) { + lastElementView.setVisibility(VISIBLE); + } else { + lastElementView.setVisibility(GONE); + } + } + } + }); + } + + public static void removeInvisibleElementScrollListener(RecyclerView recyclerView) { + recyclerView.removeOnScrollListener(recyclerViewOnScrollListener); + hasInvisibleElementMoved = false; + } +} diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/Transaction.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/Transaction.java index 08eff3b7a..26a23b66f 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/Transaction.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/Transaction.java @@ -1,13 +1,168 @@ package ee.ria.DigiDoc.android.utils.navigator; +import android.app.Activity; import android.content.Intent; import android.os.Bundle; import androidx.annotation.Nullable; +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.Router; +import com.bluelinelabs.conductor.RouterTransaction; import com.google.auto.value.AutoValue; public abstract class Transaction { + public abstract void execute(Router router, Activity activity); + + public static RootTransaction root(Screen screen) { + return new RootTransaction(screen); + } + + public static PushTransaction push(Screen screen) { + return new PushTransaction(screen); + } + + public static PopTransaction pop() { + return new PopTransaction(); + } + + public static ReplaceTransaction replace(Screen screen) { + return new ReplaceTransaction(screen); + } + + public static ActivityTransaction activity(Intent intent, Bundle options) { + return new ActivityTransaction(intent, options); + } + + public static ActivityForResultTransaction activityForResult(int requestCode, Intent intent, + @Nullable Bundle options) { + return new ActivityForResultTransaction(requestCode, intent, options); + } + + private static RouterTransaction routerTransaction(Screen screen) { + return RouterTransaction.with((Controller) screen); + } + + public static class RootTransaction extends Transaction { + private final Screen screen; + + public RootTransaction(Screen screen) { + this.screen = screen; + } + + public Screen getScreen() { + return screen; + } + + @Override + public void execute(Router router, Activity activity) { + router.setRoot(routerTransaction(screen)); + } + } + + public static class PushTransaction extends Transaction { + private final Screen screen; + + public PushTransaction(Screen screen) { + this.screen = screen; + } + + public Screen getScreen() { + return screen; + } + + @Override + public void execute(Router router, Activity activity) { + router.pushController(routerTransaction(screen)); + } + } + + public static class PopTransaction extends Transaction { + @Override + public void execute(Router router, Activity activity) { + if (activity != null) { + activity.onBackPressed(); + } + } + } + + public static class ReplaceTransaction extends Transaction { + private final Screen screen; + + public ReplaceTransaction(Screen screen) { + this.screen = screen; + } + + public Screen getScreen() { + return screen; + } + + @Override + public void execute(Router router, Activity activity) { + router.replaceTopController(routerTransaction(screen)); + } + } + + public static class ActivityTransaction extends Transaction { + private final Intent intent; + @Nullable private final Bundle options; + + public ActivityTransaction(Intent intent, @Nullable Bundle options) { + this.intent = intent; + this.options = options; + } + + public Intent getIntent() { + return intent; + } + + @Nullable + public Bundle getOptions() { + return options; + } + + @Override + public void execute(Router router, Activity activity) { + activity.startActivity(intent, options); + } + } + + public static class ActivityForResultTransaction extends Transaction { + private final int requestCode; + private final Intent intent; + @Nullable private final Bundle options; + + public ActivityForResultTransaction(int requestCode, Intent intent, + @Nullable Bundle options) { + this.requestCode = requestCode; + this.intent = intent; + this.options = options; + } + + public int getRequestCode() { + return requestCode; + } + + public Intent getIntent() { + return intent; + } + + @Nullable + public Bundle getOptions() { + return options; + } + + @Override + public void execute(Router router, Activity activity) { + activity.startActivityForResult(intent, requestCode, options); + } + } +} + + + +/*public abstract class Transaction { + public static RootTransaction root(Screen screen) { return RootTransaction.create(screen); } @@ -98,4 +253,4 @@ static ActivityForResultTransaction create(int requestCode, Intent intent, options); } } -} +}*/ diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/conductor/ConductorNavigator.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/conductor/ConductorNavigator.java index f416e809b..9111fa654 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/conductor/ConductorNavigator.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/navigator/conductor/ConductorNavigator.java @@ -127,32 +127,7 @@ public void clearViewModel(String screenId) { @Override public void execute(Transaction transaction) { Timber.log(Log.DEBUG, "Execute: %s", transaction); - if (transaction instanceof Transaction.RootTransaction) { - router.setRoot(routerTransaction(((Transaction.RootTransaction) transaction).screen())); - } else if (transaction instanceof Transaction.PushTransaction) { - router.pushController(routerTransaction( - ((Transaction.PushTransaction) transaction).screen())); - } else if (transaction instanceof Transaction.PopTransaction) { - Activity activity = router.getActivity(); - if (activity != null) { - activity.onBackPressed(); - } - } else if (transaction instanceof Transaction.ReplaceTransaction) { - router.replaceTopController(routerTransaction( - ((Transaction.ReplaceTransaction) transaction).screen())); - } else if (transaction instanceof Transaction.ActivityTransaction) { - Transaction.ActivityTransaction activityTransaction = - (Transaction.ActivityTransaction) transaction; - activity().startActivity(activityTransaction.intent(), activityTransaction.options()); - } else if (transaction instanceof Transaction.ActivityForResultTransaction) { - Transaction.ActivityForResultTransaction activityForResultTransaction = - (Transaction.ActivityForResultTransaction) transaction; - activity().startActivityForResult(activityForResultTransaction.intent(), - activityForResultTransaction.requestCode(), - activityForResultTransaction.options()); - } else { - throw new IllegalArgumentException("Unknown transaction " + transaction); - } + transaction.execute(router, activity()); } @Override diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/validator/PersonalCodeValidator.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/validator/PersonalCodeValidator.java index 7753ceecd..250dc3063 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/validator/PersonalCodeValidator.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/validator/PersonalCodeValidator.java @@ -65,7 +65,12 @@ private static boolean isChecksumValid(String personalCode) { int pos2 = 3; while (i < 10) { - int personalCodeNumber = Integer.parseInt(personalCode.substring(i, i + 1)); + int personalCodeNumber = 0; + try { + personalCodeNumber = Integer.parseInt(personalCode.substring(i, i + 1)); + } catch (NumberFormatException nfe) { + Timber.log(Log.ERROR, nfe, "Unable to parse personal code number"); + } sum1 += personalCodeNumber * pos1; sum2 += personalCodeNumber * pos2; pos1 = pos1 == 9 ? 1 : pos1 + 1; diff --git a/app/src/main/java/ee/ria/DigiDoc/android/utils/widget/ConfirmationDialog.java b/app/src/main/java/ee/ria/DigiDoc/android/utils/widget/ConfirmationDialog.java index 0e08d3a05..ad4c687ba 100644 --- a/app/src/main/java/ee/ria/DigiDoc/android/utils/widget/ConfirmationDialog.java +++ b/app/src/main/java/ee/ria/DigiDoc/android/utils/widget/ConfirmationDialog.java @@ -14,6 +14,7 @@ import ee.ria.DigiDoc.R; import ee.ria.DigiDoc.android.Constants; import ee.ria.DigiDoc.android.utils.SecureUtil; +import ee.ria.DigiDoc.android.utils.navigator.ContentView; import ee.ria.DigiDoc.android.utils.rxbinding.app.RxDialog; import ee.ria.DigiDoc.sign.utils.UrlMessage; @@ -21,7 +22,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.Subject; -public final class ConfirmationDialog extends AlertDialog implements +public final class ConfirmationDialog extends AlertDialog implements ContentView, DialogInterface.OnClickListener { private final Subject buttonClicksSubject = PublishSubject.create(); diff --git a/app/src/main/res/drawable/ic_icon_access_padded.xml b/app/src/main/res/drawable/ic_icon_access_padded.xml new file mode 100644 index 000000000..f71d479cf --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_access_padded.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/crypto_create_decrypt_dialog.xml b/app/src/main/res/layout/crypto_create_decrypt_dialog.xml index ede266401..40f5addf1 100644 --- a/app/src/main/res/layout/crypto_create_decrypt_dialog.xml +++ b/app/src/main/res/layout/crypto_create_decrypt_dialog.xml @@ -69,7 +69,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" - android:minHeight="@dimen/material_baseline_grid_6x" + android:minHeight="@dimen/material_baseline_grid_2x" app:errorEnabled="true" app:expandedHintEnabled="false"> @@ -77,12 +77,10 @@ android:id="@id/cryptoCreateDecryptPin1" android:layout_width="match_parent" android:layout_height="wrap_content" + android:gravity="center_horizontal" android:inputType="numberPassword" - android:digits="0123456789" - android:imeOptions="actionDone" - android:maxLines="1" android:maxLength="12" - android:minHeight="@dimen/material_baseline_grid_6x" /> + android:minHeight="@dimen/material_baseline_grid_4x" /> diff --git a/app/src/main/res/layout/crypto_create_list_item_add_button.xml b/app/src/main/res/layout/crypto_create_list_item_add_button.xml index b5cc66841..f5d29bbcc 100644 --- a/app/src/main/res/layout/crypto_create_list_item_add_button.xml +++ b/app/src/main/res/layout/crypto_create_list_item_add_button.xml @@ -2,7 +2,7 @@