Commit efab24c2 authored by Yusuke Iwaki's avatar Yusuke Iwaki

implement User Registration

parent 1877e61d
......@@ -6,6 +6,7 @@ import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import chat.rocket.android.R;
import chat.rocket.android.helper.CheckSum;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.MeteorLoginServiceConfiguration;
......@@ -14,9 +15,6 @@ import chat.rocket.android.model.ServerConfigCredential;
import chat.rocket.android.renderer.ServerConfigCredentialRenderer;
import io.realm.Realm;
import io.realm.RealmResults;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import jp.co.crowdworks.realm_java_helpers.RealmHelper;
import jp.co.crowdworks.realm_java_helpers.RealmListObserver;
......@@ -69,11 +67,19 @@ public class LoginFragment extends AbstractServerConfigFragment {
.put("type", ServerConfigCredential.TYPE_EMAIL)
.put("errorMessage", JSONObject.NULL)
.put("username", username.toString())
.put("hashedPasswd", sha256sum(passwd.toString())))
.put("hashedPasswd", CheckSum.sha256(passwd.toString())))
)
).continueWith(new LogcatIfError());
});
final View btnUserRegistration = rootView.findViewById(R.id.btn_user_registration);
btnUserRegistration.setOnClickListener(view -> {
UserRegistrationDialogFragment.create(serverConfigId,
txtUsername.getText().toString(), txtPasswd.getText().toString())
.show(getFragmentManager(), UserRegistrationDialogFragment.class.getSimpleName());
});
showErrorIfNeeded();
}
......@@ -102,23 +108,6 @@ public class LoginFragment extends AbstractServerConfigFragment {
errorShowingHandler.sendMessageDelayed(msg, 160);
}
private static String sha256sum(String orig) {
MessageDigest messageDigest = null;
try {
messageDigest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exception) {
return null;
}
messageDigest.update(orig.getBytes(Charset.forName("UTF-8")));
StringBuilder stringBuilder = new StringBuilder();
for (byte b : messageDigest.digest()) {
stringBuilder.append(String.format("%02x", b & 0xff));
}
return stringBuilder.toString();
}
private void onRenderAuthProviders(List<MeteorLoginServiceConfiguration> authProviders) {
final View btnTwitter = rootView.findViewById(R.id.btn_login_with_twitter);
final View btnGitHub = rootView.findViewById(R.id.btn_login_with_github);
......
package chat.rocket.android.fragment.server_config;
import android.app.Dialog;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.util.Patterns;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import chat.rocket.android.R;
import chat.rocket.android.helper.CheckSum;
import chat.rocket.android.helper.MethodCallHelper;
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.
*/
public class UserRegistrationDialogFragment extends DialogFragment {
private String serverConfigId;
private String username;
private String email;
private String password;
/**
* create UserRegistrationDialogFragment with auto-detect email/username.
*/
public static UserRegistrationDialogFragment create(String serverConfigId,
String usernameOrEmail, String password) {
if (Patterns.EMAIL_ADDRESS.matcher(usernameOrEmail).matches()) {
return create(serverConfigId, null, usernameOrEmail, password);
} else {
return create(serverConfigId, usernameOrEmail, null, password);
}
}
/**
* create UserRegistrationDialogFragment.
*/
public static UserRegistrationDialogFragment create(String serverConfigId,
String username, String email, String password) {
Bundle args = new Bundle();
args.putString("serverConfigId", serverConfigId);
if (!TextUtils.isEmpty(username)) {
args.putString("username", username);
}
if (!TextUtils.isEmpty(email)) {
args.putString("email", email);
}
if (!TextUtils.isEmpty(password)) {
args.putString("password", password);
}
UserRegistrationDialogFragment dialog = new UserRegistrationDialogFragment();
dialog.setArguments(args);
return dialog;
}
public UserRegistrationDialogFragment() {
super();
}
@Override public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args != null) {
serverConfigId = args.getString("serverConfigId");
username = args.getString("username");
email = args.getString("email");
password = args.getString("password");
}
}
@NonNull
@Override public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getContext(), R.style.AppTheme_Dialog)
.setView(createDialogView())
.create();
}
private View createDialogView() {
View dialog = LayoutInflater.from(getContext())
.inflate(R.layout.dialog_user_registration, null, false);
final TextView txtUsername = (TextView) dialog.findViewById(R.id.editor_username);
final TextView txtEmail = (TextView) dialog.findViewById(R.id.editor_email);
final TextView txtPasswd = (TextView) dialog.findViewById(R.id.editor_passwd);
if (!TextUtils.isEmpty(username)) {
txtUsername.setText(username);
}
if (!TextUtils.isEmpty(email)) {
txtEmail.setText(email);
}
if (!TextUtils.isEmpty(password)) {
txtPasswd.setText(password);
}
final View waitingView = dialog.findViewById(R.id.waiting);
waitingView.setVisibility(View.GONE);
dialog.findViewById(R.id.btn_register_user).setOnClickListener(view -> {
view.setEnabled(false);
waitingView.setVisibility(View.VISIBLE);
username = txtUsername.getText().toString();
email = txtEmail.getText().toString();
password = txtPasswd.getText().toString();
MethodCallHelper.registerUser(username, email, password, password).onSuccessTask(task ->
RealmHelperBolts.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(ServerConfig.class, new JSONObject()
.put("id", serverConfigId)
.put("credential", new JSONObject()
.put("id", serverConfigId)
.put("type", ServerConfigCredential.TYPE_EMAIL)
.put("errorMessage", JSONObject.NULL)
.put("username", email)
.put("hashedPasswd", CheckSum.sha256(password)))
)
)
).onSuccessTask(task -> {
dismiss();
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;
}
private void showError(String errMessage) {
Toast.makeText(getContext(), errMessage, Toast.LENGTH_SHORT).show();
}
}
package chat.rocket.android.helper;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* SHA-256, MD5, ...
*/
public class CheckSum {
/**
* SHA-256.
*/
public static String sha256(String orig) {
MessageDigest messageDigest = null;
try {
messageDigest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException exception) {
return null;
}
messageDigest.update(orig.getBytes(Charset.forName("UTF-8")));
StringBuilder stringBuilder = new StringBuilder();
for (byte b : messageDigest.digest()) {
stringBuilder.append(String.format("%02x", b & 0xff));
}
return stringBuilder.toString();
}
}
package chat.rocket.android.helper;
import android.util.Patterns;
import bolts.Task;
import chat.rocket.android.model.MethodCall;
import chat.rocket.android.model.ServerConfigCredential;
import chat.rocket.android_ddp.DDPClientCallback;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import static android.icu.lang.UCharacter.GraphemeClusterBreak.T;
/**
* Utility class for creating/handling MethodCall.
*/
public class MethodCallHelper {
private static <T> Task<T> injectErrorMessageHandler(Task<T> task) {
return task.continueWithTask(_task -> {
if (_task.isFaulted()) {
Exception exception = _task.getError();
if (exception instanceof MethodCall.Error) {
String errMessage = new JSONObject(exception.getMessage()).getString("message");
return Task.forError(new Exception(errMessage));
} else {
return Task.forError(exception);
}
} else {
return _task;
}
});
}
/**
* Register User.
*/
public static Task<Void> registerUser(String name, String email, String passwd,
String confirmPasswd) {
JSONObject param = new JSONObject();
try {
param.put("name", name).put("email", email)
.put("pass", passwd).put("confirm-pass", confirmPasswd);
} catch (JSONException exception) {
return Task.forError(exception);
}
return injectErrorMessageHandler(MethodCall.execute("registerUser", param.toString()))
.onSuccessTask(task -> Task.forResult(null)); // nothing to do?
}
}
package chat.rocket.android.model;
import bolts.Task;
import bolts.TaskCompletionSource;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.helper.TextUtils;
import io.realm.Realm;
import io.realm.RealmObject;
import io.realm.RealmQuery;
import io.realm.annotations.PrimaryKey;
import java.util.UUID;
import jp.co.crowdworks.realm_java_helpers.RealmObjectObserver;
import jp.co.crowdworks.realm_java_helpers_bolts.RealmHelperBolts;
import org.json.JSONException;
import org.json.JSONObject;
@SuppressWarnings("PMD.ShortVariable")
public class MethodCall extends RealmObject {
@PrimaryKey private String id;
private int syncstate;
private String name;
private String paramsJson;
private String resultJson;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getSyncstate() {
return syncstate;
}
public void setSyncstate(int syncstate) {
this.syncstate = syncstate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getParamsJson() {
return paramsJson;
}
public void setParamsJson(String paramsJson) {
this.paramsJson = paramsJson;
}
public String getResultJson() {
return resultJson;
}
public void setResultJson(String resultJson) {
this.resultJson = resultJson;
}
public static class Error extends Exception {
public Error(String message) {
super(message);
}
public Error(Throwable exception) {
super(exception);
}
}
public static Task<JSONObject> execute(String name, String paramsJson) {
final String newId = UUID.randomUUID().toString();
TaskCompletionSource<JSONObject> task = new TaskCompletionSource<>();
RealmHelperBolts.executeTransaction(realm -> {
MethodCall call = realm.createObject(MethodCall.class, newId);
call.setSyncstate(SyncState.NOT_SYNCED);
call.setName(name);
call.setParamsJson(paramsJson);
return null;
}).continueWith(_task -> {
if (_task.isFaulted()) {
task.setError(_task.getError());
} else {
new RealmObjectObserver<MethodCall>() {
@Override protected RealmQuery<MethodCall> query(Realm realm) {
return realm.where(MethodCall.class).equalTo("id", newId);
}
@Override protected void onChange(MethodCall methodCall) {
int syncstate = methodCall.getSyncstate();
if (syncstate == SyncState.SYNCED) {
try {
String resultJson = methodCall.getResultJson();
task.setResult(TextUtils.isEmpty(resultJson) ? null : new JSONObject(resultJson));
} catch (JSONException exception) {
task.setError(new Error(exception));
}
exit(methodCall.getId());
} else if (syncstate == SyncState.FAILED) {
task.setError(new Error(methodCall.getResultJson()));
exit(methodCall.getId());
}
}
private void exit(String newId) {
unsub();
remove(newId).continueWith(new LogcatIfError());
}
}.sub();
}
return null;
});
return task.getTask();
}
public static final Task<Void> remove(String id) {
return RealmHelperBolts.executeTransaction(realm ->
realm.where(MethodCall.class).equalTo("id", id).findAll().deleteAllFromRealm());
}
}
package chat.rocket.android.model;
/**
* The sync status of each model.
*/
public interface SyncState {
int NOT_SYNCED = 0;
int SYNCING = 1;
int SYNCED = 2;
int FAILED = 3;
}
......@@ -11,6 +11,7 @@ import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfig;
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.ws.RocketChatWebSocketAPI;
import chat.rocket.android_ddp.DDPClientCallback;
import hugo.weaving.DebugLog;
......@@ -28,7 +29,8 @@ import timber.log.Timber;
public class RocketChatWebSocketThread extends HandlerThread {
private static final Class[] REGISTERABLE_CLASSES = {
LoginServiceConfigurationSubscriber.class,
LoginCredentialObserver.class
LoginCredentialObserver.class,
MethodCallObserver.class
};
private final Context appContext;
private final String serverConfigId;
......
package chat.rocket.android.service.observer;
import android.content.Context;
import chat.rocket.android.helper.LogcatIfError;
import chat.rocket.android.model.MethodCall;
import chat.rocket.android.model.SyncState;
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;
/**
* Observing MethodCall record, executing RPC if needed.
*/
public class MethodCallObserver extends AbstractModelObserver<MethodCall> {
/**
* constructor.
*/
public MethodCallObserver(Context context, String serverConfigId, RocketChatWebSocketAPI api) {
super(context, serverConfigId, api);
RealmHelperBolts.executeTransaction(realm -> {
RealmResults<MethodCall> pendingMethodCalls = realm.where(MethodCall.class)
.equalTo("syncstate", SyncState.SYNCING)
.findAll();
for (MethodCall call : pendingMethodCalls) {
call.setSyncstate(SyncState.NOT_SYNCED);
}
return null;
}).continueWith(new LogcatIfError());
}
@Override protected RealmResults<MethodCall> queryItems(Realm realm) {
return realm.where(MethodCall.class)
.isNotNull("name")
.equalTo("syncstate", SyncState.NOT_SYNCED)
.findAll();
}
@Override protected void onCollectionChanged(List<MethodCall> list) {
if (list == null || list.isEmpty()) {
return;
}
MethodCall call = list.get(0);
final String methodCallId = call.getId();
final String methodName = call.getName();
final String params = call.getParamsJson();
RealmHelperBolts.executeTransaction(realm ->
realm.createOrUpdateObjectFromJson(MethodCall.class, new JSONObject()
.put("id", methodCallId)
.put("syncstate", SyncState.SYNCING))
).onSuccessTask(task ->
webSocketAPI.rpc(methodCallId, methodName, params).onSuccessTask(_task ->
RealmHelperBolts.executeTransaction(realm -> {
JSONObject result = _task.getResult().result;
return realm.createOrUpdateObjectFromJson(MethodCall.class, new JSONObject()
.put("id", methodCallId)
.put("syncstate", SyncState.SYNCED)
.put("resultJson", result == null ? null : result.toString()));
})
)
).continueWith(task -> {
if (task.isFaulted()) {
RealmHelperBolts.executeTransaction(realm -> {
Exception exception = task.getError();
final String errMessage = (exception instanceof DDPClientCallback.RPC.Error)
? ((DDPClientCallback.RPC.Error) exception).error.toString()
: exception.getMessage();
realm.createOrUpdateObjectFromJson(MethodCall.class, new JSONObject()
.put("id", methodCallId)
.put("syncstate", SyncState.FAILED)
.put("resultJson", errMessage));
return null;
}).continueWith(new LogcatIfError());
}
return null;
});
}
}
......@@ -4,6 +4,7 @@ import android.support.annotation.Nullable;
import android.util.Patterns;
import bolts.Task;
import chat.rocket.android.helper.OkHttpHelper;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.model.ServerConfigCredential;
import chat.rocket.android_ddp.DDPClient;
import chat.rocket.android_ddp.DDPClientCallback;
......@@ -79,6 +80,21 @@ public class RocketChatWebSocketAPI {
return method + "-" + UUID.randomUUID().toString().replace("-", "");
}
/**
* Execute raw RPC.
*/
public Task<DDPClientCallback.RPC> rpc(String methodCallId, String methodName, String params) {
if (TextUtils.isEmpty(params)) {
return ddpClient.rpc(methodName, null, methodCallId);
}
try {
return ddpClient.rpc(methodName, new JSONArray().put(new JSONObject(params)), methodCallId);
} catch (JSONException exception) {
return Task.forError(exception);
}
}
/**
* Login with ServerConfigCredential.
*/
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF2D91FA"
android:pathData="M15,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM6,10L6,7L4,7v3L1,10v2h3v3h2v-3h3v-2L6,10zM15,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minWidth="288dp"
android:padding="@dimen/margin_24"
>
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<android.support.design.widget.TextInputEditText
android:id="@+id/editor_email"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="email address"
android:imeOptions="actionNext"
android:inputType="textWebEmailAddress"
android:singleLine="true"
/>
</android.support.design.widget.TextInputLayout>
<Space
android:layout_width="wrap_content"
android:layout_height="@dimen/margin_8"
/>
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<android.support.design.widget.TextInputEditText
android:id="@+id/editor_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="username"
android:imeOptions="actionNext"
android:inputType="textWebEmailAddress"
android:singleLine="true"
/>
</android.support.design.widget.TextInputLayout>
<Space
android:layout_width="wrap_content"
android:layout_height="@dimen/margin_8"
/>
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_passwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true"
>
<android.support.design.widget.TextInputEditText
android:id="@+id/editor_passwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="password"
android:imeOptions="actionNext"
android:inputType="textWebPassword"
android:singleLine="true"
/>
</android.support.design.widget.TextInputLayout>
<Space
android:layout_width="wrap_content"
android:layout_height="@dimen/margin_16"
/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/btn_register_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
app:elevation="2dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_arrow_forward_white_24dp"
/>
<chat.rocket.android.widget.WaitingView
android:id="@+id/waiting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:dotCount="5"
app:dotSize="12dp"
android:layout_gravity="center"
/>
</LinearLayout>
\ No newline at end of file
......@@ -12,96 +12,105 @@
android:layout_gravity="center"
android:background="@color/white"
android:minWidth="288dp"
android:orientation="horizontal"
android:orientation="vertical"
android:padding="@dimen/margin_24"
>
<LinearLayout
android:layout_width="0px"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:orientation="horizontal"
>
<chat.rocket.android.widget.FontAwesomeButton
android:id="@+id/btn_login_with_twitter"
android:layout_width="48dp"
android:layout_height="48dp"
android:text="@string/fa_twitter"
android:textSize="16dp"
android:layout_marginEnd="@dimen/margin_8"
android:enabled="false"
/>
<chat.rocket.android.widget.FontAwesomeButton
android:id="@+id/btn_login_with_github"
android:layout_width="48dp"
android:layout_height="48dp"
android:text="@string/fa_github"
android:textSize="16dp"
android:layout_marginEnd="@dimen/margin_8"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<chat.rocket.android.widget.FontAwesomeButton
android:id="@+id/btn_login_with_twitter"
android:layout_width="48dp"
android:layout_height="48dp"
android:text="@string/fa_twitter"
android:textSize="16dp"
android:layout_marginEnd="@dimen/margin_8"
android:enabled="false"
/>
<chat.rocket.android.widget.FontAwesomeButton
android:id="@+id/btn_login_with_github"
android:layout_width="48dp"
android:layout_height="48dp"
android:text="@string/fa_github"
android:textSize="16dp"
android:layout_marginEnd="@dimen/margin_8"
/>
</LinearLayout>
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_username"
<android.support.design.widget.TextInputEditText
android:id="@+id/editor_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
android:hint="username or email"
android:imeOptions="actionNext"
android:inputType="textWebEmailAddress"
android:singleLine="true"
/>
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputEditText
android:id="@+id/editor_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="username or email"
android:imeOptions="actionNext"
android:inputType="textWebEmailAddress"
android:singleLine="true"
/>
</android.support.design.widget.TextInputLayout>
<Space
android:layout_width="wrap_content"
android:layout_height="@dimen/margin_8"
/>
<Space
android:layout_width="wrap_content"
android:layout_height="@dimen/margin_8"
/>
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_passwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true"
>
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_passwd"
<android.support.design.widget.TextInputEditText
android:id="@+id/editor_passwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:passwordToggleEnabled="true"
>
<android.support.design.widget.TextInputEditText
android:id="@+id/editor_passwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="password"
android:imeOptions="actionNext"
android:inputType="textWebPassword"
android:singleLine="true"
/>
</android.support.design.widget.TextInputLayout>
</LinearLayout>
android:hint="password"
android:imeOptions="actionNext"
android:inputType="textWebPassword"
android:singleLine="true"
/>
</android.support.design.widget.TextInputLayout>
<Space
android:layout_width="@dimen/margin_8"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_height="@dimen/margin_16"
/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/btn_login_with_email"
android:layout_width="wrap_content"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
app:elevation="2dp"
app:fabSize="mini"
app:srcCompat="@drawable/ic_arrow_forward_white_24dp"
/>
android:orientation="horizontal"
>
<android.support.design.widget.FloatingActionButton
android:id="@+id/btn_user_registration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom"
app:elevation="2dp"
app:fabSize="mini"
app:backgroundTint="@color/white"
app:srcCompat="@drawable/ic_user_registration_blue_24dp"
/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/btn_login_with_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
app:elevation="2dp"
app:fabSize="normal"
app:srcCompat="@drawable/ic_arrow_forward_white_24dp"
/>
</FrameLayout>
</LinearLayout>
</FrameLayout>
\ No newline at end of file
......@@ -72,7 +72,7 @@ public class WaitingView extends LinearLayout {
FrameLayout frameLayout = new FrameLayout(context);
frameLayout.setLayoutParams(new LinearLayout.LayoutParams(size * 3 / 2, size * 3 / 2));
ImageView dot = new ImageView(context);
dot.setImageResource(R.drawable.white_circle);
dot.setImageResource(R.drawable.normal_circle);
dot.setLayoutParams(new FrameLayout.LayoutParams(size, size, Gravity.CENTER));
frameLayout.addView(dot);
addView(frameLayout);
......
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/white"/>
<solid android:color="?attr/colorControlNormal"/>
</shape>
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