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