package org.commcare.activities;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.preference.PreferenceManager;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.StateSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.TextView;
import org.commcare.CommCareApplication;
import org.commcare.dalvik.R;
import org.commcare.interfaces.CommCareActivityUIController;
import org.commcare.models.database.SqlStorage;
import org.commcare.android.database.app.models.UserKeyRecord;
import org.commcare.android.database.global.models.ApplicationRecord;
import org.commcare.preferences.CommCarePreferences;
import org.commcare.preferences.DevSessionRestorer;
import org.commcare.utils.MediaUtil;
import org.commcare.utils.MultipleAppsUtil;
import org.commcare.views.CustomBanner;
import org.commcare.views.ManagedUi;
import org.commcare.views.ManagedUiFramework;
import org.commcare.views.PasswordShow;
import org.commcare.views.UiElement;
import org.javarosa.core.services.locale.Localization;
import java.util.ArrayList;
import java.util.Vector;
/**
* Handles login activity UI
*
* @author Aliza Stone (astone@dimagi.com)
*/
@ManagedUi(R.layout.screen_login)
public class LoginActivityUIController implements CommCareActivityUIController {
@UiElement(value = R.id.screen_login_bad_password)
private TextView errorBox;
@UiElement(value = R.id.edit_username, locale = "login.username")
private AutoCompleteTextView username;
@UiElement(value = R.id.edit_password)
private EditText passwordOrPin;
@UiElement(value = R.id.show_password)
private Button showPasswordButton;
@UiElement(R.id.screen_login_banner_pane)
private View banner;
@UiElement(value = R.id.login_button, locale = "login.button")
private Button loginButton;
@UiElement(value = R.id.restore_session_checkbox)
private CheckBox restoreSessionCheckbox;
@UiElement(R.id.app_selection_spinner)
private Spinner spinner;
@UiElement(R.id.welcome_msg)
private TextView welcomeMessage;
@UiElement(value = R.id.primed_password_message, locale = "login.primed.prompt")
private TextView loginPrimedMessage;
protected final LoginActivity activity;
private LoginMode loginMode;
private boolean manuallySwitchedToPasswordMode;
private final TextWatcher usernameTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
setStyleDefault();
checkEnteredUsernameForMatch();
}
};
private final TextWatcher passwordTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
setStyleDefault();
}
};
public LoginActivityUIController(LoginActivity activity) {
this.activity = activity;
this.loginMode = LoginMode.PASSWORD;
}
@Override
public void setupUI() {
setupUsernameEntryBox();
setLoginBoxesColorNormal();
setTextChangeListeners();
setBannerLayoutLogic();
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View arg0) {
activity.initiateLoginAttempt(isRestoreSessionChecked());
}
});
}
private void setTextChangeListeners() {
username.addTextChangedListener(usernameTextWatcher);
passwordOrPin.addTextChangedListener(passwordTextWatcher);
}
private void setupUsernameEntryBox() {
username.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS |
InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
username.setHint(Localization.get("login.username"));
}
private void setBannerLayoutLogic() {
final View activityRootView = activity.findViewById(R.id.screen_login_main);
final SharedPreferences prefs = CommCareApplication.instance().getCurrentApp().getAppPreferences();
activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int hideAll = getResources().getInteger(
R.integer.login_screen_hide_all_cuttoff);
int hideBanner = getResources().getInteger(
R.integer.login_screen_hide_banner_cuttoff);
int height = activityRootView.getHeight();
if (height < hideAll) {
banner.setVisibility(View.GONE);
} else if (height < hideBanner) {
banner.setVisibility(View.GONE);
} else {
// Override default CommCare banner if requested
String customBannerURI = prefs.getString(
CommCarePreferences.BRAND_BANNER_LOGIN, "");
if (!"".equals(customBannerURI)) {
Bitmap bitmap = MediaUtil.inflateDisplayImage(activity, customBannerURI);
if (bitmap != null) {
ImageView bannerView =
(ImageView)banner.findViewById(R.id.main_top_banner);
bannerView.setImageBitmap(bitmap);
}
}
banner.setVisibility(View.VISIBLE);
}
}
});
}
@Override
public void refreshView() {
updateBanner();
activity.restoreEnteredTextFromRotation();
// Decide whether or not to show the app selection spinner based upon # of usable apps
ArrayList<ApplicationRecord> readyApps = MultipleAppsUtil.getUsableAppRecords();
if (readyApps.size() == 1) {
// Set this app as the last selected app, for use in choosing what app to initialize
// on first startup
ApplicationRecord r = readyApps.get(0);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
prefs.edit().putString(LoginActivity.KEY_LAST_APP, r.getUniqueId()).commit();
setSingleAppUIState();
} else {
activity.populateAppSpinner(readyApps);
}
// Not using this for now, but may turn back on later
//refreshUsernamesAdapter();
// Update checkbox visibility
if (DevSessionRestorer.savedSessionPresent()) {
restoreSessionCheckbox.setVisibility(View.VISIBLE);
} else {
restoreSessionCheckbox.setVisibility(View.GONE);
}
if (activity.checkForSeatedAppChange()) {
refreshForNewApp();
} else {
checkEnteredUsernameForMatch();
}
}
protected void refreshForNewApp() {
// Remove any error content from trying to log into a different app
setStyleDefault();
final SharedPreferences prefs = CommCareApplication.instance().getCurrentApp().getAppPreferences();
String lastUser = prefs.getString(CommCarePreferences.LAST_LOGGED_IN_USER, null);
if (lastUser != null) {
// If there was a last user for this app, show it
username.setText(lastUser);
passwordOrPin.requestFocus();
} else {
// Otherwise, clear the username text so it does not show a username from a different app
username.setText("");
username.requestFocus();
}
// Since the entered username may have changed, need to re-check if we should be in PIN mode
checkEnteredUsernameForMatch();
// Clear any password text that was entered for a different app
passwordOrPin.setText("");
// Refresh the breadcrumb bar for new app name
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
activity.refreshActionBar();
}
// Refresh UI for potential new language
ManagedUiFramework.loadUiElements(activity);
// Refresh welcome msg separately bc cannot set a single locale for its UiElement
welcomeMessage.setText(Localization.get("login.welcome.multiple"));
}
private void checkEnteredUsernameForMatch() {
UserKeyRecord matchingRecord = getActiveRecordForUsername(getEnteredUsername());
if (matchingRecord != null) {
setExistingUserMode(matchingRecord);
} else {
setNewUserMode();
}
}
/**
* @return the active UKR for the given username, or null if none exists
*/
private static UserKeyRecord getActiveRecordForUsername(String username) {
SqlStorage<UserKeyRecord> existingUsers =
CommCareApplication.instance().getCurrentApp().getStorage(UserKeyRecord.class);
// Even though we don't allow multiple users with same username in a domain, there can be
// multiple UKRs for 1 user (for ex if password changes)
Vector<UserKeyRecord> matchingRecords = existingUsers.
getRecordsForValue(UserKeyRecord.META_USERNAME, username);
// However, we guarantee that there will be at most 1 record marked ACTIVE per username
for (UserKeyRecord record : matchingRecords) {
if (record.isActive()) {
return record;
}
}
return null;
}
private void setExistingUserMode(UserKeyRecord existingRecord) {
if (existingRecord.isPrimedForNextLogin()) {
// Primed login takes precedence (meaning if a record has a PIN set AND is primed for
// next login, we show primed mode rather than PIN mode)
setPrimedLoginMode();
} else if (existingRecord.hasPinSet()) {
setPinPasswordMode();
} else {
setNormalPasswordMode();
}
}
private void setNewUserMode() {
setNormalPasswordMode();
}
private void setPrimedLoginMode() {
loginMode = LoginMode.PRIMED;
loginPrimedMessage.setVisibility(View.VISIBLE);
passwordOrPin.setVisibility(View.GONE);
manuallySwitchedToPasswordMode = false;
// Switch focus to a dummy (invisible) LinearLayout so that the keyboard doesn't show
View dummyView = activity.findViewById(R.id.dummy_focusable_view);
dummyView.requestFocus();
}
protected void setNormalPasswordMode() {
loginMode = LoginMode.PASSWORD;
loginPrimedMessage.setVisibility(View.GONE);
passwordOrPin.setVisibility(View.VISIBLE);
passwordOrPin.setHint(Localization.get("login.password"));
passwordOrPin.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
new PasswordShow(showPasswordButton, passwordOrPin).setupPasswordVisibility();
manuallySwitchedToPasswordMode = false;
}
private void setPinPasswordMode() {
loginMode = LoginMode.PIN;
loginPrimedMessage.setVisibility(View.GONE);
passwordOrPin.setVisibility(View.VISIBLE);
passwordOrPin.setHint(Localization.get("login.pin.password"));
passwordOrPin.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
manuallySwitchedToPasswordMode = false;
}
protected void manualSwitchToPasswordMode() {
setNormalPasswordMode();
setStyleDefault();
setPasswordOrPin("");
manuallySwitchedToPasswordMode = true;
}
protected boolean userManuallySwitchedToPasswordMode() {
return manuallySwitchedToPasswordMode;
}
protected LoginMode getLoginMode() {
return loginMode;
}
protected void setErrorMessageUI(String message) {
setLoginBoxesColorError();
username.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.icon_user_attnneg), null, null, null);
passwordOrPin.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.icon_lock_attnneg), null, null, null);
loginButton.setBackgroundColor(getResources().getColor(R.color.cc_attention_negative_bg));
loginButton.setTextColor(getResources().getColor(R.color.cc_attention_negative_text));
errorBox.setVisibility(View.VISIBLE);
errorBox.setText(message);
}
private void setLoginBoxesColorNormal() {
int normalColor = getResources().getColor(R.color.login_edit_text_color);
username.setTextColor(normalColor);
passwordOrPin.setTextColor(normalColor);
}
private void setLoginBoxesColorError() {
int errorColor = getResources().getColor(R.color.login_edit_text_color_error);
username.setTextColor(errorColor);
passwordOrPin.setTextColor(errorColor);
}
private void setStyleDefault() {
setLoginBoxesColorNormal();
username.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.icon_user_neutral50), null, null, null);
passwordOrPin.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.icon_lock_neutral50), null, null, null);
setupLoginButton();
if (loginButton.isEnabled()) {
// don't hide error box when showing permission error
errorBox.setVisibility(View.GONE);
}
}
protected void clearErrorMessage() {
errorBox.setVisibility(View.GONE);
}
private void setSingleAppUIState() {
spinner.setVisibility(View.GONE);
welcomeMessage.setText(Localization.get("login.welcome.single"));
}
protected void setMultipleAppsUIState(ArrayList<String> appNames, int position) {
welcomeMessage.setText(Localization.get("login.welcome.multiple"));
ArrayAdapter<String> adapter = new ArrayAdapter<>(activity,
R.layout.spinner_text_view, appNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
spinner.setOnItemSelectedListener(activity);
spinner.setSelection(position);
spinner.setVisibility(View.VISIBLE);
}
protected void setPermissionsGrantedState() {
loginButton.setEnabled(true);
errorBox.setVisibility(View.GONE);
errorBox.setText("");
}
protected void setPermissionDeniedState() {
loginButton.setEnabled(false);
errorBox.setVisibility(View.VISIBLE);
errorBox.setText(Localization.get("permission.all.denial.message"));
}
private void setupLoginButton() {
ColorDrawable colorDrawable = new ColorDrawable(getResources().getColor(R.color.cc_brand_color));
ColorDrawable disabledColor = new ColorDrawable(getResources().getColor(R.color.grey));
StateListDrawable sld = new StateListDrawable();
sld.addState(new int[]{-android.R.attr.state_enabled}, disabledColor);
sld.addState(StateSet.WILD_CARD, colorDrawable);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
loginButton.setBackground(sld);
} else {
loginButton.setBackgroundDrawable(sld);
}
loginButton.setTextColor(getResources().getColor(R.color.cc_neutral_bg));
}
protected void restoreLastUser() {
SharedPreferences prefs = CommCareApplication.instance().getCurrentApp().getAppPreferences();
String lastUser = prefs.getString(CommCarePreferences.LAST_LOGGED_IN_USER, null);
if (lastUser != null) {
username.setText(lastUser);
passwordOrPin.requestFocus();
}
}
protected boolean isRestoreSessionChecked() {
return restoreSessionCheckbox.isChecked();
}
protected String getEnteredUsername() {
return username.getText().toString();
}
protected String getEnteredPasswordOrPin() {
return passwordOrPin.getText().toString();
}
protected void setUsername(String s) {
username.setText(s);
}
protected void setPasswordOrPin(String s) {
passwordOrPin.setText(s);
}
private void updateBanner() {
ImageView topBannerImageView =
(ImageView)banner.findViewById(org.commcare.dalvik.R.id.main_top_banner);
if (!CustomBanner.useCustomBannerFitToActivity(activity, topBannerImageView)) {
topBannerImageView.setImageResource(R.drawable.commcare_logo);
}
}
private Resources getResources() {
return activity.getResources();
}
}