PassCodeActivity.java 18 KB

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