diff --git a/app/build.gradle b/app/build.gradle
index e3b5fe2..7f32fe9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,9 +7,8 @@ android {
applicationId "com.m2049r.xmrwallet"
minSdkVersion 21
targetSdkVersion 28
- versionCode 303
- versionName "1.13.3 'ReStart'"
-
+ versionCode 400
+ versionName "1.14 'On Board'"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
@@ -93,6 +92,11 @@ android {
outputFileName = "$rootProject.ext.apkName-" + v + "_" + abiName + ".apk"
}
}
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
}
dependencies {
@@ -102,6 +106,7 @@ dependencies {
implementation "com.android.support:recyclerview-v7:$rootProject.ext.supportVersion"
implementation "com.android.support:cardview-v7:$rootProject.ext.supportVersion"
implementation "com.android.support:swiperefreshlayout:$rootProject.ext.supportVersion"
+ implementation "com.android.support.constraint:constraint-layout:$rootProject.ext.constraintVersion"
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation "com.squareup.okhttp3:okhttp:$rootProject.ext.okHttpVersion"
@@ -124,5 +129,4 @@ dependencies {
testImplementation "com.squareup.okhttp3:mockwebserver:$rootProject.ext.okHttpVersion"
testImplementation 'org.json:json:20180813'
testImplementation 'net.jodah:concurrentunit:0.4.4'
-
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 948520d..7c28e38 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,24 +20,27 @@
android:supportsRtl="true"
android:theme="@style/MyMaterialTheme"
android:usesCleartextTraffic="true">
-
+
+
+
+
+
+
-
-
-
-
-
@@ -62,6 +65,10 @@
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/usb_device_filter" />
+
-
\ No newline at end of file
+
diff --git a/app/src/main/java/com/m2049r/xmrwallet/MainActivity.java b/app/src/main/java/com/m2049r/xmrwallet/MainActivity.java
new file mode 100644
index 0000000..cff572d
--- /dev/null
+++ b/app/src/main/java/com/m2049r/xmrwallet/MainActivity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2018-2020 EarlOfEgo, m2049r
+ *
+ * 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 com.m2049r.xmrwallet;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+
+import com.m2049r.xmrwallet.onboarding.OnBoardingActivity;
+import com.m2049r.xmrwallet.onboarding.OnBoardingManager;
+
+import timber.log.Timber;
+
+public class MainActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(@Nullable final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (OnBoardingManager.shouldShowOnBoarding(getApplicationContext()) || BuildConfig.DEBUG) {
+ startActivity(new Intent(this, OnBoardingActivity.class));
+ } else {
+ startActivity(new Intent(this, LoginActivity.class));
+ }
+ finish();
+ }
+}
diff --git a/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingActivity.java b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingActivity.java
new file mode 100644
index 0000000..6d17a5c
--- /dev/null
+++ b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingActivity.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2018-2020 EarlOfEgo, m2049r
+ *
+ * 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 com.m2049r.xmrwallet.onboarding;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.design.widget.TabLayout;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.AppCompatActivity;
+import android.util.TypedValue;
+import android.view.View;
+
+import com.m2049r.xmrwallet.LoginActivity;
+import com.m2049r.xmrwallet.R;
+
+public class OnBoardingActivity extends AppCompatActivity implements OnBoardingAdapter.Listener {
+
+ private ViewPager pager;
+ private OnBoardingAdapter pagerAdapter;
+ private int mustAgreePages = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_on_boarding);
+
+ final View nextButton = findViewById(R.id.buttonNext);
+
+ pager = findViewById(R.id.pager);
+ pagerAdapter = new OnBoardingAdapter(getApplicationContext(), this);
+ pager.setAdapter(pagerAdapter);
+ int pixels = (int) TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics());
+ pager.setPageMargin(pixels);
+ pager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int i) {
+ setButtonState();
+ }
+ });
+
+ final TabLayout tabLayout = (TabLayout) findViewById(R.id.tabLayout);
+ if (pagerAdapter.getCount() > 1) {
+ tabLayout.setupWithViewPager(pager, true);
+ } else {
+ tabLayout.setVisibility(View.GONE);
+ }
+
+ nextButton.setOnClickListener(v -> {
+ final int item = pager.getCurrentItem();
+ if (item + 1 >= pagerAdapter.getCount()) {
+ finishOnboarding();
+ } else {
+ pager.setCurrentItem(item + 1);
+ }
+ });
+
+ for (int i = 0; i < OnBoardingScreen.values().length; i++) {
+ if (OnBoardingScreen.values()[i].isMustAgree()) mustAgreePages++;
+ }
+ }
+
+ private void finishOnboarding() {
+ OnBoardingManager.setOnBoardingShown(getApplicationContext());
+ startActivity(new Intent(this, LoginActivity.class));
+ finish();
+ }
+
+ int agreeCounter = 0;
+ boolean agreed[] = new boolean[OnBoardingScreen.values().length];
+
+ @Override
+ public void setAgreeClicked(int position, boolean isChecked) {
+ if (isChecked) {
+ agreeCounter++;
+ } else {
+ agreeCounter--;
+ }
+ agreed[position] = isChecked;
+ setButtonState();
+ }
+
+ @Override
+ public boolean isAgreeClicked(int position) {
+ return agreed[position];
+ }
+
+ @Override
+ public void setButtonState() {
+ if (pager.getCurrentItem() + 1 == pagerAdapter.getCount()) { // last page
+ findViewById(R.id.buttonNext).setEnabled(mustAgreePages == agreeCounter);
+ } else {
+ findViewById(R.id.buttonNext).setEnabled(true);
+ }
+ }
+}
diff --git a/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingAdapter.java b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingAdapter.java
new file mode 100644
index 0000000..5ec1936
--- /dev/null
+++ b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingAdapter.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2018-2020 EarlOfEgo, m2049r
+ *
+ * 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 com.m2049r.xmrwallet.onboarding;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.PagerAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.m2049r.xmrwallet.R;
+
+import timber.log.Timber;
+
+public class OnBoardingAdapter extends PagerAdapter {
+
+ interface Listener {
+ void setAgreeClicked(int position, boolean isChecked);
+
+ boolean isAgreeClicked(int position);
+
+ void setButtonState();
+ }
+
+ private final Context context;
+ private Listener listener;
+
+ OnBoardingAdapter(final Context context, final Listener listener) {
+ this.context = context;
+ this.listener = listener;
+ }
+
+ @NonNull
+ @Override
+ public Object instantiateItem(@NonNull ViewGroup collection, int position) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ final View view = inflater.inflate(R.layout.view_onboarding, collection, false);
+ final OnBoardingScreen onBoardingScreen = OnBoardingScreen.values()[position];
+
+ final Drawable drawable = ContextCompat.getDrawable(context, onBoardingScreen.getDrawable());
+ ((ImageView) view.findViewById(R.id.onboardingImage)).setImageDrawable(drawable);
+ ((TextView) view.findViewById(R.id.onboardingTitle)).setText(onBoardingScreen.getTitle());
+ ((TextView) view.findViewById(R.id.onboardingInformation)).setText(onBoardingScreen.getInformation());
+ if (onBoardingScreen.isMustAgree()) {
+ final CheckBox agree = ((CheckBox) view.findViewById(R.id.onboardingAgree));
+ agree.setVisibility(View.VISIBLE);
+ agree.setChecked(listener.isAgreeClicked(position));
+ agree.setOnClickListener(v -> {
+ listener.setAgreeClicked(position, ((CheckBox) v).isChecked());
+ });
+ listener.setButtonState();
+ }
+ collection.addView(view);
+ Timber.d("add " + position);
+ return view;
+ }
+
+ @Override
+ public int getCount() {
+ return OnBoardingScreen.values().length;
+ }
+
+ @Override
+ public void destroyItem(@NonNull ViewGroup collection, int position, @NonNull Object view) {
+ Timber.d("destroy " + position);
+ collection.removeView((View) view);
+ }
+
+ @Override
+ public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
+ return view == object;
+ }
+}
diff --git a/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingManager.java b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingManager.java
new file mode 100644
index 0000000..7be6a03
--- /dev/null
+++ b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingManager.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018-2020 EarlOfEgo, m2049r
+ *
+ * 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 com.m2049r.xmrwallet.onboarding;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.m2049r.xmrwallet.util.KeyStoreHelper;
+
+import java.util.Date;
+
+import timber.log.Timber;
+
+public class OnBoardingManager {
+
+ private static final String PREFS_ONBOARDING = "PREFS_ONBOARDING";
+ private static final String ONBOARDING_SHOWN = "ONBOARDING_SHOWN";
+
+ public static boolean shouldShowOnBoarding(final Context context) {
+ return !getSharedPreferences(context).contains(ONBOARDING_SHOWN) && KeyStoreHelper.hasStoredPasswords(context);
+ }
+
+ public static void setOnBoardingShown(final Context context) {
+ Timber.d("Set onboarding shown.");
+ SharedPreferences sharedPreferences = getSharedPreferences(context);
+ sharedPreferences.edit().putLong(ONBOARDING_SHOWN, new Date().getTime()).apply();
+ }
+
+ public static void clearOnBoardingShown(final Context context) {
+ SharedPreferences sharedPreferences = getSharedPreferences(context);
+ sharedPreferences.edit().remove(ONBOARDING_SHOWN).apply();
+ }
+
+ private static SharedPreferences getSharedPreferences(final Context context) {
+ return context.getSharedPreferences(PREFS_ONBOARDING, Context.MODE_PRIVATE);
+ }
+}
diff --git a/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingScreen.java b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingScreen.java
new file mode 100644
index 0000000..bbef7ad
--- /dev/null
+++ b/app/src/main/java/com/m2049r/xmrwallet/onboarding/OnBoardingScreen.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2018-2020 EarlOfEgo, m2049r
+ *
+ * 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 com.m2049r.xmrwallet.onboarding;
+
+import com.m2049r.xmrwallet.R;
+
+enum OnBoardingScreen {
+ WELCOME(R.string.onboarding_welcome_title, R.string.onboarding_welcome_information, R.drawable.ic_onboarding_welcome, false),
+ SEED(R.string.onboarding_seed_title, R.string.onboarding_seed_information, R.drawable.ic_onboarding_seed, true),
+ FPSEND(R.string.onboarding_fpsend_title, R.string.onboarding_fpsend_information, R.drawable.ic_onboarding_fingerprint, true),
+ XMRTO(R.string.onboarding_xmrto_title, R.string.onboarding_xmrto_information, R.drawable.ic_onboarding_xmrto, false),
+ NODES(R.string.onboarding_nodes_title, R.string.onboarding_nodes_information, R.drawable.ic_onboarding_nodes, false);
+
+ private final int title;
+ private final int information;
+ private final int drawable;
+ private final boolean mustAgree;
+
+ OnBoardingScreen(final int title, final int information, final int drawable, final boolean mustAgree) {
+ this.title = title;
+ this.information = information;
+ this.drawable = drawable;
+ this.mustAgree = mustAgree;
+ }
+
+ public int getTitle() {
+ return title;
+ }
+
+ public int getInformation() {
+ return information;
+ }
+
+ public int getDrawable() {
+ return drawable;
+ }
+
+ public boolean isMustAgree() {
+ return mustAgree;
+ }
+}
diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java
index d00b357..eb94e62 100644
--- a/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java
+++ b/app/src/main/java/com/m2049r/xmrwallet/util/KeyStoreHelper.java
@@ -140,6 +140,11 @@ public class KeyStoreHelper {
}
}
+ public static boolean hasStoredPasswords(@NonNull Context context) {
+ SharedPreferences prefs = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE);
+ return prefs.getAll().size() > 0;
+ }
+
public static String loadWalletUserPass(@NonNull Context context, String wallet) throws BrokenPasswordStoreException {
String walletKeyAlias = SecurityConstants.WALLET_PASS_KEY_PREFIX + wallet;
String encoded = context.getSharedPreferences(SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
diff --git a/app/src/main/res/drawable/dot_dark.xml b/app/src/main/res/drawable/dot_dark.xml
new file mode 100644
index 0000000..c9dfb9c
--- /dev/null
+++ b/app/src/main/res/drawable/dot_dark.xml
@@ -0,0 +1,12 @@
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/drawable/dot_light.xml b/app/src/main/res/drawable/dot_light.xml
new file mode 100644
index 0000000..e608d7a
--- /dev/null
+++ b/app/src/main/res/drawable/dot_light.xml
@@ -0,0 +1,12 @@
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_fingerprint.xml b/app/src/main/res/drawable/ic_onboarding_fingerprint.xml
new file mode 100644
index 0000000..9ec70a9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_fingerprint.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_nodes.xml b/app/src/main/res/drawable/ic_onboarding_nodes.xml
new file mode 100644
index 0000000..391d346
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_nodes.xml
@@ -0,0 +1,279 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_seed.xml b/app/src/main/res/drawable/ic_onboarding_seed.xml
new file mode 100644
index 0000000..3481157
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_seed.xml
@@ -0,0 +1,393 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_welcome.xml b/app/src/main/res/drawable/ic_onboarding_welcome.xml
new file mode 100644
index 0000000..0158b09
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_welcome.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_xmrto.xml b/app/src/main/res/drawable/ic_onboarding_xmrto.xml
new file mode 100644
index 0000000..6476a20
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_xmrto.xml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/onboarding_dots.xml b/app/src/main/res/drawable/onboarding_dots.xml
new file mode 100644
index 0000000..d388982
--- /dev/null
+++ b/app/src/main/res/drawable/onboarding_dots.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_on_boarding.xml b/app/src/main/res/layout/activity_on_boarding.xml
new file mode 100644
index 0000000..4f94588
--- /dev/null
+++ b/app/src/main/res/layout/activity_on_boarding.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/view_onboarding.xml b/app/src/main/res/layout/view_onboarding.xml
new file mode 100644
index 0000000..af56bda
--- /dev/null
+++ b/app/src/main/res/layout/view_onboarding.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 1aed642..1ae359a 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -15,7 +15,7 @@
#ffffff
#000000
- #1F4E97
+ #1f4e97
#FD9B9B9B
#D81F0759
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a7f8ddb..a8953e9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -438,4 +438,20 @@
Start Monero App on %1$s
Rescan!
+
+ I get it!
+ Next
+
+ Welcome to Monerujo!
+ This app allows you to create and use Monero wallets. You can store your sweet Monero (XMR) in them.
+ Write down your seed
+ Your seed is secret and we cannot help you recover it. It unlocks your money to whoever has it. If you lose it, you lose your beloved Monero.
+ Send Bitcoin
+ Monerujo has XMR.to support builtin. You can send BTC by spending XMR. Just paste or scan a BTC address when sending.
+ Nodes, your way
+ Nodes connect you to the Monero network. Choose between searching for public nodes or go full cypherpunk using your own.
+ Send with fingerprint
+ You\'ll be able to authorize sending XMR with just your fingerprint.
+ If you prefer to secure sending by password, please disable fingerprint access for that wallet.
+
diff --git a/build.gradle b/build.gradle
index fb3f087..79b96ae 100644
--- a/build.gradle
+++ b/build.gradle
@@ -34,4 +34,5 @@ ext {
mockitoVersion = '1.10.19'
timberVersion = '4.7.1'
supportVersion = '28.0.0'
+ constraintVersion = "2.0.1"
}