@@ -46,6 +46,8 @@ | |||
</activity> | |||
<activity android:name=".activity.LoginActivity" | |||
android:screenOrientation="portrait"/> | |||
<activity android:name=".activity.SetLauncherActivity" | |||
android:screenOrientation="portrait"/> | |||
<activity | |||
android:name=".activity.MFAVerifyActivity" | |||
android:screenOrientation="portrait" /> | |||
@@ -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); | |||
} | |||
@@ -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); | |||
} | |||
@@ -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); | |||
} | |||
} | |||
} |
@@ -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<StatusResource<List<Network>>> checkBubbleOnce(final Context context) { | |||
final MutableLiveData<StatusResource<List<Network>>> data = new MutableLiveData<>(); | |||
final HashMap<String, String> header = new HashMap<>(); | |||
@@ -161,12 +181,13 @@ public final class DataRepository { | |||
final MutableLiveData<StatusResource<List<Network>>> data = new MutableLiveData<>(); | |||
final HashMap<String, String> 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<List<Network>>() { | |||
.subscribeWith(new DisposableSubscriber<List<Network>>() { | |||
@Override public void onNext(final List<Network> networks) { | |||
final List<Network> alive = getAliveBubbles(networks); | |||
if (!alive.isEmpty()) { | |||
@@ -366,7 +387,7 @@ public final class DataRepository { | |||
final HashMap<String, String> 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<byte[]>() { | |||
@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<ObservableTunnel> tunnels = new ArrayList<>(); | |||
if(pendingTunnel != null) { | |||
tunnels.add(pendingTunnel); | |||
if (pendingTunnel != null) { | |||
Application.getTunnelManager().delete(pendingTunnel); | |||
pendingTunnel = null; | |||
} | |||
} | |||
public MutableLiveData<StatusResource<Boolean>> isHaveTunnel(Context context) { | |||
public MutableLiveData<StatusResource<Boolean>> isHaveTunnel(final Context context) { | |||
return new NetworkBoundStatusResource<Boolean>(){ | |||
@Override protected void createCall() { | |||
final byte[] configBytes = UserStore.getInstance(context).getConfig().getBytes(); | |||
@@ -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()); | |||
} | |||
@@ -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); | |||
} | |||
} |
@@ -177,5 +177,17 @@ | |||
android:textStyle="normal" | |||
android:clickable="true" | |||
android:focusable="true" /> | |||
<TextView | |||
android:id="@+id/setLauncherButton" | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
android:layout_marginTop="@dimen/margin_start_and_end" | |||
android:text="@string/set_launcher" | |||
android:textAlignment="center" | |||
android:textSize="14sp" | |||
android:textStyle="normal" | |||
android:clickable="true" | |||
android:focusable="true" /> | |||
</LinearLayout> | |||
</androidx.constraintlayout.widget.ConstraintLayout> |
@@ -0,0 +1,110 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | |||
xmlns:app="http://schemas.android.com/apk/res-auto" | |||
xmlns:tools="http://schemas.android.com/tools" | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:background="@color/white" | |||
tools:context="com.getbubblenow.android.activity.LoginActivity"> | |||
<androidx.constraintlayout.widget.ConstraintLayout | |||
android:id="@+id/imageConstraint" | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
app:layout_constraintEnd_toEndOf="parent" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toTopOf="parent"> | |||
<ImageView | |||
android:id="@+id/color_curve" | |||
android:layout_width="match_parent" | |||
android:layout_height="wrap_content" | |||
android:layout_alignParentStart="true" | |||
android:layout_alignParentTop="true" | |||
android:background="@color/white" | |||
android:scaleType="fitXY" | |||
android:src="@drawable/color_curves" | |||
app:layout_constraintBottom_toBottomOf="parent" | |||
app:layout_constraintEnd_toEndOf="parent" | |||
app:layout_constraintHorizontal_bias="0.0" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toTopOf="parent" | |||
app:layout_constraintVertical_bias="1.0" /> | |||
<ImageView | |||
android:layout_width="wrap_content" | |||
android:layout_height="wrap_content" | |||
android:layout_marginStart="@dimen/margin_start_and_end" | |||
android:layout_marginTop="24dp" | |||
android:layout_marginEnd="@dimen/margin_start_and_end" | |||
android:src="@drawable/bubble_top" | |||
app:layout_constraintBottom_toBottomOf="parent" | |||
app:layout_constraintEnd_toEndOf="parent" | |||
app:layout_constraintHorizontal_bias="0.51" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toTopOf="parent" | |||
app:layout_constraintVertical_bias="0.0" /> | |||
</androidx.constraintlayout.widget.ConstraintLayout> | |||
<TextView | |||
android:id="@+id/set_launcher_title" | |||
android:layout_width="wrap_content" | |||
android:layout_height="wrap_content" | |||
android:layout_marginStart="@dimen/margin_start_and_end" | |||
android:layout_marginTop="50dp" | |||
android:layout_marginEnd="@dimen/margin_start_and_end" | |||
android:fontFamily="@font/josefin_sans_bold" | |||
android:text="@string/set_launcher_title" | |||
android:textColor="@color/black" | |||
android:textSize="@dimen/title" | |||
app:layout_constraintEnd_toEndOf="parent" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toBottomOf="@+id/imageConstraint" /> | |||
<EditText | |||
android:id="@+id/launcher_url" | |||
android:layout_width="@dimen/edit_text_width" | |||
android:layout_height="@dimen/edit_text_height" | |||
android:layout_marginStart="@dimen/margin_start_and_end" | |||
android:layout_marginTop="64dp" | |||
android:layout_marginEnd="@dimen/margin_start_and_end" | |||
android:background="@drawable/rectangle" | |||
android:hint="@string/launcher_url_to_sign_in" | |||
android:inputType="textUri" | |||
android:textAlignment="center" | |||
android:textColor="@color/black" | |||
android:textSize="@dimen/text_title" | |||
app:layout_constraintEnd_toEndOf="parent" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toBottomOf="@+id/set_launcher_title" | |||
android:importantForAutofill="no" /> | |||
<LinearLayout | |||
android:id="@+id/linearLayout" | |||
android:layout_width="@dimen/edit_text_width" | |||
android:layout_height="@dimen/edit_text_height" | |||
android:layout_marginStart="@dimen/margin_start_and_end" | |||
android:layout_marginTop="50dp" | |||
android:layout_marginEnd="@dimen/margin_start_and_end" | |||
android:background="@color/white" | |||
android:orientation="vertical" | |||
app:layout_constraintEnd_toEndOf="parent" | |||
app:layout_constraintStart_toStartOf="parent" | |||
app:layout_constraintTop_toBottomOf="@+id/launcher_url"> | |||
<androidx.appcompat.widget.AppCompatButton | |||
android:id="@+id/setLauncherButton" | |||
android:layout_width="match_parent" | |||
android:layout_height="match_parent" | |||
android:background="@drawable/sign_in_disable" | |||
android:enabled="false" | |||
android:text="@string/set_launcher_title" | |||
android:textAlignment="center" | |||
android:textAllCaps="false" | |||
android:textColor="@color/white" | |||
android:textSize="@dimen/text_title" /> | |||
</LinearLayout> | |||
</androidx.constraintlayout.widget.ConstraintLayout> |
@@ -269,6 +269,7 @@ | |||
<string name="building_bubble_title">We Are Building</string> | |||
<string name="private_bubble">Your Private Bubble!</string> | |||
<string name="no_account_yet">No account yet? <u>Create one.</u></string> | |||
<string name="set_launcher">Running your own Bubble? <u>Set Launcher.</u></string> | |||
<string name="launch_new_bubble">LAUNCH NEW BUBBLE</string> | |||
<string name="select_your_bubble">Select your Bubble</string> | |||
<string name="select_your_bubble_ok">OK</string> | |||
@@ -277,4 +278,6 @@ | |||
<string name="enter_verification_code">Enter Verification Code</string> | |||
<string name="enter_6_digit_code">Enter 6-digit Code</string> | |||
<string name="invalid_mfa_token_format">Invalid MFA token format</string> | |||
<string name="set_launcher_title">Set launcher</string> | |||
<string name="launcher_url_to_sign_in">Launcher URL to Sign In</string> | |||
</resources> |