diff --git a/build.gradle b/build.gradle
index df69918f7..7c156fc29 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,12 @@
-// Top-level build file where you can add configuration options common to all
-// sub-projects/modules.
buildscript {
+
+ ext {
+ room_version = "2.5.0"
+ navVersion = '2.5.3'
+ appcompatVersion = "1.6.1"
+ lifecycleVersion = "2.2.0"
+ }
+
repositories {
google()
mavenCentral()
@@ -8,11 +14,13 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.4.1'
classpath "com.diffplug.spotless:spotless-plugin-gradle:6.13.0"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion"
}
}
apply plugin: 'com.android.application'
apply plugin: "com.diffplug.spotless"
+apply plugin: "androidx.navigation.safeargs"
repositories {
@@ -45,17 +53,25 @@ dependencies {
// Conversations 3.0 dependencies
- def room_version = "2.5.0"
-
implementation project(':libs:annotation')
annotationProcessor project(':libs:annotation-processor')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.8'
+ implementation "androidx.appcompat:appcompat:$rootProject.ext.appcompatVersion"
+
+ implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.ext.lifecycleVersion"
+
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-guava:$room_version"
+ implementation "androidx.navigation:navigation-fragment:$rootProject.ext.navVersion"
+ implementation "androidx.navigation:navigation-ui:$rootProject.ext.navVersion"
+
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+
+
implementation "androidx.security:security-crypto:1.0.0"
@@ -82,8 +98,7 @@ dependencies {
quicksyPlaystoreImplementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
implementation 'org.sufficientlysecure:openpgp-api:10.0'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
- implementation 'androidx.appcompat:appcompat:1.6.0'
- implementation 'androidx.exifinterface:exifinterface:1.3.5'
+ implementation 'androidx.exifinterface:exifinterface:1.3.6'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.8.0'
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 837ebd3be..f3ea49578 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -85,7 +85,7 @@
android:networkSecurityConfig="@xml/network_security_configuration"
android:preserveLegacyExternalStorage="true"
android:requestLegacyExternalStorage="true"
- android:theme="@style/ConversationsTheme"
+ android:theme="@style/Conversations3Theme"
tools:replace="android:label"
tools:targetApi="q">
@@ -121,6 +121,14 @@
+
+
+
+
+
+
+
@@ -138,10 +146,7 @@
android:name=".ui.ConversationActivity"
android:exported="true"
android:theme="@style/SplashTheme">
-
-
-
-
+
= Build.VERSION_CODES.O) {
+ window.setNavigationBarColor(SurfaceColors.SURFACE_1.getColor(activity));
+ if (isLightMode) {
+ view.setSystemUiVisibility(
+ flags
+ | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
+ }
+ } else if (isLightMode) {
+ view.setSystemUiVisibility(flags | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ }
+ }
+
+ private static boolean isLightMode(final Context context) {
+ final int nightModeFlags =
+ context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
+ return nightModeFlags != Configuration.UI_MODE_NIGHT_YES;
+ }
+}
diff --git a/src/main/java/im/conversations/android/ui/BindingAdapters.java b/src/main/java/im/conversations/android/ui/BindingAdapters.java
new file mode 100644
index 000000000..9d9796fee
--- /dev/null
+++ b/src/main/java/im/conversations/android/ui/BindingAdapters.java
@@ -0,0 +1,31 @@
+package im.conversations.android.ui;
+
+import android.view.KeyEvent;
+import androidx.annotation.NonNull;
+import androidx.databinding.BindingAdapter;
+import androidx.lifecycle.LiveData;
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.android.material.textfield.TextInputLayout;
+import com.google.common.base.Supplier;
+
+public class BindingAdapters {
+
+ @BindingAdapter("errorText")
+ public static void setErrorText(
+ final TextInputLayout textInputLayout, final LiveData error) {
+ textInputLayout.setError(error.getValue());
+ }
+
+ @BindingAdapter("editorAction")
+ public static void setEditorAction(
+ final TextInputEditText editText, final @NonNull Supplier callback) {
+ editText.setOnEditorActionListener(
+ (v, actionId, event) -> {
+ // event is null when using software keyboard
+ if (event == null || event.getAction() == KeyEvent.ACTION_UP) {
+ return Boolean.TRUE.equals(callback.get());
+ }
+ return true;
+ });
+ }
+}
diff --git a/src/main/java/im/conversations/android/ui/Event.java b/src/main/java/im/conversations/android/ui/Event.java
new file mode 100644
index 000000000..35ff27c58
--- /dev/null
+++ b/src/main/java/im/conversations/android/ui/Event.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 Daniel Gultsch
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.conversations.android.ui;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class Event {
+
+ private final T event;
+ private final AtomicBoolean isConsumable = new AtomicBoolean(true);
+
+ public Event(T event) {
+ this.event = event;
+ }
+
+ public T consume() {
+ if (isConsumable.compareAndSet(true, false)) {
+ return event;
+ }
+ throw new IllegalStateException("Event has already been consumed");
+ }
+
+ public boolean isConsumable() {
+ return isConsumable.get();
+ }
+}
diff --git a/src/main/java/im/conversations/android/ui/NavControllers.java b/src/main/java/im/conversations/android/ui/NavControllers.java
new file mode 100644
index 000000000..4dc39e805
--- /dev/null
+++ b/src/main/java/im/conversations/android/ui/NavControllers.java
@@ -0,0 +1,23 @@
+package im.conversations.android.ui;
+
+import androidx.annotation.IdRes;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+
+public final class NavControllers {
+
+ private NavControllers() {}
+
+ public static NavController findNavController(
+ final AppCompatActivity activity, @IdRes int fragmentId) {
+ final FragmentManager fragmentManager = activity.getSupportFragmentManager();
+ final Fragment fragment = fragmentManager.findFragmentById(fragmentId);
+ if (fragment instanceof NavHostFragment) {
+ return ((NavHostFragment) fragment).getNavController();
+ }
+ throw new IllegalStateException("Fragment was not of type NavHostFragment");
+ }
+}
diff --git a/src/main/java/im/conversations/android/ui/activity/SetupActivity.java b/src/main/java/im/conversations/android/ui/activity/SetupActivity.java
new file mode 100644
index 000000000..ca814d840
--- /dev/null
+++ b/src/main/java/im/conversations/android/ui/activity/SetupActivity.java
@@ -0,0 +1,50 @@
+package im.conversations.android.ui.activity;
+
+import android.os.Bundle;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.databinding.DataBindingUtil;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.navigation.NavController;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.SetupNavigationDirections;
+import eu.siacs.conversations.databinding.ActivitySetupBinding;
+import im.conversations.android.ui.Activities;
+import im.conversations.android.ui.Event;
+import im.conversations.android.ui.NavControllers;
+import im.conversations.android.ui.model.SetupViewModel;
+
+public class SetupActivity extends AppCompatActivity {
+
+ private SetupViewModel setupViewModel;
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final ActivitySetupBinding binding =
+ DataBindingUtil.setContentView(this, R.layout.activity_setup);
+ final ViewModelProvider viewModelProvider =
+ new ViewModelProvider(this, getDefaultViewModelProviderFactory());
+ this.setupViewModel = viewModelProvider.get(SetupViewModel.class);
+ this.setupViewModel.getRedirection().observe(this, this::onRedirectionEvent);
+ Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
+ }
+
+ private void onRedirectionEvent(final Event targetEvent) {
+ if (targetEvent.isConsumable()) {
+ final NavController navController = getNavController();
+ final SetupViewModel.Target target = targetEvent.consume();
+ switch (target) {
+ case ENTER_PASSWORD:
+ navController.navigate(SetupNavigationDirections.enterPassword());
+ break;
+ default:
+ throw new IllegalStateException(
+ String.format("Unable to navigate to target %s", target));
+ }
+ }
+ }
+
+ private NavController getNavController() {
+ return NavControllers.findNavController(this, R.id.nav_host_fragment);
+ }
+}
diff --git a/src/main/java/im/conversations/android/ui/fragment/setup/AbstractSetupFragment.java b/src/main/java/im/conversations/android/ui/fragment/setup/AbstractSetupFragment.java
new file mode 100644
index 000000000..45adbcc25
--- /dev/null
+++ b/src/main/java/im/conversations/android/ui/fragment/setup/AbstractSetupFragment.java
@@ -0,0 +1,18 @@
+package im.conversations.android.ui.fragment.setup;
+
+import android.os.Bundle;
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+import im.conversations.android.ui.model.SetupViewModel;
+
+public abstract class AbstractSetupFragment extends Fragment {
+
+ SetupViewModel setupViewModel;
+
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final ViewModelProvider viewModelProvider =
+ new ViewModelProvider(requireActivity(), getDefaultViewModelProviderFactory());
+ this.setupViewModel = viewModelProvider.get(SetupViewModel.class);
+ }
+}
diff --git a/src/main/java/im/conversations/android/ui/fragment/setup/HostnameFragment.java b/src/main/java/im/conversations/android/ui/fragment/setup/HostnameFragment.java
new file mode 100644
index 000000000..d609c6700
--- /dev/null
+++ b/src/main/java/im/conversations/android/ui/fragment/setup/HostnameFragment.java
@@ -0,0 +1,3 @@
+package im.conversations.android.ui.fragment.setup;
+
+public class HostnameFragment extends AbstractSetupFragment {}
diff --git a/src/main/java/im/conversations/android/ui/fragment/setup/PasswordFragment.java b/src/main/java/im/conversations/android/ui/fragment/setup/PasswordFragment.java
new file mode 100644
index 000000000..389aaca59
--- /dev/null
+++ b/src/main/java/im/conversations/android/ui/fragment/setup/PasswordFragment.java
@@ -0,0 +1,24 @@
+package im.conversations.android.ui.fragment.setup;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.databinding.DataBindingUtil;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.FragmentPasswordBinding;
+
+public class PasswordFragment extends AbstractSetupFragment {
+
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ FragmentPasswordBinding binding =
+ DataBindingUtil.inflate(inflater, R.layout.fragment_password, container, false);
+ binding.setSetupViewModel(setupViewModel);
+ binding.setLifecycleOwner(getViewLifecycleOwner());
+ return binding.getRoot();
+ }
+}
diff --git a/src/main/java/im/conversations/android/ui/fragment/setup/SignInFragment.java b/src/main/java/im/conversations/android/ui/fragment/setup/SignInFragment.java
new file mode 100644
index 000000000..dac120a11
--- /dev/null
+++ b/src/main/java/im/conversations/android/ui/fragment/setup/SignInFragment.java
@@ -0,0 +1,24 @@
+package im.conversations.android.ui.fragment.setup;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.databinding.DataBindingUtil;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.FragmentSignInBinding;
+
+public class SignInFragment extends AbstractSetupFragment {
+
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ FragmentSignInBinding binding =
+ DataBindingUtil.inflate(inflater, R.layout.fragment_sign_in, container, false);
+ binding.setSetupViewModel(setupViewModel);
+ binding.setLifecycleOwner(getViewLifecycleOwner());
+ return binding.getRoot();
+ }
+}
diff --git a/src/main/java/im/conversations/android/ui/model/SetupViewModel.java b/src/main/java/im/conversations/android/ui/model/SetupViewModel.java
new file mode 100644
index 000000000..db1a0d410
--- /dev/null
+++ b/src/main/java/im/conversations/android/ui/model/SetupViewModel.java
@@ -0,0 +1,63 @@
+package im.conversations.android.ui.model;
+
+import android.app.Application;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import im.conversations.android.ui.Event;
+import org.jetbrains.annotations.NotNull;
+
+public class SetupViewModel extends AndroidViewModel {
+
+ private final MutableLiveData xmppAddress = new MutableLiveData<>();
+ private final MutableLiveData xmppAddressError = new MutableLiveData<>();
+ private final MutableLiveData password = new MutableLiveData<>();
+ private final MutableLiveData passwordError = new MutableLiveData<>();
+ private final MutableLiveData loading = new MutableLiveData<>(false);
+
+ private final MutableLiveData> redirection = new MutableLiveData<>();
+
+ public SetupViewModel(@NonNull @NotNull Application application) {
+ super(application);
+ }
+
+ public LiveData isLoading() {
+ return this.loading;
+ }
+
+ public LiveData getXmppAddressError() {
+ return Transformations.distinctUntilChanged(xmppAddressError);
+ }
+
+ public MutableLiveData getXmppAddress() {
+ return this.xmppAddress;
+ }
+
+ public MutableLiveData getPassword() {
+ return password;
+ }
+
+ public LiveData getPasswordError() {
+ return Transformations.distinctUntilChanged(this.passwordError);
+ }
+
+ public boolean submitXmppAddress() {
+ redirection.postValue(new Event<>(Target.ENTER_PASSWORD));
+ return true;
+ }
+
+ public boolean submitPassword() {
+ return true;
+ }
+
+ public LiveData> getRedirection() {
+ return this.redirection;
+ }
+
+ public enum Target {
+ ENTER_PASSWORD,
+ ENTER_HOSTNAME
+ }
+}
diff --git a/src/main/res/anim/slide_from_left.xml b/src/main/res/anim/slide_from_left.xml
new file mode 100644
index 000000000..b7d1e72a8
--- /dev/null
+++ b/src/main/res/anim/slide_from_left.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/anim/slide_from_right.xml b/src/main/res/anim/slide_from_right.xml
new file mode 100644
index 000000000..7116130b4
--- /dev/null
+++ b/src/main/res/anim/slide_from_right.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/anim/slide_to_left.xml b/src/main/res/anim/slide_to_left.xml
new file mode 100644
index 000000000..7fd524385
--- /dev/null
+++ b/src/main/res/anim/slide_to_left.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/anim/slide_to_right.xml b/src/main/res/anim/slide_to_right.xml
new file mode 100644
index 000000000..0dbe63b6f
--- /dev/null
+++ b/src/main/res/anim/slide_to_right.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/drawable/ic_account_circle_24dp.xml b/src/main/res/drawable/ic_account_circle_24dp.xml
new file mode 100644
index 000000000..f9110996c
--- /dev/null
+++ b/src/main/res/drawable/ic_account_circle_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/main/res/drawable/ic_settings_24dp.xml b/src/main/res/drawable/ic_settings_24dp.xml
new file mode 100644
index 000000000..a7c7678dc
--- /dev/null
+++ b/src/main/res/drawable/ic_settings_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/main/res/layout/activity_setup.xml b/src/main/res/layout/activity_setup.xml
new file mode 100644
index 000000000..1ea709c2b
--- /dev/null
+++ b/src/main/res/layout/activity_setup.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/layout/fragment_password.xml b/src/main/res/layout/fragment_password.xml
new file mode 100644
index 000000000..fa9d2b8ca
--- /dev/null
+++ b/src/main/res/layout/fragment_password.xml
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/layout/fragment_sign_in.xml b/src/main/res/layout/fragment_sign_in.xml
new file mode 100644
index 000000000..018557a7d
--- /dev/null
+++ b/src/main/res/layout/fragment_sign_in.xml
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/menu/setup.xml b/src/main/res/menu/setup.xml
new file mode 100644
index 000000000..515dd2fca
--- /dev/null
+++ b/src/main/res/menu/setup.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/src/main/res/navigation/setup_navigation.xml b/src/main/res/navigation/setup_navigation.xml
new file mode 100644
index 000000000..51291c860
--- /dev/null
+++ b/src/main/res/navigation/setup_navigation.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/values-land/dimens.xml b/src/main/res/values-land/dimens.xml
index 73ac76ef7..f76542bc7 100644
--- a/src/main/res/values-land/dimens.xml
+++ b/src/main/res/values-land/dimens.xml
@@ -2,4 +2,7 @@
96dp
128dp
16dp
+ 8dp
+ 16dp
+ 288dp
\ No newline at end of file
diff --git a/src/main/res/values/dimens.xml b/src/main/res/values/dimens.xml
index baa9d4ea9..bf0cc6c2d 100644
--- a/src/main/res/values/dimens.xml
+++ b/src/main/res/values/dimens.xml
@@ -44,4 +44,7 @@
128dp
96dp
24dp
+ 48dp
+ 336dp
+ 24dp
diff --git a/src/main/res/values/integers.xml b/src/main/res/values/integers.xml
new file mode 100644
index 000000000..0ece1b3ed
--- /dev/null
+++ b/src/main/res/values/integers.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ 300
+
\ No newline at end of file
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 4edf1ae7d..27b3498ef 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -1005,5 +1005,11 @@
Push Server
A user-chosen push server to relay push messages via XMPP to your device.
None (deactivated)
+ Sign in
+ with your XMPP account.
+ Type in your password
+ Welcome
+ No account? Register
+ Account settings
diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml
index 9e757a626..57b4d0fd0 100644
--- a/src/main/res/values/themes.xml
+++ b/src/main/res/values/themes.xml
@@ -1,6 +1,12 @@
+
+
+