diff --git a/android/app/build.gradle b/android/app/build.gradle index d3463411b8..24c067b176 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -133,6 +133,10 @@ dependencies { implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation 'com.android.support:design:26.1.0' + compile 'com.android.support:support-v4:26.1.0' + compile 'com.android.support:appcompat-v7:26.1.0' + compile 'com.android.support:support-vector-drawable:26.1.0' + implementation 'com.android.support:appcompat-v7:26.1.0' compile 'com.android.support:recyclerview-v7:26.1.0' compile 'com.android.support:cardview-v7:26.1.0' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7255e1f295..b216819ed0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + @@ -75,6 +76,15 @@ android:enabled="true" android:exported="false" android:process=":breakpad_uploader"/> + + + + + + diff --git a/android/app/src/main/cpp/native.cpp b/android/app/src/main/cpp/native.cpp index ce5af01f29..c858092f87 100644 --- a/android/app/src/main/cpp/native.cpp +++ b/android/app/src/main/cpp/native.cpp @@ -156,7 +156,7 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCrea JavaVM* jvm; env->GetJavaVM(&jvm); - QObject::connect(&AndroidHelper::instance(), &AndroidHelper::androidActivityRequested, [jvm](const QString& a, const bool backToScene, QList args) { + QObject::connect(&AndroidHelper::instance(), &AndroidHelper::androidActivityRequested, [jvm](const QString& a, const bool backToScene, QMap args) { JNIEnv* myNewEnv; JavaVMAttachArgs jvmArgs; jvmArgs.version = JNI_VERSION_1_6; // choose your JNI version @@ -182,9 +182,11 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_InterfaceActivity_nativeOnCrea jmethodID mapClassConstructor = myNewEnv->GetMethodID(hashMapClass, "", "()V"); jobject hashmap = myNewEnv->NewObject(hashMapClass, mapClassConstructor); jmethodID mapClassPut = myNewEnv->GetMethodID(hashMapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); - for (const QString& arg: args) { - QAndroidJniObject jArg = QAndroidJniObject::fromString(arg); - myNewEnv->CallObjectMethod(hashmap, mapClassPut, QAndroidJniObject::fromString("url").object(), jArg.object()); + QMap::iterator i; + for (i = args.begin(); i != args.end(); ++i) { + QAndroidJniObject jKey = QAndroidJniObject::fromString(i.key()); + QAndroidJniObject jValue = QAndroidJniObject::fromString(i.value()); + myNewEnv->CallObjectMethod(hashmap, mapClassPut, jKey.object(), jValue.object()); } __interfaceActivity.callMethod("openAndroidActivity", "(Ljava/lang/String;ZLjava/util/HashMap;)V", string.object(), jBackToScene, hashmap); if (attachedHere) { @@ -255,6 +257,16 @@ JNIEXPORT jstring JNICALL Java_io_highfidelity_hifiinterface_fragment_HomeFragme return env->NewStringUTF(lastLocation.toString().toLatin1().data()); } +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeCancelLogin(JNIEnv *env, jobject instance) { + + auto accountManager = DependencyManager::get(); + + QObject::disconnect(accountManager.data(), &AccountManager::loginComplete, nullptr, nullptr); + QObject::disconnect(accountManager.data(), &AccountManager::loginFailed, nullptr, nullptr); + +} + JNIEXPORT void JNICALL Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeLogin(JNIEnv *env, jobject instance, jstring username_, jstring password_, @@ -273,17 +285,23 @@ Java_io_highfidelity_hifiinterface_fragment_LoginFragment_nativeLogin(JNIEnv *en QObject::connect(accountManager.data(), &AccountManager::loginComplete, [](const QUrl& authURL) { jboolean jSuccess = (jboolean) true; - __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + if (__loginCompletedListener.isValid()) { + __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + } }); QObject::connect(accountManager.data(), &AccountManager::loginFailed, []() { jboolean jSuccess = (jboolean) false; - __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + if (__loginCompletedListener.isValid()) { + __loginCompletedListener.callMethod("handleLoginCompleted", "(Z)V", jSuccess); + } }); QObject::connect(accountManager.data(), &AccountManager::usernameChanged, [](const QString& username) { QAndroidJniObject string = QAndroidJniObject::fromString(username); - __usernameChangedListener.callMethod("handleUsernameChanged", "(Ljava/lang/String;)V", string.object()); + if (__usernameChangedListener.isValid()) { + __usernameChangedListener.callMethod("handleUsernameChanged", "(Ljava/lang/String;)V", string.object()); + } }); QMetaObject::invokeMethod(accountManager.data(), "requestAccessToken", @@ -355,5 +373,51 @@ JNIEXPORT void Java_io_highfidelity_hifiinterface_WebViewActivity_nativeProcessU AndroidHelper::instance().processURL(QString::fromUtf8(nativeString)); } +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_fragment_SettingsFragment_updateHifiSetting(JNIEnv *env, + jobject instance, + jstring group_, + jstring key_, + jboolean value_) { + const char *c_group = env->GetStringUTFChars(group_, 0); + const char *c_key = env->GetStringUTFChars(key_, 0); + + const QString group = QString::fromUtf8(c_group); + const QString key = QString::fromUtf8(c_key); + + env->ReleaseStringUTFChars(group_, c_group); + env->ReleaseStringUTFChars(key_, c_key); + + bool value = value_; + + Setting::Handle setting { QStringList() << group << key , !value }; + setting.set(value); +} + +JNIEXPORT jboolean JNICALL +Java_io_highfidelity_hifiinterface_fragment_SettingsFragment_getHifiSettingBoolean(JNIEnv *env, + jobject instance, + jstring group_, + jstring key_, + jboolean defaultValue) { + const char *c_group = env->GetStringUTFChars(group_, 0); + const char *c_key = env->GetStringUTFChars(key_, 0); + + const QString group = QString::fromUtf8(c_group); + const QString key = QString::fromUtf8(c_key); + + env->ReleaseStringUTFChars(group_, c_group); + env->ReleaseStringUTFChars(key_, c_key); + + Setting::Handle setting { QStringList() << group << key , defaultValue}; + return setting.get(); +} + +JNIEXPORT void JNICALL +Java_io_highfidelity_hifiinterface_receiver_HeadsetStateReceiver_notifyHeadsetOn(JNIEnv *env, + jobject instance, + jboolean pluggedIn) { + AndroidHelper::instance().notifyHeadsetOn(pluggedIn); +} } 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 f161783d6a..3d43e20c63 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/InterfaceActivity.java @@ -13,6 +13,7 @@ package io.highfidelity.hifiinterface; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -38,8 +39,10 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; import io.highfidelity.hifiinterface.fragment.WebViewFragment; +import io.highfidelity.hifiinterface.receiver.HeadsetStateReceiver; /*import com.google.vr.cardboard.DisplaySynchronizer; import com.google.vr.cardboard.DisplayUtils; @@ -55,6 +58,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW private static final int NORMAL_DPI = 160; private Vibrator mVibrator; + private HeadsetStateReceiver headsetStateReceiver; //public static native void handleHifiURL(String hifiURLString); private native long nativeOnCreate(InterfaceActivity instance, AssetManager assetManager); @@ -151,6 +155,8 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW layoutParams.resolveLayoutDirection(View.LAYOUT_DIRECTION_RTL); qtLayout.addView(webSlidingDrawer, layoutParams); webSlidingDrawer.setVisibility(View.GONE); + + headsetStateReceiver = new HeadsetStateReceiver(); } @Override @@ -161,6 +167,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW } else { nativeEnterBackground(); } + unregisterReceiver(headsetStateReceiver); //gvrApi.pauseTracking(); } @@ -183,6 +190,7 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW nativeEnterForeground(); surfacesWorkaround(); keepInterfaceRunning = false; + registerReceiver(headsetStateReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); //gvrApi.resumeTracking(); } @@ -296,14 +304,22 @@ public class InterfaceActivity extends QtActivity implements WebViewFragment.OnW switch (activityName) { case "Home": case "Privacy Policy": - case "Login": { nativeBeforeEnterBackground(); Intent intent = new Intent(this, MainActivity.class); intent.putExtra(MainActivity.EXTRA_FRAGMENT, activityName); intent.putExtra(MainActivity.EXTRA_BACK_TO_SCENE, backToScene); startActivity(intent); break; - } + case "Login": + nativeBeforeEnterBackground(); + Intent loginIntent = new Intent(this, MainActivity.class); + loginIntent.putExtra(MainActivity.EXTRA_FRAGMENT, activityName); + loginIntent.putExtra(MainActivity.EXTRA_BACK_TO_SCENE, backToScene); + if (args != null && args.containsKey(DOMAIN_URL)) { + loginIntent.putExtra(DOMAIN_URL, (String) args.get(DOMAIN_URL)); + } + startActivity(loginIntent); + break; case "WebView": runOnUiThread(() -> { webSlidingDrawer.setVisibility(View.VISIBLE); 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 db6f0fca24..ff91409b9e 100644 --- a/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/MainActivity.java @@ -29,10 +29,14 @@ import android.widget.TextView; import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; +import java.util.HashMap; +import java.util.Map; + import io.highfidelity.hifiinterface.fragment.FriendsFragment; 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; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, @@ -44,6 +48,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On public static final String DEFAULT_FRAGMENT = "Home"; public static final String EXTRA_FRAGMENT = "fragment"; public static final String EXTRA_BACK_TO_SCENE = "backToScene"; + public static final String EXTRA_BACK_TO_URL = "url"; private String TAG = "HighFidelity"; @@ -61,6 +66,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On private MenuItem mPeopleMenuItem; private boolean backToScene; + private String backToUrl; @Override protected void onCreate(Bundle savedInstanceState) { @@ -80,6 +86,8 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On mPeopleMenuItem = mNavigationView.getMenu().findItem(R.id.action_people); + updateDebugMenu(mNavigationView.getMenu()); + Toolbar toolbar = findViewById(R.id.toolbar); toolbar.setTitleTextAppearance(this, R.style.HomeActionBarTitleStyle); setSupportActionBar(toolbar); @@ -102,8 +110,17 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On loadFragment(DEFAULT_FRAGMENT); } - if (getIntent().hasExtra(EXTRA_BACK_TO_SCENE)) { - backToScene = getIntent().getBooleanExtra(EXTRA_BACK_TO_SCENE, false); + backToScene = getIntent().getBooleanExtra(EXTRA_BACK_TO_SCENE, false); + backToUrl = getIntent().getStringExtra(EXTRA_BACK_TO_URL); + } + } + + private void updateDebugMenu(Menu menu) { + if (BuildConfig.DEBUG) { + for (int i=0; i < menu.size(); i++) { + if (menu.getItem(i).getItemId() == R.id.action_debug_settings) { + menu.getItem(i).setVisible(true); + } } } } @@ -151,6 +168,13 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On loadFragment(fragment, getString(R.string.people), getString(R.string.tagFragmentPeople), true); } + private void loadSettingsFragment() { + SettingsFragment fragment = SettingsFragment.newInstance(); + + loadFragment(fragment, getString(R.string.settings), getString(R.string.tagSettings), true); + } + + private void loadFragment(Fragment fragment, String title, String tag, boolean addToBackStack) { FragmentManager fragmentManager = getFragmentManager(); @@ -241,6 +265,9 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On case R.id.action_people: loadPeopleFragment(); return true; + case R.id.action_debug_settings: + loadSettingsFragment(); + return true; } return false; } @@ -278,7 +305,7 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On } private void goToLastLocation() { - goToDomain(""); + goToDomain(backToUrl != null? backToUrl : ""); } private void goToDomain(String domainUrl) { 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 f29c237ed7..92cdec19a1 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 @@ -4,12 +4,10 @@ import android.app.Activity; import android.app.Fragment; import android.app.ProgressDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -19,8 +17,13 @@ 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 LoginFragment extends Fragment { private EditText mUsername; @@ -32,6 +35,7 @@ public class LoginFragment extends Fragment { private ProgressDialog mDialog; public native void nativeLogin(String username, String password, Activity usernameChangedListener); + public native void nativeCancelLogin(); private LoginFragment.OnLoginInteractionListener mListener; @@ -55,44 +59,6 @@ public class LoginFragment extends Fragment { mLoginButton = rootView.findViewById(R.id.loginButton); mForgotPassword = rootView.findViewById(R.id.forgotPassword); - mUsername.addTextChangedListener(new TextWatcher() { - boolean ignoreNextChange = false; - boolean hadBlankSpace = false; - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - hadBlankSpace = charSequence.length() > 0 && charSequence.charAt(charSequence.length()-1) == ' '; - } - - @Override - public void onTextChanged(CharSequence charSequence, int start, int count, int after) { - - } - - @Override - public void afterTextChanged(Editable editable) { - if (!ignoreNextChange) { - ignoreNextChange = true; - boolean spaceFound = false; - for (int i = 0; i < editable.length(); i++) { - if (editable.charAt(i) == ' ') { - spaceFound=true; - editable.delete(i, i + 1); - i--; - } - } - - if (hadBlankSpace && !spaceFound && editable.length() > 0) { - editable.delete(editable.length()-1, editable.length()); - } - - editable.append(' '); - ignoreNextChange = false; - } - - } - }); - - mLoginButton.setOnClickListener(view -> login()); mForgotPassword.setOnClickListener(view -> forgotPassword()); @@ -125,10 +91,19 @@ public class LoginFragment extends Fragment { 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(); } @@ -164,7 +139,15 @@ public class LoginFragment extends Fragment { mDialog = new ProgressDialog(getContext()); } mDialog.setMessage(getString(R.string.logging_in)); - mDialog.setCancelable(false); + mDialog.setCancelable(true); + mDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialogInterface) { + nativeCancelLogin(); + cancelActivityIndicator(); + mLoginButton.setEnabled(true); + } + }); mDialog.show(); } @@ -184,7 +167,6 @@ public class LoginFragment extends Fragment { } public void handleLoginCompleted(boolean success) { - Log.d("[LOGIN]", "handleLoginCompleted " + success); getActivity().runOnUiThread(() -> { mLoginButton.setEnabled(true); cancelActivityIndicator(); diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java new file mode 100644 index 0000000000..cc23665e72 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/fragment/SettingsFragment.java @@ -0,0 +1,63 @@ +package io.highfidelity.hifiinterface.fragment; + +import android.content.SharedPreferences; +import android.media.audiofx.AcousticEchoCanceler; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.support.annotation.Nullable; + +import io.highfidelity.hifiinterface.R; + +public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { + + public native void updateHifiSetting(String group, String key, boolean value); + public native boolean getHifiSettingBoolean(String group, String key, boolean defaultValue); + + private final String HIFI_SETTINGS_ANDROID_GROUP = "Android"; + private final String HIFI_SETTINGS_AEC_KEY = "aec"; + private final String PREFERENCE_KEY_AEC = "aec"; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.settings); + + if (!AcousticEchoCanceler.isAvailable()) { + getPreferenceScreen().getPreferenceManager().findPreference("aec").setEnabled(false); + } + + getPreferenceScreen().getSharedPreferences().edit().putBoolean(PREFERENCE_KEY_AEC, + getHifiSettingBoolean(HIFI_SETTINGS_ANDROID_GROUP, HIFI_SETTINGS_AEC_KEY, false)); + } + + public static SettingsFragment newInstance() { + SettingsFragment fragment = new SettingsFragment(); + return fragment; + } + + @Override + public void onResume() { + super.onResume(); + getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() { + super.onPause(); + getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + Preference pref = findPreference(key); + switch (key) { + case "aec": + updateHifiSetting(HIFI_SETTINGS_ANDROID_GROUP, HIFI_SETTINGS_AEC_KEY, sharedPreferences.getBoolean(key, false)); + break; + default: + break; + } + } +} diff --git a/android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java b/android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java new file mode 100644 index 0000000000..5645912d73 --- /dev/null +++ b/android/app/src/main/java/io/highfidelity/hifiinterface/receiver/HeadsetStateReceiver.java @@ -0,0 +1,18 @@ +package io.highfidelity.hifiinterface.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.util.Log; + +public class HeadsetStateReceiver extends BroadcastReceiver { + + private native void notifyHeadsetOn(boolean pluggedIn); + + @Override + public void onReceive(Context context, Intent intent) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + notifyHeadsetOn(audioManager.isWiredHeadsetOn()); + } +} diff --git a/android/app/src/main/res/drawable/ic_eye_noshow.xml b/android/app/src/main/res/drawable/ic_eye_noshow.xml new file mode 100644 index 0000000000..1d5304afac --- /dev/null +++ b/android/app/src/main/res/drawable/ic_eye_noshow.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_eye_show.xml b/android/app/src/main/res/drawable/ic_eye_show.xml new file mode 100644 index 0000000000..273ecc8339 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_eye_show.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/selector_show_password.xml b/android/app/src/main/res/drawable/selector_show_password.xml new file mode 100644 index 0000000000..a44092aceb --- /dev/null +++ b/android/app/src/main/res/drawable/selector_show_password.xml @@ -0,0 +1,5 @@ + + + + + \ 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 c50e6c1380..46ed783898 100644 --- a/android/app/src/main/res/layout/fragment_login.xml +++ b/android/app/src/main/res/layout/fragment_login.xml @@ -41,38 +41,51 @@ android:paddingTop="14dp" android:ems="10" android:fontFamily="@font/raleway" - android:textSize="14sp" + android:textSize="17sp" android:inputType="textEmailAddress" android:textStyle="italic" android:textColor="@color/editTextColor" android:textColorHint="@color/editTextColor" - android:gravity="right|center_vertical" + android:gravity="left|center_vertical" app:layout_constraintTop_toBottomOf="@id/header" android:layout_marginTop="70dp" android:hint="@string/username_or_email" /> - + + android:inputType="textPassword" /> +