PassCodeActivity.java 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /*
  2. * ownCloud Android client application
  3. *
  4. * @author Bartek Przybylski
  5. * @author masensio
  6. * @author David A. Velasco
  7. * Copyright (C) 2011 Bartek Przybylski
  8. * Copyright (C) 2015 ownCloud Inc.
  9. * Copyright (C) 2020 Kwon Yuna <yunaghgh@naver.com>
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU General Public License version 2,
  13. * as published by the Free Software Foundation.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. package com.owncloud.android.ui.activity;
  25. import android.content.Intent;
  26. import android.os.Bundle;
  27. import android.os.SystemClock;
  28. import android.text.Editable;
  29. import android.text.TextUtils;
  30. import android.text.TextWatcher;
  31. import android.view.KeyEvent;
  32. import android.view.View;
  33. import android.view.View.OnClickListener;
  34. import android.view.Window;
  35. import android.view.inputmethod.InputMethodManager;
  36. import android.widget.EditText;
  37. import com.google.android.material.snackbar.Snackbar;
  38. import com.nextcloud.client.di.Injectable;
  39. import com.nextcloud.client.preferences.AppPreferences;
  40. import com.nextcloud.client.preferences.AppPreferencesImpl;
  41. import com.owncloud.android.R;
  42. import com.owncloud.android.databinding.PasscodelockBinding;
  43. import com.owncloud.android.lib.common.utils.Log_OC;
  44. import com.owncloud.android.utils.theme.ThemeButtonUtils;
  45. import com.owncloud.android.utils.theme.ThemeColorUtils;
  46. import com.owncloud.android.utils.theme.ThemeTextInputUtils;
  47. import java.util.Arrays;
  48. import javax.inject.Inject;
  49. import androidx.annotation.NonNull;
  50. import androidx.annotation.VisibleForTesting;
  51. import androidx.appcompat.app.AppCompatActivity;
  52. public class PassCodeActivity extends AppCompatActivity implements Injectable {
  53. private static final String TAG = PassCodeActivity.class.getSimpleName();
  54. private static final String KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS";
  55. private static final String KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE";
  56. public final static String ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT";
  57. public final static String ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT";
  58. public final static String ACTION_CHECK = "ACTION_CHECK";
  59. public final static String KEY_PASSCODE = "KEY_PASSCODE";
  60. public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT";
  61. public final static String PREFERENCE_PASSCODE_D = "PrefPinCode";
  62. public final static String PREFERENCE_PASSCODE_D1 = "PrefPinCode1";
  63. public final static String PREFERENCE_PASSCODE_D2 = "PrefPinCode2";
  64. public final static String PREFERENCE_PASSCODE_D3 = "PrefPinCode3";
  65. public final static String PREFERENCE_PASSCODE_D4 = "PrefPinCode4";
  66. @Inject AppPreferences preferences;
  67. private PasscodelockBinding binding;
  68. private final EditText[] passCodeEditTexts = new EditText[4];
  69. private String [] passCodeDigits = {"","","",""};
  70. private boolean confirmingPassCode;
  71. private boolean changed = true; // to control that only one blocks jump
  72. /**
  73. * Initializes the activity.
  74. *
  75. * An intent with a valid ACTION is expected; if none is found, an
  76. * {@link IllegalArgumentException} will be thrown.
  77. *
  78. * @param savedInstanceState Previously saved state - irrelevant in this case
  79. */
  80. protected void onCreate(Bundle savedInstanceState) {
  81. super.onCreate(savedInstanceState);
  82. binding = PasscodelockBinding.inflate(getLayoutInflater());
  83. setContentView(binding.getRoot());
  84. int elementColor = ThemeColorUtils.primaryColor(this, true);
  85. ThemeButtonUtils.themeBorderlessButton(ThemeColorUtils.primaryColor(this, true), binding.cancel);
  86. passCodeEditTexts[0] = binding.txt0;
  87. ThemeTextInputUtils.colorEditText(passCodeEditTexts[0], elementColor);
  88. ThemeTextInputUtils.themeEditText(this, passCodeEditTexts[0], false);
  89. passCodeEditTexts[0].requestFocus();
  90. passCodeEditTexts[1] = binding.txt1;
  91. ThemeTextInputUtils.colorEditText(passCodeEditTexts[1], elementColor);
  92. ThemeTextInputUtils.themeEditText(this, passCodeEditTexts[1], false);
  93. passCodeEditTexts[2] = binding.txt2;
  94. ThemeTextInputUtils.colorEditText(passCodeEditTexts[2], elementColor);
  95. ThemeTextInputUtils.themeEditText(this, passCodeEditTexts[2], false);
  96. passCodeEditTexts[3] = binding.txt3;
  97. ThemeTextInputUtils.colorEditText(passCodeEditTexts[3], elementColor);
  98. ThemeTextInputUtils.themeEditText(this, passCodeEditTexts[3], false);
  99. Window window = getWindow();
  100. if (window != null) {
  101. window.setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
  102. }
  103. if (ACTION_CHECK.equals(getIntent().getAction())) {
  104. /// this is a pass code request; the user has to input the right value
  105. binding.header.setText(R.string.pass_code_enter_pass_code);
  106. binding.explanation.setVisibility(View.INVISIBLE);
  107. setCancelButtonEnabled(false); // no option to cancel
  108. showDelay();
  109. } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
  110. if (savedInstanceState != null) {
  111. confirmingPassCode = savedInstanceState.getBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE);
  112. passCodeDigits = savedInstanceState.getStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS);
  113. }
  114. if(confirmingPassCode){
  115. // the app was in the passcodeconfirmation
  116. requestPassCodeConfirmation();
  117. }else{
  118. // pass code preference has just been activated in SettingsActivity;
  119. // will receive and confirm pass code value
  120. binding.header.setText(R.string.pass_code_configure_your_pass_code);
  121. binding.explanation.setVisibility(View.VISIBLE);
  122. setCancelButtonEnabled(true);
  123. }
  124. } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
  125. // pass code preference has just been disabled in SettingsActivity;
  126. // will confirm user knows pass code, then remove it
  127. binding.header.setText(R.string.pass_code_remove_your_pass_code);
  128. binding.explanation.setVisibility(View.INVISIBLE);
  129. setCancelButtonEnabled(true);
  130. } else {
  131. throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to " + TAG);
  132. }
  133. setTextListeners();
  134. }
  135. /**
  136. * Enables or disables the cancel button to allow the user interrupt the ACTION
  137. * requested to the activity.
  138. *
  139. * @param enabled 'True' makes the cancel button available, 'false' hides it.
  140. */
  141. protected void setCancelButtonEnabled(boolean enabled){
  142. if(enabled){
  143. binding.cancel.setVisibility(View.VISIBLE);
  144. binding.cancel.setOnClickListener(new OnClickListener() {
  145. @Override
  146. public void onClick(View v) {
  147. finish();
  148. }
  149. });
  150. } else {
  151. binding.cancel.setVisibility(View.INVISIBLE);
  152. binding.cancel.setOnClickListener(null);
  153. }
  154. }
  155. @VisibleForTesting
  156. public PasscodelockBinding getBinding() {
  157. return binding;
  158. }
  159. /**
  160. * Binds the appropriate listeners to the input boxes receiving each digit of the pass code.
  161. */
  162. protected void setTextListeners() {
  163. passCodeEditTexts[0].addTextChangedListener(new PassCodeDigitTextWatcher(0, false));
  164. passCodeEditTexts[1].addTextChangedListener(new PassCodeDigitTextWatcher(1, false));
  165. passCodeEditTexts[2].addTextChangedListener(new PassCodeDigitTextWatcher(2, false));
  166. passCodeEditTexts[3].addTextChangedListener(new PassCodeDigitTextWatcher(3, true));
  167. setOnKeyListener(1);
  168. setOnKeyListener(2);
  169. setOnKeyListener(3);
  170. passCodeEditTexts[1].setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(1));
  171. passCodeEditTexts[2].setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(2));
  172. passCodeEditTexts[3].setOnFocusChangeListener((v, hasFocus) -> onPassCodeEditTextFocusChange(3));
  173. }
  174. private void onPassCodeEditTextFocusChange(final int passCodeIndex) {
  175. for (int i = 0; i < passCodeIndex; i++) {
  176. if (TextUtils.isEmpty(passCodeEditTexts[i].getText())) {
  177. passCodeEditTexts[i].requestFocus();
  178. break;
  179. }
  180. }
  181. }
  182. private void setOnKeyListener(final int passCodeIndex) {
  183. passCodeEditTexts[passCodeIndex].setOnKeyListener((v, keyCode, event) -> {
  184. if (keyCode == KeyEvent.KEYCODE_DEL && changed) {
  185. passCodeEditTexts[passCodeIndex - 1].requestFocus();
  186. if (!confirmingPassCode) {
  187. passCodeDigits[passCodeIndex - 1] = "";
  188. }
  189. passCodeEditTexts[passCodeIndex - 1].setText("");
  190. changed = false;
  191. } else if (!changed) {
  192. changed = true;
  193. }
  194. return false;
  195. });
  196. }
  197. /**
  198. * Processes the pass code entered by the user just after the last digit was in.
  199. *
  200. * Takes into account the action requested to the activity, the currently saved pass code and
  201. * the previously typed pass code, if any.
  202. */
  203. private void processFullPassCode() {
  204. if (ACTION_CHECK.equals(getIntent().getAction())) {
  205. if (checkPassCode()) {
  206. preferences.resetPinWrongAttempts();
  207. /// pass code accepted in request, user is allowed to access the app
  208. AppPreferencesImpl.fromContext(this).setLockTimestamp(SystemClock.elapsedRealtime());
  209. hideSoftKeyboard();
  210. finish();
  211. } else {
  212. preferences.increasePinWrongAttempts();
  213. showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE);
  214. }
  215. } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
  216. if (checkPassCode()) {
  217. preferences.setLockTimestamp(SystemClock.elapsedRealtime());
  218. Intent resultIntent = new Intent();
  219. resultIntent.putExtra(KEY_CHECK_RESULT, true);
  220. setResult(RESULT_OK, resultIntent);
  221. hideSoftKeyboard();
  222. finish();
  223. } else {
  224. showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE);
  225. }
  226. } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
  227. /// enabling pass code
  228. if (!confirmingPassCode) {
  229. requestPassCodeConfirmation();
  230. } else if (confirmPassCode()) {
  231. /// confirmed: user typed the same pass code twice
  232. savePassCodeAndExit();
  233. } else {
  234. showErrorAndRestart(
  235. R.string.pass_code_mismatch, R.string.pass_code_configure_your_pass_code, View.VISIBLE
  236. );
  237. }
  238. }
  239. }
  240. private void hideSoftKeyboard() {
  241. View focusedView = getCurrentFocus();
  242. if (focusedView != null) {
  243. InputMethodManager inputMethodManager =
  244. (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
  245. inputMethodManager.hideSoftInputFromWindow(
  246. focusedView.getWindowToken(),
  247. 0
  248. );
  249. }
  250. }
  251. private void showErrorAndRestart(int errorMessage, int headerMessage,
  252. int explanationVisibility) {
  253. Arrays.fill(passCodeDigits, null);
  254. Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show();
  255. binding.header.setText(headerMessage); // TODO check if really needed
  256. binding.explanation.setVisibility(explanationVisibility); // TODO check if really needed
  257. clearBoxes();
  258. showDelay();
  259. }
  260. /**
  261. * Ask to the user for retyping the pass code just entered before saving it as the current pass
  262. * code.
  263. */
  264. protected void requestPassCodeConfirmation(){
  265. clearBoxes();
  266. binding.header.setText(R.string.pass_code_reenter_your_pass_code);
  267. binding.explanation.setVisibility(View.INVISIBLE);
  268. confirmingPassCode = true;
  269. }
  270. /**
  271. * Compares pass code entered by the user with the value currently saved in the app.
  272. *
  273. * @return 'True' if entered pass code equals to the saved one.
  274. */
  275. protected boolean checkPassCode() {
  276. String[] savedPassCodeDigits = preferences.getPassCode();
  277. boolean result = true;
  278. for (int i = 0; i < passCodeDigits.length && result; i++) {
  279. result = passCodeDigits[i] != null && passCodeDigits[i].equals(savedPassCodeDigits[i]);
  280. }
  281. return result;
  282. }
  283. /**
  284. * Compares pass code retyped by the user in the input fields with the value entered just
  285. * before.
  286. *
  287. * @return 'True' if retyped pass code equals to the entered before.
  288. */
  289. protected boolean confirmPassCode(){
  290. confirmingPassCode = false;
  291. boolean result = true;
  292. for (int i = 0; i < passCodeEditTexts.length && result; i++) {
  293. result = passCodeEditTexts[i].getText().toString().equals(passCodeDigits[i]);
  294. }
  295. return result;
  296. }
  297. /**
  298. * Sets the input fields to empty strings and puts the focus on the first one.
  299. */
  300. protected void clearBoxes(){
  301. for (EditText mPassCodeEditText : passCodeEditTexts) {
  302. mPassCodeEditText.setText("");
  303. }
  304. passCodeEditTexts[0].requestFocus();
  305. }
  306. /**
  307. * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while
  308. * preventing than ACTION_CHECK may be worked around.
  309. *
  310. * @param keyCode Key code of the key that triggered the down event.
  311. * @param event Event triggered.
  312. * @return 'True' when the key event was processed by this method.
  313. */
  314. @Override
  315. public boolean onKeyDown(int keyCode, KeyEvent event) {
  316. if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
  317. if (ACTION_CHECK.equals(getIntent().getAction())) {
  318. moveTaskToBack(true);
  319. finishAndRemoveTask();
  320. } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction()) ||
  321. ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
  322. finish();
  323. }// else, do nothing, but report that the key was consumed to stay alive
  324. return true;
  325. }
  326. return super.onKeyDown(keyCode, event);
  327. }
  328. /**
  329. * Saves the pass code input by the user as the current pass code.
  330. */
  331. protected void savePassCodeAndExit() {
  332. Intent resultIntent = new Intent();
  333. resultIntent.putExtra(KEY_PASSCODE,
  334. passCodeDigits[0] + passCodeDigits[1] + passCodeDigits[2] + passCodeDigits[3]);
  335. setResult(RESULT_OK, resultIntent);
  336. finish();
  337. }
  338. private void showDelay() {
  339. int delay = preferences.pinBruteForceDelay();
  340. if (delay > 0) {
  341. binding.explanation.setText(R.string.brute_force_delay);
  342. binding.explanation.setVisibility(View.VISIBLE);
  343. binding.txt0.setEnabled(false);
  344. binding.txt1.setEnabled(false);
  345. binding.txt2.setEnabled(false);
  346. binding.txt3.setEnabled(false);
  347. new Thread(new Runnable() {
  348. @Override
  349. public void run() {
  350. try {
  351. Thread.sleep(delay * 1000);
  352. runOnUiThread(() -> {
  353. binding.explanation.setVisibility(View.INVISIBLE);
  354. binding.txt0.setEnabled(true);
  355. binding.txt1.setEnabled(true);
  356. binding.txt2.setEnabled(true);
  357. binding.txt3.setEnabled(true);
  358. });
  359. } catch (InterruptedException e) {
  360. Log_OC.e(this, "Could not delay password input prompt");
  361. }
  362. }
  363. }).start();
  364. }
  365. }
  366. @Override
  367. public void onSaveInstanceState(@NonNull Bundle outState) {
  368. super.onSaveInstanceState(outState);
  369. outState.putBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE, confirmingPassCode);
  370. outState.putStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS, passCodeDigits);
  371. }
  372. private class PassCodeDigitTextWatcher implements TextWatcher {
  373. private int mIndex = -1;
  374. private boolean mLastOne;
  375. /**
  376. * Constructor
  377. *
  378. * @param index Position in the pass code of the input field that will be bound to
  379. * this watcher.
  380. * @param lastOne 'True' means that watcher corresponds to the last position of the
  381. * pass code.
  382. */
  383. PassCodeDigitTextWatcher(int index, boolean lastOne) {
  384. mIndex = index;
  385. mLastOne = lastOne;
  386. if (mIndex < 0) {
  387. throw new IllegalArgumentException(
  388. "Invalid index in " + PassCodeDigitTextWatcher.class.getSimpleName() +
  389. " constructor"
  390. );
  391. }
  392. }
  393. private int next() {
  394. return mLastOne ? 0 : mIndex + 1;
  395. }
  396. /**
  397. * Performs several actions when the user types a digit in an input field:
  398. * - saves the input digit to the state of the activity; this will allow retyping the
  399. * pass code to confirm it.
  400. * - moves the focus automatically to the next field
  401. * - for the last field, triggers the processing of the full pass code
  402. *
  403. * @param s Changed text
  404. */
  405. @Override
  406. public void afterTextChanged(Editable s) {
  407. if (s.length() > 0) {
  408. if (!confirmingPassCode) {
  409. passCodeDigits[mIndex] = passCodeEditTexts[mIndex].getText().toString();
  410. }
  411. passCodeEditTexts[next()].requestFocus();
  412. if (mLastOne) {
  413. processFullPassCode();
  414. }
  415. } else {
  416. Log_OC.d(TAG, "Text box " + mIndex + " was cleaned");
  417. }
  418. }
  419. @Override
  420. public void beforeTextChanged(CharSequence s, int start, int count, int after) {
  421. // nothing to do
  422. }
  423. @Override
  424. public void onTextChanged(CharSequence s, int start, int before, int count) {
  425. // nothing to do
  426. }
  427. }
  428. }