diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6fb5c32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# Local configuration file (sdk path, etc) +local.properties + +# Intellij project files +*.iml +*.ipr +*.iws +.idea/ + +# Gradle +build/ +.gradle/ +gradle/ +gradlew +gradlew.bat + +.DS_Store +/captures +.externalNativeBuild diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..224a5ee --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..245f622 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.0" + defaultConfig { + applicationId "eu.inloop.support" + minSdkVersion 15 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:appcompat-v7:25.0.1' + testCompile 'junit:junit:4.12' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..eb04d8b --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/adammihalik/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/eu/inloop/support/sample/ExampleInstrumentedTest.java b/app/src/androidTest/java/eu/inloop/support/sample/ExampleInstrumentedTest.java new file mode 100644 index 0000000..8146b78 --- /dev/null +++ b/app/src/androidTest/java/eu/inloop/support/sample/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package eu.inloop.support.sample; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("sk.inloop.support.sample", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0e51918 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/eu/inloop/support/sample/FixedMainActivity.java b/app/src/main/java/eu/inloop/support/sample/FixedMainActivity.java new file mode 100644 index 0000000..907816c --- /dev/null +++ b/app/src/main/java/eu/inloop/support/sample/FixedMainActivity.java @@ -0,0 +1,56 @@ +package eu.inloop.support.sample; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.view.ViewPager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.View; +import android.widget.Button; + +import eu.inloop.support.sample.adapter.CorrectFragmentAdapter; + +public class FixedMainActivity extends AppCompatActivity { + + private CorrectFragmentAdapter mSectionsPagerAdapter; + + private ViewPager mViewPager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar); + setSupportActionBar(myToolbar); + + mSectionsPagerAdapter = new CorrectFragmentAdapter(getSupportFragmentManager()); + + mViewPager = (ViewPager) findViewById(R.id.container); + mViewPager.setAdapter(mSectionsPagerAdapter); + + ((Button)findViewById(R.id.test_start_btn)).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mSectionsPagerAdapter.toggleState(); + } + }); + + ((Button)findViewById(R.id.switch_tests_btn)).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(FixedMainActivity.this, IncorrectMainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + FixedMainActivity.this.startActivity(intent); + } + }); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return true; + } + +} diff --git a/app/src/main/java/eu/inloop/support/sample/IncorrectMainActivity.java b/app/src/main/java/eu/inloop/support/sample/IncorrectMainActivity.java new file mode 100644 index 0000000..d0876fc --- /dev/null +++ b/app/src/main/java/eu/inloop/support/sample/IncorrectMainActivity.java @@ -0,0 +1,56 @@ +package eu.inloop.support.sample; + +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.view.ViewPager; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.View; +import android.widget.Button; + +import eu.inloop.support.sample.adapter.IncorrectFragmentAdapter; + +public class IncorrectMainActivity extends AppCompatActivity { + + private IncorrectFragmentAdapter mSectionsPagerAdapter; + + private ViewPager mViewPager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar); + setSupportActionBar(myToolbar); + + mSectionsPagerAdapter = new IncorrectFragmentAdapter(getSupportFragmentManager()); + + mViewPager = (ViewPager) findViewById(R.id.container); + mViewPager.setAdapter(mSectionsPagerAdapter); + + ((Button)findViewById(R.id.test_start_btn)).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mSectionsPagerAdapter.toggleState(); + } + }); + + ((Button)findViewById(R.id.switch_tests_btn)).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(IncorrectMainActivity.this, FixedMainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + IncorrectMainActivity.this.startActivity(intent); + } + }); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return true; + } + +} diff --git a/app/src/main/java/eu/inloop/support/sample/SampleFragment.java b/app/src/main/java/eu/inloop/support/sample/SampleFragment.java new file mode 100644 index 0000000..a4cc154 --- /dev/null +++ b/app/src/main/java/eu/inloop/support/sample/SampleFragment.java @@ -0,0 +1,38 @@ +package eu.inloop.support.sample; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +/** + * Created by adammihalik on 05/12/2016. + */ + +public class SampleFragment extends Fragment { + public static SampleFragment newInstance(String label) { + Bundle bundle = new Bundle(); + bundle.putString("label", label); + SampleFragment testFragment = new SampleFragment(); + testFragment.setArguments(bundle); + return testFragment; + } + + public String getLabel() { + return getArguments().getString("label"); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_template, container, false); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + ((TextView)view.findViewById(R.id.fragment_label)).setText(getLabel()); + } +} diff --git a/app/src/main/java/eu/inloop/support/sample/adapter/CorrectFragmentAdapter.java b/app/src/main/java/eu/inloop/support/sample/adapter/CorrectFragmentAdapter.java new file mode 100644 index 0000000..662e4a9 --- /dev/null +++ b/app/src/main/java/eu/inloop/support/sample/adapter/CorrectFragmentAdapter.java @@ -0,0 +1,62 @@ +package eu.inloop.support.sample.adapter; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; + +import eu.inloop.support.sample.SampleFragment; +import eu.inloop.support.v4.app.FragmentStatePagerAdapter; + +/** + * Extends {@link FragmentStatePagerAdapter}, therefore {@link #notifyDataSetChanged()} will not fail. + */ +public class CorrectFragmentAdapter extends FragmentStatePagerAdapter { + + private boolean mState = true; + + public CorrectFragmentAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + } + + @Override + public int getCount() { + return 3; + } + + public void toggleState() { + mState = !mState; + notifyDataSetChanged(); + } + + private String getLabel(int position) { + switch (position) { + case 0: + return "A"; + case 1: + return mState ? "B" : "C"; + default: + return mState ? "C" : "B"; + } + } + + @Override + public int getItemPosition(Object object) { + String label = ((SampleFragment) object).getLabel(); + if (label.equals("A")) { + return 0; + } else if (label.equals("B")) { + return mState ? 1 : 2; + } else { + return mState ? 2 : 1; + } + } + + @Override + public CharSequence getPageTitle(int position) { + return getLabel(position); + } + + @Override + public Fragment getItem(int position) { + return SampleFragment.newInstance(getLabel(position)); + } +} diff --git a/app/src/main/java/eu/inloop/support/sample/adapter/IncorrectFragmentAdapter.java b/app/src/main/java/eu/inloop/support/sample/adapter/IncorrectFragmentAdapter.java new file mode 100644 index 0000000..a1c3c5c --- /dev/null +++ b/app/src/main/java/eu/inloop/support/sample/adapter/IncorrectFragmentAdapter.java @@ -0,0 +1,62 @@ +package eu.inloop.support.sample.adapter; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; + +import eu.inloop.support.sample.SampleFragment; + +/** + * Extends {@link FragmentStatePagerAdapter} that fails on {@link #notifyDataSetChanged()}. + */ + +public class IncorrectFragmentAdapter extends FragmentStatePagerAdapter { + private boolean mState = true; + + public IncorrectFragmentAdapter(FragmentManager fragmentManager) { + super(fragmentManager); + } + + @Override + public int getCount() { + return 3; + } + + public void toggleState() { + mState = !mState; + notifyDataSetChanged(); + } + + private String getLabel(int position) { + switch (position) { + case 0: + return "A"; + case 1: + return mState ? "B" : "C"; + default: + return mState ? "C" : "B"; + } + } + + @Override + public int getItemPosition(Object object) { + String label = ((SampleFragment) object).getLabel(); + if (label.equals("A")) { + return 0; + } else if (label.equals("B")) { + return mState ? 1 : 2; + } else { + return mState ? 2 : 1; + } + } + + @Override + public CharSequence getPageTitle(int position) { + return getLabel(position); + } + + @Override + public Fragment getItem(int position) { + return SampleFragment.newInstance(getLabel(position)); + } +} diff --git a/app/src/main/java/eu/inloop/support/v4/app/FragmentStatePagerAdapter.java b/app/src/main/java/eu/inloop/support/v4/app/FragmentStatePagerAdapter.java new file mode 100644 index 0000000..9849e12 --- /dev/null +++ b/app/src/main/java/eu/inloop/support/v4/app/FragmentStatePagerAdapter.java @@ -0,0 +1,236 @@ +package eu.inloop.support.v4.app; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Created by adammihalik on 05/12/2016. + */ + +public abstract class FragmentStatePagerAdapter extends PagerAdapter { + + @NonNull private static final String TAG = FragmentStatePagerAdapter.class.getSimpleName(); + + @NonNull private final FragmentManager mFragmentManager; + @Nullable private FragmentTransaction mCurTransaction = null; + + @NonNull private List mSavedState; + @NonNull private List mFragments; + @Nullable private Fragment mCurrentPrimaryItem = null; + + public FragmentStatePagerAdapter(@NonNull FragmentManager fm) { + mFragmentManager = fm; + mFragments = new ArrayList(); + mSavedState = new ArrayList(); + } + + /** + * Return the Fragment associated with a specified position. + */ + @NonNull + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(@NonNull ViewGroup container) { + if (container.getId() == View.NO_ID) { + throw new IllegalStateException("ViewPager with adapter " + this + " requires a view id"); + } + } + + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + Fragment result = tryGetExistingFragment(position); //performance, if fragment has been initialized before, just return it + if (result != null) { + return result; + } + + Fragment fragment = getItem(position); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Adding item #" + position + ": f=" + fragment); + } + Fragment.SavedState fss = tryGetSavedState(position); + if (fss != null) { + fragment.setInitialSavedState(fss); + } + setupFragmentAsSecondary(fragment); + addFragmentToList(position, fragment); + getOrCreateFragmentTransaction().add(container.getId(), fragment); + + return fragment; + } + + /** + * Add given {@link Fragment} to fragments list. Ensure, that this fragment will be found on given 'position' anytime (except if fragment will be removed). + */ + private void addFragmentToList(int position, @Nullable Fragment fragment) { + while (mFragments.size() <= position) { + //TODO: could be more effective??? + mFragments.add(null); + } + mFragments.set(position, fragment); + } + + /** + * Get existing {@link FragmentTransaction}. If transaction is not initialized, create it. + */ + @NonNull + private FragmentTransaction getOrCreateFragmentTransaction() { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + return mCurTransaction; + } + + /** + * Return saved {@link android.support.v4.app.Fragment.SavedState} for given 'position'. If state is not saved, NULL is returned. + */ + @Nullable + private Fragment.SavedState tryGetSavedState(int position) { + if (position >= 0 && mSavedState.size() > position) { + return mSavedState.get(position); + } + return null; + } + + /** + * Return existing {@link Fragment} at defined 'position'. If fragment does not exist, NULL is returned. + */ + @Nullable + private Fragment tryGetExistingFragment(int position) { + if (position >= 0 && mFragments.size() > position) { + return mFragments.get(position); + } + return null; + } + + @Override + public void destroyItem(@Nullable ViewGroup container, int position, @NonNull Object object) { + Fragment fragment = (Fragment) object; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Removing item #" + position + ": f=" + object + " v=" + fragment.getView()); + } + addStateOfFragmentToList(position, (fragment.isAdded() ? mFragmentManager.saveFragmentInstanceState(fragment) : null)); + addFragmentToList(position, null); + getOrCreateFragmentTransaction().remove(fragment); + } + + /** + * Add given {@link Fragment.SavedState} to states list. Ensure, that this state will be found on given 'position' anytime for restoration. + */ + private void addStateOfFragmentToList(int position, @Nullable Fragment.SavedState state) { + while (mSavedState.size() <= position) { + //TODO: could be more effective??? + mSavedState.add(null); + } + mSavedState.set(position, state); + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @Nullable Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + setupFragmentAsSecondary(mCurrentPrimaryItem); + } + if (fragment != null) { + setupFragmentAsPrimary(fragment); + } + mCurrentPrimaryItem = fragment; + } + } + + /** + * Setup given {@link Fragment} to be ready as primary. + */ + private void setupFragmentAsPrimary(@NonNull Fragment fragment) { + fragment.setMenuVisibility(true); + fragment.setUserVisibleHint(true); + } + + /** + * Setup given {@link Fragment} to be ready as NOT primary. + */ + private void setupFragmentAsSecondary(@NonNull Fragment fragment) { + fragment.setMenuVisibility(false); + fragment.setUserVisibleHint(false); + } + + @Override + public void finishUpdate(@Nullable ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitNowAllowingStateLoss(); + mCurTransaction = null; + } + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return ((Fragment)object).getView() == view; + } + + @NonNull + @Override + public Parcelable saveState() { + Bundle state = null; + if (mSavedState.size() > 0) { + state = new Bundle(); + Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; + mSavedState.toArray(fss); + state.putParcelableArray("states", fss); + } + for (int i=0; i keys = bundle.keySet(); + for (String key: keys) { + if (key.startsWith("f")) { + int index = Integer.parseInt(key.substring(1)); + Fragment f = mFragmentManager.getFragment(bundle, key); + if (f != null) { + f.setMenuVisibility(false); + addFragmentToList(index, f); + } else { + Log.w(TAG, "Bad fragment at key " + key); + } + } + } + } + } + +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ddd9a4a --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,42 @@ + + + + +