diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png index 497107941f..0e9310c7e5 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_check.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png index 98f675eec0..cf556b7259 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_delete.png differ diff --git a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png index 02e0f1f84e..8ae67d62bf 100644 Binary files a/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png and b/app/screenshots/gplay/debug/com.owncloud.android.ui.activity.PassCodeActivityIT_request.png differ diff --git a/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java deleted file mode 100644 index 279b8cd9a0..0000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java +++ /dev/null @@ -1,478 +0,0 @@ -/* - * ownCloud Android client application - * - * @author Bartek Przybylski - * @author masensio - * @author David A. Velasco - * Copyright (C) 2011 Bartek Przybylski - * Copyright (C) 2015 ownCloud Inc. - * Copyright (C) 2020 Kwon Yuna - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.owncloud.android.ui.activity; - -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextUtils; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.View; -import android.view.Window; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; - -import com.google.android.material.snackbar.Snackbar; -import com.nextcloud.client.di.Injectable; -import com.nextcloud.client.preferences.AppPreferences; -import com.owncloud.android.R; -import com.owncloud.android.authentication.PassCodeManager; -import com.owncloud.android.databinding.PasscodelockBinding; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.ui.components.PassCodeEditText; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.util.Arrays; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AppCompatActivity; - -public class PassCodeActivity extends AppCompatActivity implements Injectable { - - private static final String TAG = PassCodeActivity.class.getSimpleName(); - private static final String KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS"; - private static final String KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE"; - - public final static String ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT"; - public final static String ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT"; - public final static String ACTION_CHECK = "ACTION_CHECK"; - public final static String KEY_PASSCODE = "KEY_PASSCODE"; - public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT"; - - public final static String PREFERENCE_PASSCODE_D = "PrefPinCode"; - public final static String PREFERENCE_PASSCODE_D1 = "PrefPinCode1"; - public final static String PREFERENCE_PASSCODE_D2 = "PrefPinCode2"; - public final static String PREFERENCE_PASSCODE_D3 = "PrefPinCode3"; - public final static String PREFERENCE_PASSCODE_D4 = "PrefPinCode4"; - - @Inject AppPreferences preferences; - @Inject PassCodeManager passCodeManager; - @Inject ViewThemeUtils viewThemeUtils; - private PasscodelockBinding binding; - private final PassCodeEditText[] passCodeEditTexts = new PassCodeEditText[4]; - private String[] passCodeDigits = {"", "", "", ""}; - private boolean confirmingPassCode; - private boolean changed = true; // to control that only one blocks jump - - /** - * Initializes the activity. - *

- * An intent with a valid ACTION is expected; if none is found, an {@link IllegalArgumentException} will be thrown. - * - * @param savedInstanceState Previously saved state - irrelevant in this case - */ - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = PasscodelockBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - viewThemeUtils.platform.colorTextButtons(binding.cancel); - - passCodeEditTexts[0] = binding.txt0; - passCodeEditTexts[1] = binding.txt1; - passCodeEditTexts[2] = binding.txt2; - passCodeEditTexts[3] = binding.txt3; - - for (EditText passCodeEditText : passCodeEditTexts) { - viewThemeUtils.platform.colorEditText(passCodeEditText); - } - - passCodeEditTexts[0].requestFocus(); - - Window window = getWindow(); - if (window != null) { - window.setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); - } - - if (ACTION_CHECK.equals(getIntent().getAction())) { - /// this is a pass code request; the user has to input the right value - binding.header.setText(R.string.pass_code_enter_pass_code); - binding.explanation.setVisibility(View.INVISIBLE); - setCancelButtonEnabled(false); // no option to cancel - - showDelay(); - - } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) { - if (savedInstanceState != null) { - confirmingPassCode = savedInstanceState.getBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE); - passCodeDigits = savedInstanceState.getStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS); - } - if (confirmingPassCode) { - // the app was in the passcode confirmation - requestPassCodeConfirmation(); - } else { - // pass code preference has just been activated in SettingsActivity; - // will receive and confirm pass code value - binding.header.setText(R.string.pass_code_configure_your_pass_code); - - binding.explanation.setVisibility(View.VISIBLE); - } - setCancelButtonEnabled(true); - - } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) { - // pass code preference has just been disabled in SettingsActivity; - // will confirm user knows pass code, then remove it - binding.header.setText(R.string.pass_code_remove_your_pass_code); - binding.explanation.setVisibility(View.INVISIBLE); - setCancelButtonEnabled(true); - - } else { - throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to " + TAG); - } - - setTextListeners(); - } - - /** - * Enables or disables the cancel button to allow the user interrupt the ACTION requested to the activity. - * - * @param enabled 'True' makes the cancel button available, 'false' hides it. - */ - protected void setCancelButtonEnabled(boolean enabled) { - if (enabled) { - binding.cancel.setVisibility(View.VISIBLE); - binding.cancel.setOnClickListener(v -> finish()); - } else { - binding.cancel.setVisibility(View.INVISIBLE); - binding.cancel.setOnClickListener(null); - } - } - - @VisibleForTesting - public PasscodelockBinding getBinding() { - return binding; - } - - /** - * Binds the appropriate listeners to the input boxes receiving each digit of the pass code. - */ - protected void setTextListeners() { - for (int i = 0; i < passCodeEditTexts.length; i++) { - final PassCodeEditText editText = passCodeEditTexts[i]; - boolean isLast = (i == 3); - - editText.addTextChangedListener(new PassCodeDigitTextWatcher(i, isLast)); - if (i > 0) { - setOnKeyListener(i); - } - - int finalIndex = i; - editText.setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(finalIndex)); - } - } - - private void onPassCodeEditTextFocusChange(final int passCodeIndex) { - for (int i = 0; i < passCodeIndex; i++) { - if (TextUtils.isEmpty(passCodeEditTexts[i].getText())) { - passCodeEditTexts[i].requestFocus(); - break; - } - } - } - - private void setOnKeyListener(final int passCodeIndex) { - passCodeEditTexts[passCodeIndex].setOnKeyListener((v, keyCode, event) -> { - if (keyCode == KeyEvent.KEYCODE_DEL && changed) { - passCodeEditTexts[passCodeIndex - 1].requestFocus(); - if (!confirmingPassCode) { - passCodeDigits[passCodeIndex - 1] = ""; - } - passCodeEditTexts[passCodeIndex - 1].setText(""); - changed = false; - - } else if (!changed) { - changed = true; - } - return false; - }); - } - - /** - * Processes the pass code entered by the user just after the last digit was in. - *

- * Takes into account the action requested to the activity, the currently saved pass code and the previously typed - * pass code, if any. - */ - private void processFullPassCode() { - if (ACTION_CHECK.equals(getIntent().getAction())) { - if (checkPassCode()) { - preferences.resetPinWrongAttempts(); - - /// pass code accepted in request, user is allowed to access the app - passCodeManager.updateLockTimestamp(); - hideSoftKeyboard(); - finish(); - - } else { - preferences.increasePinWrongAttempts(); - - showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE); - } - - } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) { - if (checkPassCode()) { - passCodeManager.updateLockTimestamp(); - Intent resultIntent = new Intent(); - resultIntent.putExtra(KEY_CHECK_RESULT, true); - setResult(RESULT_OK, resultIntent); - hideSoftKeyboard(); - finish(); - } else { - showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE); - } - - } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) { - /// enabling pass code - if (!confirmingPassCode) { - requestPassCodeConfirmation(); - - } else if (confirmPassCode()) { - /// confirmed: user typed the same pass code twice - savePassCodeAndExit(); - - } else { - showErrorAndRestart(R.string.pass_code_mismatch, R.string.pass_code_configure_your_pass_code, View.VISIBLE); - } - } - } - - private void hideSoftKeyboard() { - View focusedView = getCurrentFocus(); - if (focusedView != null) { - InputMethodManager inputMethodManager = - (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow( - focusedView.getWindowToken(), - 0); - } - } - - private void showErrorAndRestart(int errorMessage, int headerMessage, int explanationVisibility) { - Arrays.fill(passCodeDigits, null); - Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show(); - binding.header.setText(headerMessage); // TODO check if really needed - binding.explanation.setVisibility(explanationVisibility); // TODO check if really needed - clearBoxes(); - - showDelay(); - } - - - /** - * Ask to the user for retyping the pass code just entered before saving it as the current pass code. - */ - protected void requestPassCodeConfirmation() { - clearBoxes(); - binding.header.setText(R.string.pass_code_reenter_your_pass_code); - binding.explanation.setVisibility(View.INVISIBLE); - confirmingPassCode = true; - } - - /** - * Compares pass code entered by the user with the value currently saved in the app. - * - * @return 'True' if entered pass code equals to the saved one. - */ - protected boolean checkPassCode() { - String[] savedPassCodeDigits = preferences.getPassCode(); - - boolean result = true; - for (int i = 0; i < passCodeDigits.length && result; i++) { - result = passCodeDigits[i] != null && passCodeDigits[i].equals(savedPassCodeDigits[i]); - } - return result; - } - - /** - * Compares pass code retyped by the user in the input fields with the value entered just before. - * - * @return 'True' if retyped pass code equals to the entered before. - */ - protected boolean confirmPassCode() { - confirmingPassCode = false; - - for (int i = 0; i < passCodeEditTexts.length; i++) { - Editable passCodeText = passCodeEditTexts[i].getText(); - if (passCodeText == null || !passCodeText.toString().equals(passCodeDigits[i])) { - return false; - } - } - return true; - } - - /** - * Sets the input fields to empty strings and puts the focus on the first one. - */ - protected void clearBoxes() { - for (EditText mPassCodeEditText : passCodeEditTexts) { - mPassCodeEditText.setText(""); - } - passCodeEditTexts[0].requestFocus(); - } - - /** - * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing than - * ACTION_CHECK may be worked around. - * - * @param keyCode Key code of the key that triggered the down event. - * @param event Event triggered. - * @return 'True' when the key event was processed by this method. - */ - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { - if (ACTION_CHECK.equals(getIntent().getAction())) { - moveTaskToBack(true); - finishAndRemoveTask(); - } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction()) || - ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) { - finish(); - }// else, do nothing, but report that the key was consumed to stay alive - return true; - } - return super.onKeyDown(keyCode, event); - } - - /** - * Saves the pass code input by the user as the current pass code. - */ - protected void savePassCodeAndExit() { - Intent resultIntent = new Intent(); - resultIntent.putExtra(KEY_PASSCODE, - passCodeDigits[0] + passCodeDigits[1] + passCodeDigits[2] + passCodeDigits[3]); - - setResult(RESULT_OK, resultIntent); - - passCodeManager.updateLockTimestamp(); - - finish(); - } - - private void showDelay() { - int delay = preferences.pinBruteForceDelay(); - - if (delay > 0) { - binding.explanation.setText(R.string.brute_force_delay); - binding.explanation.setVisibility(View.VISIBLE); - binding.txt0.setEnabled(false); - binding.txt1.setEnabled(false); - binding.txt2.setEnabled(false); - binding.txt3.setEnabled(false); - - new Thread(new Runnable() { - @Override - public void run() { - try { - Thread.sleep(delay * 1000L); - - runOnUiThread(() -> { - binding.explanation.setVisibility(View.INVISIBLE); - binding.txt0.setEnabled(true); - binding.txt1.setEnabled(true); - binding.txt2.setEnabled(true); - binding.txt3.setEnabled(true); - }); - } catch (InterruptedException e) { - Log_OC.e(this, "Could not delay password input prompt"); - } - } - }).start(); - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE, confirmingPassCode); - outState.putStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS, passCodeDigits); - } - - private class PassCodeDigitTextWatcher implements TextWatcher { - - private int mIndex = -1; - private boolean mLastOne; - - /** - * Constructor - * - * @param index Position in the pass code of the input field that will be bound to this watcher. - * @param lastOne 'True' means that watcher corresponds to the last position of the pass code. - */ - PassCodeDigitTextWatcher(int index, boolean lastOne) { - mIndex = index; - mLastOne = lastOne; - - if (mIndex < 0) { - throw new IllegalArgumentException( - "Invalid index in " + PassCodeDigitTextWatcher.class.getSimpleName() + - " constructor" - ); - } - } - - private int next() { - return mLastOne ? 0 : mIndex + 1; - } - - /** - * Performs several actions when the user types a digit in an input field: - saves the input digit to the state - * of the activity; this will allow retyping the pass code to confirm it. - moves the focus automatically to the - * next field - for the last field, triggers the processing of the full pass code - * - * @param s Changed text - */ - @Override - public void afterTextChanged(Editable s) { - if (s.length() > 0) { - if (!confirmingPassCode) { - Editable passCodeText = passCodeEditTexts[mIndex].getText(); - - if (passCodeText != null) { - passCodeDigits[mIndex] = passCodeText.toString(); - } - } - - if (mLastOne) { - processFullPassCode(); - } else { - passCodeEditTexts[next()].requestFocus(); - } - - } else { - Log_OC.d(TAG, "Text box " + mIndex + " was cleaned"); - } - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) {} - } - -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt new file mode 100644 index 0000000000..db7f1f54a2 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.kt @@ -0,0 +1,437 @@ +/* + * ownCloud Android client application + * + * @author Bartek Przybylski + * @author masensio + * @author David A. Velasco + * Copyright (C) 2011 Bartek Przybylski + * Copyright (C) 2015 ownCloud Inc. + * Copyright (C) 2020 Kwon Yuna + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.owncloud.android.ui.activity + +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.text.TextUtils +import android.text.TextWatcher +import android.view.KeyEvent +import android.view.View +import android.view.WindowManager +import android.view.inputmethod.InputMethodManager +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.snackbar.Snackbar +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.nextcloud.client.di.Injectable +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.R +import com.owncloud.android.authentication.PassCodeManager +import com.owncloud.android.databinding.PasscodelockBinding +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.components.PassCodeEditText +import com.owncloud.android.utils.theme.ViewThemeUtils +import java.util.Arrays +import javax.inject.Inject + +@Suppress("TooManyFunctions", "MagicNumber") +class PassCodeActivity : AppCompatActivity(), Injectable { + + companion object { + private val TAG = PassCodeActivity::class.java.simpleName + + private const val KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS" + private const val KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE" + const val ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT" + const val ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT" + const val ACTION_CHECK = "ACTION_CHECK" + const val KEY_PASSCODE = "KEY_PASSCODE" + const val KEY_CHECK_RESULT = "KEY_CHECK_RESULT" + const val PREFERENCE_PASSCODE_D = "PrefPinCode" + const val PREFERENCE_PASSCODE_D1 = "PrefPinCode1" + const val PREFERENCE_PASSCODE_D2 = "PrefPinCode2" + const val PREFERENCE_PASSCODE_D3 = "PrefPinCode3" + const val PREFERENCE_PASSCODE_D4 = "PrefPinCode4" + } + + @JvmField + @Inject + var preferences: AppPreferences? = null + + @JvmField + @Inject + var passCodeManager: PassCodeManager? = null + + @JvmField + @Inject + var viewThemeUtils: ViewThemeUtils? = null + + @get:VisibleForTesting + lateinit var binding: PasscodelockBinding + private set + + private val passCodeEditTexts = arrayOfNulls(4) + private var passCodeDigits: Array? = arrayOf("", "", "", "") + private var confirmingPassCode = false + private var changed = true // to control that only one blocks jump + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = PasscodelockBinding.inflate(layoutInflater) + setContentView(binding.root) + + applyTint() + setupPasscodeEditTexts() + setSoftInputMode() + setupUI(savedInstanceState) + setTextListeners() + } + + private fun applyTint() { + viewThemeUtils?.platform?.colorViewBackground(binding.cardViewContent, ColorRole.SURFACE_VARIANT) + viewThemeUtils?.material?.colorMaterialButtonPrimaryBorderless(binding.cancel) + } + + private fun setupPasscodeEditTexts() { + passCodeEditTexts[0] = binding.txt0 + passCodeEditTexts[1] = binding.txt1 + passCodeEditTexts[2] = binding.txt2 + passCodeEditTexts[3] = binding.txt3 + + passCodeEditTexts.forEach { + it?.let { viewThemeUtils?.platform?.colorEditText(it) } + } + + passCodeEditTexts[0]?.requestFocus() + } + + private fun setSoftInputMode() { + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + } + + private fun setupUI(savedInstanceState: Bundle?) { + if (ACTION_CHECK == intent.action) { + // / this is a pass code request; the user has to input the right value + binding.header.setText(R.string.pass_code_enter_pass_code) + binding.explanation.visibility = View.INVISIBLE + setCancelButtonEnabled(false) // no option to cancel + showDelay() + } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { + if (savedInstanceState != null) { + confirmingPassCode = savedInstanceState.getBoolean(KEY_CONFIRMING_PASSCODE) + passCodeDigits = savedInstanceState.getStringArray(KEY_PASSCODE_DIGITS) + } + if (confirmingPassCode) { + // the app was in the passcode confirmation + requestPassCodeConfirmation() + } else { + // pass code preference has just been activated in SettingsActivity; + // will receive and confirm pass code value + binding.header.setText(R.string.pass_code_configure_your_pass_code) + binding.explanation.visibility = View.VISIBLE + } + setCancelButtonEnabled(true) + } else if (ACTION_CHECK_WITH_RESULT == intent.action) { + // pass code preference has just been disabled in SettingsActivity; + // will confirm user knows pass code, then remove it + binding.header.setText(R.string.pass_code_remove_your_pass_code) + binding.explanation.visibility = View.INVISIBLE + setCancelButtonEnabled(true) + } else { + throw IllegalArgumentException("A valid ACTION is needed in the Intent passed to $TAG") + } + } + + private fun setCancelButtonEnabled(enabled: Boolean) { + binding.cancel.visibility = if (enabled) { + View.VISIBLE + } else { + View.INVISIBLE + } + binding.cancel.setOnClickListener { + if (enabled) { + finish() + } + } + } + + private fun setTextListeners() { + for (i in passCodeEditTexts.indices) { + val editText = passCodeEditTexts[i] + val isLast = (i == 3) + + editText?.addTextChangedListener(PassCodeDigitTextWatcher(i, isLast)) + + if (i > 0) { + setOnKeyListener(i) + } + + editText?.onFocusChangeListener = View.OnFocusChangeListener { _: View?, _: Boolean -> + onPassCodeEditTextFocusChange(i) + } + } + } + + private fun onPassCodeEditTextFocusChange(passCodeIndex: Int) { + for (i in 0 until passCodeIndex) { + if (TextUtils.isEmpty(passCodeEditTexts[i]?.text)) { + passCodeEditTexts[i]?.requestFocus() + break + } + } + } + + private fun setOnKeyListener(passCodeIndex: Int) { + passCodeEditTexts[passCodeIndex]?.setOnKeyListener { _: View?, keyCode: Int, _: KeyEvent? -> + if (keyCode == KeyEvent.KEYCODE_DEL && changed) { + passCodeEditTexts[passCodeIndex - 1]?.requestFocus() + + if (!confirmingPassCode) { + passCodeDigits?.set(passCodeIndex - 1, "") + } + + passCodeEditTexts[passCodeIndex - 1]?.setText("") + + changed = false + } else if (!changed) { + changed = true + } + false + } + } + + /** + * Processes the pass code entered by the user just after the last digit was in. + * + * + * Takes into account the action requested to the activity, the currently saved pass code and the previously typed + * pass code, if any. + */ + private fun processFullPassCode() { + if (ACTION_CHECK == intent.action) { + if (checkPassCode()) { + preferences?.resetPinWrongAttempts() + + // / pass code accepted in request, user is allowed to access the app + passCodeManager?.updateLockTimestamp() + hideSoftKeyboard() + finish() + } else { + preferences?.increasePinWrongAttempts() + showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE) + } + } else if (ACTION_CHECK_WITH_RESULT == intent.action) { + if (checkPassCode()) { + passCodeManager?.updateLockTimestamp() + + val resultIntent = Intent() + resultIntent.putExtra(KEY_CHECK_RESULT, true) + setResult(RESULT_OK, resultIntent) + hideSoftKeyboard() + finish() + } else { + showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE) + } + } else if (ACTION_REQUEST_WITH_RESULT == intent.action) { + // / enabling pass code + if (!confirmingPassCode) { + requestPassCodeConfirmation() + } else if (confirmPassCode()) { + // / confirmed: user typed the same pass code twice + savePassCodeAndExit() + } else { + showErrorAndRestart( + R.string.pass_code_mismatch, + R.string.pass_code_configure_your_pass_code, + View.VISIBLE + ) + } + } + } + + private fun hideSoftKeyboard() { + currentFocus?.let { + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow( + it.windowToken, + 0 + ) + } + } + + private fun showErrorAndRestart(errorMessage: Int, headerMessage: Int, explanationVisibility: Int) { + passCodeDigits?.let { Arrays.fill(it, null) } + + Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show() + binding.header.setText(headerMessage) // TODO check if really needed + binding.explanation.visibility = explanationVisibility // TODO check if really needed + clearBoxes() + showDelay() + } + + /** + * Ask to the user for retyping the pass code just entered before saving it as the current pass code. + */ + private fun requestPassCodeConfirmation() { + clearBoxes() + binding.header.setText(R.string.pass_code_reenter_your_pass_code) + binding.explanation.visibility = View.INVISIBLE + confirmingPassCode = true + } + + private fun checkPassCode(): Boolean { + val savedPassCodeDigits = preferences?.passCode + return passCodeDigits?.zip(savedPassCodeDigits.orEmpty()) { input, saved -> + input != null && input == saved + }?.all { it } ?: false + } + + private fun confirmPassCode(): Boolean { + return passCodeEditTexts.indices.all { i -> + passCodeEditTexts[i]?.text.toString() == passCodeDigits!![i] + } + } + + private fun clearBoxes() { + passCodeEditTexts.forEach { it?.text?.clear() } + passCodeEditTexts.firstOrNull()?.requestFocus() + } + + /** + * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while preventing than + * ACTION_CHECK may be worked around. + * + * @param keyCode Key code of the key that triggered the down event. + * @param event Event triggered. + * @return 'True' when the key event was processed by this method. + */ + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK && event.repeatCount == 0) { + if (ACTION_CHECK == intent.action) { + moveTaskToBack(true) + finishAndRemoveTask() + } else if (ACTION_REQUEST_WITH_RESULT == intent.action || ACTION_CHECK_WITH_RESULT == intent.action) { + finish() + } // else, do nothing, but report that the key was consumed to stay alive + return true + } + return super.onKeyDown(keyCode, event) + } + + private fun savePassCodeAndExit() { + val resultIntent = Intent() + resultIntent.putExtra( + KEY_PASSCODE, + passCodeDigits!![0] + passCodeDigits!![1] + passCodeDigits!![2] + passCodeDigits!![3] + ) + setResult(RESULT_OK, resultIntent) + passCodeManager?.updateLockTimestamp() + finish() + } + + private fun showDelay() { + val delay = preferences?.pinBruteForceDelay() ?: 0 + + if (delay <= 0) { + return + } + + binding.explanation.setText(R.string.brute_force_delay) + binding.explanation.visibility = View.VISIBLE + binding.txt0.isEnabled = false + binding.txt1.isEnabled = false + binding.txt2.isEnabled = false + binding.txt3.isEnabled = false + + Thread(object : Runnable { + override fun run() { + try { + Thread.sleep(delay * 1000L) + + runOnUiThread { + binding.explanation.visibility = View.INVISIBLE + binding.txt0.isEnabled = true + binding.txt1.isEnabled = true + binding.txt2.isEnabled = true + binding.txt3.isEnabled = true + } + } catch (e: InterruptedException) { + Log_OC.e(this, "Could not delay password input prompt") + } + } + }).start() + } + + public override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putBoolean(KEY_CONFIRMING_PASSCODE, confirmingPassCode) + outState.putStringArray(KEY_PASSCODE_DIGITS, passCodeDigits) + } + + private inner class PassCodeDigitTextWatcher(index: Int, lastOne: Boolean) : TextWatcher { + private var mIndex = -1 + private val mLastOne: Boolean + + init { + mIndex = index + mLastOne = lastOne + + require(mIndex >= 0) { + "Invalid index in " + PassCodeDigitTextWatcher::class.java.simpleName + + " constructor" + } + } + + private operator fun next(): Int { + return if (mLastOne) 0 else mIndex + 1 + } + + /** + * Performs several actions when the user types a digit in an input field: - saves the input digit to the state + * of the activity; this will allow retyping the pass code to confirm it. - moves the focus automatically to the + * next field - for the last field, triggers the processing of the full pass code + * + * @param s Changed text + */ + override fun afterTextChanged(s: Editable) { + if (s.isNotEmpty()) { + if (!confirmingPassCode) { + val passCodeText = passCodeEditTexts[mIndex]?.text + + if (passCodeText != null) { + passCodeDigits!![mIndex] = passCodeText.toString() + } + } + + if (mLastOne) { + processFullPassCode() + } else { + passCodeEditTexts[next()]?.requestFocus() + } + } else { + Log_OC.d(TAG, "Text box $mIndex was cleaned") + } + } + + @Suppress("EmptyFunctionBlock") + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + } + + @Suppress("EmptyFunctionBlock") + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java b/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java deleted file mode 100644 index 495445c6a0..0000000000 --- a/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Tobias Kaminsky - * @author Chris Narkiewicz - * - * Copyright (C) 2018 Tobias Kaminsky - * Copyright (C) 2018 Nextcloud GmbH. - * Copyright (C) 2019 Chris Narkiewicz - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.ui.activity; - -import android.content.Intent; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.webkit.JavascriptInterface; - -import com.nextcloud.client.account.CurrentAccountProvider; -import com.nextcloud.client.account.User; -import com.nextcloud.client.network.ClientFactory; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.lib.common.OwnCloudAccount; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.operations.RichDocumentsCreateAssetOperation; -import com.owncloud.android.ui.asynctasks.PrintAsyncTask; -import com.owncloud.android.ui.asynctasks.RichDocumentsLoadUrlTask; -import com.owncloud.android.ui.fragment.OCFileListFragment; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.FileStorageUtils; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.File; -import java.lang.ref.WeakReference; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -/** - * Opens document for editing via Richdocuments app in a web view - */ -public class RichDocumentsEditorWebView extends EditorWebView { - private static final int REQUEST_REMOTE_FILE = 100; - private static final String URL = "URL"; - private static final String HYPERLINK = "Url"; - private static final String TYPE = "Type"; - private static final String PRINT = "print"; - private static final String SLIDESHOW = "slideshow"; - private static final String NEW_NAME = "NewName"; - - @Inject - protected CurrentAccountProvider currentAccountProvider; - - @Inject - protected ClientFactory clientFactory; - - @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT_INTERFACE") - @Override - protected void postOnCreate() { - super.postOnCreate(); - - getWebView().addJavascriptInterface(new RichDocumentsMobileInterface(), "RichDocumentsMobileInterface"); - - // load url in background - loadUrl(getIntent().getStringExtra(EXTRA_URL)); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - } - - private void openFileChooser() { - Intent action = new Intent(this, FilePickerActivity.class); - action.putExtra(OCFileListFragment.ARG_MIMETYPE, "image/"); - startActivityForResult(action, REQUEST_REMOTE_FILE); - } - - @Override - protected void handleActivityResult(int requestCode, int resultCode, Intent data) { - switch (requestCode) { - case REQUEST_REMOTE_FILE: - handleRemoteFile(data); - break; - - default: - // unexpected, do nothing - break; - } - - super.handleActivityResult(requestCode, resultCode, data); - } - - private void handleRemoteFile(Intent data) { - OCFile file = data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES); - - new Thread(() -> { - User user = currentAccountProvider.getUser(); - RichDocumentsCreateAssetOperation operation = new RichDocumentsCreateAssetOperation(file.getRemotePath()); - RemoteOperationResult result = operation.execute(user, this); - - if (result.isSuccess()) { - String asset = (String) result.getSingleData(); - - runOnUiThread(() -> getWebView().evaluateJavascript("OCA.RichDocuments.documentsMain.postAsset('" + - file.getFileName() + "', '" + asset + "');", null)); - } else { - runOnUiThread(() -> DisplayUtils.showSnackMessage(this, "Inserting image failed!")); - } - }).start(); - } - - @Override - protected void onSaveInstanceState(@NonNull Bundle outState) { - outState.putString(EXTRA_URL, url); - super.onSaveInstanceState(outState); - } - - @Override - public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - url = savedInstanceState.getString(EXTRA_URL); - super.onRestoreInstanceState(savedInstanceState); - } - - @Override - protected void onResume() { - super.onResume(); - - getWebView().evaluateJavascript("if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined') " + - "{ OCA.RichDocuments.documentsMain.postGrabFocus(); }", - null); - } - - private void printFile(Uri url) { - OwnCloudAccount account = accountManager.getCurrentOwnCloudAccount(); - - if (account == null) { - DisplayUtils.showSnackMessage(getWebView(), getString(R.string.failed_to_print)); - return; - } - - File targetFile = new File(FileStorageUtils.getTemporalPath(account.getName()) + "/print.pdf"); - - new PrintAsyncTask(targetFile, url.toString(), new WeakReference<>(this)).execute(); - } - - @Override - public void loadUrl(String url) { - if (TextUtils.isEmpty(url)) { - new RichDocumentsLoadUrlTask(this, getUser().get(), getFile()).execute(); - } else { - super.loadUrl(url); - } - } - - private void showSlideShow(Uri url) { - Intent intent = new Intent(this, ExternalSiteWebView.class); - intent.putExtra(ExternalSiteWebView.EXTRA_URL, url.toString()); - intent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false); - intent.putExtra(ExternalSiteWebView.EXTRA_SHOW_TOOLBAR, false); - startActivity(intent); - } - - private class RichDocumentsMobileInterface extends MobileInterface { - @JavascriptInterface - public void insertGraphic() { - openFileChooser(); - } - - @JavascriptInterface - public void documentLoaded() { - runOnUiThread(RichDocumentsEditorWebView.this::hideLoading); - } - - @JavascriptInterface - public void downloadAs(String json) { - try { - JSONObject downloadJson = new JSONObject(json); - - Uri url = Uri.parse(downloadJson.getString(URL)); - - switch (downloadJson.getString(TYPE)) { - case PRINT: - printFile(url); - break; - - case SLIDESHOW: - showSlideShow(url); - break; - - default: - downloadFile(url); - break; - } - } catch (JSONException e) { - Log_OC.e(this, "Failed to parse download json message: " + e); - } - } - - @JavascriptInterface - public void fileRename(String renameString) { - // when shared file is renamed in another instance, we will get notified about it - // need to change filename for sharing - try { - JSONObject renameJson = new JSONObject(renameString); - String newName = renameJson.getString(NEW_NAME); - getFile().setFileName(newName); - } catch (JSONException e) { - Log_OC.e(this, "Failed to parse rename json message: " + e); - } - } - - @JavascriptInterface - public void paste() { - // Javascript cannot do this by itself, so help out. - getWebView().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE)); - getWebView().dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE)); - } - - @JavascriptInterface - public void hyperlink(String hyperlink) { - try { - String url = new JSONObject(hyperlink).getString(HYPERLINK); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(url)); - startActivity(intent); - } catch (JSONException e) { - Log_OC.e(this, "Failed to parse download json message: " + e); - } - } - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt b/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt new file mode 100644 index 0000000000..c2697bf7cd --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/activity/RichDocumentsEditorWebView.kt @@ -0,0 +1,244 @@ +/* + * Nextcloud Android client application + * + * @author Tobias Kaminsky + * @author Chris Narkiewicz + * + * Copyright (C) 2018 Tobias Kaminsky + * Copyright (C) 2018 Nextcloud GmbH. + * Copyright (C) 2019 Chris Narkiewicz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.ui.activity + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import android.view.KeyEvent +import android.webkit.JavascriptInterface +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.client.network.ClientFactory +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.operations.RichDocumentsCreateAssetOperation +import com.owncloud.android.ui.asynctasks.PrintAsyncTask +import com.owncloud.android.ui.asynctasks.RichDocumentsLoadUrlTask +import com.owncloud.android.ui.fragment.OCFileListFragment +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.FileStorageUtils +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.lang.ref.WeakReference +import javax.inject.Inject + +/** + * Opens document for editing via Richdocuments app in a web view + */ +class RichDocumentsEditorWebView : EditorWebView() { + @JvmField + @Inject + var currentAccountProvider: CurrentAccountProvider? = null + + @JvmField + @Inject + var clientFactory: ClientFactory? = null + + private var activityResult: ActivityResultLauncher? = null + + @SuppressFBWarnings("ANDROID_WEB_VIEW_JAVASCRIPT_INTERFACE") + override fun postOnCreate() { + super.postOnCreate() + + webView.addJavascriptInterface(RichDocumentsMobileInterface(), "RichDocumentsMobileInterface") + + intent.getStringExtra(EXTRA_URL)?.let { + loadUrl(it) + } + + registerActivityResult() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + } + + private fun openFileChooser() { + val action = Intent(this, FilePickerActivity::class.java) + action.putExtra(OCFileListFragment.ARG_MIMETYPE, "image/") + activityResult?.launch(action) + } + + private fun registerActivityResult() { + activityResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (RESULT_OK == result.resultCode) { + result.data?.let { + handleRemoteFile(it) + } + } + } + } + + private fun handleRemoteFile(data: Intent) { + val file = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES, OCFile::class.java) + } else { + @Suppress("DEPRECATION") + data.getParcelableExtra(FolderPickerActivity.EXTRA_FILES) + } + + Thread { + val user = currentAccountProvider?.user + val operation = RichDocumentsCreateAssetOperation(file?.remotePath) + val result = operation.execute(user, this) + if (result.isSuccess) { + val asset = result.singleData as String + runOnUiThread { + webView.evaluateJavascript( + "OCA.RichDocuments.documentsMain.postAsset('" + + file?.fileName + "', '" + asset + "');", + null + ) + } + } else { + runOnUiThread { DisplayUtils.showSnackMessage(this, "Inserting image failed!") } + } + }.start() + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putString(EXTRA_URL, url) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + url = savedInstanceState.getString(EXTRA_URL) + super.onRestoreInstanceState(savedInstanceState) + } + + override fun onResume() { + super.onResume() + webView.evaluateJavascript( + "if (typeof OCA.RichDocuments.documentsMain.postGrabFocus !== 'undefined') " + + "{ OCA.RichDocuments.documentsMain.postGrabFocus(); }", + null + ) + } + + private fun printFile(url: Uri) { + val account = accountManager.currentOwnCloudAccount + if (account == null) { + DisplayUtils.showSnackMessage(webView, getString(R.string.failed_to_print)) + return + } + val targetFile = File(FileStorageUtils.getTemporalPath(account.name) + "/print.pdf") + PrintAsyncTask(targetFile, url.toString(), WeakReference(this)).execute() + } + + public override fun loadUrl(url: String) { + if (TextUtils.isEmpty(url)) { + RichDocumentsLoadUrlTask(this, user.get(), file).execute() + } else { + super.loadUrl(url) + } + } + + private fun showSlideShow(url: Uri) { + val intent = Intent(this, ExternalSiteWebView::class.java) + intent.putExtra(EXTRA_URL, url.toString()) + intent.putExtra(EXTRA_SHOW_SIDEBAR, false) + intent.putExtra(EXTRA_SHOW_TOOLBAR, false) + startActivity(intent) + } + + private inner class RichDocumentsMobileInterface : MobileInterface() { + @JavascriptInterface + fun insertGraphic() { + openFileChooser() + } + + @JavascriptInterface + fun documentLoaded() { + runOnUiThread { hideLoading() } + } + + @JavascriptInterface + fun downloadAs(json: String?) { + try { + json ?: return + val downloadJson = JSONObject(json) + val url = Uri.parse(downloadJson.getString(URL)) + when (downloadJson.getString(TYPE)) { + PRINT -> printFile(url) + SLIDESHOW -> showSlideShow(url) + else -> downloadFile(url) + } + } catch (e: JSONException) { + Log_OC.e(this, "Failed to parse download json message: $e") + } + } + + @JavascriptInterface + fun fileRename(renameString: String?) { + // when shared file is renamed in another instance, we will get notified about it + // need to change filename for sharing + try { + renameString ?: return + val renameJson = JSONObject(renameString) + val newName = renameJson.getString(NEW_NAME) + file.fileName = newName + } catch (e: JSONException) { + Log_OC.e(this, "Failed to parse rename json message: $e") + } + } + + @JavascriptInterface + fun paste() { + // Javascript cannot do this by itself, so help out. + webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_PASTE)) + webView.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_PASTE)) + } + + @JavascriptInterface + fun hyperlink(hyperlink: String?) { + try { + hyperlink ?: return + val url = JSONObject(hyperlink).getString(HYPERLINK) + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(url) + startActivity(intent) + } catch (e: JSONException) { + Log_OC.e(this, "Failed to parse download json message: $e") + } + } + } + + companion object { + private const val URL = "URL" + private const val HYPERLINK = "Url" + private const val TYPE = "Type" + private const val PRINT = "print" + private const val SLIDESHOW = "slideshow" + private const val NEW_NAME = "NewName" + } +} diff --git a/app/src/main/res/layout/passcodelock.xml b/app/src/main/res/layout/passcodelock.xml index 81004efa37..7610bf74a2 100644 --- a/app/src/main/res/layout/passcodelock.xml +++ b/app/src/main/res/layout/passcodelock.xml @@ -17,36 +17,36 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . --> - - + android:padding="@dimen/standard_padding"> - - + - + + diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 2e77e1d75e..a68e13ba8a 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -18,6 +18,7 @@ Másolás Új mappa Áthelyezés + Áthelyezés vagy Másolás Megnyitás ezzel Keresés Részletek @@ -324,6 +325,7 @@ Törlés Hiba a fájl tevékenységeinek lekérésekor A részletek betöltése sikertelen + Letöltés \u0020 Fájl Megtartás Töltsön fel új tartalmat vagy szinkronizáljon az eszközeivel @@ -962,7 +964,9 @@ A Nextcloud itt érhető el: https://nextcloud.com Egy pillanat… Tárolt hitelesítő adatok ellenőrzése Fájl másolása a privát tárolóról + A bejelentkezéshez frissítse az Android System WebView alkalmazást Frissítés + Frissítse az Android System WebView-t Újdonságok kép Kihagyás Új itt: %1$s diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 9c66b218e5..8ed39ee928 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -18,6 +18,7 @@ Копіювати Новий каталог Перемістити + Перемістити або копіювати Відкрити за допомогою Пошук Деталі @@ -322,6 +323,7 @@ Вилучити Помилка з отриманням дії для файлу Не вдалося завантажити подробиці + Звантаження \u0020 Файл Зберегти Додати дані або синхронізувати з вашими пристроями. @@ -574,6 +576,7 @@ Налаштування синхронізації календаря та контактів Про програму Деталі + Розробка Основне Більше Щоденне створення резервних копій календарів та контактів @@ -934,7 +937,9 @@ Зачекайте трохи… Перевірка збережених даних авторизації Копіювання файлу з приватного сховища + Оновіть застосунок Android System WebView для отримання доступу до входу Оновлення + Оновити Android System WebView Зображення про нові функції Пропустити Нове у %1$s