diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index 6ca9a71..cce624c 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -1,2 +1,9 @@ -keep class org.xmlpull.v1.** { *; } --dontwarn org.xmlpull.v1.** \ No newline at end of file +-dontwarn org.xmlpull.v1.** + +-keep public class org.simpleframework.**{ *; } +-keep class org.simpleframework.xml.**{ *; } +-keep class org.simpleframework.xml.core.**{ *; } +-keep class org.simpleframework.xml.util.**{ *; } +-keepattributes *Annotation* +-keepattributes Signature \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/MainGraph.kt b/android/app/src/main/java/app/eduroam/geteduroam/MainGraph.kt index c182709..90bdb99 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/MainGraph.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/MainGraph.kt @@ -11,9 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController @@ -22,8 +20,8 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import app.eduroam.geteduroam.config.WifiConfigScreen import app.eduroam.geteduroam.config.WifiConfigViewModel -import app.eduroam.geteduroam.institutions.SelectInstitutionScreen -import app.eduroam.geteduroam.institutions.SelectInstitutionViewModel +import app.eduroam.geteduroam.organizations.SelectOrganizationScreen +import app.eduroam.geteduroam.organizations.SelectOrganizationViewModel import app.eduroam.geteduroam.oauth.OAuthScreen import app.eduroam.geteduroam.oauth.OAuthViewModel import app.eduroam.geteduroam.profile.SelectProfileModal @@ -49,9 +47,9 @@ fun MainGraph( navController = navController, startDestination = Route.SelectInstitution.route ) { composable(Route.SelectInstitution.route) { entry -> - val viewModel = hiltViewModel(entry) + val viewModel = hiltViewModel(entry) val focusManager = LocalFocusManager.current - SelectInstitutionScreen( + SelectOrganizationScreen( viewModel = viewModel, openProfileModal = { institutionId -> // Remove the focus from the search field (if it was there) @@ -80,7 +78,8 @@ fun MainGraph( arguments = Route.SelectProfile.arguments, ) { entry -> val viewModel = hiltViewModel(entry) - SelectProfileModal(viewModel = viewModel, + SelectProfileModal( + viewModel = viewModel, goToOAuth = { auth, token -> navController.navigate(Route.OAuth.encodeArguments(auth, token)) }, diff --git a/android/app/src/main/java/app/eduroam/geteduroam/config/model/ProviderLogo.kt b/android/app/src/main/java/app/eduroam/geteduroam/config/model/ProviderLogo.kt index f8585cb..cc68b35 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/config/model/ProviderLogo.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/config/model/ProviderLogo.kt @@ -1,9 +1,13 @@ package app.eduroam.geteduroam.config.model +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Base64 import com.squareup.moshi.JsonClass import org.simpleframework.xml.Attribute import org.simpleframework.xml.Text + @JsonClass(generateAdapter = true) class ProviderLogo { @@ -15,4 +19,12 @@ class ProviderLogo { @field:Attribute var encoding: String? = null -} \ No newline at end of file + + fun convertToBitmap() : Bitmap? { + if (encoding?.lowercase() == "base64") { + val binary = Base64.decode(value, Base64.DEFAULT) + return BitmapFactory.decodeByteArray(binary, 0, binary.size) + } + return null + } +} diff --git a/android/app/src/main/java/app/eduroam/geteduroam/di/api/GetEduroamApi.kt b/android/app/src/main/java/app/eduroam/geteduroam/di/api/GetEduroamApi.kt index 8288654..3f3a31f 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/di/api/GetEduroamApi.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/di/api/GetEduroamApi.kt @@ -1,13 +1,10 @@ package app.eduroam.geteduroam.di.api -import app.eduroam.geteduroam.models.InstitutionResult +import app.eduroam.geteduroam.models.OrganizationResult import retrofit2.Response import retrofit2.http.GET -import retrofit2.http.Header -import retrofit2.http.POST -import retrofit2.http.Url interface GetEduroamApi { @GET("v1/discovery.json") - suspend fun getInstitutions(): Response + suspend fun getOrganizations(): Response } \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/extensions/StringExtensions.kt b/android/app/src/main/java/app/eduroam/geteduroam/extensions/StringExtensions.kt new file mode 100644 index 0000000..719a39d --- /dev/null +++ b/android/app/src/main/java/app/eduroam/geteduroam/extensions/StringExtensions.kt @@ -0,0 +1,7 @@ +package app.eduroam.geteduroam.extensions + +import java.text.Normalizer + +fun String.removeNonSpacingMarks() = + Normalizer.normalize(this, Normalizer.Form.NFD) + .replace("\\p{Mn}+".toRegex(), "") \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/institutions/TermsOfUseDialog.kt b/android/app/src/main/java/app/eduroam/geteduroam/institutions/TermsOfUseDialog.kt deleted file mode 100644 index 3dbc7ce..0000000 --- a/android/app/src/main/java/app/eduroam/geteduroam/institutions/TermsOfUseDialog.kt +++ /dev/null @@ -1,54 +0,0 @@ -package app.eduroam.geteduroam.institutions - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.TextButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import app.eduroam.geteduroam.R - -@Composable -fun TermsOfUseDialog( - onConfirmClicked: () -> Unit, - onDismiss: () -> Unit, -) = Dialog( - onDismissRequest = onDismiss, -) { - Surface( - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(16.dp)) { - Text(text = stringResource(R.string.terms_of_use_dialog_title)) - Spacer(modifier = Modifier.size(8.dp)) - Text( - text = stringResource(R.string.terms_of_use_dialog_text) - ) - Spacer(modifier = Modifier.size(16.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .weight(weight = 1f, fill = false) - ) { - - TextButton(onClick = onConfirmClicked) { - Text(text = stringResource(R.string.terms_of_use_dialog_agree)) - } - TextButton(onClick = onDismiss) { - Text(text = stringResource(R.string.terms_of_use_dialog_disagree)) - } - } - } - } -} diff --git a/android/app/src/main/java/app/eduroam/geteduroam/institutions/UiState.kt b/android/app/src/main/java/app/eduroam/geteduroam/institutions/UiState.kt deleted file mode 100644 index fbe71db..0000000 --- a/android/app/src/main/java/app/eduroam/geteduroam/institutions/UiState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.eduroam.geteduroam.institutions - -import app.eduroam.geteduroam.models.Institution -import app.eduroam.geteduroam.ui.ErrorData - -data class UiState( - val institutions: List = emptyList(), - val filter: String = "", - val isLoading: Boolean = false, - val selectedInstitution: Institution? = null, - val promptAuth: Unit? = null, - val errorData: ErrorData? = null, -) \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/models/Institution.kt b/android/app/src/main/java/app/eduroam/geteduroam/models/Institution.kt deleted file mode 100644 index d9f66ef..0000000 --- a/android/app/src/main/java/app/eduroam/geteduroam/models/Institution.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.eduroam.geteduroam.models - -import android.os.Parcelable -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import kotlinx.parcelize.Parcelize - -@Parcelize -@JsonClass(generateAdapter = true) -data class Institution( - @Json(name = "cat_idp") - val catIdp: Int, - val country: String, - val id: String, - val name: String, - val profiles: List, -) : Parcelable { - fun hasSingleProfile() = profiles.size == 1 - - fun requiresAuth(): Boolean = - if (hasSingleProfile()) { - profiles[0].oauth - } else false - -} \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/models/Organization.kt b/android/app/src/main/java/app/eduroam/geteduroam/models/Organization.kt new file mode 100644 index 0000000..40d31fe --- /dev/null +++ b/android/app/src/main/java/app/eduroam/geteduroam/models/Organization.kt @@ -0,0 +1,31 @@ +package app.eduroam.geteduroam.models + +import android.os.Parcelable +import app.eduroam.geteduroam.extensions.removeNonSpacingMarks +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +@JsonClass(generateAdapter = true) +data class Organization( + @Json(name = "cat_idp") + val catIdp: Int, + val country: String, + val id: String, + val name: String?, + val profiles: List, +) : Parcelable { + val nameOrId get() = name ?: id + @IgnoredOnParcel + val matchWords: List + init { + // Split on anything which is non-alphanumeric + val words = nameOrId.split("\\W+").filter { it.isNotEmpty() }.toMutableList() + val abbreviation = words.map { it.first() }.joinToString() + words += nameOrId + words += abbreviation + matchWords = words.map { it.removeNonSpacingMarks() } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/models/InstitutionResult.kt b/android/app/src/main/java/app/eduroam/geteduroam/models/OrganizationResult.kt similarity index 76% rename from android/app/src/main/java/app/eduroam/geteduroam/models/InstitutionResult.kt rename to android/app/src/main/java/app/eduroam/geteduroam/models/OrganizationResult.kt index a482518..9a8772e 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/models/InstitutionResult.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/models/OrganizationResult.kt @@ -6,7 +6,7 @@ import kotlinx.parcelize.Parcelize @Parcelize @JsonClass(generateAdapter = true) -data class InstitutionResult( - val instances: List, +data class OrganizationResult( + val instances: List, val version: Int, ) : Parcelable diff --git a/android/app/src/main/java/app/eduroam/geteduroam/models/WifiConfigData.kt b/android/app/src/main/java/app/eduroam/geteduroam/models/WifiConfigData.kt deleted file mode 100644 index e69de29..0000000 diff --git a/android/app/src/main/java/app/eduroam/geteduroam/institutions/InstitutionSearchHeader.kt b/android/app/src/main/java/app/eduroam/geteduroam/organizations/InstitutionSearchHeader.kt similarity index 88% rename from android/app/src/main/java/app/eduroam/geteduroam/institutions/InstitutionSearchHeader.kt rename to android/app/src/main/java/app/eduroam/geteduroam/organizations/InstitutionSearchHeader.kt index 85ebaba..38bd986 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/institutions/InstitutionSearchHeader.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/organizations/InstitutionSearchHeader.kt @@ -1,4 +1,4 @@ -package app.eduroam.geteduroam.institutions +package app.eduroam.geteduroam.organizations import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions @@ -7,12 +7,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview @@ -21,7 +19,7 @@ import app.eduroam.geteduroam.R import app.eduroam.geteduroam.ui.theme.AppTheme @Composable -fun InstitutionSearchHeader( +fun OrganizationSearchHeader( searchText: String, onSearchTextChange: (String) -> Unit, modifier: Modifier = Modifier, ) = Column(modifier.fillMaxWidth()) { val focusManager = LocalFocusManager.current @@ -40,7 +38,7 @@ fun InstitutionSearchHeader( }, placeholder = { Text( - text = stringResource(id = R.string.institution_search_text), + text = stringResource(id = R.string.organization_search_text), color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.7f), modifier = Modifier.fillMaxWidth() ) @@ -68,6 +66,6 @@ fun InstitutionSearchHeader( @Composable private fun MarketPlaceHeader_Preview() { AppTheme { - InstitutionSearchHeader("filterOn", {}) + OrganizationSearchHeader("filterOn", {}) } } \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/institutions/LoginDialog.kt b/android/app/src/main/java/app/eduroam/geteduroam/organizations/LoginDialog.kt similarity index 94% rename from android/app/src/main/java/app/eduroam/geteduroam/institutions/LoginDialog.kt rename to android/app/src/main/java/app/eduroam/geteduroam/organizations/LoginDialog.kt index 74ddd4c..8293147 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/institutions/LoginDialog.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/organizations/LoginDialog.kt @@ -1,4 +1,4 @@ -package app.eduroam.geteduroam.institutions +package app.eduroam.geteduroam.organizations import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -54,6 +54,7 @@ fun LoginDialog( .padding(vertical = 16.dp) ) { Text( + modifier = Modifier.padding(bottom = 4.dp), text = stringResource(R.string.login_dialog_text) ) @@ -83,7 +84,8 @@ fun LoginDialog( } Row( - modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.End ) { TextButton(onClick = { openDialog.value = false diff --git a/android/app/src/main/java/app/eduroam/geteduroam/institutions/InstitutionRow.kt b/android/app/src/main/java/app/eduroam/geteduroam/organizations/OrganizationRow.kt similarity index 81% rename from android/app/src/main/java/app/eduroam/geteduroam/institutions/InstitutionRow.kt rename to android/app/src/main/java/app/eduroam/geteduroam/organizations/OrganizationRow.kt index befe26a..9a98d75 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/institutions/InstitutionRow.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/organizations/OrganizationRow.kt @@ -1,4 +1,4 @@ -package app.eduroam.geteduroam.institutions +package app.eduroam.geteduroam.organizations import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -15,13 +15,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import app.eduroam.geteduroam.models.Institution +import app.eduroam.geteduroam.models.Organization @Composable -fun InstitutionRow( - institution: Institution, - onSelectInstitution: (Institution) -> Unit, +fun OrganizationRow( + organization: Organization, + onSelectOrganization: (Organization) -> Unit, modifier: Modifier = Modifier, ) = Surface( color = MaterialTheme.colorScheme.surface @@ -30,10 +30,10 @@ fun InstitutionRow( modifier .fillMaxWidth() .padding(horizontal = 16.dp) - .clickable { onSelectInstitution(institution) }) { + .clickable { onSelectOrganization(organization) }) { Spacer(Modifier.height(8.dp)) Text( - text = institution.name, + text = organization.nameOrId, style = MaterialTheme.typography.titleMedium, fontSize = 18.sp, fontWeight = FontWeight.Bold, @@ -41,7 +41,7 @@ fun InstitutionRow( ) Spacer(Modifier.height(4.dp)) Text( - text = institution.country, + text = organization.country, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.secondary ) diff --git a/android/app/src/main/java/app/eduroam/geteduroam/institutions/SelectInstitutionScreen.kt b/android/app/src/main/java/app/eduroam/geteduroam/organizations/SelectOrganizationScreen.kt similarity index 87% rename from android/app/src/main/java/app/eduroam/geteduroam/institutions/SelectInstitutionScreen.kt rename to android/app/src/main/java/app/eduroam/geteduroam/organizations/SelectOrganizationScreen.kt index 5394873..6054523 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/institutions/SelectInstitutionScreen.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/organizations/SelectOrganizationScreen.kt @@ -1,6 +1,5 @@ -package app.eduroam.geteduroam.institutions +package app.eduroam.geteduroam.organizations -import android.view.Gravity import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -45,7 +44,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.flowWithLifecycle import app.eduroam.geteduroam.R import app.eduroam.geteduroam.config.model.EAPIdentityProviderList -import app.eduroam.geteduroam.models.Institution +import app.eduroam.geteduroam.models.Organization import app.eduroam.geteduroam.models.Profile import app.eduroam.geteduroam.ui.ErrorData import app.eduroam.geteduroam.ui.theme.AppTheme @@ -53,8 +52,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @Composable -fun SelectInstitutionScreen( - viewModel: SelectInstitutionViewModel, +fun SelectOrganizationScreen( + viewModel: SelectOrganizationViewModel, openProfileModal: (String) -> Unit, goToOAuth: (Profile) -> Unit, goToConfigScreen: (EAPIdentityProviderList) -> Unit, @@ -89,22 +88,22 @@ fun SelectInstitutionScreen( val currentOpenProfileModal by rememberUpdatedState(newValue = openProfileModal) LaunchedEffect(viewModel, lifecycle) { snapshotFlow { viewModel.uiState }.distinctUntilChanged() - .filter { it.selectedInstitution != null }.flowWithLifecycle(lifecycle).collect { + .filter { it.selectedOrganization != null }.flowWithLifecycle(lifecycle).collect { waitForVmEvent = false currentOpenProfileModal( - it.selectedInstitution?.id.orEmpty(), + it.selectedOrganization?.id.orEmpty(), ) viewModel.clearSelection() } } } - SelectInstitutionContent( - institutions = viewModel.uiState.institutions, + SelectOrganizationContent( + organizations = viewModel.uiState.organizations, isLoading = viewModel.uiState.isLoading, - onSelectInstitution = { institution -> + onSelectOrganization = { organization -> waitForVmEvent = true - viewModel.onInstitutionSelect(institution) + viewModel.onOrganizationSelect(organization) }, searchText = viewModel.uiState.filter, onSearchTextChange = { viewModel.onSearchTextChange(it) }, @@ -116,12 +115,12 @@ fun SelectInstitutionScreen( @Composable -fun SelectInstitutionContent( - institutions: List = emptyList(), +fun SelectOrganizationContent( + organizations: List = emptyList(), isLoading: Boolean = false, showDialog: Boolean = false, errorData: ErrorData? = null, - onSelectInstitution: (Institution) -> Unit, + onSelectOrganization: (Organization) -> Unit, searchText: String, onSearchTextChange: (String) -> Unit = {}, onClearDialog: () -> Unit = {}, @@ -174,7 +173,7 @@ fun SelectInstitutionContent( .imePadding(), horizontalAlignment = Alignment.CenterHorizontally ) { - InstitutionSearchHeader( + OrganizationSearchHeader( searchText = searchText, onSearchTextChange = onSearchTextChange, modifier = Modifier.fillMaxWidth() @@ -199,19 +198,19 @@ fun SelectInstitutionContent( ) } } else if (!isLoading) { - if (institutions.isEmpty()) { + if (organizations.isEmpty() && searchText.isNotEmpty()) { item { Text( modifier = Modifier.padding(16.dp), color = MaterialTheme.colorScheme.secondary, - text = stringResource(id = R.string.institutions_no_results), + text = stringResource(id = R.string.organizations_no_results), fontWeight = FontWeight.Medium ) } } else { - institutions.forEach { institution -> + organizations.forEach { organization -> item { - InstitutionRow(institution, onSelectInstitution) + OrganizationRow(organization, onSelectOrganization) } } } @@ -223,10 +222,10 @@ fun SelectInstitutionContent( @Preview @Composable -fun Preview_SelectInstitutionContent() { +fun Preview_SelectOrganizationContent() { AppTheme { - SelectInstitutionContent( - onSelectInstitution = {}, + SelectOrganizationContent( + onSelectOrganization = {}, searchText = "" ) } diff --git a/android/app/src/main/java/app/eduroam/geteduroam/institutions/SelectInstitutionViewModel.kt b/android/app/src/main/java/app/eduroam/geteduroam/organizations/SelectOrganizationViewModel.kt similarity index 58% rename from android/app/src/main/java/app/eduroam/geteduroam/institutions/SelectInstitutionViewModel.kt rename to android/app/src/main/java/app/eduroam/geteduroam/organizations/SelectOrganizationViewModel.kt index 37a53a4..bac9617 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/institutions/SelectInstitutionViewModel.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/organizations/SelectOrganizationViewModel.kt @@ -1,4 +1,4 @@ -package app.eduroam.geteduroam.institutions +package app.eduroam.geteduroam.organizations import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -8,7 +8,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.eduroam.geteduroam.R import app.eduroam.geteduroam.di.api.GetEduroamApi -import app.eduroam.geteduroam.models.Institution +import app.eduroam.geteduroam.extensions.removeNonSpacingMarks +import app.eduroam.geteduroam.models.Organization import app.eduroam.geteduroam.ui.ErrorData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -17,7 +18,7 @@ import timber.log.Timber import javax.inject.Inject @HiltViewModel -class SelectInstitutionViewModel @Inject constructor( +class SelectOrganizationViewModel @Inject constructor( savedStateHandle: SavedStateHandle, val api: GetEduroamApi, ) : ViewModel() { @@ -25,7 +26,7 @@ class SelectInstitutionViewModel @Inject constructor( var uiState by mutableStateOf(UiState()) private set - private var allInstitutions by mutableStateOf(emptyList()) + private var allOrganizations by mutableStateOf(emptyList()) val creds: MutableStateFlow> = @@ -35,20 +36,19 @@ class SelectInstitutionViewModel @Inject constructor( viewModelScope.launch { uiState = uiState.copy(isLoading = true) try { - val response = api.getInstitutions() - val institutionResult = response.body() - if (response.isSuccessful && institutionResult != null) { - allInstitutions = institutionResult.instances - uiState = uiState.copy( - isLoading = false, institutions = institutionResult.instances - ) + val response = api.getOrganizations() + val organizationResult = response.body() + if (response.isSuccessful && organizationResult != null) { + allOrganizations = organizationResult.instances + uiState = uiState.copy(isLoading = false) } else { val failReason = "${response.code()}/${response.message()}]${ response.errorBody()?.string() }" - Timber.e("Failed to load institutions: $failReason") + Timber.e("Failed to load organizations: $failReason") uiState = uiState.copy( - isLoading = false, errorData = ErrorData( + isLoading = false, + errorData = ErrorData( titleId = R.string.err_title_generic_fail, messageId = R.string.err_msg_generic_unexpected_with_arg, messageArg = failReason @@ -56,9 +56,10 @@ class SelectInstitutionViewModel @Inject constructor( ) } } catch (e: Exception) { - Timber.e("Failed to get institutions", e) + Timber.e("Failed to get organizations", e) uiState = uiState.copy( - isLoading = false, errorData = ErrorData( + isLoading = false, + errorData = ErrorData( titleId = R.string.err_title_generic_fail, messageId = R.string.err_msg_generic_unexpected_with_arg, messageArg = "${e.message}/${e.javaClass.name}" @@ -72,17 +73,22 @@ class SelectInstitutionViewModel @Inject constructor( } - fun onInstitutionSelect(institution: Institution) { - uiState = uiState.copy(selectedInstitution = institution) + fun onOrganizationSelect(organization: Organization) { + uiState = uiState.copy(selectedOrganization = organization) } fun onSearchTextChange(filter: String) { val filtered = if (filter.isNotBlank()) { - allInstitutions.filter { it.name.contains(filter, ignoreCase = true) } + val normalizedFilter = filter.removeNonSpacingMarks() + allOrganizations.filter { organization -> + organization.matchWords.any { + it.contains(filter, ignoreCase = true) + } + }.sortedBy { it.nameOrId.lowercase() } } else { - allInstitutions + emptyList() } - uiState = uiState.copy(filter = filter, institutions = filtered) + uiState = uiState.copy(filter = filter, organizations = filtered) } fun clearDialog() { @@ -90,6 +96,6 @@ class SelectInstitutionViewModel @Inject constructor( } fun clearSelection() { - uiState = uiState.copy(selectedInstitution = null, filter = "") + uiState = uiState.copy(selectedOrganization = null, filter = "") } } \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/institutions/Step.kt b/android/app/src/main/java/app/eduroam/geteduroam/organizations/Step.kt similarity index 66% rename from android/app/src/main/java/app/eduroam/geteduroam/institutions/Step.kt rename to android/app/src/main/java/app/eduroam/geteduroam/organizations/Step.kt index f86c9f9..8985771 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/institutions/Step.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/organizations/Step.kt @@ -1,12 +1,12 @@ -package app.eduroam.geteduroam.institutions +package app.eduroam.geteduroam.organizations import app.eduroam.geteduroam.config.model.EAPIdentityProviderList -import app.eduroam.geteduroam.models.Institution +import app.eduroam.geteduroam.models.Organization import app.eduroam.geteduroam.models.Profile sealed class Step { object Start : Step() data class DoConfig(val eapIdentityProviderList: EAPIdentityProviderList) : Step() data class DoOAuthFor(val profile: Profile, val authorizationUrl: String) : Step() - data class PickProfileFrom(val institution: Institution) : Step() + data class PickProfileFrom(val organization: Organization) : Step() } \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/organizations/TermsOfUseDialog.kt b/android/app/src/main/java/app/eduroam/geteduroam/organizations/TermsOfUseDialog.kt new file mode 100644 index 0000000..08e1e01 --- /dev/null +++ b/android/app/src/main/java/app/eduroam/geteduroam/organizations/TermsOfUseDialog.kt @@ -0,0 +1,76 @@ +package app.eduroam.geteduroam.organizations + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.TextButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import app.eduroam.geteduroam.R +import app.eduroam.geteduroam.config.model.ProviderInfo +import app.eduroam.geteduroam.extensions.removeNonSpacingMarks +import app.eduroam.geteduroam.ui.LinkifyText + +@Composable +fun TermsOfUseDialog( + providerInfo: ProviderInfo?, + onConfirmClicked: () -> Unit, + onDismiss: () -> Unit, +) = Dialog( + onDismissRequest = onDismiss, +) { + Surface( + shape = MaterialTheme.shapes.medium + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.terms_of_use_dialog_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.size(16.dp)) + var dialogDescription = stringResource(R.string.terms_of_use_dialog_text) + providerInfo?.termsOfUse?.let { termsOfUse -> + dialogDescription += "\n\n" + termsOfUse.trim() + } + LinkifyText( + text = dialogDescription, + color = MaterialTheme.colorScheme.secondary, + linkColor = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodyMedium + ) + Spacer(modifier = Modifier.size(16.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .weight(weight = 1f, fill = false) + ) { + + TextButton(onClick = onConfirmClicked) { + Text( + text = stringResource(R.string.terms_of_use_dialog_agree), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.ExtraBold + ) + } + TextButton(onClick = onDismiss) { + Text( + text = stringResource(R.string.terms_of_use_dialog_disagree), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.ExtraBold + ) + } + } + } + } +} diff --git a/android/app/src/main/java/app/eduroam/geteduroam/organizations/UiState.kt b/android/app/src/main/java/app/eduroam/geteduroam/organizations/UiState.kt new file mode 100644 index 0000000..bf62abb --- /dev/null +++ b/android/app/src/main/java/app/eduroam/geteduroam/organizations/UiState.kt @@ -0,0 +1,13 @@ +package app.eduroam.geteduroam.organizations + +import app.eduroam.geteduroam.models.Organization +import app.eduroam.geteduroam.ui.ErrorData + +data class UiState( + val organizations: List = emptyList(), + val filter: String = "", + val isLoading: Boolean = false, + val selectedOrganization: Organization? = null, + val promptAuth: Unit? = null, + val errorData: ErrorData? = null, +) \ No newline at end of file diff --git a/android/app/src/main/java/app/eduroam/geteduroam/institutions/UsernamePasswordDialog.kt b/android/app/src/main/java/app/eduroam/geteduroam/organizations/UsernamePasswordDialog.kt similarity index 96% rename from android/app/src/main/java/app/eduroam/geteduroam/institutions/UsernamePasswordDialog.kt rename to android/app/src/main/java/app/eduroam/geteduroam/organizations/UsernamePasswordDialog.kt index 68e2e9e..9d48fff 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/institutions/UsernamePasswordDialog.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/organizations/UsernamePasswordDialog.kt @@ -1,4 +1,4 @@ -package app.eduroam.geteduroam.institutions +package app.eduroam.geteduroam.organizations import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -10,8 +10,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button -import androidx.compose.material3.ButtonColors -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -27,7 +25,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation diff --git a/android/app/src/main/java/app/eduroam/geteduroam/profile/SelectProfileModal.kt b/android/app/src/main/java/app/eduroam/geteduroam/profile/SelectProfileModal.kt index 5e8b330..3b3337d 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/profile/SelectProfileModal.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/profile/SelectProfileModal.kt @@ -1,6 +1,7 @@ package app.eduroam.geteduroam.profile import android.content.res.Configuration +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -14,9 +15,11 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.sizeIn +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Check import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator @@ -30,20 +33,25 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.flowWithLifecycle import app.eduroam.geteduroam.R import app.eduroam.geteduroam.config.model.EAPIdentityProviderList -import app.eduroam.geteduroam.institutions.TermsOfUseDialog -import app.eduroam.geteduroam.institutions.UsernamePasswordDialog +import app.eduroam.geteduroam.config.model.ProviderInfo +import app.eduroam.geteduroam.organizations.TermsOfUseDialog +import app.eduroam.geteduroam.organizations.UsernamePasswordDialog import app.eduroam.geteduroam.models.Profile import app.eduroam.geteduroam.ui.AlertDialogWithSingleButton import app.eduroam.geteduroam.ui.ErrorData +import app.eduroam.geteduroam.ui.LinkifyText import app.eduroam.geteduroam.ui.PrimaryButton import app.eduroam.geteduroam.ui.theme.AppTheme import kotlinx.coroutines.android.awaitFrame @@ -81,6 +89,7 @@ fun SelectProfileModal( SelectProfileContent( profiles = viewModel.uiState.profiles, institution = viewModel.uiState.institution, + providerInfo = viewModel.uiState.providerInfo, inProgress = viewModel.uiState.inProgress, errorData = viewModel.uiState.errorData, errorDataShown = viewModel::errorDataShown, @@ -89,11 +98,14 @@ fun SelectProfileModal( ) if (viewModel.uiState.showTermsOfUseDialog) { - TermsOfUseDialog(onConfirmClicked = { - viewModel.didAgreeToTerms(true) - }, onDismiss = { - viewModel.didAgreeToTerms(false) - }) + TermsOfUseDialog( + providerInfo = viewModel.uiState.providerInfo, + onConfirmClicked = { + viewModel.didAgreeToTerms(true) + }, onDismiss = { + viewModel.didAgreeToTerms(false) + } + ) } if (viewModel.uiState.showUsernameDialog) { UsernamePasswordDialog( @@ -111,6 +123,7 @@ fun SelectProfileModal( fun SelectProfileContent( profiles: List, institution: PresentInstitution? = null, + providerInfo: ProviderInfo? = null, inProgress: Boolean = false, errorData: ErrorData? = null, errorDataShown: () -> Unit = {}, @@ -152,7 +165,7 @@ fun SelectProfileContent( Text( text = it, style = MaterialTheme.typography.titleSmall, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(top = 4.dp) ) } Spacer(Modifier.height(24.dp)) @@ -202,11 +215,77 @@ fun SelectProfileContent( ) } } + if (providerInfo != null) { + Row(modifier = Modifier.padding(vertical = 16.dp), + verticalAlignment = Alignment.Top) { + providerInfo.providerLogo?.convertToBitmap()?.let { logoBitmap -> + Surface( + modifier = Modifier.size(104.dp), + color = Color.White, + shape = MaterialTheme.shapes.medium + ) { + Image( + modifier = Modifier.fillMaxSize(), + bitmap = logoBitmap.asImageBitmap(), + contentDescription = stringResource(id = R.string.content_description_provider_logo) + ) + } + Spacer(modifier = Modifier.size(16.dp)) + } + Column( + modifier = Modifier.fillMaxWidth(fraction = 1f) + ) { + providerInfo.displayName?.let { displayName -> + Text( + text = displayName, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.size(8.dp)) + } + val contactDetails = listOfNotNull( + providerInfo.helpdesk?.webAddress, + providerInfo.helpdesk?.emailAddress, + providerInfo.helpdesk?.phone + ) + if (contactDetails.isNotEmpty()) { + Text( + text = stringResource(id = R.string.helpdesk_title), + style = MaterialTheme.typography.titleSmall, + ) + Spacer(modifier = Modifier.size(8.dp)) + contactDetails.forEach { + LinkifyText( + text = it, + color = MaterialTheme.colorScheme.secondary, + linkColor = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodySmall + ) + Spacer(modifier = Modifier.size(4.dp)) + } + } + providerInfo.termsOfUse?.let { termsOfUse -> + Text( + text = stringResource(id = R.string.terms_of_use_dialog_title), + style = MaterialTheme.typography.titleSmall, + ) + Spacer(modifier = Modifier.size(4.dp)) + LinkifyText( + text = termsOfUse, + color = MaterialTheme.colorScheme.secondary, + linkColor = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.bodySmall + ) + } + } + } + } PrimaryButton( text = stringResource(R.string.button_connect), enabled = !inProgress, onClick = { connectWithSelectedProfile() }, - modifier = Modifier.weight(1f, false) + modifier = Modifier + .weight(1f, false) .navigationBarsPadding() ) } diff --git a/android/app/src/main/java/app/eduroam/geteduroam/profile/SelectProfileViewModel.kt b/android/app/src/main/java/app/eduroam/geteduroam/profile/SelectProfileViewModel.kt index dca01f8..8685d7d 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/profile/SelectProfileViewModel.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/profile/SelectProfileViewModel.kt @@ -20,7 +20,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.OkHttpClient import okhttp3.Request import timber.log.Timber @@ -56,7 +55,7 @@ class SelectProfileViewModel @Inject constructor( private fun loadData() = viewModelScope.launch { uiState = uiState.copy(inProgress = true) - val response = api.getInstitutions() + val response = api.getOrganizations() val institutionResult = response.body() if (response.isSuccessful && institutionResult != null) { val selectedInstitution = institutionResult.instances.find { it.id == institutionId } @@ -202,7 +201,8 @@ class SelectProfileViewModel @Inject constructor( termsOfUse = info?.termsOfUse, helpDesk = info?.helpdesk, requiredSuffix = firstProvider.authenticationMethod?.firstOrNull()?.clientSideCredential?.innerIdentitySuffix - ) + ), + providerInfo = info ) if (info?.termsOfUse != null && !didAgreeToTerms) { uiState = uiState.copy(inProgress = false, showTermsOfUseDialog = true) @@ -224,7 +224,8 @@ class SelectProfileViewModel @Inject constructor( private fun displayEapError() { uiState = uiState.copy( - inProgress = false, errorData = ErrorData( + inProgress = false, + errorData = ErrorData( titleId = R.string.err_title_generic_fail, messageId = R.string.err_msg_no_valid_provider ) diff --git a/android/app/src/main/java/app/eduroam/geteduroam/profile/UiState.kt b/android/app/src/main/java/app/eduroam/geteduroam/profile/UiState.kt index ffe8edf..a5594f3 100644 --- a/android/app/src/main/java/app/eduroam/geteduroam/profile/UiState.kt +++ b/android/app/src/main/java/app/eduroam/geteduroam/profile/UiState.kt @@ -2,6 +2,7 @@ package app.eduroam.geteduroam.profile import app.eduroam.geteduroam.config.model.EAPIdentityProviderList import app.eduroam.geteduroam.config.model.Helpdesk +import app.eduroam.geteduroam.config.model.ProviderInfo import app.eduroam.geteduroam.models.Credentials import app.eduroam.geteduroam.models.Profile import app.eduroam.geteduroam.ui.ErrorData @@ -10,6 +11,7 @@ data class UiState( val profiles: List = emptyList(), val credentialsEnteredForProviderList: EAPIdentityProviderList? = null, val institution: PresentInstitution? = null, + val providerInfo: ProviderInfo? = null, val inProgress: Boolean = false, val errorData: ErrorData? = null, val promptForOAuth: Unit? = null, diff --git a/android/app/src/main/java/app/eduroam/geteduroam/ui/LinkifyText.kt b/android/app/src/main/java/app/eduroam/geteduroam/ui/LinkifyText.kt new file mode 100644 index 0000000..5542e7b --- /dev/null +++ b/android/app/src/main/java/app/eduroam/geteduroam/ui/LinkifyText.kt @@ -0,0 +1,164 @@ +/** + * Text which works like the regular TextView with autoLink on. + * Taken from: https://github.com/firefinchdev/linkify-text + */ +package app.eduroam.geteduroam.ui + +import android.os.Build +import android.text.SpannableString +import android.text.style.URLSpan +import android.text.util.Linkify +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit + +@Composable +fun LinkifyText( + text: String, + modifier: Modifier = Modifier, + linkColor: Color = Color.Blue, + linkEntire: Boolean = false, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + clickable: Boolean = true, + onClickLink: ((linkText: String) -> Unit)? = null +) { + val uriHandler = LocalUriHandler.current + val linkInfos = if (linkEntire) listOf(LinkInfo(text, 0, text.length)) else SpannableStr.getLinkInfos(text) + val annotatedString = buildAnnotatedString { + append(text) + linkInfos.forEach { + addStyle( + style = SpanStyle( + color = linkColor, + textDecoration = TextDecoration.Underline + ), + start = it.start, + end = it.end + ) + addStringAnnotation( + tag = "tag", + annotation = it.url, + start = it.start, + end = it.end + ) + } + } + if (clickable) { + ClickableText( + text = annotatedString, + modifier = modifier, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + onTextLayout = onTextLayout, + style = style.copy(color = color), + onClick = { offset -> + annotatedString.getStringAnnotations( + start = offset, + end = offset, + ).firstOrNull()?.let { result -> + if (linkEntire) { + onClickLink?.invoke(annotatedString.substring(result.start, result.end)) + } else { + uriHandler.openUri(result.item) + onClickLink?.invoke(annotatedString.substring(result.start, result.end)) + } + } + } + ) + } else { + Text( + text = annotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + onTextLayout = onTextLayout, + style = style + ) + } +} + + +private data class LinkInfo( + val url: String, + val start: Int, + val end: Int +) + +private class SpannableStr(source: CharSequence): SpannableString(source) { + companion object { + fun getLinkInfos(text: String): List { + val spannableStr = SpannableStr(text) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Linkify.addLinks(spannableStr, Linkify.ALL) { str: String -> URLSpan(str) } + } else { + Linkify.addLinks(spannableStr, Linkify.ALL) + } + return spannableStr.linkInfos + } + } + private inner class Data( + val what: Any?, + val start: Int, + val end: Int + ) + private val spanList = mutableListOf() + + private val linkInfos: List + get() = spanList.filter { it.what is URLSpan }.map { + LinkInfo( + (it.what as URLSpan).url, + it.start, + it.end + ) + } + + override fun removeSpan(what: Any?) { + super.removeSpan(what) + spanList.removeAll { it.what == what } + } + + override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) { + super.setSpan(what, start, end, flags) + spanList.add(Data(what, start, end)) + } +} \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 78f5e8a..661fef9 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2,8 +2,8 @@ geteduroam Welcome - Could not find any institutions. - Search for your organization + Could not find any institutions. + Search for your organization Select the institution you want to connect to. Profiles Establishing WiFi connection @@ -17,7 +17,7 @@ Cancel Log in Terms of use - You must agree with to the terms of use before you can use this network + You must agree to the terms of use before you can use this network Agree Disagree @@ -46,6 +46,10 @@ CONNECT Back + Provider logo + + Helpdesk + Login required Please enter your username and password. Username