diff --git a/android/app/src/main/cpp/native.cpp b/android/app/src/main/cpp/native.cpp index c858092f87..c7bca428e9 100644 --- a/android/app/src/main/cpp/native.cpp +++ b/android/app/src/main/cpp/native.cpp @@ -26,6 +26,7 @@ QAndroidJniObject __interfaceActivity; QAndroidJniObject __loginCompletedListener; +QAndroidJniObject __signupCompletedListener; QAndroidJniObject __loadCompleteListener; QAndroidJniObject __usernameChangedListener; void tempMessageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { @@ -267,6 +268,14 @@ Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeCancelLogin(JNIE } +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_SignupFragment_nativeCancelLogin(JNIEnv *env, + jobject instance) { + + Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeCancelLogin(env, instance); +} + + JNIEXPORT void JNICALL Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeLogin(JNIEnv *env, jobject instance, jstring username_, jstring password_, @@ -308,6 +317,67 @@ Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeLogin(JNIEnv *en Q_ARG(const QString&, username), Q_ARG(const QString&, password)); } +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_SignupFragment_nativeLogin(JNIEnv *env, + jobject instance, + jstring username_, + jstring password_, + jobject usernameChangedListener) { + Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeLogin(env, instance, username_, password_, usernameChangedListener); +} + +JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeInitAfterAppLoaded(JNIEnv* env, jobject obj) { + AndroidHelper::instance().moveToThread(qApp->thread()); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_SignupFragment_nativeSignup(JNIEnv *env, jobject instance, + jstring email_, jstring username_, + jstring password_) { + + const char *c_email = env->GetStringUTFChars(email_, 0); + const char *c_username = env->GetStringUTFChars(username_, 0); + const char *c_password = env->GetStringUTFChars(password_, 0); + QString email = QString(c_email); + QString username = QString(c_username); + QString password = QString(c_password); + env->ReleaseStringUTFChars(email_, c_email); + env->ReleaseStringUTFChars(username_, c_username); + env->ReleaseStringUTFChars(password_, c_password); + + __signupCompletedListener = QAndroidJniObject(instance); + + // disconnect any previous callback + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::handleSignupCompleted, nullptr, nullptr); + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::handleSignupFailed, nullptr, nullptr); + + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::handleSignupCompleted, []() { + jboolean jSuccess = (jboolean) true; + if (__signupCompletedListener.isValid()) { + __signupCompletedListener.callMethod("handleSignupCompleted", "()V", jSuccess); + } + }); + + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::handleSignupFailed, [](QString errorString) { + jboolean jSuccess = (jboolean) false; + jstring jError = QAndroidJniObject::fromString(errorString).object(); + if (__signupCompletedListener.isValid()) { + QAndroidJniObject string = QAndroidJniObject::fromString(errorString); + __signupCompletedListener.callMethod("handleSignupFailed", "(Ljava/lang/String;)V", string.object()); + } + }); + + AndroidHelper::instance().signup(email, username, password); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_SignupFragment_nativeCancelSignup(JNIEnv *env, jobject instance) { + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::handleSignupCompleted, nullptr, nullptr); + QObject::disconnect(&AndroidHelper::instance(), &AndroidHelper::handleSignupFailed, nullptr, nullptr); + + __signupCompletedListener = nullptr; +} + JNIEXPORT jboolean JNICALL Java_io_highfidelity_hifiinterface_fragment_FriendsFragment_nativeIsLoggedIn(JNIEnv *env, jobject instance) { auto accountManager = DependencyManager::get(); diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java index 3d43e20c63..90e411173b 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -69,6 +69,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW private native void nativeEnterBackground(); private native void nativeEnterForeground(); private native long nativeOnExitVr(); + private native void nativeInitAfterAppLoaded(); private AssetManager assetManager; @@ -351,6 +352,9 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW if (nativeEnterBackgroundCallEnqueued) { nativeEnterBackground(); } + runOnUiThread(() -> { + nativeInitAfterAppLoaded(); + }); } public void performHapticFeedback(int duration) { diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java b/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java index ff91409b9e..7df04100b0 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java @@ -37,12 +37,15 @@ import io.highfidelity.hifiinterface.fragment.HomeFragment; import io.highfidelity.hifiinterface.fragment.LoginFragment; import io.highfidelity.hifiinterface.fragment.PolicyFragment; import io.highfidelity.hifiinterface.fragment.SettingsFragment; -import io.highfidelity.hifiinterface.task.DownloadProfileImageTask; +import io.highfidelity.hifiinterface.fragment.SignedInFragment; +import io.highfidelity.hifiinterface.fragment.SignupFragment;import io.highfidelity.hifiinterface.task.DownloadProfileImageTask; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, LoginFragment.OnLoginInteractionListener, HomeFragment.OnHomeInteractionListener, - FriendsFragment.OnHomeInteractionListener { + FriendsFragment.OnHomeInteractionListener, + SignupFragment.OnSignupInteractionListener, + SignedInFragment.OnSignedInInteractionListener { private static final int PROFILE_PICTURE_PLACEHOLDER = R.drawable.default_profile_avatar; public static final String DEFAULT_FRAGMENT = "Home"; @@ -147,35 +150,44 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On private void loadHomeFragment(boolean addToBackStack) { Fragment fragment = HomeFragment.newInstance(); - loadFragment(fragment, getString(R.string.home), getString(R.string.tagFragmentHome), addToBackStack); + loadFragment(fragment, getString(R.string.home), getString(R.string.tagFragmentHome), addToBackStack, true); } private void loadLoginFragment() { Fragment fragment = LoginFragment.newInstance(); + loadFragment(fragment, getString(R.string.login), getString(R.string.tagFragmentLogin), true, true); + } - loadFragment(fragment, getString(R.string.login), getString(R.string.tagFragmentLogin), true); + private void loadSignedInFragment() { + Fragment fragment = SignedInFragment.newInstance(); + loadFragment(fragment, getString(R.string.welcome), getString(R.string.tagFragmentSignedIn), true, true); + } + + private void loadSignupFragment() { + Fragment fragment = SignupFragment.newInstance(); + loadFragment(fragment, getString(R.string.signup), getString(R.string.tagFragmentSignup), true, false); } private void loadPrivacyPolicyFragment() { Fragment fragment = PolicyFragment.newInstance(); - loadFragment(fragment, getString(R.string.privacyPolicy), getString(R.string.tagFragmentPolicy), true); + loadFragment(fragment, getString(R.string.privacyPolicy), getString(R.string.tagFragmentPolicy), true, true); } private void loadPeopleFragment() { Fragment fragment = FriendsFragment.newInstance(); - loadFragment(fragment, getString(R.string.people), getString(R.string.tagFragmentPeople), true); + loadFragment(fragment, getString(R.string.people), getString(R.string.tagFragmentPeople), true, true); } private void loadSettingsFragment() { SettingsFragment fragment = SettingsFragment.newInstance(); - loadFragment(fragment, getString(R.string.settings), getString(R.string.tagSettings), true); + loadFragment(fragment, getString(R.string.settings), getString(R.string.tagSettings), true, true); } - private void loadFragment(Fragment fragment, String title, String tag, boolean addToBackStack) { + private void loadFragment(Fragment newFragment, String title, String tag, boolean addToBackStack, boolean goBackUntilHome) { FragmentManager fragmentManager = getFragmentManager(); // check if it's the same fragment @@ -187,17 +199,19 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On return; // cancel as we are already in that fragment } - // go back until first transaction - int backStackEntryCount = fragmentManager.getBackStackEntryCount(); - for (int i = 0; i < backStackEntryCount - 1; i++) { - fragmentManager.popBackStackImmediate(); + if (goBackUntilHome) { + // go back until first transaction + int backStackEntryCount = fragmentManager.getBackStackEntryCount(); + for (int i = 0; i < backStackEntryCount - 1; i++) { + fragmentManager.popBackStackImmediate(); + } } // this case is when we wanted to go home.. rollback already did that! // But asking for a new Home fragment makes it easier to have an updated list so we let it to continue FragmentTransaction ft = fragmentManager.beginTransaction(); - ft.replace(R.id.content_frame, fragment, tag); + ft.replace(R.id.content_frame, newFragment, tag); if (addToBackStack) { ft.addToBackStack(title); @@ -334,6 +348,32 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On } } + @Override + public void onGettingStarted() { + loadHomeFragment(false); + if (backToScene) { + backToScene = false; + goToLastLocation(); + } + } + + @Override + public void onLoginRequested() { + // go back from signup to login + onBackPressed(); + } + + @Override + public void onSignupRequested() { + loadSignupFragment(); + } + + @Override + public void onSignupCompleted() { + loadSignedInFragment(); + updateLoginMenu(); + } + public void handleUsernameChanged(String username) { runOnUiThread(() -> updateProfileHeader(username)); } diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java index 92cdec19a1..f22e5cd6bb 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/LoginFragment.java @@ -30,6 +30,7 @@ public class LoginFragment extends Fragment { private EditText mPassword; private TextView mError; private TextView mForgotPassword; + private TextView mSignup; private Button mLoginButton; private ProgressDialog mDialog; @@ -58,10 +59,12 @@ public class LoginFragment extends Fragment { mError = rootView.findViewById(R.id.error); mLoginButton = rootView.findViewById(R.id.loginButton); mForgotPassword = rootView.findViewById(R.id.forgotPassword); + mSignup = rootView.findViewById(R.id.signupButton); mLoginButton.setOnClickListener(view -> login()); mForgotPassword.setOnClickListener(view -> forgotPassword()); + mSignup.setOnClickListener(view -> signup()); mPassword.setOnEditorActionListener( (textView, actionId, keyEvent) -> { @@ -121,6 +124,12 @@ public class LoginFragment extends Fragment { } } + public void signup() { + if (mListener != null) { + mListener.onSignupRequested(); + } + } + private void hideKeyboard() { View view = getActivity().getCurrentFocus(); if (view != null) { @@ -182,6 +191,7 @@ public class LoginFragment extends Fragment { public interface OnLoginInteractionListener { void onLoginCompleted(); + void onSignupRequested(); } } diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignedInFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignedInFragment.java new file mode 100644 index 0000000000..9ed2f1c7f5 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignedInFragment.java @@ -0,0 +1,73 @@ +package io.highfidelity.hifiinterface.fragment; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.text.Html; +import android.text.Spanned; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import java.io.IOException; +import java.io.InputStream; + +import io.highfidelity.hifiinterface.R; + +public class SignedInFragment extends Fragment { + + private Button mGetStartedButton; + private OnSignedInInteractionListener mListener; + + public SignedInFragment() { + // Required empty public constructor + } + + public static SignedInFragment newInstance() { + SignedInFragment fragment = new SignedInFragment(); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_signedin, container, false); + mGetStartedButton = rootView.findViewById(R.id.getStarted); + + mGetStartedButton.setOnClickListener(view -> { + getStarted(); + }); + + return rootView; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof SignedInFragment.OnSignedInInteractionListener) { + mListener = (SignedInFragment.OnSignedInInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement OnSignedInInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + public void getStarted() { + if (mListener != null) { + mListener.onGettingStarted(); + } + } + + public interface OnSignedInInteractionListener { + void onGettingStarted(); + } + +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java new file mode 100644 index 0000000000..33644e5bda --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SignupFragment.java @@ -0,0 +1,217 @@ +package io.highfidelity.hifiinterface.fragment; + +import android.app.Activity; +import android.app.Fragment; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import org.qtproject.qt5.android.QtNative; + +import io.highfidelity.hifiinterface.R; + +import static org.qtproject.qt5.android.QtActivityDelegate.ApplicationActive; +import static org.qtproject.qt5.android.QtActivityDelegate.ApplicationInactive; + +public class SignupFragment extends Fragment { + + private EditText mEmail; + private EditText mUsername; + private EditText mPassword; + private TextView mError; + private TextView mCancelButton; + + private Button mSignupButton; + + private ProgressDialog mDialog; + + public native void nativeSignup(String email, String username, String password); // move to SignupFragment + public native void nativeCancelSignup(); + public native void nativeLogin(String username, String password, Activity usernameChangedListener); + public native void nativeCancelLogin(); + + private SignupFragment.OnSignupInteractionListener mListener; + + public SignupFragment() { + // Required empty public constructor + } + + public static SignupFragment newInstance() { + SignupFragment fragment = new SignupFragment(); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_signup, container, false); + + mEmail = rootView.findViewById(R.id.email); + mUsername = rootView.findViewById(R.id.username); + mPassword = rootView.findViewById(R.id.password); + mError = rootView.findViewById(R.id.error); + mSignupButton = rootView.findViewById(R.id.signupButton); + mCancelButton = rootView.findViewById(R.id.cancelButton); + + mSignupButton.setOnClickListener(view -> signup()); + mCancelButton.setOnClickListener(view -> login()); + mPassword.setOnEditorActionListener( + (textView, actionId, keyEvent) -> { + if (actionId == EditorInfo.IME_ACTION_DONE) { + mSignupButton.performClick(); + return true; + } + return false; + }); + return rootView; + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnSignupInteractionListener) { + mListener = (OnSignupInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement OnSignupInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + @Override + public void onResume() { + super.onResume(); + // This hack intends to keep Qt threads running even after the app comes from background + QtNative.setApplicationState(ApplicationActive); + } + + @Override + public void onStop() { + super.onStop(); + cancelActivityIndicator(); + // Leave the Qt app paused + QtNative.setApplicationState(ApplicationInactive); + hideKeyboard(); + } + + private void login() { + if (mListener != null) { + mListener.onLoginRequested(); + } + } + + public void signup() { + String email = mEmail.getText().toString().trim(); + String username = mUsername.getText().toString().trim(); + String password = mPassword.getText().toString(); + hideKeyboard(); + if (email.isEmpty() || username.isEmpty() || password.isEmpty()) { + showError(getString(R.string.signup_email_username_or_password_incorrect)); + } else { + mSignupButton.setEnabled(false); + hideError(); + showActivityIndicator(); + nativeSignup(email, username, password); + } + } + + private void hideKeyboard() { + View view = getActivity().getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + private void showActivityIndicator() { + if (mDialog == null) { + mDialog = new ProgressDialog(getContext()); + } + mDialog.setMessage(getString(R.string.creating_account)); + mDialog.setCancelable(true); + mDialog.setOnCancelListener(dialogInterface -> { + nativeCancelSignup(); + cancelActivityIndicator(); + mSignupButton.setEnabled(true); + }); + mDialog.show(); + } + + private void cancelActivityIndicator() { + if (mDialog != null) { + mDialog.cancel(); + } + } + private void showError(String error) { + mError.setText(error); + mError.setVisibility(View.VISIBLE); + } + + private void hideError() { + mError.setText(""); + mError.setVisibility(View.INVISIBLE); + } + + public interface OnSignupInteractionListener { + void onSignupCompleted(); + void onLoginRequested(); + } + + public void handleSignupCompleted() { + String username = mUsername.getText().toString().trim(); + String password = mPassword.getText().toString(); + mDialog.setMessage(getString(R.string.logging_in)); + mDialog.setCancelable(true); + mDialog.setOnCancelListener(dialogInterface -> { + nativeCancelLogin(); + cancelActivityIndicator(); + if (mListener != null) { + mListener.onLoginRequested(); + } + }); + mDialog.show(); + nativeLogin(username, password, getActivity()); + } + + public void handleSignupFailed(String error) { + getActivity().runOnUiThread(() -> { + mSignupButton.setEnabled(true); + cancelActivityIndicator(); + mError.setText(error); + mError.setVisibility(View.VISIBLE); + }); + } + + public void handleLoginCompleted(boolean success) { + getActivity().runOnUiThread(() -> { + mSignupButton.setEnabled(true); + cancelActivityIndicator(); + + if (success) { + if (mListener != null) { + mListener.onSignupCompleted(); + } + } else { + // Registration was successful but login failed. + // Let the user to login manually + mListener.onLoginRequested(); + } + }); + } + + + +} diff --git a/android/app/src/main/res/drawable/rounded_secondary_button.xml b/android/app/src/main/res/drawable/rounded_secondary_button.xml new file mode 100644 index 0000000000..6230885b30 --- /dev/null +++ b/android/app/src/main/res/drawable/rounded_secondary_button.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_login.xml b/android/app/src/main/res/layout/fragment_login.xml index 46ed783898..6933ad1eb5 100644 --- a/android/app/src/main/res/layout/fragment_login.xml +++ b/android/app/src/main/res/layout/fragment_login.xml @@ -21,10 +21,12 @@ android:id="@+id/error" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginBottom="16dp" + android:layout_marginBottom="25dp" + android:layout_marginLeft="9dp" + android:layout_marginRight="9dp" android:fontFamily="@font/raleway" android:textColor="@color/colorLoginError" - android:textSize="12sp" + android:textSize="14sp" app:layout_constraintBottom_toTopOf="@id/username" app:layout_constraintLeft_toLeftOf="@id/username" android:visibility="invisible"/> @@ -91,35 +93,50 @@ android:id="@+id/loginButton" android:layout_width="154dp" android:layout_height="38dp" - android:layout_marginTop="16dp" android:background="@drawable/rounded_button" android:fontFamily="@font/raleway_semibold" android:paddingBottom="0dp" - android:paddingLeft="55dp" - android:paddingRight="55dp" android:paddingTop="0dp" android:text="@string/login" android:textColor="@color/white_opaque" android:textAllCaps="false" - android:textSize="15sp" + android:textSize="18sp" app:layout_constraintRight_toRightOf="@id/username" - app:layout_constraintTop_toBottomOf="@id/passwordLayout" + app:layout_constraintTop_toBottomOf="@id/forgotPassword" app:layout_goneMarginTop="4dp"/> + +