Commit a7d90d74 authored by Yusuke Iwaki's avatar Yusuke Iwaki

Simplefy login/user registration process!

parent efab24c2
...@@ -2,6 +2,7 @@ package chat.rocket.android.activity; ...@@ -2,6 +2,7 @@ package chat.rocket.android.activity;
import android.support.annotation.IdRes; import android.support.annotation.IdRes;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import chat.rocket.android.helper.OnBackPressListener; import chat.rocket.android.helper.OnBackPressListener;
...@@ -9,16 +10,31 @@ abstract class AbstractFragmentActivity extends AppCompatActivity { ...@@ -9,16 +10,31 @@ abstract class AbstractFragmentActivity extends AppCompatActivity {
protected abstract @IdRes int getLayoutContainerForFragment(); protected abstract @IdRes int getLayoutContainerForFragment();
@Override public void onBackPressed() { @Override public final void onBackPressed() {
Fragment fragment = if (!onBackPress()) {
getSupportFragmentManager().findFragmentById(getLayoutContainerForFragment()); onBackPresseNotHandled();
}
}
protected boolean onBackPress() {
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(getLayoutContainerForFragment());
if (fragment instanceof OnBackPressListener if (fragment instanceof OnBackPressListener
&& ((OnBackPressListener) fragment).onBackPressed()) { && ((OnBackPressListener) fragment).onBackPressed()) {
//consumed. do nothing. return true;
} else { }
super.onBackPressed();
if (fragmentManager.getBackStackEntryCount() > 0) {
fragmentManager.popBackStack();
return true;
} }
return false;
}
protected void onBackPresseNotHandled() {
super.onBackPressed();
} }
protected void showFragment(Fragment fragment) { protected void showFragment(Fragment fragment) {
......
...@@ -7,21 +7,18 @@ import android.support.annotation.Nullable; ...@@ -7,21 +7,18 @@ import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import chat.rocket.android.LaunchUtil; import chat.rocket.android.LaunchUtil;
import chat.rocket.android.R; import chat.rocket.android.R;
import chat.rocket.android.fragment.oauth.GitHubOAuthWebViewFragment;
import chat.rocket.android.fragment.server_config.AuthenticatingFragment;
import chat.rocket.android.fragment.server_config.ConnectingToHostFragment;
import chat.rocket.android.fragment.server_config.InputHostnameFragment; import chat.rocket.android.fragment.server_config.InputHostnameFragment;
import chat.rocket.android.fragment.server_config.LoginFragment; import chat.rocket.android.fragment.server_config.LoginFragment;
import chat.rocket.android.fragment.server_config.WaitingFragment;
import chat.rocket.android.helper.LogcatIfError; import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.MethodCallHelper;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfig; import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.model.ServerConfigCredential;
import chat.rocket.android.service.RocketChatService; import chat.rocket.android.service.RocketChatService;
import io.realm.Realm; import io.realm.Realm;
import io.realm.RealmQuery; import io.realm.RealmQuery;
import java.util.List; import java.util.List;
import jp.co.crowdworks.realm_java_helpers.RealmObjectObserver; import jp.co.crowdworks.realm_java_helpers.RealmObjectObserver;
import jp.co.crowdworks.realm_java_helpers_bolts.RealmHelperBolts;
/** /**
* Activity for Login, Sign-up, and Connecting... * Activity for Login, Sign-up, and Connecting...
...@@ -58,15 +55,6 @@ public class ServerConfigActivity extends AbstractFragmentActivity { ...@@ -58,15 +55,6 @@ public class ServerConfigActivity extends AbstractFragmentActivity {
} }
} }
for (ServerConfig config : configList) {
ServerConfigCredential credential = config.getCredential();
if (credential != null
&& !TextUtils.isEmpty(credential.getType())
&& TextUtils.isEmpty(credential.getErrorMessage())) {
return launchFor(context, config);
}
}
for (ServerConfig config : configList) { for (ServerConfig config : configList) {
if (TextUtils.isEmpty(config.getToken())) { if (TextUtils.isEmpty(config.getToken())) {
return launchFor(context, config); return launchFor(context, config);
...@@ -134,19 +122,9 @@ public class ServerConfigActivity extends AbstractFragmentActivity { ...@@ -134,19 +122,9 @@ public class ServerConfigActivity extends AbstractFragmentActivity {
final String token = config.getToken(); final String token = config.getToken();
if (!TextUtils.isEmpty(token)) { if (!TextUtils.isEmpty(token)) {
showFragment(new AuthenticatingFragment()); showFragment(WaitingFragment.create("Authenticating..."));
return; new MethodCallHelper(serverConfigId).loginWithToken(token)
} .continueWith(new LogcatIfError());
final ServerConfigCredential credential = config.getCredential();
if (credential != null
&& !TextUtils.isEmpty(credential.getType())
&& TextUtils.isEmpty(credential.getErrorMessage())) {
if (ServerConfigCredential.hasSecret(credential)) {
showFragment(new AuthenticatingFragment());
} else {
showFragment(getAuthFragmentFor(credential.getType()));
}
return; return;
} }
...@@ -158,28 +136,13 @@ public class ServerConfigActivity extends AbstractFragmentActivity { ...@@ -158,28 +136,13 @@ public class ServerConfigActivity extends AbstractFragmentActivity {
final String error = config.getConnectionError(); final String error = config.getConnectionError();
String hostname = config.getHostname(); String hostname = config.getHostname();
if (!TextUtils.isEmpty(hostname) && TextUtils.isEmpty(error)) { if (!TextUtils.isEmpty(hostname) && TextUtils.isEmpty(error)) {
showFragment(new ConnectingToHostFragment()); showFragment(WaitingFragment.create("Connecting to server..."));
return; return;
} }
showFragment(new InputHostnameFragment()); showFragment(new InputHostnameFragment());
} }
private Fragment getAuthFragmentFor(final String authType) {
if ("github".equals(authType)) {
return GitHubOAuthWebViewFragment.create(serverConfigId);
} else if ("twitter".equals(authType)) {
// TODO
}
RealmHelperBolts.executeTransaction(realm -> realm.where(ServerConfigCredential.class)
.equalTo("type", authType)
.findAll()
.deleteAllFromRealm()
).continueWith(new LogcatIfError());
throw new IllegalArgumentException("Invalid authType given:" + authType);
}
@Override protected void showFragment(Fragment fragment) { @Override protected void showFragment(Fragment fragment) {
injectServerConfigIdArgTo(fragment); injectServerConfigIdArgTo(fragment);
super.showFragment(fragment); super.showFragment(fragment);
...@@ -199,9 +162,9 @@ public class ServerConfigActivity extends AbstractFragmentActivity { ...@@ -199,9 +162,9 @@ public class ServerConfigActivity extends AbstractFragmentActivity {
fragment.setArguments(args); fragment.setArguments(args);
} }
@Override public void onBackPressed() { @Override protected void onBackPresseNotHandled() {
if (ServerConfig.hasActiveConnection()) { if (ServerConfig.hasActiveConnection()) {
super.onBackPressed(); super.onBackPresseNotHandled();
} else { } else {
moveTaskToBack(true); moveTaskToBack(true);
} }
......
...@@ -2,27 +2,25 @@ package chat.rocket.android.fragment.oauth; ...@@ -2,27 +2,25 @@ package chat.rocket.android.fragment.oauth;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Base64; import android.util.Base64;
import android.webkit.JavascriptInterface; import android.webkit.JavascriptInterface;
import android.webkit.WebView; import android.webkit.WebView;
import chat.rocket.android.fragment.AbstractWebViewFragment; import chat.rocket.android.fragment.AbstractWebViewFragment;
import chat.rocket.android.helper.LogcatIfError; import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.MethodCallHelper;
import chat.rocket.android.model.MeteorLoginServiceConfiguration; import chat.rocket.android.model.MeteorLoginServiceConfiguration;
import chat.rocket.android.model.ServerConfig; import chat.rocket.android.model.ServerConfig;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import jp.co.crowdworks.realm_java_helpers.RealmHelper; import jp.co.crowdworks.realm_java_helpers.RealmHelper;
import jp.co.crowdworks.realm_java_helpers_bolts.RealmHelperBolts;
import okhttp3.HttpUrl; import okhttp3.HttpUrl;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import timber.log.Timber; import timber.log.Timber;
public class GitHubOAuthWebViewFragment extends AbstractWebViewFragment { public class GitHubOAuthFragment extends AbstractWebViewFragment {
private String serverConfigId; private String serverConfigId;
private String credentialId;
private String hostname; private String hostname;
private String url; private String url;
private boolean resultOK; private boolean resultOK;
...@@ -30,17 +28,17 @@ public class GitHubOAuthWebViewFragment extends AbstractWebViewFragment { ...@@ -30,17 +28,17 @@ public class GitHubOAuthWebViewFragment extends AbstractWebViewFragment {
/** /**
* create new Fragment with ServerConfig-ID. * create new Fragment with ServerConfig-ID.
*/ */
public static Fragment create(final String serverConfigId) { public static GitHubOAuthFragment create(final String serverConfigId) {
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putString("server_config_id", serverConfigId); args.putString("serverConfigId", serverConfigId);
Fragment fragment = new GitHubOAuthWebViewFragment(); GitHubOAuthFragment fragment = new GitHubOAuthFragment();
fragment.setArguments(args); fragment.setArguments(args);
return fragment; return fragment;
} }
private boolean hasValidArgs(Bundle args) { private boolean hasValidArgs(Bundle args) {
return args != null return args != null
&& args.containsKey("server_config_id"); && args.containsKey("serverConfigId");
} }
@Override public void onCreate(@Nullable Bundle savedInstanceState) { @Override public void onCreate(@Nullable Bundle savedInstanceState) {
...@@ -48,10 +46,10 @@ public class GitHubOAuthWebViewFragment extends AbstractWebViewFragment { ...@@ -48,10 +46,10 @@ public class GitHubOAuthWebViewFragment extends AbstractWebViewFragment {
Bundle args = getArguments(); Bundle args = getArguments();
if (!hasValidArgs(args)) { if (!hasValidArgs(args)) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"server_config_id required"); "serverConfigId required");
} }
serverConfigId = args.getString("server_config_id"); serverConfigId = args.getString("serverConfigId");
ServerConfig serverConfig = RealmHelper.executeTransactionForRead(realm -> ServerConfig serverConfig = RealmHelper.executeTransactionForRead(realm ->
realm.where(ServerConfig.class).equalTo("id", serverConfigId).findFirst()); realm.where(ServerConfig.class).equalTo("id", serverConfigId).findFirst());
MeteorLoginServiceConfiguration oauthConfig = RealmHelper.executeTransactionForRead(realm -> MeteorLoginServiceConfiguration oauthConfig = RealmHelper.executeTransactionForRead(realm ->
...@@ -61,9 +59,8 @@ public class GitHubOAuthWebViewFragment extends AbstractWebViewFragment { ...@@ -61,9 +59,8 @@ public class GitHubOAuthWebViewFragment extends AbstractWebViewFragment {
.findFirst()); .findFirst());
if (serverConfig == null || oauthConfig == null) { if (serverConfig == null || oauthConfig == null) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Invalid server_config_id given,"); "Invalid serverConfigId given,");
} }
credentialId = serverConfig.getCredential().getId();
hostname = serverConfig.getHostname(); hostname = serverConfig.getHostname();
url = generateURL(oauthConfig.getClientId()); url = generateURL(oauthConfig.getClientId());
} }
...@@ -154,16 +151,8 @@ public class GitHubOAuthWebViewFragment extends AbstractWebViewFragment { ...@@ -154,16 +151,8 @@ public class GitHubOAuthWebViewFragment extends AbstractWebViewFragment {
} }
private void handleOAuthCallback(final String credentialToken, final String credentialSecret) { private void handleOAuthCallback(final String credentialToken, final String credentialSecret) {
RealmHelperBolts.executeTransaction(realm -> new MethodCallHelper(serverConfigId).loginWithGitHub(credentialToken, credentialSecret)
realm.createOrUpdateObjectFromJson(ServerConfig.class, new JSONObject() .continueWith(new LogcatIfError());
.put("id", serverConfigId)
.put("credential", new JSONObject()
.put("id", credentialId)
.put("type", "github")
.put("credentialToken", credentialToken)
.put("credentialSecret", credentialSecret))
)
).continueWith(new LogcatIfError());
} }
private void onOAuthCompleted() { private void onOAuthCompleted() {
......
...@@ -2,10 +2,14 @@ package chat.rocket.android.fragment.server_config; ...@@ -2,10 +2,14 @@ package chat.rocket.android.fragment.server_config;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import chat.rocket.android.R;
import chat.rocket.android.fragment.AbstractFragment; import chat.rocket.android.fragment.AbstractFragment;
import chat.rocket.android.helper.OnBackPressListener;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
abstract class AbstractServerConfigFragment extends AbstractFragment { abstract class AbstractServerConfigFragment extends AbstractFragment
implements OnBackPressListener {
protected String serverConfigId; protected String serverConfigId;
@Override public void onCreate(@Nullable Bundle savedInstanceState) { @Override public void onCreate(@Nullable Bundle savedInstanceState) {
...@@ -23,4 +27,27 @@ abstract class AbstractServerConfigFragment extends AbstractFragment { ...@@ -23,4 +27,27 @@ abstract class AbstractServerConfigFragment extends AbstractFragment {
return; return;
} }
} }
protected void showFragment(Fragment fragment) {
getFragmentManager().beginTransaction()
.add(R.id.content, fragment)
.commit();
}
protected void showFragmentWithBackStack(Fragment fragment) {
getFragmentManager().beginTransaction()
.add(R.id.content, fragment)
.addToBackStack(null)
.commit();
}
@Override public boolean onBackPressed() {
if (getFragmentManager().getBackStackEntryCount() > 0) {
getFragmentManager().popBackStack();
return true;
}
return false;
}
} }
package chat.rocket.android.fragment.server_config;
import android.view.View;
import android.widget.TextView;
import chat.rocket.android.R;
/**
* Just show "Authenticating..."
*/
public class AuthenticatingFragment extends AbstractServerConfigFragment {
@Override protected int getLayout() {
return R.layout.fragment_wait_for_connection;
}
@Override protected void onSetupView() {
TextView caption = (TextView) rootView.findViewById(R.id.txt_caption);
caption.setVisibility(View.VISIBLE);
caption.setText("Authenticating...");
}
}
\ No newline at end of file
package chat.rocket.android.fragment.server_config;
import android.view.View;
import android.widget.TextView;
import chat.rocket.android.R;
/**
* Just showing "connecting..." screen.
*/
public class ConnectingToHostFragment extends AbstractServerConfigFragment {
@Override protected int getLayout() {
return R.layout.fragment_wait_for_connection;
}
@Override protected void onSetupView() {
TextView caption = (TextView) rootView.findViewById(R.id.txt_caption);
caption.setVisibility(View.GONE);
}
}
package chat.rocket.android.fragment.server_config; package chat.rocket.android.fragment.server_config;
import android.os.Handler; import android.support.design.widget.Snackbar;
import android.os.Message;
import android.view.View; import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import chat.rocket.android.R; import chat.rocket.android.R;
import chat.rocket.android.helper.CheckSum; import chat.rocket.android.fragment.oauth.GitHubOAuthFragment;
import chat.rocket.android.helper.LogcatIfError; import chat.rocket.android.helper.MethodCallHelper;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.MeteorLoginServiceConfiguration; import chat.rocket.android.model.MeteorLoginServiceConfiguration;
import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.model.ServerConfigCredential;
import chat.rocket.android.renderer.ServerConfigCredentialRenderer;
import io.realm.Realm; import io.realm.Realm;
import io.realm.RealmResults; import io.realm.RealmResults;
import java.util.List; import java.util.List;
import jp.co.crowdworks.realm_java_helpers.RealmHelper;
import jp.co.crowdworks.realm_java_helpers.RealmListObserver; import jp.co.crowdworks.realm_java_helpers.RealmListObserver;
import jp.co.crowdworks.realm_java_helpers_bolts.RealmHelperBolts;
import org.json.JSONObject;
/** /**
* Login screen. * Login screen.
...@@ -29,12 +21,6 @@ public class LoginFragment extends AbstractServerConfigFragment { ...@@ -29,12 +21,6 @@ public class LoginFragment extends AbstractServerConfigFragment {
return R.layout.fragment_login; return R.layout.fragment_login;
} }
private Handler errorShowingHandler = new Handler() {
@Override public void handleMessage(Message msg) {
Toast.makeText(rootView.getContext(), (String) msg.obj, Toast.LENGTH_SHORT).show();
}
};
private RealmListObserver<MeteorLoginServiceConfiguration> authProvidersObserver = private RealmListObserver<MeteorLoginServiceConfiguration> authProvidersObserver =
new RealmListObserver<MeteorLoginServiceConfiguration>() { new RealmListObserver<MeteorLoginServiceConfiguration>() {
@Override protected RealmResults<MeteorLoginServiceConfiguration> queryItems(Realm realm) { @Override protected RealmResults<MeteorLoginServiceConfiguration> queryItems(Realm realm) {
...@@ -58,54 +44,28 @@ public class LoginFragment extends AbstractServerConfigFragment { ...@@ -58,54 +44,28 @@ public class LoginFragment extends AbstractServerConfigFragment {
if (TextUtils.isEmpty(username) || TextUtils.isEmpty(passwd)) { if (TextUtils.isEmpty(username) || TextUtils.isEmpty(passwd)) {
return; return;
} }
view.setEnabled(false);
RealmHelperBolts.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(ServerConfig.class, new JSONObject() new MethodCallHelper(serverConfigId).loginWithEmail(username.toString(), passwd.toString())
.put("id", serverConfigId) .continueWith(task -> {
.put("credential", new JSONObject() if (task.isFaulted()) {
.put("id", serverConfigId) showError(task.getError().getMessage());
.put("type", ServerConfigCredential.TYPE_EMAIL) view.setEnabled(true);
.put("errorMessage", JSONObject.NULL) }
.put("username", username.toString()) return null;
.put("hashedPasswd", CheckSum.sha256(passwd.toString()))) });
)
).continueWith(new LogcatIfError());
}); });
final View btnUserRegistration = rootView.findViewById(R.id.btn_user_registration); final View btnUserRegistration = rootView.findViewById(R.id.btn_user_registration);
btnUserRegistration.setOnClickListener(view -> { btnUserRegistration.setOnClickListener(view -> {
UserRegistrationDialogFragment.create(serverConfigId, UserRegistrationDialogFragment.create(serverConfigId,
txtUsername.getText().toString(), txtPasswd.getText().toString()) txtUsername.getText().toString(), txtPasswd.getText().toString())
.show(getFragmentManager(), UserRegistrationDialogFragment.class.getSimpleName()); .show(getFragmentManager(), UserRegistrationDialogFragment.class.getSimpleName());
}); });
showErrorIfNeeded();
}
private void showErrorIfNeeded() {
ServerConfig config = RealmHelper.executeTransactionForRead(realm ->
realm.where(ServerConfig.class)
.equalTo("id", serverConfigId)
.isNotNull("credential.errorMessage")
.findFirst());
if (config != null) {
ServerConfigCredential credential = config.getCredential();
new ServerConfigCredentialRenderer(getContext(), credential)
.usernameInto((TextView) rootView.findViewById(R.id.editor_username));
String errorMessage = credential.getErrorMessage();
if (!TextUtils.isEmpty(errorMessage)) {
showError(errorMessage);
}
}
} }
private void showError(String errString) { private void showError(String errString) {
errorShowingHandler.removeMessages(0); Snackbar.make(rootView, errString, Snackbar.LENGTH_SHORT).show();
Message msg = Message.obtain(errorShowingHandler, 0, errString);
errorShowingHandler.sendMessageDelayed(msg, 160);
} }
private void onRenderAuthProviders(List<MeteorLoginServiceConfiguration> authProviders) { private void onRenderAuthProviders(List<MeteorLoginServiceConfiguration> authProviders) {
...@@ -116,17 +76,17 @@ public class LoginFragment extends AbstractServerConfigFragment { ...@@ -116,17 +76,17 @@ public class LoginFragment extends AbstractServerConfigFragment {
boolean hasGitHub = false; boolean hasGitHub = false;
for (MeteorLoginServiceConfiguration authProvider : authProviders) { for (MeteorLoginServiceConfiguration authProvider : authProviders) {
if (!hasTwitter if (!hasTwitter
&& ServerConfigCredential.TYPE_TWITTER.equals(authProvider.getService())) { && "twitter".equals(authProvider.getService())) {
hasTwitter = true; hasTwitter = true;
btnTwitter.setOnClickListener(view -> { btnTwitter.setOnClickListener(view -> {
setAuthType(authProvider.getId(), ServerConfigCredential.TYPE_TWITTER);
}); });
} }
if (!hasGitHub if (!hasGitHub
&& ServerConfigCredential.TYPE_GITHUB.equals(authProvider.getService())) { && "github".equals(authProvider.getService())) {
hasGitHub = true; hasGitHub = true;
btnGitHub.setOnClickListener(view -> { btnGitHub.setOnClickListener(view -> {
setAuthType(authProvider.getId(), ServerConfigCredential.TYPE_GITHUB); showFragmentWithBackStack(GitHubOAuthFragment.create(serverConfigId));
}); });
} }
} }
...@@ -135,17 +95,6 @@ public class LoginFragment extends AbstractServerConfigFragment { ...@@ -135,17 +95,6 @@ public class LoginFragment extends AbstractServerConfigFragment {
btnGitHub.setVisibility(hasGitHub ? View.VISIBLE : View.GONE); btnGitHub.setVisibility(hasGitHub ? View.VISIBLE : View.GONE);
} }
private void setAuthType(final String authProviderId, final String authType) {
RealmHelperBolts.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(ServerConfig.class, new JSONObject()
.put("id", serverConfigId)
.put("credential", new JSONObject()
.put("id", authProviderId)
.put("type", authType))
.put("errorMessage", JSONObject.NULL))
).continueWith(new LogcatIfError());
}
@Override public void onResume() { @Override public void onResume() {
super.onResume(); super.onResume();
authProvidersObserver.sub(); authProvidersObserver.sub();
......
...@@ -12,13 +12,8 @@ import android.view.View; ...@@ -12,13 +12,8 @@ import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import chat.rocket.android.R; import chat.rocket.android.R;
import chat.rocket.android.helper.CheckSum;
import chat.rocket.android.helper.MethodCallHelper; import chat.rocket.android.helper.MethodCallHelper;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.model.ServerConfigCredential;
import jp.co.crowdworks.realm_java_helpers_bolts.RealmHelperBolts;
import org.json.JSONObject;
/** /**
* Dialog for user registration. * Dialog for user registration.
...@@ -114,30 +109,22 @@ public class UserRegistrationDialogFragment extends DialogFragment { ...@@ -114,30 +109,22 @@ public class UserRegistrationDialogFragment extends DialogFragment {
email = txtEmail.getText().toString(); email = txtEmail.getText().toString();
password = txtPasswd.getText().toString(); password = txtPasswd.getText().toString();
MethodCallHelper.registerUser(username, email, password, password).onSuccessTask(task -> MethodCallHelper methodCallHelper = new MethodCallHelper(serverConfigId);
RealmHelperBolts.executeTransaction(realm -> methodCallHelper.registerUser(username, email, password, password)
realm.createOrUpdateObjectFromJson(ServerConfig.class, new JSONObject() .onSuccessTask(task -> methodCallHelper.loginWithEmail(email, password))
.put("id", serverConfigId) .onSuccessTask(task -> {
.put("credential", new JSONObject() dismiss();
.put("id", serverConfigId) return task;
.put("type", ServerConfigCredential.TYPE_EMAIL) })
.put("errorMessage", JSONObject.NULL) .continueWith(task -> {
.put("username", email) if (task.isFaulted()) {
.put("hashedPasswd", CheckSum.sha256(password))) Exception exception = task.getError();
) showError(exception.getMessage());
) view.setEnabled(true);
).onSuccessTask(task -> { waitingView.setVisibility(View.GONE);
dismiss(); }
return null; return null;
}).continueWith(task -> { });
if (task.isFaulted()) {
Exception exception = task.getError();
showError(exception.getMessage());
view.setEnabled(true);
waitingView.setVisibility(View.GONE);
}
return null;
});
}); });
return dialog; return dialog;
} }
......
package chat.rocket.android.fragment.server_config;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import chat.rocket.android.R;
import chat.rocket.android.helper.TextUtils;
/**
* Just showing "connecting..." screen.
*/
public class WaitingFragment extends AbstractServerConfigFragment {
private String caption;
/**
* create new "Waiting..." screen with caption.
*/
public static WaitingFragment create(String caption) {
Bundle args = new Bundle();
args.putString("caption", caption);
WaitingFragment fragment = new WaitingFragment();
fragment.setArguments(args);
return fragment;
}
public WaitingFragment() {}
@Override protected int getLayout() {
return R.layout.fragment_waiting;
}
@Override protected void onSetupView() {
Bundle args = getArguments();
if (args != null) {
caption = args.getString("caption");
}
TextView captionView = (TextView) rootView.findViewById(R.id.txt_caption);
if (TextUtils.isEmpty(caption)) {
captionView.setVisibility(View.GONE);
} else {
captionView.setText(caption);
captionView.setVisibility(View.VISIBLE);
}
}
}
package chat.rocket.android.helper; package chat.rocket.android.helper;
import android.util.Patterns; import android.util.Patterns;
import bolts.Continuation;
import bolts.Task; import bolts.Task;
import chat.rocket.android.model.MethodCall; import chat.rocket.android.model.MethodCall;
import chat.rocket.android.model.ServerConfigCredential; import chat.rocket.android.model.ServerConfig;
import chat.rocket.android_ddp.DDPClientCallback; import jp.co.crowdworks.realm_java_helpers_bolts.RealmHelperBolts;
import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import static android.icu.lang.UCharacter.GraphemeClusterBreak.T;
/** /**
* Utility class for creating/handling MethodCall. * Utility class for creating/handling MethodCall.
*/ */
public class MethodCallHelper { public class MethodCallHelper {
private static <T> Task<T> injectErrorMessageHandler(Task<T> task) { private final String serverConfigId;
public MethodCallHelper(String serverConfigId) {
this.serverConfigId = serverConfigId;
}
private Task<JSONObject> injectErrorHandler(Task<JSONObject> task) {
return task.continueWithTask(_task -> { return task.continueWithTask(_task -> {
if (_task.isFaulted()) { if (_task.isFaulted()) {
Exception exception = _task.getError(); Exception exception = _task.getError();
...@@ -32,21 +36,91 @@ public class MethodCallHelper { ...@@ -32,21 +36,91 @@ public class MethodCallHelper {
}); });
} }
/** private interface ParamBuilder {
* Register User. void buildParam(JSONObject param) throws JSONException;
*/ }
public static Task<Void> registerUser(String name, String email, String passwd,
String confirmPasswd) { private <T> Task<T> call(String methodName,
Continuation<JSONObject, Task<T>> onSuccess) {
return injectErrorHandler(MethodCall.execute(serverConfigId, methodName, null))
.onSuccessTask(onSuccess);
}
private <T> Task<T> call(String methodName, ParamBuilder paramBuilder,
Continuation<JSONObject, Task<T>> onSuccess) {
JSONObject param = new JSONObject(); JSONObject param = new JSONObject();
try { try {
param.put("name", name).put("email", email) paramBuilder.buildParam(param);
.put("pass", passwd).put("confirm-pass", confirmPasswd);
} catch (JSONException exception) { } catch (JSONException exception) {
return Task.forError(exception); return Task.forError(exception);
} }
return injectErrorMessageHandler(MethodCall.execute("registerUser", param.toString())) return injectErrorHandler(MethodCall.execute(serverConfigId, methodName, param.toString()))
.onSuccessTask(task -> Task.forResult(null)); // nothing to do? .onSuccessTask(onSuccess);
}
/**
* Register User.
*/
public Task<Void> registerUser(final String name, final String email,
final String password, final String confirmPassword) {
return call("registerUser", param -> param
.put("name", name)
.put("email", email)
.put("pass", password)
.put("confirm-pass", confirmPassword),
task -> Task.forResult(null)); // nothing to do.
}
private Continuation<String, Task<Void>> saveToken() {
return task -> RealmHelperBolts.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(ServerConfig.class, new JSONObject()
.put("id", serverConfigId)
.put("token", task.getResult())
.put("tokenVerified", true)));
}
/**
* Login with username/email and password.
*/
public Task<Void> loginWithEmail(final String usernameOrEmail, final String password) {
return call("login", param -> {
if (Patterns.EMAIL_ADDRESS.matcher(usernameOrEmail).matches()) {
param.put("user", new JSONObject().put("email", usernameOrEmail));
} else {
param.put("user", new JSONObject().put("username", usernameOrEmail));
}
param.put("password", new JSONObject()
.put("digest", CheckSum.sha256(password))
.put("algorithm", "sha-256"));
}, task -> Task.forResult(task.getResult().getString("token"))).onSuccessTask(saveToken());
}
/**
* Login with GitHub OAuth.
*/
public Task<Void> loginWithGitHub(final String credentialToken,
final String credentialSecret) {
return call("login", param -> param
.put("oauth", new JSONObject()
.put("credentialToken", credentialToken)
.put("credentialSecret", credentialSecret)),
task -> Task.forResult(task.getResult().getString("token"))).onSuccessTask(saveToken());
}
/**
* Login with token.
*/
public Task<Void> loginWithToken(final String token) {
return call("login", param -> param.put("resume", token),
task -> Task.forResult(task.getResult().getString("token"))).onSuccessTask(saveToken());
}
/**
* Logout.
*/
public Task<Void> logout() {
return call("logout", task -> Task.forResult(null));
} }
} }
...@@ -18,6 +18,7 @@ import org.json.JSONObject; ...@@ -18,6 +18,7 @@ import org.json.JSONObject;
public class MethodCall extends RealmObject { public class MethodCall extends RealmObject {
@PrimaryKey private String id; @PrimaryKey private String id;
private String serverConfigId; //not ServerConfig!(not to be notified the change of ServerConfig)
private int syncstate; private int syncstate;
private String name; private String name;
private String paramsJson; private String paramsJson;
...@@ -31,6 +32,14 @@ public class MethodCall extends RealmObject { ...@@ -31,6 +32,14 @@ public class MethodCall extends RealmObject {
this.id = id; this.id = id;
} }
public String getServerConfigId() {
return serverConfigId;
}
public void setServerConfigId(String serverConfigId) {
this.serverConfigId = serverConfigId;
}
public int getSyncstate() { public int getSyncstate() {
return syncstate; return syncstate;
} }
...@@ -73,13 +82,15 @@ public class MethodCall extends RealmObject { ...@@ -73,13 +82,15 @@ public class MethodCall extends RealmObject {
} }
} }
public static Task<JSONObject> execute(String name, String paramsJson) { public static Task<JSONObject> execute(String serverConfigId, String name, String paramsJson) {
final String newId = UUID.randomUUID().toString(); final String newId = UUID.randomUUID().toString();
TaskCompletionSource<JSONObject> task = new TaskCompletionSource<>(); TaskCompletionSource<JSONObject> task = new TaskCompletionSource<>();
RealmHelperBolts.executeTransaction(realm -> { RealmHelperBolts.executeTransaction(realm -> {
MethodCall call = realm.createObject(MethodCall.class, newId); MethodCall call = realm.createObjectFromJson(MethodCall.class, new JSONObject()
call.setSyncstate(SyncState.NOT_SYNCED); .put("id", newId)
call.setName(name); .put("serverConfigId", serverConfigId)
.put("syncstate", SyncState.NOT_SYNCED)
.put("name", name));
call.setParamsJson(paramsJson); call.setParamsJson(paramsJson);
return null; return null;
}).continueWith(_task -> { }).continueWith(_task -> {
......
...@@ -21,7 +21,6 @@ public class ServerConfig extends RealmObject { ...@@ -21,7 +21,6 @@ public class ServerConfig extends RealmObject {
private String session; private String session;
private String token; private String token;
private boolean tokenVerified; private boolean tokenVerified;
private ServerConfigCredential credential;
public static RealmQuery<ServerConfig> queryLoginRequiredConnections(Realm realm) { public static RealmQuery<ServerConfig> queryLoginRequiredConnections(Realm realm) {
return realm.where(ServerConfig.class).equalTo("tokenVerified", false); return realm.where(ServerConfig.class).equalTo("tokenVerified", false);
...@@ -94,12 +93,4 @@ public class ServerConfig extends RealmObject { ...@@ -94,12 +93,4 @@ public class ServerConfig extends RealmObject {
public void setTokenVerified(boolean tokenVerified) { public void setTokenVerified(boolean tokenVerified) {
this.tokenVerified = tokenVerified; this.tokenVerified = tokenVerified;
} }
public ServerConfigCredential getCredential() {
return credential;
}
public void setCredential(ServerConfigCredential credential) {
this.credential = credential;
}
} }
package chat.rocket.android.model;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android_ddp.DDPClientCallback;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
import jp.co.crowdworks.realm_java_helpers_bolts.RealmHelperBolts;
import org.json.JSONException;
import org.json.JSONObject;
@SuppressWarnings("PMD.ShortVariable")
public class ServerConfigCredential extends RealmObject {
public static final String TYPE_EMAIL = "email";
public static final String TYPE_TWITTER = "twitter";
public static final String TYPE_GITHUB = "github";
@PrimaryKey private String id;
private String type;
private String credentialToken;
private String credentialSecret;
private String username;
private String hashedPasswd;
private String errorMessage;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getCredentialToken() {
return credentialToken;
}
public void setCredentialToken(String credentialToken) {
this.credentialToken = credentialToken;
}
public String getCredentialSecret() {
return credentialSecret;
}
public void setCredentialSecret(String credentialSecret) {
this.credentialSecret = credentialSecret;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getHashedPasswd() {
return hashedPasswd;
}
public void setHashedPasswd(String hashedPasswd) {
this.hashedPasswd = hashedPasswd;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
public static boolean hasSecret(ServerConfigCredential credential) {
if (credential == null) {
return false;
}
final String authType = credential.getType();
if (TextUtils.isEmpty(authType)) {
return false;
}
if ("github".equals(authType) || "twitter".equals(authType)) {
return !TextUtils.isEmpty(credential.getCredentialToken())
&& !TextUtils.isEmpty(credential.getCredentialSecret());
} else if ("email".equals(authType)) {
return !TextUtils.isEmpty(credential.getUsername())
&& !TextUtils.isEmpty(credential.getHashedPasswd());
}
return false;
}
public static void logError(final String id, final Exception exception) {
RealmHelperBolts.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(ServerConfigCredential.class, new JSONObject()
.put("id", id)
.put("errorMessage", getErrorMessageFor(exception)))
).continueWith(new LogcatIfError());
}
private static String getErrorMessageFor(Exception exception) throws JSONException {
if (exception instanceof DDPClientCallback.RPC.Error) {
JSONObject error = ((DDPClientCallback.RPC.Error) exception).error;
if (!error.isNull("message")) {
return error.getString("message");
} else {
return error.toString();
}
} else {
return exception.getMessage();
}
}
}
package chat.rocket.android.renderer;
import android.content.Context;
import android.widget.TextView;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfigCredential;
/**
* renderer class for ServerConfigCredential.
*/
public class ServerConfigCredentialRenderer extends AbstractRenderer<ServerConfigCredential> {
public ServerConfigCredentialRenderer(Context context, ServerConfigCredential credential) {
super(context, credential);
}
/**
* Inject username into TextView.
*/
public ServerConfigCredentialRenderer usernameInto(TextView textView) {
if (!shouldHandle(textView)) {
return this;
}
if ("email".equals(object.getType())
&& !TextUtils.isEmpty(object.getUsername())) {
textView.setText(object.getUsername());
}
return this;
}
}
...@@ -10,7 +10,6 @@ import chat.rocket.android.helper.LogcatIfError; ...@@ -10,7 +10,6 @@ import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfig; import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.service.ddp_subscriber.LoginServiceConfigurationSubscriber; import chat.rocket.android.service.ddp_subscriber.LoginServiceConfigurationSubscriber;
import chat.rocket.android.service.observer.LoginCredentialObserver;
import chat.rocket.android.service.observer.MethodCallObserver; import chat.rocket.android.service.observer.MethodCallObserver;
import chat.rocket.android.ws.RocketChatWebSocketAPI; import chat.rocket.android.ws.RocketChatWebSocketAPI;
import chat.rocket.android_ddp.DDPClientCallback; import chat.rocket.android_ddp.DDPClientCallback;
...@@ -29,7 +28,6 @@ import timber.log.Timber; ...@@ -29,7 +28,6 @@ import timber.log.Timber;
public class RocketChatWebSocketThread extends HandlerThread { public class RocketChatWebSocketThread extends HandlerThread {
private static final Class[] REGISTERABLE_CLASSES = { private static final Class[] REGISTERABLE_CLASSES = {
LoginServiceConfigurationSubscriber.class, LoginServiceConfigurationSubscriber.class,
LoginCredentialObserver.class,
MethodCallObserver.class MethodCallObserver.class
}; };
private final Context appContext; private final Context appContext;
......
package chat.rocket.android.service.observer;
import android.content.Context;
import bolts.Task;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfig;
import chat.rocket.android.model.ServerConfigCredential;
import chat.rocket.android.ws.RocketChatWebSocketAPI;
import chat.rocket.android_ddp.DDPClientCallback;
import io.realm.Realm;
import io.realm.RealmResults;
import java.util.List;
import jp.co.crowdworks.realm_java_helpers_bolts.RealmHelperBolts;
import org.json.JSONObject;
public class LoginCredentialObserver extends AbstractModelObserver<ServerConfig> {
public LoginCredentialObserver(Context context, String serverConfigId,
RocketChatWebSocketAPI api) {
super(context, serverConfigId, api);
}
@Override protected RealmResults<ServerConfig> queryItems(Realm realm) {
return realm.where(ServerConfig.class)
.equalTo("tokenVerified", false)
.beginGroup()
.equalTo("credential.type", "email")
.isNotNull("credential.username")
.isNotNull("credential.hashedPasswd")
.or()
.notEqualTo("credential.type", "email")
.isNotNull("credential.credentialToken")
.isNotNull("credential.credentialSecret")
.endGroup()
.isNull("credential.errorMessage")
.findAll();
}
@Override protected void onCollectionChanged(List<ServerConfig> list) {
if (list.isEmpty()) {
return;
}
ServerConfig config = list.get(0);
final String serverConfigId = config.getId();
final String credentialId = config.getCredential().getId();
login(config).onSuccessTask(task -> {
final String token = task.getResult().result.getString("token");
return RealmHelperBolts.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(ServerConfig.class, new JSONObject()
.put("id", serverConfigId)
.put("token", token)
.put("tokenVerified", true)));
}).continueWith(task -> {
if (task.isFaulted()) {
ServerConfigCredential.logError(credentialId, task.getError());
}
return null;
});
}
private Task<DDPClientCallback.RPC> login(ServerConfig config) {
if (!TextUtils.isEmpty(config.getToken())) {
return webSocketAPI.loginWithToken(config.getToken());
}
return webSocketAPI.login(config.getCredential());
}
}
...@@ -24,11 +24,22 @@ public class MethodCallObserver extends AbstractModelObserver<MethodCall> { ...@@ -24,11 +24,22 @@ public class MethodCallObserver extends AbstractModelObserver<MethodCall> {
super(context, serverConfigId, api); super(context, serverConfigId, api);
RealmHelperBolts.executeTransaction(realm -> { RealmHelperBolts.executeTransaction(realm -> {
RealmResults<MethodCall> pendingMethodCalls = realm.where(MethodCall.class) RealmResults<MethodCall> pendingMethodCalls = realm.where(MethodCall.class)
.equalTo("serverConfigId", serverConfigId)
.equalTo("syncstate", SyncState.SYNCING) .equalTo("syncstate", SyncState.SYNCING)
.findAll(); .findAll();
for (MethodCall call : pendingMethodCalls) { for (MethodCall call : pendingMethodCalls) {
call.setSyncstate(SyncState.NOT_SYNCED); call.setSyncstate(SyncState.NOT_SYNCED);
} }
// clean up records.
realm.where(MethodCall.class)
.equalTo("serverConfigId", serverConfigId)
.beginGroup()
.equalTo("syncstate", SyncState.SYNCED)
.or()
.equalTo("syncstate", SyncState.FAILED)
.endGroup()
.findAll().deleteAllFromRealm();
return null; return null;
}).continueWith(new LogcatIfError()); }).continueWith(new LogcatIfError());
} }
...@@ -36,6 +47,7 @@ public class MethodCallObserver extends AbstractModelObserver<MethodCall> { ...@@ -36,6 +47,7 @@ public class MethodCallObserver extends AbstractModelObserver<MethodCall> {
@Override protected RealmResults<MethodCall> queryItems(Realm realm) { @Override protected RealmResults<MethodCall> queryItems(Realm realm) {
return realm.where(MethodCall.class) return realm.where(MethodCall.class)
.isNotNull("name") .isNotNull("name")
.equalTo("serverConfigId", serverConfigId)
.equalTo("syncstate", SyncState.NOT_SYNCED) .equalTo("syncstate", SyncState.NOT_SYNCED)
.findAll(); .findAll();
} }
...@@ -51,8 +63,8 @@ public class MethodCallObserver extends AbstractModelObserver<MethodCall> { ...@@ -51,8 +63,8 @@ public class MethodCallObserver extends AbstractModelObserver<MethodCall> {
final String params = call.getParamsJson(); final String params = call.getParamsJson();
RealmHelperBolts.executeTransaction(realm -> RealmHelperBolts.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(MethodCall.class, new JSONObject() realm.createOrUpdateObjectFromJson(MethodCall.class, new JSONObject()
.put("id", methodCallId) .put("id", methodCallId)
.put("syncstate", SyncState.SYNCING)) .put("syncstate", SyncState.SYNCING))
).onSuccessTask(task -> ).onSuccessTask(task ->
webSocketAPI.rpc(methodCallId, methodName, params).onSuccessTask(_task -> webSocketAPI.rpc(methodCallId, methodName, params).onSuccessTask(_task ->
RealmHelperBolts.executeTransaction(realm -> { RealmHelperBolts.executeTransaction(realm -> {
......
package chat.rocket.android.ws; package chat.rocket.android.ws;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Patterns;
import bolts.Task; import bolts.Task;
import chat.rocket.android.helper.OkHttpHelper; import chat.rocket.android.helper.OkHttpHelper;
import chat.rocket.android.helper.TextUtils; import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfigCredential;
import chat.rocket.android_ddp.DDPClient; import chat.rocket.android_ddp.DDPClient;
import chat.rocket.android_ddp.DDPClientCallback; import chat.rocket.android_ddp.DDPClientCallback;
import chat.rocket.android_ddp.DDPSubscription; import chat.rocket.android_ddp.DDPSubscription;
...@@ -94,56 +92,4 @@ public class RocketChatWebSocketAPI { ...@@ -94,56 +92,4 @@ public class RocketChatWebSocketAPI {
return Task.forError(exception); return Task.forError(exception);
} }
} }
/**
* Login with ServerConfigCredential.
*/
public Task<DDPClientCallback.RPC> login(ServerConfigCredential credential) {
JSONObject param = new JSONObject();
try {
String authType = credential.getType();
if ("email".equals(authType)) {
String username = credential.getUsername();
if (Patterns.EMAIL_ADDRESS.matcher(username).matches()) {
param.put("user", new JSONObject().put("email", username));
} else {
param.put("user", new JSONObject().put("username", username));
}
param.put("password", new JSONObject()
.put("digest", credential.getHashedPasswd())
.put("algorithm", "sha-256"));
} else if ("github".equals(authType) || "twitter".equals(authType)) {
param.put("oauth", new JSONObject()
.put("credentialToken", credential.getCredentialToken())
.put("credentialSecret", credential.getCredentialSecret()));
}
} catch (JSONException exception) {
return Task.forError(exception);
}
return ddpClient.rpc("login", new JSONArray().put(param), generateId("login"));
}
/**
* Login with token.
*/
public Task<DDPClientCallback.RPC> loginWithToken(final String token) {
JSONObject param = new JSONObject();
try {
param.put("resume", token);
} catch (JSONException exception) {
return Task.forError(exception);
}
return ddpClient.rpc("login", new JSONArray().put(param), generateId("login-token"));
}
/**
* Logout.
*/
public Task<DDPClientCallback.RPC> logout() {
return ddpClient.rpc("logout", null, generateId("logout"));
}
} }
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:minWidth="288dp"
android:padding="@dimen/margin_24"
android:gravity="center"
>
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginBottom="@dimen/margin_24"
android:scaleType="fitCenter"
android:src="@mipmap/ic_launcher"/>
<chat.rocket.android.widget.WaitingView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/txt_caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:layout_marginTop="@dimen/margin_16"/>
</LinearLayout>
\ No newline at end of file
...@@ -22,8 +22,7 @@ ...@@ -22,8 +22,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Caption" android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:layout_marginTop="@dimen/margin_32" android:layout_marginTop="@dimen/margin_32"/>
android:text="Connecting..."/>
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment