diff --git a/ui/build.gradle b/ui/build.gradle index 1ff05ce..5c123a1 100644 --- a/ui/build.gradle +++ b/ui/build.gradle @@ -96,6 +96,13 @@ dependencies { implementation "com.squareup.retrofit2:adapter-rxjava2:$adapterrxjava2Version" implementation "com.airbnb.android:lottie:$lottieVersion" implementation "com.jakewharton.rxbinding4:rxbinding:$rxbindingVersion" + + // Replace bundled strings dynamically + implementation 'dev.b3nedikt.restring:restring:4.0.5' + // Intercept view inflation + implementation 'io.github.inflationx:viewpump:2.0.3' + // Allows to update the text of views at runtime without recreating the activity + implementation 'dev.b3nedikt.reword:reword:1.1.0' } tasks.withType(JavaCompile) { diff --git a/ui/src/main/java/com/getbubblenow/android/Application.kt b/ui/src/main/java/com/getbubblenow/android/Application.kt index 74d8ac9..3d2d388 100644 --- a/ui/src/main/java/com/getbubblenow/android/Application.kt +++ b/ui/src/main/java/com/getbubblenow/android/Application.kt @@ -13,17 +13,21 @@ import android.os.StrictMode.VmPolicy import android.util.Log import androidx.appcompat.app.AppCompatDelegate import androidx.preference.PreferenceManager -import com.wireguard.android.backend.Backend -import com.wireguard.android.backend.GoBackend -import com.wireguard.android.backend.WgQuickBackend import com.getbubblenow.android.configStore.FileConfigStore import com.getbubblenow.android.model.TunnelManager -import com.getbubblenow.android.repository.DataRepository +import com.getbubblenow.android.repository.RestringMessageRepository import com.getbubblenow.android.util.AsyncWorker import com.getbubblenow.android.util.ExceptionLoggers +import com.wireguard.android.backend.Backend +import com.wireguard.android.backend.GoBackend +import com.wireguard.android.backend.WgQuickBackend import com.wireguard.android.util.ModuleLoader import com.wireguard.android.util.RootShell import com.wireguard.android.util.ToolsInstaller +import dev.b3nedikt.restring.Restring +import dev.b3nedikt.restring.RestringConfig +import dev.b3nedikt.reword.RewordInterceptor +import io.github.inflationx.viewpump.ViewPump import java9.util.concurrent.CompletableFuture import java.lang.ref.WeakReference import java.util.Locale @@ -157,5 +161,16 @@ class Application : android.app.Application(), OnSharedPreferenceChangeListener init { weakSelf = WeakReference(this) + + Restring.init(this, + RestringConfig.Builder() + .stringRepository(RestringMessageRepository.getInstance()) + .build() + ) + + ViewPump.init(ViewPump.builder() + .addInterceptor(RewordInterceptor) + .build() + ) } } diff --git a/ui/src/main/java/com/getbubblenow/android/activity/BaseActivityBubble.java b/ui/src/main/java/com/getbubblenow/android/activity/BaseActivityBubble.java index 2f852b2..3167229 100644 --- a/ui/src/main/java/com/getbubblenow/android/activity/BaseActivityBubble.java +++ b/ui/src/main/java/com/getbubblenow/android/activity/BaseActivityBubble.java @@ -1,5 +1,7 @@ package com.getbubblenow.android.activity; +import android.content.Context; +import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.view.Gravity; @@ -16,6 +18,9 @@ import com.getbubblenow.android.repository.DataRepository; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.Lifecycle; +import dev.b3nedikt.restring.Restring; +import dev.b3nedikt.reword.Reword; +import io.github.inflationx.viewpump.ViewPumpContextWrapper; public class BaseActivityBubble extends AppCompatActivity{ @@ -24,6 +29,7 @@ public class BaseActivityBubble extends AppCompatActivity{ public static final String ERROR_TAG = "error_tag"; public static final String PROGRESS_TAG = "progress_tag"; public static final String RATE_TAG = "rate tag"; + public static final String LOCALIZATION_TAG = "localization_tag"; private final long LOADER_DELAY = 1000; private LoadingDialogFragment loadingDialog; @@ -38,6 +44,20 @@ public class BaseActivityBubble extends AppCompatActivity{ } else { DataRepository.getRepositoryInstance().buildClientService(ApiConstants.BOOTSTRAP_URL); } + + // The layout containing the views you want to localize + final View rootView = getWindow().getDecorView().findViewById(android.R.id.content); + Reword.reword(rootView); + } + + @Override + protected void attachBaseContext(final Context newBase) { + super.attachBaseContext(ViewPumpContextWrapper.wrap(Restring.wrapContext(newBase))); + } + + @Override + public Resources getResources() { + return Restring.wrapContext(getBaseContext()).getResources(); } diff --git a/ui/src/main/java/com/getbubblenow/android/activity/LoginActivity.java b/ui/src/main/java/com/getbubblenow/android/activity/LoginActivity.java index fe2442a..3544861 100644 --- a/ui/src/main/java/com/getbubblenow/android/activity/LoginActivity.java +++ b/ui/src/main/java/com/getbubblenow/android/activity/LoginActivity.java @@ -169,7 +169,7 @@ public class LoginActivity extends BaseActivityBubble { if (statusResource.message.equals(NO_INTERNET_CONNECTION)) { showNetworkNotAvailableMessage(); } else if (statusResource.message.equals(LOGIN_FAILED)) { - Toast.makeText(this, LOGIN_FAILED, Toast.LENGTH_LONG).show(); + Toast.makeText(this, getString(R.string.login_failed), Toast.LENGTH_LONG).show(); } else { showErrorDialog(statusResource.message); } diff --git a/ui/src/main/java/com/getbubblenow/android/activity/MainActivity.java b/ui/src/main/java/com/getbubblenow/android/activity/MainActivity.java index 026d755..c16f506 100644 --- a/ui/src/main/java/com/getbubblenow/android/activity/MainActivity.java +++ b/ui/src/main/java/com/getbubblenow/android/activity/MainActivity.java @@ -30,6 +30,8 @@ import com.getbubblenow.android.util.UserStore; import com.getbubblenow.android.viewmodel.MainViewModel; import com.getbubblenow.android.R; +import java.util.Locale; + public class MainActivity extends BaseActivityBubble { private static final int PROGRESS_ANIMATION_DURATION = 2000; @@ -54,7 +56,6 @@ public class MainActivity extends BaseActivityBubble { private static final String NO_INTERNET_CONNECTION = "no internet connection"; private static final String LOGIN_FAILED = "Login Failed"; private static final int REQUEST_CODE = 1555; - private static final String CERTIFICATE_NAME = "Bubble Certificate"; private static final String SUPPORT_URL = "https://support.getbubblenow.com"; private static final String AUTH_TYPE_KEY = "authType"; @@ -89,18 +90,40 @@ public class MainActivity extends BaseActivityBubble { super.onCreate(savedInstanceState); mainViewModel = new ViewModelProvider(this).get(MainViewModel.class); showLoadingDialog(); + + final MutableLiveData data = (mainViewModel.isUserLoggedInToSage(this)) ? + mainViewModel.updatePostAuthLocalization(this, Locale.getDefault()) + : mainViewModel.updatePreAuthLocalization(this, Locale.getDefault()); + + data.observe(this, isLocalizationDataUpdated -> { + if (isLocalizationDataUpdated) { + Log.d(LOCALIZATION_TAG, "Success"); + } else { + Log.d(LOCALIZATION_TAG, "Fail"); + } + startBubbleSession(); + }); + } + + private void startBubbleSession() { if (mainViewModel.isHaveSageURL(this)) { + setContentView(R.layout.activity_main); + initUI(); if (mainViewModel.isHaveHostName(this)) { closeLoadingDialog(); - setContentView(R.layout.activity_main); mainViewModel.buildRepositoryInstance(this, mainViewModel.getSageURL(this)); - initUI(); logout.setEnabled(true); connectButton.setEnabled(true); connectButton.setVisibility(View.VISIBLE); + + if (mainViewModel.isVPNConnected(this, connectionStateFlag)) { + connectionStateFlag = false; + setConnectionStateUI(false); + } else { + connectionStateFlag = true; + setConnectionStateUI(true); + } } else { - setContentView(R.layout.activity_main); - initUI(); logout.setEnabled(true); if (mainViewModel.checkRepositoryInstance()) { mainViewModel.buildRepositoryInstance(this, mainViewModel.getSageURL(this)); @@ -109,8 +132,7 @@ public class MainActivity extends BaseActivityBubble { } checkBubbleCurrentStatus(); } - } - else{ + } else { closeLoadingDialog(); final Intent intent = new Intent(this, LoginActivity.class); startActivity(intent); @@ -119,35 +141,48 @@ public class MainActivity extends BaseActivityBubble { } private void checkBubbleCurrentStatus() { - mainViewModel.checkBubble(this); - mainViewModel.getBubbleCheckLiveData(this).observe(this, availableBubbleList -> { - mainViewModel.getBubbleCheckLiveData(this).removeObservers(this); + mainViewModel.checkBubbleOnce(this).observe(this, statusResource -> { closeLoadingDialog(); - if (availableBubbleList.isEmpty()) { - setNonAvailableBubbleUI(); - requestBubbleStatusContinuously(); - } else { - if (availableBubbleList.size() == 1) { - getCertificateData(availableBubbleList.get(0).getUuid()); - } else { - networkListDialogFragment = new NetworkListDialogFragment( - availableBubbleList, (view, bubble) -> { + switch (statusResource.status) { + case SUCCESS: + if (statusResource.data.isEmpty()) { + setNonAvailableBubbleUI(); + requestBubbleStatusContinuously(); + } else { + if (statusResource.data.size() == 1) { + getCertificateData(statusResource.data.get(0).getUuid()); + } else { + networkListDialogFragment = new NetworkListDialogFragment( + statusResource.data, (view, bubble) -> { getCertificateData(bubble.getUuid()); networkListDialogFragment.dismiss(); } - ); - networkListDialogFragment.show(getSupportFragmentManager(), "networkListDialog"); - } + ); + networkListDialogFragment.show(getSupportFragmentManager(), "networkListDialog"); + } + } + break; + case ERROR: + setNonAvailableBubbleUI(); + if (NO_INTERNET_CONNECTION.equals(statusResource.message)) { + showNetworkNotAvailableMessage(); + requestBubbleStatusContinuously(); + } else { + showErrorDialog(statusResource.message); + } + break; } }); } private void requestBubbleStatusContinuously() { - mainViewModel.getBubbleCheckLiveData(this).observe(this, availableBubbleList -> { - if (!availableBubbleList.isEmpty()) { - setConnectedBubbleUI(); - mainViewModel.getBubbleCheckLiveData(this).removeObservers(MainActivity.this); - getCertificateData(availableBubbleList.get(0).getUuid()); + mainViewModel.checkBubbleContinuously(this).observe(this, statusResource -> { + switch (statusResource.status) { + case SUCCESS: + if (!statusResource.data.isEmpty()) { + setConnectedBubbleUI(); + getCertificateData(statusResource.data.get(0).getUuid()); + } } }); } @@ -221,7 +256,7 @@ public class MainActivity extends BaseActivityBubble { } else { final Intent intent = KeyChain.createInstallIntent(); intent.putExtra(KeyChain.EXTRA_CERTIFICATE, certificateEncode); - intent.putExtra(KeyChain.EXTRA_NAME, CERTIFICATE_NAME); + intent.putExtra(KeyChain.EXTRA_NAME, getString(R.string.certificate_name)); closeLoadingDialog(); isNeedConnection = false; startActivityForResult(intent, REQUEST_CODE); @@ -244,21 +279,6 @@ public class MainActivity extends BaseActivityBubble { } } - @Override protected void onResume() { - super.onResume(); - if (mainViewModel.isHaveSageURL(this)) { - if (mainViewModel.isHaveHostName(this)) { - if (mainViewModel.isVPNConnected(this, connectionStateFlag)) { - connectionStateFlag = false; - setConnectionStateUI(false); - } else { - connectionStateFlag = true; - setConnectionStateUI(true); - } - } - } - } - private void initUI() { initViews(); initListeners(); @@ -351,7 +371,7 @@ public class MainActivity extends BaseActivityBubble { closeLoadingDialog(); final Intent intent = KeyChain.createInstallIntent(); intent.putExtra(KeyChain.EXTRA_CERTIFICATE, certificateEncode); - intent.putExtra(KeyChain.EXTRA_NAME, CERTIFICATE_NAME); + intent.putExtra(KeyChain.EXTRA_NAME, getString(R.string.certificate_name)); isNeedConnection = true; startActivityForResult(intent, REQUEST_CODE); } @@ -380,7 +400,7 @@ public class MainActivity extends BaseActivityBubble { if (NO_INTERNET_CONNECTION.equals(objectStatusResource.message)) { showNetworkNotAvailableMessage(); } else if (LOGIN_FAILED.equals(objectStatusResource.message)) { - Toast.makeText(this, LOGIN_FAILED, Toast.LENGTH_LONG).show(); + Toast.makeText(this, getString(R.string.login_failed), Toast.LENGTH_LONG).show(); } else { showErrorDialog(objectStatusResource.message); } @@ -409,7 +429,7 @@ public class MainActivity extends BaseActivityBubble { } else { logout.setEnabled(true); connectButton.setEnabled(true); - Toast.makeText(this, getString(R.string.cerificate_install), Toast.LENGTH_LONG).show(); + Toast.makeText(this, getString(R.string.certificate_install), Toast.LENGTH_LONG).show(); } } } @@ -439,7 +459,6 @@ public class MainActivity extends BaseActivityBubble { intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); mainViewModel.deleteTunnel(); mainViewModel.removeSharedPreferences(this); - mainViewModel.logout(this); startActivity(intent); } diff --git a/ui/src/main/java/com/getbubblenow/android/api/ApiConstants.java b/ui/src/main/java/com/getbubblenow/android/api/ApiConstants.java index 3932938..b67d23c 100644 --- a/ui/src/main/java/com/getbubblenow/android/api/ApiConstants.java +++ b/ui/src/main/java/com/getbubblenow/android/api/ApiConstants.java @@ -19,4 +19,5 @@ public class ApiConstants { public static final String DEVICE_TYPE = "deviceType"; public static final String CERTIFICATE_URL = "auth/cacert?deviceType=android"; public static final String NETWORK_STATUS = "users/{userId}/networks/{networkId}/actions/status"; + public static final String LOCALIZATION = "messages/{locale}/{bundle}"; } diff --git a/ui/src/main/java/com/getbubblenow/android/api/enums/LocalizationDataType.java b/ui/src/main/java/com/getbubblenow/android/api/enums/LocalizationDataType.java new file mode 100644 index 0000000..7cd517c --- /dev/null +++ b/ui/src/main/java/com/getbubblenow/android/api/enums/LocalizationDataType.java @@ -0,0 +1,9 @@ +package com.getbubblenow.android.api.enums; + +/** + * Enum for localization data type. It can be before user logged in and after. + **/ +public enum LocalizationDataType { + PRE_AUTH, + POST_AUTH +} \ No newline at end of file diff --git a/ui/src/main/java/com/getbubblenow/android/api/network/ClientApi.java b/ui/src/main/java/com/getbubblenow/android/api/network/ClientApi.java index 7489f0f..1396adb 100644 --- a/ui/src/main/java/com/getbubblenow/android/api/network/ClientApi.java +++ b/ui/src/main/java/com/getbubblenow/android/api/network/ClientApi.java @@ -9,6 +9,7 @@ import com.getbubblenow.android.api.ApiConstants; import java.util.HashMap; import java.util.List; +import java.util.Map; import io.reactivex.Single; import okhttp3.ResponseBody; @@ -51,4 +52,10 @@ public interface ClientApi { @GET(ApiConstants.NETWORK_STATUS) Single> getNetworkState(@Path("userId") String userId , @Path("networkId") String networkId , @HeaderMap HashMap header); + + @GET(ApiConstants.LOCALIZATION) + Single> getMessageBundle(@Path("locale") String locale, @Path("bundle") String bundle, @HeaderMap HashMap header); + + @GET(ApiConstants.LOCALIZATION) + Single> getMessageBundle(@Path("locale") String locale, @Path("bundle") String bundle); } diff --git a/ui/src/main/java/com/getbubblenow/android/repository/DataRepository.java b/ui/src/main/java/com/getbubblenow/android/repository/DataRepository.java index efb106b..1d23621 100644 --- a/ui/src/main/java/com/getbubblenow/android/repository/DataRepository.java +++ b/ui/src/main/java/com/getbubblenow/android/repository/DataRepository.java @@ -3,6 +3,7 @@ package com.getbubblenow.android.repository; import android.content.Context; import android.content.Intent; import android.os.Build; +import android.os.Handler; import android.provider.Settings; import android.provider.Settings.Secure; import android.util.Log; @@ -60,7 +61,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.Disposable; import io.reactivex.functions.Predicate; +import io.reactivex.observers.DisposableSingleObserver; import io.reactivex.schedulers.Schedulers; +import io.reactivex.subscribers.DisposableSubscriber; import okhttp3.Request; import okhttp3.ResponseBody; import okio.Buffer; @@ -69,8 +72,12 @@ import retrofit2.HttpException; import retrofit2.Response; public final class DataRepository { + + private static final String TAG = "DataRepository"; + private static final int DELAY_VALUE = 10; private static final int PROGRESS_DELAY_VALUE = 5; + private static final int NET_UNAVAILABLE_DELAY_VALUE = 10; private static final int HTTP_422_ERR_CODE = 422; private static final int HTTP_500_ERR_CODE = 500; private static final Long ZERO_PROGRESS = 0L; @@ -107,9 +114,6 @@ public final class DataRepository { private final MutableLiveData> networkStatusLiveData = new MutableLiveData<>(); private final MutableLiveData progressLiveData = new MutableLiveData<>(); - private final MutableLiveData> bubbleCheckLiveData = new MutableLiveData<>(); - private Disposable bubbleCheckDisposable; - private DataRepository(final Context context, final String url) { baseUrl = url; clientApi = ClientService.getInstance().createClientApi(url); @@ -127,15 +131,7 @@ public final class DataRepository { } } - public void logout(final Context context) { - if (bubbleCheckDisposable != null) { - bubbleCheckDisposable.dispose(); - bubbleCheckDisposable = null; - bubbleCheckLiveData.removeObservers((LifecycleOwner) context); - } - } - - public void buildClientService(final String url) { + public void buildClientService(String url) { baseUrl = url; clientApi = ClientService.getInstance().createClientApi(url); } @@ -144,32 +140,55 @@ public final class DataRepository { return instance; } - public MutableLiveData> getBubbleCheckLiveData() { - return bubbleCheckLiveData; + public MutableLiveData>> checkBubbleOnce(final Context context) { + final MutableLiveData>> data = new MutableLiveData<>(); + final HashMap header = new HashMap<>(); + header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken()); + clientApi.getNodeBaseURI(header) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new DisposableSingleObserver>() { + @Override public void onSuccess(final List networks) { + data.postValue(StatusResource.success(getAliveBubbles(networks))); + } + @Override public void onError(final Throwable throwable) { + setErrorMessage(throwable, data); + final String message = throwable.getMessage() == null ? "" : throwable.getMessage(); + Log.e(TAG, "Failed to acquire list of bubble networks from the remote server. " + message); + Log.e(TAG, Arrays.toString(throwable.getStackTrace())); + } + }); + return data; } - public void checkBubble(final Context context) { - - if (bubbleCheckDisposable != null) { - return; - } - + public MutableLiveData>> checkBubbleContinuously(final Context context) { + final MutableLiveData>> data = new MutableLiveData<>(); final HashMap header = new HashMap<>(); header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken()); - - bubbleCheckDisposable = clientApi.getNodeBaseURI(header) + clientApi.getNodeBaseURI(header) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .retry((attempts, error) -> attempts < 5 && error instanceof IOException) - .repeatWhen(completed -> completed.delay(DELAY_VALUE, TimeUnit.SECONDS)) - .subscribe(networks -> { - final List alive = getAliveBubbles(networks); - bubbleCheckLiveData.postValue(alive); - if (!alive.isEmpty()) { - bubbleCheckDisposable.dispose(); - bubbleCheckDisposable = null; + .repeatWhen(success -> success.delay(DELAY_VALUE, TimeUnit.SECONDS)) + .retryWhen(error -> error.delay(NET_UNAVAILABLE_DELAY_VALUE, TimeUnit.SECONDS)) + .subscribe(new DisposableSubscriber>() { + @Override public void onNext(final List networks) { + final List alive = getAliveBubbles(networks); + if (!alive.isEmpty()) { + data.postValue(StatusResource.success(alive)); + onComplete(); + } + } + + @Override public void onError(final Throwable throwable) { + Log.e(TAG, "Failed to acquire list of bubble networks from the remote server. " + + throwable.getMessage(), throwable); + } + + @Override public void onComplete() { + dispose(); } }); + return data; } private static List getAliveBubbles(final List networkList) { @@ -303,7 +322,7 @@ public final class DataRepository { if (message != null) { Log.e("ERR", message); } - setErrorMessage(throwable, liveData); + setErrorMessage(throwable, liveData.getMutableLiveData()); }); compositeDisposable.add(getNodeBaseURIDisposable); } @@ -333,7 +352,7 @@ public final class DataRepository { } }, throwable -> { Log.e("ERR", "getNetworkState"); - setErrorMessage(throwable, this); + setErrorMessage(throwable, this.getMutableLiveData()); }); compositeDisposable.add(getNetworkStateDisposable); } @@ -360,7 +379,7 @@ public final class DataRepository { } }, throwable -> { Log.d("ERR", "getNodeBaseURI"); - setErrorMessage(throwable, this); + setErrorMessage(throwable, this.getMutableLiveData()); }); compositeDisposable.add(getNodeBaseURIDisposable); } @@ -417,7 +436,7 @@ public final class DataRepository { } }, throwable -> { Log.e("ERR", "Bootstrap URL cannot be reached."); - setErrorMessage(throwable, this); + setErrorMessage(throwable, this.getMutableLiveData()); }); compositeDisposable.add(sagesDisposable); } @@ -438,7 +457,7 @@ public final class DataRepository { getNodeIndex(nodeIndex, username, password, context, this); }, throwable -> { Log.d("ERR", "getSages"); - setErrorMessage(throwable, this); + setErrorMessage(throwable, this.getMutableLiveData()); }); compositeDisposable.add(sagesDisposable); } @@ -543,7 +562,7 @@ public final class DataRepository { liveData.postMutableLiveData(StatusResource.auth(MFAuthType.TOTP)); } else { Log.d("ERR", "login"); - setErrorMessage(throwable, liveData); + setErrorMessage(throwable, liveData.getMutableLiveData()); } }); } @@ -572,7 +591,7 @@ public final class DataRepository { } }, throwable -> { Log.d("ERR", "getAllDevices"); - setErrorMessage(throwable, liveData); + setErrorMessage(throwable, liveData.getMutableLiveData()); }); compositeDisposable.add(disposableAllDevices); } @@ -654,7 +673,7 @@ public final class DataRepository { }); // getConfig(context); }, throwable -> { - setErrorMessage(throwable, liveData); + setErrorMessage(throwable, liveData.getMutableLiveData()); Log.d("ERR", "addDevice5"); // setMutableLiveData(StatusResource.error(throwable.getMessage())); }); @@ -711,7 +730,7 @@ public final class DataRepository { // getConfig(context); }, throwable -> { Log.d("ERR", "addDevice10"); - setErrorMessage(throwable, liveData); + setErrorMessage(throwable, liveData.getMutableLiveData()); // setMutableLiveData(StatusResource.error(throwable.getMessage())); }); compositeDisposable.add(disposableAddDevice); @@ -719,7 +738,7 @@ public final class DataRepository { } } }, throwable -> { - setErrorMessage(throwable, liveData); + setErrorMessage(throwable, liveData.getMutableLiveData()); Log.d("ERR", "getAllDevices2"); // setMutableLiveData(StatusResource.error(NO_INTERNET_CONNECTION)); }); @@ -748,7 +767,7 @@ public final class DataRepository { postMutableLiveData(StatusResource.success(data)); }, throwable -> { Log.d("ERR", "getConfig"); - setErrorMessage(throwable, this); + setErrorMessage(throwable, this.getMutableLiveData()); }); compositeDisposable.add(configDisposable); } @@ -772,12 +791,12 @@ public final class DataRepository { postMutableLiveData(StatusResource.success(null)); } else { Log.d("ERR", "createTunnel11"); - setErrorMessage(throwable, this); + setErrorMessage(throwable, this.getMutableLiveData()); } }); } catch (Exception e) { Log.d("ERR", "createTunnel12"); - setErrorMessage(e, this); + setErrorMessage(e, this.getMutableLiveData()); } } }.getMutableLiveData(); @@ -899,7 +918,7 @@ public final class DataRepository { x509Certificate = X509Certificate.getInstance(cert); } catch (final CertificateException e) { Log.d("ERR", "getCertificate12"); - setErrorMessage(e, this); + setErrorMessage(e, this.getMutableLiveData()); } try { if (x509Certificate != null) { @@ -908,11 +927,11 @@ public final class DataRepository { } } catch (final CertificateEncodingException e) { Log.d("ERR", "getCertificate13"); - setErrorMessage(e, this); + setErrorMessage(e, this.getMutableLiveData()); } }, throwable -> { Log.d("ERR", "getCertificate14"); - setErrorMessage(throwable, this); + setErrorMessage(throwable, this.getMutableLiveData()); }); compositeDisposable.add(certificateDisposable); } @@ -983,9 +1002,9 @@ public final class DataRepository { return UserStore.getInstance(context).getHostname(); } - private void setErrorMessage(Throwable throwable, NetworkBoundStatusResource liveData) { + private void setErrorMessage(Throwable throwable, MutableLiveData> liveData) { if (throwable instanceof IOException) { - liveData.postMutableLiveData(StatusResource.error(NO_INTERNET_CONNECTION)); + liveData.postValue(StatusResource.error(NO_INTERNET_CONNECTION)); } if (throwable instanceof HttpException) { if (((HttpException) throwable).code() == HTTP_500_ERR_CODE) { @@ -997,9 +1016,9 @@ public final class DataRepository { "BODY:" + requestBody + '\n' + "METHOD:" + requestMethod + '\n' + "STACK_TRACE:" + stackTrace; - liveData.postMutableLiveData(StatusResource.error(message)); + liveData.postValue(StatusResource.error(message)); } else { - liveData.postMutableLiveData(StatusResource.error(LOGIN_FAILED)); + liveData.postValue(StatusResource.error(LOGIN_FAILED)); } } } diff --git a/ui/src/main/java/com/getbubblenow/android/repository/MessageLocalLoader.java b/ui/src/main/java/com/getbubblenow/android/repository/MessageLocalLoader.java new file mode 100644 index 0000000..d4e43d2 --- /dev/null +++ b/ui/src/main/java/com/getbubblenow/android/repository/MessageLocalLoader.java @@ -0,0 +1,73 @@ +package com.getbubblenow.android.repository; + +import android.content.Context; +import android.util.Log; + +import com.getbubblenow.android.api.enums.LocalizationDataType; +import com.getbubblenow.android.util.UserStore; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; + +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; + +import androidx.lifecycle.MutableLiveData; +import io.reactivex.Completable; + +/*package*/ class MessageLocalLoader { + + private static final String TAG = "MessageLocalLoader"; + + /*package*/ MutableLiveData> updatePreAuth(final Context context, final Locale locale) { + return update(UserStore.getInstance(context).getLocalizationPre()); + } + + /*package*/ MutableLiveData> updatePostAuth(final Context context, final Locale locale) { + return update(UserStore.getInstance(context).getLocalizationPost()); + } + + private MutableLiveData> update(final String data) { + final MutableLiveData> mutableLiveData = new MutableLiveData<>(); + final Gson gson = new Gson(); + try { + final Map map = gson.fromJson(data, Map.class); + mutableLiveData.postValue(map); + } catch (final JsonSyntaxException ex) { + final String message = ex.getMessage() == null ? "" : ex.getMessage(); + Log.e(TAG, "Failed to deserialize localization data from the local storage. " + message); + Log.e(TAG, Arrays.toString(ex.getStackTrace())); + mutableLiveData.postValue(null); + } + return mutableLiveData; + } + + + /*package*/ Completable savePreAuth(final Map map, final Context context, final Locale locale) { + return Completable.fromRunnable(() -> saveToSharedPrefs(map, context, locale, LocalizationDataType.PRE_AUTH)); + } + + /*package*/ Completable savePostAuth(final Map map, final Context context, final Locale locale) { + return Completable.fromRunnable(() -> saveToSharedPrefs(map, context, locale, LocalizationDataType.POST_AUTH)); + } + + private void saveToSharedPrefs( + final Map map, + final Context context, + final Locale locale, + final LocalizationDataType localizationDataType) { + final Gson gson = new Gson(); + final String data = gson.toJson(map); + + switch (localizationDataType) { + case PRE_AUTH: + UserStore.getInstance(context).setLocalizationPre(data); + break; + case POST_AUTH: + UserStore.getInstance(context).setLocalizationPost(data); + break; + default: + throw new IllegalStateException("Message localization data type is not defined."); + } + } +} diff --git a/ui/src/main/java/com/getbubblenow/android/repository/MessageRemoteLoader.java b/ui/src/main/java/com/getbubblenow/android/repository/MessageRemoteLoader.java new file mode 100644 index 0000000..bd22996 --- /dev/null +++ b/ui/src/main/java/com/getbubblenow/android/repository/MessageRemoteLoader.java @@ -0,0 +1,100 @@ +package com.getbubblenow.android.repository; + +import android.content.Context; +import android.util.Log; + +import com.getbubblenow.android.api.ApiConstants; +import com.getbubblenow.android.api.network.ClientApi; +import com.getbubblenow.android.api.network.ClientService; +import com.getbubblenow.android.model.Sages; +import com.getbubblenow.android.util.UserStore; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import androidx.lifecycle.MutableLiveData; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.observers.DisposableSingleObserver; +import io.reactivex.schedulers.Schedulers; + +/*package*/ class MessageRemoteLoader { + + private static final String TAG = "MessageRemoteLoader"; + + private static final String PRE_AUTH_BUNDLE = "pre_auth"; + private static final String POST_AUTH_BUNDLE = "post_auth"; + private static final String API_SUFFIX = "/api/"; + + private ClientApi clientApi; + + /*package*/ MessageRemoteLoader() { + clientApi = ClientService.getInstance().createClientApi(ApiConstants.BOOTSTRAP_URL); + } + + /*package*/ MutableLiveData> updatePreAuth(final Context context, final Locale locale) { + final MutableLiveData> mutableLiveData = new MutableLiveData<>(); + clientApi.getSages() + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new DisposableSingleObserver() { + @Override public void onSuccess(final Sages sages) { + // just get the first available + final String url = sages.getSages().get(0) + API_SUFFIX; + update(getLocaleBundle(locale), url, null, mutableLiveData); + dispose(); + } + + @Override public void onError(final Throwable e) { + final String message = e.getMessage() == null ? "" : e.getMessage(); + Log.e(TAG, "Failed to connect to the remote server. " + message); + Log.e(TAG, Arrays.toString(e.getStackTrace())); + mutableLiveData.postValue(null); + dispose(); + } + }); + return mutableLiveData; + } + + /*package*/ MutableLiveData> updatePostAuth(final Context context, final Locale locale) { + final MutableLiveData> mutableLiveData = new MutableLiveData<>(); + final HashMap header = new HashMap<>(); + header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken()); + final String url = UserStore.getInstance(context).getSageURL(); + update(getLocaleBundle(locale), url, header, mutableLiveData); + return mutableLiveData; + } + + private void update( + final String localeBundle, + final String url, + final HashMap header , + final MutableLiveData> mutableLiveData) { + clientApi = ClientService.getInstance().createClientApi(url); + final Single> getMessageBundle = (header == null) ? clientApi.getMessageBundle(localeBundle, PRE_AUTH_BUNDLE) + : clientApi.getMessageBundle(localeBundle, POST_AUTH_BUNDLE, header); + getMessageBundle + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(new DisposableSingleObserver>() { + @Override public void onSuccess(final Map map) { + mutableLiveData.postValue(map); + dispose(); + } + + @Override public void onError(final Throwable e) { + final String message = e.getMessage() == null ? "" : e.getMessage(); + Log.e(TAG, "Failed to get localization data from the remote server. " + message); + Log.e(TAG, Arrays.toString(e.getStackTrace())); + mutableLiveData.postValue(null); + dispose(); + } + }); + } + + private String getLocaleBundle(final Locale locale) { + return locale.getLanguage() + '_' + locale.getCountry(); + } +} diff --git a/ui/src/main/java/com/getbubblenow/android/repository/RestringMessageRepository.java b/ui/src/main/java/com/getbubblenow/android/repository/RestringMessageRepository.java new file mode 100644 index 0000000..aec5a79 --- /dev/null +++ b/ui/src/main/java/com/getbubblenow/android/repository/RestringMessageRepository.java @@ -0,0 +1,247 @@ +package com.getbubblenow.android.repository; + +import android.content.Context; +import android.util.Log; + +import com.getbubblenow.android.api.enums.LocalizationDataType; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import dev.b3nedikt.restring.PluralKeyword; +import dev.b3nedikt.restring.StringRepository; +import io.reactivex.Completable; +import io.reactivex.observers.DisposableCompletableObserver; + + +public final class RestringMessageRepository implements StringRepository { + + private static final String PLURALS_PREFIX = "plurals_"; + private static final String PLURAL_ITEM_DELIMITER = "\\|"; + private static final String PLURAL_ITEM_KEY_VALUE_DELIMITER = "="; + private static final String ONE = "one"; + private static final String OTHER = "other"; + private static final String TAG = "RestringMessageRepository"; + + private static final List APP_LOCALES = Arrays.asList(Locale.ENGLISH, Locale.US); + + private static volatile RestringMessageRepository instance; + private final MessageRemoteLoader remoteLoader = new MessageRemoteLoader(); + private final MessageLocalLoader localLoader = new MessageLocalLoader(); + + private final Map strings = new HashMap<>(); + private final Map stringArrays = new HashMap<>(); + private final Map> quantityStrings = new HashMap<>(); + + + public static RestringMessageRepository getInstance() { + if (instance == null) { + synchronized (RestringMessageRepository.class) { + if (instance == null) { + instance = new RestringMessageRepository(); + } + } + } + return instance; + } + + public MutableLiveData updatePreAuthLocalization(final Context context, final Locale locale) { + return update(context, locale, LocalizationDataType.PRE_AUTH); + } + + public MutableLiveData updatePostAuthLocalization(final Context context, final Locale locale) { + return update(context, locale, LocalizationDataType.POST_AUTH); + } + + private MutableLiveData update( + final Context context, + final Locale locale, + final LocalizationDataType localizationDataType) { + final MutableLiveData mutableLiveData = new MutableLiveData<>(); + + final MutableLiveData> data; + switch (localizationDataType) { + case PRE_AUTH: + data = remoteLoader.updatePreAuth(context, locale); + break; + case POST_AUTH: + data = remoteLoader.updatePostAuth(context, locale); + break; + default: + throw new IllegalStateException("Message localization data type is not defined."); + } + + data.observe((LifecycleOwner) context, map -> { + if (map == null || map.isEmpty()) { + updateFromLocal(context, locale, localizationDataType, mutableLiveData); + } else { + processStrings(map, locale); + + saveToLocal(map, context, locale, localizationDataType).subscribe(new DisposableCompletableObserver() { + @Override + public void onComplete() { + Log.d(TAG, "Data is saved to the local storage."); + } + @Override + public void onError(final Throwable e) { + final String message = e.getMessage() == null ? "" : e.getMessage(); + Log.e(TAG, "Failed to save data to the local storage. " + message); + Log.e(TAG, Arrays.toString(e.getStackTrace())); + } + }); + + mutableLiveData.postValue(true); + } + }); + return mutableLiveData; + } + + private void updateFromLocal( + final Context context, + final Locale locale, + final LocalizationDataType localizationDataType, + final MutableLiveData mutableLiveData) { + + final MutableLiveData> data; + switch (localizationDataType) { + case PRE_AUTH: + data = localLoader.updatePreAuth(context, locale); + break; + case POST_AUTH: + data = localLoader.updatePostAuth(context, locale); + break; + default: + throw new IllegalStateException("Message localization data type is not defined."); + } + + data.observe((LifecycleOwner) context, map -> { + if (map == null || map.isEmpty()) { + mutableLiveData.postValue(false); + } else { + processStrings(map, locale); + mutableLiveData.postValue(true); + } + }); + } + + private Completable saveToLocal( + final Map map, + final Context context, + final Locale locale, + final LocalizationDataType localizationDataType) { + final Completable saveCompletable; + switch (localizationDataType) { + case PRE_AUTH: + saveCompletable = localLoader.savePreAuth(map, context, locale); + break; + case POST_AUTH: + saveCompletable = localLoader.savePostAuth(map, context, locale); + break; + default: + throw new IllegalStateException("Message localization data type is not defined."); + } + return saveCompletable; + } + + private void processStrings(final Map map, final Locale locale) { + for (final Entry entry : map.entrySet()) { + final String key = entry.getKey(); + if (key.startsWith(PLURALS_PREFIX)) { + processEntryValue(key.substring(PLURALS_PREFIX.length()), entry.getValue(), locale); + } else { + setString(locale, key, entry.getValue()); + } + } + } + + private void processEntryValue(final String key, final String value, final Locale locale) { + final Map map = new HashMap<>(); + final String[] pluralItems = value.split(PLURAL_ITEM_DELIMITER); + + for (final String pluralItem : pluralItems) { + final int idx = pluralItem.indexOf(PLURAL_ITEM_KEY_VALUE_DELIMITER); + final String pluralKey = pluralItem.substring(0, idx); + final String pluralValue = pluralItem.substring(idx + 1); + + switch (pluralKey) { + case ONE: + map.put(PluralKeyword.ONE, pluralValue); + break; + case OTHER: + map.put(PluralKeyword.OTHER, pluralValue); + break; + } + } + setQuantityString(locale, key, map); + } + + + @Nullable @Override public Map getQuantityString(@NotNull final Locale locale, @NotNull final String s) { + return quantityStrings.get(s); + } + + @NotNull @Override public Map> getQuantityStrings(@NotNull final Locale locale) { + return quantityStrings; + } + + @Nullable @Override public CharSequence getString(@NotNull final Locale locale, @NotNull final String s) { + return strings.get(s); + } + + @Nullable @Override public CharSequence[] getStringArray(@NotNull final Locale locale, @NotNull final String s) { + return stringArrays.get(s); + } + + @NotNull @Override public Map getStringArrays(@NotNull final Locale locale) { + return stringArrays; + } + + @NotNull @Override public Map getStrings(@NotNull final Locale locale) { + return strings; + } + + @NotNull @Override public Set getSupportedLocales() { + return new HashSet<>(APP_LOCALES); + } + + @Override public void setQuantityString(@NotNull final Locale locale, @NotNull final String s, @NotNull final Map map) { + quantityStrings.put(s, (Map) map); + } + + @Override public void setQuantityStrings(@NotNull final Locale locale, @NotNull final Map> map) { + for (final Entry> entry : map.entrySet()) { + quantityStrings.put(entry.getKey(), (Map) entry.getValue()); + } + } + + @Override public void setString(@NotNull final Locale locale, @NotNull final String s, @NotNull final CharSequence charSequence) { + strings.put(s, charSequence); + } + + @Override public void setStringArray(@NotNull final Locale locale, @NotNull final String s, @NotNull final CharSequence[] charSequences) { + stringArrays.put(s, charSequences); + } + + @Override public void setStringArrays(@NotNull final Locale locale, @NotNull final Map map) { + for (final Entry entry : map.entrySet()) { + stringArrays.put(entry.getKey(), entry.getValue()); + } + } + + @Override public void setStrings(@NotNull final Locale locale, @NotNull final Map map) { + for (final Entry entry : map.entrySet()) { + strings.put(entry.getKey(), entry.getValue()); + } + } +} diff --git a/ui/src/main/java/com/getbubblenow/android/util/UserStore.java b/ui/src/main/java/com/getbubblenow/android/util/UserStore.java index 80d8f82..4af1abc 100644 --- a/ui/src/main/java/com/getbubblenow/android/util/UserStore.java +++ b/ui/src/main/java/com/getbubblenow/android/util/UserStore.java @@ -3,9 +3,9 @@ package com.getbubblenow.android.util; import android.content.Context; import android.content.SharedPreferences; -public class UserStore { +public final class UserStore { private static UserStore instance; - private SharedPreferences sharedPreferences; + private final SharedPreferences sharedPreferences; public static final String USER_SHARED_PREF = "com.wireguard.android.util.bubbleUserSharedPref"; private static final String USER_DATA_KEY = "com.wireguard.android.util.bubbleUserResponse"; @@ -20,21 +20,25 @@ public class UserStore { private static final String SAGE_KEY = "com.wireguard.android.util.bubbleSageResponse"; private static final String HOSTNAME_FOR_ACCOUNT_KEY = "com.wireguard.android.util.bubbleHostNameForAccountResponse"; private static final String MFA_TOKEN_KEY = "MFA_TOKEN_KEY"; + private static final String LOCALIZATION_PRE_KEY = "LOCALIZATION_PRE_KEY"; + private static final String LOCALIZATION_POST_KEY = "LOCALIZATION_POST_KEY"; - public static final String USER_TOKEN_DEFAULT_VALUE = ""; + public static final String USER_TOKEN_DEFAULT_VALUE = ""; public static final String DEVICE_DEFAULT_VALUE = ""; - public static final String DEVICE_ID_DEFAULT_VALUE = ""; + private static final String DEVICE_ID_DEFAULT_VALUE = ""; private static final String HOSTNAME_DEFAULT_VALUE = ""; - public static final String USERNAME_DEFAULT_VALUE = ""; + private static final String USERNAME_DEFAULT_VALUE = ""; private static final String PASSWORD_DEFAULT_VALUE = ""; - private static final String SAGE_TOKEN_DEFAULT_VALUE = ""; + public static final String SAGE_TOKEN_DEFAULT_VALUE = ""; private static final String CONFIG_DEFAULT_VALUE = ""; private static final String SAGE_HOSTNAME_DEFAULT_VALUE = ""; private static final String SAGE_DEFAULT_VALUE = ""; private static final String HOSTNAME_FOR_ACCOUNT_DEFAULT_VALUE = ""; private static final String MFA_TOKEN_DEFAULT_VALUE = ""; + private static final String LOCALIZATION_PRE_DEFAULT_VALUE = ""; + private static final String LOCALIZATION_POST_DEFAULT_VALUE = ""; - public static UserStore getInstance(Context context) { + public static UserStore getInstance(final Context context) { if (instance == null) { synchronized (UserStore.class) { if (instance == null) { @@ -42,15 +46,14 @@ public class UserStore { } } } - return instance; } - private UserStore(Context context) { + private UserStore(final Context context) { sharedPreferences = context.getSharedPreferences(USER_SHARED_PREF, Context.MODE_PRIVATE); } - public void setToken(String response) { + public void setToken(final String response) { sharedPreferences.edit().putString(USER_DATA_KEY, response).apply(); } @@ -58,25 +61,25 @@ public class UserStore { return sharedPreferences.getString(USER_DATA_KEY, USER_TOKEN_DEFAULT_VALUE); } - public void setDevice(String deviceName, String deviceID){ + public void setDevice(final String deviceName, final String deviceID) { sharedPreferences.edit().putString(DEVICE_DATA_KEY, deviceName).apply(); - sharedPreferences.edit().putString(DEVICE_ID_KEY,deviceID).apply(); + sharedPreferences.edit().putString(DEVICE_ID_KEY, deviceID).apply(); } - public String getDeviceName(){ + public String getDeviceName() { return sharedPreferences.getString(DEVICE_DATA_KEY, DEVICE_DEFAULT_VALUE); } - public String getDeviceID(){ - return sharedPreferences.getString(DEVICE_ID_KEY,DEVICE_ID_DEFAULT_VALUE); + public String getDeviceID() { + return sharedPreferences.getString(DEVICE_ID_KEY, DEVICE_ID_DEFAULT_VALUE); } - public void setHostname(String hostName){ - sharedPreferences.edit().putString(HOSTNAME_KEY,hostName).apply(); + public void setHostname(final String hostName) { + sharedPreferences.edit().putString(HOSTNAME_KEY, hostName).apply(); } - public String getHostname(){ - return sharedPreferences.getString(HOSTNAME_KEY,HOSTNAME_DEFAULT_VALUE); + public String getHostname() { + return sharedPreferences.getString(HOSTNAME_KEY, HOSTNAME_DEFAULT_VALUE); } public void setMfaToken(final String mfaToken) { @@ -87,56 +90,72 @@ public class UserStore { return sharedPreferences.getString(MFA_TOKEN_KEY, MFA_TOKEN_DEFAULT_VALUE); } - public void setUserData(String username , String password){ + public void setUserData(final String username, final String password) { sharedPreferences.edit().putString(USERNAME_KEY, username).apply(); - sharedPreferences.edit().putString(PASSWORD_KEY,password).apply(); + sharedPreferences.edit().putString(PASSWORD_KEY, password).apply(); } - public String getUsername(){ - return sharedPreferences.getString(USERNAME_KEY,USERNAME_DEFAULT_VALUE); + public String getUsername() { + return sharedPreferences.getString(USERNAME_KEY, USERNAME_DEFAULT_VALUE); } - public String getPassword(){ - return sharedPreferences.getString(PASSWORD_KEY,PASSWORD_DEFAULT_VALUE); + public String getPassword() { + return sharedPreferences.getString(PASSWORD_KEY, PASSWORD_DEFAULT_VALUE); } - public void setSageURL(String sage){ + public void setSageURL(final String sage) { sharedPreferences.edit().putString(SAGE_KEY, sage).apply(); } - public String getSageURL(){ - return sharedPreferences.getString(SAGE_KEY,SAGE_DEFAULT_VALUE); + public String getSageURL() { + return sharedPreferences.getString(SAGE_KEY, SAGE_DEFAULT_VALUE); } - public void setConfig(String config){ + public void setConfig(final String config) { sharedPreferences.edit().putString(CONFIG_KEY, config).apply(); } - public String getConfig(){ - return sharedPreferences.getString(CONFIG_KEY,CONFIG_DEFAULT_VALUE); + public String getConfig() { + return sharedPreferences.getString(CONFIG_KEY, CONFIG_DEFAULT_VALUE); } - public void setSageHostname(String sageHostname){ + public void setSageHostname(final String sageHostname) { sharedPreferences.edit().putString(SAGE_HOSTNAME_KEY, sageHostname).apply(); } - public String getSageHostname(){ - return sharedPreferences.getString(SAGE_HOSTNAME_KEY,SAGE_HOSTNAME_DEFAULT_VALUE); + public String getSageHostname() { + return sharedPreferences.getString(SAGE_HOSTNAME_KEY, SAGE_HOSTNAME_DEFAULT_VALUE); } - public void setSageToken(String sageToken){ + public void setSageToken(final String sageToken) { sharedPreferences.edit().putString(SAGE_TOKEN_KEY, sageToken).apply(); } - public String getSageToken(){ - return sharedPreferences.getString(SAGE_TOKEN_KEY,SAGE_TOKEN_DEFAULT_VALUE); + public String getSageToken() { + return sharedPreferences.getString(SAGE_TOKEN_KEY, SAGE_TOKEN_DEFAULT_VALUE); } - public void setHostNameForAccount(String hostNameForAccount){ + public void setHostNameForAccount(final String hostNameForAccount) { sharedPreferences.edit().putString(HOSTNAME_FOR_ACCOUNT_KEY, hostNameForAccount).apply(); } - public String getHostNameForAccount(){ - return sharedPreferences.getString(HOSTNAME_FOR_ACCOUNT_KEY,HOSTNAME_FOR_ACCOUNT_DEFAULT_VALUE); + public String getHostNameForAccount() { + return sharedPreferences.getString(HOSTNAME_FOR_ACCOUNT_KEY, HOSTNAME_FOR_ACCOUNT_DEFAULT_VALUE); + } + + public void setLocalizationPre(final String data) { + sharedPreferences.edit().putString(LOCALIZATION_PRE_KEY, data).apply(); + } + + public String getLocalizationPre() { + return sharedPreferences.getString(LOCALIZATION_PRE_KEY, LOCALIZATION_PRE_DEFAULT_VALUE); + } + + public void setLocalizationPost(final String data) { + sharedPreferences.edit().putString(LOCALIZATION_POST_KEY, data).apply(); + } + + public String getLocalizationPost() { + return sharedPreferences.getString(LOCALIZATION_POST_KEY, LOCALIZATION_POST_DEFAULT_VALUE); } } diff --git a/ui/src/main/java/com/getbubblenow/android/viewmodel/MFAVerifyViewModel.java b/ui/src/main/java/com/getbubblenow/android/viewmodel/MFAVerifyViewModel.java index db156de..0e3df00 100644 --- a/ui/src/main/java/com/getbubblenow/android/viewmodel/MFAVerifyViewModel.java +++ b/ui/src/main/java/com/getbubblenow/android/viewmodel/MFAVerifyViewModel.java @@ -8,7 +8,7 @@ import com.getbubblenow.android.resource.StatusResource; import androidx.lifecycle.MutableLiveData; -public class MFAVerifyViewModel extends BaseViewModel { +public class MFAVerifyViewModel extends BaseViewModel { private MFAuthType authType = MFAuthType.NONE; @@ -25,4 +25,4 @@ public class MFAVerifyViewModel extends BaseViewModel { public MFAuthType getAuthType() { return authType; } -} \ No newline at end of file +} diff --git a/ui/src/main/java/com/getbubblenow/android/viewmodel/MainViewModel.java b/ui/src/main/java/com/getbubblenow/android/viewmodel/MainViewModel.java index 0415cfa..81654ef 100644 --- a/ui/src/main/java/com/getbubblenow/android/viewmodel/MainViewModel.java +++ b/ui/src/main/java/com/getbubblenow/android/viewmodel/MainViewModel.java @@ -5,14 +5,30 @@ import android.content.Context; import com.getbubblenow.android.model.Network; import com.getbubblenow.android.repository.DataRepository; import com.getbubblenow.android.model.ObservableTunnel; +import com.getbubblenow.android.repository.RestringMessageRepository; import com.getbubblenow.android.resource.StatusResource; import com.getbubblenow.android.util.UserStore; +import java.util.Locale; + import java.util.List; import androidx.lifecycle.MutableLiveData; public class MainViewModel extends BaseViewModel { + + public boolean isUserLoggedInToSage(final Context context) { + return !UserStore.SAGE_TOKEN_DEFAULT_VALUE.equals(UserStore.getInstance(context).getSageToken()); + } + + public MutableLiveData updatePreAuthLocalization(final Context context, final Locale locale) { + return RestringMessageRepository.getInstance().updatePreAuthLocalization(context, locale); + } + + public MutableLiveData updatePostAuthLocalization(final Context context, final Locale locale) { + return RestringMessageRepository.getInstance().updatePostAuthLocalization(context, locale); + } + public boolean isUserLoggedIn(Context context){ return !UserStore.USER_TOKEN_DEFAULT_VALUE.equals(UserStore.getInstance(context).getToken()); } @@ -37,10 +53,6 @@ public class MainViewModel extends BaseViewModel { return DataRepository.getRepositoryInstance().getHostname(context); } - public void logout(final Context context) { - DataRepository.getRepositoryInstance().logout(context); - } - public void removeSharedPreferences(Context context){ DataRepository.getRepositoryInstance().removeSharedPreferences(context); } @@ -81,12 +93,12 @@ public class MainViewModel extends BaseViewModel { UserStore.getInstance(context).setConfig(config); } - public void checkBubble(final Context context) { - DataRepository.getRepositoryInstance().checkBubble(context); + public MutableLiveData>> checkBubbleOnce(final Context context) { + return DataRepository.getRepositoryInstance().checkBubbleOnce(context); } - public MutableLiveData> getBubbleCheckLiveData(final Context context) { - return DataRepository.getRepositoryInstance().getBubbleCheckLiveData(); + public MutableLiveData>> checkBubbleContinuously(final Context context) { + return DataRepository.getRepositoryInstance().checkBubbleContinuously(context); } public MutableLiveData> getCertificateData(final Context context, final String uuid) { diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index d3753c2..c80e0a3 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -144,7 +144,7 @@ The experimental kernel module can improve performance No modules are available for your device Download and install kernel module - Downloading and installing… + Downloading and installing... Unable to determine kernel module version MTU Turning on one tunnel will turn off others @@ -178,7 +178,7 @@ Shell cannot read exit status Shell expected 4 markers, received %d Shell failed to start: %d - Success. The application will now restart… + Success. The application will now restart... Toggle All Error toggling WireGuard tunnel: %s wg and wg-quick are already installed @@ -226,44 +226,46 @@ Authenticate to view private key Authentication failure Authentication failure: %s + Bubble Name: User Name: - Password: - Sign In Don\'t have a Bubble? Start New Bubble + This Device Status: + Disable Apps + Please turn on internet connection + Loading + Hostname not valid + You can find your Bubble name in your email + Enter your Bubble Name + + Password: + Sign In Bubble Connection MY BUBBLE Running Starting Restoring - This Device Status: Not Connected CONNECT - Disable Apps - Please turn on internet connection - Loading Not Connected ... Connected! Failed DISCONNECT - Hostname not valid Login Failed - "Please install a certificate" + Please install a certificate Success Your password Email you used to Sign Up Email: - You can find your Bubble name in your email - Enter your Bubble Name You are safe in your Bubble! Sorry, friends. Network not currently available. Sadface. We have \n encountered \n an error. CONTACT SUPPORT Log out Bubble - "Email and password should not be empty" + Email and password should not be empty We Are Building Your Private Bubble! No account yet? Create one. @@ -275,4 +277,5 @@ Enter Verification Code Enter 6-digit Code Invalid MFA token format + Bubble Certificate