package org.commcare.activities; import android.annotation.TargetApi; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.util.Pair; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.Toast; import org.commcare.CommCareApp; import org.commcare.CommCareApplication; import org.commcare.dalvik.BuildConfig; import org.commcare.engine.resource.AppInstallStatus; import org.commcare.engine.resource.ResourceInstallUtils; import org.commcare.interfaces.CommCareActivityUIController; import org.commcare.interfaces.RuntimePermissionRequester; import org.commcare.interfaces.WithUIController; import org.commcare.android.database.app.models.UserKeyRecord; import org.commcare.android.database.global.models.ApplicationRecord; import org.commcare.models.database.user.DemoUserBuilder; import org.commcare.preferences.DevSessionRestorer; import org.commcare.suite.model.OfflineUserRestore; import org.commcare.tasks.DataPullTask; import org.commcare.tasks.InstallStagedUpdateTask; import org.commcare.tasks.ManageKeyRecordTask; import org.commcare.tasks.PullTaskResultReceiver; import org.commcare.tasks.ResultAndError; import org.commcare.utils.ACRAUtil; import org.commcare.utils.ConsumerAppsUtil; import org.commcare.utils.Permissions; import org.commcare.views.ViewUtil; import org.commcare.views.dialogs.CustomProgressDialog; import org.commcare.views.dialogs.DialogCreationHelpers; import org.commcare.views.notifications.MessageTag; import org.commcare.views.notifications.NotificationMessage; import org.commcare.views.notifications.NotificationMessageFactory; import org.commcare.views.notifications.NotificationMessageFactory.StockMessages; import org.javarosa.core.services.locale.Localization; import java.util.ArrayList; /** * @author ctsims */ public class LoginActivity extends CommCareActivity<LoginActivity> implements OnItemSelectedListener, DataPullController, RuntimePermissionRequester, WithUIController, PullTaskResultReceiver { private static final String TAG = LoginActivity.class.getSimpleName(); public static final int MENU_DEMO = Menu.FIRST; private static final int MENU_ABOUT = Menu.FIRST + 1; private static final int MENU_PERMISSIONS = Menu.FIRST + 2; private static final int MENU_PASSWORD_MODE = Menu.FIRST + 3; private static final int MENU_APP_MANAGER = Menu.FIRST + 4; public static final String NOTIFICATION_MESSAGE_LOGIN = "login_message"; public final static String KEY_LAST_APP = "id-last-seated-app"; public final static String KEY_ENTERED_USER = "entered-username"; public final static String KEY_ENTERED_PW_OR_PIN = "entered-password-or-pin"; private static final int SEAT_APP_ACTIVITY = 0; public final static String USER_TRIGGERED_LOGOUT = "user-triggered-logout"; public final static String LOGIN_MODE = "login-mode"; public final static String MANUAL_SWITCH_TO_PW_MODE = "manually-swithced-to-password-mode"; private static final int TASK_KEY_EXCHANGE = 1; private static final int TASK_UPGRADE_INSTALL = 2; private final ArrayList<String> appIdDropdownList = new ArrayList<>(); private String usernameBeforeRotation; private String passwordOrPinBeforeRotation; private LoginActivityUIController uiController; private FormAndDataSyncer formAndDataSyncer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (shouldFinish()) { // If we're going to finish in onResume() because there is no usable seated app, // don't bother with all of the setup here return; } uiController.setupUI(); formAndDataSyncer = new FormAndDataSyncer(); if (savedInstanceState == null) { // Only restore last user on the initial creation uiController.restoreLastUser(); } else { // If the screen was rotated with entered text present, we will want to restore it // in onResume (can't do it here b/c will get overriden by logic in refreshForNewApp()) usernameBeforeRotation = savedInstanceState.getString(KEY_ENTERED_USER); passwordOrPinBeforeRotation = savedInstanceState.getString(KEY_ENTERED_PW_OR_PIN); } Permissions.acquireAllAppPermissions(this, this, Permissions.ALL_PERMISSIONS_REQUEST); } @Override @TargetApi(Build.VERSION_CODES.M) public void requestNeededPermissions(int requestCode) { ActivityCompat.requestPermissions(this, Permissions.getAppPermissions(), requestCode); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { String[] requiredPerms = Permissions.getRequiredPerms(); if (requestCode == Permissions.ALL_PERMISSIONS_REQUEST) { for (int i = 0; i < permissions.length; i++) { for (String requiredPerm : requiredPerms) { if (requiredPerm.equals(permissions[i]) && grantResults[i] == PackageManager.PERMISSION_DENIED) { uiController.setPermissionDeniedState(); return; } } } } uiController.setPermissionsGrantedState(); } @Override protected void onSaveInstanceState(Bundle savedInstanceState) { super.onSaveInstanceState(savedInstanceState); String enteredUsername = uiController.getEnteredUsername(); if (!"".equals(enteredUsername) && enteredUsername != null) { savedInstanceState.putString(KEY_ENTERED_USER, enteredUsername); } String enteredPasswordOrPin = uiController.getEnteredPasswordOrPin(); if (!"".equals(enteredPasswordOrPin) && enteredPasswordOrPin != null) { savedInstanceState.putString(KEY_ENTERED_PW_OR_PIN, enteredPasswordOrPin); } } /** * @param restoreSession Indicates if CommCare should attempt to restore the saved session * upon successful login */ protected void initiateLoginAttempt(boolean restoreSession) { LoginMode loginMode = uiController.getLoginMode(); if ("".equals(uiController.getEnteredPasswordOrPin()) && loginMode != LoginMode.PRIMED) { if (loginMode == LoginMode.PASSWORD) { raiseLoginMessage(StockMessages.Auth_EmptyPassword, false); } else { raiseLoginMessage(StockMessages.Auth_EmptyPin, false); } return; } uiController.clearErrorMessage(); ViewUtil.hideVirtualKeyboard(LoginActivity.this); if (loginMode == LoginMode.PASSWORD) { DevSessionRestorer.tryAutoLoginPasswordSave(uiController.getEnteredPasswordOrPin(), false); } if (ResourceInstallUtils.isUpdateReadyToInstall()) { // install update, which triggers login upon completion installPendingUpdate(); } else { localLoginOrPullAndLogin(restoreSession); } } @Override public String getActivityTitle() { return null; } @Override public void startDataPull(DataPullMode mode) { switch(mode) { case CONSUMER_APP: formAndDataSyncer.performLocalRestore(this, getUniformUsername(), uiController.getEnteredPasswordOrPin()); break; case CCZ_DEMO: OfflineUserRestore offlineUserRestore = CommCareApplication.instance().getCommCarePlatform().getDemoUserRestore(); uiController.setUsername(offlineUserRestore.getUsername()); uiController.setPasswordOrPin(OfflineUserRestore.DEMO_USER_PASSWORD); formAndDataSyncer.performDemoUserRestore(this, offlineUserRestore); break; case NORMAL: formAndDataSyncer.performOtaRestore(this, getUniformUsername(), uiController.getEnteredPasswordOrPin()); break; } } @Override protected void onResume() { super.onResume(); if (shouldFinish()) { return; } // Otherwise, refresh the activity for current conditions uiController.refreshView(); } protected boolean checkForSeatedAppChange() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); String lastSeatedId = prefs.getString(KEY_LAST_APP, ""); String currentSeatedId = CommCareApplication.instance().getCurrentApp().getUniqueId(); if (!lastSeatedId.equals(currentSeatedId)) { prefs.edit().putString(KEY_LAST_APP, currentSeatedId).commit(); return true; } return false; } private static boolean shouldFinish() { CommCareApp currentApp = CommCareApplication.instance().getCurrentApp(); return currentApp == null || !currentApp.getAppRecord().isUsable(); } @Override protected void onResumeFragments() { super.onResumeFragments(); // It is possible that we left off at the LoginActivity last time we were on the main CC // screen, but have since done something in the app manager to either leave no seated app // at all, or to render the seated app unusable. Redirect to dispatch activity if we // encounter either case if (shouldFinish()) { setResult(RESULT_OK); this.finish(); return; } if (CommCareApplication.instance().isConsumerApp()) { uiController.setUsername(BuildConfig.CONSUMER_APP_USERNAME); uiController.setPasswordOrPin(BuildConfig.CONSUMER_APP_PASSWORD); localLoginOrPullAndLogin(false); } else { tryAutoLogin(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { if (requestCode == SEAT_APP_ACTIVITY && resultCode == RESULT_OK) { uiController.refreshForNewApp(); } super.onActivityResult(requestCode, resultCode, intent); } private void tryAutoLogin() { Pair<String, String> userAndPass = DevSessionRestorer.getAutoLoginCreds(forceAutoLogin()); if (userAndPass != null) { uiController.setUsername(userAndPass.first); uiController.setPasswordOrPin(userAndPass.second); // If we're doing auto-login, means we're using a password so switch UI to pw mode uiController.setNormalPasswordMode(); if (!getIntent().getBooleanExtra(USER_TRIGGERED_LOGOUT, false)) { // If we are attempting auto-login, assume that we want to restore a saved session initiateLoginAttempt(true); } } } private boolean forceAutoLogin() { return CommCareApplication.instance().checkPendingBuildRefresh(); } private String getUniformUsername() { return uiController.getEnteredUsername().toLowerCase().trim(); } private boolean tryLocalLogin(final boolean warnMultipleAccounts, boolean restoreSession) { //TODO: check username/password for emptiness return tryLocalLogin(getUniformUsername(), uiController.getEnteredPasswordOrPin(), warnMultipleAccounts, restoreSession, uiController.getLoginMode(), false); } private boolean tryLocalLogin(final String username, String passwordOrPin, final boolean warnMultipleAccounts, final boolean restoreSession, LoginMode loginMode, boolean forCustomDemoUser) { try { final boolean triggerMultipleUsersWarning = getMatchingUsersCount(username) > 1 && warnMultipleAccounts; ManageKeyRecordTask<LoginActivity> task = new ManageKeyRecordTask<LoginActivity>(this, TASK_KEY_EXCHANGE, username, passwordOrPin, loginMode, CommCareApplication.instance().getCurrentApp(), restoreSession, triggerMultipleUsersWarning, forCustomDemoUser) { @Override protected void deliverUpdate(LoginActivity receiver, String... update) { receiver.updateProgress(update[0], TASK_KEY_EXCHANGE); } }; task.connect(this); task.executeParallel(); return true; } catch (Exception e) { e.printStackTrace(); return false; } } private int getMatchingUsersCount(String username) { int count = 0; for (UserKeyRecord record : CommCareApplication.instance().getAppStorage(UserKeyRecord.class)) { if (record.getUsername().equals(username)) { count++; } } return count; } @Override public void dataPullCompleted() { ACRAUtil.registerUserData(); ViewUtil.hideVirtualKeyboard(LoginActivity.this); CommCareApplication.notificationManager().clearNotifications(NOTIFICATION_MESSAGE_LOGIN); Intent i = new Intent(); i.putExtra(LOGIN_MODE, uiController.getLoginMode()); i.putExtra(MANUAL_SWITCH_TO_PW_MODE, uiController.userManuallySwitchedToPasswordMode()); setResult(RESULT_OK, i); finish(); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); menu.add(0, MENU_DEMO, 0, Localization.get("login.menu.demo")).setIcon(android.R.drawable.ic_menu_preferences); menu.add(0, MENU_ABOUT, 1, Localization.get("home.menu.about")).setIcon(android.R.drawable.ic_menu_help); menu.add(0, MENU_PERMISSIONS, 1, Localization.get("permission.acquire.required")).setIcon(android.R.drawable.ic_menu_manage); menu.add(0, MENU_PASSWORD_MODE, 1, Localization.get("login.menu.password.mode")); menu.add(0, MENU_APP_MANAGER, 1, Localization.get("login.menu.app.manager")); return true; } @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); menu.findItem(MENU_PERMISSIONS).setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M); menu.findItem(MENU_PASSWORD_MODE).setVisible(uiController.getLoginMode() == LoginMode.PIN); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { boolean otherResult = super.onOptionsItemSelected(item); switch (item.getItemId()) { case MENU_DEMO: loginDemoUser(); return true; case MENU_ABOUT: DialogCreationHelpers.buildAboutCommCareDialog(this).showNonPersistentDialog(); return true; case MENU_PERMISSIONS: Permissions.acquireAllAppPermissions(this, this, Permissions.ALL_PERMISSIONS_REQUEST); return true; case MENU_PASSWORD_MODE: uiController.manualSwitchToPasswordMode(); return true; case MENU_APP_MANAGER: Intent i = new Intent(this, AppManagerActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(i); return true; default: return otherResult; } } private void loginDemoUser() { OfflineUserRestore offlineUserRestore = CommCareApplication.instance().getCommCarePlatform().getDemoUserRestore(); if (offlineUserRestore != null) { tryLocalLogin(offlineUserRestore.getUsername(), OfflineUserRestore.DEMO_USER_PASSWORD, false, false, LoginMode.PASSWORD, true); } else { DemoUserBuilder.build(this, CommCareApplication.instance().getCurrentApp()); tryLocalLogin(DemoUserBuilder.DEMO_USERNAME, DemoUserBuilder.DEMO_PASSWORD, false, false, LoginMode.PASSWORD, false); } } @Override public void raiseLoginMessageWithInfo(MessageTag messageTag, String additionalInfo, boolean showTop) { NotificationMessage message = NotificationMessageFactory.message(messageTag, new String[]{null, null, additionalInfo}, NOTIFICATION_MESSAGE_LOGIN); raiseMessage(message, showTop); } @Override public void raiseLoginMessage(MessageTag messageTag, boolean showTop) { NotificationMessage message = NotificationMessageFactory.message(messageTag, NOTIFICATION_MESSAGE_LOGIN); raiseMessage(message, showTop); } @Override public void raiseMessage(NotificationMessage message, boolean showTop) { String toastText = message.getTitle(); if (showTop) { CommCareApplication.notificationManager().reportNotificationMessage(message); toastText = Localization.get("notification.for.details.wrapper", new String[]{toastText}); } uiController.setErrorMessageUI(toastText); } /** * Implementation of generateProgressDialog() for DialogController -- other methods * handled entirely in CommCareActivity */ @Override public CustomProgressDialog generateProgressDialog(int taskId) { if (CommCareApplication.instance().isConsumerApp()) { return ConsumerAppsUtil.getGenericConsumerAppsProgressDialog(taskId, false); } CustomProgressDialog dialog; switch (taskId) { case TASK_KEY_EXCHANGE: dialog = CustomProgressDialog.newInstance(Localization.get("key.manage.title"), Localization.get("key.manage.start"), taskId); break; case DataPullTask.DATA_PULL_TASK_ID: dialog = CustomProgressDialog.newInstance(Localization.get("sync.communicating.title"), Localization.get("sync.progress.starting"), taskId); dialog.addCancelButton(); dialog.addProgressBar(); break; case TASK_UPGRADE_INSTALL: dialog = CustomProgressDialog.newInstance(Localization.get("updates.installing.title"), Localization.get("updates.installing.message"), taskId); break; default: Log.w(TAG, "taskId passed to generateProgressDialog does not match " + "any valid possibilities in LoginActivity"); return null; } return dialog; } protected void restoreEnteredTextFromRotation() { if (usernameBeforeRotation != null) { uiController.setUsername(usernameBeforeRotation); usernameBeforeRotation = null; } if (passwordOrPinBeforeRotation != null) { uiController.setPasswordOrPin(passwordOrPinBeforeRotation); passwordOrPinBeforeRotation = null; } } protected void populateAppSpinner(ArrayList<ApplicationRecord> readyApps) { ArrayList<String> appNames = new ArrayList<>(); appIdDropdownList.clear(); for (ApplicationRecord r : readyApps) { appNames.add(r.getDisplayName()); appIdDropdownList.add(r.getUniqueId()); } // Want to set the spinner's selection to match whatever the currently seated app is String currAppId = CommCareApplication.instance().getCurrentApp().getUniqueId(); int position = appIdDropdownList.indexOf(currAppId); uiController.setMultipleAppsUIState(appNames, position); } @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { // Retrieve the app record corresponding to the app selected String appId = appIdDropdownList.get(position); boolean selectedNewApp = !appId.equals(CommCareApplication.instance().getCurrentApp().getUniqueId()); if (selectedNewApp) { // Set the id of the last selected app SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); prefs.edit().putString(KEY_LAST_APP, appId).commit(); // Launch the activity to seat the new app Intent i = new Intent(this, SeatAppActivity.class); i.putExtra(SeatAppActivity.KEY_APP_TO_SEAT, appId); this.startActivityForResult(i, SEAT_APP_ACTIVITY); } } @Override public void onNothingSelected(AdapterView<?> parent) { } /** * Block the user with a dialog while downloaded update is installed. */ private void installPendingUpdate() { InstallStagedUpdateTask<LoginActivity> task = new InstallStagedUpdateTask<LoginActivity>(TASK_UPGRADE_INSTALL) { @Override protected void deliverResult(LoginActivity receiver, AppInstallStatus result) { if (result == AppInstallStatus.Installed) { Toast.makeText(receiver, Localization.get("login.update.install.success"), Toast.LENGTH_LONG).show(); } else { CommCareApplication.notificationManager().reportNotificationMessage(NotificationMessageFactory.message(result)); } localLoginOrPullAndLogin(uiController.isRestoreSessionChecked()); } @Override protected void deliverUpdate(LoginActivity receiver, int[]... update) { } @Override protected void deliverError(LoginActivity receiver, Exception e) { e.printStackTrace(); Log.e(TAG, "update installation on login failed: " + e.getMessage()); Toast.makeText(receiver, Localization.get("login.update.install.failure"), Toast.LENGTH_LONG).show(); localLoginOrPullAndLogin(uiController.isRestoreSessionChecked()); } }; task.connect(this); task.executeParallel(); } private void localLoginOrPullAndLogin(boolean restoreSession) { if (tryLocalLogin(false, restoreSession)) { return; } // If local login was not successful startDataPull(CommCareApplication.instance().isConsumerApp() ? DataPullMode.CONSUMER_APP : DataPullMode.NORMAL); } @Override public void initUIController() { if (CommCareApplication.instance().isConsumerApp()) { uiController = new BlankLoginActivityUIController(this); } else { uiController = new LoginActivityUIController(this); } } @Override public CommCareActivityUIController getUIController() { return this.uiController; } @Override public void handlePullTaskResult(ResultAndError<DataPullTask.PullTaskResult> resultAndErrorMessage, boolean userTriggeredSync, boolean formsToSend) { DataPullTask.PullTaskResult result = resultAndErrorMessage.data; if (result == null) { // The task crashed unexpectedly raiseLoginMessage(StockMessages.Restore_Unknown, true); return; } switch (result) { case AUTH_FAILED: raiseLoginMessage(StockMessages.Auth_BadCredentials, false); break; case BAD_DATA_REQUIRES_INTERVENTION: raiseLoginMessageWithInfo(StockMessages.Remote_BadRestoreRequiresIntervention, resultAndErrorMessage.errorMessage, true); break; case BAD_DATA: raiseLoginMessageWithInfo(StockMessages.Remote_BadRestore, resultAndErrorMessage.errorMessage, true); break; case STORAGE_FULL: raiseLoginMessage(StockMessages.Storage_Full, true); break; case DOWNLOAD_SUCCESS: if (!tryLocalLogin(true, uiController.isRestoreSessionChecked())) { raiseLoginMessage(StockMessages.Auth_CredentialMismatch, true); } break; case UNREACHABLE_HOST: raiseLoginMessage(StockMessages.Remote_NoNetwork, true); break; case CONNECTION_TIMEOUT: raiseLoginMessage(StockMessages.Remote_Timeout, true); break; case SERVER_ERROR: raiseLoginMessage(StockMessages.Remote_ServerError, true); break; case UNKNOWN_FAILURE: raiseLoginMessageWithInfo(StockMessages.Restore_Unknown, resultAndErrorMessage.errorMessage, true); break; case ACTIONABLE_FAILURE: raiseLoginMessageWithInfo(StockMessages.Restore_Unknown, resultAndErrorMessage.errorMessage, true); break; } } @Override public void handlePullTaskUpdate(Integer... update) { if (CommCareApplication.instance().isConsumerApp()) { return; } SyncCapableCommCareActivity.handleSyncUpdate(this, update); } @Override public void handlePullTaskError() { raiseLoginMessage(StockMessages.Restore_Unknown, true); } }