diff --git a/app/src/androidTest/java/com/nmc/android/onboarding/OnBoardingIT.kt b/app/src/androidTest/java/com/nmc/android/onboarding/OnBoardingIT.kt new file mode 100644 index 000000000000..3ab709a21538 --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/onboarding/OnBoardingIT.kt @@ -0,0 +1,49 @@ +package com.nmc.android.onboarding + +import androidx.test.espresso.Espresso.* +import androidx.test.espresso.action.ViewActions.swipeLeft +import androidx.test.espresso.action.ViewActions.swipeRight +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isClickable +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.nextcloud.client.onboarding.FirstRunActivity +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class OnBoardingIT : AbstractIT() { + + @get:Rule + var activityRule = ActivityScenarioRule(FirstRunActivity::class.java) + + @Test + fun runAllOnboardingTests() { + verifyUIElements() + + shortSleep() + + verifyOnBoardingSwipe() + } + + private fun verifyUIElements() { + onView(withId(R.id.contentPanel)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.progressIndicator)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.login)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.login)).check(matches(isClickable())) + } + + private fun verifyOnBoardingSwipe() { + onView(withId(R.id.contentPanel)).perform(swipeLeft()) + onView(withId(R.id.contentPanel)).perform(swipeLeft()) + onView(withId(R.id.contentPanel)).perform(swipeLeft()) + + onView(withId(R.id.contentPanel)).perform(swipeRight()) + onView(withId(R.id.contentPanel)).perform(swipeRight()) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt b/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt index 5f9e8724642a..995a2dfbfe07 100644 --- a/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt +++ b/app/src/main/java/com/nextcloud/client/onboarding/FirstRunActivity.kt @@ -8,31 +8,32 @@ package com.nextcloud.client.onboarding import android.accounts.AccountManager +import android.annotation.SuppressLint import android.content.Intent +import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout +import android.view.ViewGroup.MarginLayoutParams import androidx.activity.OnBackPressedCallback import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.viewpager2.widget.ViewPager2 +import androidx.viewpager.widget.ViewPager import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.UserAccountManager import com.nextcloud.client.appinfo.AppInfo import com.nextcloud.client.di.Injectable import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.mdm.MDMConfig +import com.nmc.android.helper.OnBoardingPagerAdapter +import com.nmc.android.helper.OnBoardingUtils.Companion.getOnBoardingItems +import com.nmc.android.utils.DisplayUtils.isLandscapeOrientation import com.owncloud.android.BuildConfig import com.owncloud.android.R import com.owncloud.android.authentication.AuthenticatorActivity import com.owncloud.android.databinding.FirstRunActivityBinding -import com.owncloud.android.features.FeatureItem import com.owncloud.android.ui.activity.BaseActivity import com.owncloud.android.ui.activity.FileDisplayActivity -import com.owncloud.android.ui.adapter.FeaturesViewAdapter import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.theme.ViewThemeUtils import javax.inject.Inject @@ -40,7 +41,7 @@ import javax.inject.Inject /** * Activity displaying general feature after a fresh install. */ -class FirstRunActivity : BaseActivity(), Injectable { +class FirstRunActivity : BaseActivity(), ViewPager.OnPageChangeListener, Injectable { @JvmField @Inject @@ -67,6 +68,9 @@ class FirstRunActivity : BaseActivity(), Injectable { private lateinit var binding: FirstRunActivityBinding private var defaultViewThemeUtils: ViewThemeUtils? = null + private var selectedPosition = 0 + + @SuppressLint("SourceLockedOrientationActivity") override fun onCreate(savedInstanceState: Bundle?) { enableAccountHandling = false @@ -74,17 +78,21 @@ class FirstRunActivity : BaseActivity(), Injectable { applyDefaultTheme() + // NMC Customization + // if device is not tablet then we have to lock it to Portrait mode + // as we don't have images for that + if (!com.nmc.android.utils.DisplayUtils.isTablet()) { + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + binding = FirstRunActivityBinding.inflate(layoutInflater) setContentView(binding.root) - setSlideshowSize(resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) - registerActivityResult() setupLoginButton() - setupSignupButton(MDMConfig.showIntro(this)) - setupHostOwnServerTextView(MDMConfig.showIntro(this)) deleteAccountAtFirstLaunch() - setupFeaturesViewAdapter() + updateLoginButtonMargin() + updateOnBoardingPager(selectedPosition) handleOnBackPressed() } @@ -123,45 +131,18 @@ class FirstRunActivity : BaseActivity(), Injectable { val authenticatorActivityIntent = getAuthenticatorActivityIntent(false) activityResult?.launch(authenticatorActivityIntent) } else { + preferences?.onBoardingComplete = true finish() } } } - private fun setupSignupButton(isProviderOrOwnInstallationVisible: Boolean) { - defaultViewThemeUtils?.material?.colorMaterialButtonOutlinedOnPrimary(binding.signup) - binding.signup.visibility = if (isProviderOrOwnInstallationVisible) View.VISIBLE else View.GONE - binding.signup.setOnClickListener { - val authenticatorActivityIntent = getAuthenticatorActivityIntent(true) - - if (intent.getBooleanExtra(EXTRA_ALLOW_CLOSE, false)) { - activityResult?.launch(authenticatorActivityIntent) - } else { - authenticatorActivityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(authenticatorActivityIntent) - } - } - } - private fun getAuthenticatorActivityIntent(extraUseProviderAsWebLogin: Boolean): Intent { val intent = Intent(this, AuthenticatorActivity::class.java) intent.putExtra(AuthenticatorActivity.EXTRA_USE_PROVIDER_AS_WEBLOGIN, extraUseProviderAsWebLogin) return intent } - private fun setupHostOwnServerTextView(isProviderOrOwnInstallationVisible: Boolean) { - defaultViewThemeUtils?.platform?.colorTextView(binding.hostOwnServer, ColorRole.ON_PRIMARY) - binding.hostOwnServer.visibility = if (isProviderOrOwnInstallationVisible) View.VISIBLE else View.GONE - if (isProviderOrOwnInstallationVisible) { - binding.hostOwnServer.setOnClickListener { - DisplayUtils.startLinkIntent( - this, - R.string.url_server_install - ) - } - } - } - // Sometimes, accounts are not deleted when you uninstall the application so we'll do it now private fun deleteAccountAtFirstLaunch() { if (onboarding?.isFirstRun == true) { @@ -169,16 +150,34 @@ class FirstRunActivity : BaseActivity(), Injectable { } } - @Suppress("SpreadOperator") - private fun setupFeaturesViewAdapter() { - val featuresViewAdapter = FeaturesViewAdapter(this, *firstRun) - binding.progressIndicator.setNumberOfSteps(featuresViewAdapter.itemCount) - binding.contentPanel.adapter = featuresViewAdapter - binding.contentPanel.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - binding.progressIndicator.animateToStep(position + 1) + private fun updateLoginButtonMargin() { + if (isLandscapeOrientation()) { + if (binding.login.layoutParams is MarginLayoutParams) { + (binding.login.layoutParams as MarginLayoutParams).setMargins( + 0, 0, 0, resources.getDimensionPixelOffset( + R.dimen.login_btn_bottom_margin_land + ) + ) + binding.login.requestLayout() + } + } else { + if (binding.login.layoutParams is MarginLayoutParams) { + (binding.login.layoutParams as MarginLayoutParams).setMargins( + 0, 0, 0, resources.getDimensionPixelOffset( + R.dimen.login_btn_bottom_margin + ) + ) + binding.login.requestLayout() } - }) + } + } + + private fun updateOnBoardingPager(selectedPosition: Int) { + val featuresViewAdapter = OnBoardingPagerAdapter(this, getOnBoardingItems()) + binding.progressIndicator.setNumberOfSteps(featuresViewAdapter.count) + binding.contentPanel.adapter = featuresViewAdapter + binding.contentPanel.currentItem = selectedPosition + binding.contentPanel.addOnPageChangeListener(this) } private fun handleOnBackPressed() { @@ -188,46 +187,27 @@ class FirstRunActivity : BaseActivity(), Injectable { override fun handleOnBackPressed() { val isFromAddAccount = intent.getBooleanExtra(EXTRA_ALLOW_CLOSE, false) - val destination: Intent = if (isFromAddAccount) { - Intent(applicationContext, FileDisplayActivity::class.java) + // NMC Customization -> Modified the condition for readability + if (isFromAddAccount) { + val destination = Intent(applicationContext, FileDisplayActivity::class.java) + destination.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(destination) + finish() } else { - Intent(applicationContext, AuthenticatorActivity::class.java) - } - - if (!isFromAddAccount) { - destination.putExtra(EXTRA_EXIT, true) + // NMC Customization -> No redirection to AuthenticatorActivity is required + // just close the app + finishAffinity() } - destination.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP - startActivity(destination) - finish() } } ) } - private fun setSlideshowSize(isLandscape: Boolean) { - binding.buttonLayout.orientation = if (isLandscape) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL - - val layoutParams: LinearLayout.LayoutParams = if (MDMConfig.showIntro(this)) { - LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } else { - @Suppress("MagicNumber") - LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - DisplayUtils.convertDpToPixel(if (isLandscape) 100f else 150f, this) - ) - } - - binding.bottomLayout.layoutParams = layoutParams - } - override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - setSlideshowSize(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) + updateLoginButtonMargin() + updateOnBoardingPager(selectedPosition) } private fun onFinish() { @@ -239,16 +219,24 @@ class FirstRunActivity : BaseActivity(), Injectable { super.onStop() } + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { + // unused but to be implemented due to abstract parent + } + + override fun onPageSelected(position: Int) { + //-1 to position because this position doesn't start from 0 + selectedPosition = position - 1 + + //pass directly the position here because this position will doesn't start from 0 + binding.progressIndicator.animateToStep(position) + } + + override fun onPageScrollStateChanged(state: Int) { + // unused but to be implemented due to abstract parent + } + companion object { const val EXTRA_ALLOW_CLOSE = "ALLOW_CLOSE" const val EXTRA_EXIT = "EXIT" - - val firstRun: Array - get() = arrayOf( - FeatureItem(R.drawable.logo, R.string.first_run_1_text, R.string.empty, true, false), - FeatureItem(R.drawable.first_run_files, R.string.first_run_2_text, R.string.empty, true, false), - FeatureItem(R.drawable.first_run_groupware, R.string.first_run_3_text, R.string.empty, true, false), - FeatureItem(R.drawable.first_run_talk, R.string.first_run_4_text, R.string.empty, true, false) - ) } } diff --git a/app/src/main/java/com/nextcloud/client/onboarding/OnboardingServiceImpl.kt b/app/src/main/java/com/nextcloud/client/onboarding/OnboardingServiceImpl.kt index e121d32d023e..1c20425ebc48 100644 --- a/app/src/main/java/com/nextcloud/client/onboarding/OnboardingServiceImpl.kt +++ b/app/src/main/java/com/nextcloud/client/onboarding/OnboardingServiceImpl.kt @@ -62,7 +62,7 @@ internal class OnboardingServiceImpl( } override fun launchFirstRunIfNeeded(activity: Activity): Boolean { - val canLaunch = MDMConfig.showIntro(activity) && isFirstRun && activity is AuthenticatorActivity + val canLaunch = !preferences.onBoardingComplete && activity is AuthenticatorActivity if (canLaunch) { val intent = Intent(activity, FirstRunActivity::class.java) activity.startActivityForResult(intent, AuthenticatorActivity.REQUEST_CODE_FIRST_RUN) diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java index 6433eea24f91..b06f564bf206 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java @@ -366,6 +366,10 @@ default void onDarkThemeModeChanged(DarkMode mode) { void setGlobalUploadPaused(boolean globalPausedState); + void setOnBoardingComplete(boolean isCompleted); + + boolean getOnBoardingComplete(); + void setPdfZoomTipShownCount(int count); int getPdfZoomTipShownCount(); diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index 82542b291aaa..9d0f73b01387 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java @@ -82,6 +82,8 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF__FOLDER_SORT_ORDER = "folder_sort_order"; private static final String PREF__FOLDER_LAYOUT = "folder_layout"; + private static final String PREF__ON_BOARDING_COMPLETE = "on_boarding_complete"; + private static final String PREF__LOCK_TIMESTAMP = "lock_timestamp"; private static final String PREF__SHOW_MEDIA_SCAN_NOTIFICATIONS = "show_media_scan_notifications"; private static final String PREF__LOCK = SettingsActivity.PREFERENCE_LOCK; @@ -743,6 +745,16 @@ public void setGlobalUploadPaused(boolean globalPausedState) { preferences.edit().putBoolean(PREF__GLOBAL_PAUSE_STATE, globalPausedState).apply(); } + @Override + public void setOnBoardingComplete(boolean isCompleted) { + preferences.edit().putBoolean(PREF__ON_BOARDING_COMPLETE, isCompleted).apply(); + } + + @Override + public boolean getOnBoardingComplete() { + return preferences.getBoolean(PREF__ON_BOARDING_COMPLETE, false); + } + @Override public void setPdfZoomTipShownCount(int count) { preferences.edit().putInt(PREF__PDF_ZOOM_TIP_SHOWN, count).apply(); diff --git a/app/src/main/java/com/nmc/android/helper/LoopPagerAdapterWrapper.java b/app/src/main/java/com/nmc/android/helper/LoopPagerAdapterWrapper.java new file mode 100644 index 000000000000..42da6471720e --- /dev/null +++ b/app/src/main/java/com/nmc/android/helper/LoopPagerAdapterWrapper.java @@ -0,0 +1,164 @@ +package com.nmc.android.helper; + +import android.os.Parcelable; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.viewpager.widget.PagerAdapter; + +/** + * A PagerAdapter wrapper responsible for providing a proper page to + * LoopViewPager + * + * This class shouldn't be used directly + */ +public class LoopPagerAdapterWrapper extends PagerAdapter { + + private final PagerAdapter mAdapter; + + private SparseArray mToDestroy = new SparseArray(); + + private boolean mBoundaryCaching; + + void setBoundaryCaching(boolean flag) { + mBoundaryCaching = flag; + } + + LoopPagerAdapterWrapper(PagerAdapter adapter) { + this.mAdapter = adapter; + } + + @Override + public void notifyDataSetChanged() { + mToDestroy = new SparseArray(); + super.notifyDataSetChanged(); + } + + int toRealPosition(int position) { + int realCount = getRealCount(); + if (realCount == 0) + return 0; + int realPosition = (position-1) % realCount; + if (realPosition < 0) + realPosition += realCount; + + return realPosition; + } + + public int toInnerPosition(int realPosition) { + int position = (realPosition + 1); + return position; + } + + private int getRealFirstPosition() { + return 1; + } + + private int getRealLastPosition() { + return getRealFirstPosition() + getRealCount() - 1; + } + + @Override + public int getCount() { + return mAdapter.getCount() + 2; + } + + public int getRealCount() { + return mAdapter.getCount(); + } + + public PagerAdapter getRealAdapter() { + return mAdapter; + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter) + ? position + : toRealPosition(position); + + if (mBoundaryCaching) { + ToDestroy toDestroy = mToDestroy.get(position); + if (toDestroy != null) { + mToDestroy.remove(position); + return toDestroy.object; + } + } + return mAdapter.instantiateItem(container, realPosition); + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + int realFirst = getRealFirstPosition(); + int realLast = getRealLastPosition(); + int realPosition = (mAdapter instanceof FragmentPagerAdapter || mAdapter instanceof FragmentStatePagerAdapter) + ? position + : toRealPosition(position); + + if (mBoundaryCaching && (position == realFirst || position == realLast)) { + mToDestroy.put(position, new ToDestroy(container, realPosition, + object)); + } else { + mAdapter.destroyItem(container, realPosition, object); + } + } + + /* + * Delegate rest of methods directly to the inner adapter. + */ + + @Override + public void finishUpdate(@NonNull ViewGroup container) { + mAdapter.finishUpdate(container); + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return mAdapter.isViewFromObject(view, object); + } + + @Override + public void restoreState(Parcelable bundle, ClassLoader classLoader) { + mAdapter.restoreState(bundle, classLoader); + } + + @Override + public Parcelable saveState() { + return mAdapter.saveState(); + } + + @Override + public void startUpdate(@NonNull ViewGroup container) { + mAdapter.startUpdate(container); + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + mAdapter.setPrimaryItem(container, position, object); + } + + /* + * End delegation + */ + + /** + * Container class for caching the boundary views + */ + static class ToDestroy { + ViewGroup container; + int position; + Object object; + + public ToDestroy(ViewGroup container, int position, Object object) { + this.container = container; + this.position = position; + this.object = object; + } + } + +} diff --git a/app/src/main/java/com/nmc/android/helper/LoopViewPager.java b/app/src/main/java/com/nmc/android/helper/LoopViewPager.java new file mode 100644 index 000000000000..cad14d90bb06 --- /dev/null +++ b/app/src/main/java/com/nmc/android/helper/LoopViewPager.java @@ -0,0 +1,176 @@ +package com.nmc.android.helper; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +/** + * A ViewPager subclass enabling infinte scrolling of the viewPager elements + *

+ * When used for paginating views (in opposite to fragments), no code changes should be needed only change xml's from + * to + *

+ * If "blinking" can be seen when paginating to first or last view, simply call seBoundaryCaching( true ), or change + * DEFAULT_BOUNDARY_CASHING to true + *

+ * When using a FragmentPagerAdapter or FragmentStatePagerAdapter, additional changes in the adapter must be done. The + * adapter must be prepared to create 2 extra items e.g.: + *

+ * The original adapter creates 4 items: [0,1,2,3] The modified adapter will have to create 6 items [0,1,2,3,4,5] with + * mapping realPosition=(position-1)%count [0->3, 1->0, 2->1, 3->2, 4->3, 5->0] + */ +public class LoopViewPager extends ViewPager { + + private static final boolean DEFAULT_BOUNDARY_CASHING = false; + + OnPageChangeListener mOuterPageChangeListener; + private LoopPagerAdapterWrapper mAdapter; + private boolean mBoundaryCaching = DEFAULT_BOUNDARY_CASHING; + + + /** + * helper function which may be used when implementing FragmentPagerAdapter + * + * @param position + * @param count + * @return (position - 1)%count + */ + public static int toRealPosition(int position, int count) { + position = position - 1; + if (position < 0) { + position += count; + } else { + position = position % count; + } + return position; + } + + /** + * If set to true, the boundary views (i.e. first and last) will never be destroyed This may help to prevent + * "blinking" of some views + * + * @param flag + */ + public void setBoundaryCaching(boolean flag) { + mBoundaryCaching = flag; + if (mAdapter != null) { + mAdapter.setBoundaryCaching(flag); + } + } + + @Override + public void setAdapter(PagerAdapter adapter) { + mAdapter = new LoopPagerAdapterWrapper(adapter); + mAdapter.setBoundaryCaching(mBoundaryCaching); + super.setAdapter(mAdapter); + setCurrentItem(0, false); + } + + @Override + public PagerAdapter getAdapter() { + return mAdapter != null ? mAdapter.getRealAdapter() : null; + } + + @Override + public int getCurrentItem() { + return mAdapter != null ? mAdapter.toRealPosition(super.getCurrentItem()) : 0; + } + + public void setCurrentItem(int item, boolean smoothScroll) { + if (mAdapter != null) { + int realItem = mAdapter.toInnerPosition(item); + super.setCurrentItem(realItem, smoothScroll); + } + } + + @Override + public void setCurrentItem(int item) { + if (getCurrentItem() != item) { + setCurrentItem(item, true); + } + } + + @Override + public void setOnPageChangeListener(OnPageChangeListener listener) { + mOuterPageChangeListener = listener; + } + + public LoopViewPager(Context context) { + super(context); + init(); + } + + public LoopViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + super.setOnPageChangeListener(onPageChangeListener); + } + + private final OnPageChangeListener onPageChangeListener = new OnPageChangeListener() { + private float mPreviousOffset = -1; + private float mPreviousPosition = -1; + + @Override + public void onPageSelected(int position) { + + int realPosition = mAdapter.toRealPosition(position); + if (mPreviousPosition != realPosition) { + mPreviousPosition = realPosition; + if (mOuterPageChangeListener != null) { + mOuterPageChangeListener.onPageSelected(realPosition); + } + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, + int positionOffsetPixels) { + int realPosition = position; + if (mAdapter != null) { + realPosition = mAdapter.toRealPosition(position); + + if (positionOffset == 0 + && mPreviousOffset == 0 + && (position == 0 || position == mAdapter.getCount() - 1)) { + setCurrentItem(realPosition, false); + } + } + + mPreviousOffset = positionOffset; + if (mOuterPageChangeListener != null && mAdapter != null) { + if (realPosition != mAdapter.getRealCount() - 1) { + mOuterPageChangeListener.onPageScrolled(realPosition, + positionOffset, positionOffsetPixels); + } else { + if (positionOffset > .5) { + mOuterPageChangeListener.onPageScrolled(0, 0, 0); + } else { + mOuterPageChangeListener.onPageScrolled(realPosition, + 0, 0); + } + } + } + } + + @Override + public void onPageScrollStateChanged(int state) { + if (mAdapter != null) { + int position = LoopViewPager.super.getCurrentItem(); + int realPosition = mAdapter.toRealPosition(position); + if (state == ViewPager.SCROLL_STATE_IDLE + && (position == 0 || position == mAdapter.getCount() - 1)) { + setCurrentItem(realPosition, false); + } + } + if (mOuterPageChangeListener != null) { + mOuterPageChangeListener.onPageScrollStateChanged(state); + } + } + }; + +} diff --git a/app/src/main/java/com/nmc/android/helper/OnBoardingPagerAdapter.kt b/app/src/main/java/com/nmc/android/helper/OnBoardingPagerAdapter.kt new file mode 100644 index 000000000000..38398349eebf --- /dev/null +++ b/app/src/main/java/com/nmc/android/helper/OnBoardingPagerAdapter.kt @@ -0,0 +1,52 @@ +package com.nmc.android.helper + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.viewpager.widget.PagerAdapter +import com.bumptech.glide.Glide +import com.owncloud.android.databinding.OnboardingElementBinding +import com.owncloud.android.features.FeatureItem +import com.owncloud.android.utils.DisplayUtils + +class OnBoardingPagerAdapter(val context: Context, val items: List) : + PagerAdapter() { + + override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { + container.removeView(`object` as View) + } + + override fun getCount(): Int { + return items.size + } + + override fun isViewFromObject(view: View, `object`: Any): Boolean { + return view == `object` + } + + override fun instantiateItem(container: ViewGroup, position: Int): Any { + val binding = OnboardingElementBinding.inflate(LayoutInflater.from(context), container, false) + + //val fontColor = ResourcesCompat.getColor(requireContext().resources, R.color.login_text_color, null) + //binding.ivOnboarding.setImageDrawable(ThemeDrawableUtils.tintDrawable(onBoardingItem?.image ?: 0, fontColor)) + //binding.tvOnboarding.setText(onBoardingItem?.content ?: 0) + + //due to cropping of image in landscape mode we are using fix xy for landscape and + //center crop for other + if (com.nmc.android.utils.DisplayUtils.isLandscapeOrientation()) { + binding.ivOnboarding.scaleType = ImageView.ScaleType.FIT_XY + } else { + binding.ivOnboarding.scaleType = ImageView.ScaleType.CENTER_CROP + } + + Glide.with(context) + .load(items[position].image) + .into(binding.ivOnboarding) + + container.addView(binding.root, 0) + + return binding.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/helper/OnBoardingUtils.kt b/app/src/main/java/com/nmc/android/helper/OnBoardingUtils.kt new file mode 100644 index 000000000000..81b7ae7c5e8e --- /dev/null +++ b/app/src/main/java/com/nmc/android/helper/OnBoardingUtils.kt @@ -0,0 +1,48 @@ +package com.nmc.android.helper + +import com.owncloud.android.R +import com.owncloud.android.features.FeatureItem +import com.owncloud.android.utils.DisplayUtils + +class OnBoardingUtils { + + companion object { + private val DEFAULT_IMAGES = + arrayOf( + R.drawable.intro_screen_first, R.drawable.intro_screen_second, R.drawable + .intro_screen_third + ) + private val TAB_PORT_IMAGES = + arrayOf( + R.drawable.intro_screen_first_port_tab, R.drawable.intro_screen_second_port_tab, R.drawable + .intro_screen_third_port_tab + ) + private val TAB_LAND_IMAGES = + arrayOf( + R.drawable.intro_screen_first_land_tab, R.drawable.intro_screen_second_land_tab, R.drawable + .intro_screen_third_land_tab + ) + + fun getOnBoardingItems(): List { + val onBoardingItems = mutableListOf() + val onBoardingImages = getOnBoardingImages() + for (i in onBoardingImages.indices) { + val onBoardingItem = FeatureItem(onBoardingImages[i], R.string.empty, R.string.empty, false, false) + onBoardingItems.add(onBoardingItem) + } + return onBoardingItems + } + + private fun getOnBoardingImages(): Array { + return if (com.nmc.android.utils.DisplayUtils.isTablet()) { + if (com.nmc.android.utils.DisplayUtils.isLandscapeOrientation()) { + TAB_LAND_IMAGES + } else { + TAB_PORT_IMAGES + } + } else { + DEFAULT_IMAGES + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/DisplayUtils.kt b/app/src/main/java/com/nmc/android/utils/DisplayUtils.kt new file mode 100644 index 000000000000..f58e93c4252a --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/DisplayUtils.kt @@ -0,0 +1,18 @@ +package com.nmc.android.utils + +import android.content.res.Configuration +import com.owncloud.android.MainApp +import com.owncloud.android.R + +object DisplayUtils { + + @JvmStatic + fun isShowDividerForList(): Boolean = isTablet() || isLandscapeOrientation() + + @JvmStatic + fun isTablet(): Boolean = MainApp.getAppContext().resources.getBoolean(R.bool.isTablet) + + @JvmStatic + fun isLandscapeOrientation(): Boolean = + MainApp.getAppContext().resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE +} \ No newline at end of file diff --git a/app/src/main/res/drawable-xxxhdpi/intro_screen_first.png b/app/src/main/res/drawable-xxxhdpi/intro_screen_first.png new file mode 100644 index 000000000000..880421f9de3b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/intro_screen_first.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/intro_screen_first_land_tab.png b/app/src/main/res/drawable-xxxhdpi/intro_screen_first_land_tab.png new file mode 100644 index 000000000000..6e3df043031b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/intro_screen_first_land_tab.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/intro_screen_first_port_tab.png b/app/src/main/res/drawable-xxxhdpi/intro_screen_first_port_tab.png new file mode 100644 index 000000000000..8a6354f8e936 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/intro_screen_first_port_tab.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/intro_screen_second.png b/app/src/main/res/drawable-xxxhdpi/intro_screen_second.png new file mode 100644 index 000000000000..75cd26aadb5e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/intro_screen_second.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/intro_screen_second_land_tab.png b/app/src/main/res/drawable-xxxhdpi/intro_screen_second_land_tab.png new file mode 100644 index 000000000000..faa2188a4876 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/intro_screen_second_land_tab.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/intro_screen_second_port_tab.png b/app/src/main/res/drawable-xxxhdpi/intro_screen_second_port_tab.png new file mode 100644 index 000000000000..395767ba7f9a Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/intro_screen_second_port_tab.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/intro_screen_third.png b/app/src/main/res/drawable-xxxhdpi/intro_screen_third.png new file mode 100644 index 000000000000..2cabc9cd1c4c Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/intro_screen_third.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/intro_screen_third_land_tab.png b/app/src/main/res/drawable-xxxhdpi/intro_screen_third_land_tab.png new file mode 100644 index 000000000000..c5e3bca8af28 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/intro_screen_third_land_tab.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/intro_screen_third_port_tab.png b/app/src/main/res/drawable-xxxhdpi/intro_screen_third_port_tab.png new file mode 100644 index 000000000000..58357b3fb754 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/intro_screen_third_port_tab.png differ diff --git a/app/src/main/res/layout/first_run_activity.xml b/app/src/main/res/layout/first_run_activity.xml index a9a44b69226b..f53ad2c30b63 100644 --- a/app/src/main/res/layout/first_run_activity.xml +++ b/app/src/main/res/layout/first_run_activity.xml @@ -6,89 +6,45 @@ ~ SPDX-FileCopyrightText: 2018 Nextcloud GmbH ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> - + android:background="@color/primary"> - - - - - - - - - - + + - - - - - - - - - - - - + android:layout_height="0dp" + android:layout_gravity="center_vertical|center_horizontal" + android:layout_marginBottom="@dimen/alternate_double_margin" + android:layout_weight="1" + app:layout_constraintBottom_toTopOf="@+id/login" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + + + diff --git a/app/src/main/res/layout/onboarding_element.xml b/app/src/main/res/layout/onboarding_element.xml new file mode 100644 index 000000000000..a6e9401959a1 --- /dev/null +++ b/app/src/main/res/layout/onboarding_element.xml @@ -0,0 +1,40 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1b5885325067..4ed9b1a91cc4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -158,6 +158,7 @@ Konto wechseln Zu Konto wechseln Ja + Anmelden Testen Sie die Entwickler-Version Dies beinhaltet neue Funktionalitäten und ist nicht komplett qualitätsgesichert. Es können daher Fehler/Bugs auftreten, melden Sie uns solche bitte. Forum diff --git a/app/src/main/res/values-sw480dp/bool.xml b/app/src/main/res/values-sw480dp/bool.xml new file mode 100644 index 000000000000..8e66f10e898c --- /dev/null +++ b/app/src/main/res/values-sw480dp/bool.xml @@ -0,0 +1,4 @@ + + + true + diff --git a/app/src/main/res/values-sw600dp/dims.xml b/app/src/main/res/values-sw600dp/dims.xml index 7436d593a3c5..e78b278b0d34 100644 --- a/app/src/main/res/values-sw600dp/dims.xml +++ b/app/src/main/res/values-sw600dp/dims.xml @@ -9,4 +9,5 @@ 6 512dp + 96dp diff --git a/app/src/main/res/values/bool.xml b/app/src/main/res/values/bool.xml new file mode 100644 index 000000000000..c2dcd8baf0ea --- /dev/null +++ b/app/src/main/res/values/bool.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000000..cc9e25255a10 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,31 @@ + + + 4dp + 16dp + 24dp + 6dp + 18sp + 15sp + 15dp + 56dp + 86dp + 80dp + 11sp + 30dp + 55dp + 258dp + 17sp + 20dp + 160dp + 50dp + 150dp + 55dp + 48dp + 48dp + 24dp + 26dp + 20sp + 145dp + 1dp + 13sp + \ No newline at end of file diff --git a/app/src/main/res/values/dims.xml b/app/src/main/res/values/dims.xml index 316c12ef4906..9f444d6208a6 100644 --- a/app/src/main/res/values/dims.xml +++ b/app/src/main/res/values/dims.xml @@ -63,6 +63,7 @@ 20dp 40dp 10dp + 20dp 5dp 10dp 10dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf1866a5a7d4..b2300ab53de1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -219,6 +219,7 @@ Share Skip Copy + Login About Remove local account Remove account from device and delete all local files