diff --git a/ui/src/main/AndroidManifest.xml b/ui/src/main/AndroidManifest.xml index cac7954..0e89e16 100644 --- a/ui/src/main/AndroidManifest.xml +++ b/ui/src/main/AndroidManifest.xml @@ -46,6 +46,8 @@ + 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 a7a34f4..84fca07 100644 --- a/ui/src/main/java/com/getbubblenow/android/activity/LoginActivity.java +++ b/ui/src/main/java/com/getbubblenow/android/activity/LoginActivity.java @@ -37,6 +37,7 @@ public class LoginActivity extends BaseActivityBubble { private EditText password; private AppCompatButton sign; private TextView signUpButton; + private TextView setLauncherButton; private String resource; private boolean userNameStateFlag; @@ -69,6 +70,12 @@ public class LoginActivity extends BaseActivityBubble { passwordStateListener(); sendListener(); signUpButton.setOnClickListener(v -> signUp()); + setLauncherButton.setOnClickListener(v -> setLauncher()); + } + + private void setLauncher() { + final Intent intent = new Intent(this, SetLauncherActivity.class); + startActivity(intent); } private void signUp() { @@ -106,6 +113,7 @@ public class LoginActivity extends BaseActivityBubble { password = findViewById(R.id.password); sign = findViewById(R.id.signButton); signUpButton = findViewById(R.id.signUpButton); + setLauncherButton = findViewById(R.id.setLauncherButton); } 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 b8538af..f6a643d 100644 --- a/ui/src/main/java/com/getbubblenow/android/activity/MainActivity.java +++ b/ui/src/main/java/com/getbubblenow/android/activity/MainActivity.java @@ -103,6 +103,11 @@ public class MainActivity extends BaseActivityBubble { }); } + @Override protected void onStop() { + super.onStop(); + mainViewModel.disposeProgress(); + } + private void startBubbleSession() { if (mainViewModel.isHaveSageURL(this)) { if (mainViewModel.isHaveHostName(this)) { @@ -463,6 +468,7 @@ public class MainActivity extends BaseActivityBubble { mainViewModel.deleteTunnel(); mainViewModel.removeSharedPreferences(this); mainViewModel.clearCredentials(getApplicationContext()); + mainViewModel.disposeBubbleCheck(); startActivity(intent); } diff --git a/ui/src/main/java/com/getbubblenow/android/activity/SetLauncherActivity.java b/ui/src/main/java/com/getbubblenow/android/activity/SetLauncherActivity.java new file mode 100644 index 0000000..c125c19 --- /dev/null +++ b/ui/src/main/java/com/getbubblenow/android/activity/SetLauncherActivity.java @@ -0,0 +1,98 @@ +package com.getbubblenow.android.activity; + +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.widget.EditText; + +import com.getbubblenow.android.R; +import com.getbubblenow.android.viewmodel.SetLauncherViewModel; +import com.jakewharton.rxbinding4.widget.RxTextView; + +import androidx.appcompat.widget.AppCompatButton; +import androidx.lifecycle.ViewModelProvider; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; + + +public class SetLauncherActivity extends BaseActivityBubble { + + private static final String PREFIX = "https://"; + + private SetLauncherViewModel setLauncherViewModel; + private EditText launcherUrl; + private AppCompatButton setLauncherButton; + + private final CompositeDisposable compositeSubscription = new CompositeDisposable(); + + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login_sage); + initUI(); + setLauncherViewModel = new ViewModelProvider(this).get(SetLauncherViewModel.class); + } + + @Override protected void onDestroy() { + super.onDestroy(); + compositeSubscription.dispose(); + } + + private void initUI() { + initViews(); + initListeners(); + } + + private void initListeners() { + setLauncherButton.setOnClickListener(v -> setLauncher()); + sendListener(); + launcherUrlStateListener(); + } + + private void initViews() { + launcherUrl = findViewById(R.id.launcher_url); + setLauncherButton = findViewById(R.id.setLauncherButton); + } + + private void sendListener() { + launcherUrl.setOnEditorActionListener((v, actionId, event) -> { + if ((event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) + || (actionId == EditorInfo.IME_ACTION_SEND)) { + setLauncher(); + return true; + } + return false; + }); + } + + private void setLauncher() { + String launcherInput = launcherUrl.getText().toString().trim(); + + if (!launcherInput.isEmpty()) { + if (!launcherInput.startsWith(PREFIX)) { + launcherInput = PREFIX + launcherInput; + } + } + setLauncherViewModel.setSageHost(this, launcherInput); + finish(); + } + + private void launcherUrlStateListener() { + final Disposable subscribe = RxTextView + .afterTextChangeEvents(launcherUrl) + .subscribe(view -> setButtonState(!launcherUrl.getText().toString().trim().isEmpty())); + compositeSubscription.add(subscribe); + } + + private void setButtonState(final boolean launcherUrlStateFlag) { + if (launcherUrlStateFlag) { + setLauncherButton.setBackgroundDrawable(getDrawable(R.drawable.sign_in_enable)); + setLauncherButton.setEnabled(true); + } else { + setLauncherButton.setBackgroundDrawable(getDrawable(R.drawable.sign_in_disable)); + setLauncherButton.setEnabled(false); + } + } + +} 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 1d196e6..5189f82 100644 --- a/ui/src/main/java/com/getbubblenow/android/repository/DataRepository.java +++ b/ui/src/main/java/com/getbubblenow/android/repository/DataRepository.java @@ -11,12 +11,10 @@ import android.widget.Toast; import com.getbubblenow.android.Application; import com.getbubblenow.android.R; import com.getbubblenow.android.api.enums.MFAuthType; -import com.getbubblenow.android.configStore.FileConfigStore; import com.getbubblenow.android.model.LoginErrBody; import com.getbubblenow.android.model.Network; import com.getbubblenow.android.model.NetworkStatus; import com.getbubblenow.android.model.ObservableTunnel; -import com.getbubblenow.android.model.TunnelManager; import com.getbubblenow.android.activity.MainActivity; import com.getbubblenow.android.api.ApiConstants; import com.getbubblenow.android.api.network.ClientApi; @@ -39,6 +37,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -56,6 +55,7 @@ import io.reactivex.observers.DisposableSingleObserver; import io.reactivex.schedulers.Schedulers; import io.reactivex.subscribers.DisposableSubscriber; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.ResponseBody; import okio.Buffer; import retrofit2.Call; @@ -97,6 +97,8 @@ public final class DataRepository { private static volatile DataRepository instance; private String baseUrl = ""; + private Disposable bubbleCheckDisposable; + private Disposable progressDisposable; private String deviceName; private String deviceID; @@ -135,6 +137,24 @@ public final class DataRepository { return instance; } + public void disposeBubbleCheck() { + if (bubbleCheckDisposable == null) { + return; + } + if (!bubbleCheckDisposable.isDisposed()) { + bubbleCheckDisposable.dispose(); + } + } + + public void disposeProgress() { + if (progressDisposable == null) { + return; + } + if (!progressDisposable.isDisposed()) { + progressDisposable.dispose(); + } + } + public MutableLiveData>> checkBubbleOnce(final Context context) { final MutableLiveData>> data = new MutableLiveData<>(); final HashMap header = new HashMap<>(); @@ -161,12 +181,13 @@ public final class DataRepository { final MutableLiveData>> data = new MutableLiveData<>(); final HashMap header = new HashMap<>(); header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken()); - clientApi.getNodeBaseURI(header) + bubbleCheckDisposable = clientApi.getNodeBaseURI(header) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .repeatWhen(success -> success.delay(DELAY_VALUE, TimeUnit.SECONDS)) .retryWhen(error -> error.delay(NET_UNAVAILABLE_DELAY_VALUE, TimeUnit.SECONDS)) - .subscribe(new DisposableSubscriber>() { + .subscribeWith(new DisposableSubscriber>() { + @Override public void onNext(final List networks) { final List alive = getAliveBubbles(networks); if (!alive.isEmpty()) { @@ -366,7 +387,7 @@ public final class DataRepository { final HashMap header = new HashMap<>(); header.put(ApiConstants.AUTHORIZATION_HEADER, UserStore.getInstance(context).getSageToken()); - final Disposable getNetworkStateDisposable = clientApi.getNetworkState( + progressDisposable = clientApi.getNetworkState( CredentialRepository.getInstance(context).getUsername(), network.getUuid(), header ) .subscribeOn(Schedulers.newThread()) @@ -393,7 +414,6 @@ public final class DataRepository { stopNetworkStatusLiveData = true; setErrorMessage(throwable, getMutableLiveData()); }); - compositeDisposable.add(getNetworkStateDisposable); } private void loginToNetworkWithCheck() { @@ -492,6 +512,13 @@ public final class DataRepository { return new NetworkBoundStatusResource() { @Override protected void createCall() { + final String sageHostName = UserStore.getInstance(context).getSageHostname(); + if (!sageHostName.isEmpty()) { + nodes = Collections.singletonList(sageHostName); + getNodeIndex(0, username, password, context, this); + return; + } + final Disposable sagesDisposable = clientApi.getSages() .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) @@ -985,6 +1012,7 @@ public final class DataRepository { liveData.postValue(StatusResource.error(LOGIN_FAILED)); } } else { + Log.e("ERR", "Unhandled http error.", throwable); liveData.postValue(StatusResource.error(LOGIN_FAILED)); } } @@ -995,27 +1023,29 @@ public final class DataRepository { try { final Request copy = request.newBuilder().build(); final Buffer buffer = new Buffer(); - copy.body().writeTo(buffer); + final RequestBody body = copy.body(); + if (body != null) { + body.writeTo(buffer); + } return buffer.readUtf8(); } catch (final IOException e) { return "did not work"; } } - public void removeSharedPreferences(Context context) { + public void removeSharedPreferences(final Context context) { context.getSharedPreferences(UserStore.USER_SHARED_PREF, 0).edit().clear().apply(); context.getSharedPreferences(TunnelStore.TUNNEL_SHARED_PREF, 0).edit().clear().apply(); } public void deleteTunnel() { - ArrayList tunnels = new ArrayList<>(); - if(pendingTunnel != null) { - tunnels.add(pendingTunnel); + if (pendingTunnel != null) { Application.getTunnelManager().delete(pendingTunnel); + pendingTunnel = null; } } - public MutableLiveData> isHaveTunnel(Context context) { + public MutableLiveData> isHaveTunnel(final Context context) { return new NetworkBoundStatusResource(){ @Override protected void createCall() { final byte[] configBytes = UserStore.getInstance(context).getConfig().getBytes(); 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 a334689..6c757d9 100644 --- a/ui/src/main/java/com/getbubblenow/android/viewmodel/MainViewModel.java +++ b/ui/src/main/java/com/getbubblenow/android/viewmodel/MainViewModel.java @@ -5,7 +5,6 @@ import android.content.Context; import com.getbubblenow.android.model.Network; import com.getbubblenow.android.repository.CredentialRepository; 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; @@ -22,6 +21,14 @@ public class MainViewModel extends BaseViewModel { CredentialRepository.getInstance(context).clear(); } + public void disposeBubbleCheck() { + DataRepository.getRepositoryInstance().disposeBubbleCheck(); + } + + public void disposeProgress() { + DataRepository.getRepositoryInstance().disposeProgress(); + } + public boolean isUserLoggedInToSage(final Context context) { return !UserStore.SAGE_TOKEN_DEFAULT_VALUE.equals(UserStore.getInstance(context).getSageToken()); } diff --git a/ui/src/main/java/com/getbubblenow/android/viewmodel/SetLauncherViewModel.java b/ui/src/main/java/com/getbubblenow/android/viewmodel/SetLauncherViewModel.java new file mode 100644 index 0000000..92d9d0f --- /dev/null +++ b/ui/src/main/java/com/getbubblenow/android/viewmodel/SetLauncherViewModel.java @@ -0,0 +1,13 @@ +package com.getbubblenow.android.viewmodel; + +import android.content.Context; + +import com.getbubblenow.android.util.UserStore; + +public class SetLauncherViewModel extends BaseViewModel { + + public void setSageHost(final Context context, final String sageHost) { + UserStore.getInstance(context).setSageHostname(sageHost); + } + +} diff --git a/ui/src/main/res/layout/activity_login.xml b/ui/src/main/res/layout/activity_login.xml index f63b604..2555397 100644 --- a/ui/src/main/res/layout/activity_login.xml +++ b/ui/src/main/res/layout/activity_login.xml @@ -177,5 +177,17 @@ android:textStyle="normal" android:clickable="true" android:focusable="true" /> + + diff --git a/ui/src/main/res/layout/activity_login_sage.xml b/ui/src/main/res/layout/activity_login_sage.xml new file mode 100644 index 0000000..61550d8 --- /dev/null +++ b/ui/src/main/res/layout/activity_login_sage.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index 0f8ef7b..9ce6ee0 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -269,6 +269,7 @@ We Are Building Your Private Bubble! No account yet? Create one. + Running your own Bubble? Set Launcher. LAUNCH NEW BUBBLE Select your Bubble OK @@ -277,4 +278,6 @@ Enter Verification Code Enter 6-digit Code Invalid MFA token format + Set launcher + Launcher URL to Sign In