PassCodeActivity.java 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554
  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.content.SharedPreferences;
  26. import android.graphics.PorterDuff;
  27. import android.os.Bundle;
  28. import android.preference.PreferenceManager;
  29. import android.support.design.widget.Snackbar;
  30. import android.support.v7.app.AppCompatActivity;
  31. import android.text.Editable;
  32. import android.text.TextWatcher;
  33. import android.view.KeyEvent;
  34. import android.view.View;
  35. import android.view.View.OnClickListener;
  36. import android.view.inputmethod.InputMethodManager;
  37. import android.widget.Button;
  38. import android.widget.EditText;
  39. import android.widget.TextView;
  40. import com.owncloud.android.R;
  41. import com.owncloud.android.lib.common.utils.Log_OC;
  42. import com.owncloud.android.utils.AnalyticsUtils;
  43. import com.owncloud.android.utils.ThemeUtils;
  44. import java.util.Arrays;
  45. public class PassCodeActivity extends AppCompatActivity {
  46. private static final String TAG = PassCodeActivity.class.getSimpleName();
  47. private static final String SCREEN_NAME = "Passcode lock";
  48. public final static String ACTION_REQUEST_WITH_RESULT = "ACTION_REQUEST_WITH_RESULT";
  49. public final static String ACTION_CHECK_WITH_RESULT = "ACTION_CHECK_WITH_RESULT";
  50. public final static String ACTION_CHECK = "ACTION_CHECK";
  51. public final static String KEY_PASSCODE = "KEY_PASSCODE";
  52. public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT";
  53. // NOTE: PREFERENCE_SET_PASSCODE must have the same value as preferences.xml-->android:key for passcode preference
  54. public final static String PREFERENCE_SET_PASSCODE = "set_pincode";
  55. public final static String PREFERENCE_PASSCODE_D = "PrefPinCode";
  56. public final static String PREFERENCE_PASSCODE_D1 = "PrefPinCode1";
  57. public final static String PREFERENCE_PASSCODE_D2 = "PrefPinCode2";
  58. public final static String PREFERENCE_PASSCODE_D3 = "PrefPinCode3";
  59. public final static String PREFERENCE_PASSCODE_D4 = "PrefPinCode4";
  60. private Button mBCancel;
  61. private TextView mPassCodeHdr;
  62. private TextView mPassCodeHdrExplanation;
  63. private EditText[] mPassCodeEditTexts = new EditText[4];
  64. private String [] mPassCodeDigits = {"","","",""};
  65. private static final String KEY_PASSCODE_DIGITS = "PASSCODE_DIGITS";
  66. private boolean mConfirmingPassCode = false;
  67. private static final String KEY_CONFIRMING_PASSCODE = "CONFIRMING_PASSCODE";
  68. private boolean mBChange = true; // to control that only one blocks jump
  69. /**
  70. * Initializes the activity.
  71. *
  72. * An intent with a valid ACTION is expected; if none is found, an
  73. * {@link IllegalArgumentException} will be thrown.
  74. *
  75. * @param savedInstanceState Previously saved state - irrelevant in this case
  76. */
  77. protected void onCreate(Bundle savedInstanceState) {
  78. super.onCreate(savedInstanceState);
  79. setContentView(R.layout.passcodelock);
  80. mBCancel = (Button) findViewById(R.id.cancel);
  81. mBCancel.getBackground().setColorFilter(ThemeUtils.primaryColor(), PorterDuff.Mode.SRC_ATOP);
  82. mPassCodeHdr = (TextView) findViewById(R.id.header);
  83. mPassCodeHdrExplanation = (TextView) findViewById(R.id.explanation);
  84. mPassCodeEditTexts[0] = (EditText) findViewById(R.id.txt0);
  85. mPassCodeEditTexts[0].setTextColor(ThemeUtils.primaryColor());
  86. mPassCodeEditTexts[0].getBackground().setColorFilter(ThemeUtils.primaryColor(), PorterDuff.Mode.SRC_ATOP);
  87. mPassCodeEditTexts[0].requestFocus();
  88. getWindow().setSoftInputMode(
  89. android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
  90. mPassCodeEditTexts[1] = (EditText) findViewById(R.id.txt1);
  91. mPassCodeEditTexts[1].setTextColor(ThemeUtils.primaryColor());
  92. mPassCodeEditTexts[1].getBackground().setColorFilter(ThemeUtils.primaryColor(), PorterDuff.Mode.SRC_ATOP);
  93. mPassCodeEditTexts[2] = (EditText) findViewById(R.id.txt2);
  94. mPassCodeEditTexts[2].setTextColor(ThemeUtils.primaryColor());
  95. mPassCodeEditTexts[2].getBackground().setColorFilter(ThemeUtils.primaryColor(), PorterDuff.Mode.SRC_ATOP);
  96. mPassCodeEditTexts[3] = (EditText) findViewById(R.id.txt3);
  97. mPassCodeEditTexts[3].setTextColor(ThemeUtils.primaryColor());
  98. mPassCodeEditTexts[3].getBackground().setColorFilter(ThemeUtils.primaryColor(), PorterDuff.Mode.SRC_ATOP);
  99. if (ACTION_CHECK.equals(getIntent().getAction())) {
  100. /// this is a pass code request; the user has to input the right value
  101. mPassCodeHdr.setText(R.string.pass_code_enter_pass_code);
  102. mPassCodeHdrExplanation.setVisibility(View.INVISIBLE);
  103. setCancelButtonEnabled(false); // no option to cancel
  104. } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
  105. if (savedInstanceState != null) {
  106. mConfirmingPassCode = savedInstanceState.getBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE);
  107. mPassCodeDigits = savedInstanceState.getStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS);
  108. }
  109. if(mConfirmingPassCode){
  110. //the app was in the passcodeconfirmation
  111. requestPassCodeConfirmation();
  112. }else{
  113. /// pass code preference has just been activated in Preferences;
  114. // will receive and confirm pass code value
  115. mPassCodeHdr.setText(R.string.pass_code_configure_your_pass_code);
  116. //mPassCodeHdr.setText(R.string.pass_code_enter_pass_code);
  117. // TODO choose a header, check iOS
  118. mPassCodeHdrExplanation.setVisibility(View.VISIBLE);
  119. setCancelButtonEnabled(true);
  120. }
  121. } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
  122. /// pass code preference has just been disabled in Preferences;
  123. // will confirm user knows pass code, then remove it
  124. mPassCodeHdr.setText(R.string.pass_code_remove_your_pass_code);
  125. mPassCodeHdrExplanation.setVisibility(View.INVISIBLE);
  126. setCancelButtonEnabled(true);
  127. } else {
  128. throw new IllegalArgumentException("A valid ACTION is needed in the Intent passed to "
  129. + TAG);
  130. }
  131. setTextListeners();
  132. }
  133. @Override
  134. protected void onResume() {
  135. super.onResume();
  136. AnalyticsUtils.setCurrentScreenName(this, SCREEN_NAME, TAG);
  137. }
  138. /**
  139. * Enables or disables the cancel button to allow the user interrupt the ACTION
  140. * requested to the activity.
  141. *
  142. * @param enabled 'True' makes the cancel button available, 'false' hides it.
  143. */
  144. protected void setCancelButtonEnabled(boolean enabled){
  145. if(enabled){
  146. mBCancel.setVisibility(View.VISIBLE);
  147. mBCancel.setOnClickListener(new OnClickListener() {
  148. @Override
  149. public void onClick(View v) {
  150. finish();
  151. }
  152. });
  153. } else {
  154. mBCancel.setVisibility(View.GONE);
  155. mBCancel.setVisibility(View.INVISIBLE);
  156. mBCancel.setOnClickListener(null);
  157. }
  158. }
  159. /**
  160. * Binds the appropriate listeners to the input boxes receiving each digit of the pass code.
  161. */
  162. protected void setTextListeners() {
  163. /// First input field
  164. mPassCodeEditTexts[0].addTextChangedListener(new PassCodeDigitTextWatcher(0, false));
  165. /*------------------------------------------------
  166. * SECOND BOX
  167. -------------------------------------------------*/
  168. mPassCodeEditTexts[1].addTextChangedListener(new PassCodeDigitTextWatcher(1, false));
  169. mPassCodeEditTexts[1].setOnKeyListener(new View.OnKeyListener() {
  170. @Override
  171. public boolean onKey(View v, int keyCode, KeyEvent event) {
  172. if (keyCode == KeyEvent.KEYCODE_DEL && mBChange) { // TODO WIP: event should be
  173. // used to control what's exactly happening with DEL, not any custom field...
  174. mPassCodeEditTexts[0].setText("");
  175. mPassCodeEditTexts[0].requestFocus();
  176. if (!mConfirmingPassCode) {
  177. mPassCodeDigits[0] = "";
  178. }
  179. mBChange = false;
  180. } else if (!mBChange) {
  181. mBChange = true;
  182. }
  183. return false;
  184. }
  185. });
  186. mPassCodeEditTexts[1].setOnFocusChangeListener(new View.OnFocusChangeListener() {
  187. @Override
  188. public void onFocusChange(View v, boolean hasFocus) {
  189. /// TODO WIP: should take advantage of hasFocus to reduce processing
  190. if (mPassCodeEditTexts[0].getText().toString().equals("")) { // TODO WIP validation
  191. // could be done in a global way, with a single OnFocusChangeListener for all the
  192. // input fields
  193. mPassCodeEditTexts[0].requestFocus();
  194. }
  195. }
  196. });
  197. /*------------------------------------------------
  198. * THIRD BOX
  199. -------------------------------------------------*/
  200. mPassCodeEditTexts[2].addTextChangedListener(new PassCodeDigitTextWatcher(2, false));
  201. mPassCodeEditTexts[2].setOnKeyListener(new View.OnKeyListener() {
  202. @Override
  203. public boolean onKey(View v, int keyCode, KeyEvent event) {
  204. if (keyCode == KeyEvent.KEYCODE_DEL && mBChange) {
  205. mPassCodeEditTexts[1].requestFocus();
  206. if (!mConfirmingPassCode) {
  207. mPassCodeDigits[1] = "";
  208. }
  209. mPassCodeEditTexts[1].setText("");
  210. mBChange = false;
  211. } else if (!mBChange) {
  212. mBChange = true;
  213. }
  214. return false;
  215. }
  216. });
  217. mPassCodeEditTexts[2].setOnFocusChangeListener(new View.OnFocusChangeListener() {
  218. @Override
  219. public void onFocusChange(View v, boolean hasFocus) {
  220. if (mPassCodeEditTexts[0].getText().toString().equals("")) {
  221. mPassCodeEditTexts[0].requestFocus();
  222. } else if (mPassCodeEditTexts[1].getText().toString().equals("")) {
  223. mPassCodeEditTexts[1].requestFocus();
  224. }
  225. }
  226. });
  227. /*------------------------------------------------
  228. * FOURTH BOX
  229. -------------------------------------------------*/
  230. mPassCodeEditTexts[3].addTextChangedListener(new PassCodeDigitTextWatcher(3, true));
  231. mPassCodeEditTexts[3].setOnKeyListener(new View.OnKeyListener() {
  232. @Override
  233. public boolean onKey(View v, int keyCode, KeyEvent event) {
  234. if (keyCode == KeyEvent.KEYCODE_DEL && mBChange) {
  235. mPassCodeEditTexts[2].requestFocus();
  236. if (!mConfirmingPassCode) {
  237. mPassCodeDigits[2] = "";
  238. }
  239. mPassCodeEditTexts[2].setText("");
  240. mBChange = false;
  241. } else if (!mBChange) {
  242. mBChange = true;
  243. }
  244. return false;
  245. }
  246. });
  247. mPassCodeEditTexts[3].setOnFocusChangeListener(new View.OnFocusChangeListener() {
  248. @Override
  249. public void onFocusChange(View v, boolean hasFocus) {
  250. if (mPassCodeEditTexts[0].getText().toString().equals("")) {
  251. mPassCodeEditTexts[0].requestFocus();
  252. } else if (mPassCodeEditTexts[1].getText().toString().equals("")) {
  253. mPassCodeEditTexts[1].requestFocus();
  254. } else if (mPassCodeEditTexts[2].getText().toString().equals("")) {
  255. mPassCodeEditTexts[2].requestFocus();
  256. }
  257. }
  258. });
  259. } // end setTextListener
  260. /**
  261. * Processes the pass code entered by the user just after the last digit was in.
  262. *
  263. * Takes into account the action requested to the activity, the currently saved pass code and
  264. * the previously typed pass code, if any.
  265. */
  266. private void processFullPassCode() {
  267. if (ACTION_CHECK.equals(getIntent().getAction())) {
  268. if (checkPassCode()) {
  269. /// pass code accepted in request, user is allowed to access the app
  270. hideSoftKeyboard();
  271. finish();
  272. } else {
  273. showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code,
  274. View.INVISIBLE);
  275. }
  276. } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
  277. if (checkPassCode()) {
  278. Intent resultIntent = new Intent();
  279. resultIntent.putExtra(KEY_CHECK_RESULT, true);
  280. setResult(RESULT_OK, resultIntent);
  281. hideSoftKeyboard();
  282. finish();
  283. } else {
  284. showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code,
  285. View.INVISIBLE);
  286. }
  287. } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
  288. /// enabling pass code
  289. if (!mConfirmingPassCode) {
  290. requestPassCodeConfirmation();
  291. } else if (confirmPassCode()) {
  292. /// confirmed: user typed the same pass code twice
  293. savePassCodeAndExit();
  294. } else {
  295. showErrorAndRestart(
  296. R.string.pass_code_mismatch, R.string.pass_code_configure_your_pass_code, View.VISIBLE
  297. );
  298. }
  299. }
  300. }
  301. private void hideSoftKeyboard() {
  302. View focusedView = getCurrentFocus();
  303. if (focusedView != null) {
  304. InputMethodManager inputMethodManager =
  305. (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
  306. inputMethodManager.hideSoftInputFromWindow(
  307. focusedView.getWindowToken(),
  308. 0
  309. );
  310. }
  311. }
  312. private void showErrorAndRestart(int errorMessage, int headerMessage,
  313. int explanationVisibility) {
  314. Arrays.fill(mPassCodeDigits, null);
  315. Snackbar.make(findViewById(android.R.id.content), getString(errorMessage), Snackbar.LENGTH_LONG).show();
  316. mPassCodeHdr.setText(headerMessage); // TODO check if really needed
  317. mPassCodeHdrExplanation.setVisibility(explanationVisibility); // TODO check if really needed
  318. clearBoxes();
  319. }
  320. /**
  321. * Ask to the user for retyping the pass code just entered before saving it as the current pass
  322. * code.
  323. */
  324. protected void requestPassCodeConfirmation(){
  325. clearBoxes();
  326. mPassCodeHdr.setText(R.string.pass_code_reenter_your_pass_code);
  327. mPassCodeHdrExplanation.setVisibility(View.INVISIBLE);
  328. mConfirmingPassCode = true;
  329. }
  330. /**
  331. * Compares pass code entered by the user with the value currently saved in the app.
  332. *
  333. * @return 'True' if entered pass code equals to the saved one.
  334. */
  335. protected boolean checkPassCode(){
  336. SharedPreferences appPrefs = PreferenceManager
  337. .getDefaultSharedPreferences(getApplicationContext());
  338. String savedPassCodeDigits[] = new String[4];
  339. savedPassCodeDigits[0] = appPrefs.getString(PREFERENCE_PASSCODE_D1, null);
  340. savedPassCodeDigits[1] = appPrefs.getString(PREFERENCE_PASSCODE_D2, null);
  341. savedPassCodeDigits[2] = appPrefs.getString(PREFERENCE_PASSCODE_D3, null);
  342. savedPassCodeDigits[3] = appPrefs.getString(PREFERENCE_PASSCODE_D4, null);
  343. boolean result = true;
  344. for (int i = 0; i < mPassCodeDigits.length && result; i++) {
  345. result = (mPassCodeDigits[i] != null) &&
  346. mPassCodeDigits[i].equals(savedPassCodeDigits[i]);
  347. }
  348. return result;
  349. }
  350. /**
  351. * Compares pass code retyped by the user in the input fields with the value entered just
  352. * before.
  353. *
  354. * @return 'True' if retyped pass code equals to the entered before.
  355. */
  356. protected boolean confirmPassCode(){
  357. mConfirmingPassCode = false;
  358. boolean result = true;
  359. for (int i = 0; i < mPassCodeEditTexts.length && result; i++) {
  360. result = ((mPassCodeEditTexts[i].getText().toString()).equals(mPassCodeDigits[i]));
  361. }
  362. return result;
  363. }
  364. /**
  365. * Sets the input fields to empty strings and puts the focus on the first one.
  366. */
  367. protected void clearBoxes(){
  368. for (EditText mPassCodeEditText : mPassCodeEditTexts) {
  369. mPassCodeEditText.setText("");
  370. }
  371. mPassCodeEditTexts[0].requestFocus();
  372. }
  373. /**
  374. * Overrides click on the BACK arrow to correctly cancel ACTION_ENABLE or ACTION_DISABLE, while
  375. * preventing than ACTION_CHECK may be worked around.
  376. *
  377. * @param keyCode Key code of the key that triggered the down event.
  378. * @param event Event triggered.
  379. * @return 'True' when the key event was processed by this method.
  380. */
  381. @Override
  382. public boolean onKeyDown(int keyCode, KeyEvent event){
  383. if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount()== 0){
  384. if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction()) ||
  385. ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
  386. finish();
  387. } // else, do nothing, but report that the key was consumed to stay alive
  388. return true;
  389. }
  390. return super.onKeyDown(keyCode, event);
  391. }
  392. /**
  393. * Saves the pass code input by the user as the current pass code.
  394. */
  395. protected void savePassCodeAndExit() {
  396. Intent resultIntent = new Intent();
  397. resultIntent.putExtra(KEY_PASSCODE,
  398. mPassCodeDigits[0] + mPassCodeDigits[1] + mPassCodeDigits[2] + mPassCodeDigits[3]);
  399. setResult(RESULT_OK, resultIntent);
  400. finish();
  401. }
  402. @Override
  403. public void onSaveInstanceState(Bundle outState) {
  404. super.onSaveInstanceState(outState);
  405. outState.putBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE, mConfirmingPassCode);
  406. outState.putStringArray(PassCodeActivity.KEY_PASSCODE_DIGITS, mPassCodeDigits);
  407. }
  408. private class PassCodeDigitTextWatcher implements TextWatcher {
  409. private int mIndex = -1;
  410. private boolean mLastOne = false;
  411. /**
  412. * Constructor
  413. *
  414. * @param index Position in the pass code of the input field that will be bound to
  415. * this watcher.
  416. * @param lastOne 'True' means that watcher corresponds to the last position of the
  417. * pass code.
  418. */
  419. PassCodeDigitTextWatcher(int index, boolean lastOne) {
  420. mIndex = index;
  421. mLastOne = lastOne;
  422. if (mIndex < 0) {
  423. throw new IllegalArgumentException(
  424. "Invalid index in " + PassCodeDigitTextWatcher.class.getSimpleName() +
  425. " constructor"
  426. );
  427. }
  428. }
  429. private int next() {
  430. return mLastOne ? 0 : mIndex + 1;
  431. }
  432. /**
  433. * Performs several actions when the user types a digit in an input field:
  434. * - saves the input digit to the state of the activity; this will allow retyping the
  435. * pass code to confirm it.
  436. * - moves the focus automatically to the next field
  437. * - for the last field, triggers the processing of the full pass code
  438. *
  439. * @param s Changed text
  440. */
  441. @Override
  442. public void afterTextChanged(Editable s) {
  443. if (s.length() > 0) {
  444. if (!mConfirmingPassCode) {
  445. mPassCodeDigits[mIndex] = mPassCodeEditTexts[mIndex].getText().toString();
  446. }
  447. mPassCodeEditTexts[next()].requestFocus();
  448. if (mLastOne) {
  449. processFullPassCode();
  450. }
  451. } else {
  452. Log_OC.d(TAG, "Text box " + mIndex + " was cleaned");
  453. }
  454. }
  455. @Override
  456. public void beforeTextChanged(CharSequence s, int start, int count, int after) {
  457. // nothing to do
  458. }
  459. @Override
  460. public void onTextChanged(CharSequence s, int start, int before, int count) {
  461. // nothing to do
  462. }
  463. }
  464. }