From 97a0a5cd419f6a4bf8559668e306dfd18812f135 Mon Sep 17 00:00:00 2001 From: Yuriy Liskov Date: Sat, 14 Oct 2023 21:10:02 +0300 Subject: [PATCH] add modded androidx.fragment lib --- build.gradle | 2 + common/build.gradle | 1 + fragment-1.1.0/.gitignore | 1 + fragment-1.1.0/build.gradle | 41 + fragment-1.1.0/src/main/AndroidManifest.xml | 22 + .../fragment/app/BackStackRecord.java | 667 ++++ .../androidx/fragment/app/BackStackState.java | 182 + .../androidx/fragment/app/DialogFragment.java | 548 +++ .../java/androidx/fragment/app/Fragment.java | 3041 ++++++++++++++ .../fragment/app/FragmentActivity.java | 1013 +++++ .../fragment/app/FragmentContainer.java | 59 + .../fragment/app/FragmentController.java | 532 +++ .../fragment/app/FragmentFactory.java | 132 + .../fragment/app/FragmentHostCallback.java | 210 + .../fragment/app/FragmentManager.java | 601 +++ .../fragment/app/FragmentManagerImpl.java | 3484 +++++++++++++++++ .../app/FragmentManagerNonConfig.java | 86 + .../fragment/app/FragmentManagerState.java | 70 + .../app/FragmentManagerViewModel.java | 280 ++ .../fragment/app/FragmentPagerAdapter.java | 271 ++ .../androidx/fragment/app/FragmentState.java | 185 + .../app/FragmentStatePagerAdapter.java | 323 ++ .../fragment/app/FragmentTabHost.java | 441 +++ .../fragment/app/FragmentTransaction.java | 731 ++++ .../fragment/app/FragmentTransition.java | 1267 ++++++ .../app/FragmentTransitionCompat21.java | 318 ++ .../fragment/app/FragmentTransitionImpl.java | 368 ++ .../app/FragmentViewLifecycleOwner.java | 53 + .../androidx/fragment/app/ListFragment.java | 401 ++ .../fragment/app/SuperNotCalledException.java | 25 + leanback-1.0.0/build.gradle | 11 +- settings.gradle | 2 +- smarttubetv/build.gradle | 1 + 33 files changed, 15363 insertions(+), 6 deletions(-) create mode 100644 fragment-1.1.0/.gitignore create mode 100644 fragment-1.1.0/build.gradle create mode 100644 fragment-1.1.0/src/main/AndroidManifest.xml create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackRecord.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackState.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/DialogFragment.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/Fragment.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentActivity.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentContainer.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentController.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentFactory.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentHostCallback.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManager.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerImpl.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerNonConfig.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerState.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerViewModel.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentPagerAdapter.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentState.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentStatePagerAdapter.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTabHost.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransaction.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransition.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/ListFragment.java create mode 100644 fragment-1.1.0/src/main/java/androidx/fragment/app/SuperNotCalledException.java diff --git a/build.gradle b/build.gradle index 379d90c85..273800211 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,8 @@ allprojects { // Replace with modded library (leanback-1.0.0) exclude group: 'androidx.leanback', module: 'leanback' + // Replace with modded library (fragment-1.1.0) + exclude group: 'androidx.fragment', module: 'fragment' // Don't work! Replace with modded library (leanback-1.0.0) // resolutionStrategy.dependencySubstitution { diff --git a/common/build.gradle b/common/build.gradle index eafeb060d..d2c9696ae 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -54,6 +54,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:' + testXSupportLibraryVersion implementation project(':sharedutils') + implementation project(':fragment-1.1.0') implementation project(':mediaserviceinterfaces') implementation project(':youtubeapi') diff --git a/fragment-1.1.0/.gitignore b/fragment-1.1.0/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/fragment-1.1.0/.gitignore @@ -0,0 +1 @@ +/build diff --git a/fragment-1.1.0/build.gradle b/fragment-1.1.0/build.gradle new file mode 100644 index 000000000..ea2d539f7 --- /dev/null +++ b/fragment-1.1.0/build.gradle @@ -0,0 +1,41 @@ +apply from: gradle.ext.sharedModulesConstants +apply plugin: 'com.android.library' + +android { + // FIX: Default interface methods are only supported starting with Android N (--min-api 24) + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + compileSdkVersion project.properties.compileSdkVersion + buildToolsVersion project.properties.buildToolsVersion + testOptions.unitTests.includeAndroidResources = true + + defaultConfig { + minSdkVersion project.properties.minSdkVersion + targetSdkVersion project.properties.targetSdkVersion + versionCode 10 + versionName "1.0.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + api 'androidx.activity:activity:' + leanbackXLibraryVersion // used inside other app modules + implementation 'androidx.annotation:annotation:' + annotationXLibraryVersion + implementation 'androidx.core:core:' + kotlinCoreVersion + implementation 'androidx.viewpager:viewpager:' + leanbackXLibraryVersion + implementation 'androidx.loader:loader:' + leanbackXLibraryVersion +} diff --git a/fragment-1.1.0/src/main/AndroidManifest.xml b/fragment-1.1.0/src/main/AndroidManifest.xml new file mode 100644 index 000000000..48a146811 --- /dev/null +++ b/fragment-1.1.0/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackRecord.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackRecord.java new file mode 100644 index 000000000..73fceec02 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackRecord.java @@ -0,0 +1,667 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.LogWriter; +import androidx.lifecycle.Lifecycle; + +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Entry of an operation on the fragment back stack. + */ +final class BackStackRecord extends FragmentTransaction implements + FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator { + static final String TAG = FragmentManagerImpl.TAG; + + final FragmentManagerImpl mManager; + + boolean mCommitted; + int mIndex = -1; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("BackStackEntry{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + if (mIndex >= 0) { + sb.append(" #"); + sb.append(mIndex); + } + if (mName != null) { + sb.append(" "); + sb.append(mName); + } + sb.append("}"); + return sb.toString(); + } + + public void dump(String prefix, PrintWriter writer) { + dump(prefix, writer, true); + } + + public void dump(String prefix, PrintWriter writer, boolean full) { + if (full) { + writer.print(prefix); writer.print("mName="); writer.print(mName); + writer.print(" mIndex="); writer.print(mIndex); + writer.print(" mCommitted="); writer.println(mCommitted); + if (mTransition != FragmentTransaction.TRANSIT_NONE) { + writer.print(prefix); writer.print("mTransition=#"); + writer.print(Integer.toHexString(mTransition)); + writer.print(" mTransitionStyle=#"); + writer.println(Integer.toHexString(mTransitionStyle)); + } + if (mEnterAnim != 0 || mExitAnim !=0) { + writer.print(prefix); writer.print("mEnterAnim=#"); + writer.print(Integer.toHexString(mEnterAnim)); + writer.print(" mExitAnim=#"); + writer.println(Integer.toHexString(mExitAnim)); + } + if (mPopEnterAnim != 0 || mPopExitAnim !=0) { + writer.print(prefix); writer.print("mPopEnterAnim=#"); + writer.print(Integer.toHexString(mPopEnterAnim)); + writer.print(" mPopExitAnim=#"); + writer.println(Integer.toHexString(mPopExitAnim)); + } + if (mBreadCrumbTitleRes != 0 || mBreadCrumbTitleText != null) { + writer.print(prefix); writer.print("mBreadCrumbTitleRes=#"); + writer.print(Integer.toHexString(mBreadCrumbTitleRes)); + writer.print(" mBreadCrumbTitleText="); + writer.println(mBreadCrumbTitleText); + } + if (mBreadCrumbShortTitleRes != 0 || mBreadCrumbShortTitleText != null) { + writer.print(prefix); writer.print("mBreadCrumbShortTitleRes=#"); + writer.print(Integer.toHexString(mBreadCrumbShortTitleRes)); + writer.print(" mBreadCrumbShortTitleText="); + writer.println(mBreadCrumbShortTitleText); + } + } + + if (!mOps.isEmpty()) { + writer.print(prefix); writer.println("Operations:"); + final int numOps = mOps.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + final Op op = mOps.get(opNum); + String cmdStr; + switch (op.mCmd) { + case OP_NULL: cmdStr="NULL"; break; + case OP_ADD: cmdStr="ADD"; break; + case OP_REPLACE: cmdStr="REPLACE"; break; + case OP_REMOVE: cmdStr="REMOVE"; break; + case OP_HIDE: cmdStr="HIDE"; break; + case OP_SHOW: cmdStr="SHOW"; break; + case OP_DETACH: cmdStr="DETACH"; break; + case OP_ATTACH: cmdStr="ATTACH"; break; + case OP_SET_PRIMARY_NAV: cmdStr="SET_PRIMARY_NAV"; break; + case OP_UNSET_PRIMARY_NAV: cmdStr="UNSET_PRIMARY_NAV";break; + case OP_SET_MAX_LIFECYCLE: cmdStr = "OP_SET_MAX_LIFECYCLE"; break; + default: cmdStr = "cmd=" + op.mCmd; break; + } + writer.print(prefix); writer.print(" Op #"); writer.print(opNum); + writer.print(": "); writer.print(cmdStr); + writer.print(" "); writer.println(op.mFragment); + if (full) { + if (op.mEnterAnim != 0 || op.mExitAnim != 0) { + writer.print(prefix); writer.print("enterAnim=#"); + writer.print(Integer.toHexString(op.mEnterAnim)); + writer.print(" exitAnim=#"); + writer.println(Integer.toHexString(op.mExitAnim)); + } + if (op.mPopEnterAnim != 0 || op.mPopExitAnim != 0) { + writer.print(prefix); writer.print("popEnterAnim=#"); + writer.print(Integer.toHexString(op.mPopEnterAnim)); + writer.print(" popExitAnim=#"); + writer.println(Integer.toHexString(op.mPopExitAnim)); + } + } + } + } + } + + public BackStackRecord(FragmentManagerImpl manager) { + mManager = manager; + } + + @Override + public int getId() { + return mIndex; + } + + @Override + public int getBreadCrumbTitleRes() { + return mBreadCrumbTitleRes; + } + + @Override + public int getBreadCrumbShortTitleRes() { + return mBreadCrumbShortTitleRes; + } + + @Override + @Nullable + public CharSequence getBreadCrumbTitle() { + if (mBreadCrumbTitleRes != 0) { + return mManager.mHost.getContext().getText(mBreadCrumbTitleRes); + } + return mBreadCrumbTitleText; + } + + @Override + @Nullable + public CharSequence getBreadCrumbShortTitle() { + if (mBreadCrumbShortTitleRes != 0) { + return mManager.mHost.getContext().getText(mBreadCrumbShortTitleRes); + } + return mBreadCrumbShortTitleText; + } + + @Override + void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) { + super.doAddOp(containerViewId, fragment, tag, opcmd); + fragment.mFragmentManager = mManager; + } + + @NonNull + @Override + public FragmentTransaction remove(@NonNull Fragment fragment) { + if (fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) { + throw new IllegalStateException("Cannot remove Fragment attached to " + + "a different FragmentManager. Fragment " + fragment.toString() + " is already" + + " attached to a FragmentManager."); + } + return super.remove(fragment); + } + + @NonNull + @Override + public FragmentTransaction hide(@NonNull Fragment fragment) { + if (fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) { + throw new IllegalStateException("Cannot hide Fragment attached to " + + "a different FragmentManager. Fragment " + fragment.toString() + " is already" + + " attached to a FragmentManager."); + } + return super.hide(fragment); + } + + @NonNull + @Override + public FragmentTransaction show(@NonNull Fragment fragment) { + if (fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) { + throw new IllegalStateException("Cannot show Fragment attached to " + + "a different FragmentManager. Fragment " + fragment.toString() + " is already" + + " attached to a FragmentManager."); + } + return super.show(fragment); + } + + @NonNull + @Override + public FragmentTransaction detach(@NonNull Fragment fragment) { + if (fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) { + throw new IllegalStateException("Cannot detach Fragment attached to " + + "a different FragmentManager. Fragment " + fragment.toString() + " is already" + + " attached to a FragmentManager."); + } + return super.detach(fragment); + } + + @NonNull + @Override + public FragmentTransaction setPrimaryNavigationFragment(@Nullable Fragment fragment) { + if (fragment != null + && fragment.mFragmentManager != null && fragment.mFragmentManager != mManager) { + throw new IllegalStateException("Cannot setPrimaryNavigation for Fragment attached to " + + "a different FragmentManager. Fragment " + fragment.toString() + " is already" + + " attached to a FragmentManager."); + } + return super.setPrimaryNavigationFragment(fragment); + } + + @NonNull + @Override + public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment, + @NonNull Lifecycle.State state) { + if (fragment.mFragmentManager != mManager) { + throw new IllegalArgumentException("Cannot setMaxLifecycle for Fragment not attached to" + + " FragmentManager " + mManager); + } + if (!state.isAtLeast(Lifecycle.State.CREATED)) { + throw new IllegalArgumentException("Cannot set maximum Lifecycle below " + + Lifecycle.State.CREATED); + } + return super.setMaxLifecycle(fragment, state); + } + + void bumpBackStackNesting(int amt) { + if (!mAddToBackStack) { + return; + } + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting in " + this + + " by " + amt); + final int numOps = mOps.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + final Op op = mOps.get(opNum); + if (op.mFragment != null) { + op.mFragment.mBackStackNesting += amt; + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of " + + op.mFragment + " to " + op.mFragment.mBackStackNesting); + } + } + } + + public void runOnCommitRunnables() { + if (mCommitRunnables != null) { + for (int i = 0; i < mCommitRunnables.size(); i++) { + mCommitRunnables.get(i).run(); + } + mCommitRunnables = null; + } + } + + @Override + public int commit() { + return commitInternal(false); + } + + @Override + public int commitAllowingStateLoss() { + return commitInternal(true); + } + + @Override + public void commitNow() { + disallowAddToBackStack(); + mManager.execSingleAction(this, false); + } + + @Override + public void commitNowAllowingStateLoss() { + disallowAddToBackStack(); + mManager.execSingleAction(this, true); + } + + int commitInternal(boolean allowStateLoss) { + if (mCommitted) throw new IllegalStateException("commit already called"); + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Commit: " + this); + LogWriter logw = new LogWriter(TAG); + PrintWriter pw = new PrintWriter(logw); + dump(" ", pw); + pw.close(); + } + mCommitted = true; + if (mAddToBackStack) { + mIndex = mManager.allocBackStackIndex(this); + } else { + mIndex = -1; + } + mManager.enqueueAction(this, allowStateLoss); + return mIndex; + } + + /** + * Implementation of {@link FragmentManagerImpl.OpGenerator}. + * This operation is added to the list of pending actions during {@link #commit()}, and + * will be executed on the UI thread to run this FragmentTransaction. + * + * @param records Modified to add this BackStackRecord + * @param isRecordPop Modified to add a false (this isn't a pop) + * @return true always because the records and isRecordPop will always be changed + */ + @Override + public boolean generateOps(ArrayList records, ArrayList isRecordPop) { + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Run: " + this); + } + + records.add(this); + isRecordPop.add(false); + if (mAddToBackStack) { + mManager.addBackStackState(this); + } + return true; + } + + boolean interactsWith(int containerId) { + final int numOps = mOps.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + final Op op = mOps.get(opNum); + final int fragContainer = op.mFragment != null ? op.mFragment.mContainerId : 0; + if (fragContainer != 0 && fragContainer == containerId) { + return true; + } + } + return false; + } + + boolean interactsWith(ArrayList records, int startIndex, int endIndex) { + if (endIndex == startIndex) { + return false; + } + final int numOps = mOps.size(); + int lastContainer = -1; + for (int opNum = 0; opNum < numOps; opNum++) { + final Op op = mOps.get(opNum); + final int container = op.mFragment != null ? op.mFragment.mContainerId : 0; + if (container != 0 && container != lastContainer) { + lastContainer = container; + for (int i = startIndex; i < endIndex; i++) { + BackStackRecord record = records.get(i); + final int numThoseOps = record.mOps.size(); + for (int thoseOpIndex = 0; thoseOpIndex < numThoseOps; thoseOpIndex++) { + final Op thatOp = record.mOps.get(thoseOpIndex); + final int thatContainer = thatOp.mFragment != null + ? thatOp.mFragment.mContainerId : 0; + if (thatContainer == container) { + return true; + } + } + } + } + } + return false; + } + + /** + * Executes the operations contained within this transaction. The Fragment states will only + * be modified if optimizations are not allowed. + */ + void executeOps() { + final int numOps = mOps.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + final Op op = mOps.get(opNum); + final Fragment f = op.mFragment; + if (f != null) { + f.setNextTransition(mTransition, mTransitionStyle); + } + switch (op.mCmd) { + case OP_ADD: + f.setNextAnim(op.mEnterAnim); + mManager.addFragment(f, false); + break; + case OP_REMOVE: + f.setNextAnim(op.mExitAnim); + mManager.removeFragment(f); + break; + case OP_HIDE: + f.setNextAnim(op.mExitAnim); + mManager.hideFragment(f); + break; + case OP_SHOW: + f.setNextAnim(op.mEnterAnim); + mManager.showFragment(f); + break; + case OP_DETACH: + f.setNextAnim(op.mExitAnim); + mManager.detachFragment(f); + break; + case OP_ATTACH: + f.setNextAnim(op.mEnterAnim); + mManager.attachFragment(f); + break; + case OP_SET_PRIMARY_NAV: + mManager.setPrimaryNavigationFragment(f); + break; + case OP_UNSET_PRIMARY_NAV: + mManager.setPrimaryNavigationFragment(null); + break; + case OP_SET_MAX_LIFECYCLE: + mManager.setMaxLifecycle(f, op.mCurrentMaxState); + break; + default: + throw new IllegalArgumentException("Unknown cmd: " + op.mCmd); + } + if (!mReorderingAllowed && op.mCmd != OP_ADD && f != null) { + mManager.moveFragmentToExpectedState(f); + } + } + if (!mReorderingAllowed) { + // Added fragments are added at the end to comply with prior behavior. + mManager.moveToState(mManager.mCurState, true); + } + } + + /** + * Reverses the execution of the operations within this transaction. The Fragment states will + * only be modified if reordering is not allowed. + * + * @param moveToState {@code true} if added fragments should be moved to their final state + * in ordered transactions + */ + void executePopOps(boolean moveToState) { + for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) { + final Op op = mOps.get(opNum); + Fragment f = op.mFragment; + if (f != null) { + f.setNextTransition(FragmentManagerImpl.reverseTransit(mTransition), + mTransitionStyle); + } + switch (op.mCmd) { + case OP_ADD: + f.setNextAnim(op.mPopExitAnim); + mManager.removeFragment(f); + break; + case OP_REMOVE: + f.setNextAnim(op.mPopEnterAnim); + mManager.addFragment(f, false); + break; + case OP_HIDE: + f.setNextAnim(op.mPopEnterAnim); + mManager.showFragment(f); + break; + case OP_SHOW: + f.setNextAnim(op.mPopExitAnim); + mManager.hideFragment(f); + break; + case OP_DETACH: + f.setNextAnim(op.mPopEnterAnim); + mManager.attachFragment(f); + break; + case OP_ATTACH: + f.setNextAnim(op.mPopExitAnim); + mManager.detachFragment(f); + break; + case OP_SET_PRIMARY_NAV: + mManager.setPrimaryNavigationFragment(null); + break; + case OP_UNSET_PRIMARY_NAV: + mManager.setPrimaryNavigationFragment(f); + break; + case OP_SET_MAX_LIFECYCLE: + mManager.setMaxLifecycle(f, op.mOldMaxState); + break; + default: + throw new IllegalArgumentException("Unknown cmd: " + op.mCmd); + } + if (!mReorderingAllowed && op.mCmd != OP_REMOVE && f != null) { + mManager.moveFragmentToExpectedState(f); + } + } + if (!mReorderingAllowed && moveToState) { + mManager.moveToState(mManager.mCurState, true); + } + } + + /** + * Expands all meta-ops into their more primitive equivalents. This must be called prior to + * {@link #executeOps()} or any other call that operations on mOps for forward navigation. + * It should not be called for pop/reverse navigation operations. + * + *

Removes all OP_REPLACE ops and replaces them with the proper add and remove + * operations that are equivalent to the replace.

+ * + *

Adds OP_UNSET_PRIMARY_NAV ops to match OP_SET_PRIMARY_NAV, OP_REMOVE and OP_DETACH + * ops so that we can restore the old primary nav fragment later. Since callers call this + * method in a loop before running ops from several transactions at once, the caller should + * pass the return value from this method as the oldPrimaryNav parameter for the next call. + * The first call in such a loop should pass the value of + * {@link FragmentManager#getPrimaryNavigationFragment()}.

+ * + * @param added Initialized to the fragments that are in the mManager.mAdded, this + * will be modified to contain the fragments that will be in mAdded + * after the execution ({@link #executeOps()}. + * @param oldPrimaryNav The tracked primary navigation fragment as of the beginning of + * this set of ops + * @return the new oldPrimaryNav fragment after this record's ops would be run + */ + @SuppressWarnings("ReferenceEquality") + Fragment expandOps(ArrayList added, Fragment oldPrimaryNav) { + for (int opNum = 0; opNum < mOps.size(); opNum++) { + final Op op = mOps.get(opNum); + switch (op.mCmd) { + case OP_ADD: + case OP_ATTACH: + added.add(op.mFragment); + break; + case OP_REMOVE: + case OP_DETACH: { + added.remove(op.mFragment); + if (op.mFragment == oldPrimaryNav) { + mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, op.mFragment)); + opNum++; + oldPrimaryNav = null; + } + } + break; + case OP_REPLACE: { + final Fragment f = op.mFragment; + final int containerId = f.mContainerId; + boolean alreadyAdded = false; + for (int i = added.size() - 1; i >= 0; i--) { + final Fragment old = added.get(i); + if (old.mContainerId == containerId) { + if (old == f) { + alreadyAdded = true; + } else { + // This is duplicated from above since we only make + // a single pass for expanding ops. Unset any outgoing primary nav. + if (old == oldPrimaryNav) { + mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, old)); + opNum++; + oldPrimaryNav = null; + } + final Op removeOp = new Op(OP_REMOVE, old); + removeOp.mEnterAnim = op.mEnterAnim; + removeOp.mPopEnterAnim = op.mPopEnterAnim; + removeOp.mExitAnim = op.mExitAnim; + removeOp.mPopExitAnim = op.mPopExitAnim; + mOps.add(opNum, removeOp); + added.remove(old); + opNum++; + } + } + } + if (alreadyAdded) { + mOps.remove(opNum); + opNum--; + } else { + op.mCmd = OP_ADD; + added.add(f); + } + } + break; + case OP_SET_PRIMARY_NAV: { + // It's ok if this is null, that means we will restore to no active + // primary navigation fragment on a pop. + mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, oldPrimaryNav)); + opNum++; + // Will be set by the OP_SET_PRIMARY_NAV we inserted before when run + oldPrimaryNav = op.mFragment; + } + break; + } + } + return oldPrimaryNav; + } + + /** + * Removes fragments that are added or removed during a pop operation. + * + * @param added Initialized to the fragments that are in the mManager.mAdded, this + * will be modified to contain the fragments that will be in mAdded + * after the execution ({@link #executeOps()}. + * @param oldPrimaryNav The tracked primary navigation fragment as of the beginning of + * this set of ops + * @return the new oldPrimaryNav fragment after this record's ops would be popped + */ + Fragment trackAddedFragmentsInPop(ArrayList added, Fragment oldPrimaryNav) { + for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) { + final Op op = mOps.get(opNum); + switch (op.mCmd) { + case OP_ADD: + case OP_ATTACH: + added.remove(op.mFragment); + break; + case OP_REMOVE: + case OP_DETACH: + added.add(op.mFragment); + break; + case OP_UNSET_PRIMARY_NAV: + oldPrimaryNav = op.mFragment; + break; + case OP_SET_PRIMARY_NAV: + oldPrimaryNav = null; + break; + case OP_SET_MAX_LIFECYCLE: + op.mCurrentMaxState = op.mOldMaxState; + break; + } + } + return oldPrimaryNav; + } + + boolean isPostponed() { + for (int opNum = 0; opNum < mOps.size(); opNum++) { + final Op op = mOps.get(opNum); + if (isFragmentPostponed(op)) { + return true; + } + } + return false; + } + + void setOnStartPostponedListener(Fragment.OnStartEnterTransitionListener listener) { + for (int opNum = 0; opNum < mOps.size(); opNum++) { + final Op op = mOps.get(opNum); + if (isFragmentPostponed(op)) { + op.mFragment.setOnStartEnterTransitionListener(listener); + } + } + } + + private static boolean isFragmentPostponed(Op op) { + final Fragment fragment = op.mFragment; + return fragment != null && fragment.mAdded && fragment.mView != null && !fragment.mDetached + && !fragment.mHidden && fragment.isPostponed(); + } + + @Override + @Nullable + public String getName() { + return mName; + } + + @Override + public boolean isEmpty() { + return mOps.isEmpty(); + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackState.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackState.java new file mode 100644 index 000000000..7ae173953 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/BackStackState.java @@ -0,0 +1,182 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.annotation.SuppressLint; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import androidx.lifecycle.Lifecycle; + +import java.util.ArrayList; + +@SuppressLint("BanParcelableUsage") +final class BackStackState implements Parcelable { + final int[] mOps; + final ArrayList mFragmentWhos; + final int[] mOldMaxLifecycleStates; + final int[] mCurrentMaxLifecycleStates; + final int mTransition; + final int mTransitionStyle; + final String mName; + final int mIndex; + final int mBreadCrumbTitleRes; + final CharSequence mBreadCrumbTitleText; + final int mBreadCrumbShortTitleRes; + final CharSequence mBreadCrumbShortTitleText; + final ArrayList mSharedElementSourceNames; + final ArrayList mSharedElementTargetNames; + final boolean mReorderingAllowed; + + public BackStackState(BackStackRecord bse) { + final int numOps = bse.mOps.size(); + mOps = new int[numOps * 5]; + + if (!bse.mAddToBackStack) { + throw new IllegalStateException("Not on back stack"); + } + + mFragmentWhos = new ArrayList<>(numOps); + mOldMaxLifecycleStates = new int[numOps]; + mCurrentMaxLifecycleStates = new int[numOps]; + int pos = 0; + for (int opNum = 0; opNum < numOps; opNum++) { + final BackStackRecord.Op op = bse.mOps.get(opNum); + mOps[pos++] = op.mCmd; + mFragmentWhos.add(op.mFragment != null ? op.mFragment.mWho : null); + mOps[pos++] = op.mEnterAnim; + mOps[pos++] = op.mExitAnim; + mOps[pos++] = op.mPopEnterAnim; + mOps[pos++] = op.mPopExitAnim; + mOldMaxLifecycleStates[opNum] = op.mOldMaxState.ordinal(); + mCurrentMaxLifecycleStates[opNum] = op.mCurrentMaxState.ordinal(); + } + mTransition = bse.mTransition; + mTransitionStyle = bse.mTransitionStyle; + mName = bse.mName; + mIndex = bse.mIndex; + mBreadCrumbTitleRes = bse.mBreadCrumbTitleRes; + mBreadCrumbTitleText = bse.mBreadCrumbTitleText; + mBreadCrumbShortTitleRes = bse.mBreadCrumbShortTitleRes; + mBreadCrumbShortTitleText = bse.mBreadCrumbShortTitleText; + mSharedElementSourceNames = bse.mSharedElementSourceNames; + mSharedElementTargetNames = bse.mSharedElementTargetNames; + mReorderingAllowed = bse.mReorderingAllowed; + } + + public BackStackState(Parcel in) { + mOps = in.createIntArray(); + mFragmentWhos = in.createStringArrayList(); + mOldMaxLifecycleStates = in.createIntArray(); + mCurrentMaxLifecycleStates = in.createIntArray(); + mTransition = in.readInt(); + mTransitionStyle = in.readInt(); + mName = in.readString(); + mIndex = in.readInt(); + mBreadCrumbTitleRes = in.readInt(); + mBreadCrumbTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mBreadCrumbShortTitleRes = in.readInt(); + mBreadCrumbShortTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mSharedElementSourceNames = in.createStringArrayList(); + mSharedElementTargetNames = in.createStringArrayList(); + mReorderingAllowed = in.readInt() != 0; + } + + public BackStackRecord instantiate(FragmentManagerImpl fm) { + BackStackRecord bse = new BackStackRecord(fm); + int pos = 0; + int num = 0; + while (pos < mOps.length) { + BackStackRecord.Op op = new BackStackRecord.Op(); + op.mCmd = mOps[pos++]; + if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG, + "Instantiate " + bse + " op #" + num + " base fragment #" + mOps[pos]); + String fWho = mFragmentWhos.get(num); + if (fWho != null) { + Fragment f = fm.mActive.get(fWho); + op.mFragment = f; + } else { + op.mFragment = null; + } + op.mOldMaxState = Lifecycle.State.values()[mOldMaxLifecycleStates[num]]; + op.mCurrentMaxState = Lifecycle.State.values()[mCurrentMaxLifecycleStates[num]]; + op.mEnterAnim = mOps[pos++]; + op.mExitAnim = mOps[pos++]; + op.mPopEnterAnim = mOps[pos++]; + op.mPopExitAnim = mOps[pos++]; + bse.mEnterAnim = op.mEnterAnim; + bse.mExitAnim = op.mExitAnim; + bse.mPopEnterAnim = op.mPopEnterAnim; + bse.mPopExitAnim = op.mPopExitAnim; + bse.addOp(op); + num++; + } + bse.mTransition = mTransition; + bse.mTransitionStyle = mTransitionStyle; + bse.mName = mName; + bse.mIndex = mIndex; + bse.mAddToBackStack = true; + bse.mBreadCrumbTitleRes = mBreadCrumbTitleRes; + bse.mBreadCrumbTitleText = mBreadCrumbTitleText; + bse.mBreadCrumbShortTitleRes = mBreadCrumbShortTitleRes; + bse.mBreadCrumbShortTitleText = mBreadCrumbShortTitleText; + bse.mSharedElementSourceNames = mSharedElementSourceNames; + bse.mSharedElementTargetNames = mSharedElementTargetNames; + bse.mReorderingAllowed = mReorderingAllowed; + bse.bumpBackStackNesting(1); + return bse; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeIntArray(mOps); + dest.writeStringList(mFragmentWhos); + dest.writeIntArray(mOldMaxLifecycleStates); + dest.writeIntArray(mCurrentMaxLifecycleStates); + dest.writeInt(mTransition); + dest.writeInt(mTransitionStyle); + dest.writeString(mName); + dest.writeInt(mIndex); + dest.writeInt(mBreadCrumbTitleRes); + TextUtils.writeToParcel(mBreadCrumbTitleText, dest, 0); + dest.writeInt(mBreadCrumbShortTitleRes); + TextUtils.writeToParcel(mBreadCrumbShortTitleText, dest, 0); + dest.writeStringList(mSharedElementSourceNames); + dest.writeStringList(mSharedElementTargetNames); + dest.writeInt(mReorderingAllowed ? 1 : 0); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public BackStackState createFromParcel(Parcel in) { + return new BackStackState(in); + } + + @Override + public BackStackState[] newArray(int size) { + return new BackStackState[size]; + } + }; +} \ No newline at end of file diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/DialogFragment.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/DialogFragment.java new file mode 100644 index 000000000..74f516bdc --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/DialogFragment.java @@ -0,0 +1,548 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.StyleRes; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Static library support version of the framework's {@link android.app.DialogFragment}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class DialogFragment extends Fragment + implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { + + /** @hide */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}) + @Retention(RetentionPolicy.SOURCE) + private @interface DialogStyle {} + + /** + * Style for {@link #setStyle(int, int)}: a basic, + * normal dialog. + */ + public static final int STYLE_NORMAL = 0; + + /** + * Style for {@link #setStyle(int, int)}: don't include + * a title area. + */ + public static final int STYLE_NO_TITLE = 1; + + /** + * Style for {@link #setStyle(int, int)}: don't draw + * any frame at all; the view hierarchy returned by {@link #onCreateView} + * is entirely responsible for drawing the dialog. + */ + public static final int STYLE_NO_FRAME = 2; + + /** + * Style for {@link #setStyle(int, int)}: like + * {@link #STYLE_NO_FRAME}, but also disables all input to the dialog. + * The user can not touch it, and its window will not receive input focus. + */ + public static final int STYLE_NO_INPUT = 3; + + private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState"; + private static final String SAVED_STYLE = "android:style"; + private static final String SAVED_THEME = "android:theme"; + private static final String SAVED_CANCELABLE = "android:cancelable"; + private static final String SAVED_SHOWS_DIALOG = "android:showsDialog"; + private static final String SAVED_BACK_STACK_ID = "android:backStackId"; + + private Handler mHandler; + private Runnable mDismissRunnable = new Runnable() { + @Override + public void run() { + if (mDialog != null) { + onDismiss(mDialog); + } + } + }; + int mStyle = STYLE_NORMAL; + int mTheme = 0; + boolean mCancelable = true; + boolean mShowsDialog = true; + int mBackStackId = -1; + + @Nullable Dialog mDialog; + boolean mViewDestroyed; + boolean mDismissed; + boolean mShownByMe; + + public DialogFragment() { + } + + /** + * Call to customize the basic appearance and behavior of the + * fragment's dialog. This can be used for some common dialog behaviors, + * taking care of selecting flags, theme, and other options for you. The + * same effect can be achieve by manually setting Dialog and Window + * attributes yourself. Calling this after the fragment's Dialog is + * created will have no effect. + * + * @param style Selects a standard style: may be {@link #STYLE_NORMAL}, + * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or + * {@link #STYLE_NO_INPUT}. + * @param theme Optional custom theme. If 0, an appropriate theme (based + * on the style) will be selected for you. + */ + public void setStyle(@DialogStyle int style, @StyleRes int theme) { + mStyle = style; + if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) { + mTheme = android.R.style.Theme_Panel; + } + if (theme != 0) { + mTheme = theme; + } + } + + /** + * Display the dialog, adding the fragment to the given FragmentManager. This + * is a convenience for explicitly creating a transaction, adding the + * fragment to it with the given tag, and {@link FragmentTransaction#commit() committing} it. + * This does not add the transaction to the fragment back stack. When the fragment + * is dismissed, a new transaction will be executed to remove it from + * the activity. + * @param manager The FragmentManager this fragment will be added to. + * @param tag The tag for this fragment, as per + * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. + */ + public void show(@NonNull FragmentManager manager, @Nullable String tag) { + mDismissed = false; + mShownByMe = true; + FragmentTransaction ft = manager.beginTransaction(); + ft.add(this, tag); + ft.commit(); + } + + /** + * Display the dialog, adding the fragment using an existing transaction + * and then {@link FragmentTransaction#commit() committing} the transaction. + * @param transaction An existing transaction in which to add the fragment. + * @param tag The tag for this fragment, as per + * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. + * @return Returns the identifier of the committed transaction, as per + * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. + */ + public int show(@NonNull FragmentTransaction transaction, @Nullable String tag) { + mDismissed = false; + mShownByMe = true; + transaction.add(this, tag); + mViewDestroyed = false; + mBackStackId = transaction.commit(); + return mBackStackId; + } + + /** + * Display the dialog, immediately adding the fragment to the given FragmentManager. This + * is a convenience for explicitly creating a transaction, adding the + * fragment to it with the given tag, and calling {@link FragmentTransaction#commitNow()}. + * This does not add the transaction to the fragment back stack. When the fragment + * is dismissed, a new transaction will be executed to remove it from + * the activity. + * @param manager The FragmentManager this fragment will be added to. + * @param tag The tag for this fragment, as per + * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. + */ + public void showNow(@NonNull FragmentManager manager, @Nullable String tag) { + mDismissed = false; + mShownByMe = true; + FragmentTransaction ft = manager.beginTransaction(); + ft.add(this, tag); + ft.commitNow(); + } + + /** + * Dismiss the fragment and its dialog. If the fragment was added to the + * back stack, all back stack state up to and including this entry will + * be popped. Otherwise, a new transaction will be committed to remove + * the fragment. + */ + public void dismiss() { + dismissInternal(false, false); + } + + /** + * Version of {@link #dismiss()} that uses + * {@link FragmentTransaction#commitAllowingStateLoss() + * FragmentTransaction.commitAllowingStateLoss()}. See linked + * documentation for further details. + */ + public void dismissAllowingStateLoss() { + dismissInternal(true, false); + } + + void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss) { + if (mDismissed) { + return; + } + mDismissed = true; + mShownByMe = false; + if (mDialog != null) { + // Instead of waiting for a posted onDismiss(), null out + // the listener and call onDismiss() manually to ensure + // that the callback happens before onDestroy() + mDialog.setOnDismissListener(null); + mDialog.dismiss(); + if (!fromOnDismiss) { + // onDismiss() is always called on the main thread, so + // we mimic that behavior here. The difference here is that + // we don't post the message to ensure that the onDismiss() + // callback still happens before onDestroy() + if (Looper.myLooper() == mHandler.getLooper()) { + onDismiss(mDialog); + } else { + mHandler.post(mDismissRunnable); + } + } + } + mViewDestroyed = true; + if (mBackStackId >= 0) { + requireFragmentManager().popBackStack(mBackStackId, + FragmentManager.POP_BACK_STACK_INCLUSIVE); + mBackStackId = -1; + } else { + FragmentTransaction ft = requireFragmentManager().beginTransaction(); + ft.remove(this); + if (allowStateLoss) { + ft.commitAllowingStateLoss(); + } else { + ft.commit(); + } + } + } + + /** + * Return the {@link Dialog} this fragment is currently controlling. + * + * @see #requireDialog() + */ + @Nullable + public Dialog getDialog() { + return mDialog; + } + + /** + * Return the {@link Dialog} this fragment is currently controlling. + * + * @throws IllegalStateException if the Dialog has not yet been created (before + * {@link #onCreateDialog(Bundle)}) or has been destroyed (after {@link #onDestroyView()}. + * @see #getDialog() + */ + @NonNull + public final Dialog requireDialog() { + Dialog dialog = getDialog(); + if (dialog == null) { + throw new IllegalStateException("DialogFragment " + this + " does not have a Dialog."); + } + return dialog; + } + + @StyleRes + public int getTheme() { + return mTheme; + } + + /** + * Control whether the shown Dialog is cancelable. Use this instead of + * directly calling {@link Dialog#setCancelable(boolean) + * Dialog.setCancelable(boolean)}, because DialogFragment needs to change + * its behavior based on this. + * + * @param cancelable If true, the dialog is cancelable. The default + * is true. + */ + public void setCancelable(boolean cancelable) { + mCancelable = cancelable; + if (mDialog != null) mDialog.setCancelable(cancelable); + } + + /** + * Return the current value of {@link #setCancelable(boolean)}. + */ + public boolean isCancelable() { + return mCancelable; + } + + /** + * Controls whether this fragment should be shown in a dialog. If not + * set, no Dialog will be created in {@link #onActivityCreated(Bundle)}, + * and the fragment's view hierarchy will thus not be added to it. This + * allows you to instead use it as a normal fragment (embedded inside of + * its activity). + * + *

This is normally set for you based on whether the fragment is + * associated with a container view ID passed to + * {@link FragmentTransaction#add(int, Fragment) FragmentTransaction.add(int, Fragment)}. + * If the fragment was added with a container, setShowsDialog will be + * initialized to false; otherwise, it will be true. + * + * @param showsDialog If true, the fragment will be displayed in a Dialog. + * If false, no Dialog will be created and the fragment's view hierarchy + * left undisturbed. + */ + public void setShowsDialog(boolean showsDialog) { + mShowsDialog = showsDialog; + } + + /** + * Return the current value of {@link #setShowsDialog(boolean)}. + */ + public boolean getShowsDialog() { + return mShowsDialog; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + if (!mShownByMe) { + // If not explicitly shown through our API, take this as an + // indication that the dialog is no longer dismissed. + mDismissed = false; + } + } + + @Override + public void onDetach() { + super.onDetach(); + if (!mShownByMe && !mDismissed) { + // The fragment was not shown by a direct call here, it is not + // dismissed, and now it is being detached... well, okay, thou + // art now dismissed. Have fun. + mDismissed = true; + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // This assumes that onCreate() is being called on the main thread + mHandler = new Handler(); + + mShowsDialog = mContainerId == 0; + + if (savedInstanceState != null) { + mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL); + mTheme = savedInstanceState.getInt(SAVED_THEME, 0); + mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true); + mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); + mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1); + } + } + + @Override + @NonNull + public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) { + if (!mShowsDialog) { + return super.onGetLayoutInflater(savedInstanceState); + } + + mDialog = onCreateDialog(savedInstanceState); + + if (mDialog != null) { + setupDialog(mDialog, mStyle); + + return (LayoutInflater) mDialog.getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + return (LayoutInflater) mHost.getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + + /** @hide */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + public void setupDialog(@NonNull Dialog dialog, int style) { + switch (style) { + case STYLE_NO_INPUT: + dialog.getWindow().addFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + // fall through... + case STYLE_NO_FRAME: + case STYLE_NO_TITLE: + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + } + } + + /** + * Override to build your own custom Dialog container. This is typically + * used to show an AlertDialog instead of a generic Dialog; when doing so, + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} does not need + * to be implemented since the AlertDialog takes care of its own content. + * + *

This method will be called after {@link #onCreate(Bundle)} and + * before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. The + * default implementation simply instantiates and returns a {@link Dialog} + * class. + * + *

Note: DialogFragment own the {@link Dialog#setOnCancelListener + * Dialog.setOnCancelListener} and {@link Dialog#setOnDismissListener + * Dialog.setOnDismissListener} callbacks. You must not set them yourself. + * To find out about these events, override {@link #onCancel(DialogInterface)} + * and {@link #onDismiss(DialogInterface)}.

+ * + * @param savedInstanceState The last saved instance state of the Fragment, + * or null if this is a freshly created Fragment. + * + * @return Return a new Dialog instance to be displayed by the Fragment. + */ + @NonNull + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + return new Dialog(requireContext(), getTheme()); + } + + @Override + public void onCancel(@NonNull DialogInterface dialog) { + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + if (!mViewDestroyed) { + // Note: we need to use allowStateLoss, because the dialog + // dispatches this asynchronously so we can receive the call + // after the activity is paused. Worst case, when the user comes + // back to the activity they see the dialog again. + dismissInternal(true, true); + } + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (!mShowsDialog) { + return; + } + + View view = getView(); + if (view != null) { + if (view.getParent() != null) { + throw new IllegalStateException( + "DialogFragment can not be attached to a container view"); + } + mDialog.setContentView(view); + } + final Activity activity = getActivity(); + if (activity != null) { + mDialog.setOwnerActivity(activity); + } + mDialog.setCancelable(mCancelable); + mDialog.setOnCancelListener(this); + mDialog.setOnDismissListener(this); + if (savedInstanceState != null) { + Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG); + if (dialogState != null) { + mDialog.onRestoreInstanceState(dialogState); + } + } + } + + @Override + public void onStart() { + super.onStart(); + + if (mDialog != null) { + mViewDestroyed = false; + mDialog.show(); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + if (mDialog != null) { + Bundle dialogState = mDialog.onSaveInstanceState(); + if (dialogState != null) { + outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); + } + } + if (mStyle != STYLE_NORMAL) { + outState.putInt(SAVED_STYLE, mStyle); + } + if (mTheme != 0) { + outState.putInt(SAVED_THEME, mTheme); + } + if (!mCancelable) { + outState.putBoolean(SAVED_CANCELABLE, mCancelable); + } + if (!mShowsDialog) { + outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); + } + if (mBackStackId != -1) { + outState.putInt(SAVED_BACK_STACK_ID, mBackStackId); + } + } + + @Override + public void onStop() { + super.onStop(); + if (mDialog != null) { + mDialog.hide(); + } + } + + /** + * Remove dialog. + */ + @Override + public void onDestroyView() { + super.onDestroyView(); + if (mDialog != null) { + // Set removed here because this dismissal is just to hide + // the dialog -- we don't want this to cause the fragment to + // actually be removed. + mViewDestroyed = true; + // Instead of waiting for a posted onDismiss(), null out + // the listener and call onDismiss() manually to ensure + // that the callback happens before onDestroy() + mDialog.setOnDismissListener(null); + mDialog.dismiss(); + if (!mDismissed) { + // Don't send a second onDismiss() callback if we've already + // dismissed the dialog manually in dismissInternal() + onDismiss(mDialog); + } + mDialog = null; + } + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/Fragment.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/Fragment.java new file mode 100644 index 000000000..ed482bda8 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/Fragment.java @@ -0,0 +1,3041 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +import android.animation.Animator; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ComponentCallbacks; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnCreateContextMenuListener; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.widget.AdapterView; + +import androidx.annotation.CallSuper; +import androidx.annotation.ContentView; +import androidx.annotation.LayoutRes; +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.StringRes; +import androidx.core.app.SharedElementCallback; +import androidx.core.util.DebugUtils; +import androidx.core.view.LayoutInflaterCompat; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModelStore; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.loader.app.LoaderManager; +import androidx.savedstate.SavedStateRegistry; +import androidx.savedstate.SavedStateRegistryController; +import androidx.savedstate.SavedStateRegistryOwner; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.reflect.InvocationTargetException; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Static library support version of the framework's {@link android.app.Fragment}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework {@link android.app.Fragment} + * documentation for a class overview. + * + *

The main differences when using this support version instead of the framework version are: + *

    + *
  • Your activity must extend {@link FragmentActivity} + *
  • You must call {@link FragmentActivity#getSupportFragmentManager} to get the + * {@link FragmentManager} + *
+ * + */ +public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner, + ViewModelStoreOwner, SavedStateRegistryOwner { + + static final Object USE_DEFAULT_TRANSITION = new Object(); + + static final int INITIALIZING = 0; // Not yet created. + static final int CREATED = 1; // Created. + static final int ACTIVITY_CREATED = 2; // Fully created, not started. + static final int STARTED = 3; // Created and started, not resumed. + static final int RESUMED = 4; // Created started and resumed. + + int mState = INITIALIZING; + + // When instantiated from saved state, this is the saved state. + Bundle mSavedFragmentState; + SparseArray mSavedViewState; + // If the userVisibleHint is changed before the state is set, + // it is stored here + @Nullable Boolean mSavedUserVisibleHint; + + // Internal unique name for this fragment; + @NonNull + String mWho = UUID.randomUUID().toString(); + + // Construction arguments; + Bundle mArguments; + + // Target fragment. + Fragment mTarget; + + // For use when retaining a fragment: this is the who of the last mTarget. + String mTargetWho = null; + + // Target request code. + int mTargetRequestCode; + + // Boolean indicating whether this Fragment is the primary navigation fragment + private Boolean mIsPrimaryNavigationFragment = null; + + // True if the fragment is in the list of added fragments. + boolean mAdded; + + // If set this fragment is being removed from its activity. + boolean mRemoving; + + // Set to true if this fragment was instantiated from a layout file. + boolean mFromLayout; + + // Set to true when the view has actually been inflated in its layout. + boolean mInLayout; + + // True if this fragment has been restored from previously saved state. + boolean mRestored; + + // True if performCreateView has been called and a matching call to performDestroyView + // has not yet happened. + boolean mPerformedCreateView; + + // Number of active back stack entries this fragment is in. + int mBackStackNesting; + + // The fragment manager we are associated with. Set as soon as the + // fragment is used in a transaction; cleared after it has been removed + // from all transactions. + FragmentManagerImpl mFragmentManager; + + // Host this fragment is attached to. + FragmentHostCallback mHost; + + // Private fragment manager for child fragments inside of this one. + @NonNull + FragmentManagerImpl mChildFragmentManager = new FragmentManagerImpl(); + + // If this Fragment is contained in another Fragment, this is that container. + Fragment mParentFragment; + + // The optional identifier for this fragment -- either the container ID if it + // was dynamically added to the view hierarchy, or the ID supplied in + // layout. + int mFragmentId; + + // When a fragment is being dynamically added to the view hierarchy, this + // is the identifier of the parent container it is being added to. + int mContainerId; + + // The optional named tag for this fragment -- usually used to find + // fragments that are not part of the layout. + String mTag; + + // Set to true when the app has requested that this fragment be hidden + // from the user. + boolean mHidden; + + // Set to true when the app has requested that this fragment be deactivated. + boolean mDetached; + + // If set this fragment would like its instance retained across + // configuration changes. + boolean mRetainInstance; + + // If set this fragment changed its mRetainInstance while it was detached + boolean mRetainInstanceChangedWhileDetached; + + // If set this fragment has menu items to contribute. + boolean mHasMenu; + + // Set to true to allow the fragment's menu to be shown. + boolean mMenuVisible = true; + + // Used to verify that subclasses call through to super class. + private boolean mCalled; + + // The parent container of the fragment after dynamically added to UI. + ViewGroup mContainer; + + // The View generated for this fragment. + View mView; + + // The real inner view that will save/restore state. + View mInnerView; + + // Whether this fragment should defer starting until after other fragments + // have been started and their loaders are finished. + boolean mDeferStart; + + // Hint provided by the app that this fragment is currently visible to the user. + boolean mUserVisibleHint = true; + + // The animation and transition information for the fragment. This will be null + // unless the elements are explicitly accessed and should remain null for Fragments + // without Views. + AnimationInfo mAnimationInfo; + + // Runnable that is used to indicate if the Fragment has a postponed transition that is on a + // timeout. + Runnable mPostponedDurationRunnable = new Runnable() { + @Override + public void run() { + startPostponedEnterTransition(); + } + }; + + // True if the View was added, and its animation has yet to be run. This could + // also indicate that the fragment view hasn't been made visible, even if there is no + // animation for this fragment. + boolean mIsNewlyAdded; + + // True if mHidden has been changed and the animation should be scheduled. + boolean mHiddenChanged; + + // The alpha of the view when the view was added and then postponed. If the value is less + // than zero, this means that the view's add was canceled and should not participate in + // removal animations. + float mPostponedAlpha; + + // The cached value from onGetLayoutInflater(Bundle) that will be returned from + // getLayoutInflater() + LayoutInflater mLayoutInflater; + + // Keep track of whether or not this Fragment has run performCreate(). Retained instance + // fragments can have mRetaining set to true without going through creation, so we must + // track it separately. + boolean mIsCreated; + + // Max Lifecycle state this Fragment can achieve. + Lifecycle.State mMaxState = Lifecycle.State.RESUMED; + + LifecycleRegistry mLifecycleRegistry; + + // This is initialized in performCreateView and unavailable outside of the + // onCreateView/onDestroyView lifecycle + @Nullable FragmentViewLifecycleOwner mViewLifecycleOwner; + MutableLiveData mViewLifecycleOwnerLiveData = new MutableLiveData<>(); + + SavedStateRegistryController mSavedStateRegistryController; + + @LayoutRes + private int mContentLayoutId; + + /** + * {@inheritDoc} + *

+ * Overriding this method is no longer supported and this method will be made + * final in a future version of Fragment. + */ + @Override + @NonNull + public Lifecycle getLifecycle() { + return mLifecycleRegistry; + } + + /** + * Get a {@link LifecycleOwner} that represents the {@link #getView() Fragment's View} + * lifecycle. In most cases, this mirrors the lifecycle of the Fragment itself, but in cases + * of {@link FragmentTransaction#detach(Fragment) detached} Fragments, the lifecycle of the + * Fragment can be considerably longer than the lifecycle of the View itself. + *

+ * Namely, the lifecycle of the Fragment's View is: + *

    + *
  1. {@link Lifecycle.Event#ON_CREATE created} after {@link #onViewStateRestored(Bundle)}
  2. + *
  3. {@link Lifecycle.Event#ON_START started} after {@link #onStart()}
  4. + *
  5. {@link Lifecycle.Event#ON_RESUME resumed} after {@link #onResume()}
  6. + *
  7. {@link Lifecycle.Event#ON_PAUSE paused} before {@link #onPause()}
  8. + *
  9. {@link Lifecycle.Event#ON_STOP stopped} before {@link #onStop()}
  10. + *
  11. {@link Lifecycle.Event#ON_DESTROY destroyed} before {@link #onDestroyView()}
  12. + *
+ * + * The first method where it is safe to access the view lifecycle is + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} under the condition that you must + * return a non-null view (an IllegalStateException will be thrown if you access the view + * lifecycle but don't return a non-null view). + *

The view lifecycle remains valid through the call to {@link #onDestroyView()}, after which + * {@link #getView()} will return null, the view lifecycle will be destroyed, and this method + * will throw an IllegalStateException. Consider using + * {@link #getViewLifecycleOwnerLiveData()} or {@link FragmentTransaction#runOnCommit(Runnable)} + * to receive a callback for when the Fragment's view lifecycle is available. + *

+ * This should only be called on the main thread. + *

+ * Overriding this method is no longer supported and this method will be made + * final in a future version of Fragment. + * + * @return A {@link LifecycleOwner} that represents the {@link #getView() Fragment's View} + * lifecycle. + * @throws IllegalStateException if the {@link #getView() Fragment's View is null}. + */ + @MainThread + @NonNull + public LifecycleOwner getViewLifecycleOwner() { + if (mViewLifecycleOwner == null) { + throw new IllegalStateException("Can't access the Fragment View's LifecycleOwner when " + + "getView() is null i.e., before onCreateView() or after onDestroyView()"); + } + return mViewLifecycleOwner; + } + + /** + * Retrieve a {@link LiveData} which allows you to observe the + * {@link #getViewLifecycleOwner() lifecycle of the Fragment's View}. + *

+ * This will be set to the new {@link LifecycleOwner} after {@link #onCreateView} returns a + * non-null View and will set to null after {@link #onDestroyView()}. + *

+ * Overriding this method is no longer supported and this method will be made + * final in a future version of Fragment. + * + * @return A LiveData that changes in sync with {@link #getViewLifecycleOwner()}. + */ + @NonNull + public LiveData getViewLifecycleOwnerLiveData() { + return mViewLifecycleOwnerLiveData; + } + + /** + * Returns the {@link ViewModelStore} associated with this Fragment + *

+ * Overriding this method is no longer supported and this method will be made + * final in a future version of Fragment. + * + * @return a {@code ViewModelStore} + * @throws IllegalStateException if called before the Fragment is attached i.e., before + * onAttach(). + */ + @NonNull + @Override + public ViewModelStore getViewModelStore() { + if (mFragmentManager == null) { + throw new IllegalStateException("Can't access ViewModels from detached fragment"); + } + return mFragmentManager.getViewModelStore(this); + } + + @NonNull + @Override + public final SavedStateRegistry getSavedStateRegistry() { + return mSavedStateRegistryController.getSavedStateRegistry(); + } + + /** + * State information that has been retrieved from a fragment instance + * through {@link FragmentManager#saveFragmentInstanceState(Fragment) + * FragmentManager.saveFragmentInstanceState}. + */ + @SuppressLint("BanParcelableUsage") + public static class SavedState implements Parcelable { + final Bundle mState; + + SavedState(Bundle state) { + mState = state; + } + + SavedState(@NonNull Parcel in, @Nullable ClassLoader loader) { + mState = in.readBundle(); + if (loader != null && mState != null) { + mState.setClassLoader(loader); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBundle(mState); + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.ClassLoaderCreator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * Thrown by {@link FragmentFactory#instantiate(ClassLoader, String)} when + * there is an instantiation failure. + */ + @SuppressWarnings("JavaLangClash") + public static class InstantiationException extends RuntimeException { + public InstantiationException(@NonNull String msg, @Nullable Exception cause) { + super(msg, cause); + } + } + + /** + * Constructor used by the default {@link FragmentFactory}. You must + * {@link FragmentManager#setFragmentFactory(FragmentFactory) set a custom FragmentFactory} + * if you want to use a non-default constructor to ensure that your constructor + * is called when the fragment is re-instantiated. + * + *

It is strongly recommended to supply arguments with {@link #setArguments} + * and later retrieved by the Fragment with {@link #getArguments}. These arguments + * are automatically saved and restored alongside the Fragment. + * + *

Applications should generally not implement a constructor. Prefer + * {@link #onAttach(Context)} instead. It is the first place application code can run where + * the fragment is ready to be used - the point where the fragment is actually associated with + * its context. Some applications may also want to implement {@link #onInflate} to retrieve + * attributes from a layout resource, although note this happens when the fragment is attached. + */ + public Fragment() { + initLifecycle(); + } + + /** + * Alternate constructor that can be used to provide a default layout + * that will be inflated by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * + * @see #Fragment() + * @see #onCreateView(LayoutInflater, ViewGroup, Bundle) + */ + @ContentView + public Fragment(@LayoutRes int contentLayoutId) { + this(); + mContentLayoutId = contentLayoutId; + } + + private void initLifecycle() { + mLifecycleRegistry = new LifecycleRegistry(this); + mSavedStateRegistryController = SavedStateRegistryController.create(this); + if (Build.VERSION.SDK_INT >= 19) { + mLifecycleRegistry.addObserver(new LifecycleEventObserver() { + @Override + public void onStateChanged(@NonNull LifecycleOwner source, + @NonNull Lifecycle.Event event) { + if (event == Lifecycle.Event.ON_STOP) { + if (mView != null) { + mView.cancelPendingInputEvents(); + } + } + } + }); + } + } + + /** + * Like {@link #instantiate(Context, String, Bundle)} but with a null + * argument Bundle. + * @deprecated Use {@link FragmentManager#getFragmentFactory()} and + * {@link FragmentFactory#instantiate(ClassLoader, String)} + */ + @SuppressWarnings("deprecation") + @Deprecated + @NonNull + public static Fragment instantiate(@NonNull Context context, @NonNull String fname) { + return instantiate(context, fname, null); + } + + /** + * Create a new instance of a Fragment with the given class name. This is + * the same as calling its empty constructor, setting the {@link ClassLoader} on the + * supplied arguments, then calling {@link #setArguments(Bundle)}. + * + * @param context The calling context being used to instantiate the fragment. + * This is currently just used to get its ClassLoader. + * @param fname The class name of the fragment to instantiate. + * @param args Bundle of arguments to supply to the fragment, which it + * can retrieve with {@link #getArguments()}. May be null. + * @return Returns a new fragment instance. + * @throws InstantiationException If there is a failure in instantiating + * the given fragment class. This is a runtime exception; it is not + * normally expected to happen. + * @deprecated Use {@link FragmentManager#getFragmentFactory()} and + * {@link FragmentFactory#instantiate(ClassLoader, String)}, manually calling + * {@link #setArguments(Bundle)} on the returned Fragment. + */ + @Deprecated + @NonNull + public static Fragment instantiate(@NonNull Context context, @NonNull String fname, + @Nullable Bundle args) { + try { + Class clazz = FragmentFactory.loadFragmentClass( + context.getClassLoader(), fname); + Fragment f = clazz.getConstructor().newInstance(); + if (args != null) { + args.setClassLoader(f.getClass().getClassLoader()); + f.setArguments(args); + } + return f; + } catch (java.lang.InstantiationException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (IllegalAccessException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (NoSuchMethodException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": could not find Fragment constructor", e); + } catch (InvocationTargetException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": calling Fragment constructor caused an exception", e); + } + } + + final void restoreViewState(Bundle savedInstanceState) { + if (mSavedViewState != null) { + mInnerView.restoreHierarchyState(mSavedViewState); + mSavedViewState = null; + } + mCalled = false; + onViewStateRestored(savedInstanceState); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onViewStateRestored()"); + } + if (mView != null) { + mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + } + } + + final boolean isInBackStack() { + return mBackStackNesting > 0; + } + + /** + * Subclasses can not override equals(). + */ + @Override public final boolean equals(@Nullable Object o) { + return super.equals(o); + } + + /** + * Subclasses can not override hashCode(). + */ + @Override public final int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + DebugUtils.buildShortClassTag(this, sb); + sb.append(" ("); + sb.append(mWho); + sb.append(")"); + if (mFragmentId != 0) { + sb.append(" id=0x"); + sb.append(Integer.toHexString(mFragmentId)); + } + if (mTag != null) { + sb.append(" "); + sb.append(mTag); + } + sb.append('}'); + return sb.toString(); + } + + /** + * Return the identifier this fragment is known by. This is either + * the android:id value supplied in a layout or the container view ID + * supplied when adding the fragment. + */ + final public int getId() { + return mFragmentId; + } + + /** + * Get the tag name of the fragment, if specified. + */ + @Nullable + final public String getTag() { + return mTag; + } + + /** + * Supply the construction arguments for this fragment. + * The arguments supplied here will be retained across fragment destroy and + * creation. + *

This method cannot be called if the fragment is added to a FragmentManager and + * if {@link #isStateSaved()} would return true.

+ */ + public void setArguments(@Nullable Bundle args) { + if (mFragmentManager != null && isStateSaved()) { + throw new IllegalStateException("Fragment already added and state has been saved"); + } + mArguments = args; + } + + /** + * Return the arguments supplied when the fragment was instantiated, + * if any. + */ + @Nullable + final public Bundle getArguments() { + return mArguments; + } + + /** + * Return the arguments supplied when the fragment was instantiated. + * + * @throws IllegalStateException if no arguments were supplied to the Fragment. + * @see #getArguments() + */ + @NonNull + public final Bundle requireArguments() { + Bundle arguments = getArguments(); + if (arguments == null) { + throw new IllegalStateException("Fragment " + this + " does not have any arguments."); + } + return arguments; + } + + /** + * Returns true if this fragment is added and its state has already been saved + * by its host. Any operations that would change saved state should not be performed + * if this method returns true, and some operations such as {@link #setArguments(Bundle)} + * will fail. + * + * @return true if this fragment's state has already been saved by its host + */ + public final boolean isStateSaved() { + if (mFragmentManager == null) { + return false; + } + return mFragmentManager.isStateSaved(); + } + + /** + * Set the initial saved state that this Fragment should restore itself + * from when first being constructed, as returned by + * {@link FragmentManager#saveFragmentInstanceState(Fragment) + * FragmentManager.saveFragmentInstanceState}. + * + * @param state The state the fragment should be restored from. + */ + public void setInitialSavedState(@Nullable SavedState state) { + if (mFragmentManager != null) { + throw new IllegalStateException("Fragment already added"); + } + mSavedFragmentState = state != null && state.mState != null + ? state.mState : null; + } + + /** + * Optional target for this fragment. This may be used, for example, + * if this fragment is being started by another, and when done wants to + * give a result back to the first. The target set here is retained + * across instances via {@link FragmentManager#putFragment + * FragmentManager.putFragment()}. + * + * @param fragment The fragment that is the target of this one. + * @param requestCode Optional request code, for convenience if you + * are going to call back with {@link #onActivityResult(int, int, Intent)}. + */ + @SuppressWarnings("ReferenceEquality") + public void setTargetFragment(@Nullable Fragment fragment, int requestCode) { + // Don't allow a caller to set a target fragment in another FragmentManager, + // but there's a snag: people do set target fragments before fragments get added. + // We'll have the FragmentManager check that for validity when we move + // the fragments to a valid state. + final FragmentManager mine = getFragmentManager(); + final FragmentManager theirs = fragment != null ? fragment.getFragmentManager() : null; + if (mine != null && theirs != null && mine != theirs) { + throw new IllegalArgumentException("Fragment " + fragment + + " must share the same FragmentManager to be set as a target fragment"); + } + + // Don't let someone create a cycle. + for (Fragment check = fragment; check != null; check = check.getTargetFragment()) { + if (check == this) { + throw new IllegalArgumentException("Setting " + fragment + " as the target of " + + this + " would create a target cycle"); + } + } + if (fragment == null) { + mTargetWho = null; + mTarget = null; + } else if (mFragmentManager != null && fragment.mFragmentManager != null) { + // Just save the reference to the Fragment + mTargetWho = fragment.mWho; + mTarget = null; + } else { + // Save the Fragment itself, waiting until we're attached + mTargetWho = null; + mTarget = fragment; + } + mTargetRequestCode = requestCode; + } + + /** + * Return the target fragment set by {@link #setTargetFragment}. + */ + @Nullable + final public Fragment getTargetFragment() { + if (mTarget != null) { + // Ensure that any Fragment set with setTargetFragment is immediately + // available here + return mTarget; + } else if (mFragmentManager != null && mTargetWho != null) { + // Look up the target Fragment from the FragmentManager + return mFragmentManager.mActive.get(mTargetWho); + } + return null; + } + + /** + * Return the target request code set by {@link #setTargetFragment}. + */ + final public int getTargetRequestCode() { + return mTargetRequestCode; + } + + /** + * Return the {@link Context} this fragment is currently associated with. + * + * @see #requireContext() + */ + @Nullable + public Context getContext() { + return mHost == null ? null : mHost.getContext(); + } + + /** + * Return the {@link Context} this fragment is currently associated with. + * + * @throws IllegalStateException if not currently associated with a context. + * @see #getContext() + */ + @NonNull + public final Context requireContext() { + Context context = getContext(); + if (context == null) { + throw new IllegalStateException("Fragment " + this + " not attached to a context."); + } + return context; + } + + /** + * Return the {@link FragmentActivity} this fragment is currently associated with. + * May return {@code null} if the fragment is associated with a {@link Context} + * instead. + * + * @see #requireActivity() + */ + @Nullable + final public FragmentActivity getActivity() { + return mHost == null ? null : (FragmentActivity) mHost.getActivity(); + } + + /** + * Return the {@link FragmentActivity} this fragment is currently associated with. + * + * @throws IllegalStateException if not currently associated with an activity or if associated + * only with a context. + * @see #getActivity() + */ + @NonNull + public final FragmentActivity requireActivity() { + FragmentActivity activity = getActivity(); + if (activity == null) { + throw new IllegalStateException("Fragment " + this + " not attached to an activity."); + } + return activity; + } + + /** + * Return the host object of this fragment. May return {@code null} if the fragment + * isn't currently being hosted. + * + * @see #requireHost() + */ + @Nullable + final public Object getHost() { + return mHost == null ? null : mHost.onGetHost(); + } + + /** + * Return the host object of this fragment. + * + * @throws IllegalStateException if not currently associated with a host. + * @see #getHost() + */ + @NonNull + public final Object requireHost() { + Object host = getHost(); + if (host == null) { + throw new IllegalStateException("Fragment " + this + " not attached to a host."); + } + return host; + } + + /** + * Return requireActivity().getResources(). + */ + @NonNull + final public Resources getResources() { + return requireContext().getResources(); + } + + /** + * Return a localized, styled CharSequence from the application's package's + * default string table. + * + * @param resId Resource id for the CharSequence text + */ + @NonNull + public final CharSequence getText(@StringRes int resId) { + return getResources().getText(resId); + } + + /** + * Return a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + */ + @NonNull + public final String getString(@StringRes int resId) { + return getResources().getString(resId); + } + + /** + * Return a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * {@link java.util.Formatter} and {@link java.lang.String#format}. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for substitution. + */ + @NonNull + public final String getString(@StringRes int resId, @Nullable Object... formatArgs) { + return getResources().getString(resId, formatArgs); + } + + /** + * Return the FragmentManager for interacting with fragments associated + * with this fragment's activity. Note that this will be non-null slightly + * before {@link #getActivity()}, during the time from when the fragment is + * placed in a {@link FragmentTransaction} until it is committed and + * attached to its activity. + * + *

If this Fragment is a child of another Fragment, the FragmentManager + * returned here will be the parent's {@link #getChildFragmentManager()}. + * + * @see #requireFragmentManager() + */ + @Nullable + final public FragmentManager getFragmentManager() { + return mFragmentManager; + } + + /** + * Return the FragmentManager for interacting with fragments associated + * with this fragment's activity. Note that this will available slightly + * before {@link #getActivity()}, during the time from when the fragment is + * placed in a {@link FragmentTransaction} until it is committed and + * attached to its activity. + * + *

If this Fragment is a child of another Fragment, the FragmentManager + * returned here will be the parent's {@link #getChildFragmentManager()}. + * + * @throws IllegalStateException if not associated with a transaction or host. + * @see #getFragmentManager() + */ + @NonNull + public final FragmentManager requireFragmentManager() { + FragmentManager fragmentManager = getFragmentManager(); + if (fragmentManager == null) { + throw new IllegalStateException( + "Fragment " + this + " not associated with a fragment manager."); + } + return fragmentManager; + } + + /** + * Return a private FragmentManager for placing and managing Fragments + * inside of this Fragment. + */ + @NonNull + final public FragmentManager getChildFragmentManager() { + if (mHost == null) { + throw new IllegalStateException("Fragment " + this + " has not been attached yet."); + } + return mChildFragmentManager; + } + + /** + * Returns the parent Fragment containing this Fragment. If this Fragment + * is attached directly to an Activity, returns null. + */ + @Nullable + final public Fragment getParentFragment() { + return mParentFragment; + } + + /** + * Returns the parent Fragment containing this Fragment. + * + * @throws IllegalStateException if this Fragment is attached directly to an Activity or + * other Fragment host. + * @see #getParentFragment() + */ + @NonNull + public final Fragment requireParentFragment() { + Fragment parentFragment = getParentFragment(); + if (parentFragment == null) { + Context context = getContext(); + if (context == null) { + throw new IllegalStateException("Fragment " + this + " is not attached to" + + " any Fragment or host"); + } else { + throw new IllegalStateException("Fragment " + this + " is not a child Fragment, it" + + " is directly attached to " + getContext()); + } + } + return parentFragment; + } + + /** + * Return true if the fragment is currently added to its activity. + */ + final public boolean isAdded() { + return mHost != null && mAdded; + } + + /** + * Return true if the fragment has been explicitly detached from the UI. + * That is, {@link FragmentTransaction#detach(Fragment) + * FragmentTransaction.detach(Fragment)} has been used on it. + */ + final public boolean isDetached() { + return mDetached; + } + + /** + * Return true if this fragment is currently being removed from its + * activity. This is not whether its activity is finishing, but + * rather whether it is in the process of being removed from its activity. + */ + final public boolean isRemoving() { + return mRemoving; + } + + /** + * Return true if the layout is included as part of an activity view + * hierarchy via the <fragment> tag. This will always be true when + * fragments are created through the <fragment> tag, except + * in the case where an old fragment is restored from a previous state and + * it does not appear in the layout of the current state. + */ + final public boolean isInLayout() { + return mInLayout; + } + + /** + * Return true if the fragment is in the resumed state. This is true + * for the duration of {@link #onResume()} and {@link #onPause()} as well. + */ + final public boolean isResumed() { + return mState >= RESUMED; + } + + /** + * Return true if the fragment is currently visible to the user. This means + * it: (1) has been added, (2) has its view attached to the window, and + * (3) is not hidden. + */ + final public boolean isVisible() { + return isAdded() && !isHidden() && mView != null + && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE; + } + + /** + * Return true if the fragment has been hidden. By default fragments + * are shown. You can find out about changes to this state with + * {@link #onHiddenChanged}. Note that the hidden state is orthogonal + * to other states -- that is, to be visible to the user, a fragment + * must be both started and not hidden. + */ + final public boolean isHidden() { + return mHidden; + } + + /** @hide */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + final public boolean hasOptionsMenu() { + return mHasMenu; + } + + /** @hide */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + final public boolean isMenuVisible() { + return mMenuVisible; + } + + /** + * Called when the hidden state (as returned by {@link #isHidden()} of + * the fragment has changed. Fragments start out not hidden; this will + * be called whenever the fragment changes state from that. + * @param hidden True if the fragment is now hidden, false otherwise. + */ + public void onHiddenChanged(boolean hidden) { + } + + /** + * Control whether a fragment instance is retained across Activity + * re-creation (such as from a configuration change). If set, the fragment + * lifecycle will be slightly different when an activity is recreated: + *

    + *
  • {@link #onDestroy()} will not be called (but {@link #onDetach()} still + * will be, because the fragment is being detached from its current activity). + *
  • {@link #onCreate(Bundle)} will not be called since the fragment + * is not being re-created. + *
  • {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} will + * still be called. + *
+ */ + public void setRetainInstance(boolean retain) { + mRetainInstance = retain; + if (mFragmentManager != null) { + if (retain) { + mFragmentManager.addRetainedFragment(this); + } else { + mFragmentManager.removeRetainedFragment(this); + } + } else { + mRetainInstanceChangedWhileDetached = true; + } + } + + final public boolean getRetainInstance() { + return mRetainInstance; + } + + /** + * Report that this fragment would like to participate in populating + * the options menu by receiving a call to {@link #onCreateOptionsMenu} + * and related methods. + * + * @param hasMenu If true, the fragment has menu items to contribute. + */ + public void setHasOptionsMenu(boolean hasMenu) { + if (mHasMenu != hasMenu) { + mHasMenu = hasMenu; + if (isAdded() && !isHidden()) { + mHost.onSupportInvalidateOptionsMenu(); + } + } + } + + /** + * Set a hint for whether this fragment's menu should be visible. This + * is useful if you know that a fragment has been placed in your view + * hierarchy so that the user can not currently seen it, so any menu items + * it has should also not be shown. + * + * @param menuVisible The default is true, meaning the fragment's menu will + * be shown as usual. If false, the user will not see the menu. + */ + public void setMenuVisibility(boolean menuVisible) { + if (mMenuVisible != menuVisible) { + mMenuVisible = menuVisible; + if (mHasMenu && isAdded() && !isHidden()) { + mHost.onSupportInvalidateOptionsMenu(); + } + } + } + + /** + * Set a hint to the system about whether this fragment's UI is currently visible + * to the user. This hint defaults to true and is persistent across fragment instance + * state save and restore. + * + *

An app may set this to false to indicate that the fragment's UI is + * scrolled out of visibility or is otherwise not directly visible to the user. + * This may be used by the system to prioritize operations such as fragment lifecycle updates + * or loader ordering behavior.

+ * + *

Note: This method may be called outside of the fragment lifecycle. + * and thus has no ordering guarantees with regard to fragment lifecycle method calls.

+ * + * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default), + * false if it is not. + * + * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)} + * instead. + */ + @Deprecated + public void setUserVisibleHint(boolean isVisibleToUser) { + if (!mUserVisibleHint && isVisibleToUser && mState < STARTED + && mFragmentManager != null && isAdded() && mIsCreated) { + mFragmentManager.performPendingDeferredStart(this); + } + mUserVisibleHint = isVisibleToUser; + mDeferStart = mState < STARTED && !isVisibleToUser; + if (mSavedFragmentState != null) { + // Ensure that if the user visible hint is set before the Fragment has + // restored its state that we don't lose the new value + mSavedUserVisibleHint = isVisibleToUser; + } + } + + /** + * @return The current value of the user-visible hint on this fragment. + * @see #setUserVisibleHint(boolean) + * + * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)} + * instead. + */ + @Deprecated + public boolean getUserVisibleHint() { + return mUserVisibleHint; + } + + /** + * Return the LoaderManager for this fragment. + * + * @deprecated Use + * {@link LoaderManager#getInstance(LifecycleOwner) LoaderManager.getInstance(this)}. + */ + @Deprecated + @NonNull + public LoaderManager getLoaderManager() { + return LoaderManager.getInstance(this); + } + + /** + * Call {@link Activity#startActivity(Intent)} from the fragment's + * containing Activity. + */ + public void startActivity(@SuppressLint("UnknownNullness") Intent intent) { + startActivity(intent, null); + } + + /** + * Call {@link Activity#startActivity(Intent, Bundle)} from the fragment's + * containing Activity. + */ + public void startActivity(@SuppressLint("UnknownNullness") Intent intent, + @Nullable Bundle options) { + if (mHost == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mHost.onStartActivityFromFragment(this /*fragment*/, intent, -1, options); + } + + /** + * Call {@link Activity#startActivityForResult(Intent, int)} from the fragment's + * containing Activity. + */ + public void startActivityForResult(@SuppressLint("UnknownNullness") Intent intent, + int requestCode) { + startActivityForResult(intent, requestCode, null); + } + + /** + * Call {@link Activity#startActivityForResult(Intent, int, Bundle)} from the fragment's + * containing Activity. + */ + public void startActivityForResult(@SuppressLint("UnknownNullness") Intent intent, + int requestCode, @Nullable Bundle options) { + if (mHost == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mHost.onStartActivityFromFragment(this /*fragment*/, intent, requestCode, options); + } + + /** + * Call {@link Activity#startIntentSenderForResult(IntentSender, int, Intent, int, int, int, + * Bundle)} from the fragment's containing Activity. + */ + public void startIntentSenderForResult(@SuppressLint("UnknownNullness") IntentSender intent, + int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, + int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException { + if (mHost == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mHost.onStartIntentSenderFromFragment(this, intent, requestCode, fillInIntent, flagsMask, + flagsValues, extraFlags, options); + } + + /** + * Receive the result from a previous call to + * {@link #startActivityForResult(Intent, int)}. This follows the + * related Activity API as described there in + * {@link Activity#onActivityResult(int, int, Intent)}. + * + * @param requestCode The integer request code originally supplied to + * startActivityForResult(), allowing you to identify who this + * result came from. + * @param resultCode The integer result code returned by the child activity + * through its setResult(). + * @param data An Intent, which can return result data to the caller + * (various data can be attached to Intent "extras"). + */ + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + } + + /** + * Requests permissions to be granted to this application. These permissions + * must be requested in your manifest, they should not be granted to your app, + * and they should have protection level {@link android.content.pm.PermissionInfo + * #PROTECTION_DANGEROUS dangerous}, regardless whether they are declared by + * the platform or a third-party app. + *

+ * Normal permissions {@link android.content.pm.PermissionInfo#PROTECTION_NORMAL} + * are granted at install time if requested in the manifest. Signature permissions + * {@link android.content.pm.PermissionInfo#PROTECTION_SIGNATURE} are granted at + * install time if requested in the manifest and the signature of your app matches + * the signature of the app declaring the permissions. + *

+ *

+ * If your app does not have the requested permissions the user will be presented + * with UI for accepting them. After the user has accepted or rejected the + * requested permissions you will receive a callback on {@link + * #onRequestPermissionsResult(int, String[], int[])} reporting whether the + * permissions were granted or not. + *

+ *

+ * Note that requesting a permission does not guarantee it will be granted and + * your app should be able to run without having this permission. + *

+ *

+ * This method may start an activity allowing the user to choose which permissions + * to grant and which to reject. Hence, you should be prepared that your activity + * may be paused and resumed. Further, granting some permissions may require + * a restart of you application. In such a case, the system will recreate the + * activity stack before delivering the result to {@link + * #onRequestPermissionsResult(int, String[], int[])}. + *

+ *

+ * When checking whether you have a permission you should use {@link + * android.content.Context#checkSelfPermission(String)}. + *

+ *

+ * Calling this API for permissions already granted to your app would show UI + * to the user to decided whether the app can still hold these permissions. This + * can be useful if the way your app uses the data guarded by the permissions + * changes significantly. + *

+ *

+ * A sample permissions request looks like this: + *

+ *

+ * private void showContacts() { + * if (getActivity().checkSelfPermission(Manifest.permission.READ_CONTACTS) + * != PackageManager.PERMISSION_GRANTED) { + * requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, + * PERMISSIONS_REQUEST_READ_CONTACTS); + * } else { + * doShowContacts(); + * } + * } + * + * {@literal @}Override + * public void onRequestPermissionsResult(int requestCode, String[] permissions, + * int[] grantResults) { + * if (requestCode == PERMISSIONS_REQUEST_READ_CONTACTS + * && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + * doShowContacts(); + * } + * } + *

+ * + * @param permissions The requested permissions. + * @param requestCode Application specific request code to match with a result + * reported to {@link #onRequestPermissionsResult(int, String[], int[])}. + * + * @see #onRequestPermissionsResult(int, String[], int[]) + * @see android.content.Context#checkSelfPermission(String) + */ + public final void requestPermissions(@NonNull String[] permissions, int requestCode) { + if (mHost == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mHost.onRequestPermissionsFromFragment(this, permissions, requestCode); + } + + /** + * Callback for the result from requesting permissions. This method + * is invoked for every call on {@link #requestPermissions(String[], int)}. + *

+ * Note: It is possible that the permissions request interaction + * with the user is interrupted. In this case you will receive empty permissions + * and results arrays which should be treated as a cancellation. + *

+ * + * @param requestCode The request code passed in {@link #requestPermissions(String[], int)}. + * @param permissions The requested permissions. Never null. + * @param grantResults The grant results for the corresponding permissions + * which is either {@link android.content.pm.PackageManager#PERMISSION_GRANTED} + * or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null. + * + * @see #requestPermissions(String[], int) + */ + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + /* callback - do nothing */ + } + + /** + * Gets whether you should show UI with rationale for requesting a permission. + * You should do this only if you do not have the permission and the context in + * which the permission is requested does not clearly communicate to the user + * what would be the benefit from granting this permission. + *

+ * For example, if you write a camera app, requesting the camera permission + * would be expected by the user and no rationale for why it is requested is + * needed. If however, the app needs location for tagging photos then a non-tech + * savvy user may wonder how location is related to taking photos. In this case + * you may choose to show UI with rationale of requesting this permission. + *

+ * + * @param permission A permission your app wants to request. + * @return Whether you can show permission rationale UI. + * + * @see Context#checkSelfPermission(String) + * @see #requestPermissions(String[], int) + * @see #onRequestPermissionsResult(int, String[], int[]) + */ + public boolean shouldShowRequestPermissionRationale(@NonNull String permission) { + if (mHost != null) { + return mHost.onShouldShowRequestPermissionRationale(permission); + } + return false; + } + + /** + * Returns the LayoutInflater used to inflate Views of this Fragment. The default + * implementation will throw an exception if the Fragment is not attached. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + * @return The LayoutInflater used to inflate Views of this Fragment. + */ + @NonNull + public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) { + // TODO: move the implementation in getLayoutInflater to here + return getLayoutInflater(savedInstanceState); + } + + /** + * Returns the cached LayoutInflater used to inflate Views of this Fragment. If + * {@link #onGetLayoutInflater(Bundle)} has not been called {@link #onGetLayoutInflater(Bundle)} + * will be called with a {@code null} argument and that value will be cached. + *

+ * The cached LayoutInflater will be replaced immediately prior to + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} and cleared immediately after + * {@link #onDetach()}. + * + * @return The LayoutInflater used to inflate Views of this Fragment. + */ + @NonNull + public final LayoutInflater getLayoutInflater() { + if (mLayoutInflater == null) { + return performGetLayoutInflater(null); + } + return mLayoutInflater; + } + + /** + * Calls {@link #onGetLayoutInflater(Bundle)} and caches the result for use by + * {@link #getLayoutInflater()}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + * @return The LayoutInflater used to inflate Views of this Fragment. + */ + @NonNull + LayoutInflater performGetLayoutInflater(@Nullable Bundle savedInstanceState) { + LayoutInflater layoutInflater = onGetLayoutInflater(savedInstanceState); + mLayoutInflater = layoutInflater; + return mLayoutInflater; + } + + /** + * Override {@link #onGetLayoutInflater(Bundle)} when you need to change the + * LayoutInflater or call {@link #getLayoutInflater()} when you want to + * retrieve the current LayoutInflater. + * + * @hide + * @deprecated Override {@link #onGetLayoutInflater(Bundle)} or call + * {@link #getLayoutInflater()} instead of this method. + */ + @Deprecated + @NonNull + @RestrictTo(LIBRARY_GROUP_PREFIX) + public LayoutInflater getLayoutInflater(@Nullable Bundle savedFragmentState) { + if (mHost == null) { + throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the " + + "Fragment is attached to the FragmentManager."); + } + LayoutInflater result = mHost.onGetLayoutInflater(); + LayoutInflaterCompat.setFactory2(result, mChildFragmentManager.getLayoutInflaterFactory()); + return result; + } + + /** + * Called when a fragment is being created as part of a view layout + * inflation, typically from setting the content view of an activity. This + * may be called immediately after the fragment is created from a + * tag in a layout file. Note this is before the fragment's + * {@link #onAttach(Activity)} has been called; all you should do here is + * parse the attributes and save them away. + * + *

This is called every time the fragment is inflated, even if it is + * being inflated into a new instance with saved state. It typically makes + * sense to re-parse the parameters each time, to allow them to change with + * different configurations.

+ * + *

Here is a typical implementation of a fragment that can take parameters + * both through attributes supplied here as well from {@link #getArguments()}:

+ * + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentArgumentsSupport.java + * fragment} + * + *

Note that parsing the XML attributes uses a "styleable" resource. The + * declaration for the styleable used here is:

+ * + * {@sample frameworks/support/samples/Support4Demos/src/main/res/values/attrs.xml fragment_arguments} + * + *

The fragment can then be declared within its activity's content layout + * through a tag like this:

+ * + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_arguments_support.xml from_attributes} + * + *

This fragment can also be created dynamically from arguments given + * at runtime in the arguments Bundle; here is an example of doing so at + * creation of the containing activity:

+ * + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentArgumentsSupport.java + * create} + * + * @param context The Activity that is inflating this fragment. + * @param attrs The attributes at the tag where the fragment is + * being created. + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + @CallSuper + public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs, + @Nullable Bundle savedInstanceState) { + mCalled = true; + final Activity hostActivity = mHost == null ? null : mHost.getActivity(); + if (hostActivity != null) { + mCalled = false; + onInflate(hostActivity, attrs, savedInstanceState); + } + } + + /** + * Called when a fragment is being created as part of a view layout + * inflation, typically from setting the content view of an activity. + * + * @deprecated See {@link #onInflate(Context, AttributeSet, Bundle)}. + */ + @Deprecated + @CallSuper + public void onInflate(@NonNull Activity activity, @NonNull AttributeSet attrs, + @Nullable Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when a fragment is attached as a child of this fragment. + * + *

This is called after the attached fragment's onAttach and before + * the attached fragment's onCreate if the fragment has not yet had a previous + * call to onCreate.

+ * + * @param childFragment child fragment being attached + */ + public void onAttachFragment(@NonNull Fragment childFragment) { + } + + /** + * Called when a fragment is first attached to its context. + * {@link #onCreate(Bundle)} will be called after this. + */ + @CallSuper + public void onAttach(@NonNull Context context) { + mCalled = true; + final Activity hostActivity = mHost == null ? null : mHost.getActivity(); + if (hostActivity != null) { + mCalled = false; + onAttach(hostActivity); + } + } + + /** + * Called when a fragment is first attached to its activity. + * {@link #onCreate(Bundle)} will be called after this. + * + * @deprecated See {@link #onAttach(Context)}. + */ + @Deprecated + @CallSuper + public void onAttach(@NonNull Activity activity) { + mCalled = true; + } + + /** + * Called when a fragment loads an animation. Note that if + * {@link FragmentTransaction#setCustomAnimations(int, int)} was called with + * {@link Animator} resources instead of {@link Animation} resources, {@code nextAnim} + * will be an animator resource. + * + * @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not + * set. + * @param enter {@code true} when the fragment is added/attached/shown or {@code false} when + * the fragment is removed/detached/hidden. + * @param nextAnim The resource set in + * {@link FragmentTransaction#setCustomAnimations(int, int)}, + * {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or + * 0 if neither was called. The value will depend on the current operation. + */ + @Nullable + public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { + return null; + } + + /** + * Called when a fragment loads an animator. This will be called when + * {@link #onCreateAnimation(int, boolean, int)} returns null. Note that if + * {@link FragmentTransaction#setCustomAnimations(int, int)} was called with + * {@link Animation} resources instead of {@link Animator} resources, {@code nextAnim} + * will be an animation resource. + * + * @param transit The value set in {@link FragmentTransaction#setTransition(int)} or 0 if not + * set. + * @param enter {@code true} when the fragment is added/attached/shown or {@code false} when + * the fragment is removed/detached/hidden. + * @param nextAnim The resource set in + * {@link FragmentTransaction#setCustomAnimations(int, int)}, + * {@link FragmentTransaction#setCustomAnimations(int, int, int, int)}, or + * 0 if neither was called. The value will depend on the current operation. + */ + @Nullable + public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { + return null; + } + + /** + * Called to do initial creation of a fragment. This is called after + * {@link #onAttach(Activity)} and before + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * + *

Note that this can be called while the fragment's activity is + * still in the process of being created. As such, you can not rely + * on things like the activity's content view hierarchy being initialized + * at this point. If you want to do work once the activity itself is + * created, see {@link #onActivityCreated(Bundle)}. + * + *

Any restored child fragments will be created before the base + * Fragment.onCreate method returns.

+ * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + @CallSuper + public void onCreate(@Nullable Bundle savedInstanceState) { + mCalled = true; + restoreChildFragmentState(savedInstanceState); + if (!mChildFragmentManager.isStateAtLeast(Fragment.CREATED)) { + mChildFragmentManager.dispatchCreate(); + } + } + + /** + * Restore the state of the child FragmentManager. Called by either + * {@link #onCreate(Bundle)} for non-retained instance fragments or by + * {@link FragmentManagerImpl#moveToState(Fragment, int, int, int, boolean)} + * for retained instance fragments. + * + *

Postcondition: if there were child fragments to restore, + * the child FragmentManager will be instantiated and brought to the {@link #CREATED} state. + *

+ * + * @param savedInstanceState the savedInstanceState potentially containing fragment info + */ + void restoreChildFragmentState(@Nullable Bundle savedInstanceState) { + if (savedInstanceState != null) { + Parcelable p = savedInstanceState.getParcelable( + FragmentActivity.FRAGMENTS_TAG); + if (p != null) { + mChildFragmentManager.restoreSaveState(p); + mChildFragmentManager.dispatchCreate(); + } + } + } + + /** + * Called to have the fragment instantiate its user interface view. + * This is optional, and non-graphical fragments can return null. This will be called between + * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}. + *

A default View can be returned by calling {@link #Fragment(int)} in your + * constructor. Otherwise, this method returns null. + * + *

It is recommended to only inflate the layout in this method and move + * logic that operates on the returned View to {@link #onViewCreated(View, Bundle)}. + * + *

If you return a View from here, you will later be called in + * {@link #onDestroyView} when the view is being released. + * + * @param inflater The LayoutInflater object that can be used to inflate + * any views in the fragment, + * @param container If non-null, this is the parent view that the fragment's + * UI should be attached to. The fragment should not add the view itself, + * but this can be used to generate the LayoutParams of the view. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + * + * @return Return the View for the fragment's UI, or null. + */ + @Nullable + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + if (mContentLayoutId != 0) { + return inflater.inflate(mContentLayoutId, container, false); + } + return null; + } + + /** + * Called immediately after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} + * has returned, but before any saved state has been restored in to the view. + * This gives subclasses a chance to initialize themselves once + * they know their view hierarchy has been completely created. The fragment's + * view hierarchy is not however attached to its parent at this point. + * @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + */ + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + } + + /** + * Get the root view for the fragment's layout (the one returned by {@link #onCreateView}), + * if provided. + * + * @return The fragment's root view, or null if it has no layout. + */ + @Nullable + public View getView() { + return mView; + } + + /** + * Get the root view for the fragment's layout (the one returned by {@link #onCreateView}). + * + * @throws IllegalStateException if no view was returned by {@link #onCreateView}. + * @see #getView() + */ + @NonNull + public final View requireView() { + View view = getView(); + if (view == null) { + throw new IllegalStateException("Fragment " + this + " did not return a View from" + + " onCreateView() or this was called before onCreateView()."); + } + return view; + } + + /** + * Called when the fragment's activity has been created and this + * fragment's view hierarchy instantiated. It can be used to do final + * initialization once these pieces are in place, such as retrieving + * views or restoring state. It is also useful for fragments that use + * {@link #setRetainInstance(boolean)} to retain their instance, + * as this callback tells the fragment when it is fully associated with + * the new activity instance. This is called after {@link #onCreateView} + * and before {@link #onViewStateRestored(Bundle)}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + @CallSuper + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when all saved state has been restored into the view hierarchy + * of the fragment. This can be used to do initialization based on saved + * state that you are letting the view hierarchy track itself, such as + * whether check box widgets are currently checked. This is called + * after {@link #onActivityCreated(Bundle)} and before + * {@link #onStart()}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + @CallSuper + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when the Fragment is visible to the user. This is generally + * tied to {@link Activity#onStart() Activity.onStart} of the containing + * Activity's lifecycle. + */ + @CallSuper + public void onStart() { + mCalled = true; + } + + /** + * Called when the fragment is visible to the user and actively running. + * This is generally + * tied to {@link Activity#onResume() Activity.onResume} of the containing + * Activity's lifecycle. + */ + @CallSuper + public void onResume() { + mCalled = true; + } + + /** + * Called to ask the fragment to save its current dynamic state, so it + * can later be reconstructed in a new instance of its process is + * restarted. If a new instance of the fragment later needs to be + * created, the data you place in the Bundle here will be available + * in the Bundle given to {@link #onCreate(Bundle)}, + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}, and + * {@link #onActivityCreated(Bundle)}. + * + *

This corresponds to {@link Activity#onSaveInstanceState(Bundle) + * Activity.onSaveInstanceState(Bundle)} and most of the discussion there + * applies here as well. Note however: this method may be called + * at any time before {@link #onDestroy()}. There are many situations + * where a fragment may be mostly torn down (such as when placed on the + * back stack with no UI showing), but its state will not be saved until + * its owning activity actually needs to save its state. + * + * @param outState Bundle in which to place your saved state. + */ + public void onSaveInstanceState(@NonNull Bundle outState) { + } + + /** + * Called when the Fragment's activity changes from fullscreen mode to multi-window mode and + * visa-versa. This is generally tied to {@link Activity#onMultiWindowModeChanged} of the + * containing Activity. + * + * @param isInMultiWindowMode True if the activity is in multi-window mode. + */ + public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { + } + + /** + * Called by the system when the activity changes to and from picture-in-picture mode. This is + * generally tied to {@link Activity#onPictureInPictureModeChanged} of the containing Activity. + * + * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode. + */ + public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) { + } + + @Override + @CallSuper + public void onConfigurationChanged(@NonNull Configuration newConfig) { + mCalled = true; + } + + /** + * Callback for when the primary navigation state of this Fragment has changed. This can be + * the result of the {@link #getFragmentManager() containing FragmentManager} having its + * primary navigation fragment changed via + * {@link androidx.fragment.app.FragmentTransaction#setPrimaryNavigationFragment} or due to + * the primary navigation fragment changing in a parent FragmentManager. + * + * @param isPrimaryNavigationFragment True if and only if this Fragment and any + * {@link #getParentFragment() parent fragment} is set as the primary navigation fragment + * via {@link androidx.fragment.app.FragmentTransaction#setPrimaryNavigationFragment}. + */ + public void onPrimaryNavigationFragmentChanged(boolean isPrimaryNavigationFragment) { + } + + /** + * Called when the Fragment is no longer resumed. This is generally + * tied to {@link Activity#onPause() Activity.onPause} of the containing + * Activity's lifecycle. + */ + @CallSuper + public void onPause() { + mCalled = true; + } + + /** + * Called when the Fragment is no longer started. This is generally + * tied to {@link Activity#onStop() Activity.onStop} of the containing + * Activity's lifecycle. + */ + @CallSuper + public void onStop() { + mCalled = true; + } + + @Override + @CallSuper + public void onLowMemory() { + mCalled = true; + } + + /** + * Called when the view previously created by {@link #onCreateView} has + * been detached from the fragment. The next time the fragment needs + * to be displayed, a new view will be created. This is called + * after {@link #onStop()} and before {@link #onDestroy()}. It is called + * regardless of whether {@link #onCreateView} returned a + * non-null view. Internally it is called after the view's state has + * been saved but before it has been removed from its parent. + */ + @CallSuper + public void onDestroyView() { + mCalled = true; + } + + /** + * Called when the fragment is no longer in use. This is called + * after {@link #onStop()} and before {@link #onDetach()}. + */ + @CallSuper + public void onDestroy() { + mCalled = true; + } + + /** + * Called by the fragment manager once this fragment has been removed, + * so that we don't have any left-over state if the application decides + * to re-use the instance. This only clears state that the framework + * internally manages, not things the application sets. + */ + void initState() { + initLifecycle(); + mWho = UUID.randomUUID().toString(); + mAdded = false; + mRemoving = false; + mFromLayout = false; + mInLayout = false; + mRestored = false; + mBackStackNesting = 0; + mFragmentManager = null; + mChildFragmentManager = new FragmentManagerImpl(); + mHost = null; + mFragmentId = 0; + mContainerId = 0; + mTag = null; + mHidden = false; + mDetached = false; + } + + /** + * Called when the fragment is no longer attached to its activity. This + * is called after {@link #onDestroy()}. + */ + @CallSuper + public void onDetach() { + mCalled = true; + } + + /** + * Initialize the contents of the Fragment host's standard options menu. You + * should place your menu items in to menu. For this method + * to be called, you must have first called {@link #setHasOptionsMenu}. See + * {@link Activity#onCreateOptionsMenu(Menu) Activity.onCreateOptionsMenu} + * for more information. + * + * @param menu The options menu in which you place your items. + * + * @see #setHasOptionsMenu + * @see #onPrepareOptionsMenu + * @see #onOptionsItemSelected + */ + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + } + + /** + * Prepare the Fragment host's standard options menu to be displayed. This is + * called right before the menu is shown, every time it is shown. You can + * use this method to efficiently enable/disable items or otherwise + * dynamically modify the contents. See + * {@link Activity#onPrepareOptionsMenu(Menu) Activity.onPrepareOptionsMenu} + * for more information. + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + * + * @see #setHasOptionsMenu + * @see #onCreateOptionsMenu + */ + public void onPrepareOptionsMenu(@NonNull Menu menu) { + } + + /** + * Called when this fragment's option menu items are no longer being + * included in the overall options menu. Receiving this call means that + * the menu needed to be rebuilt, but this fragment's items were not + * included in the newly built menu (its {@link #onCreateOptionsMenu(Menu, MenuInflater)} + * was not called). + */ + public void onDestroyOptionsMenu() { + } + + /** + * This hook is called whenever an item in your options menu is selected. + * The default implementation simply returns false to have the normal + * processing happen (calling the item's Runnable or sending a message to + * its Handler as appropriate). You can use this method for any items + * for which you would like to do processing without those other + * facilities. + * + *

Derived classes should call through to the base class for it to + * perform the default menu handling. + * + * @param item The menu item that was selected. + * + * @return boolean Return false to allow normal menu processing to + * proceed, true to consume it here. + * + * @see #onCreateOptionsMenu + */ + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + return false; + } + + /** + * This hook is called whenever the options menu is being closed (either by the user canceling + * the menu with the back/menu button, or when an item is selected). + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + */ + public void onOptionsMenuClosed(@NonNull Menu menu) { + } + + /** + * Called when a context menu for the {@code view} is about to be shown. + * Unlike {@link #onCreateOptionsMenu}, this will be called every + * time the context menu is about to be shown and should be populated for + * the view (or item inside the view for {@link AdapterView} subclasses, + * this can be found in the {@code menuInfo})). + *

+ * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an + * item has been selected. + *

+ * The default implementation calls up to + * {@link Activity#onCreateContextMenu Activity.onCreateContextMenu}, though + * you can not call this implementation if you don't want that behavior. + *

+ * It is not safe to hold onto the context menu after this method returns. + * {@inheritDoc} + */ + @Override + public void onCreateContextMenu(@NonNull ContextMenu menu, @NonNull View v, + @Nullable ContextMenuInfo menuInfo) { + requireActivity().onCreateContextMenu(menu, v, menuInfo); + } + + /** + * Registers a context menu to be shown for the given view (multiple views + * can show the context menu). This method will set the + * {@link OnCreateContextMenuListener} on the view to this fragment, so + * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be + * called when it is time to show the context menu. + * + * @see #unregisterForContextMenu(View) + * @param view The view that should show a context menu. + */ + public void registerForContextMenu(@NonNull View view) { + view.setOnCreateContextMenuListener(this); + } + + /** + * Prevents a context menu to be shown for the given view. This method will + * remove the {@link OnCreateContextMenuListener} on the view. + * + * @see #registerForContextMenu(View) + * @param view The view that should stop showing a context menu. + */ + public void unregisterForContextMenu(@NonNull View view) { + view.setOnCreateContextMenuListener(null); + } + + /** + * This hook is called whenever an item in a context menu is selected. The + * default implementation simply returns false to have the normal processing + * happen (calling the item's Runnable or sending a message to its Handler + * as appropriate). You can use this method for any items for which you + * would like to do processing without those other facilities. + *

+ * Use {@link MenuItem#getMenuInfo()} to get extra information set by the + * View that added this menu item. + *

+ * Derived classes should call through to the base class for it to perform + * the default menu handling. + * + * @param item The context menu item that was selected. + * @return boolean Return false to allow normal context menu processing to + * proceed, true to consume it here. + */ + public boolean onContextItemSelected(@NonNull MenuItem item) { + return false; + } + + /** + * When custom transitions are used with Fragments, the enter transition callback + * is called when this Fragment is attached or detached when not popping the back stack. + * + * @param callback Used to manipulate the shared element transitions on this Fragment + * when added not as a pop from the back stack. + */ + public void setEnterSharedElementCallback(@Nullable SharedElementCallback callback) { + ensureAnimationInfo().mEnterTransitionCallback = callback; + } + + /** + * When custom transitions are used with Fragments, the exit transition callback + * is called when this Fragment is attached or detached when popping the back stack. + * + * @param callback Used to manipulate the shared element transitions on this Fragment + * when added as a pop from the back stack. + */ + public void setExitSharedElementCallback(@Nullable SharedElementCallback callback) { + ensureAnimationInfo().mExitTransitionCallback = callback; + } + + /** + * Sets the Transition that will be used to move Views into the initial scene. The entering + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#INVISIBLE} to {@link View#VISIBLE}. If transition is null, + * entering Views will remain unaffected. + * + * @param transition The Transition to use to move Views into the initial Scene. + * transition must be an + * {@link android.transition.Transition} or + * {@link androidx.transition.Transition}. + */ + public void setEnterTransition(@Nullable Object transition) { + ensureAnimationInfo().mEnterTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views into the initial scene. The entering + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#INVISIBLE} to {@link View#VISIBLE}. + * + * @return the Transition to use to move Views into the initial Scene. + */ + @Nullable + public Object getEnterTransition() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mEnterTransition; + } + + /** + * Sets the Transition that will be used to move Views out of the scene when the Fragment is + * preparing to be removed, hidden, or detached because of popping the back stack. The exiting + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * entering Views will remain unaffected. If nothing is set, the default will be to + * use the same value as set in {@link #setEnterTransition(Object)}. + * + * @param transition The Transition to use to move Views out of the Scene when the Fragment + * is preparing to close due to popping the back stack. transition must be + * an {@link android.transition.Transition} or + * {@link androidx.transition.Transition}. + */ + public void setReturnTransition(@Nullable Object transition) { + ensureAnimationInfo().mReturnTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views out of the scene when the Fragment is + * preparing to be removed, hidden, or detached because of popping the back stack. The exiting + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#VISIBLE} to {@link View#INVISIBLE}. If nothing is set, the default will be to use + * the same transition as {@link #getEnterTransition()}. + * + * @return the Transition to use to move Views out of the Scene when the Fragment + * is preparing to close due to popping the back stack. + */ + @Nullable + public Object getReturnTransition() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition() + : mAnimationInfo.mReturnTransition; + } + + /** + * Sets the Transition that will be used to move Views out of the scene when the + * fragment is removed, hidden, or detached when not popping the back stack. + * The exiting Views will be those that are regular Views or ViewGroups that + * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as exiting is governed by changing visibility + * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will + * remain unaffected. + * + * @param transition The Transition to use to move Views out of the Scene when the Fragment + * is being closed not due to popping the back stack. transition + * must be an + * {@link android.transition.Transition} or + * {@link androidx.transition.Transition}. + */ + public void setExitTransition(@Nullable Object transition) { + ensureAnimationInfo().mExitTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views out of the scene when the + * fragment is removed, hidden, or detached when not popping the back stack. + * The exiting Views will be those that are regular Views or ViewGroups that + * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as exiting is governed by changing visibility + * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will + * remain unaffected. + * + * @return the Transition to use to move Views out of the Scene when the Fragment + * is being closed not due to popping the back stack. + */ + @Nullable + public Object getExitTransition() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mExitTransition; + } + + /** + * Sets the Transition that will be used to move Views in to the scene when returning due + * to popping a back stack. The entering Views will be those that are regular Views + * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions + * will extend {@link android.transition.Visibility} as exiting is governed by changing + * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * the views will remain unaffected. If nothing is set, the default will be to use the same + * transition as {@link #getExitTransition()}. + * + * @param transition The Transition to use to move Views into the scene when reentering from a + * previously-started Activity due to popping the back stack. transition + * must be an + * {@link android.transition.Transition} or + * {@link androidx.transition.Transition}. + */ + public void setReenterTransition(@Nullable Object transition) { + ensureAnimationInfo().mReenterTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views in to the scene when returning due + * to popping a back stack. The entering Views will be those that are regular Views + * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions + * will extend {@link android.transition.Visibility} as exiting is governed by changing + * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If nothing is set, the + * default will be to use the same transition as {@link #getExitTransition()}. + * + * @return the Transition to use to move Views into the scene when reentering from a + * previously-started Activity due to popping the back stack. + */ + @Nullable + public Object getReenterTransition() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition() + : mAnimationInfo.mReenterTransition; + } + + /** + * Sets the Transition that will be used for shared elements transferred into the content + * Scene. Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * + * @param transition The Transition to use for shared elements transferred into the content + * Scene. transition must be an + * {@link android.transition.Transition android.transition.Transition} or + * {@link androidx.transition.Transition androidx.transition.Transition}. + */ + public void setSharedElementEnterTransition(@Nullable Object transition) { + ensureAnimationInfo().mSharedElementEnterTransition = transition; + } + + /** + * Returns the Transition that will be used for shared elements transferred into the content + * Scene. Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * + * @return The Transition to use for shared elements transferred into the content + * Scene. + */ + @Nullable + public Object getSharedElementEnterTransition() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mSharedElementEnterTransition; + } + + /** + * Sets the Transition that will be used for shared elements transferred back during a + * pop of the back stack. This Transition acts in the leaving Fragment. + * Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * If no value is set, the default will be to use the same value as + * {@link #setSharedElementEnterTransition(Object)}. + * + * @param transition The Transition to use for shared elements transferred out of the content + * Scene. transition must be an + * {@link android.transition.Transition android.transition.Transition} or + * {@link androidx.transition.Transition androidx.transition.Transition}. + */ + public void setSharedElementReturnTransition(@Nullable Object transition) { + ensureAnimationInfo().mSharedElementReturnTransition = transition; + } + + /** + * Return the Transition that will be used for shared elements transferred back during a + * pop of the back stack. This Transition acts in the leaving Fragment. + * Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * If no value is set, the default will be to use the same value as + * {@link #setSharedElementEnterTransition(Object)}. + * + * @return The Transition to use for shared elements transferred out of the content + * Scene. + */ + @Nullable + public Object getSharedElementReturnTransition() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mSharedElementReturnTransition == USE_DEFAULT_TRANSITION + ? getSharedElementEnterTransition() + : mAnimationInfo.mSharedElementReturnTransition; + } + + /** + * Sets whether the the exit transition and enter transition overlap or not. + * When true, the enter transition will start as soon as possible. When false, the + * enter transition will wait until the exit transition completes before starting. + * + * @param allow true to start the enter transition when possible or false to + * wait until the exiting transition completes. + */ + public void setAllowEnterTransitionOverlap(boolean allow) { + ensureAnimationInfo().mAllowEnterTransitionOverlap = allow; + } + + /** + * Returns whether the the exit transition and enter transition overlap or not. + * When true, the enter transition will start as soon as possible. When false, the + * enter transition will wait until the exit transition completes before starting. + * + * @return true when the enter transition should start as soon as possible or false to + * when it should wait until the exiting transition completes. + */ + public boolean getAllowEnterTransitionOverlap() { + return (mAnimationInfo == null || mAnimationInfo.mAllowEnterTransitionOverlap == null) + ? true : mAnimationInfo.mAllowEnterTransitionOverlap; + } + + /** + * Sets whether the the return transition and reenter transition overlap or not. + * When true, the reenter transition will start as soon as possible. When false, the + * reenter transition will wait until the return transition completes before starting. + * + * @param allow true to start the reenter transition when possible or false to wait until the + * return transition completes. + */ + public void setAllowReturnTransitionOverlap(boolean allow) { + ensureAnimationInfo().mAllowReturnTransitionOverlap = allow; + } + + /** + * Returns whether the the return transition and reenter transition overlap or not. + * When true, the reenter transition will start as soon as possible. When false, the + * reenter transition will wait until the return transition completes before starting. + * + * @return true to start the reenter transition when possible or false to wait until the + * return transition completes. + */ + public boolean getAllowReturnTransitionOverlap() { + return (mAnimationInfo == null || mAnimationInfo.mAllowReturnTransitionOverlap == null) + ? true : mAnimationInfo.mAllowReturnTransitionOverlap; + } + + /** + * Postpone the entering Fragment transition until {@link #startPostponedEnterTransition()} + * or {@link FragmentManager#executePendingTransactions()} has been called. + *

+ * This method gives the Fragment the ability to delay Fragment animations + * until all data is loaded. Until then, the added, shown, and + * attached Fragments will be INVISIBLE and removed, hidden, and detached Fragments won't + * be have their Views removed. The transaction runs when all postponed added Fragments in the + * transaction have called {@link #startPostponedEnterTransition()}. + *

+ * This method should be called before being added to the FragmentTransaction or + * in {@link #onCreate(Bundle)}, {@link #onAttach(Context)}, or + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}}. + * {@link #startPostponedEnterTransition()} must be called to allow the Fragment to + * start the transitions. + *

+ * When a FragmentTransaction is started that may affect a postponed FragmentTransaction, + * based on which containers are in their operations, the postponed FragmentTransaction + * will have its start triggered. The early triggering may result in faulty or nonexistent + * animations in the postponed transaction. FragmentTransactions that operate only on + * independent containers will not interfere with each other's postponement. + *

+ * Calling postponeEnterTransition on Fragments with a null View will not postpone the + * transition. Likewise, postponement only works if + * {@link FragmentTransaction#setReorderingAllowed(boolean) FragmentTransaction reordering} is + * enabled. + * + * @see Activity#postponeEnterTransition() + * @see FragmentTransaction#setReorderingAllowed(boolean) + */ + public void postponeEnterTransition() { + ensureAnimationInfo().mEnterTransitionPostponed = true; + } + + /** + * Postpone the entering Fragment transition for a given amount of time and then call + * {@link #startPostponedEnterTransition()}. + *

+ * This method gives the Fragment the ability to delay Fragment animations for a given amount + * of time. Until then, the added, shown, and attached Fragments will be INVISIBLE and removed, + * hidden, and detached Fragments won't be have their Views removed. The transaction runs when + * all postponed added Fragments in the transaction have called + * {@link #startPostponedEnterTransition()}. + *

+ * This method should be called before being added to the FragmentTransaction or + * in {@link #onCreate(Bundle)}, {@link #onAttach(Context)}, or + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}}. + *

+ * When a FragmentTransaction is started that may affect a postponed FragmentTransaction, + * based on which containers are in their operations, the postponed FragmentTransaction + * will have its start triggered. The early triggering may result in faulty or nonexistent + * animations in the postponed transaction. FragmentTransactions that operate only on + * independent containers will not interfere with each other's postponement. + *

+ * Calling postponeEnterTransition on Fragments with a null View will not postpone the + * transition. Likewise, postponement only works if + * {@link FragmentTransaction#setReorderingAllowed(boolean) FragmentTransaction reordering} is + * enabled. + * + * @param duration The length of the delay in {@code timeUnit} units + * @param timeUnit The units of time for {@code duration} + * @see Activity#postponeEnterTransition() + * @see FragmentTransaction#setReorderingAllowed(boolean) + */ + public final void postponeEnterTransition(long duration, @NonNull TimeUnit timeUnit) { + ensureAnimationInfo().mEnterTransitionPostponed = true; + Handler handler; + if (mFragmentManager != null) { + handler = mFragmentManager.mHost.getHandler(); + } else { + handler = new Handler(Looper.getMainLooper()); + } + handler.removeCallbacks(mPostponedDurationRunnable); + handler.postDelayed(mPostponedDurationRunnable, timeUnit.toMillis(duration)); + } + + /** + * Begin postponed transitions after {@link #postponeEnterTransition()} was called. + * If postponeEnterTransition() was called, you must call startPostponedEnterTransition() + * or {@link FragmentManager#executePendingTransactions()} to complete the FragmentTransaction. + * If postponement was interrupted with {@link FragmentManager#executePendingTransactions()}, + * before {@code startPostponedEnterTransition()}, animations may not run or may execute + * improperly. + * + * @see Activity#startPostponedEnterTransition() + */ + public void startPostponedEnterTransition() { + if (mFragmentManager == null || mFragmentManager.mHost == null) { + ensureAnimationInfo().mEnterTransitionPostponed = false; + } else if (Looper.myLooper() != mFragmentManager.mHost.getHandler().getLooper()) { + mFragmentManager.mHost.getHandler().postAtFrontOfQueue(new Runnable() { + @Override + public void run() { + callStartTransitionListener(); + } + }); + } else { + callStartTransitionListener(); + } + } + + /** + * Calls the start transition listener. This must be called on the UI thread. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void callStartTransitionListener() { + final OnStartEnterTransitionListener listener; + if (mAnimationInfo == null) { + listener = null; + } else { + mAnimationInfo.mEnterTransitionPostponed = false; + listener = mAnimationInfo.mStartEnterTransitionListener; + mAnimationInfo.mStartEnterTransitionListener = null; + } + if (listener != null) { + listener.onStartEnterTransition(); + } + } + + /** + * Print the Fragments's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + */ + public void dump(@NonNull String prefix, @Nullable FileDescriptor fd, + @NonNull PrintWriter writer, @Nullable String[] args) { + writer.print(prefix); writer.print("mFragmentId=#"); + writer.print(Integer.toHexString(mFragmentId)); + writer.print(" mContainerId=#"); + writer.print(Integer.toHexString(mContainerId)); + writer.print(" mTag="); writer.println(mTag); + writer.print(prefix); writer.print("mState="); writer.print(mState); + writer.print(" mWho="); writer.print(mWho); + writer.print(" mBackStackNesting="); writer.println(mBackStackNesting); + writer.print(prefix); writer.print("mAdded="); writer.print(mAdded); + writer.print(" mRemoving="); writer.print(mRemoving); + writer.print(" mFromLayout="); writer.print(mFromLayout); + writer.print(" mInLayout="); writer.println(mInLayout); + writer.print(prefix); writer.print("mHidden="); writer.print(mHidden); + writer.print(" mDetached="); writer.print(mDetached); + writer.print(" mMenuVisible="); writer.print(mMenuVisible); + writer.print(" mHasMenu="); writer.println(mHasMenu); + writer.print(prefix); writer.print("mRetainInstance="); writer.print(mRetainInstance); + writer.print(" mUserVisibleHint="); writer.println(mUserVisibleHint); + if (mFragmentManager != null) { + writer.print(prefix); writer.print("mFragmentManager="); + writer.println(mFragmentManager); + } + if (mHost != null) { + writer.print(prefix); writer.print("mHost="); + writer.println(mHost); + } + if (mParentFragment != null) { + writer.print(prefix); writer.print("mParentFragment="); + writer.println(mParentFragment); + } + if (mArguments != null) { + writer.print(prefix); writer.print("mArguments="); writer.println(mArguments); + } + if (mSavedFragmentState != null) { + writer.print(prefix); writer.print("mSavedFragmentState="); + writer.println(mSavedFragmentState); + } + if (mSavedViewState != null) { + writer.print(prefix); writer.print("mSavedViewState="); + writer.println(mSavedViewState); + } + Fragment target = getTargetFragment(); + if (target != null) { + writer.print(prefix); writer.print("mTarget="); writer.print(target); + writer.print(" mTargetRequestCode="); + writer.println(mTargetRequestCode); + } + if (getNextAnim() != 0) { + writer.print(prefix); writer.print("mNextAnim="); writer.println(getNextAnim()); + } + if (mContainer != null) { + writer.print(prefix); writer.print("mContainer="); writer.println(mContainer); + } + if (mView != null) { + writer.print(prefix); writer.print("mView="); writer.println(mView); + } + if (mInnerView != null) { + writer.print(prefix); writer.print("mInnerView="); writer.println(mView); + } + if (getAnimatingAway() != null) { + writer.print(prefix); + writer.print("mAnimatingAway="); + writer.println(getAnimatingAway()); + writer.print(prefix); + writer.print("mStateAfterAnimating="); + writer.println(getStateAfterAnimating()); + } + if (getContext() != null) { + LoaderManager.getInstance(this).dump(prefix, fd, writer, args); + } + writer.print(prefix); + writer.println("Child " + mChildFragmentManager + ":"); + mChildFragmentManager.dump(prefix + " ", fd, writer, args); + } + + @Nullable + Fragment findFragmentByWho(@NonNull String who) { + if (who.equals(mWho)) { + return this; + } + return mChildFragmentManager.findFragmentByWho(who); + } + + void performAttach() { + mChildFragmentManager.attachController(mHost, new FragmentContainer() { + @Override + @Nullable + public View onFindViewById(int id) { + if (mView == null) { + throw new IllegalStateException("Fragment " + this + " does not have a view"); + } + return mView.findViewById(id); + } + + @Override + public boolean onHasView() { + return (mView != null); + } + }, this); + mCalled = false; + onAttach(mHost.getContext()); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onAttach()"); + } + } + + void performCreate(Bundle savedInstanceState) { + mChildFragmentManager.noteStateNotSaved(); + mState = CREATED; + mCalled = false; + mSavedStateRegistryController.performRestore(savedInstanceState); + onCreate(savedInstanceState); + mIsCreated = true; + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onCreate()"); + } + mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + } + + void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + mChildFragmentManager.noteStateNotSaved(); + mPerformedCreateView = true; + mViewLifecycleOwner = new FragmentViewLifecycleOwner(); + mView = onCreateView(inflater, container, savedInstanceState); + if (mView != null) { + // Initialize the view lifecycle + mViewLifecycleOwner.initialize(); + // Then inform any Observers of the new LifecycleOwner + mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner); + } else { + if (mViewLifecycleOwner.isInitialized()) { + throw new IllegalStateException("Called getViewLifecycleOwner() but " + + "onCreateView() returned null"); + } + mViewLifecycleOwner = null; + } + } + + void performActivityCreated(Bundle savedInstanceState) { + mChildFragmentManager.noteStateNotSaved(); + mState = ACTIVITY_CREATED; + mCalled = false; + onActivityCreated(savedInstanceState); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onActivityCreated()"); + } + mChildFragmentManager.dispatchActivityCreated(); + } + + void performStart() { + mChildFragmentManager.noteStateNotSaved(); + mChildFragmentManager.execPendingActions(); + mState = STARTED; + mCalled = false; + onStart(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onStart()"); + } + mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + if (mView != null) { + mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START); + } + mChildFragmentManager.dispatchStart(); + } + + void performResume() { + mChildFragmentManager.noteStateNotSaved(); + mChildFragmentManager.execPendingActions(); + mState = RESUMED; + mCalled = false; + onResume(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onResume()"); + } + mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + if (mView != null) { + mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + } + mChildFragmentManager.dispatchResume(); + mChildFragmentManager.execPendingActions(); + } + + void noteStateNotSaved() { + mChildFragmentManager.noteStateNotSaved(); + } + + void performPrimaryNavigationFragmentChanged() { + boolean isPrimaryNavigationFragment = mFragmentManager.isPrimaryNavigation(this); + // Only send out the callback / dispatch if the state has changed + if (mIsPrimaryNavigationFragment == null + || mIsPrimaryNavigationFragment != isPrimaryNavigationFragment) { + mIsPrimaryNavigationFragment = isPrimaryNavigationFragment; + onPrimaryNavigationFragmentChanged(isPrimaryNavigationFragment); + mChildFragmentManager.dispatchPrimaryNavigationFragmentChanged(); + } + } + + void performMultiWindowModeChanged(boolean isInMultiWindowMode) { + onMultiWindowModeChanged(isInMultiWindowMode); + mChildFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode); + } + + void performPictureInPictureModeChanged(boolean isInPictureInPictureMode) { + onPictureInPictureModeChanged(isInPictureInPictureMode); + mChildFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode); + } + + void performConfigurationChanged(@NonNull Configuration newConfig) { + onConfigurationChanged(newConfig); + mChildFragmentManager.dispatchConfigurationChanged(newConfig); + } + + void performLowMemory() { + onLowMemory(); + mChildFragmentManager.dispatchLowMemory(); + } + + /* + void performTrimMemory(int level) { + onTrimMemory(level); + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchTrimMemory(level); + } + } + */ + + boolean performCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + boolean show = false; + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + show = true; + onCreateOptionsMenu(menu, inflater); + } + show |= mChildFragmentManager.dispatchCreateOptionsMenu(menu, inflater); + } + return show; + } + + boolean performPrepareOptionsMenu(@NonNull Menu menu) { + boolean show = false; + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + show = true; + onPrepareOptionsMenu(menu); + } + show |= mChildFragmentManager.dispatchPrepareOptionsMenu(menu); + } + return show; + } + + boolean performOptionsItemSelected(@NonNull MenuItem item) { + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + if (onOptionsItemSelected(item)) { + return true; + } + } + if (mChildFragmentManager.dispatchOptionsItemSelected(item)) { + return true; + } + } + return false; + } + + boolean performContextItemSelected(@NonNull MenuItem item) { + if (!mHidden) { + if (onContextItemSelected(item)) { + return true; + } + if (mChildFragmentManager.dispatchContextItemSelected(item)) { + return true; + } + } + return false; + } + + void performOptionsMenuClosed(@NonNull Menu menu) { + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + onOptionsMenuClosed(menu); + } + mChildFragmentManager.dispatchOptionsMenuClosed(menu); + } + } + + void performSaveInstanceState(Bundle outState) { + onSaveInstanceState(outState); + mSavedStateRegistryController.performSave(outState); + Parcelable p = mChildFragmentManager.saveAllState(); + if (p != null) { + outState.putParcelable(FragmentActivity.FRAGMENTS_TAG, p); + } + } + + void performPause() { + mChildFragmentManager.dispatchPause(); + if (mView != null) { + mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); + } + mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); + mState = STARTED; + mCalled = false; + onPause(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onPause()"); + } + } + + void performStop() { + mChildFragmentManager.dispatchStop(); + if (mView != null) { + mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + } + mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + mState = ACTIVITY_CREATED; + mCalled = false; + onStop(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onStop()"); + } + } + + void performDestroyView() { + mChildFragmentManager.dispatchDestroyView(); + if (mView != null) { + mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + } + mState = CREATED; + mCalled = false; + onDestroyView(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onDestroyView()"); + } + // Handles the detach/reattach case where the view hierarchy + // is destroyed and recreated and an additional call to + // onLoadFinished may be needed to ensure the new view + // hierarchy is populated from data from the Loaders + LoaderManager.getInstance(this).markForRedelivery(); + mPerformedCreateView = false; + } + + void performDestroy() { + mChildFragmentManager.dispatchDestroy(); + mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + mState = INITIALIZING; + mCalled = false; + mIsCreated = false; + onDestroy(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onDestroy()"); + } + } + + void performDetach() { + mCalled = false; + onDetach(); + mLayoutInflater = null; + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onDetach()"); + } + + // Destroy the child FragmentManager if we still have it here. + // This is normally done in performDestroy(), but is done here + // specifically if the Fragment is retained. + if (!mChildFragmentManager.isDestroyed()) { + mChildFragmentManager.dispatchDestroy(); + mChildFragmentManager = new FragmentManagerImpl(); + } + } + + void setOnStartEnterTransitionListener(OnStartEnterTransitionListener listener) { + ensureAnimationInfo(); + if (listener == mAnimationInfo.mStartEnterTransitionListener) { + return; + } + if (listener != null && mAnimationInfo.mStartEnterTransitionListener != null) { + throw new IllegalStateException("Trying to set a replacement " + + "startPostponedEnterTransition on " + this); + } + if (mAnimationInfo.mEnterTransitionPostponed) { + mAnimationInfo.mStartEnterTransitionListener = listener; + } + if (listener != null) { + listener.startListening(); + } + } + + private AnimationInfo ensureAnimationInfo() { + if (mAnimationInfo == null) { + mAnimationInfo = new AnimationInfo(); + } + return mAnimationInfo; + } + + int getNextAnim() { + if (mAnimationInfo == null) { + return 0; + } + return mAnimationInfo.mNextAnim; + } + + void setNextAnim(int animResourceId) { + if (mAnimationInfo == null && animResourceId == 0) { + return; // no change! + } + ensureAnimationInfo().mNextAnim = animResourceId; + } + + int getNextTransition() { + if (mAnimationInfo == null) { + return 0; + } + return mAnimationInfo.mNextTransition; + } + + void setNextTransition(int nextTransition, int nextTransitionStyle) { + if (mAnimationInfo == null && nextTransition == 0 && nextTransitionStyle == 0) { + return; // no change! + } + ensureAnimationInfo(); + mAnimationInfo.mNextTransition = nextTransition; + mAnimationInfo.mNextTransitionStyle = nextTransitionStyle; + } + + int getNextTransitionStyle() { + if (mAnimationInfo == null) { + return 0; + } + return mAnimationInfo.mNextTransitionStyle; + } + + SharedElementCallback getEnterTransitionCallback() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mEnterTransitionCallback; + } + + SharedElementCallback getExitTransitionCallback() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mExitTransitionCallback; + } + + View getAnimatingAway() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mAnimatingAway; + } + + void setAnimatingAway(View view) { + ensureAnimationInfo().mAnimatingAway = view; + } + + void setAnimator(Animator animator) { + ensureAnimationInfo().mAnimator = animator; + } + + Animator getAnimator() { + if (mAnimationInfo == null) { + return null; + } + return mAnimationInfo.mAnimator; + } + + int getStateAfterAnimating() { + if (mAnimationInfo == null) { + return 0; + } + return mAnimationInfo.mStateAfterAnimating; + } + + void setStateAfterAnimating(int state) { + ensureAnimationInfo().mStateAfterAnimating = state; + } + + boolean isPostponed() { + if (mAnimationInfo == null) { + return false; + } + return mAnimationInfo.mEnterTransitionPostponed; + } + + boolean isHideReplaced() { + if (mAnimationInfo == null) { + return false; + } + return mAnimationInfo.mIsHideReplaced; + } + + void setHideReplaced(boolean replaced) { + ensureAnimationInfo().mIsHideReplaced = replaced; + } + + /** + * Used internally to be notified when {@link #startPostponedEnterTransition()} has + * been called. This listener will only be called once and then be removed from the + * listeners. + */ + interface OnStartEnterTransitionListener { + void onStartEnterTransition(); + void startListening(); + } + + /** + * Contains all the animation and transition information for a fragment. This will only + * be instantiated for Fragments that have Views. + */ + static class AnimationInfo { + // Non-null if the fragment's view hierarchy is currently animating away, + // meaning we need to wait a bit on completely destroying it. This is the + // view that is animating. + View mAnimatingAway; + + // Non-null if the fragment's view hierarchy is currently animating away with an + // animator instead of an animation. + Animator mAnimator; + + // If mAnimatingAway != null, this is the state we should move to once the + // animation is done. + int mStateAfterAnimating; + + // If app has requested a specific animation, this is the one to use. + int mNextAnim; + + // If app has requested a specific transition, this is the one to use. + int mNextTransition; + + // If app has requested a specific transition style, this is the one to use. + int mNextTransitionStyle; + + Object mEnterTransition = null; + Object mReturnTransition = USE_DEFAULT_TRANSITION; + Object mExitTransition = null; + Object mReenterTransition = USE_DEFAULT_TRANSITION; + Object mSharedElementEnterTransition = null; + Object mSharedElementReturnTransition = USE_DEFAULT_TRANSITION; + Boolean mAllowReturnTransitionOverlap; + Boolean mAllowEnterTransitionOverlap; + + SharedElementCallback mEnterTransitionCallback = null; + SharedElementCallback mExitTransitionCallback = null; + + // True when postponeEnterTransition has been called and startPostponeEnterTransition + // hasn't been called yet. + boolean mEnterTransitionPostponed; + + // Listener to wait for startPostponeEnterTransition. After being called, it will + // be set to null + OnStartEnterTransitionListener mStartEnterTransitionListener; + + // True if the View was hidden, but the transition is handling the hide + boolean mIsHideReplaced; + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentActivity.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentActivity.java new file mode 100644 index 000000000..cafe39baa --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentActivity.java @@ -0,0 +1,1013 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.Window; + +import androidx.activity.ComponentActivity; +import androidx.activity.OnBackPressedDispatcher; +import androidx.activity.OnBackPressedDispatcherOwner; +import androidx.annotation.CallSuper; +import androidx.annotation.ContentView; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.collection.SparseArrayCompat; +import androidx.core.app.ActivityCompat; +import androidx.core.app.SharedElementCallback; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.lifecycle.ViewModelStore; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.loader.app.LoaderManager; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Collection; + +/** + * Base class for activities that want to use the support-based + * {@link Fragment Fragments}. + * + *

Known limitations:

+ *
    + *
  • When using the <fragment> tag, this implementation can not + * use the parent view's ID as the new fragment's ID. You must explicitly + * specify an ID (or tag) in the <fragment>.

    + *
+ */ +public class FragmentActivity extends ComponentActivity implements + ActivityCompat.OnRequestPermissionsResultCallback, + ActivityCompat.RequestPermissionsRequestCodeValidator { + private static final String TAG = "FragmentActivity"; + + static final String FRAGMENTS_TAG = "android:support:fragments"; + static final String NEXT_CANDIDATE_REQUEST_INDEX_TAG = "android:support:next_request_index"; + static final String ALLOCATED_REQUEST_INDICIES_TAG = "android:support:request_indicies"; + static final String REQUEST_FRAGMENT_WHO_TAG = "android:support:request_fragment_who"; + static final int MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS = 0xffff - 1; + + final FragmentController mFragments = FragmentController.createController(new HostCallbacks()); + /** + * A {@link Lifecycle} that is exactly nested outside of when the FragmentController + * has its state changed, providing the proper nesting of Lifecycle callbacks + *

+ * TODO(b/127528777) Drive Fragment Lifecycle with LifecycleObserver + */ + final LifecycleRegistry mFragmentLifecycleRegistry = new LifecycleRegistry(this); + + boolean mCreated; + boolean mResumed; + boolean mStopped = true; + + boolean mRequestedPermissionsFromFragment; + + // We need to keep track of whether startIntentSenderForResult originated from a Fragment, so we + // can conditionally check whether the requestCode collides with our reserved ID space for the + // request index (see above). Unfortunately we can't just call + // super.startIntentSenderForResult(...) to bypass the check when the call didn't come from a + // fragment, since we need to use the ActivityCompat version for backward compatibility. + boolean mStartedIntentSenderFromFragment; + // We need to keep track of whether startActivityForResult originated from a Fragment, so we + // can conditionally check whether the requestCode collides with our reserved ID space for the + // request index (see above). Unfortunately we can't just call + // super.startActivityForResult(...) to bypass the check when the call didn't come from a + // fragment, since we need to use the ActivityCompat version for backward compatibility. + boolean mStartedActivityFromFragment; + + // A hint for the next candidate request index. Request indicies are ints between 0 and 2^16-1 + // which are encoded into the upper 16 bits of the requestCode for + // Fragment.startActivityForResult(...) calls. This allows us to dispatch onActivityResult(...) + // to the appropriate Fragment. Request indicies are allocated by allocateRequestIndex(...). + int mNextCandidateRequestIndex; + // A map from request index to Fragment "who" (i.e. a Fragment's unique identifier). Used to + // keep track of the originating Fragment for Fragment.startActivityForResult(...) calls, so we + // can dispatch the onActivityResult(...) to the appropriate Fragment. Will only contain entries + // for startActivityForResult calls where a result has not yet been delivered. + SparseArrayCompat mPendingFragmentActivityResults; + + /** + * Default constructor for FragmentActivity. All Activities must have a default constructor + * for API 27 and lower devices or when using the default + * {@link android.app.AppComponentFactory}. + */ + public FragmentActivity() { + super(); + } + + /** + * Alternate constructor that can be used to provide a default layout + * that will be inflated as part of super.onCreate(savedInstanceState). + * + *

This should generally be called from your constructor that takes no parameters, + * as is required for API 27 and lower or when using the default + * {@link android.app.AppComponentFactory}. + * + * @see #FragmentActivity() + */ + @ContentView + public FragmentActivity(@LayoutRes int contentLayoutId) { + super(contentLayoutId); + } + + // ------------------------------------------------------------------------ + // HOOKS INTO ACTIVITY + // ------------------------------------------------------------------------ + + /** + * Dispatch incoming result to the correct fragment. + */ + @Override + @CallSuper + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + mFragments.noteStateNotSaved(); + int requestIndex = requestCode>>16; + if (requestIndex != 0) { + requestIndex--; + + String who = mPendingFragmentActivityResults.get(requestIndex); + mPendingFragmentActivityResults.remove(requestIndex); + if (who == null) { + Log.w(TAG, "Activity result delivered for unknown Fragment."); + return; + } + Fragment targetFragment = mFragments.findFragmentByWho(who); + if (targetFragment == null) { + Log.w(TAG, "Activity result no fragment exists for who: " + who); + } else { + targetFragment.onActivityResult(requestCode & 0xffff, resultCode, data); + } + return; + } + ActivityCompat.PermissionCompatDelegate delegate = + ActivityCompat.getPermissionCompatDelegate(); + if (delegate != null && delegate.onActivityResult(this, requestCode, resultCode, data)) { + // Delegate has handled the activity result + return; + } + + super.onActivityResult(requestCode, resultCode, data); + } + + /** + * Reverses the Activity Scene entry Transition and triggers the calling Activity + * to reverse its exit Transition. When the exit Transition completes, + * {@link #finish()} is called. If no entry Transition was used, finish() is called + * immediately and the Activity exit Transition is run. + * + *

On Android 4.4 or lower, this method only finishes the Activity with no + * special exit transition.

+ */ + public void supportFinishAfterTransition() { + ActivityCompat.finishAfterTransition(this); + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, callback + * will be called to handle shared elements on the launched Activity. This requires + * {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param callback Used to manipulate shared element transitions on the launched Activity. + */ + public void setEnterSharedElementCallback(@Nullable SharedElementCallback callback) { + ActivityCompat.setEnterSharedElementCallback(this, callback); + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, listener + * will be called to handle shared elements on the launching Activity. Most + * calls will only come when returning from the started Activity. + * This requires {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param listener Used to manipulate shared element transitions on the launching Activity. + */ + public void setExitSharedElementCallback(@Nullable SharedElementCallback listener) { + ActivityCompat.setExitSharedElementCallback(this, listener); + } + + /** + * Support library version of {@link android.app.Activity#postponeEnterTransition()} that works + * only on API 21 and later. + */ + public void supportPostponeEnterTransition() { + ActivityCompat.postponeEnterTransition(this); + } + + /** + * Support library version of {@link android.app.Activity#startPostponedEnterTransition()} + * that only works with API 21 and later. + */ + public void supportStartPostponedEnterTransition() { + ActivityCompat.startPostponedEnterTransition(this); + } + + /** + * {@inheritDoc} + * + *

Note: If you override this method you must call + * super.onMultiWindowModeChanged to correctly dispatch the event + * to support fragments attached to this activity.

+ * + * @param isInMultiWindowMode True if the activity is in multi-window mode. + */ + @Override + @CallSuper + public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { + mFragments.dispatchMultiWindowModeChanged(isInMultiWindowMode); + } + + /** + * {@inheritDoc} + * + *

Note: If you override this method you must call + * super.onPictureInPictureModeChanged to correctly dispatch the event + * to support fragments attached to this activity.

+ * + * @param isInPictureInPictureMode True if the activity is in picture-in-picture mode. + */ + @Override + @CallSuper + public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) { + mFragments.dispatchPictureInPictureModeChanged(isInPictureInPictureMode); + } + + /** + * Dispatch configuration change to all fragments. + */ + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mFragments.noteStateNotSaved(); + mFragments.dispatchConfigurationChanged(newConfig); + } + + /** + * Perform initialization of all fragments. + */ + @SuppressWarnings("deprecation") + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + mFragments.attachHost(null /*parent*/); + + if (savedInstanceState != null) { + Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); + mFragments.restoreSaveState(p); + + // Check if there are any pending onActivityResult calls to descendent Fragments. + if (savedInstanceState.containsKey(NEXT_CANDIDATE_REQUEST_INDEX_TAG)) { + mNextCandidateRequestIndex = + savedInstanceState.getInt(NEXT_CANDIDATE_REQUEST_INDEX_TAG); + int[] requestCodes = savedInstanceState.getIntArray(ALLOCATED_REQUEST_INDICIES_TAG); + String[] fragmentWhos = savedInstanceState.getStringArray(REQUEST_FRAGMENT_WHO_TAG); + if (requestCodes == null || fragmentWhos == null || + requestCodes.length != fragmentWhos.length) { + Log.w(TAG, "Invalid requestCode mapping in savedInstanceState."); + } else { + mPendingFragmentActivityResults = new SparseArrayCompat<>(requestCodes.length); + for (int i = 0; i < requestCodes.length; i++) { + mPendingFragmentActivityResults.put(requestCodes[i], fragmentWhos[i]); + } + } + } + } + + if (mPendingFragmentActivityResults == null) { + mPendingFragmentActivityResults = new SparseArrayCompat<>(); + mNextCandidateRequestIndex = 0; + } + + super.onCreate(savedInstanceState); + + mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); + mFragments.dispatchCreate(); + } + + /** + * Dispatch to Fragment.onCreateOptionsMenu(). + */ + @Override + public boolean onCreatePanelMenu(int featureId, @NonNull Menu menu) { + if (featureId == Window.FEATURE_OPTIONS_PANEL) { + boolean show = super.onCreatePanelMenu(featureId, menu); + show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater()); + return show; + } + return super.onCreatePanelMenu(featureId, menu); + } + + @Override + @Nullable + public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, + @NonNull AttributeSet attrs) { + final View v = dispatchFragmentsOnCreateView(parent, name, context, attrs); + if (v == null) { + return super.onCreateView(parent, name, context, attrs); + } + return v; + } + + @Override + @Nullable + public View onCreateView(@NonNull String name, @NonNull Context context, + @NonNull AttributeSet attrs) { + final View v = dispatchFragmentsOnCreateView(null, name, context, attrs); + if (v == null) { + return super.onCreateView(name, context, attrs); + } + return v; + } + + @Nullable + final View dispatchFragmentsOnCreateView(@Nullable View parent, @NonNull String name, + @NonNull Context context, @NonNull AttributeSet attrs) { + return mFragments.onCreateView(parent, name, context, attrs); + } + + /** + * Destroy all fragments. + */ + @Override + protected void onDestroy() { + super.onDestroy(); + mFragments.dispatchDestroy(); + mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); + } + + /** + * Dispatch onLowMemory() to all fragments. + */ + @Override + public void onLowMemory() { + super.onLowMemory(); + mFragments.dispatchLowMemory(); + } + + /** + * Dispatch context and options menu to fragments. + */ + @Override + public boolean onMenuItemSelected(int featureId, @NonNull MenuItem item) { + if (super.onMenuItemSelected(featureId, item)) { + return true; + } + + switch (featureId) { + case Window.FEATURE_OPTIONS_PANEL: + return mFragments.dispatchOptionsItemSelected(item); + + case Window.FEATURE_CONTEXT_MENU: + return mFragments.dispatchContextItemSelected(item); + + default: + return false; + } + } + + /** + * Call onOptionsMenuClosed() on fragments. + */ + @Override + public void onPanelClosed(int featureId, @NonNull Menu menu) { + switch (featureId) { + case Window.FEATURE_OPTIONS_PANEL: + mFragments.dispatchOptionsMenuClosed(menu); + break; + } + super.onPanelClosed(featureId, menu); + } + + /** + * Dispatch onPause() to fragments. + */ + @Override + protected void onPause() { + super.onPause(); + mResumed = false; + mFragments.dispatchPause(); + mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); + } + + /** + * Handle onNewIntent() to inform the fragment manager that the + * state is not saved. If you are handling new intents and may be + * making changes to the fragment state, you want to be sure to call + * through to the super-class here first. Otherwise, if your state + * is saved but the activity is not stopped, you could get an + * onNewIntent() call which happens before onResume() and trying to + * perform fragment operations at that point will throw IllegalStateException + * because the fragment manager thinks the state is still saved. + */ + @Override + @CallSuper + protected void onNewIntent(@SuppressLint("UnknownNullness") Intent intent) { + super.onNewIntent(intent); + mFragments.noteStateNotSaved(); + } + + /** + * Hook in to note that fragment state is no longer saved. + */ + @Override + public void onStateNotSaved() { + mFragments.noteStateNotSaved(); + } + + /** + * Dispatch onResume() to fragments. Note that for better inter-operation + * with older versions of the platform, at the point of this call the + * fragments attached to the activity are not resumed. + */ + @Override + protected void onResume() { + super.onResume(); + mResumed = true; + mFragments.noteStateNotSaved(); + mFragments.execPendingActions(); + } + + /** + * Dispatch onResume() to fragments. + */ + @Override + protected void onPostResume() { + super.onPostResume(); + onResumeFragments(); + } + + /** + * This is the fragment-orientated version of {@link #onResume()} that you + * can override to perform operations in the Activity at the same point + * where its fragments are resumed. Be sure to always call through to + * the super-class. + */ + protected void onResumeFragments() { + mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); + mFragments.dispatchResume(); + } + + /** + * Dispatch onPrepareOptionsMenu() to fragments. + */ + @Override + public boolean onPreparePanel(int featureId, @Nullable View view, @NonNull Menu menu) { + if (featureId == Window.FEATURE_OPTIONS_PANEL) { + boolean goforit = onPrepareOptionsPanel(view, menu); + goforit |= mFragments.dispatchPrepareOptionsMenu(menu); + return goforit; + } + return super.onPreparePanel(featureId, view, menu); + } + + /** + * @hide + * @deprecated Override {@link #onPreparePanel(int, View, Menu)}. + */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + @Deprecated + protected boolean onPrepareOptionsPanel(@Nullable View view, @NonNull Menu menu) { + return super.onPreparePanel(Window.FEATURE_OPTIONS_PANEL, view, menu); + } + + /** + * Save all appropriate fragment state. + */ + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + markFragmentsCreated(); + mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + Parcelable p = mFragments.saveAllState(); + if (p != null) { + outState.putParcelable(FRAGMENTS_TAG, p); + } + if (mPendingFragmentActivityResults.size() > 0) { + outState.putInt(NEXT_CANDIDATE_REQUEST_INDEX_TAG, mNextCandidateRequestIndex); + + int[] requestCodes = new int[mPendingFragmentActivityResults.size()]; + String[] fragmentWhos = new String[mPendingFragmentActivityResults.size()]; + for (int i = 0; i < mPendingFragmentActivityResults.size(); i++) { + requestCodes[i] = mPendingFragmentActivityResults.keyAt(i); + fragmentWhos[i] = mPendingFragmentActivityResults.valueAt(i); + } + outState.putIntArray(ALLOCATED_REQUEST_INDICIES_TAG, requestCodes); + outState.putStringArray(REQUEST_FRAGMENT_WHO_TAG, fragmentWhos); + } + } + + /** + * Dispatch onStart() to all fragments. + */ + @Override + protected void onStart() { + super.onStart(); + + mStopped = false; + + if (!mCreated) { + mCreated = true; + mFragments.dispatchActivityCreated(); + } + + mFragments.noteStateNotSaved(); + mFragments.execPendingActions(); + + // NOTE: HC onStart goes here. + + mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); + mFragments.dispatchStart(); + } + + /** + * Dispatch onStop() to all fragments. + */ + @Override + protected void onStop() { + super.onStop(); + + mStopped = true; + markFragmentsCreated(); + + mFragments.dispatchStop(); + mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); + } + + // ------------------------------------------------------------------------ + // NEW METHODS + // ------------------------------------------------------------------------ + + /** + * Support library version of {@link Activity#invalidateOptionsMenu}. + * + *

Invalidate the activity's options menu. This will cause relevant presentations + * of the menu to fully update via calls to onCreateOptionsMenu and + * onPrepareOptionsMenu the next time the menu is requested. + * + * @deprecated Call {@link Activity#invalidateOptionsMenu} directly. + */ + @Deprecated + public void supportInvalidateOptionsMenu() { + invalidateOptionsMenu(); + } + + /** + * Print the Activity's state into the given stream. This gets invoked if + * you run "adb shell dumpsys activity ". + * + * @param prefix Desired prefix to prepend at each line of output. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + */ + @Override + public void dump(@NonNull String prefix, @Nullable FileDescriptor fd, + @NonNull PrintWriter writer, @Nullable String[] args) { + super.dump(prefix, fd, writer, args); + writer.print(prefix); writer.print("Local FragmentActivity "); + writer.print(Integer.toHexString(System.identityHashCode(this))); + writer.println(" State:"); + String innerPrefix = prefix + " "; + writer.print(innerPrefix); writer.print("mCreated="); + writer.print(mCreated); writer.print(" mResumed="); + writer.print(mResumed); writer.print(" mStopped="); + writer.print(mStopped); + + if (getApplication() != null) { + LoaderManager.getInstance(this).dump(innerPrefix, fd, writer, args); + } + mFragments.getSupportFragmentManager().dump(prefix, fd, writer, args); + } + + // ------------------------------------------------------------------------ + // FRAGMENT SUPPORT + // ------------------------------------------------------------------------ + + /** + * Called when a fragment is attached to the activity. + * + *

This is called after the attached fragment's onAttach and before + * the attached fragment's onCreate if the fragment has not yet had a previous + * call to onCreate.

+ */ + @SuppressWarnings("unused") + public void onAttachFragment(@NonNull Fragment fragment) { + } + + /** + * Return the FragmentManager for interacting with fragments associated + * with this activity. + */ + @NonNull + public FragmentManager getSupportFragmentManager() { + return mFragments.getSupportFragmentManager(); + } + + /** + * @deprecated Use + * {@link LoaderManager#getInstance(LifecycleOwner) LoaderManager.getInstance(this)}. + */ + @Deprecated + @NonNull + public LoaderManager getSupportLoaderManager() { + return LoaderManager.getInstance(this); + } + + /** + * Modifies the standard behavior to allow results to be delivered to fragments. + * This imposes a restriction that requestCode be <= 0xffff. + */ + @Override + public void startActivityForResult(@SuppressLint("UnknownNullness") Intent intent, + int requestCode) { + // If this was started from a Fragment we've already checked the upper 16 bits were not in + // use, and then repurposed them for the Fragment's index. + if (!mStartedActivityFromFragment) { + if (requestCode != -1) { + checkForValidRequestCode(requestCode); + } + } + super.startActivityForResult(intent, requestCode); + } + + @Override + public void startActivityForResult(@SuppressLint("UnknownNullness") Intent intent, + int requestCode, @Nullable Bundle options) { + // If this was started from a Fragment we've already checked the upper 16 bits were not in + // use, and then repurposed them for the Fragment's index. + if (!mStartedActivityFromFragment) { + if (requestCode != -1) { + checkForValidRequestCode(requestCode); + } + } + super.startActivityForResult(intent, requestCode, options); + } + + @Override + public void startIntentSenderForResult(@SuppressLint("UnknownNullness") IntentSender intent, + int requestCode, @Nullable Intent fillInIntent, int flagsMask, + int flagsValues, int extraFlags) throws IntentSender.SendIntentException { + // If this was started from a Fragment we've already checked the upper 16 bits were not in + // use, and then repurposed them for the Fragment's index. + if (!mStartedIntentSenderFromFragment) { + if (requestCode != -1) { + checkForValidRequestCode(requestCode); + } + } + super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, + extraFlags); + } + + @Override + public void startIntentSenderForResult(@SuppressLint("UnknownNullness") IntentSender intent, + int requestCode, @Nullable Intent fillInIntent, int flagsMask, int flagsValues, + int extraFlags, @Nullable Bundle options) throws IntentSender.SendIntentException { + // If this was started from a Fragment we've already checked the upper 16 bits were not in + // use, and then repurposed them for the Fragment's index. + if (!mStartedIntentSenderFromFragment) { + if (requestCode != -1) { + checkForValidRequestCode(requestCode); + } + } + super.startIntentSenderForResult(intent, requestCode, fillInIntent, flagsMask, flagsValues, + extraFlags, options); + } + + /** + * Checks whether the given request code is a valid code by masking it with 0xffff0000. Throws + * an {@link IllegalArgumentException} if the code is not valid. + */ + static void checkForValidRequestCode(int requestCode) { + if ((requestCode & 0xffff0000) != 0) { + throw new IllegalArgumentException("Can only use lower 16 bits for requestCode"); + } + } + + @Override + public final void validateRequestPermissionsRequestCode(int requestCode) { + // We use 16 bits of the request code to encode the fragment id when + // requesting permissions from a fragment. Hence, requestPermissions() + // should validate the code against that but we cannot override it as + // we can not then call super and also the ActivityCompat would call + // back to this override. To handle this we use dependency inversion + // where we are the validator of request codes when requesting + // permissions in ActivityCompat. + if (!mRequestedPermissionsFromFragment + && requestCode != -1) { + checkForValidRequestCode(requestCode); + } + } + + /** + * Callback for the result from requesting permissions. This method + * is invoked for every call on {@link #requestPermissions(String[], int)}. + *

+ * Note: It is possible that the permissions request interaction + * with the user is interrupted. In this case you will receive empty permissions + * and results arrays which should be treated as a cancellation. + *

+ * + * @param requestCode The request code passed in {@link #requestPermissions(String[], int)}. + * @param permissions The requested permissions. Never null. + * @param grantResults The grant results for the corresponding permissions + * which is either {@link android.content.pm.PackageManager#PERMISSION_GRANTED} + * or {@link android.content.pm.PackageManager#PERMISSION_DENIED}. Never null. + * + * @see #requestPermissions(String[], int) + */ + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + mFragments.noteStateNotSaved(); + int index = (requestCode >> 16) & 0xffff; + if (index != 0) { + index--; + + String who = mPendingFragmentActivityResults.get(index); + mPendingFragmentActivityResults.remove(index); + if (who == null) { + Log.w(TAG, "Activity result delivered for unknown Fragment."); + return; + } + Fragment frag = mFragments.findFragmentByWho(who); + if (frag == null) { + Log.w(TAG, "Activity result no fragment exists for who: " + who); + } else { + frag.onRequestPermissionsResult(requestCode & 0xffff, permissions, grantResults); + } + } + } + + /** + * Called by Fragment.startActivityForResult() to implement its behavior. + */ + public void startActivityFromFragment(@NonNull Fragment fragment, + @SuppressLint("UnknownNullness") Intent intent, int requestCode) { + startActivityFromFragment(fragment, intent, requestCode, null); + } + + /** + * Called by Fragment.startActivityForResult() to implement its behavior. + */ + public void startActivityFromFragment(@NonNull Fragment fragment, + @SuppressLint("UnknownNullness") Intent intent, int requestCode, + @Nullable Bundle options) { + mStartedActivityFromFragment = true; + try { + if (requestCode == -1) { + ActivityCompat.startActivityForResult(this, intent, -1, options); + return; + } + checkForValidRequestCode(requestCode); + int requestIndex = allocateRequestIndex(fragment); + ActivityCompat.startActivityForResult( + this, intent, ((requestIndex + 1) << 16) + (requestCode & 0xffff), options); + } finally { + mStartedActivityFromFragment = false; + } + } + + /** + * Called by Fragment.startIntentSenderForResult() to implement its behavior. + */ + public void startIntentSenderFromFragment(@NonNull Fragment fragment, + @SuppressLint("UnknownNullness") IntentSender intent, int requestCode, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + @Nullable Bundle options) throws IntentSender.SendIntentException { + mStartedIntentSenderFromFragment = true; + try { + if (requestCode == -1) { + ActivityCompat.startIntentSenderForResult(this, intent, requestCode, fillInIntent, + flagsMask, flagsValues, extraFlags, options); + return; + } + checkForValidRequestCode(requestCode); + int requestIndex = allocateRequestIndex(fragment); + ActivityCompat.startIntentSenderForResult(this, intent, + ((requestIndex + 1) << 16) + (requestCode & 0xffff), fillInIntent, + flagsMask, flagsValues, extraFlags, options); + } finally { + mStartedIntentSenderFromFragment = false; + } + } + + // Allocates the next available startActivityForResult request index. + private int allocateRequestIndex(@NonNull Fragment fragment) { + // Sanity check that we havn't exhaused the request index space. + if (mPendingFragmentActivityResults.size() >= MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS) { + throw new IllegalStateException("Too many pending Fragment activity results."); + } + + // Find an unallocated request index in the mPendingFragmentActivityResults map. + while (mPendingFragmentActivityResults.indexOfKey(mNextCandidateRequestIndex) >= 0) { + mNextCandidateRequestIndex = + (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS; + } + + int requestIndex = mNextCandidateRequestIndex; + mPendingFragmentActivityResults.put(requestIndex, fragment.mWho); + mNextCandidateRequestIndex = + (mNextCandidateRequestIndex + 1) % MAX_NUM_PENDING_FRAGMENT_ACTIVITY_RESULTS; + + return requestIndex; + } + + /** + * Called by Fragment.requestPermissions() to implement its behavior. + */ + void requestPermissionsFromFragment(@NonNull Fragment fragment, @NonNull String[] permissions, + int requestCode) { + if (requestCode == -1) { + ActivityCompat.requestPermissions(this, permissions, requestCode); + return; + } + checkForValidRequestCode(requestCode); + try { + mRequestedPermissionsFromFragment = true; + int requestIndex = allocateRequestIndex(fragment); + ActivityCompat.requestPermissions(this, permissions, + ((requestIndex + 1) << 16) + (requestCode & 0xffff)); + } finally { + mRequestedPermissionsFromFragment = false; + } + } + + class HostCallbacks extends FragmentHostCallback implements + ViewModelStoreOwner, + OnBackPressedDispatcherOwner { + public HostCallbacks() { + super(FragmentActivity.this /*fragmentActivity*/); + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + // Instead of directly using the Activity's Lifecycle, we + // use a LifecycleRegistry that is nested exactly outside of + // when Fragments get their lifecycle changed + // TODO(b/127528777) Drive Fragment Lifecycle with LifecycleObserver + return mFragmentLifecycleRegistry; + } + + @NonNull + @Override + public ViewModelStore getViewModelStore() { + return FragmentActivity.this.getViewModelStore(); + } + + @NonNull + @Override + public OnBackPressedDispatcher getOnBackPressedDispatcher() { + return FragmentActivity.this.getOnBackPressedDispatcher(); + } + + @Override + public void onDump(@NonNull String prefix, @Nullable FileDescriptor fd, + @NonNull PrintWriter writer, @Nullable String[] args) { + FragmentActivity.this.dump(prefix, fd, writer, args); + } + + @Override + public boolean onShouldSaveFragmentState(@NonNull Fragment fragment) { + return !isFinishing(); + } + + @Override + @NonNull + public LayoutInflater onGetLayoutInflater() { + return FragmentActivity.this.getLayoutInflater().cloneInContext(FragmentActivity.this); + } + + @Override + public FragmentActivity onGetHost() { + return FragmentActivity.this; + } + + @Override + public void onSupportInvalidateOptionsMenu() { + FragmentActivity.this.supportInvalidateOptionsMenu(); + } + + @Override + public void onStartActivityFromFragment(@NonNull Fragment fragment, Intent intent, + int requestCode) { + FragmentActivity.this.startActivityFromFragment(fragment, intent, requestCode); + } + + @Override + public void onStartActivityFromFragment(@NonNull Fragment fragment, Intent intent, + int requestCode, @Nullable Bundle options) { + FragmentActivity.this.startActivityFromFragment(fragment, intent, requestCode, options); + } + + @Override + public void onStartIntentSenderFromFragment( + @NonNull Fragment fragment, IntentSender intent, int requestCode, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, + int extraFlags, Bundle options) throws IntentSender.SendIntentException { + FragmentActivity.this.startIntentSenderFromFragment(fragment, intent, requestCode, + fillInIntent, flagsMask, flagsValues, extraFlags, options); + } + + @Override + public void onRequestPermissionsFromFragment(@NonNull Fragment fragment, + @NonNull String[] permissions, int requestCode) { + FragmentActivity.this.requestPermissionsFromFragment(fragment, permissions, + requestCode); + } + + @Override + public boolean onShouldShowRequestPermissionRationale(@NonNull String permission) { + return ActivityCompat.shouldShowRequestPermissionRationale( + FragmentActivity.this, permission); + } + + @Override + public boolean onHasWindowAnimations() { + return getWindow() != null; + } + + @Override + public int onGetWindowAnimations() { + final Window w = getWindow(); + return (w == null) ? 0 : w.getAttributes().windowAnimations; + } + + @Override + public void onAttachFragment(@NonNull Fragment fragment) { + FragmentActivity.this.onAttachFragment(fragment); + } + + @Nullable + @Override + public View onFindViewById(int id) { + return FragmentActivity.this.findViewById(id); + } + + @Override + public boolean onHasView() { + final Window w = getWindow(); + return (w != null && w.peekDecorView() != null); + } + } + + private void markFragmentsCreated() { + boolean reiterate; + do { + reiterate = markState(getSupportFragmentManager(), Lifecycle.State.CREATED); + } while (reiterate); + } + + private static boolean markState(FragmentManager manager, Lifecycle.State state) { + boolean hadNotMarked = false; + Collection fragments = manager.getFragments(); + for (Fragment fragment : fragments) { + if (fragment == null) { + continue; + } + if (fragment.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { + fragment.mLifecycleRegistry.setCurrentState(state); + hadNotMarked = true; + } + + if (fragment.getHost() != null) { + FragmentManager childFragmentManager = fragment.getChildFragmentManager(); + hadNotMarked |= markState(childFragmentManager, state); + } + } + return hadNotMarked; + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentContainer.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentContainer.java new file mode 100644 index 000000000..2afb8f76e --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentContainer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.content.Context; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + + +/** + * Callbacks to a {@link Fragment}'s container. + */ +public abstract class FragmentContainer { + /** + * Return the view with the given resource ID. May return {@code null} if the + * view is not a child of this container. + */ + @Nullable + public abstract View onFindViewById(@IdRes int id); + + /** + * Return {@code true} if the container holds any view. + */ + public abstract boolean onHasView(); + + + /** + * Creates an instance of the specified fragment, can be overridden to construct fragments + * with dependencies, or change the fragment being constructed. By default just calls + * {@link Fragment#instantiate(Context, String, Bundle)}. + * @deprecated Use {@link FragmentManager#setFragmentFactory} to control how Fragments are + * instantiated. + */ + @SuppressWarnings("deprecation") + @Deprecated + @NonNull + public Fragment instantiate(@NonNull Context context, @NonNull String className, + @Nullable Bundle arguments) { + return Fragment.instantiate(context, className, arguments); + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentController.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentController.java new file mode 100644 index 000000000..5b5e07f76 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentController.java @@ -0,0 +1,532 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import static androidx.core.util.Preconditions.checkNotNull; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.SimpleArrayMap; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.loader.app.LoaderManager; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Provides integration points with a {@link FragmentManager} for a fragment host. + *

+ * It is the responsibility of the host to take care of the Fragment's lifecycle. + * The methods provided by {@link FragmentController} are for that purpose. + */ +public class FragmentController { + private final FragmentHostCallback mHost; + + /** + * Returns a {@link FragmentController}. + */ + @NonNull + public static FragmentController createController(@NonNull FragmentHostCallback callbacks) { + return new FragmentController(checkNotNull(callbacks, "callbacks == null")); + } + + private FragmentController(FragmentHostCallback callbacks) { + mHost = callbacks; + } + + /** + * Returns a {@link FragmentManager} for this controller. + */ + @NonNull + public FragmentManager getSupportFragmentManager() { + return mHost.mFragmentManager; + } + + /** + * Returns a {@link LoaderManager}. + * + * @deprecated Loaders are managed separately from FragmentController and this now throws an + * {@link UnsupportedOperationException}. Use {@link LoaderManager#getInstance} to obtain a + * LoaderManager. + * @see LoaderManager#getInstance + */ + @Deprecated + @SuppressLint("UnknownNullness") + public LoaderManager getSupportLoaderManager() { + throw new UnsupportedOperationException("Loaders are managed separately from " + + "FragmentController, use LoaderManager.getInstance() to obtain a LoaderManager."); + } + + /** + * Returns a fragment with the given identifier. + */ + @Nullable + public Fragment findFragmentByWho(@NonNull String who) { + return mHost.mFragmentManager.findFragmentByWho(who); + } + + /** + * Returns the number of active fragments. + */ + public int getActiveFragmentsCount() { + return mHost.mFragmentManager.getActiveFragmentCount(); + } + + /** + * Returns the list of active fragments. + */ + @NonNull + public List getActiveFragments(@SuppressLint("UnknownNullness") + List actives) { + return mHost.mFragmentManager.getActiveFragments(); + } + + /** + * Attaches the host to the FragmentManager for this controller. The host must be + * attached before the FragmentManager can be used to manage Fragments. + */ + public void attachHost(@Nullable Fragment parent) { + mHost.mFragmentManager.attachController( + mHost, mHost /*container*/, parent); + } + + /** + * Instantiates a Fragment's view. + * + * @param parent The parent that the created view will be placed + * in; note that this may be null. + * @param name Tag name to be inflated. + * @param context The context the view is being created in. + * @param attrs Inflation attributes as specified in XML file. + * + * @return view the newly created view + */ + @Nullable + public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, + @NonNull AttributeSet attrs) { + return mHost.mFragmentManager.onCreateView(parent, name, context, attrs); + } + + /** + * Marks the fragment state as unsaved. This allows for "state loss" detection. + */ + public void noteStateNotSaved() { + mHost.mFragmentManager.noteStateNotSaved(); + } + + /** + * Saves the state for all Fragments. + * + * @see #restoreSaveState(Parcelable) + */ + @Nullable + public Parcelable saveAllState() { + return mHost.mFragmentManager.saveAllState(); + } + + /** + * Restores the saved state for all Fragments. The given Fragment list are Fragment + * instances retained across configuration changes. + * + * @deprecated Have your {@link FragmentHostCallback} implement {@link ViewModelStoreOwner} + * to automatically restore the Fragment's non configuration state and use + * {@link #restoreSaveState(Parcelable)} to restore the Fragment's save state. + */ + @Deprecated + public void restoreAllState(@Nullable Parcelable state, + @Nullable List nonConfigList) { + mHost.mFragmentManager.restoreAllState(state, + new FragmentManagerNonConfig(nonConfigList, null, null)); + } + + /** + * Restores the saved state for all Fragments. The given FragmentManagerNonConfig are Fragment + * instances retained across configuration changes, including nested fragments + * + * @deprecated Have your {@link FragmentHostCallback} implement {@link ViewModelStoreOwner} + * to automatically restore the Fragment's non configuration state and use + * {@link #restoreSaveState(Parcelable)} to restore the Fragment's save state. + */ + @Deprecated + public void restoreAllState(@Nullable Parcelable state, + @Nullable FragmentManagerNonConfig nonConfig) { + mHost.mFragmentManager.restoreAllState(state, nonConfig); + } + + /** + * Restores the saved state for all Fragments. + * + * @param state the saved state containing the Parcelable returned by {@link #saveAllState()} + * @see #saveAllState() + */ + public void restoreSaveState(@Nullable Parcelable state) { + if (!(mHost instanceof ViewModelStoreOwner)) { + throw new IllegalStateException("Your FragmentHostCallback must implement " + + "ViewModelStoreOwner to call restoreSaveState(). Call restoreAllState() " + + " if you're still using retainNestedNonConfig()."); + } + mHost.mFragmentManager.restoreSaveState(state); + } + + /** + * Returns a list of Fragments that have opted to retain their instance across + * configuration changes. + * + * @deprecated Have your {@link FragmentHostCallback} implement {@link ViewModelStoreOwner} + * to automatically retain the Fragment's non configuration state. + */ + @Deprecated + @Nullable + public List retainNonConfig() { + FragmentManagerNonConfig nonconf = mHost.mFragmentManager.retainNonConfig(); + return nonconf != null && nonconf.getFragments() != null + ? new ArrayList<>(nonconf.getFragments()) + : null; + } + + /** + * Returns a nested tree of Fragments that have opted to retain their instance across + * configuration changes. + * + * @deprecated Have your {@link FragmentHostCallback} implement {@link ViewModelStoreOwner} + * to automatically retain the Fragment's non configuration state. + */ + @Deprecated + @Nullable + public FragmentManagerNonConfig retainNestedNonConfig() { + return mHost.mFragmentManager.retainNonConfig(); + } + + /** + * Moves all Fragments managed by the controller's FragmentManager + * into the create state. + *

Call when Fragments should be created. + * + * @see Fragment#onCreate(Bundle) + */ + public void dispatchCreate() { + mHost.mFragmentManager.dispatchCreate(); + } + + /** + * Moves all Fragments managed by the controller's FragmentManager + * into the activity created state. + *

Call when Fragments should be informed their host has been created. + * + * @see Fragment#onActivityCreated(Bundle) + */ + public void dispatchActivityCreated() { + mHost.mFragmentManager.dispatchActivityCreated(); + } + + /** + * Moves all Fragments managed by the controller's FragmentManager + * into the start state. + *

Call when Fragments should be started. + * + * @see Fragment#onStart() + */ + public void dispatchStart() { + mHost.mFragmentManager.dispatchStart(); + } + + /** + * Moves all Fragments managed by the controller's FragmentManager + * into the resume state. + *

Call when Fragments should be resumed. + * + * @see Fragment#onResume() + */ + public void dispatchResume() { + mHost.mFragmentManager.dispatchResume(); + } + + /** + * Moves all Fragments managed by the controller's FragmentManager + * into the pause state. + *

Call when Fragments should be paused. + * + * @see Fragment#onPause() + */ + public void dispatchPause() { + mHost.mFragmentManager.dispatchPause(); + } + + /** + * Moves all Fragments managed by the controller's FragmentManager + * into the stop state. + *

Call when Fragments should be stopped. + * + * @see Fragment#onStop() + */ + public void dispatchStop() { + mHost.mFragmentManager.dispatchStop(); + } + + /** + * @deprecated This functionality has been rolled into {@link #dispatchStop()}. + */ + @Deprecated + public void dispatchReallyStop() { + } + + /** + * Moves all Fragments managed by the controller's FragmentManager + * into the destroy view state. + *

Call when the Fragment's views should be destroyed. + * + * @see Fragment#onDestroyView() + */ + public void dispatchDestroyView() { + mHost.mFragmentManager.dispatchDestroyView(); + } + + /** + * Moves Fragments managed by the controller's FragmentManager + * into the destroy state. + *

+ * If the {@link androidx.fragment.app.FragmentHostCallback} is an instance of {@link ViewModelStoreOwner}, + * then retained Fragments and any other non configuration state such as any + * {@link androidx.lifecycle.ViewModel} attached to Fragments will only be destroyed if + * {@link androidx.lifecycle.ViewModelStore#clear()} is called prior to this method. + *

+ * Otherwise, the FragmentManager will look to see if the + * {@link FragmentHostCallback host's} Context is an {@link Activity} + * and if {@link Activity#isChangingConfigurations()} returns true. In only that case + * will non configuration state be retained. + *

Call when Fragments should be destroyed. + * + * @see Fragment#onDestroy() + */ + public void dispatchDestroy() { + mHost.mFragmentManager.dispatchDestroy(); + } + + /** + * Lets all Fragments managed by the controller's FragmentManager know the multi-window mode of + * the activity changed. + *

Call when the multi-window mode of the activity changed. + * + * @see Fragment#onMultiWindowModeChanged + */ + public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode) { + mHost.mFragmentManager.dispatchMultiWindowModeChanged(isInMultiWindowMode); + } + + /** + * Lets all Fragments managed by the controller's FragmentManager know the picture-in-picture + * mode of the activity changed. + *

Call when the picture-in-picture mode of the activity changed. + * + * @see Fragment#onPictureInPictureModeChanged + */ + public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode) { + mHost.mFragmentManager.dispatchPictureInPictureModeChanged(isInPictureInPictureMode); + } + + /** + * Lets all Fragments managed by the controller's FragmentManager + * know a configuration change occurred. + *

Call when there is a configuration change. + * + * @see Fragment#onConfigurationChanged(Configuration) + */ + public void dispatchConfigurationChanged(@NonNull Configuration newConfig) { + mHost.mFragmentManager.dispatchConfigurationChanged(newConfig); + } + + /** + * Lets all Fragments managed by the controller's FragmentManager + * know the device is in a low memory condition. + *

Call when the device is low on memory and Fragment's should trim + * their memory usage. + * + * @see Fragment#onLowMemory() + */ + public void dispatchLowMemory() { + mHost.mFragmentManager.dispatchLowMemory(); + } + + /** + * Lets all Fragments managed by the controller's FragmentManager + * know they should create an options menu. + *

Call when the Fragment should create an options menu. + * + * @return {@code true} if the options menu contains items to display + * @see Fragment#onCreateOptionsMenu(Menu, MenuInflater) + */ + public boolean dispatchCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + return mHost.mFragmentManager.dispatchCreateOptionsMenu(menu, inflater); + } + + /** + * Lets all Fragments managed by the controller's FragmentManager + * know they should prepare their options menu for display. + *

Call immediately before displaying the Fragment's options menu. + * + * @return {@code true} if the options menu contains items to display + * @see Fragment#onPrepareOptionsMenu(Menu) + */ + public boolean dispatchPrepareOptionsMenu(@NonNull Menu menu) { + return mHost.mFragmentManager.dispatchPrepareOptionsMenu(menu); + } + + /** + * Sends an option item selection event to the Fragments managed by the + * controller's FragmentManager. Once the event has been consumed, + * no additional handling will be performed. + *

Call immediately after an options menu item has been selected + * + * @return {@code true} if the options menu selection event was consumed + * @see Fragment#onOptionsItemSelected(MenuItem) + */ + public boolean dispatchOptionsItemSelected(@NonNull MenuItem item) { + return mHost.mFragmentManager.dispatchOptionsItemSelected(item); + } + + /** + * Sends a context item selection event to the Fragments managed by the + * controller's FragmentManager. Once the event has been consumed, + * no additional handling will be performed. + *

Call immediately after an options menu item has been selected + * + * @return {@code true} if the context menu selection event was consumed + * @see Fragment#onContextItemSelected(MenuItem) + */ + public boolean dispatchContextItemSelected(@NonNull MenuItem item) { + return mHost.mFragmentManager.dispatchContextItemSelected(item); + } + + /** + * Lets all Fragments managed by the controller's FragmentManager + * know their options menu has closed. + *

Call immediately after closing the Fragment's options menu. + * + * @see Fragment#onOptionsMenuClosed(Menu) + */ + public void dispatchOptionsMenuClosed(@NonNull Menu menu) { + mHost.mFragmentManager.dispatchOptionsMenuClosed(menu); + } + + /** + * Execute any pending actions for the Fragments managed by the + * controller's FragmentManager. + *

Call when queued actions can be performed [eg when the + * Fragment moves into a start or resume state]. + * @return {@code true} if queued actions were performed + */ + public boolean execPendingActions() { + return mHost.mFragmentManager.execPendingActions(); + } + + /** + * Starts the loaders. + * + * @deprecated Loaders are managed separately from FragmentController + */ + @Deprecated + public void doLoaderStart() { + } + + /** + * Stops the loaders, optionally retaining their state. This is useful for keeping the + * loader state across configuration changes. + * + * @param retain When {@code true}, the loaders aren't stopped, but, their instances + * are retained in a started state + * + * @deprecated Loaders are managed separately from FragmentController + */ + @Deprecated + public void doLoaderStop(boolean retain) { + } + + /** + * Retains the state of each of the loaders. + * + * @deprecated Loaders are managed separately from FragmentController + */ + @Deprecated + public void doLoaderRetain() { + } + + /** + * Destroys the loaders and, if their state is not being retained, removes them. + * + * @deprecated Loaders are managed separately from FragmentController + */ + @Deprecated + public void doLoaderDestroy() { + } + + /** + * Lets the loaders know the host is ready to receive notifications. + * + * @deprecated Loaders are managed separately from FragmentController + */ + @Deprecated + public void reportLoaderStart() { + } + + /** + * Returns a list of LoaderManagers that have opted to retain their instance across + * configuration changes. + * + * @deprecated Loaders are managed separately from FragmentController + */ + @Deprecated + @Nullable + public SimpleArrayMap retainLoaderNonConfig() { + return null; + } + + /** + * Restores the saved state for all LoaderManagers. The given LoaderManager list are + * LoaderManager instances retained across configuration changes. + * + * @see #retainLoaderNonConfig() + * + * @deprecated Loaders are managed separately from FragmentController + */ + @Deprecated + public void restoreLoaderNonConfig(@SuppressLint("UnknownNullness") + SimpleArrayMap loaderManagers) { + } + + /** + * Dumps the current state of the loaders. + * + * @deprecated Loaders are managed separately from FragmentController + */ + @Deprecated + public void dumpLoaders(@NonNull String prefix, @Nullable FileDescriptor fd, + @NonNull PrintWriter writer, @Nullable String[] args) { + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentFactory.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentFactory.java new file mode 100644 index 000000000..8214fc96b --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentFactory.java @@ -0,0 +1,132 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import androidx.annotation.NonNull; +import androidx.collection.SimpleArrayMap; + +import java.lang.reflect.InvocationTargetException; + +/** + * Interface used to control the instantiation of {@link Fragment} instances. + * Implementations can be registered with a {@link FragmentManager} via + * {@link FragmentManager#setFragmentFactory(FragmentFactory)}. + * + * @see FragmentManager#setFragmentFactory(FragmentFactory) + */ +public class FragmentFactory { + private static final SimpleArrayMap> sClassMap = new SimpleArrayMap<>(); + + /** + * Determine if the given fragment name is a support library fragment class. + * + * @param classLoader The default classloader to use for loading the Class + * @param className Class name of the fragment to load + * @return Returns the parsed Class + */ + @NonNull + private static Class loadClass(@NonNull ClassLoader classLoader, + @NonNull String className) throws ClassNotFoundException { + Class clazz = sClassMap.get(className); + if (clazz == null) { + // Class not found in the cache, see if it's real, and try to add it + clazz = Class.forName(className, false, classLoader); + sClassMap.put(className, clazz); + } + return clazz; + } + + /** + * Determine if the given fragment name is a valid Fragment class. + * + * @param classLoader The default classloader to use for loading the Class + * @param className Class name of the fragment to test + * @return true if className is androidx.fragment.app.Fragment + * or a subclass, false otherwise. + */ + static boolean isFragmentClass(@NonNull ClassLoader classLoader, + @NonNull String className) { + try { + Class clazz = loadClass(classLoader, className); + return Fragment.class.isAssignableFrom(clazz); + } catch (ClassNotFoundException e) { + return false; + } + } + + /** + * Parse a Fragment Class from the given class name. The resulting Class is kept in a global + * cache, bypassing the {@link Class#forName(String)} calls when passed the same + * class name again. + * + * @param classLoader The default classloader to use for loading the Class + * @param className The class name of the fragment to parse. + * @return Returns the parsed Fragment Class + * @throws Fragment.InstantiationException If there is a failure in parsing + * the given fragment class. This is a runtime exception; it is not + * normally expected to happen. + */ + @SuppressWarnings("unchecked") + @NonNull + public static Class loadFragmentClass(@NonNull ClassLoader classLoader, + @NonNull String className) { + try { + Class clazz = loadClass(classLoader, className); + return (Class) clazz; + } catch (ClassNotFoundException e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + className + + ": make sure class name exists", e); + } catch (ClassCastException e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + className + + ": make sure class is a valid subclass of Fragment", e); + } + } + + /** + * Create a new instance of a Fragment with the given class name. This uses + * {@link #loadFragmentClass(ClassLoader, String)} and the empty + * constructor of the resulting Class by default. + * + * @param classLoader The default classloader to use for instantiation + * @param className The class name of the fragment to instantiate. + * @return Returns a new fragment instance. + * @throws Fragment.InstantiationException If there is a failure in instantiating + * the given fragment class. This is a runtime exception; it is not + * normally expected to happen. + */ + @NonNull + public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) { + try { + Class cls = loadFragmentClass(classLoader, className); + return cls.getConstructor().newInstance(); + } catch (java.lang.InstantiationException e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + className + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (IllegalAccessException e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + className + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (NoSuchMethodException e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + className + + ": could not find Fragment constructor", e); + } catch (InvocationTargetException e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + className + + ": calling Fragment constructor caused an exception", e); + } + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentHostCallback.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentHostCallback.java new file mode 100644 index 000000000..f9d820f08 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentHostCallback.java @@ -0,0 +1,210 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; +import androidx.core.util.Preconditions; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Integration points with the Fragment host. + *

+ * Fragments may be hosted by any object; such as an {@link Activity}. In order to + * host fragments, implement {@link FragmentHostCallback}, overriding the methods + * applicable to the host. + */ +public abstract class FragmentHostCallback extends FragmentContainer { + @Nullable private final Activity mActivity; + @NonNull private final Context mContext; + @NonNull private final Handler mHandler; + private final int mWindowAnimations; + final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl(); + + public FragmentHostCallback(@NonNull Context context, @NonNull Handler handler, + int windowAnimations) { + this(context instanceof Activity ? (Activity) context : null, context, handler, + windowAnimations); + } + + FragmentHostCallback(@NonNull FragmentActivity activity) { + this(activity, activity /*context*/, new Handler(), 0 /*windowAnimations*/); + } + + FragmentHostCallback(@Nullable Activity activity, @NonNull Context context, + @NonNull Handler handler, int windowAnimations) { + mActivity = activity; + mContext = Preconditions.checkNotNull(context, "context == null"); + mHandler = Preconditions.checkNotNull(handler, "handler == null"); + mWindowAnimations = windowAnimations; + } + + /** + * Print internal state into the given stream. + * + * @param prefix Desired prefix to prepend at each line of output. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be closed + * for you after you return. + * @param args additional arguments to the dump request. + */ + public void onDump(@NonNull String prefix, @Nullable FileDescriptor fd, + @NonNull PrintWriter writer, @Nullable String[] args) { + } + + /** + * Return {@code true} if the fragment's state needs to be saved. + */ + public boolean onShouldSaveFragmentState(@NonNull Fragment fragment) { + return true; + } + + /** + * Return a {@link LayoutInflater}. + * See {@link Activity#getLayoutInflater()}. + */ + @NonNull + public LayoutInflater onGetLayoutInflater() { + return LayoutInflater.from(mContext); + } + + /** + * Return the object that's currently hosting the fragment. If a {@link Fragment} + * is hosted by a {@link FragmentActivity}, the object returned here should be + * the same object returned from {@link Fragment#getActivity()}. + */ + @Nullable + public abstract E onGetHost(); + + /** + * Invalidates the activity's options menu. + * See {@link FragmentActivity#supportInvalidateOptionsMenu()} + */ + public void onSupportInvalidateOptionsMenu() { + } + + /** + * Starts a new {@link Activity} from the given fragment. + * See {@link FragmentActivity#startActivityForResult(Intent, int)}. + */ + public void onStartActivityFromFragment(@NonNull Fragment fragment, + @SuppressLint("UnknownNullness") Intent intent, int requestCode) { + onStartActivityFromFragment(fragment, intent, requestCode, null); + } + + /** + * Starts a new {@link Activity} from the given fragment. + * See {@link FragmentActivity#startActivityForResult(Intent, int, Bundle)}. + */ + public void onStartActivityFromFragment( + @NonNull Fragment fragment, @SuppressLint("UnknownNullness") Intent intent, + int requestCode, @Nullable Bundle options) { + if (requestCode != -1) { + throw new IllegalStateException( + "Starting activity with a requestCode requires a FragmentActivity host"); + } + mContext.startActivity(intent); + } + + /** + * Starts a new {@link IntentSender} from the given fragment. + * See {@link Activity#startIntentSender(IntentSender, Intent, int, int, int, Bundle)}. + */ + public void onStartIntentSenderFromFragment(@NonNull Fragment fragment, + @SuppressLint("UnknownNullness") IntentSender intent, int requestCode, + @Nullable Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + @Nullable Bundle options) throws IntentSender.SendIntentException { + if (requestCode != -1) { + throw new IllegalStateException( + "Starting intent sender with a requestCode requires a FragmentActivity host"); + } + ActivityCompat.startIntentSenderForResult(mActivity, intent, requestCode, fillInIntent, + flagsMask, flagsValues, extraFlags, options); + } + + /** + * Requests permissions from the given fragment. + * See {@link FragmentActivity#requestPermissions(String[], int)} + */ + public void onRequestPermissionsFromFragment(@NonNull Fragment fragment, + @NonNull String[] permissions, int requestCode) { + } + + /** + * Checks whether to show permission rationale UI from a fragment. + * See {@link FragmentActivity#shouldShowRequestPermissionRationale(String)} + */ + public boolean onShouldShowRequestPermissionRationale(@NonNull String permission) { + return false; + } + + /** + * Return {@code true} if there are window animations. + */ + public boolean onHasWindowAnimations() { + return true; + } + + /** + * Return the window animations. + */ + public int onGetWindowAnimations() { + return mWindowAnimations; + } + + @Nullable + @Override + public View onFindViewById(int id) { + return null; + } + + @Override + public boolean onHasView() { + return true; + } + + @Nullable + Activity getActivity() { + return mActivity; + } + + @NonNull + Context getContext() { + return mContext; + } + + @NonNull + Handler getHandler() { + return mHandler; + } + + void onAttachFragment(@NonNull Fragment fragment) { + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManager.java new file mode 100644 index 000000000..446002c06 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManager.java @@ -0,0 +1,601 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.StringRes; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.List; + +/** + * Static library support version of the framework's {@link android.app.FragmentManager}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework {@link FragmentManager} + * documentation for a class overview. + * + *

Your activity must derive from {@link FragmentActivity} to use this. From such an activity, + * you can acquire the {@link FragmentManager} by calling + * {@link FragmentActivity#getSupportFragmentManager}. + */ +public abstract class FragmentManager { + static final FragmentFactory DEFAULT_FACTORY = new FragmentFactory(); + + /** + * Representation of an entry on the fragment back stack, as created + * with {@link FragmentTransaction#addToBackStack(String) + * FragmentTransaction.addToBackStack()}. Entries can later be + * retrieved with {@link FragmentManager#getBackStackEntryAt(int) + * FragmentManager.getBackStackEntryAt()}. + * + *

Note that you should never hold on to a BackStackEntry object; + * the identifier as returned by {@link #getId} is the only thing that + * will be persisted across activity instances. + */ + public interface BackStackEntry { + /** + * Return the unique identifier for the entry. This is the only + * representation of the entry that will persist across activity + * instances. + */ + public int getId(); + + /** + * Get the name that was supplied to + * {@link FragmentTransaction#addToBackStack(String) + * FragmentTransaction.addToBackStack(String)} when creating this entry. + */ + @Nullable + public String getName(); + + /** + * Return the full bread crumb title resource identifier for the entry, + * or 0 if it does not have one. + */ + @StringRes + public int getBreadCrumbTitleRes(); + + /** + * Return the short bread crumb title resource identifier for the entry, + * or 0 if it does not have one. + */ + @StringRes + public int getBreadCrumbShortTitleRes(); + + /** + * Return the full bread crumb title for the entry, or null if it + * does not have one. + */ + @Nullable + public CharSequence getBreadCrumbTitle(); + + /** + * Return the short bread crumb title for the entry, or null if it + * does not have one. + */ + @Nullable + public CharSequence getBreadCrumbShortTitle(); + } + + /** + * Interface to watch for changes to the back stack. + */ + public interface OnBackStackChangedListener { + /** + * Called whenever the contents of the back stack change. + */ + public void onBackStackChanged(); + } + + private FragmentFactory mFragmentFactory = null; + + /** + * Start a series of edit operations on the Fragments associated with + * this FragmentManager. + * + *

Note: A fragment transaction can only be created/committed prior + * to an activity saving its state. If you try to commit a transaction + * after {@link FragmentActivity#onSaveInstanceState FragmentActivity.onSaveInstanceState()} + * (and prior to a following {@link FragmentActivity#onStart FragmentActivity.onStart} + * or {@link FragmentActivity#onResume FragmentActivity.onResume()}, you will get an error. + * This is because the framework takes care of saving your current fragments + * in the state, and if changes are made after the state is saved then they + * will be lost.

+ */ + @NonNull + public abstract FragmentTransaction beginTransaction(); + + /** + * @hide -- remove once prebuilts are in. + * @deprecated + */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + @Deprecated + @NonNull + public FragmentTransaction openTransaction() { + return beginTransaction(); + } + + /** + * After a {@link FragmentTransaction} is committed with + * {@link FragmentTransaction#commit FragmentTransaction.commit()}, it + * is scheduled to be executed asynchronously on the process's main thread. + * If you want to immediately executing any such pending operations, you + * can call this function (only from the main thread) to do so. Note that + * all callbacks and other related behavior will be done from within this + * call, so be careful about where this is called from. + * + *

If you are committing a single transaction that does not modify the + * fragment back stack, strongly consider using + * {@link FragmentTransaction#commitNow()} instead. This can help avoid + * unwanted side effects when other code in your app has pending committed + * transactions that expect different timing.

+ *

+ * This also forces the start of any postponed Transactions where + * {@link Fragment#postponeEnterTransition()} has been called. + * + * @return Returns true if there were any pending transactions to be + * executed. + */ + public abstract boolean executePendingTransactions(); + + /** + * Finds a fragment that was identified by the given id either when inflated + * from XML or as the container ID when added in a transaction. This first + * searches through fragments that are currently added to the manager's + * activity; if no such fragment is found, then all fragments currently + * on the back stack associated with this ID are searched. + * @return The fragment if found or null otherwise. + */ + @Nullable + public abstract Fragment findFragmentById(@IdRes int id); + + /** + * Finds a fragment that was identified by the given tag either when inflated + * from XML or as supplied when added in a transaction. This first + * searches through fragments that are currently added to the manager's + * activity; if no such fragment is found, then all fragments currently + * on the back stack are searched. + * @return The fragment if found or null otherwise. + */ + @Nullable + public abstract Fragment findFragmentByTag(@Nullable String tag); + + /** + * Flag for {@link #popBackStack(String, int)} + * and {@link #popBackStack(int, int)}: If set, and the name or ID of + * a back stack entry has been supplied, then all matching entries will + * be consumed until one that doesn't match is found or the bottom of + * the stack is reached. Otherwise, all entries up to but not including that entry + * will be removed. + */ + public static final int POP_BACK_STACK_INCLUSIVE = 1<<0; + + /** + * Pop the top state off the back stack. This function is asynchronous -- it enqueues the + * request to pop, but the action will not be performed until the application + * returns to its event loop. + */ + public abstract void popBackStack(); + + /** + * Like {@link #popBackStack()}, but performs the operation immediately + * inside of the call. This is like calling {@link #executePendingTransactions()} + * afterwards without forcing the start of postponed Transactions. + * @return Returns true if there was something popped, else false. + */ + public abstract boolean popBackStackImmediate(); + + /** + * Pop the last fragment transition from the manager's fragment + * back stack. + * This function is asynchronous -- it enqueues the + * request to pop, but the action will not be performed until the application + * returns to its event loop. + * + * @param name If non-null, this is the name of a previous back state + * to look for; if found, all states up to that state will be popped. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. If null, only the top state is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + */ + public abstract void popBackStack(@Nullable String name, int flags); + + /** + * Like {@link #popBackStack(String, int)}, but performs the operation immediately + * inside of the call. This is like calling {@link #executePendingTransactions()} + * afterwards without forcing the start of postponed Transactions. + * @return Returns true if there was something popped, else false. + */ + public abstract boolean popBackStackImmediate(@Nullable String name, int flags); + + /** + * Pop all back stack states up to the one with the given identifier. + * This function is asynchronous -- it enqueues the + * request to pop, but the action will not be performed until the application + * returns to its event loop. + * + * @param id Identifier of the stated to be popped. If no identifier exists, + * false is returned. + * The identifier is the number returned by + * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + */ + public abstract void popBackStack(int id, int flags); + + /** + * Like {@link #popBackStack(int, int)}, but performs the operation immediately + * inside of the call. This is like calling {@link #executePendingTransactions()} + * afterwards without forcing the start of postponed Transactions. + * @return Returns true if there was something popped, else false. + */ + public abstract boolean popBackStackImmediate(int id, int flags); + + /** + * Return the number of entries currently in the back stack. + */ + public abstract int getBackStackEntryCount(); + + /** + * Return the BackStackEntry at index index in the back stack; + * entries start index 0 being the bottom of the stack. + */ + @NonNull + public abstract BackStackEntry getBackStackEntryAt(int index); + + /** + * Add a new listener for changes to the fragment back stack. + */ + public abstract void addOnBackStackChangedListener( + @NonNull OnBackStackChangedListener listener); + + /** + * Remove a listener that was previously added with + * {@link #addOnBackStackChangedListener(OnBackStackChangedListener)}. + */ + public abstract void removeOnBackStackChangedListener( + @NonNull OnBackStackChangedListener listener); + + /** + * Put a reference to a fragment in a Bundle. This Bundle can be + * persisted as saved state, and when later restoring + * {@link #getFragment(Bundle, String)} will return the current + * instance of the same fragment. + * + * @param bundle The bundle in which to put the fragment reference. + * @param key The name of the entry in the bundle. + * @param fragment The Fragment whose reference is to be stored. + */ + public abstract void putFragment(@NonNull Bundle bundle, @NonNull String key, + @NonNull Fragment fragment); + + /** + * Retrieve the current Fragment instance for a reference previously + * placed with {@link #putFragment(Bundle, String, Fragment)}. + * + * @param bundle The bundle from which to retrieve the fragment reference. + * @param key The name of the entry in the bundle. + * @return Returns the current Fragment instance that is associated with + * the given reference. + */ + @Nullable + public abstract Fragment getFragment(@NonNull Bundle bundle, @NonNull String key); + + /** + * Get a list of all fragments that are currently added to the FragmentManager. + * This may include those that are hidden as well as those that are shown. + * This will not include any fragments only in the back stack, or fragments that + * are detached or removed. + *

+ * The order of the fragments in the list is the order in which they were + * added or attached. + * + * @return A list of all fragments that are added to the FragmentManager. + */ + @NonNull + public abstract List getFragments(); + + /** + * Save the current instance state of the given Fragment. This can be + * used later when creating a new instance of the Fragment and adding + * it to the fragment manager, to have it create itself to match the + * current state returned here. Note that there are limits on how + * this can be used: + * + *

    + *
  • The Fragment must currently be attached to the FragmentManager. + *
  • A new Fragment created using this saved state must be the same class + * type as the Fragment it was created from. + *
  • The saved state can not contain dependencies on other fragments -- + * that is it can't use {@link #putFragment(Bundle, String, Fragment)} to + * store a fragment reference because that reference may not be valid when + * this saved state is later used. Likewise the Fragment's target and + * result code are not included in this state. + *
+ * + * @param f The Fragment whose state is to be saved. + * @return The generated state. This will be null if there was no + * interesting state created by the fragment. + */ + @Nullable + public abstract Fragment.SavedState saveFragmentInstanceState(@NonNull Fragment f); + + /** + * Returns true if the final {@link android.app.Activity#onDestroy() Activity.onDestroy()} + * call has been made on the FragmentManager's Activity, so this instance is now dead. + */ + public abstract boolean isDestroyed(); + + /** + * Registers a {@link FragmentLifecycleCallbacks} to listen to fragment lifecycle events + * happening in this FragmentManager. All registered callbacks will be automatically + * unregistered when this FragmentManager is destroyed. + * + * @param cb Callbacks to register + * @param recursive true to automatically register this callback for all child FragmentManagers + */ + public abstract void registerFragmentLifecycleCallbacks(@NonNull FragmentLifecycleCallbacks cb, + boolean recursive); + + /** + * Unregisters a previously registered {@link FragmentLifecycleCallbacks}. If the callback + * was not previously registered this call has no effect. All registered callbacks will be + * automatically unregistered when this FragmentManager is destroyed. + * + * @param cb Callbacks to unregister + */ + public abstract void unregisterFragmentLifecycleCallbacks( + @NonNull FragmentLifecycleCallbacks cb); + + /** + * Return the currently active primary navigation fragment for this FragmentManager. + * The primary navigation fragment is set by fragment transactions using + * {@link FragmentTransaction#setPrimaryNavigationFragment(Fragment)}. + * + *

The primary navigation fragment's + * {@link Fragment#getChildFragmentManager() child FragmentManager} will be called first + * to process delegated navigation actions such as {@link #popBackStack()} if no ID + * or transaction name is provided to pop to.

+ * + * @return the fragment designated as the primary navigation fragment + */ + @Nullable + public abstract Fragment getPrimaryNavigationFragment(); + + /** + * Set a {@link FragmentFactory} for this FragmentManager that will be used + * to create new Fragment instances from this point onward. + * + * @param fragmentFactory the factory to use to create new Fragment instances + */ + public void setFragmentFactory(@NonNull FragmentFactory fragmentFactory) { + mFragmentFactory = fragmentFactory; + } + + /** + * Gets the current {@link FragmentFactory} used to instantiate new Fragment instances. + * + * @return the current FragmentFactory + */ + @NonNull + public FragmentFactory getFragmentFactory() { + if (mFragmentFactory == null) { + mFragmentFactory = DEFAULT_FACTORY; + } + return mFragmentFactory; + } + + /** + * Print the FragmentManager's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer A PrintWriter to which the dump is to be set. + * @param args Additional arguments to the dump request. + */ + public abstract void dump(@NonNull String prefix, @Nullable FileDescriptor fd, + @NonNull PrintWriter writer, @Nullable String[] args); + + /** + * Control whether the framework's internal fragment manager debugging + * logs are turned on. If enabled, you will see output in logcat as + * the framework performs fragment operations. + */ + public static void enableDebugLogging(boolean enabled) { + FragmentManagerImpl.DEBUG = enabled; + } + + /** + * Returns {@code true} if the FragmentManager's state has already been saved + * by its host. Any operations that would change saved state should not be performed + * if this method returns true. For example, any popBackStack() method, such as + * {@link #popBackStackImmediate()} or any FragmentTransaction using + * {@link FragmentTransaction#commit()} instead of + * {@link FragmentTransaction#commitAllowingStateLoss()} will change + * the state and will result in an error. + * + * @return true if this FragmentManager's state has already been saved by its host + */ + public abstract boolean isStateSaved(); + + /** + * Callback interface for listening to fragment state changes that happen + * within a given FragmentManager. + */ + public abstract static class FragmentLifecycleCallbacks { + /** + * Called right before the fragment's {@link Fragment#onAttach(Context)} method is called. + * This is a good time to inject any required dependencies or perform other configuration + * for the fragment before any of the fragment's lifecycle methods are invoked. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + * @param context Context that the Fragment is being attached to + */ + public void onFragmentPreAttached(@NonNull FragmentManager fm, @NonNull Fragment f, + @NonNull Context context) {} + + /** + * Called after the fragment has been attached to its host. Its host will have had + * onAttachFragment called before this call happens. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + * @param context Context that the Fragment was attached to + */ + public void onFragmentAttached(@NonNull FragmentManager fm, @NonNull Fragment f, + @NonNull Context context) {} + + /** + * Called right before the fragment's {@link Fragment#onCreate(Bundle)} method is called. + * This is a good time to inject any required dependencies or perform other configuration + * for the fragment. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + * @param savedInstanceState Saved instance bundle from a previous instance + */ + public void onFragmentPreCreated(@NonNull FragmentManager fm, @NonNull Fragment f, + @Nullable Bundle savedInstanceState) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onCreate(Bundle)}. This will only happen once for any given + * fragment instance, though the fragment may be attached and detached multiple times. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + * @param savedInstanceState Saved instance bundle from a previous instance + */ + public void onFragmentCreated(@NonNull FragmentManager fm, @NonNull Fragment f, + @Nullable Bundle savedInstanceState) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onActivityCreated(Bundle)}. This will only happen once for any given + * fragment instance, though the fragment may be attached and detached multiple times. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + * @param savedInstanceState Saved instance bundle from a previous instance + */ + public void onFragmentActivityCreated(@NonNull FragmentManager fm, @NonNull Fragment f, + @Nullable Bundle savedInstanceState) {} + + /** + * Called after the fragment has returned a non-null view from the FragmentManager's + * request to {@link Fragment#onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * + * @param fm Host FragmentManager + * @param f Fragment that created and owns the view + * @param v View returned by the fragment + * @param savedInstanceState Saved instance bundle from a previous instance + */ + public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f, + @NonNull View v, @Nullable Bundle savedInstanceState) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onStart()}. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + */ + public void onFragmentStarted(@NonNull FragmentManager fm, @NonNull Fragment f) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onResume()}. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + */ + public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onPause()}. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + */ + public void onFragmentPaused(@NonNull FragmentManager fm, @NonNull Fragment f) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onStop()}. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + */ + public void onFragmentStopped(@NonNull FragmentManager fm, @NonNull Fragment f) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onSaveInstanceState(Bundle)}. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + * @param outState Saved state bundle for the fragment + */ + public void onFragmentSaveInstanceState(@NonNull FragmentManager fm, @NonNull Fragment f, + @NonNull Bundle outState) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onDestroyView()}. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + */ + public void onFragmentViewDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onDestroy()}. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + */ + public void onFragmentDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {} + + /** + * Called after the fragment has returned from the FragmentManager's call to + * {@link Fragment#onDetach()}. + * + * @param fm Host FragmentManager + * @param f Fragment changing state + */ + public void onFragmentDetached(@NonNull FragmentManager fm, @NonNull Fragment f) {} + } +} + diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerImpl.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerImpl.java new file mode 100644 index 000000000..a5f974f0d --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerImpl.java @@ -0,0 +1,3484 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Looper; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.ScaleAnimation; +import android.view.animation.Transformation; + +import androidx.activity.OnBackPressedCallback; +import androidx.activity.OnBackPressedDispatcher; +import androidx.activity.OnBackPressedDispatcherOwner; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.collection.ArraySet; +import androidx.core.util.DebugUtils; +import androidx.core.util.LogWriter; +import androidx.core.view.OneShotPreDrawListener; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ViewModelStore; +import androidx.lifecycle.ViewModelStoreOwner; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Container for fragments associated with an activity. + */ +final class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory2 { + static boolean DEBUG = false; + static final String TAG = "FragmentManager"; + + static final String TARGET_REQUEST_CODE_STATE_TAG = "android:target_req_state"; + static final String TARGET_STATE_TAG = "android:target_state"; + static final String VIEW_STATE_TAG = "android:view_state"; + static final String USER_VISIBLE_HINT_TAG = "android:user_visible_hint"; + + private static final class FragmentLifecycleCallbacksHolder { + final FragmentLifecycleCallbacks mCallback; + final boolean mRecursive; + + FragmentLifecycleCallbacksHolder(FragmentLifecycleCallbacks callback, boolean recursive) { + mCallback = callback; + mRecursive = recursive; + } + } + + ArrayList mPendingActions; + boolean mExecutingActions; + + int mNextFragmentIndex = 0; + + final ArrayList mAdded = new ArrayList<>(); + final HashMap mActive = new HashMap<>(); + ArrayList mBackStack; + ArrayList mCreatedMenus; + private OnBackPressedDispatcher mOnBackPressedDispatcher; + private final OnBackPressedCallback mOnBackPressedCallback = + new OnBackPressedCallback(false) { + @Override + public void handleOnBackPressed() { + FragmentManagerImpl.this.handleOnBackPressed(); + } + }; + + // Must be accessed while locked. + ArrayList mBackStackIndices; + ArrayList mAvailBackStackIndices; + + ArrayList mBackStackChangeListeners; + private final CopyOnWriteArrayList + mLifecycleCallbacks = new CopyOnWriteArrayList<>(); + + int mCurState = Fragment.INITIALIZING; + FragmentHostCallback mHost; + FragmentContainer mContainer; + Fragment mParent; + @Nullable + Fragment mPrimaryNav; + + boolean mNeedMenuInvalidate; + boolean mStateSaved; + boolean mStopped; + boolean mDestroyed; + boolean mHavePendingDeferredStart; + + // Temporary vars for removing redundant operations in BackStackRecords: + ArrayList mTmpRecords; + ArrayList mTmpIsPop; + ArrayList mTmpAddedFragments; + + // Temporary vars for state save and restore. + Bundle mStateBundle = null; + SparseArray mStateArray = null; + + // Postponed transactions. + ArrayList mPostponedTransactions; + + private FragmentManagerViewModel mNonConfig; + + Runnable mExecCommit = new Runnable() { + @Override + public void run() { + execPendingActions(); + } + }; + + private void throwException(RuntimeException ex) { + Log.e(TAG, ex.getMessage()); + Log.e(TAG, "Activity state:"); + LogWriter logw = new LogWriter(TAG); + PrintWriter pw = new PrintWriter(logw); + if (mHost != null) { + try { + mHost.onDump(" ", null, pw, new String[] { }); + } catch (Exception e) { + Log.e(TAG, "Failed dumping state", e); + } + } else { + try { + dump(" ", null, pw, new String[] { }); + } catch (Exception e) { + Log.e(TAG, "Failed dumping state", e); + } + } + throw ex; + } + + @NonNull + @Override + public FragmentTransaction beginTransaction() { + return new BackStackRecord(this); + } + + @Override + public boolean executePendingTransactions() { + boolean updates = execPendingActions(); + forcePostponedTransactions(); + return updates; + } + + private void updateOnBackPressedCallbackEnabled() { + // Always enable the callback if we have pending actions + // as we don't know if they'll change the back stack entry count. + // See handleOnBackPressed() for more explanation + if (mPendingActions != null && !mPendingActions.isEmpty()) { + mOnBackPressedCallback.setEnabled(true); + return; + } + // This FragmentManager needs to have a back stack for this to be enabled + // And the parent fragment, if it exists, needs to be the primary navigation + // fragment. + mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > 0 + && isPrimaryNavigation(mParent)); + } + + /** + * Recursively check up the FragmentManager hierarchy of primary + * navigation Fragments to ensure that all of the parent Fragments are the + * primary navigation Fragment for their associated FragmentManager + */ + boolean isPrimaryNavigation(@Nullable Fragment parent) { + // If the parent is null, then we're at the root host + // and we're always the primary navigation + if (parent == null) { + return true; + } + FragmentManagerImpl parentFragmentManager = parent.mFragmentManager; + Fragment primaryNavigationFragment = parentFragmentManager + .getPrimaryNavigationFragment(); + // The parent Fragment needs to be the primary navigation Fragment + // and, if it has a parent itself, that parent also needs to be + // the primary navigation fragment, recursively up the stack + return parent == primaryNavigationFragment + && isPrimaryNavigation(parentFragmentManager.mParent); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void handleOnBackPressed() { + // First, execute any pending actions to make sure we're in an + // up to date view of the world just in case anyone is queuing + // up transactions that change the back stack then immediately + // calling onBackPressed() + execPendingActions(); + if (mOnBackPressedCallback.isEnabled()) { + // We still have a back stack, so we can pop + popBackStackImmediate(); + } else { + // Sigh. Due to FragmentManager's asynchronicity, we can + // get into cases where we *think* we can handle the back + // button but because of frame perfect dispatch, we fell + // on our face. Since our callback is disabled, we can + // re-trigger the onBackPressed() to dispatch to the next + // enabled callback + mOnBackPressedDispatcher.onBackPressed(); + } + } + + @Override + public void popBackStack() { + enqueueAction(new PopBackStackState(null, -1, 0), false); + } + + @Override + public boolean popBackStackImmediate() { + checkStateLoss(); + return popBackStackImmediate(null, -1, 0); + } + + @Override + public void popBackStack(@Nullable final String name, final int flags) { + enqueueAction(new PopBackStackState(name, -1, flags), false); + } + + @Override + public boolean popBackStackImmediate(@Nullable String name, int flags) { + checkStateLoss(); + return popBackStackImmediate(name, -1, flags); + } + + @Override + public void popBackStack(final int id, final int flags) { + if (id < 0) { + throw new IllegalArgumentException("Bad id: " + id); + } + enqueueAction(new PopBackStackState(null, id, flags), false); + } + + @Override + public boolean popBackStackImmediate(int id, int flags) { + checkStateLoss(); + execPendingActions(); + if (id < 0) { + throw new IllegalArgumentException("Bad id: " + id); + } + return popBackStackImmediate(null, id, flags); + } + + /** + * Used by all public popBackStackImmediate methods, this executes pending transactions and + * returns true if the pop action did anything, regardless of what other pending + * transactions did. + * + * @return true if the pop operation did anything or false otherwise. + */ + private boolean popBackStackImmediate(String name, int id, int flags) { + execPendingActions(); + ensureExecReady(true); + + if (mPrimaryNav != null // We have a primary nav fragment + && id < 0 // No valid id (since they're local) + && name == null) { // no name to pop to (since they're local) + final FragmentManager childManager = mPrimaryNav.getChildFragmentManager(); + if (childManager.popBackStackImmediate()) { + // We did something, just not to this specific FragmentManager. Return true. + return true; + } + } + + boolean executePop = popBackStackState(mTmpRecords, mTmpIsPop, name, id, flags); + if (executePop) { + mExecutingActions = true; + try { + removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop); + } finally { + cleanupExec(); + } + } + + updateOnBackPressedCallbackEnabled(); + doPendingDeferredStart(); + burpActive(); + return executePop; + } + + @Override + public int getBackStackEntryCount() { + return mBackStack != null ? mBackStack.size() : 0; + } + + @Override + public BackStackEntry getBackStackEntryAt(int index) { + return mBackStack.get(index); + } + + @Override + public void addOnBackStackChangedListener(OnBackStackChangedListener listener) { + if (mBackStackChangeListeners == null) { + mBackStackChangeListeners = new ArrayList(); + } + mBackStackChangeListeners.add(listener); + } + + @Override + public void removeOnBackStackChangedListener(OnBackStackChangedListener listener) { + if (mBackStackChangeListeners != null) { + mBackStackChangeListeners.remove(listener); + } + } + + @Override + public void putFragment(Bundle bundle, String key, Fragment fragment) { + if (fragment.mFragmentManager != this) { + throwException(new IllegalStateException("Fragment " + fragment + + " is not currently in the FragmentManager")); + } + bundle.putString(key, fragment.mWho); + } + + @Override + @Nullable + public Fragment getFragment(Bundle bundle, String key) { + String who = bundle.getString(key); + if (who == null) { + return null; + } + Fragment f = mActive.get(who); + if (f == null) { + throwException(new IllegalStateException("Fragment no longer exists for key " + + key + ": unique id " + who)); + } + return f; + } + + @Override + @SuppressWarnings("unchecked") + public List getFragments() { + if (mAdded.isEmpty()) { + return Collections.emptyList(); + } + synchronized (mAdded) { + return (List) mAdded.clone(); + } + } + + @NonNull + ViewModelStore getViewModelStore(@NonNull Fragment f) { + return mNonConfig.getViewModelStore(f); + } + + @NonNull + FragmentManagerViewModel getChildNonConfig(@NonNull Fragment f) { + return mNonConfig.getChildNonConfig(f); + } + + void addRetainedFragment(@NonNull Fragment f) { + if (isStateSaved()) { + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Ignoring addRetainedFragment as the state is already saved"); + } + return; + } + boolean added = mNonConfig.addRetainedFragment(f); + if (added && FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Updating retained Fragments: Added " + f); + } + } + + void removeRetainedFragment(@NonNull Fragment f) { + if (isStateSaved()) { + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Ignoring removeRetainedFragment as the state is already saved"); + } + return; + } + boolean removed = mNonConfig.removeRetainedFragment(f); + if (removed && FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Updating retained Fragments: Removed " + f); + } + } + + /** + * This is used by FragmentController to get the Active fragments. + * + * @return A list of active fragments in the fragment manager, including those that are in the + * back stack. + */ + @NonNull + List getActiveFragments() { + return new ArrayList<>(mActive.values()); + } + + /** + * Used by FragmentController to get the number of Active Fragments. + * + * @return The number of active fragments. + */ + int getActiveFragmentCount() { + return mActive.size(); + } + + @Override + @Nullable + public Fragment.SavedState saveFragmentInstanceState(@NonNull Fragment fragment) { + if (fragment.mFragmentManager != this) { + throwException( new IllegalStateException("Fragment " + fragment + + " is not currently in the FragmentManager")); + } + if (fragment.mState > Fragment.INITIALIZING) { + Bundle result = saveFragmentBasicState(fragment); + return result != null ? new Fragment.SavedState(result) : null; + } + return null; + } + + @Override + public boolean isDestroyed() { + return mDestroyed; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("FragmentManager{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" in "); + if (mParent != null) { + DebugUtils.buildShortClassTag(mParent, sb); + } else { + DebugUtils.buildShortClassTag(mHost, sb); + } + sb.append("}}"); + return sb.toString(); + } + + @Override + public void dump(@NonNull String prefix, @Nullable FileDescriptor fd, + @NonNull PrintWriter writer, @Nullable String[] args) { + String innerPrefix = prefix + " "; + + if (!mActive.isEmpty()) { + writer.print(prefix); writer.print("Active Fragments in "); + writer.print(Integer.toHexString(System.identityHashCode(this))); + writer.println(":"); + for (Fragment f : mActive.values()) { + writer.print(prefix); writer.println(f); + if (f != null) { + f.dump(innerPrefix, fd, writer, args); + } + } + } + + int N = mAdded.size(); + if (N > 0) { + writer.print(prefix); writer.println("Added Fragments:"); + for (int i = 0; i < N; i++) { + Fragment f = mAdded.get(i); + writer.print(prefix); + writer.print(" #"); + writer.print(i); + writer.print(": "); + writer.println(f.toString()); + } + } + + if (mCreatedMenus != null) { + N = mCreatedMenus.size(); + if (N > 0) { + writer.print(prefix); writer.println("Fragments Created Menus:"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Back Stack:"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Back Stack Indices:"); + for (int i=0; i 0) { + writer.print(prefix); writer.print("mAvailBackStackIndices: "); + writer.println(Arrays.toString(mAvailBackStackIndices.toArray())); + } + } + + if (mPendingActions != null) { + N = mPendingActions.size(); + if (N > 0) { + writer.print(prefix); writer.println("Pending Actions:"); + for (int i=0; i= state; + } + + @SuppressWarnings("ReferenceEquality") + void moveToState(Fragment f, int newState, int transit, int transitionStyle, + boolean keepActive) { + // Fragments that are not currently added will sit in the onCreate() state. + if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) { + newState = Fragment.CREATED; + } + if (f.mRemoving && newState > f.mState) { + if (f.mState == Fragment.INITIALIZING && f.isInBackStack()) { + // Allow the fragment to be created so that it can be saved later. + newState = Fragment.CREATED; + } else { + // While removing a fragment, we can't change it to a higher state. + newState = f.mState; + } + } + // Defer start if requested; don't allow it to move to STARTED or higher + // if it's not already started. + if (f.mDeferStart && f.mState < Fragment.STARTED && newState > Fragment.ACTIVITY_CREATED) { + newState = Fragment.ACTIVITY_CREATED; + } + // Don't allow the Fragment to go above its max lifecycle state + // Ensure that Fragments are capped at CREATED instead of ACTIVITY_CREATED. + if (f.mMaxState == Lifecycle.State.CREATED) { + newState = Math.min(newState, Fragment.CREATED); + } else { + newState = Math.min(newState, f.mMaxState.ordinal()); + } + if (f.mState <= newState) { + // For fragments that are created from a layout, when restoring from + // state we don't want to allow them to be created until they are + // being reloaded from the layout. + if (f.mFromLayout && !f.mInLayout) { + return; + } + if (f.getAnimatingAway() != null || f.getAnimator() != null) { + // The fragment is currently being animated... but! Now we + // want to move our state back up. Give up on waiting for the + // animation, move to whatever the final state should be once + // the animation is done, and then we can proceed from there. + f.setAnimatingAway(null); + f.setAnimator(null); + moveToState(f, f.getStateAfterAnimating(), 0, 0, true); + } + switch (f.mState) { + case Fragment.INITIALIZING: + if (newState > Fragment.INITIALIZING) { + if (DEBUG) Log.v(TAG, "moveto CREATED: " + f); + if (f.mSavedFragmentState != null) { + f.mSavedFragmentState.setClassLoader(mHost.getContext() + .getClassLoader()); + f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray( + FragmentManagerImpl.VIEW_STATE_TAG); + Fragment target = getFragment(f.mSavedFragmentState, + FragmentManagerImpl.TARGET_STATE_TAG); + f.mTargetWho = target != null ? target.mWho : null; + if (f.mTargetWho != null) { + f.mTargetRequestCode = f.mSavedFragmentState.getInt( + FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0); + } + if (f.mSavedUserVisibleHint != null) { + f.mUserVisibleHint = f.mSavedUserVisibleHint; + f.mSavedUserVisibleHint = null; + } else { + f.mUserVisibleHint = f.mSavedFragmentState.getBoolean( + FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true); + } + if (!f.mUserVisibleHint) { + f.mDeferStart = true; + if (newState > Fragment.ACTIVITY_CREATED) { + newState = Fragment.ACTIVITY_CREATED; + } + } + } + + f.mHost = mHost; + f.mParentFragment = mParent; + f.mFragmentManager = mParent != null + ? mParent.mChildFragmentManager : mHost.mFragmentManager; + + // If we have a target fragment, push it along to at least CREATED + // so that this one can rely on it as an initialized dependency. + if (f.mTarget != null) { + if (mActive.get(f.mTarget.mWho) != f.mTarget) { + throw new IllegalStateException("Fragment " + f + + " declared target fragment " + f.mTarget + + " that does not belong to this FragmentManager!"); + } + if (f.mTarget.mState < Fragment.CREATED) { + moveToState(f.mTarget, Fragment.CREATED, 0, 0, true); + } + f.mTargetWho = f.mTarget.mWho; + f.mTarget = null; + } + if (f.mTargetWho != null) { + Fragment target = mActive.get(f.mTargetWho); + if (target == null) { + throw new IllegalStateException("Fragment " + f + + " declared target fragment " + f.mTargetWho + + " that does not belong to this FragmentManager!"); + } + if (target.mState < Fragment.CREATED) { + moveToState(target, Fragment.CREATED, 0, 0, true); + } + } + + dispatchOnFragmentPreAttached(f, mHost.getContext(), false); + f.performAttach(); + if (f.mParentFragment == null) { + mHost.onAttachFragment(f); + } else { + f.mParentFragment.onAttachFragment(f); + } + dispatchOnFragmentAttached(f, mHost.getContext(), false); + + if (!f.mIsCreated) { + dispatchOnFragmentPreCreated(f, f.mSavedFragmentState, false); + f.performCreate(f.mSavedFragmentState); + dispatchOnFragmentCreated(f, f.mSavedFragmentState, false); + } else { + f.restoreChildFragmentState(f.mSavedFragmentState); + f.mState = Fragment.CREATED; + } + } + // fall through + case Fragment.CREATED: + // We want to unconditionally run this anytime we do a moveToState that + // moves the Fragment above INITIALIZING, including cases such as when + // we move from CREATED => CREATED as part of the case fall through above. + if (newState > Fragment.INITIALIZING) { + ensureInflatedFragmentView(f); + } + + if (newState > Fragment.CREATED) { + if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f); + if (!f.mFromLayout) { + ViewGroup container = null; + if (f.mContainerId != 0) { + if (f.mContainerId == View.NO_ID) { + throwException(new IllegalArgumentException( + "Cannot create fragment " + + f + + " for a container view with no id")); + } + container = (ViewGroup) mContainer.onFindViewById(f.mContainerId); + if (container == null && !f.mRestored) { + String resName; + try { + resName = f.getResources().getResourceName(f.mContainerId); + } catch (Resources.NotFoundException e) { + resName = "unknown"; + } + throwException(new IllegalArgumentException( + "No view found for id 0x" + + Integer.toHexString(f.mContainerId) + " (" + + resName + + ") for fragment " + f)); + } + } + f.mContainer = container; + f.performCreateView(f.performGetLayoutInflater( + f.mSavedFragmentState), container, f.mSavedFragmentState); + if (f.mView != null) { + f.mInnerView = f.mView; + f.mView.setSaveFromParentEnabled(false); + if (container != null) { + container.addView(f.mView); + } + if (f.mHidden) { + f.mView.setVisibility(View.GONE); + } + f.onViewCreated(f.mView, f.mSavedFragmentState); + dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState, + false); + // Only animate the view if it is visible. This is done after + // dispatchOnFragmentViewCreated in case visibility is changed + f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE) + && f.mContainer != null; + } else { + f.mInnerView = null; + } + } + + f.performActivityCreated(f.mSavedFragmentState); + dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false); + if (f.mView != null) { + f.restoreViewState(f.mSavedFragmentState); + } + f.mSavedFragmentState = null; + } + // fall through + case Fragment.ACTIVITY_CREATED: + if (newState > Fragment.ACTIVITY_CREATED) { + if (DEBUG) Log.v(TAG, "moveto STARTED: " + f); + f.performStart(); + dispatchOnFragmentStarted(f, false); + } + // fall through + case Fragment.STARTED: + if (newState > Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f); + f.performResume(); + dispatchOnFragmentResumed(f, false); + f.mSavedFragmentState = null; + f.mSavedViewState = null; + } + } + } else if (f.mState > newState) { + switch (f.mState) { + case Fragment.RESUMED: + if (newState < Fragment.RESUMED) { + if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f); + f.performPause(); + dispatchOnFragmentPaused(f, false); + } + // fall through + case Fragment.STARTED: + if (newState < Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f); + f.performStop(); + dispatchOnFragmentStopped(f, false); + } + // fall through + case Fragment.ACTIVITY_CREATED: + if (newState < Fragment.ACTIVITY_CREATED) { + if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f); + if (f.mView != null) { + // Need to save the current view state if not + // done already. + if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) { + saveFragmentViewState(f); + } + } + f.performDestroyView(); + dispatchOnFragmentViewDestroyed(f, false); + if (f.mView != null && f.mContainer != null) { + // Stop any current animations: + f.mContainer.endViewTransition(f.mView); + f.mView.clearAnimation(); + AnimationOrAnimator anim = null; + // If parent is being removed, no need to handle child animations. + if (f.getParentFragment() == null || !f.getParentFragment().mRemoving) { + if (mCurState > Fragment.INITIALIZING && !mDestroyed + && f.mView.getVisibility() == View.VISIBLE + && f.mPostponedAlpha >= 0) { + anim = loadAnimation(f, transit, false, + transitionStyle); + } + f.mPostponedAlpha = 0; + if (anim != null) { + animateRemoveFragment(f, anim, newState); + } + f.mContainer.removeView(f.mView); + } + } + f.mContainer = null; + f.mView = null; + // Set here to ensure that Observers are called after + // the Fragment's view is set to null + f.mViewLifecycleOwner = null; + f.mViewLifecycleOwnerLiveData.setValue(null); + f.mInnerView = null; + f.mInLayout = false; + } + // fall through + case Fragment.CREATED: + if (newState < Fragment.CREATED) { + if (mDestroyed) { + // The fragment's containing activity is + // being destroyed, but this fragment is + // currently animating away. Stop the + // animation right now -- it is not needed, + // and we can't wait any more on destroying + // the fragment. + if (f.getAnimatingAway() != null) { + View v = f.getAnimatingAway(); + f.setAnimatingAway(null); + v.clearAnimation(); + } else if (f.getAnimator() != null) { + Animator animator = f.getAnimator(); + f.setAnimator(null); + animator.cancel(); + } + } + if (f.getAnimatingAway() != null || f.getAnimator() != null) { + // We are waiting for the fragment's view to finish + // animating away. Just make a note of the state + // the fragment now should move to once the animation + // is done. + f.setStateAfterAnimating(newState); + newState = Fragment.CREATED; + } else { + if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f); + boolean beingRemoved = f.mRemoving && !f.isInBackStack(); + if (beingRemoved || mNonConfig.shouldDestroy(f)) { + boolean shouldClear; + if (mHost instanceof ViewModelStoreOwner) { + shouldClear = mNonConfig.isCleared(); + } else if (mHost.getContext() instanceof Activity) { + Activity activity = (Activity) mHost.getContext(); + shouldClear = !activity.isChangingConfigurations(); + } else { + shouldClear = true; + } + if (beingRemoved || shouldClear) { + mNonConfig.clearNonConfigState(f); + } + f.performDestroy(); + dispatchOnFragmentDestroyed(f, false); + } else { + f.mState = Fragment.INITIALIZING; + } + + f.performDetach(); + dispatchOnFragmentDetached(f, false); + if (!keepActive) { + if (beingRemoved || mNonConfig.shouldDestroy(f)) { + makeInactive(f); + } else { + f.mHost = null; + f.mParentFragment = null; + f.mFragmentManager = null; + if (f.mTargetWho != null) { + Fragment target = mActive.get(f.mTargetWho); + if (target != null && target.getRetainInstance()) { + // Only keep references to other retained Fragments + // to avoid developers accessing Fragments that + // are never coming back + f.mTarget = target; + } + } + } + } + } + } + } + } + + if (f.mState != newState) { + Log.w(TAG, "moveToState: Fragment state for " + f + " not updated inline; " + + "expected state " + newState + " found " + f.mState); + f.mState = newState; + } + } + + /** + * Animates the removal of a fragment with the given animator or animation. After animating, + * the fragment's view will be removed from the hierarchy. + * + * @param fragment The fragment to animate out + * @param anim The animator or animation to run on the fragment's view + * @param newState The final state after animating. + */ + private void animateRemoveFragment(@NonNull final Fragment fragment, + @NonNull AnimationOrAnimator anim, final int newState) { + final View viewToAnimate = fragment.mView; + final ViewGroup container = fragment.mContainer; + container.startViewTransition(viewToAnimate); + fragment.setStateAfterAnimating(newState); + if (anim.animation != null) { + Animation animation = + new EndViewTransitionAnimation(anim.animation, container, viewToAnimate); + fragment.setAnimatingAway(fragment.mView); + animation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + // onAnimationEnd() comes during draw(), so there can still be some + // draw events happening after this call. We don't want to detach + // the view until after the onAnimationEnd() + container.post(new Runnable() { + @Override + public void run() { + if (fragment.getAnimatingAway() != null) { + fragment.setAnimatingAway(null); + moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, + false); + } + } + }); + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + }); + fragment.mView.startAnimation(animation); + } else { + Animator animator = anim.animator; + fragment.setAnimator(anim.animator); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator anim) { + container.endViewTransition(viewToAnimate); + // If an animator ends immediately, we can just pretend there is no animation. + // When that happens the the fragment's view won't have been removed yet. + Animator animator = fragment.getAnimator(); + fragment.setAnimator(null); + if (animator != null && container.indexOfChild(viewToAnimate) < 0) { + moveToState(fragment, fragment.getStateAfterAnimating(), 0, 0, false); + } + } + }); + animator.setTarget(fragment.mView); + animator.start(); + } + } + + void moveToState(Fragment f) { + moveToState(f, mCurState, 0, 0, false); + } + + void ensureInflatedFragmentView(Fragment f) { + if (f.mFromLayout && !f.mPerformedCreateView) { + f.performCreateView(f.performGetLayoutInflater( + f.mSavedFragmentState), null, f.mSavedFragmentState); + if (f.mView != null) { + f.mInnerView = f.mView; + f.mView.setSaveFromParentEnabled(false); + if (f.mHidden) f.mView.setVisibility(View.GONE); + f.onViewCreated(f.mView, f.mSavedFragmentState); + dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState, false); + } else { + f.mInnerView = null; + } + } + } + + /** + * Fragments that have been shown or hidden don't have their visibility changed or + * animations run during the {@link #showFragment(Fragment)} or {@link #hideFragment(Fragment)} + * calls. After fragments are brought to their final state in + * {@link #moveFragmentToExpectedState(Fragment)} the fragments that have been shown or + * hidden must have their visibility changed and their animations started here. + * + * @param fragment The fragment with mHiddenChanged = true that should change its View's + * visibility and start the show or hide animation. + */ + void completeShowHideFragment(final Fragment fragment) { + if (fragment.mView != null) { + AnimationOrAnimator anim = loadAnimation(fragment, fragment.getNextTransition(), + !fragment.mHidden, fragment.getNextTransitionStyle()); + if (anim != null && anim.animator != null) { + anim.animator.setTarget(fragment.mView); + if (fragment.mHidden) { + if (fragment.isHideReplaced()) { + fragment.setHideReplaced(false); + } else { + final ViewGroup container = fragment.mContainer; + final View animatingView = fragment.mView; + container.startViewTransition(animatingView); + // Delay the actual hide operation until the animation finishes, + // otherwise the fragment will just immediately disappear + anim.animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + container.endViewTransition(animatingView); + animation.removeListener(this); + if (fragment.mView != null && fragment.mHidden) { + fragment.mView.setVisibility(View.GONE); + } + } + }); + } + } else { + fragment.mView.setVisibility(View.VISIBLE); + } + anim.animator.start(); + } else { + if (anim != null) { + fragment.mView.startAnimation(anim.animation); + anim.animation.start(); + } + final int visibility = fragment.mHidden && !fragment.isHideReplaced() + ? View.GONE + : View.VISIBLE; + fragment.mView.setVisibility(visibility); + if (fragment.isHideReplaced()) { + fragment.setHideReplaced(false); + } + } + } + if (fragment.mAdded && isMenuAvailable(fragment)) { + mNeedMenuInvalidate = true; + } + fragment.mHiddenChanged = false; + fragment.onHiddenChanged(fragment.mHidden); + } + + /** + * Moves a fragment to its expected final state or the fragment manager's state, depending + * on whether the fragment manager's state is raised properly. + * + * @param f The fragment to change. + */ + void moveFragmentToExpectedState(Fragment f) { + if (f == null) { + return; + } + if (!mActive.containsKey(f.mWho)) { + if (DEBUG) { + Log.v(TAG, "Ignoring moving " + f + " to state " + mCurState + + "since it is not added to " + this); + } + return; + } + int nextState = mCurState; + if (f.mRemoving) { + if (f.isInBackStack()) { + nextState = Math.min(nextState, Fragment.CREATED); + } else { + nextState = Math.min(nextState, Fragment.INITIALIZING); + } + } + moveToState(f, nextState, f.getNextTransition(), f.getNextTransitionStyle(), false); + + if (f.mView != null) { + // Move the view if it is out of order + Fragment underFragment = findFragmentUnder(f); + if (underFragment != null) { + final View underView = underFragment.mView; + // make sure this fragment is in the right order. + final ViewGroup container = f.mContainer; + int underIndex = container.indexOfChild(underView); + int viewIndex = container.indexOfChild(f.mView); + if (viewIndex < underIndex) { + container.removeViewAt(viewIndex); + container.addView(f.mView, underIndex); + } + } + if (f.mIsNewlyAdded && f.mContainer != null) { + // Make it visible and run the animations + if (f.mPostponedAlpha > 0f) { + f.mView.setAlpha(f.mPostponedAlpha); + } + f.mPostponedAlpha = 0f; + f.mIsNewlyAdded = false; + // run animations: + AnimationOrAnimator anim = loadAnimation(f, f.getNextTransition(), true, + f.getNextTransitionStyle()); + if (anim != null) { + if (anim.animation != null) { + f.mView.startAnimation(anim.animation); + } else { + anim.animator.setTarget(f.mView); + anim.animator.start(); + } + } + } + } + if (f.mHiddenChanged) { + completeShowHideFragment(f); + } + } + + /** + * Changes the state of the fragment manager to {@code newState}. If the fragment manager + * changes state or {@code always} is {@code true}, any fragments within it have their + * states updated as well. + * + * @param newState The new state for the fragment manager + * @param always If {@code true}, all fragments update their state, even + * if {@code newState} matches the current fragment manager's state. + */ + void moveToState(int newState, boolean always) { + if (mHost == null && newState != Fragment.INITIALIZING) { + throw new IllegalStateException("No activity"); + } + + if (!always && newState == mCurState) { + return; + } + + mCurState = newState; + + // Must add them in the proper order. mActive fragments may be out of order + final int numAdded = mAdded.size(); + for (int i = 0; i < numAdded; i++) { + Fragment f = mAdded.get(i); + moveFragmentToExpectedState(f); + } + + // Now iterate through all active fragments. These will include those that are removed + // and detached. + for (Fragment f : mActive.values()) { + if (f != null && (f.mRemoving || f.mDetached) && !f.mIsNewlyAdded) { + moveFragmentToExpectedState(f); + } + } + + startPendingDeferredFragments(); + + if (mNeedMenuInvalidate && mHost != null && mCurState == Fragment.RESUMED) { + mHost.onSupportInvalidateOptionsMenu(); + mNeedMenuInvalidate = false; + } + } + + void startPendingDeferredFragments() { + for (Fragment f : mActive.values()) { + if (f != null) { + performPendingDeferredStart(f); + } + } + } + + void makeActive(Fragment f) { + if (mActive.get(f.mWho) != null) { + return; + } + + mActive.put(f.mWho, f); + if (f.mRetainInstanceChangedWhileDetached) { + if (f.mRetainInstance) { + addRetainedFragment(f); + } else { + removeRetainedFragment(f); + } + f.mRetainInstanceChangedWhileDetached = false; + } + if (DEBUG) Log.v(TAG, "Added fragment to active set " + f); + } + + void makeInactive(Fragment f) { + if (mActive.get(f.mWho) == null) { + return; + } + + if (DEBUG) Log.v(TAG, "Removed fragment from active set " + f); + // Ensure that any Fragment that had this Fragment as its + // target Fragment retains a reference to the Fragment + for (Fragment fragment : mActive.values()) { + if (fragment != null && f.mWho.equals(fragment.mTargetWho)) { + fragment.mTarget = f; + fragment.mTargetWho = null; + } + } + // Don't remove yet. That happens in burpActive(). This prevents + // concurrent modification while iterating over mActive + mActive.put(f.mWho, null); + removeRetainedFragment(f); + + if (f.mTargetWho != null) { + // Restore the target Fragment so that it can be accessed + // even after the Fragment is removed. + f.mTarget = mActive.get(f.mTargetWho); + } + f.initState(); + } + + public void addFragment(Fragment fragment, boolean moveToStateNow) { + if (DEBUG) Log.v(TAG, "add: " + fragment); + makeActive(fragment); + if (!fragment.mDetached) { + if (mAdded.contains(fragment)) { + throw new IllegalStateException("Fragment already added: " + fragment); + } + synchronized (mAdded) { + mAdded.add(fragment); + } + fragment.mAdded = true; + fragment.mRemoving = false; + if (fragment.mView == null) { + fragment.mHiddenChanged = false; + } + if (isMenuAvailable(fragment)) { + mNeedMenuInvalidate = true; + } + if (moveToStateNow) { + moveToState(fragment); + } + } + } + + public void removeFragment(Fragment fragment) { + if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting); + final boolean inactive = !fragment.isInBackStack(); + if (!fragment.mDetached || inactive) { + synchronized (mAdded) { + mAdded.remove(fragment); + } + if (isMenuAvailable(fragment)) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + fragment.mRemoving = true; + } + } + + /** + * Marks a fragment as hidden to be later animated in with + * {@link #completeShowHideFragment(Fragment)}. + * + * @param fragment The fragment to be shown. + */ + public void hideFragment(Fragment fragment) { + if (DEBUG) Log.v(TAG, "hide: " + fragment); + if (!fragment.mHidden) { + fragment.mHidden = true; + // Toggle hidden changed so that if a fragment goes through show/hide/show + // it doesn't go through the animation. + fragment.mHiddenChanged = !fragment.mHiddenChanged; + } + } + + /** + * Marks a fragment as shown to be later animated in with + * {@link #completeShowHideFragment(Fragment)}. + * + * @param fragment The fragment to be shown. + */ + public void showFragment(Fragment fragment) { + if (DEBUG) Log.v(TAG, "show: " + fragment); + if (fragment.mHidden) { + fragment.mHidden = false; + // Toggle hidden changed so that if a fragment goes through show/hide/show + // it doesn't go through the animation. + fragment.mHiddenChanged = !fragment.mHiddenChanged; + } + } + + public void detachFragment(Fragment fragment) { + if (DEBUG) Log.v(TAG, "detach: " + fragment); + if (!fragment.mDetached) { + fragment.mDetached = true; + if (fragment.mAdded) { + // We are not already in back stack, so need to remove the fragment. + if (DEBUG) Log.v(TAG, "remove from detach: " + fragment); + synchronized (mAdded) { + mAdded.remove(fragment); + } + if (isMenuAvailable(fragment)) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + } + } + } + + public void attachFragment(Fragment fragment) { + if (DEBUG) Log.v(TAG, "attach: " + fragment); + if (fragment.mDetached) { + fragment.mDetached = false; + if (!fragment.mAdded) { + if (mAdded.contains(fragment)) { + throw new IllegalStateException("Fragment already added: " + fragment); + } + if (DEBUG) Log.v(TAG, "add from attach: " + fragment); + synchronized (mAdded) { + mAdded.add(fragment); + } + fragment.mAdded = true; + if (isMenuAvailable(fragment)) { + mNeedMenuInvalidate = true; + } + } + } + } + + @Override + @Nullable + public Fragment findFragmentById(int id) { + // First look through added fragments. + for (int i = mAdded.size() - 1; i >= 0; i--) { + Fragment f = mAdded.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + // Now for any known fragment. + for (Fragment f : mActive.values()) { + if (f != null && f.mFragmentId == id) { + return f; + } + } + return null; + } + + @Override + @Nullable + public Fragment findFragmentByTag(@Nullable String tag) { + if (tag != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + } + if (tag != null) { + // Now for any known fragment. + for (Fragment f : mActive.values()) { + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByWho(@NonNull String who) { + for (Fragment f : mActive.values()) { + if (f != null && (f = f.findFragmentByWho(who)) != null) { + return f; + } + } + return null; + } + + private void checkStateLoss() { + if (isStateSaved()) { + // MOD: Fix fragment state loss exception + //throw new IllegalStateException( + // "Can not perform this action after onSaveInstanceState"); + } + } + + @Override + public boolean isStateSaved() { + // See saveAllState() for the explanation of this. We do this for + // all platform versions, to keep our behavior more consistent between + // them. + return mStateSaved || mStopped; + } + + /** + * Adds an action to the queue of pending actions. + * + * @param action the action to add + * @param allowStateLoss whether to allow loss of state information + * @throws IllegalStateException if the activity has been destroyed + */ + public void enqueueAction(OpGenerator action, boolean allowStateLoss) { + if (!allowStateLoss) { + checkStateLoss(); + } + synchronized (this) { + if (mDestroyed || mHost == null) { + if (allowStateLoss) { + // This FragmentManager isn't attached, so drop the entire transaction. + return; + } + throw new IllegalStateException("Activity has been destroyed"); + } + if (mPendingActions == null) { + mPendingActions = new ArrayList<>(); + } + mPendingActions.add(action); + scheduleCommit(); + } + } + + /** + * Schedules the execution when one hasn't been scheduled already. This should happen + * the first time {@link #enqueueAction(OpGenerator, boolean)} is called or when + * a postponed transaction has been started with + * {@link Fragment#startPostponedEnterTransition()} + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void scheduleCommit() { + synchronized (this) { + boolean postponeReady = + mPostponedTransactions != null && !mPostponedTransactions.isEmpty(); + boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1; + if (postponeReady || pendingReady) { + mHost.getHandler().removeCallbacks(mExecCommit); + mHost.getHandler().post(mExecCommit); + updateOnBackPressedCallbackEnabled(); + } + } + } + + public int allocBackStackIndex(BackStackRecord bse) { + synchronized (this) { + if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) { + if (mBackStackIndices == null) { + mBackStackIndices = new ArrayList(); + } + int index = mBackStackIndices.size(); + if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse); + mBackStackIndices.add(bse); + return index; + + } else { + int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1); + if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse); + mBackStackIndices.set(index, bse); + return index; + } + } + } + + public void setBackStackIndex(int index, BackStackRecord bse) { + synchronized (this) { + if (mBackStackIndices == null) { + mBackStackIndices = new ArrayList(); + } + int N = mBackStackIndices.size(); + if (index < N) { + if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse); + mBackStackIndices.set(index, bse); + } else { + while (N < index) { + mBackStackIndices.add(null); + if (mAvailBackStackIndices == null) { + mAvailBackStackIndices = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "Adding available back stack index " + N); + mAvailBackStackIndices.add(N); + N++; + } + if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse); + mBackStackIndices.add(bse); + } + } + } + + public void freeBackStackIndex(int index) { + synchronized (this) { + mBackStackIndices.set(index, null); + if (mAvailBackStackIndices == null) { + mAvailBackStackIndices = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "Freeing back stack index " + index); + mAvailBackStackIndices.add(index); + } + } + + /** + * Broken out from exec*, this prepares for gathering and executing operations. + * + * @param allowStateLoss true if state loss should be ignored or false if it should be + * checked. + */ + private void ensureExecReady(boolean allowStateLoss) { + if (mExecutingActions) { + throw new IllegalStateException("FragmentManager is already executing transactions"); + } + + if (mHost == null) { + throw new IllegalStateException("Fragment host has been destroyed"); + } + + if (Looper.myLooper() != mHost.getHandler().getLooper()) { + throw new IllegalStateException("Must be called from main thread of fragment host"); + } + + if (!allowStateLoss) { + checkStateLoss(); + } + + if (mTmpRecords == null) { + mTmpRecords = new ArrayList<>(); + mTmpIsPop = new ArrayList<>(); + } + mExecutingActions = true; + try { + executePostponedTransaction(null, null); + } finally { + mExecutingActions = false; + } + } + + public void execSingleAction(OpGenerator action, boolean allowStateLoss) { + if (allowStateLoss && (mHost == null || mDestroyed)) { + // This FragmentManager isn't attached, so drop the entire transaction. + return; + } + ensureExecReady(allowStateLoss); + if (action.generateOps(mTmpRecords, mTmpIsPop)) { + mExecutingActions = true; + try { + removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop); + } finally { + cleanupExec(); + } + } + + updateOnBackPressedCallbackEnabled(); + doPendingDeferredStart(); + burpActive(); + } + + /** + * Broken out of exec*, this cleans up the mExecutingActions and the temporary structures + * used in executing operations. + */ + private void cleanupExec() { + mExecutingActions = false; + mTmpIsPop.clear(); + mTmpRecords.clear(); + } + + /** + * Only call from main thread! + */ + public boolean execPendingActions() { + ensureExecReady(true); + + boolean didSomething = false; + while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) { + mExecutingActions = true; + try { + removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop); + } finally { + cleanupExec(); + } + didSomething = true; + } + + updateOnBackPressedCallbackEnabled(); + doPendingDeferredStart(); + burpActive(); + + return didSomething; + } + + /** + * Complete the execution of transactions that have previously been postponed, but are + * now ready. + */ + private void executePostponedTransaction(ArrayList records, + ArrayList isRecordPop) { + int numPostponed = mPostponedTransactions == null ? 0 : mPostponedTransactions.size(); + for (int i = 0; i < numPostponed; i++) { + StartEnterTransitionListener listener = mPostponedTransactions.get(i); + if (records != null && !listener.mIsBack) { + int index = records.indexOf(listener.mRecord); + if (index != -1 && isRecordPop.get(index)) { + mPostponedTransactions.remove(i); + i--; + numPostponed--; + listener.cancelTransaction(); + continue; + } + } + if (listener.isReady() || (records != null + && listener.mRecord.interactsWith(records, 0, records.size()))) { + mPostponedTransactions.remove(i); + i--; + numPostponed--; + int index; + if (records != null && !listener.mIsBack + && (index = records.indexOf(listener.mRecord)) != -1 + && isRecordPop.get(index)) { + // This is popping a postponed transaction + listener.cancelTransaction(); + } else { + listener.completeTransaction(); + } + } + } + } + + /** + * Remove redundant BackStackRecord operations and executes them. This method merges operations + * of proximate records that allow reordering. See + * {@link FragmentTransaction#setReorderingAllowed(boolean)}. + *

+ * For example, a transaction that adds to the back stack and then another that pops that + * back stack record will be optimized to remove the unnecessary operation. + *

+ * Likewise, two transactions committed that are executed at the same time will be optimized + * to remove the redundant operations as well as two pop operations executed together. + * + * @param records The records pending execution + * @param isRecordPop The direction that these records are being run. + */ + private void removeRedundantOperationsAndExecute(ArrayList records, + ArrayList isRecordPop) { + if (records == null || records.isEmpty()) { + return; + } + + if (isRecordPop == null || records.size() != isRecordPop.size()) { + throw new IllegalStateException("Internal error with the back stack records"); + } + + // Force start of any postponed transactions that interact with scheduled transactions: + executePostponedTransaction(records, isRecordPop); + + final int numRecords = records.size(); + int startIndex = 0; + for (int recordNum = 0; recordNum < numRecords; recordNum++) { + final boolean canReorder = records.get(recordNum).mReorderingAllowed; + if (!canReorder) { + // execute all previous transactions + if (startIndex != recordNum) { + executeOpsTogether(records, isRecordPop, startIndex, recordNum); + } + // execute all pop operations that don't allow reordering together or + // one add operation + int reorderingEnd = recordNum + 1; + if (isRecordPop.get(recordNum)) { + while (reorderingEnd < numRecords + && isRecordPop.get(reorderingEnd) + && !records.get(reorderingEnd).mReorderingAllowed) { + reorderingEnd++; + } + } + executeOpsTogether(records, isRecordPop, recordNum, reorderingEnd); + startIndex = reorderingEnd; + recordNum = reorderingEnd - 1; + } + } + if (startIndex != numRecords) { + executeOpsTogether(records, isRecordPop, startIndex, numRecords); + } + } + + /** + * Executes a subset of a list of BackStackRecords, all of which either allow reordering or + * do not allow ordering. + * @param records A list of BackStackRecords that are to be executed + * @param isRecordPop The direction that these records are being run. + * @param startIndex The index of the first record in records to be executed + * @param endIndex One more than the final record index in records to executed. + */ + private void executeOpsTogether(ArrayList records, + ArrayList isRecordPop, int startIndex, int endIndex) { + final boolean allowReordering = records.get(startIndex).mReorderingAllowed; + boolean addToBackStack = false; + if (mTmpAddedFragments == null) { + mTmpAddedFragments = new ArrayList<>(); + } else { + mTmpAddedFragments.clear(); + } + mTmpAddedFragments.addAll(mAdded); + Fragment oldPrimaryNav = getPrimaryNavigationFragment(); + for (int recordNum = startIndex; recordNum < endIndex; recordNum++) { + final BackStackRecord record = records.get(recordNum); + final boolean isPop = isRecordPop.get(recordNum); + if (!isPop) { + oldPrimaryNav = record.expandOps(mTmpAddedFragments, oldPrimaryNav); + } else { + oldPrimaryNav = record.trackAddedFragmentsInPop(mTmpAddedFragments, oldPrimaryNav); + } + addToBackStack = addToBackStack || record.mAddToBackStack; + } + mTmpAddedFragments.clear(); + + if (!allowReordering) { + FragmentTransition.startTransitions(this, records, isRecordPop, startIndex, endIndex, + false); + } + executeOps(records, isRecordPop, startIndex, endIndex); + + int postponeIndex = endIndex; + if (allowReordering) { + ArraySet addedFragments = new ArraySet<>(); + addAddedFragments(addedFragments); + postponeIndex = postponePostponableTransactions(records, isRecordPop, + startIndex, endIndex, addedFragments); + makeRemovedFragmentsInvisible(addedFragments); + } + + if (postponeIndex != startIndex && allowReordering) { + // need to run something now + FragmentTransition.startTransitions(this, records, isRecordPop, startIndex, + postponeIndex, true); + moveToState(mCurState, true); + } + + for (int recordNum = startIndex; recordNum < endIndex; recordNum++) { + final BackStackRecord record = records.get(recordNum); + final boolean isPop = isRecordPop.get(recordNum); + if (isPop && record.mIndex >= 0) { + freeBackStackIndex(record.mIndex); + record.mIndex = -1; + } + record.runOnCommitRunnables(); + } + if (addToBackStack) { + reportBackStackChanged(); + } + } + + /** + * Any fragments that were removed because they have been postponed should have their views + * made invisible by setting their alpha to 0. + * + * @param fragments The fragments that were added during operation execution. Only the ones + * that are no longer added will have their alpha changed. + */ + private void makeRemovedFragmentsInvisible(ArraySet fragments) { + final int numAdded = fragments.size(); + for (int i = 0; i < numAdded; i++) { + final Fragment fragment = fragments.valueAt(i); + if (!fragment.mAdded) { + final View view = fragment.requireView(); + fragment.mPostponedAlpha = view.getAlpha(); + view.setAlpha(0f); + } + } + } + + /** + * Examine all transactions and determine which ones are marked as postponed. Those will + * have their operations rolled back and moved to the end of the record list (up to endIndex). + * It will also add the postponed transaction to the queue. + * + * @param records A list of BackStackRecords that should be checked. + * @param isRecordPop The direction that these records are being run. + * @param startIndex The index of the first record in records to be checked + * @param endIndex One more than the final record index in records to be checked. + * @return The index of the first postponed transaction or endIndex if no transaction was + * postponed. + */ + private int postponePostponableTransactions(ArrayList records, + ArrayList isRecordPop, int startIndex, int endIndex, + ArraySet added) { + int postponeIndex = endIndex; + for (int i = endIndex - 1; i >= startIndex; i--) { + final BackStackRecord record = records.get(i); + final boolean isPop = isRecordPop.get(i); + boolean isPostponed = record.isPostponed() + && !record.interactsWith(records, i + 1, endIndex); + if (isPostponed) { + if (mPostponedTransactions == null) { + mPostponedTransactions = new ArrayList<>(); + } + StartEnterTransitionListener listener = + new StartEnterTransitionListener(record, isPop); + mPostponedTransactions.add(listener); + record.setOnStartPostponedListener(listener); + + // roll back the transaction + if (isPop) { + record.executeOps(); + } else { + record.executePopOps(false); + } + + // move to the end + postponeIndex--; + if (i != postponeIndex) { + records.remove(i); + records.add(postponeIndex, record); + } + + // different views may be visible now + addAddedFragments(added); + } + } + return postponeIndex; + } + + /** + * When a postponed transaction is ready to be started, this completes the transaction, + * removing, hiding, or showing views as well as starting the animations and transitions. + *

+ * {@code runtransitions} is set to false when the transaction postponement was interrupted + * abnormally -- normally by a new transaction being started that affects the postponed + * transaction. + * + * @param record The transaction to run + * @param isPop true if record is popping or false if it is adding + * @param runTransitions true if the fragment transition should be run or false otherwise. + * @param moveToState true if the state should be changed after executing the operations. + * This is false when the transaction is canceled when a postponed + * transaction is popped. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void completeExecute(BackStackRecord record, boolean isPop, boolean runTransitions, + boolean moveToState) { + if (isPop) { + record.executePopOps(moveToState); + } else { + record.executeOps(); + } + ArrayList records = new ArrayList<>(1); + ArrayList isRecordPop = new ArrayList<>(1); + records.add(record); + isRecordPop.add(isPop); + if (runTransitions) { + FragmentTransition.startTransitions(this, records, isRecordPop, 0, 1, true); + } + if (moveToState) { + moveToState(mCurState, true); + } + + for (Fragment fragment : mActive.values()) { + // Allow added fragments to be removed during the pop since we aren't going + // to move them to the final state with moveToState(mCurState). + if (fragment != null && fragment.mView != null && fragment.mIsNewlyAdded + && record.interactsWith(fragment.mContainerId)) { + if (fragment.mPostponedAlpha > 0) { + fragment.mView.setAlpha(fragment.mPostponedAlpha); + } + if (moveToState) { + fragment.mPostponedAlpha = 0; + } else { + fragment.mPostponedAlpha = -1; + fragment.mIsNewlyAdded = false; + } + } + } + } + + /** + * Find a fragment within the fragment's container whose View should be below the passed + * fragment. {@code null} is returned when the fragment has no View or if there should be + * no fragment with a View below the given fragment. + * + * As an example, if mAdded has two Fragments with Views sharing the same container: + * FragmentA + * FragmentB + * + * Then, when processing FragmentB, FragmentA will be returned. If, however, FragmentA + * had no View, null would be returned. + * + * @param f The fragment that may be on top of another fragment. + * @return The fragment with a View under f, if one exists or null if f has no View or + * there are no fragments with Views in the same container. + */ + private Fragment findFragmentUnder(Fragment f) { + final ViewGroup container = f.mContainer; + final View view = f.mView; + + if (container == null || view == null) { + return null; + } + + final int fragmentIndex = mAdded.indexOf(f); + for (int i = fragmentIndex - 1; i >= 0; i--) { + Fragment underFragment = mAdded.get(i); + if (underFragment.mContainer == container && underFragment.mView != null) { + // Found the fragment under this one + return underFragment; + } + } + return null; + } + + /** + * Run the operations in the BackStackRecords, either to push or pop. + * + * @param records The list of records whose operations should be run. + * @param isRecordPop The direction that these records are being run. + * @param startIndex The index of the first entry in records to run. + * @param endIndex One past the index of the final entry in records to run. + */ + private static void executeOps(ArrayList records, + ArrayList isRecordPop, int startIndex, int endIndex) { + for (int i = startIndex; i < endIndex; i++) { + final BackStackRecord record = records.get(i); + final boolean isPop = isRecordPop.get(i); + if (isPop) { + record.bumpBackStackNesting(-1); + // Only execute the add operations at the end of + // all transactions. + boolean moveToState = i == (endIndex - 1); + record.executePopOps(moveToState); + } else { + record.bumpBackStackNesting(1); + record.executeOps(); + } + } + } + + /** + * Ensure that fragments that are added are moved to at least the CREATED state. + * Any newly-added Views are inserted into {@code added} so that the Transaction can be + * postponed with {@link Fragment#postponeEnterTransition()}. They will later be made + * invisible (by setting their alpha to 0) if they have been removed when postponed. + */ + private void addAddedFragments(ArraySet added) { + if (mCurState < Fragment.CREATED) { + return; + } + // We want to leave the fragment in the started state + final int state = Math.min(mCurState, Fragment.STARTED); + final int numAdded = mAdded.size(); + for (int i = 0; i < numAdded; i++) { + Fragment fragment = mAdded.get(i); + if (fragment.mState < state) { + moveToState(fragment, state, fragment.getNextAnim(), fragment.getNextTransition(), + false); + if (fragment.mView != null && !fragment.mHidden && fragment.mIsNewlyAdded) { + added.add(fragment); + } + } + } + } + + /** + * Starts all postponed transactions regardless of whether they are ready or not. + */ + private void forcePostponedTransactions() { + if (mPostponedTransactions != null) { + while (!mPostponedTransactions.isEmpty()) { + mPostponedTransactions.remove(0).completeTransaction(); + } + } + } + + /** + * Ends the animations of fragments so that they immediately reach the end state. + * This is used prior to saving the state so that the correct state is saved. + */ + private void endAnimatingAwayFragments() { + for (Fragment fragment : mActive.values()) { + if (fragment != null) { + if (fragment.getAnimatingAway() != null) { + // Give up waiting for the animation and just end it. + final int stateAfterAnimating = fragment.getStateAfterAnimating(); + final View animatingAway = fragment.getAnimatingAway(); + Animation animation = animatingAway.getAnimation(); + if (animation != null) { + animation.cancel(); + // force-clear the animation, as Animation#cancel() doesn't work prior to N, + // and will instead cause the animation to infinitely loop + animatingAway.clearAnimation(); + } + fragment.setAnimatingAway(null); + moveToState(fragment, stateAfterAnimating, 0, 0, false); + } else if (fragment.getAnimator() != null) { + fragment.getAnimator().end(); + } + } + } + } + + /** + * Adds all records in the pending actions to records and whether they are add or pop + * operations to isPop. After executing, the pending actions will be empty. + * + * @param records All pending actions will generate BackStackRecords added to this. + * This contains the transactions, in order, to execute. + * @param isPop All pending actions will generate booleans to add to this. This contains + * an entry for each entry in records to indicate whether or not it is a + * pop action. + */ + private boolean generateOpsForPendingActions(ArrayList records, + ArrayList isPop) { + boolean didSomething = false; + synchronized (this) { + if (mPendingActions == null || mPendingActions.size() == 0) { + return false; + } + + final int numActions = mPendingActions.size(); + for (int i = 0; i < numActions; i++) { + didSomething |= mPendingActions.get(i).generateOps(records, isPop); + } + mPendingActions.clear(); + mHost.getHandler().removeCallbacks(mExecCommit); + } + return didSomething; + } + + void doPendingDeferredStart() { + if (mHavePendingDeferredStart) { + mHavePendingDeferredStart = false; + startPendingDeferredFragments(); + } + } + + void reportBackStackChanged() { + if (mBackStackChangeListeners != null) { + for (int i=0; i(); + } + mBackStack.add(state); + } + + @SuppressWarnings("unused") + boolean popBackStackState(ArrayList records, ArrayList isRecordPop, + String name, int id, int flags) { + if (mBackStack == null) { + return false; + } + if (name == null && id < 0 && (flags & POP_BACK_STACK_INCLUSIVE) == 0) { + int last = mBackStack.size() - 1; + if (last < 0) { + return false; + } + records.add(mBackStack.remove(last)); + isRecordPop.add(true); + } else { + int index = -1; + if (name != null || id >= 0) { + // If a name or ID is specified, look for that place in + // the stack. + index = mBackStack.size()-1; + while (index >= 0) { + BackStackRecord bss = mBackStack.get(index); + if (name != null && name.equals(bss.getName())) { + break; + } + if (id >= 0 && id == bss.mIndex) { + break; + } + index--; + } + if (index < 0) { + return false; + } + if ((flags&POP_BACK_STACK_INCLUSIVE) != 0) { + index--; + // Consume all following entries that match. + while (index >= 0) { + BackStackRecord bss = mBackStack.get(index); + if ((name != null && name.equals(bss.getName())) + || (id >= 0 && id == bss.mIndex)) { + index--; + continue; + } + break; + } + } + } + if (index == mBackStack.size()-1) { + return false; + } + for (int i = mBackStack.size() - 1; i > index; i--) { + records.add(mBackStack.remove(i)); + isRecordPop.add(true); + } + } + return true; + } + + /** + * @deprecated Ideally, all {@link androidx.fragment.app.FragmentHostCallback} instances + * implement ViewModelStoreOwner and we can remove this method entirely. + */ + @Deprecated + FragmentManagerNonConfig retainNonConfig() { + if (mHost instanceof ViewModelStoreOwner) { + throwException(new IllegalStateException("You cannot use retainNonConfig when your " + + "FragmentHostCallback implements ViewModelStoreOwner.")); + } + return mNonConfig.getSnapshot(); + } + + void saveFragmentViewState(Fragment f) { + if (f.mInnerView == null) { + return; + } + if (mStateArray == null) { + mStateArray = new SparseArray(); + } else { + mStateArray.clear(); + } + f.mInnerView.saveHierarchyState(mStateArray); + if (mStateArray.size() > 0) { + f.mSavedViewState = mStateArray; + mStateArray = null; + } + } + + Bundle saveFragmentBasicState(Fragment f) { + Bundle result = null; + + if (mStateBundle == null) { + mStateBundle = new Bundle(); + } + f.performSaveInstanceState(mStateBundle); + dispatchOnFragmentSaveInstanceState(f, mStateBundle, false); + if (!mStateBundle.isEmpty()) { + result = mStateBundle; + mStateBundle = null; + } + + if (f.mView != null) { + saveFragmentViewState(f); + } + if (f.mSavedViewState != null) { + if (result == null) { + result = new Bundle(); + } + result.putSparseParcelableArray( + FragmentManagerImpl.VIEW_STATE_TAG, f.mSavedViewState); + } + if (!f.mUserVisibleHint) { + if (result == null) { + result = new Bundle(); + } + // Only add this if it's not the default value + result.putBoolean(FragmentManagerImpl.USER_VISIBLE_HINT_TAG, f.mUserVisibleHint); + } + + return result; + } + + Parcelable saveAllState() { + // Make sure all pending operations have now been executed to get + // our state update-to-date. + forcePostponedTransactions(); + endAnimatingAwayFragments(); + execPendingActions(); + + mStateSaved = true; + + if (mActive.isEmpty()) { + return null; + } + + // First collect all active fragments. + int size = mActive.size(); + ArrayList active = new ArrayList<>(size); + boolean haveFragments = false; + for (Fragment f : mActive.values()) { + if (f != null) { + if (f.mFragmentManager != this) { + throwException(new IllegalStateException( + "Failure saving state: active " + f + + " was removed from the FragmentManager")); + } + + haveFragments = true; + + FragmentState fs = new FragmentState(f); + active.add(fs); + + if (f.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = saveFragmentBasicState(f); + + if (f.mTargetWho != null) { + Fragment target = mActive.get(f.mTargetWho); + if (target == null) { + throwException(new IllegalStateException( + "Failure saving state: " + f + + " has target not in fragment manager: " + + f.mTargetWho)); + } + if (fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = new Bundle(); + } + putFragment(fs.mSavedFragmentState, + FragmentManagerImpl.TARGET_STATE_TAG, target); + if (f.mTargetRequestCode != 0) { + fs.mSavedFragmentState.putInt( + FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, + f.mTargetRequestCode); + } + } + + } else { + fs.mSavedFragmentState = f.mSavedFragmentState; + } + + if (DEBUG) Log.v(TAG, "Saved state of " + f + ": " + + fs.mSavedFragmentState); + } + } + + if (!haveFragments) { + if (DEBUG) Log.v(TAG, "saveAllState: no fragments!"); + return null; + } + + ArrayList added = null; + BackStackState[] backStack = null; + + // Build list of currently added fragments. + size = mAdded.size(); + if (size > 0) { + added = new ArrayList<>(size); + for (Fragment f : mAdded) { + added.add(f.mWho); + if (f.mFragmentManager != this) { + throwException(new IllegalStateException( + "Failure saving state: active " + f + + " was removed from the FragmentManager")); + } + if (DEBUG) { + Log.v(TAG, "saveAllState: adding fragment (" + f.mWho + + "): " + f); + } + } + } + + // Now save back stack. + if (mBackStack != null) { + size = mBackStack.size(); + if (size > 0) { + backStack = new BackStackState[size]; + for (int i = 0; i < size; i++) { + backStack[i] = new BackStackState(mBackStack.get(i)); + if (DEBUG) Log.v(TAG, "saveAllState: adding back stack #" + i + + ": " + mBackStack.get(i)); + } + } + } + + FragmentManagerState fms = new FragmentManagerState(); + fms.mActive = active; + fms.mAdded = added; + fms.mBackStack = backStack; + if (mPrimaryNav != null) { + fms.mPrimaryNavActiveWho = mPrimaryNav.mWho; + } + fms.mNextFragmentIndex = mNextFragmentIndex; + return fms; + } + + void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) { + if (mHost instanceof ViewModelStoreOwner) { + throwException(new IllegalStateException("You must use restoreSaveState when your " + + "FragmentHostCallback implements ViewModelStoreOwner")); + } + mNonConfig.restoreFromSnapshot(nonConfig); + restoreSaveState(state); + } + + void restoreSaveState(Parcelable state) { + // If there is no saved state at all, then there's nothing else to do + if (state == null) return; + FragmentManagerState fms = (FragmentManagerState)state; + if (fms.mActive == null) return; + + // First re-attach any non-config instances we are retaining back + // to their saved state, so we don't try to instantiate them again. + for (Fragment f : mNonConfig.getRetainedFragments()) { + if (DEBUG) Log.v(TAG, "restoreSaveState: re-attaching retained " + f); + FragmentState fs = null; + for (FragmentState fragmentState : fms.mActive) { + if (fragmentState.mWho.equals(f.mWho)) { + fs = fragmentState; + break; + } + } + if (fs == null) { + if (DEBUG) { + Log.v(TAG, "Discarding retained Fragment " + f + + " that was not found in the set of active Fragments " + fms.mActive); + } + // We need to ensure that onDestroy and any other clean up is done + // so move the Fragment up to CREATED, then mark it as being removed, then + // destroy it. + moveToState(f, Fragment.CREATED, 0, 0, false); + f.mRemoving = true; + moveToState(f, Fragment.INITIALIZING, 0, 0, false); + continue; + } + fs.mInstance = f; + f.mSavedViewState = null; + f.mBackStackNesting = 0; + f.mInLayout = false; + f.mAdded = false; + f.mTargetWho = f.mTarget != null ? f.mTarget.mWho : null; + f.mTarget = null; + if (fs.mSavedFragmentState != null) { + fs.mSavedFragmentState.setClassLoader(mHost.getContext().getClassLoader()); + f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray( + FragmentManagerImpl.VIEW_STATE_TAG); + f.mSavedFragmentState = fs.mSavedFragmentState; + } + } + + // Build the full list of active fragments, instantiating them from + // their saved state. + mActive.clear(); + for (FragmentState fs : fms.mActive) { + if (fs != null) { + Fragment f = fs.instantiate(mHost.getContext().getClassLoader(), + getFragmentFactory()); + f.mFragmentManager = this; + if (DEBUG) Log.v(TAG, "restoreSaveState: active (" + f.mWho + "): " + f); + mActive.put(f.mWho, f); + // Now that the fragment is instantiated (or came from being + // retained above), clear mInstance in case we end up re-restoring + // from this FragmentState again. + fs.mInstance = null; + } + } + + // Build the list of currently added fragments. + mAdded.clear(); + if (fms.mAdded != null) { + for (String who : fms.mAdded) { + Fragment f = mActive.get(who); + if (f == null) { + throwException(new IllegalStateException( + "No instantiated fragment for (" + who + ")")); + } + f.mAdded = true; + if (DEBUG) Log.v(TAG, "restoreSaveState: added (" + who + "): " + f); + if (mAdded.contains(f)) { + throw new IllegalStateException("Already added " + f); + } + synchronized (mAdded) { + mAdded.add(f); + } + } + } + + // Build the back stack. + if (fms.mBackStack != null) { + mBackStack = new ArrayList(fms.mBackStack.length); + for (int i=0; i= 0) { + setBackStackIndex(bse.mIndex, bse); + } + } + } else { + mBackStack = null; + } + + if (fms.mPrimaryNavActiveWho != null) { + mPrimaryNav = mActive.get(fms.mPrimaryNavActiveWho); + dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav); + } + this.mNextFragmentIndex = fms.mNextFragmentIndex; + } + + /** + * To prevent list modification errors, mActive sets values to null instead of + * removing them when the Fragment becomes inactive. This cleans up the list at the + * end of executing the transactions. + */ + private void burpActive() { + Collection values = mActive.values(); + // values() provides a view into the map, so removing elements from it + // removes the relevant pairs in the Map + values.removeAll(Collections.singleton(null)); + } + + public void attachController(@NonNull FragmentHostCallback host, + @NonNull FragmentContainer container, @Nullable final Fragment parent) { + if (mHost != null) throw new IllegalStateException("Already attached"); + mHost = host; + mContainer = container; + mParent = parent; + if (mParent != null) { + // Since the callback depends on us being the primary navigation fragment, + // update our callback now that we have a parent so that we have the correct + // state by default + updateOnBackPressedCallbackEnabled(); + } + // Set up the OnBackPressedCallback + if (host instanceof OnBackPressedDispatcherOwner) { + OnBackPressedDispatcherOwner dispatcherOwner = ((OnBackPressedDispatcherOwner) host); + mOnBackPressedDispatcher = dispatcherOwner.getOnBackPressedDispatcher(); + LifecycleOwner owner = parent != null ? parent : dispatcherOwner; + mOnBackPressedDispatcher.addCallback(owner, mOnBackPressedCallback); + } + + // Get the FragmentManagerViewModel + if (parent != null) { + mNonConfig = parent.mFragmentManager.getChildNonConfig(parent); + } else if (host instanceof ViewModelStoreOwner) { + ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore(); + mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore); + } else { + mNonConfig = new FragmentManagerViewModel(false); + } + } + + public void noteStateNotSaved() { + mStateSaved = false; + mStopped = false; + final int addedCount = mAdded.size(); + for (int i = 0; i < addedCount; i++) { + Fragment fragment = mAdded.get(i); + if (fragment != null) { + fragment.noteStateNotSaved(); + } + } + } + + public void dispatchCreate() { + mStateSaved = false; + mStopped = false; + dispatchStateChange(Fragment.CREATED); + } + + public void dispatchActivityCreated() { + mStateSaved = false; + mStopped = false; + dispatchStateChange(Fragment.ACTIVITY_CREATED); + } + + public void dispatchStart() { + mStateSaved = false; + mStopped = false; + dispatchStateChange(Fragment.STARTED); + } + + public void dispatchResume() { + mStateSaved = false; + mStopped = false; + dispatchStateChange(Fragment.RESUMED); + } + + public void dispatchPause() { + dispatchStateChange(Fragment.STARTED); + } + + public void dispatchStop() { + mStopped = true; + dispatchStateChange(Fragment.ACTIVITY_CREATED); + } + + public void dispatchDestroyView() { + dispatchStateChange(Fragment.CREATED); + } + + public void dispatchDestroy() { + mDestroyed = true; + execPendingActions(); + dispatchStateChange(Fragment.INITIALIZING); + mHost = null; + mContainer = null; + mParent = null; + if (mOnBackPressedDispatcher != null) { + // mOnBackPressedDispatcher can hold a reference to the host + // so we need to null it out to prevent memory leaks + mOnBackPressedCallback.remove(); + mOnBackPressedDispatcher = null; + } + } + + private void dispatchStateChange(int nextState) { + try { + mExecutingActions = true; + moveToState(nextState, false); + } finally { + mExecutingActions = false; + } + execPendingActions(); + } + + public void dispatchMultiWindowModeChanged(boolean isInMultiWindowMode) { + for (int i = mAdded.size() - 1; i >= 0; --i) { + final Fragment f = mAdded.get(i); + if (f != null) { + f.performMultiWindowModeChanged(isInMultiWindowMode); + } + } + } + + public void dispatchPictureInPictureModeChanged(boolean isInPictureInPictureMode) { + for (int i = mAdded.size() - 1; i >= 0; --i) { + final Fragment f = mAdded.get(i); + if (f != null) { + f.performPictureInPictureModeChanged(isInPictureInPictureMode); + } + } + } + + public void dispatchConfigurationChanged(@NonNull Configuration newConfig) { + for (int i = 0; i < mAdded.size(); i++) { + Fragment f = mAdded.get(i); + if (f != null) { + f.performConfigurationChanged(newConfig); + } + } + } + + public void dispatchLowMemory() { + for (int i = 0; i < mAdded.size(); i++) { + Fragment f = mAdded.get(i); + if (f != null) { + f.performLowMemory(); + } + } + } + + public boolean dispatchCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + if (mCurState < Fragment.CREATED) { + return false; + } + boolean show = false; + ArrayList newMenus = null; + for (int i = 0; i < mAdded.size(); i++) { + Fragment f = mAdded.get(i); + if (f != null) { + if (f.performCreateOptionsMenu(menu, inflater)) { + show = true; + if (newMenus == null) { + newMenus = new ArrayList(); + } + newMenus.add(f); + } + } + } + + if (mCreatedMenus != null) { + for (int i=0; i records, ArrayList isRecordPop); + } + + /** + * A pop operation OpGenerator. This will be run on the UI thread and will generate the + * transactions that will be popped if anything can be popped. + */ + private class PopBackStackState implements OpGenerator { + final String mName; + final int mId; + final int mFlags; + + PopBackStackState(String name, int id, int flags) { + mName = name; + mId = id; + mFlags = flags; + } + + @Override + public boolean generateOps(ArrayList records, + ArrayList isRecordPop) { + if (mPrimaryNav != null // We have a primary nav fragment + && mId < 0 // No valid id (since they're local) + && mName == null) { // no name to pop to (since they're local) + final FragmentManager childManager = mPrimaryNav.getChildFragmentManager(); + if (childManager.popBackStackImmediate()) { + // We didn't add any operations for this FragmentManager even though + // a child did do work. + return false; + } + } + return popBackStackState(records, isRecordPop, mName, mId, mFlags); + } + } + + /** + * A listener for a postponed transaction. This waits until + * {@link Fragment#startPostponedEnterTransition()} is called or a transaction is started + * that interacts with this one, based on interactions with the fragment container. + */ + static class StartEnterTransitionListener + implements Fragment.OnStartEnterTransitionListener { + final boolean mIsBack; + final BackStackRecord mRecord; + private int mNumPostponed; + + StartEnterTransitionListener(BackStackRecord record, boolean isBack) { + mIsBack = isBack; + mRecord = record; + } + + /** + * Called from {@link Fragment#startPostponedEnterTransition()}, this decreases the + * number of Fragments that are postponed. This may cause the transaction to schedule + * to finish running and run transitions and animations. + */ + @Override + public void onStartEnterTransition() { + mNumPostponed--; + if (mNumPostponed != 0) { + return; + } + mRecord.mManager.scheduleCommit(); + } + + /** + * Called from {@link Fragment# + * setOnStartEnterTransitionListener(Fragment.OnStartEnterTransitionListener)}, this + * increases the number of fragments that are postponed as part of this transaction. + */ + @Override + public void startListening() { + mNumPostponed++; + } + + /** + * @return true if there are no more postponed fragments as part of the transaction. + */ + public boolean isReady() { + return mNumPostponed == 0; + } + + /** + * Completes the transaction and start the animations and transitions. This may skip + * the transitions if this is called before all fragments have called + * {@link Fragment#startPostponedEnterTransition()}. + */ + public void completeTransaction() { + final boolean canceled; + canceled = mNumPostponed > 0; + FragmentManagerImpl manager = mRecord.mManager; + final int numAdded = manager.mAdded.size(); + for (int i = 0; i < numAdded; i++) { + final Fragment fragment = manager.mAdded.get(i); + fragment.setOnStartEnterTransitionListener(null); + if (canceled && fragment.isPostponed()) { + fragment.startPostponedEnterTransition(); + } + } + mRecord.mManager.completeExecute(mRecord, mIsBack, !canceled, true); + } + + /** + * Cancels this transaction instead of completing it. That means that the state isn't + * changed, so the pop results in no change to the state. + */ + public void cancelTransaction() { + mRecord.mManager.completeExecute(mRecord, mIsBack, false, false); + } + } + + /** + * Contains either an animator or animation. One of these should be null. + */ + private static class AnimationOrAnimator { + public final Animation animation; + public final Animator animator; + + AnimationOrAnimator(Animation animation) { + this.animation = animation; + this.animator = null; + if (animation == null) { + throw new IllegalStateException("Animation cannot be null"); + } + } + + AnimationOrAnimator(Animator animator) { + this.animation = null; + this.animator = animator; + if (animator == null) { + throw new IllegalStateException("Animator cannot be null"); + } + } + } + + /** + * We must call endViewTransition() before the animation ends or else the parent doesn't + * get nulled out. We use both startViewTransition() and startAnimation() to solve a problem + * with Views remaining in the hierarchy as disappearing children after the view has been + * removed in some edge cases. + */ + private static class EndViewTransitionAnimation extends AnimationSet implements Runnable { + private final ViewGroup mParent; + private final View mChild; + private boolean mEnded; + private boolean mTransitionEnded; + private boolean mAnimating = true; + + EndViewTransitionAnimation(@NonNull Animation animation, + @NonNull ViewGroup parent, @NonNull View child) { + super(false); + mParent = parent; + mChild = child; + addAnimation(animation); + // We must call endViewTransition() even if the animation was never run or it + // is interrupted in a way that can't be detected easily (app put in background) + mParent.post(this); + } + + @Override + public boolean getTransformation(long currentTime, Transformation t) { + mAnimating = true; + if (mEnded) { + return !mTransitionEnded; + } + boolean more = super.getTransformation(currentTime, t); + if (!more) { + mEnded = true; + OneShotPreDrawListener.add(mParent, this); + } + return true; + } + + @Override + public boolean getTransformation(long currentTime, Transformation outTransformation, + float scale) { + mAnimating = true; + if (mEnded) { + return !mTransitionEnded; + } + boolean more = super.getTransformation(currentTime, outTransformation, scale); + if (!more) { + mEnded = true; + OneShotPreDrawListener.add(mParent, this); + } + return true; + } + + @Override + public void run() { + if (!mEnded && mAnimating) { + mAnimating = false; + // Called while animating, so we'll check again on next cycle + mParent.post(this); + } else { + mParent.endViewTransition(mChild); + mTransitionEnded = true; + } + } + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerNonConfig.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerNonConfig.java new file mode 100644 index 000000000..d4352f16d --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerNonConfig.java @@ -0,0 +1,86 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.os.Parcelable; + +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelStore; + +import java.util.Collection; +import java.util.Map; + +/** + * FragmentManagerNonConfig stores the retained instance fragments across + * activity recreation events. + * + *

Apps should treat objects of this type as opaque, returned by + * and passed to the state save and restore process for fragments in + * {@link FragmentController#retainNestedNonConfig()} and + * {@link FragmentController#restoreAllState(Parcelable, FragmentManagerNonConfig)}.

+ * + * @deprecated Have your {@link FragmentHostCallback} implement + * {@link androidx.lifecycle.ViewModelStoreOwner} to automatically retain the Fragment's + * non configuration state. + */ +@Deprecated +public class FragmentManagerNonConfig { + private final @Nullable Collection mFragments; + private final @Nullable Map mChildNonConfigs; + private final @Nullable Map mViewModelStores; + + FragmentManagerNonConfig(@Nullable Collection fragments, + @Nullable Map childNonConfigs, + @Nullable Map viewModelStores) { + mFragments = fragments; + mChildNonConfigs = childNonConfigs; + mViewModelStores = viewModelStores; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + boolean isRetaining(Fragment f) { + if (mFragments == null) { + return false; + } + return mFragments.contains(f); + } + + /** + * @return the retained instance fragments returned by a FragmentManager + */ + @Nullable + Collection getFragments() { + return mFragments; + } + + /** + * @return the FragmentManagerNonConfigs from any applicable fragment's child FragmentManager + */ + @Nullable + Map getChildNonConfigs() { + return mChildNonConfigs; + } + + /** + * @return the ViewModelStores for all fragments associated with the FragmentManager + */ + @Nullable + Map getViewModelStores() { + return mViewModelStores; + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerState.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerState.java new file mode 100644 index 000000000..969c3bc20 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerState.java @@ -0,0 +1,70 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.annotation.SuppressLint; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; + +@SuppressLint("BanParcelableUsage") +final class FragmentManagerState implements Parcelable { + ArrayList mActive; + ArrayList mAdded; + BackStackState[] mBackStack; + String mPrimaryNavActiveWho = null; + int mNextFragmentIndex; + + public FragmentManagerState() { + } + + public FragmentManagerState(Parcel in) { + mActive = in.createTypedArrayList(FragmentState.CREATOR); + mAdded = in.createStringArrayList(); + mBackStack = in.createTypedArray(BackStackState.CREATOR); + mPrimaryNavActiveWho = in.readString(); + mNextFragmentIndex = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedList(mActive); + dest.writeStringList(mAdded); + dest.writeTypedArray(mBackStack, flags); + dest.writeString(mPrimaryNavActiveWho); + dest.writeInt(mNextFragmentIndex); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public FragmentManagerState createFromParcel(Parcel in) { + return new FragmentManagerState(in); + } + + @Override + public FragmentManagerState[] newArray(int size) { + return new FragmentManagerState[size]; + } + }; +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerViewModel.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerViewModel.java new file mode 100644 index 000000000..b466f6d0c --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentManagerViewModel.java @@ -0,0 +1,280 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStore; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +/** + * FragmentManagerViewModel is the always up to date view of the Fragment's + * non configuration state + */ +class FragmentManagerViewModel extends ViewModel { + + private static final ViewModelProvider.Factory FACTORY = new ViewModelProvider.Factory() { + @NonNull + @Override + @SuppressWarnings("unchecked") + public T create(@NonNull Class modelClass) { + FragmentManagerViewModel viewModel = new FragmentManagerViewModel(true); + return (T) viewModel; + } + }; + + @NonNull + static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) { + ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore, + FACTORY); + return viewModelProvider.get(FragmentManagerViewModel.class); + } + + private final HashSet mRetainedFragments = new HashSet<>(); + private final HashMap mChildNonConfigs = new HashMap<>(); + private final HashMap mViewModelStores = new HashMap<>(); + + private final boolean mStateAutomaticallySaved; + // Only used when mStateAutomaticallySaved is true + private boolean mHasBeenCleared = false; + // Only used when mStateAutomaticallySaved is false + private boolean mHasSavedSnapshot = false; + + /** + * FragmentManagerViewModel simultaneously supports two modes: + *
    + *
  1. Automatically saved: in this model, it is assumed that the ViewModel is added to + * an appropriate {@link ViewModelStore} that has the same lifecycle as the + * FragmentManager and that {@link #onCleared()} indicates that the Fragment's host + * is being permanently destroyed.
  2. + *
  3. Not automatically saved: in this model, the FragmentManager is responsible for + * calling {@link #getSnapshot()} and later restoring the ViewModel with + * {@link #restoreFromSnapshot(FragmentManagerNonConfig)}.
  4. + *
+ * These states are mutually exclusive. + * + * @param stateAutomaticallySaved Whether the ViewModel will be automatically saved. + */ + FragmentManagerViewModel(boolean stateAutomaticallySaved) { + mStateAutomaticallySaved = stateAutomaticallySaved; + } + + @Override + protected void onCleared() { + if (FragmentManagerImpl.DEBUG) { + Log.d(FragmentManagerImpl.TAG, "onCleared called for " + this); + } + mHasBeenCleared = true; + } + + boolean isCleared() { + return mHasBeenCleared; + } + + boolean addRetainedFragment(@NonNull Fragment fragment) { + return mRetainedFragments.add(fragment); + } + + @NonNull + Collection getRetainedFragments() { + return mRetainedFragments; + } + + boolean shouldDestroy(@NonNull Fragment fragment) { + if (!mRetainedFragments.contains(fragment)) { + // Always destroy non-retained Fragments + return true; + } + if (mStateAutomaticallySaved) { + // If we automatically save our state, then only + // destroy a retained Fragment when we've been cleared + return mHasBeenCleared; + } else { + // Else, only destroy retained Fragments if they've + // been reaped before the state has been saved + return !mHasSavedSnapshot; + } + } + + boolean removeRetainedFragment(@NonNull Fragment fragment) { + return mRetainedFragments.remove(fragment); + } + + @NonNull + FragmentManagerViewModel getChildNonConfig(@NonNull Fragment f) { + FragmentManagerViewModel childNonConfig = mChildNonConfigs.get(f.mWho); + if (childNonConfig == null) { + childNonConfig = new FragmentManagerViewModel(mStateAutomaticallySaved); + mChildNonConfigs.put(f.mWho, childNonConfig); + } + return childNonConfig; + } + + @NonNull + ViewModelStore getViewModelStore(@NonNull Fragment f) { + ViewModelStore viewModelStore = mViewModelStores.get(f.mWho); + if (viewModelStore == null) { + viewModelStore = new ViewModelStore(); + mViewModelStores.put(f.mWho, viewModelStore); + } + return viewModelStore; + } + + void clearNonConfigState(@NonNull Fragment f) { + if (FragmentManagerImpl.DEBUG) { + Log.d(FragmentManagerImpl.TAG, "Clearing non-config state for " + f); + } + // Clear and remove the Fragment's child non config state + FragmentManagerViewModel childNonConfig = mChildNonConfigs.get(f.mWho); + if (childNonConfig != null) { + childNonConfig.onCleared(); + mChildNonConfigs.remove(f.mWho); + } + // Clear and remove the Fragment's ViewModelStore + ViewModelStore viewModelStore = mViewModelStores.get(f.mWho); + if (viewModelStore != null) { + viewModelStore.clear(); + mViewModelStores.remove(f.mWho); + } + } + + /** + * @deprecated Ideally, we only support mStateAutomaticallySaved = true and remove this + * code, alongside + * {@link FragmentController#restoreAllState(android.os.Parcelable, FragmentManagerNonConfig)}. + */ + @Deprecated + void restoreFromSnapshot(@Nullable FragmentManagerNonConfig nonConfig) { + mRetainedFragments.clear(); + mChildNonConfigs.clear(); + mViewModelStores.clear(); + if (nonConfig != null) { + Collection fragments = nonConfig.getFragments(); + if (fragments != null) { + mRetainedFragments.addAll(fragments); + } + Map childNonConfigs = nonConfig.getChildNonConfigs(); + if (childNonConfigs != null) { + for (Map.Entry entry : + childNonConfigs.entrySet()) { + FragmentManagerViewModel childViewModel = + new FragmentManagerViewModel(mStateAutomaticallySaved); + childViewModel.restoreFromSnapshot(entry.getValue()); + mChildNonConfigs.put(entry.getKey(), childViewModel); + } + } + Map viewModelStores = nonConfig.getViewModelStores(); + if (viewModelStores != null) { + mViewModelStores.putAll(viewModelStores); + } + } + mHasSavedSnapshot = false; + } + + /** + * @deprecated Ideally, we only support mStateAutomaticallySaved = true and remove this + * code, alongside {@link FragmentController#retainNestedNonConfig()}. + */ + @Deprecated + @Nullable + FragmentManagerNonConfig getSnapshot() { + if (mRetainedFragments.isEmpty() && mChildNonConfigs.isEmpty() + && mViewModelStores.isEmpty()) { + return null; + } + HashMap childNonConfigs = new HashMap<>(); + for (Map.Entry entry : mChildNonConfigs.entrySet()) { + FragmentManagerNonConfig childNonConfig = entry.getValue().getSnapshot(); + if (childNonConfig != null) { + childNonConfigs.put(entry.getKey(), childNonConfig); + } + } + + mHasSavedSnapshot = true; + if (mRetainedFragments.isEmpty() && childNonConfigs.isEmpty() + && mViewModelStores.isEmpty()) { + return null; + } + return new FragmentManagerNonConfig( + new ArrayList<>(mRetainedFragments), + childNonConfigs, + new HashMap<>(mViewModelStores)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FragmentManagerViewModel that = (FragmentManagerViewModel) o; + + return mRetainedFragments.equals(that.mRetainedFragments) + && mChildNonConfigs.equals(that.mChildNonConfigs) + && mViewModelStores.equals(that.mViewModelStores); + } + + @Override + public int hashCode() { + int result = mRetainedFragments.hashCode(); + result = 31 * result + mChildNonConfigs.hashCode(); + result = 31 * result + mViewModelStores.hashCode(); + return result; + } + + @NonNull + @Override + public String toString() { + StringBuilder sb = new StringBuilder("FragmentManagerViewModel{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append("} Fragments ("); + Iterator fragmentIterator = mRetainedFragments.iterator(); + while (fragmentIterator.hasNext()) { + sb.append(fragmentIterator.next()); + if (fragmentIterator.hasNext()) { + sb.append(", "); + } + } + sb.append(") Child Non Config ("); + Iterator childNonConfigIterator = mChildNonConfigs.keySet().iterator(); + while (childNonConfigIterator.hasNext()) { + sb.append(childNonConfigIterator.next()); + if (childNonConfigIterator.hasNext()) { + sb.append(", "); + } + } + sb.append(") ViewModelStores ("); + Iterator viewModelStoreIterator = mViewModelStores.keySet().iterator(); + while (viewModelStoreIterator.hasNext()) { + sb.append(viewModelStoreIterator.next()); + if (viewModelStoreIterator.hasNext()) { + sb.append(", "); + } + } + sb.append(')'); + return sb.toString(); + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentPagerAdapter.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentPagerAdapter.java new file mode 100644 index 000000000..d23a10757 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentPagerAdapter.java @@ -0,0 +1,271 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.os.Parcelable; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.viewpager.widget.PagerAdapter; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Implementation of {@link PagerAdapter} that + * represents each page as a {@link Fragment} that is persistently + * kept in the fragment manager as long as the user can return to the page. + * + *

This version of the pager is best for use when there are a handful of + * typically more static fragments to be paged through, such as a set of tabs. + * The fragment of each page the user visits will be kept in memory, though its + * view hierarchy may be destroyed when not visible. This can result in using + * a significant amount of memory since fragment instances can hold on to an + * arbitrary amount of state. For larger sets of pages, consider + * {@link FragmentStatePagerAdapter}. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentPagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager_list.xml + * complete} + */ +@SuppressWarnings("deprecation") +public abstract class FragmentPagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentPagerAdapter"; + private static final boolean DEBUG = false; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}) + private @interface Behavior { } + + /** + * Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current + * fragment changes. + * + * @deprecated This behavior relies on the deprecated + * {@link Fragment#setUserVisibleHint(boolean)} API. Use + * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement, + * {@link FragmentTransaction#setMaxLifecycle}. + * @see #FragmentPagerAdapter(FragmentManager, int) + */ + @Deprecated + public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0; + + /** + * Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED} + * state. All other Fragments are capped at {@link Lifecycle.State#STARTED}. + * + * @see #FragmentPagerAdapter(FragmentManager, int) + */ + public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1; + + private final FragmentManager mFragmentManager; + private final int mBehavior; + private FragmentTransaction mCurTransaction = null; + private Fragment mCurrentPrimaryItem = null; + + /** + * Constructor for {@link FragmentPagerAdapter} that sets the fragment manager for the adapter. + * This is the equivalent of calling {@link #FragmentPagerAdapter(FragmentManager, int)} and + * passing in {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}. + * + *

Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the + * current Fragment changes.

+ * + * @param fm fragment manager that will interact with this adapter + * @deprecated use {@link #FragmentPagerAdapter(FragmentManager, int)} with + * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} + */ + @Deprecated + public FragmentPagerAdapter(@NonNull FragmentManager fm) { + this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT); + } + + /** + * Constructor for {@link FragmentPagerAdapter}. + * + * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current + * Fragment is in the {@link Lifecycle.State#RESUMED} state. All other fragments are capped at + * {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is passed, all + * fragments are in the {@link Lifecycle.State#RESUMED} state and there will be callbacks to + * {@link Fragment#setUserVisibleHint(boolean)}. + * + * @param fm fragment manager that will interact with this adapter + * @param behavior determines if only current fragments are in a resumed state + */ + public FragmentPagerAdapter(@NonNull FragmentManager fm, + @Behavior int behavior) { + mFragmentManager = fm; + mBehavior = behavior; + } + + /** + * 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"); + } + } + + @SuppressWarnings({"ReferenceEquality", "deprecation"}) + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + final long itemId = getItemId(position); + + // Do we already have this fragment? + String name = makeFragmentName(container.getId(), itemId); + Fragment fragment = mFragmentManager.findFragmentByTag(name); + if (fragment != null) { + if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); + mCurTransaction.attach(fragment); + } else { + fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); + mCurTransaction.add(container.getId(), fragment, + makeFragmentName(container.getId(), itemId)); + } + if (fragment != mCurrentPrimaryItem) { + fragment.setMenuVisibility(false); + if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED); + } else { + fragment.setUserVisibleHint(false); + } + } + + return fragment; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + Fragment fragment = (Fragment) object; + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object + + " v=" + (fragment.getView())); + mCurTransaction.detach(fragment); + if (fragment == mCurrentPrimaryItem) { + mCurrentPrimaryItem = null; + } + } + + @SuppressWarnings({"ReferenceEquality", "deprecation"}) + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + mCurrentPrimaryItem.setMenuVisibility(false); + if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED); + } else { + mCurrentPrimaryItem.setUserVisibleHint(false); + } + } + fragment.setMenuVisibility(true); + if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED); + } else { + fragment.setUserVisibleHint(true); + } + + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(@NonNull ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitNowAllowingStateLoss(); + mCurTransaction = null; + } + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + @Nullable + public Parcelable saveState() { + return null; + } + + @Override + public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) { + } + + /** + * Return a unique identifier for the item at the given position. + * + *

The default implementation returns the given position. + * Subclasses should override this method if the positions of items can change.

+ * + * @param position Position within this adapter + * @return Unique identifier for the item at position + */ + public long getItemId(int position) { + return position; + } + + private static String makeFragmentName(int viewId, long id) { + return "android:switcher:" + viewId + ":" + id; + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentState.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentState.java new file mode 100644 index 000000000..f8947c3f0 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentState.java @@ -0,0 +1,185 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.annotation.SuppressLint; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; + +@SuppressLint("BanParcelableUsage") +final class FragmentState implements Parcelable { + final String mClassName; + final String mWho; + final boolean mFromLayout; + final int mFragmentId; + final int mContainerId; + final String mTag; + final boolean mRetainInstance; + final boolean mRemoving; + final boolean mDetached; + final Bundle mArguments; + final boolean mHidden; + final int mMaxLifecycleState; + + Bundle mSavedFragmentState; + + Fragment mInstance; + + FragmentState(Fragment frag) { + mClassName = frag.getClass().getName(); + mWho = frag.mWho; + mFromLayout = frag.mFromLayout; + mFragmentId = frag.mFragmentId; + mContainerId = frag.mContainerId; + mTag = frag.mTag; + mRetainInstance = frag.mRetainInstance; + mRemoving = frag.mRemoving; + mDetached = frag.mDetached; + mArguments = frag.mArguments; + mHidden = frag.mHidden; + mMaxLifecycleState = frag.mMaxState.ordinal(); + } + + FragmentState(Parcel in) { + mClassName = in.readString(); + mWho = in.readString(); + mFromLayout = in.readInt() != 0; + mFragmentId = in.readInt(); + mContainerId = in.readInt(); + mTag = in.readString(); + mRetainInstance = in.readInt() != 0; + mRemoving = in.readInt() != 0; + mDetached = in.readInt() != 0; + mArguments = in.readBundle(); + mHidden = in.readInt() != 0; + mSavedFragmentState = in.readBundle(); + mMaxLifecycleState = in.readInt(); + } + + public Fragment instantiate(@NonNull ClassLoader classLoader, + @NonNull FragmentFactory factory) { + if (mInstance == null) { + if (mArguments != null) { + mArguments.setClassLoader(classLoader); + } + + mInstance = factory.instantiate(classLoader, mClassName); + mInstance.setArguments(mArguments); + + if (mSavedFragmentState != null) { + mSavedFragmentState.setClassLoader(classLoader); + mInstance.mSavedFragmentState = mSavedFragmentState; + } else { + // When restoring a Fragment, always ensure we have a + // non-null Bundle so that developers have a signal for + // when the Fragment is being restored + mInstance.mSavedFragmentState = new Bundle(); + } + mInstance.mWho = mWho; + mInstance.mFromLayout = mFromLayout; + mInstance.mRestored = true; + mInstance.mFragmentId = mFragmentId; + mInstance.mContainerId = mContainerId; + mInstance.mTag = mTag; + mInstance.mRetainInstance = mRetainInstance; + mInstance.mRemoving = mRemoving; + mInstance.mDetached = mDetached; + mInstance.mHidden = mHidden; + mInstance.mMaxState = Lifecycle.State.values()[mMaxLifecycleState]; + + if (FragmentManagerImpl.DEBUG) { + Log.v(FragmentManagerImpl.TAG, "Instantiated fragment " + mInstance); + } + } + return mInstance; + } + + @NonNull + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("FragmentState{"); + sb.append(mClassName); + sb.append(" ("); + sb.append(mWho); + sb.append(")}:"); + if (mFromLayout) { + sb.append(" fromLayout"); + } + if (mContainerId != 0) { + sb.append(" id=0x"); + sb.append(Integer.toHexString(mContainerId)); + } + if (mTag != null && !mTag.isEmpty()) { + sb.append(" tag="); + sb.append(mTag); + } + if (mRetainInstance) { + sb.append(" retainInstance"); + } + if (mRemoving) { + sb.append(" removing"); + } + if (mDetached) { + sb.append(" detached"); + } + if (mHidden) { + sb.append(" hidden"); + } + return sb.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mClassName); + dest.writeString(mWho); + dest.writeInt(mFromLayout ? 1 : 0); + dest.writeInt(mFragmentId); + dest.writeInt(mContainerId); + dest.writeString(mTag); + dest.writeInt(mRetainInstance ? 1 : 0); + dest.writeInt(mRemoving ? 1 : 0); + dest.writeInt(mDetached ? 1 : 0); + dest.writeBundle(mArguments); + dest.writeInt(mHidden ? 1 : 0); + dest.writeBundle(mSavedFragmentState); + dest.writeInt(mMaxLifecycleState); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public FragmentState createFromParcel(Parcel in) { + return new FragmentState(in); + } + + @Override + public FragmentState[] newArray(int size) { + return new FragmentState[size]; + } + }; +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentStatePagerAdapter.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentStatePagerAdapter.java new file mode 100644 index 000000000..1e3eef059 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentStatePagerAdapter.java @@ -0,0 +1,323 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.viewpager.widget.PagerAdapter; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +/** + * Implementation of {@link PagerAdapter} that + * uses a {@link Fragment} to manage each page. This class also handles + * saving and restoring of fragment's state. + * + *

This version of the pager is more useful when there are a large number + * of pages, working more like a list view. When pages are not visible to + * the user, their entire fragment may be destroyed, only keeping the saved + * state of that fragment. This allows the pager to hold on to much less + * memory associated with each visited page as compared to + * {@link FragmentPagerAdapter} at the cost of potentially more overhead when + * switching between pages. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample frameworks/support/samples/Support4Demos/src/main/java/com/example/android/supportv4/app/FragmentStatePagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample frameworks/support/samples/Support4Demos/src/main/res/layout/fragment_pager_list.xml + * complete} + */ +@SuppressWarnings("deprecation") +public abstract class FragmentStatePagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentStatePagerAdapt"; + private static final boolean DEBUG = false; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}) + private @interface Behavior { } + + /** + * Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current + * fragment changes. + * + * @deprecated This behavior relies on the deprecated + * {@link Fragment#setUserVisibleHint(boolean)} API. Use + * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement, + * {@link FragmentTransaction#setMaxLifecycle}. + * @see #FragmentStatePagerAdapter(FragmentManager, int) + */ + @Deprecated + public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0; + + /** + * Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED} + * state. All other Fragments are capped at {@link Lifecycle.State#STARTED}. + * + * @see #FragmentStatePagerAdapter(FragmentManager, int) + */ + public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1; + + private final FragmentManager mFragmentManager; + private final int mBehavior; + private FragmentTransaction mCurTransaction = null; + + private ArrayList mSavedState = new ArrayList(); + private ArrayList mFragments = new ArrayList(); + private Fragment mCurrentPrimaryItem = null; + + /** + * Constructor for {@link FragmentStatePagerAdapter} that sets the fragment manager for the + * adapter. This is the equivalent of calling + * {@link #FragmentStatePagerAdapter(FragmentManager, int)} and passing in + * {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}. + * + *

Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the + * current Fragment changes.

+ * + * @param fm fragment manager that will interact with this adapter + * @deprecated use {@link #FragmentStatePagerAdapter(FragmentManager, int)} with + * {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} + */ + @Deprecated + public FragmentStatePagerAdapter(@NonNull FragmentManager fm) { + this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT); + } + + /** + * Constructor for {@link FragmentStatePagerAdapter}. + * + * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current + * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are + * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is + * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be + * callbacks to {@link Fragment#setUserVisibleHint(boolean)}. + * + * @param fm fragment manager that will interact with this adapter + * @param behavior determines if only current fragments are in a resumed state + */ + public FragmentStatePagerAdapter(@NonNull FragmentManager fm, + @Behavior int behavior) { + mFragmentManager = fm; + mBehavior = behavior; + } + + /** + * 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"); + } + } + + @SuppressWarnings("deprecation") + @NonNull + @Override + public Object instantiateItem(@NonNull ViewGroup container, int position) { + // If we already have this item instantiated, there is nothing + // to do. This can happen when we are restoring the entire pager + // from its saved state, where the fragment manager has already + // taken care of restoring the fragments we previously had instantiated. + if (mFragments.size() > position) { + Fragment f = mFragments.get(position); + if (f != null) { + return f; + } + } + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + Fragment fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); + if (mSavedState.size() > position) { + Fragment.SavedState fss = mSavedState.get(position); + if (fss != null) { + fragment.setInitialSavedState(fss); + } + } + while (mFragments.size() <= position) { + mFragments.add(null); + } + fragment.setMenuVisibility(false); + if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) { + fragment.setUserVisibleHint(false); + } + + mFragments.set(position, fragment); + mCurTransaction.add(container.getId(), fragment); + + if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED); + } + + return fragment; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + Fragment fragment = (Fragment) object; + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + + " v=" + ((Fragment)object).getView()); + while (mSavedState.size() <= position) { + mSavedState.add(null); + } + mSavedState.set(position, fragment.isAdded() + ? mFragmentManager.saveFragmentInstanceState(fragment) : null); + mFragments.set(position, null); + + mCurTransaction.remove(fragment); + if (fragment == mCurrentPrimaryItem) { + mCurrentPrimaryItem = null; + } + } + + @Override + @SuppressWarnings({"ReferenceEquality", "deprecation"}) + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + mCurrentPrimaryItem.setMenuVisibility(false); + if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED); + } else { + mCurrentPrimaryItem.setUserVisibleHint(false); + } + } + fragment.setMenuVisibility(true); + if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED); + } else { + fragment.setUserVisibleHint(true); + } + + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(@NonNull ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitNowAllowingStateLoss(); + mCurTransaction = null; + } + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + @Nullable + 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) { + while (mFragments.size() <= index) { + mFragments.add(null); + } + f.setMenuVisibility(false); + mFragments.set(index, f); + } else { + Log.w(TAG, "Bad fragment at key " + key); + } + } + } + } + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTabHost.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTabHost.java new file mode 100644 index 000000000..f006b7004 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTabHost.java @@ -0,0 +1,441 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TabHost; +import android.widget.TabWidget; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; + +/** + * Special TabHost that allows the use of {@link Fragment} objects for + * its tab content. When placing this in a view hierarchy, after inflating + * the hierarchy you must call {@link #setup(Context, FragmentManager, int)} + * to complete the initialization of the tab host. + * + * @deprecated Use + * TabLayout and ViewPager instead. + */ +@Deprecated +public class FragmentTabHost extends TabHost + implements TabHost.OnTabChangeListener { + private final ArrayList mTabs = new ArrayList<>(); + + private FrameLayout mRealTabContent; + private Context mContext; + private FragmentManager mFragmentManager; + private int mContainerId; + private TabHost.OnTabChangeListener mOnTabChangeListener; + private TabInfo mLastTab; + private boolean mAttached; + + static final class TabInfo { + final @NonNull String tag; + final @NonNull Class clss; + final @Nullable Bundle args; + Fragment fragment; + + TabInfo(@NonNull String _tag, @NonNull Class _class, @Nullable Bundle _args) { + tag = _tag; + clss = _class; + args = _args; + } + } + + static class DummyTabFactory implements TabHost.TabContentFactory { + private final Context mContext; + + public DummyTabFactory(Context context) { + mContext = context; + } + + @Override + public View createTabContent(String tag) { + View v = new View(mContext); + v.setMinimumWidth(0); + v.setMinimumHeight(0); + return v; + } + } + + static class SavedState extends BaseSavedState { + String curTab; + + SavedState(Parcelable superState) { + super(superState); + } + + SavedState(Parcel in) { + super(in); + curTab = in.readString(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(curTab); + } + + @Override + public String toString() { + return "FragmentTabHost.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " curTab=" + curTab + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + public FragmentTabHost(@NonNull Context context) { + // Note that we call through to the version that takes an AttributeSet, + // because the simple Context construct can result in a broken object! + super(context, null); + initFragmentTabHost(context, null); + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + public FragmentTabHost(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initFragmentTabHost(context, attrs); + } + + private void initFragmentTabHost(Context context, AttributeSet attrs) { + final TypedArray a = context.obtainStyledAttributes(attrs, + new int[] { android.R.attr.inflatedId }, 0, 0); + mContainerId = a.getResourceId(0, 0); + a.recycle(); + + super.setOnTabChangedListener(this); + } + + private void ensureHierarchy(Context context) { + // If owner hasn't made its own view hierarchy, then as a convenience + // we will construct a standard one here. + if (findViewById(android.R.id.tabs) == null) { + LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.VERTICAL); + addView(ll, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + TabWidget tw = new TabWidget(context); + tw.setId(android.R.id.tabs); + tw.setOrientation(TabWidget.HORIZONTAL); + ll.addView(tw, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0)); + + FrameLayout fl = new FrameLayout(context); + fl.setId(android.R.id.tabcontent); + ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0)); + + mRealTabContent = fl = new FrameLayout(context); + mRealTabContent.setId(mContainerId); + ll.addView(fl, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 0, 1)); + } + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Override @Deprecated + public void setup() { + throw new IllegalStateException( + "Must call setup() that takes a Context and FragmentManager"); + } + + /** + * Set up the FragmentTabHost to use the given FragmentManager + * + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + public void setup(@NonNull Context context, @NonNull FragmentManager manager) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + ensureContent(); + } + + /** + * Set up the FragmentTabHost to use the given FragmentManager + * + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + public void setup(@NonNull Context context, @NonNull FragmentManager manager, + int containerId) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + mContainerId = containerId; + ensureContent(); + mRealTabContent.setId(containerId); + + // We must have an ID to be able to save/restore our state. If + // the owner hasn't set one at this point, we will set it ourselves. + if (getId() == View.NO_ID) { + setId(android.R.id.tabhost); + } + } + + private void ensureContent() { + if (mRealTabContent == null) { + mRealTabContent = (FrameLayout)findViewById(mContainerId); + if (mRealTabContent == null) { + throw new IllegalStateException( + "No tab content FrameLayout found for id " + mContainerId); + } + } + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + @Override + public void setOnTabChangedListener(@Nullable OnTabChangeListener l) { + mOnTabChangeListener = l; + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + public void addTab(@NonNull TabHost.TabSpec tabSpec, @NonNull Class clss, + @Nullable Bundle args) { + tabSpec.setContent(new DummyTabFactory(mContext)); + + final String tag = tabSpec.getTag(); + final TabInfo info = new TabInfo(tag, clss, args); + + if (mAttached) { + // If we are already attached to the window, then check to make + // sure this tab's fragment is inactive if it exists. This shouldn't + // normally happen. + info.fragment = mFragmentManager.findFragmentByTag(tag); + if (info.fragment != null && !info.fragment.isDetached()) { + final FragmentTransaction ft = mFragmentManager.beginTransaction(); + ft.detach(info.fragment); + ft.commit(); + } + } + + mTabs.add(info); + addTab(tabSpec); + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + final String currentTag = getCurrentTabTag(); + + // Go through all tabs and make sure their fragments match + // the correct state. + FragmentTransaction ft = null; + for (int i = 0, count = mTabs.size(); i < count; i++) { + final TabInfo tab = mTabs.get(i); + tab.fragment = mFragmentManager.findFragmentByTag(tab.tag); + if (tab.fragment != null && !tab.fragment.isDetached()) { + if (tab.tag.equals(currentTag)) { + // The fragment for this tab is already there and + // active, and it is what we really want to have + // as the current tab. Nothing to do. + mLastTab = tab; + } else { + // This fragment was restored in the active state, + // but is not the current tab. Deactivate it. + if (ft == null) { + ft = mFragmentManager.beginTransaction(); + } + ft.detach(tab.fragment); + } + } + } + + // We are now ready to go. Make sure we are switched to the + // correct tab. + mAttached = true; + ft = doTabChanged(currentTag, ft); + if (ft != null) { + ft.commit(); + mFragmentManager.executePendingTransactions(); + } + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mAttached = false; + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + @Override + @NonNull + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.curTab = getCurrentTabTag(); + return ss; + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + @Override + protected void onRestoreInstanceState(@SuppressLint("UnknownNullness") Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + setCurrentTabByTag(ss.curTab); + } + + /** + * @deprecated Use + * + * TabLayout and ViewPager instead. + */ + @Deprecated + @Override + public void onTabChanged(@Nullable String tabId) { + if (mAttached) { + final FragmentTransaction ft = doTabChanged(tabId, null); + if (ft != null) { + ft.commit(); + } + } + if (mOnTabChangeListener != null) { + mOnTabChangeListener.onTabChanged(tabId); + } + } + + @Nullable + private FragmentTransaction doTabChanged(@Nullable String tag, + @Nullable FragmentTransaction ft) { + final TabInfo newTab = getTabInfoForTag(tag); + if (mLastTab != newTab) { + if (ft == null) { + ft = mFragmentManager.beginTransaction(); + } + + if (mLastTab != null) { + if (mLastTab.fragment != null) { + ft.detach(mLastTab.fragment); + } + } + + if (newTab != null) { + if (newTab.fragment == null) { + newTab.fragment = mFragmentManager.getFragmentFactory().instantiate( + mContext.getClassLoader(), newTab.clss.getName()); + newTab.fragment.setArguments(newTab.args); + ft.add(mContainerId, newTab.fragment, newTab.tag); + } else { + ft.attach(newTab.fragment); + } + } + + mLastTab = newTab; + } + + return ft; + } + + @Nullable + private TabInfo getTabInfoForTag(String tabId) { + for (int i = 0, count = mTabs.size(); i < count; i++) { + final TabInfo tab = mTabs.get(i); + if (tab.tag.equals(tabId)) { + return tab; + } + } + return null; + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransaction.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransaction.java new file mode 100644 index 000000000..af91cf4d1 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransaction.java @@ -0,0 +1,731 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.AnimRes; +import androidx.annotation.AnimatorRes; +import androidx.annotation.IdRes; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.core.view.ViewCompat; +import androidx.lifecycle.Lifecycle; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Modifier; +import java.util.ArrayList; + +/** + * Static library support version of the framework's {@link android.app.FragmentTransaction}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public abstract class FragmentTransaction { + + static final int OP_NULL = 0; + static final int OP_ADD = 1; + static final int OP_REPLACE = 2; + static final int OP_REMOVE = 3; + static final int OP_HIDE = 4; + static final int OP_SHOW = 5; + static final int OP_DETACH = 6; + static final int OP_ATTACH = 7; + static final int OP_SET_PRIMARY_NAV = 8; + static final int OP_UNSET_PRIMARY_NAV = 9; + static final int OP_SET_MAX_LIFECYCLE = 10; + + static final class Op { + int mCmd; + Fragment mFragment; + int mEnterAnim; + int mExitAnim; + int mPopEnterAnim; + int mPopExitAnim; + Lifecycle.State mOldMaxState; + Lifecycle.State mCurrentMaxState; + + Op() { + } + + Op(int cmd, Fragment fragment) { + this.mCmd = cmd; + this.mFragment = fragment; + this.mOldMaxState = Lifecycle.State.RESUMED; + this.mCurrentMaxState = Lifecycle.State.RESUMED; + } + + Op(int cmd, @NonNull Fragment fragment, Lifecycle.State state) { + this.mCmd = cmd; + this.mFragment = fragment; + this.mOldMaxState = fragment.mMaxState; + this.mCurrentMaxState = state; + } + } + + ArrayList mOps = new ArrayList<>(); + int mEnterAnim; + int mExitAnim; + int mPopEnterAnim; + int mPopExitAnim; + int mTransition; + int mTransitionStyle; + boolean mAddToBackStack; + boolean mAllowAddToBackStack = true; + @Nullable String mName; + + int mBreadCrumbTitleRes; + CharSequence mBreadCrumbTitleText; + int mBreadCrumbShortTitleRes; + CharSequence mBreadCrumbShortTitleText; + + ArrayList mSharedElementSourceNames; + ArrayList mSharedElementTargetNames; + boolean mReorderingAllowed = false; + + ArrayList mCommitRunnables; + + void addOp(Op op) { + mOps.add(op); + op.mEnterAnim = mEnterAnim; + op.mExitAnim = mExitAnim; + op.mPopEnterAnim = mPopEnterAnim; + op.mPopExitAnim = mPopExitAnim; + } + + /** + * Calls {@link #add(int, Fragment, String)} with a 0 containerViewId. + */ + @NonNull + public FragmentTransaction add(@NonNull Fragment fragment, @Nullable String tag) { + doAddOp(0, fragment, tag, OP_ADD); + return this; + } + + /** + * Calls {@link #add(int, Fragment, String)} with a null tag. + */ + @NonNull + public FragmentTransaction add(@IdRes int containerViewId, @NonNull Fragment fragment) { + doAddOp(containerViewId, fragment, null, OP_ADD); + return this; + } + + /** + * Add a fragment to the activity state. This fragment may optionally + * also have its view (if {@link Fragment#onCreateView Fragment.onCreateView} + * returns non-null) into a container view of the activity. + * + * @param containerViewId Optional identifier of the container this fragment is + * to be placed in. If 0, it will not be placed in a container. + * @param fragment The fragment to be added. This fragment must not already + * be added to the activity. + * @param tag Optional tag name for the fragment, to later retrieve the + * fragment with {@link FragmentManager#findFragmentByTag(String) + * FragmentManager.findFragmentByTag(String)}. + * + * @return Returns the same FragmentTransaction instance. + */ + @NonNull + public FragmentTransaction add(@IdRes int containerViewId, @NonNull Fragment fragment, + @Nullable String tag) { + doAddOp(containerViewId, fragment, tag, OP_ADD); + return this; + } + + void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) { + final Class fragmentClass = fragment.getClass(); + final int modifiers = fragmentClass.getModifiers(); + if (fragmentClass.isAnonymousClass() || !Modifier.isPublic(modifiers) + || (fragmentClass.isMemberClass() && !Modifier.isStatic(modifiers))) { + throw new IllegalStateException("Fragment " + fragmentClass.getCanonicalName() + + " must be a public static class to be properly recreated from" + + " instance state."); + } + + if (tag != null) { + if (fragment.mTag != null && !tag.equals(fragment.mTag)) { + throw new IllegalStateException("Can't change tag of fragment " + + fragment + ": was " + fragment.mTag + + " now " + tag); + } + fragment.mTag = tag; + } + + if (containerViewId != 0) { + if (containerViewId == View.NO_ID) { + throw new IllegalArgumentException("Can't add fragment " + + fragment + " with tag " + tag + " to container view with no id"); + } + if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) { + throw new IllegalStateException("Can't change container ID of fragment " + + fragment + ": was " + fragment.mFragmentId + + " now " + containerViewId); + } + fragment.mContainerId = fragment.mFragmentId = containerViewId; + } + + addOp(new Op(opcmd, fragment)); + } + + /** + * Calls {@link #replace(int, Fragment, String)} with a null tag. + */ + @NonNull + public FragmentTransaction replace(@IdRes int containerViewId, @NonNull Fragment fragment) { + return replace(containerViewId, fragment, null); + } + + /** + * Replace an existing fragment that was added to a container. This is + * essentially the same as calling {@link #remove(Fragment)} for all + * currently added fragments that were added with the same containerViewId + * and then {@link #add(int, Fragment, String)} with the same arguments + * given here. + * + * @param containerViewId Identifier of the container whose fragment(s) are + * to be replaced. + * @param fragment The new fragment to place in the container. + * @param tag Optional tag name for the fragment, to later retrieve the + * fragment with {@link FragmentManager#findFragmentByTag(String) + * FragmentManager.findFragmentByTag(String)}. + * + * @return Returns the same FragmentTransaction instance. + */ + @NonNull + public FragmentTransaction replace(@IdRes int containerViewId, @NonNull Fragment fragment, + @Nullable String tag) { + if (containerViewId == 0) { + throw new IllegalArgumentException("Must use non-zero containerViewId"); + } + doAddOp(containerViewId, fragment, tag, OP_REPLACE); + return this; + } + + /** + * Remove an existing fragment. If it was added to a container, its view + * is also removed from that container. + * + * @param fragment The fragment to be removed. + * + * @return Returns the same FragmentTransaction instance. + */ + @NonNull + public FragmentTransaction remove(@NonNull Fragment fragment) { + addOp(new Op(OP_REMOVE, fragment)); + + return this; + } + + /** + * Hides an existing fragment. This is only relevant for fragments whose + * views have been added to a container, as this will cause the view to + * be hidden. + * + * @param fragment The fragment to be hidden. + * + * @return Returns the same FragmentTransaction instance. + */ + @NonNull + public FragmentTransaction hide(@NonNull Fragment fragment) { + addOp(new Op(OP_HIDE, fragment)); + + return this; + } + + /** + * Shows a previously hidden fragment. This is only relevant for fragments whose + * views have been added to a container, as this will cause the view to + * be shown. + * + * @param fragment The fragment to be shown. + * + * @return Returns the same FragmentTransaction instance. + */ + @NonNull + public FragmentTransaction show(@NonNull Fragment fragment) { + addOp(new Op(OP_SHOW, fragment)); + + return this; + } + + /** + * Detach the given fragment from the UI. This is the same state as + * when it is put on the back stack: the fragment is removed from + * the UI, however its state is still being actively managed by the + * fragment manager. When going into this state its view hierarchy + * is destroyed. + * + * @param fragment The fragment to be detached. + * + * @return Returns the same FragmentTransaction instance. + */ + @NonNull + public FragmentTransaction detach(@NonNull Fragment fragment) { + addOp(new Op(OP_DETACH, fragment)); + + return this; + } + + /** + * Re-attach a fragment after it had previously been detached from + * the UI with {@link #detach(Fragment)}. This + * causes its view hierarchy to be re-created, attached to the UI, + * and displayed. + * + * @param fragment The fragment to be attached. + * + * @return Returns the same FragmentTransaction instance. + */ + @NonNull + public FragmentTransaction attach(@NonNull Fragment fragment) { + addOp(new Op(OP_ATTACH, fragment)); + + return this; + } + + /** + * Set a currently active fragment in this FragmentManager as the primary navigation fragment. + * + *

The primary navigation fragment's + * {@link Fragment#getChildFragmentManager() child FragmentManager} will be called first + * to process delegated navigation actions such as {@link FragmentManager#popBackStack()} + * if no ID or transaction name is provided to pop to. Navigation operations outside of the + * fragment system may choose to delegate those actions to the primary navigation fragment + * as returned by {@link FragmentManager#getPrimaryNavigationFragment()}.

+ * + *

The fragment provided must currently be added to the FragmentManager to be set as + * a primary navigation fragment, or previously added as part of this transaction.

+ * + * @param fragment the fragment to set as the primary navigation fragment + * @return the same FragmentTransaction instance + */ + @NonNull + public FragmentTransaction setPrimaryNavigationFragment(@Nullable Fragment fragment) { + addOp(new Op(OP_SET_PRIMARY_NAV, fragment)); + + return this; + } + + /** + * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is + * already above the received state, it will be forced down to the correct state. + * + *

The fragment provided must currently be added to the FragmentManager to have it's + * Lifecycle state capped, or previously added as part of this transaction. The + * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise + * an {@link IllegalArgumentException} will be thrown.

+ * + * @param fragment the fragment to have it's state capped. + * @param state the ceiling state for the fragment. + * @return the same FragmentTransaction instance + */ + @NonNull + public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment, + @NonNull Lifecycle.State state) { + addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state)); + return this; + } + + /** + * @return true if this transaction contains no operations, + * false otherwise. + */ + public boolean isEmpty() { + return mOps.isEmpty(); + } + + /** + * Bit mask that is set for all enter transitions. + */ + public static final int TRANSIT_ENTER_MASK = 0x1000; + + /** + * Bit mask that is set for all exit transitions. + */ + public static final int TRANSIT_EXIT_MASK = 0x2000; + + /** @hide */ + @RestrictTo(LIBRARY_GROUP_PREFIX) + @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE, TRANSIT_FRAGMENT_FADE}) + @Retention(RetentionPolicy.SOURCE) + private @interface Transit {} + + /** Not set up for a transition. */ + public static final int TRANSIT_UNSET = -1; + /** No animation for transition. */ + public static final int TRANSIT_NONE = 0; + /** Fragment is being added onto the stack */ + public static final int TRANSIT_FRAGMENT_OPEN = 1 | TRANSIT_ENTER_MASK; + /** Fragment is being removed from the stack */ + public static final int TRANSIT_FRAGMENT_CLOSE = 2 | TRANSIT_EXIT_MASK; + /** Fragment should simply fade in or out; that is, no strong navigation associated + * with it except that it is appearing or disappearing for some reason. */ + public static final int TRANSIT_FRAGMENT_FADE = 3 | TRANSIT_ENTER_MASK; + + /** + * Set specific animation resources to run for the fragments that are + * entering and exiting in this transaction. These animations will not be + * played when popping the back stack. + * + * @param enter An animation or animator resource ID used for the enter animation on the + * view of the fragment being added or attached. + * @param exit An animation or animator resource ID used for the exit animation on the + * view of the fragment being removed or detached. + */ + @NonNull + public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int enter, + @AnimatorRes @AnimRes int exit) { + return setCustomAnimations(enter, exit, 0, 0); + } + + /** + * Set specific animation resources to run for the fragments that are + * entering and exiting in this transaction. The popEnter + * and popExit animations will be played for enter/exit + * operations specifically when popping the back stack. + * + * @param enter An animation or animator resource ID used for the enter animation on the + * view of the fragment being added or attached. + * @param exit An animation or animator resource ID used for the exit animation on the + * view of the fragment being removed or detached. + * @param popEnter An animation or animator resource ID used for the enter animation on the + * view of the fragment being readded or reattached caused by + * {@link FragmentManager#popBackStack()} or similar methods. + * @param popExit An animation or animator resource ID used for the enter animation on the + * view of the fragment being removed or detached caused by + * {@link FragmentManager#popBackStack()} or similar methods. + */ + @NonNull + public FragmentTransaction setCustomAnimations(@AnimatorRes @AnimRes int enter, + @AnimatorRes @AnimRes int exit, @AnimatorRes @AnimRes int popEnter, + @AnimatorRes @AnimRes int popExit) { + mEnterAnim = enter; + mExitAnim = exit; + mPopEnterAnim = popEnter; + mPopExitAnim = popExit; + return this; + } + + /** + * Used with custom Transitions to map a View from a removed or hidden + * Fragment to a View from a shown or added Fragment. + * sharedElement must have a unique transitionName in the View hierarchy. + * + * @param sharedElement A View in a disappearing Fragment to match with a View in an + * appearing Fragment. + * @param name The transitionName for a View in an appearing Fragment to match to the shared + * element. + * @see Fragment#setSharedElementReturnTransition(Object) + * @see Fragment#setSharedElementEnterTransition(Object) + */ + @NonNull + public FragmentTransaction addSharedElement(@NonNull View sharedElement, @NonNull String name) { + if (FragmentTransition.supportsTransition()) { + String transitionName = ViewCompat.getTransitionName(sharedElement); + if (transitionName == null) { + throw new IllegalArgumentException("Unique transitionNames are required for all" + + " sharedElements"); + } + if (mSharedElementSourceNames == null) { + mSharedElementSourceNames = new ArrayList(); + mSharedElementTargetNames = new ArrayList(); + } else if (mSharedElementTargetNames.contains(name)) { + throw new IllegalArgumentException("A shared element with the target name '" + + name + "' has already been added to the transaction."); + } else if (mSharedElementSourceNames.contains(transitionName)) { + throw new IllegalArgumentException("A shared element with the source name '" + + transitionName + "' has already been added to the transaction."); + } + + mSharedElementSourceNames.add(transitionName); + mSharedElementTargetNames.add(name); + } + return this; + } + + /** + * Select a standard transition animation for this transaction. May be + * one of {@link #TRANSIT_NONE}, {@link #TRANSIT_FRAGMENT_OPEN}, + * {@link #TRANSIT_FRAGMENT_CLOSE}, or {@link #TRANSIT_FRAGMENT_FADE}. + */ + @NonNull + public FragmentTransaction setTransition(@Transit int transition) { + mTransition = transition; + return this; + } + + /** + * Set a custom style resource that will be used for resolving transit + * animations. + */ + @NonNull + public FragmentTransaction setTransitionStyle(@StyleRes int styleRes) { + mTransitionStyle = styleRes; + return this; + } + + /** + * Add this transaction to the back stack. This means that the transaction + * will be remembered after it is committed, and will reverse its operation + * when later popped off the stack. + *

+ * {@link #setReorderingAllowed(boolean)} must be set to true + * in the same transaction as addToBackStack() to allow the pop of that + * transaction to be reordered. + * + * @param name An optional name for this back stack state, or null. + */ + @NonNull + public FragmentTransaction addToBackStack(@Nullable String name) { + if (!mAllowAddToBackStack) { + throw new IllegalStateException( + "This FragmentTransaction is not allowed to be added to the back stack."); + } + mAddToBackStack = true; + mName = name; + return this; + } + + /** + * Returns true if this FragmentTransaction is allowed to be added to the back + * stack. If this method would return false, {@link #addToBackStack(String)} + * will throw {@link IllegalStateException}. + * + * @return True if {@link #addToBackStack(String)} is permitted on this transaction. + */ + public boolean isAddToBackStackAllowed() { + return mAllowAddToBackStack; + } + + /** + * Disallow calls to {@link #addToBackStack(String)}. Any future calls to + * addToBackStack will throw {@link IllegalStateException}. If addToBackStack + * has already been called, this method will throw IllegalStateException. + */ + @NonNull + public FragmentTransaction disallowAddToBackStack() { + if (mAddToBackStack) { + throw new IllegalStateException( + "This transaction is already being added to the back stack"); + } + mAllowAddToBackStack = false; + return this; + } + + /** + * Set the full title to show as a bread crumb when this transaction + * is on the back stack. + * + * @param res A string resource containing the title. + */ + @NonNull + public FragmentTransaction setBreadCrumbTitle(@StringRes int res) { + mBreadCrumbTitleRes = res; + mBreadCrumbTitleText = null; + return this; + } + + /** + * Like {@link #setBreadCrumbTitle(int)} but taking a raw string; this + * method is not recommended, as the string can not be changed + * later if the locale changes. + */ + @NonNull + public FragmentTransaction setBreadCrumbTitle(@Nullable CharSequence text) { + mBreadCrumbTitleRes = 0; + mBreadCrumbTitleText = text; + return this; + } + + /** + * Set the short title to show as a bread crumb when this transaction + * is on the back stack. + * + * @param res A string resource containing the title. + */ + @NonNull + public FragmentTransaction setBreadCrumbShortTitle(@StringRes int res) { + mBreadCrumbShortTitleRes = res; + mBreadCrumbShortTitleText = null; + return this; + } + + /** + * Like {@link #setBreadCrumbShortTitle(int)} but taking a raw string; this + * method is not recommended, as the string can not be changed + * later if the locale changes. + */ + @NonNull + public FragmentTransaction setBreadCrumbShortTitle(@Nullable CharSequence text) { + mBreadCrumbShortTitleRes = 0; + mBreadCrumbShortTitleText = text; + return this; + } + + /** + * Sets whether or not to allow optimizing operations within and across + * transactions. This will remove redundant operations, eliminating + * operations that cancel. For example, if two transactions are executed + * together, one that adds a fragment A and the next replaces it with fragment B, + * the operations will cancel and only fragment B will be added. That means that + * fragment A may not go through the creation/destruction lifecycle. + *

+ * The side effect of removing redundant operations is that fragments may have state changes + * out of the expected order. For example, one transaction adds fragment A, + * a second adds fragment B, then a third removes fragment A. Without removing the redundant + * operations, fragment B could expect that while it is being created, fragment A will also + * exist because fragment A will be removed after fragment B was added. + * With removing redundant operations, fragment B cannot expect fragment A to exist when + * it has been created because fragment A's add/remove will be optimized out. + *

+ * It can also reorder the state changes of Fragments to allow for better Transitions. + * Added Fragments may have {@link Fragment#onCreate(Bundle)} called before replaced + * Fragments have {@link Fragment#onDestroy()} called. + *

+ * {@link Fragment#postponeEnterTransition()} requires {@code setReorderingAllowed(true)}. + *

+ * The default is {@code false}. + * + * @param reorderingAllowed {@code true} to enable optimizing out redundant operations + * or {@code false} to disable optimizing out redundant + * operations on this transaction. + */ + @NonNull + public FragmentTransaction setReorderingAllowed(boolean reorderingAllowed) { + mReorderingAllowed = reorderingAllowed; + return this; + } + + /** + * @deprecated This has been renamed {@link #setReorderingAllowed(boolean)}. + */ + @Deprecated + @NonNull + public FragmentTransaction setAllowOptimization(boolean allowOptimization) { + return setReorderingAllowed(allowOptimization); + } + + /** + * Add a Runnable to this transaction that will be run after this transaction has + * been committed. If fragment transactions are {@link #setReorderingAllowed(boolean) optimized} + * this may be after other subsequent fragment operations have also taken place, or operations + * in this transaction may have been optimized out due to the presence of a subsequent + * fragment transaction in the batch. + * + *

If a transaction is committed using {@link #commitAllowingStateLoss()} this runnable + * may be executed when the FragmentManager is in a state where new transactions may not + * be committed without allowing state loss.

+ * + *

runOnCommit may not be used with transactions + * {@link #addToBackStack(String) added to the back stack} as Runnables cannot be persisted + * with back stack state. {@link IllegalStateException} will be thrown if + * {@link #addToBackStack(String)} has been previously called for this transaction + * or if it is called after a call to runOnCommit.

+ * + * @param runnable Runnable to add + * @return this FragmentTransaction + * @throws IllegalStateException if {@link #addToBackStack(String)} has been called + */ + @NonNull + public FragmentTransaction runOnCommit(@NonNull Runnable runnable) { + disallowAddToBackStack(); + if (mCommitRunnables == null) { + mCommitRunnables = new ArrayList<>(); + } + mCommitRunnables.add(runnable); + return this; + } + + /** + * Schedules a commit of this transaction. The commit does + * not happen immediately; it will be scheduled as work on the main thread + * to be done the next time that thread is ready. + * + *

A transaction can only be committed with this method + * prior to its containing activity saving its state. If the commit is + * attempted after that point, an exception will be thrown. This is + * because the state after the commit can be lost if the activity needs to + * be restored from its state. See {@link #commitAllowingStateLoss()} for + * situations where it may be okay to lose the commit.

+ * + * @return Returns the identifier of this transaction's back stack entry, + * if {@link #addToBackStack(String)} had been called. Otherwise, returns + * a negative number. + */ + public abstract int commit(); + + /** + * Like {@link #commit} but allows the commit to be executed after an + * activity's state is saved. This is dangerous because the commit can + * be lost if the activity needs to later be restored from its state, so + * this should only be used for cases where it is okay for the UI state + * to change unexpectedly on the user. + */ + public abstract int commitAllowingStateLoss(); + + /** + * Commits this transaction synchronously. Any added fragments will be + * initialized and brought completely to the lifecycle state of their host + * and any removed fragments will be torn down accordingly before this + * call returns. Committing a transaction in this way allows fragments + * to be added as dedicated, encapsulated components that monitor the + * lifecycle state of their host while providing firmer ordering guarantees + * around when those fragments are fully initialized and ready. Fragments + * that manage views will have those views created and attached. + * + *

Calling commitNow is preferable to calling + * {@link #commit()} followed by {@link FragmentManager#executePendingTransactions()} + * as the latter will have the side effect of attempting to commit all + * currently pending transactions whether that is the desired behavior + * or not.

+ * + *

Transactions committed in this way may not be added to the + * FragmentManager's back stack, as doing so would break other expected + * ordering guarantees for other asynchronously committed transactions. + * This method will throw {@link IllegalStateException} if the transaction + * previously requested to be added to the back stack with + * {@link #addToBackStack(String)}.

+ * + *

A transaction can only be committed with this method + * prior to its containing activity saving its state. If the commit is + * attempted after that point, an exception will be thrown. This is + * because the state after the commit can be lost if the activity needs to + * be restored from its state. See {@link #commitAllowingStateLoss()} for + * situations where it may be okay to lose the commit.

+ */ + public abstract void commitNow(); + + /** + * Like {@link #commitNow} but allows the commit to be executed after an + * activity's state is saved. This is dangerous because the commit can + * be lost if the activity needs to later be restored from its state, so + * this should only be used for cases where it is okay for the UI state + * to change unexpectedly on the user. + */ + public abstract void commitNowAllowingStateLoss(); +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransition.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransition.java new file mode 100644 index 000000000..997902e00 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransition.java @@ -0,0 +1,1267 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.graphics.Rect; +import android.os.Build; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import androidx.collection.ArrayMap; +import androidx.core.app.SharedElementCallback; +import androidx.core.view.OneShotPreDrawListener; +import androidx.core.view.ViewCompat; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Contains the Fragment Transition functionality for both ordered and reordered + * Fragment Transactions. With reordered fragment transactions, all Views have been + * added to the View hierarchy prior to calling startTransitions. With ordered + * fragment transactions, Views will be removed and added after calling startTransitions. + */ +class FragmentTransition { + /** + * The inverse of all BackStackRecord operation commands. This assumes that + * REPLACE operations have already been replaced by add/remove operations. + */ + private static final int[] INVERSE_OPS = { + BackStackRecord.OP_NULL, // inverse of OP_NULL (error) + BackStackRecord.OP_REMOVE, // inverse of OP_ADD + BackStackRecord.OP_NULL, // inverse of OP_REPLACE (error) + BackStackRecord.OP_ADD, // inverse of OP_REMOVE + BackStackRecord.OP_SHOW, // inverse of OP_HIDE + BackStackRecord.OP_HIDE, // inverse of OP_SHOW + BackStackRecord.OP_ATTACH, // inverse of OP_DETACH + BackStackRecord.OP_DETACH, // inverse of OP_ATTACH + BackStackRecord.OP_UNSET_PRIMARY_NAV, // inverse of OP_SET_PRIMARY_NAV + BackStackRecord.OP_SET_PRIMARY_NAV, // inverse of OP_UNSET_PRIMARY_NAV + BackStackRecord.OP_SET_MAX_LIFECYCLE + }; + + private static final FragmentTransitionImpl PLATFORM_IMPL = Build.VERSION.SDK_INT >= 21 + ? new FragmentTransitionCompat21() + : null; + + private static final FragmentTransitionImpl SUPPORT_IMPL = resolveSupportImpl(); + + private static FragmentTransitionImpl resolveSupportImpl() { + try { + @SuppressWarnings("unchecked") + Class impl = (Class) Class.forName( + "androidx.transition.FragmentTransitionSupport"); + return impl.getDeclaredConstructor().newInstance(); + } catch (Exception ignored) { + // support-transition is not loaded; ignore + } + return null; + } + + /** + * The main entry point for Fragment Transitions, this starts the transitions + * set on the leaving Fragment's {@link Fragment#getExitTransition()}, the + * entering Fragment's {@link Fragment#getEnterTransition()} and + * {@link Fragment#getSharedElementEnterTransition()}. When popping, + * the leaving Fragment's {@link Fragment#getReturnTransition()} and + * {@link Fragment#getSharedElementReturnTransition()} and the entering + * {@link Fragment#getReenterTransition()} will be run. + *

+ * With reordered Fragment Transitions, all Views have been added to the + * View hierarchy prior to calling this method. The incoming Fragment's Views + * will be INVISIBLE. With ordered Fragment Transitions, this method + * is called before any change has been made to the hierarchy. That means + * that the added Fragments have not created their Views yet and the hierarchy + * is unknown. + * + * @param fragmentManager The executing FragmentManagerImpl + * @param records The list of transactions being executed. + * @param isRecordPop For each transaction, whether it is a pop transaction or not. + * @param startIndex The first index into records and isRecordPop to execute as + * part of this transition. + * @param endIndex One past the last index into records and isRecordPop to execute + * as part of this transition. + * @param isReordered true if this is a reordered transaction, meaning that the + * Views of incoming fragments have been added. false if the + * transaction has yet to be run and Views haven't been created. + */ + static void startTransitions(FragmentManagerImpl fragmentManager, + ArrayList records, ArrayList isRecordPop, + int startIndex, int endIndex, boolean isReordered) { + if (fragmentManager.mCurState < Fragment.CREATED) { + return; + } + + SparseArray transitioningFragments = + new SparseArray<>(); + for (int i = startIndex; i < endIndex; i++) { + final BackStackRecord record = records.get(i); + final boolean isPop = isRecordPop.get(i); + if (isPop) { + calculatePopFragments(record, transitioningFragments, isReordered); + } else { + calculateFragments(record, transitioningFragments, isReordered); + } + } + + if (transitioningFragments.size() != 0) { + final View nonExistentView = new View(fragmentManager.mHost.getContext()); + final int numContainers = transitioningFragments.size(); + for (int i = 0; i < numContainers; i++) { + int containerId = transitioningFragments.keyAt(i); + ArrayMap nameOverrides = calculateNameOverrides(containerId, + records, isRecordPop, startIndex, endIndex); + + FragmentContainerTransition containerTransition = + transitioningFragments.valueAt(i); + + if (isReordered) { + configureTransitionsReordered(fragmentManager, containerId, + containerTransition, nonExistentView, nameOverrides); + } else { + configureTransitionsOrdered(fragmentManager, containerId, + containerTransition, nonExistentView, nameOverrides); + } + } + } + } + + /** + * Iterates through the transactions that affect a given fragment container + * and tracks the shared element names across transactions. This is most useful + * in pop transactions where the names of shared elements are known. + * + * @param containerId The container ID that is executing the transition. + * @param records The list of transactions being executed. + * @param isRecordPop For each transaction, whether it is a pop transaction or not. + * @param startIndex The first index into records and isRecordPop to execute as + * part of this transition. + * @param endIndex One past the last index into records and isRecordPop to execute + * as part of this transition. + * @return A map from the initial shared element name to the final shared element name + * before any onMapSharedElements is run. + */ + private static ArrayMap calculateNameOverrides(int containerId, + ArrayList records, ArrayList isRecordPop, + int startIndex, int endIndex) { + ArrayMap nameOverrides = new ArrayMap<>(); + for (int recordNum = endIndex - 1; recordNum >= startIndex; recordNum--) { + final BackStackRecord record = records.get(recordNum); + if (!record.interactsWith(containerId)) { + continue; + } + final boolean isPop = isRecordPop.get(recordNum); + if (record.mSharedElementSourceNames != null) { + final int numSharedElements = record.mSharedElementSourceNames.size(); + final ArrayList sources; + final ArrayList targets; + if (isPop) { + targets = record.mSharedElementSourceNames; + sources = record.mSharedElementTargetNames; + } else { + sources = record.mSharedElementSourceNames; + targets = record.mSharedElementTargetNames; + } + for (int i = 0; i < numSharedElements; i++) { + String sourceName = sources.get(i); + String targetName = targets.get(i); + String previousTarget = nameOverrides.remove(targetName); + if (previousTarget != null) { + nameOverrides.put(sourceName, previousTarget); + } else { + nameOverrides.put(sourceName, targetName); + } + } + } + } + return nameOverrides; + } + + /** + * Configures a transition for a single fragment container for which the transaction was + * reordered. That means that all Fragment Views have been added and incoming fragment + * Views are marked invisible. + * + * @param fragmentManager The executing FragmentManagerImpl + * @param containerId The container ID that is executing the transition. + * @param fragments A structure holding the transitioning fragments in this container. + * @param nonExistentView A View that does not exist in the hierarchy. This is used to + * prevent transitions from acting on other Views when there is no + * other target. + * @param nameOverrides A map of the shared element names from the starting fragment to + * the final fragment's Views as given in + * {@link FragmentTransaction#addSharedElement(View, String)}. + */ + private static void configureTransitionsReordered(FragmentManagerImpl fragmentManager, + int containerId, FragmentContainerTransition fragments, + View nonExistentView, ArrayMap nameOverrides) { + ViewGroup sceneRoot = null; + if (fragmentManager.mContainer.onHasView()) { + sceneRoot = (ViewGroup) fragmentManager.mContainer.onFindViewById(containerId); + } + if (sceneRoot == null) { + return; + } + final Fragment inFragment = fragments.lastIn; + final Fragment outFragment = fragments.firstOut; + final FragmentTransitionImpl impl = chooseImpl(outFragment, inFragment); + if (impl == null) { + return; + } + final boolean inIsPop = fragments.lastInIsPop; + final boolean outIsPop = fragments.firstOutIsPop; + + ArrayList sharedElementsIn = new ArrayList<>(); + ArrayList sharedElementsOut = new ArrayList<>(); + Object enterTransition = getEnterTransition(impl, inFragment, inIsPop); + Object exitTransition = getExitTransition(impl, outFragment, outIsPop); + + Object sharedElementTransition = configureSharedElementsReordered(impl, sceneRoot, + nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn, + enterTransition, exitTransition); + + if (enterTransition == null && sharedElementTransition == null + && exitTransition == null) { + return; // no transitions! + } + + ArrayList exitingViews = configureEnteringExitingViews(impl, exitTransition, + outFragment, sharedElementsOut, nonExistentView); + + ArrayList enteringViews = configureEnteringExitingViews(impl, enterTransition, + inFragment, sharedElementsIn, nonExistentView); + + setViewVisibility(enteringViews, View.INVISIBLE); + + Object transition = mergeTransitions(impl, enterTransition, exitTransition, + sharedElementTransition, inFragment, inIsPop); + + if (transition != null) { + replaceHide(impl, exitTransition, outFragment, exitingViews); + ArrayList inNames = + impl.prepareSetNameOverridesReordered(sharedElementsIn); + impl.scheduleRemoveTargets(transition, + enterTransition, enteringViews, exitTransition, exitingViews, + sharedElementTransition, sharedElementsIn); + impl.beginDelayedTransition(sceneRoot, transition); + impl.setNameOverridesReordered(sceneRoot, sharedElementsOut, + sharedElementsIn, inNames, nameOverrides); + setViewVisibility(enteringViews, View.VISIBLE); + impl.swapSharedElementTargets(sharedElementTransition, + sharedElementsOut, sharedElementsIn); + } + } + + /** + * Replace hide operations with visibility changes on the exiting views. Instead of making + * the entire fragment's view GONE, make each exiting view INVISIBLE. At the end of the + * transition, make the fragment's view GONE. + */ + private static void replaceHide(FragmentTransitionImpl impl, + Object exitTransition, Fragment exitingFragment, + final ArrayList exitingViews) { + if (exitingFragment != null && exitTransition != null && exitingFragment.mAdded + && exitingFragment.mHidden && exitingFragment.mHiddenChanged) { + exitingFragment.setHideReplaced(true); + impl.scheduleHideFragmentView(exitTransition, + exitingFragment.getView(), exitingViews); + final ViewGroup container = exitingFragment.mContainer; + OneShotPreDrawListener.add(container, new Runnable() { + @Override + public void run() { + setViewVisibility(exitingViews, View.INVISIBLE); + } + }); + } + } + + /** + * Configures a transition for a single fragment container for which the transaction was + * ordered. That means that the transaction has not been executed yet, so incoming + * Views are not yet known. + * + * @param fragmentManager The executing FragmentManagerImpl + * @param containerId The container ID that is executing the transition. + * @param fragments A structure holding the transitioning fragments in this container. + * @param nonExistentView A View that does not exist in the hierarchy. This is used to + * prevent transitions from acting on other Views when there is no + * other target. + * @param nameOverrides A map of the shared element names from the starting fragment to + * the final fragment's Views as given in + * {@link FragmentTransaction#addSharedElement(View, String)}. + */ + private static void configureTransitionsOrdered(FragmentManagerImpl fragmentManager, + int containerId, FragmentContainerTransition fragments, + View nonExistentView, ArrayMap nameOverrides) { + ViewGroup sceneRoot = null; + if (fragmentManager.mContainer.onHasView()) { + sceneRoot = (ViewGroup) fragmentManager.mContainer.onFindViewById(containerId); + } + if (sceneRoot == null) { + return; + } + final Fragment inFragment = fragments.lastIn; + final Fragment outFragment = fragments.firstOut; + final FragmentTransitionImpl impl = chooseImpl(outFragment, inFragment); + if (impl == null) { + return; + } + final boolean inIsPop = fragments.lastInIsPop; + final boolean outIsPop = fragments.firstOutIsPop; + + Object enterTransition = getEnterTransition(impl, inFragment, inIsPop); + Object exitTransition = getExitTransition(impl, outFragment, outIsPop); + + ArrayList sharedElementsOut = new ArrayList<>(); + ArrayList sharedElementsIn = new ArrayList<>(); + + Object sharedElementTransition = configureSharedElementsOrdered(impl, sceneRoot, + nonExistentView, nameOverrides, fragments, sharedElementsOut, sharedElementsIn, + enterTransition, exitTransition); + + if (enterTransition == null && sharedElementTransition == null + && exitTransition == null) { + return; // no transitions! + } + + ArrayList exitingViews = configureEnteringExitingViews(impl, exitTransition, + outFragment, sharedElementsOut, nonExistentView); + + if (exitingViews == null || exitingViews.isEmpty()) { + exitTransition = null; + } + + // Ensure the entering transition doesn't target anything until the views are made + // visible + impl.addTarget(enterTransition, nonExistentView); + + Object transition = mergeTransitions(impl, enterTransition, exitTransition, + sharedElementTransition, inFragment, fragments.lastInIsPop); + + if (transition != null) { + final ArrayList enteringViews = new ArrayList<>(); + impl.scheduleRemoveTargets(transition, + enterTransition, enteringViews, exitTransition, exitingViews, + sharedElementTransition, sharedElementsIn); + scheduleTargetChange(impl, sceneRoot, inFragment, nonExistentView, sharedElementsIn, + enterTransition, enteringViews, exitTransition, exitingViews); + impl.setNameOverridesOrdered(sceneRoot, sharedElementsIn, nameOverrides); + + impl.beginDelayedTransition(sceneRoot, transition); + impl.scheduleNameReset(sceneRoot, sharedElementsIn, nameOverrides); + } + } + + /** + * This method is used for fragment transitions for ordrered transactions to change the + * enter and exit transition targets after the call to + * {@link FragmentTransitionCompat21#beginDelayedTransition(ViewGroup, Object)}. The exit + * transition must ensure that it does not target any Views and the enter transition must start + * targeting the Views of the incoming Fragment. + * + * @param sceneRoot The fragment container View + * @param inFragment The last fragment that is entering + * @param nonExistentView A view that does not exist in the hierarchy that is used as a + * transition target to ensure no View is targeted. + * @param sharedElementsIn The shared element Views of the incoming fragment + * @param enterTransition The enter transition of the incoming fragment + * @param enteringViews The entering Views of the incoming fragment + * @param exitTransition The exit transition of the outgoing fragment + * @param exitingViews The exiting views of the outgoing fragment + */ + private static void scheduleTargetChange(final FragmentTransitionImpl impl, + final ViewGroup sceneRoot, + final Fragment inFragment, final View nonExistentView, + final ArrayList sharedElementsIn, + final Object enterTransition, final ArrayList enteringViews, + final Object exitTransition, final ArrayList exitingViews) { + OneShotPreDrawListener.add(sceneRoot, new Runnable() { + @Override + public void run() { + if (enterTransition != null) { + impl.removeTarget(enterTransition, + nonExistentView); + ArrayList views = configureEnteringExitingViews(impl, + enterTransition, inFragment, sharedElementsIn, nonExistentView); + enteringViews.addAll(views); + } + + if (exitingViews != null) { + if (exitTransition != null) { + ArrayList tempExiting = new ArrayList<>(); + tempExiting.add(nonExistentView); + impl.replaceTargets(exitTransition, exitingViews, + tempExiting); + } + exitingViews.clear(); + exitingViews.add(nonExistentView); + } + } + }); + } + + /** + * Chooses the appropriate implementation depending on the Transition instances hold by the + * Fragments. + */ + private static FragmentTransitionImpl chooseImpl(Fragment outFragment, Fragment inFragment) { + // Collect all transition instances + final ArrayList transitions = new ArrayList<>(); + if (outFragment != null) { + final Object exitTransition = outFragment.getExitTransition(); + if (exitTransition != null) { + transitions.add(exitTransition); + } + final Object returnTransition = outFragment.getReturnTransition(); + if (returnTransition != null) { + transitions.add(returnTransition); + } + final Object sharedReturnTransition = outFragment.getSharedElementReturnTransition(); + if (sharedReturnTransition != null) { + transitions.add(sharedReturnTransition); + } + } + if (inFragment != null) { + final Object enterTransition = inFragment.getEnterTransition(); + if (enterTransition != null) { + transitions.add(enterTransition); + } + final Object reenterTransition = inFragment.getReenterTransition(); + if (reenterTransition != null) { + transitions.add(reenterTransition); + } + final Object sharedEnterTransition = inFragment.getSharedElementEnterTransition(); + if (sharedEnterTransition != null) { + transitions.add(sharedEnterTransition); + } + } + if (transitions.isEmpty()) { + return null; // No transition to run + } + // Pick the implementation that can handle all the transitions + if (PLATFORM_IMPL != null && canHandleAll(PLATFORM_IMPL, transitions)) { + return PLATFORM_IMPL; + } + if (SUPPORT_IMPL != null && canHandleAll(SUPPORT_IMPL, transitions)) { + return SUPPORT_IMPL; + } + if (PLATFORM_IMPL != null || SUPPORT_IMPL != null) { + throw new IllegalArgumentException("Invalid Transition types"); + } + return null; + } + + private static boolean canHandleAll(FragmentTransitionImpl impl, List transitions) { + for (int i = 0, size = transitions.size(); i < size; i++) { + if (!impl.canHandle(transitions.get(i))) { + return false; + } + } + return true; + } + + /** + * Returns a TransitionSet containing the shared element transition. The wrapping TransitionSet + * targets all shared elements to ensure that no other Views are targeted. The shared element + * transition can then target any or all shared elements without worrying about accidentally + * targeting entering or exiting Views. + * + * @param inFragment The incoming fragment + * @param outFragment the outgoing fragment + * @param isPop True if this is a pop transaction or false if it is a normal (add) transaction. + * @return A TransitionSet wrapping the shared element transition or null if no such transition + * exists. + */ + private static Object getSharedElementTransition(FragmentTransitionImpl impl, + Fragment inFragment, Fragment outFragment, boolean isPop) { + if (inFragment == null || outFragment == null) { + return null; + } + Object transition = impl.cloneTransition(isPop + ? outFragment.getSharedElementReturnTransition() + : inFragment.getSharedElementEnterTransition()); + return impl.wrapTransitionInSet(transition); + } + + /** + * Returns a clone of the enter transition or null if no such transition exists. + */ + private static Object getEnterTransition(FragmentTransitionImpl impl, + Fragment inFragment, boolean isPop) { + if (inFragment == null) { + return null; + } + return impl.cloneTransition(isPop + ? inFragment.getReenterTransition() + : inFragment.getEnterTransition()); + } + + /** + * Returns a clone of the exit transition or null if no such transition exists. + */ + private static Object getExitTransition(FragmentTransitionImpl impl, + Fragment outFragment, boolean isPop) { + if (outFragment == null) { + return null; + } + return impl.cloneTransition(isPop + ? outFragment.getReturnTransition() + : outFragment.getExitTransition()); + } + + /** + * Configures the shared elements of a reordered fragment transaction's transition. + * This retrieves the shared elements of the outgoing and incoming fragments, maps the + * views, and sets up the epicenter on the transitions. + *

+ * The epicenter of exit and shared element transitions is the first shared element + * in the outgoing fragment. The epicenter of the entering transition is the first shared + * element in the incoming fragment. + * + * @param sceneRoot The fragment container View + * @param nonExistentView A View that does not exist in the hierarchy. This is used to + * prevent transitions from acting on other Views when there is no + * other target. + * @param nameOverrides A map of the shared element names from the starting fragment to + * the final fragment's Views as given in + * {@link FragmentTransaction#addSharedElement(View, String)}. + * @param fragments A structure holding the transitioning fragments in this container. + * @param sharedElementsOut A list modified to contain the shared elements in the outgoing + * fragment + * @param sharedElementsIn A list modified to contain the shared elements in the incoming + * fragment + * @param enterTransition The transition used for entering Views, modified by applying the + * epicenter + * @param exitTransition The transition used for exiting Views, modified by applying the + * epicenter + * @return The shared element transition or null if no shared elements exist + */ + private static Object configureSharedElementsReordered(final FragmentTransitionImpl impl, + final ViewGroup sceneRoot, + final View nonExistentView, final ArrayMap nameOverrides, + final FragmentContainerTransition fragments, + final ArrayList sharedElementsOut, + final ArrayList sharedElementsIn, + final Object enterTransition, final Object exitTransition) { + final Fragment inFragment = fragments.lastIn; + final Fragment outFragment = fragments.firstOut; + if (inFragment != null) { + inFragment.requireView().setVisibility(View.VISIBLE); + } + if (inFragment == null || outFragment == null) { + return null; // no shared element without a fragment + } + + final boolean inIsPop = fragments.lastInIsPop; + Object sharedElementTransition = nameOverrides.isEmpty() ? null + : getSharedElementTransition(impl, inFragment, outFragment, inIsPop); + + final ArrayMap outSharedElements = captureOutSharedElements(impl, + nameOverrides, sharedElementTransition, fragments); + + final ArrayMap inSharedElements = captureInSharedElements(impl, + nameOverrides, sharedElementTransition, fragments); + + if (nameOverrides.isEmpty()) { + sharedElementTransition = null; + if (outSharedElements != null) { + outSharedElements.clear(); + } + if (inSharedElements != null) { + inSharedElements.clear(); + } + } else { + addSharedElementsWithMatchingNames(sharedElementsOut, outSharedElements, + nameOverrides.keySet()); + addSharedElementsWithMatchingNames(sharedElementsIn, inSharedElements, + nameOverrides.values()); + } + + if (enterTransition == null && exitTransition == null && sharedElementTransition == null) { + // don't call onSharedElementStart/End since there is no transition + return null; + } + + callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true); + + final Rect epicenter; + final View epicenterView; + if (sharedElementTransition != null) { + sharedElementsIn.add(nonExistentView); + impl.setSharedElementTargets(sharedElementTransition, + nonExistentView, sharedElementsOut); + final boolean outIsPop = fragments.firstOutIsPop; + final BackStackRecord outTransaction = fragments.firstOutTransaction; + setOutEpicenter(impl, sharedElementTransition, exitTransition, outSharedElements, + outIsPop, outTransaction); + epicenter = new Rect(); + epicenterView = getInEpicenterView(inSharedElements, fragments, + enterTransition, inIsPop); + if (epicenterView != null) { + impl.setEpicenter(enterTransition, epicenter); + } + } else { + epicenter = null; + epicenterView = null; + } + + OneShotPreDrawListener.add(sceneRoot, new Runnable() { + @Override + public void run() { + callSharedElementStartEnd(inFragment, outFragment, inIsPop, + inSharedElements, false); + if (epicenterView != null) { + impl.getBoundsOnScreen(epicenterView, epicenter); + } + } + }); + return sharedElementTransition; + } + + /** + * Add Views from sharedElements into views that have the transitionName in the + * nameOverridesSet. + * + * @param views Views list to add shared elements to + * @param sharedElements List of shared elements + * @param nameOverridesSet The transition names for all views to be copied from + * sharedElements to views. + */ + private static void addSharedElementsWithMatchingNames(ArrayList views, + ArrayMap sharedElements, Collection nameOverridesSet) { + for (int i = sharedElements.size() - 1; i >= 0; i--) { + View view = sharedElements.valueAt(i); + if (nameOverridesSet.contains(ViewCompat.getTransitionName(view))) { + views.add(view); + } + } + } + + /** + * Configures the shared elements of an ordered fragment transaction's transition. + * This retrieves the shared elements of the incoming fragments, and schedules capturing + * the incoming fragment's shared elements. It also maps the views, and sets up the epicenter + * on the transitions. + *

+ * The epicenter of exit and shared element transitions is the first shared element + * in the outgoing fragment. The epicenter of the entering transition is the first shared + * element in the incoming fragment. + * + * @param sceneRoot The fragment container View + * @param nonExistentView A View that does not exist in the hierarchy. This is used to + * prevent transitions from acting on other Views when there is no + * other target. + * @param nameOverrides A map of the shared element names from the starting fragment to + * the final fragment's Views as given in + * {@link FragmentTransaction#addSharedElement(View, String)}. + * @param fragments A structure holding the transitioning fragments in this container. + * @param sharedElementsOut A list modified to contain the shared elements in the outgoing + * fragment + * @param sharedElementsIn A list modified to contain the shared elements in the incoming + * fragment + * @param enterTransition The transition used for entering Views, modified by applying the + * epicenter + * @param exitTransition The transition used for exiting Views, modified by applying the + * epicenter + * @return The shared element transition or null if no shared elements exist + */ + private static Object configureSharedElementsOrdered(final FragmentTransitionImpl impl, + final ViewGroup sceneRoot, + final View nonExistentView, final ArrayMap nameOverrides, + final FragmentContainerTransition fragments, + final ArrayList sharedElementsOut, + final ArrayList sharedElementsIn, + final Object enterTransition, final Object exitTransition) { + final Fragment inFragment = fragments.lastIn; + final Fragment outFragment = fragments.firstOut; + + if (inFragment == null || outFragment == null) { + return null; // no transition + } + + final boolean inIsPop = fragments.lastInIsPop; + Object sharedElementTransition = nameOverrides.isEmpty() ? null + : getSharedElementTransition(impl, inFragment, outFragment, inIsPop); + + ArrayMap outSharedElements = captureOutSharedElements(impl, nameOverrides, + sharedElementTransition, fragments); + + if (nameOverrides.isEmpty()) { + sharedElementTransition = null; + } else { + sharedElementsOut.addAll(outSharedElements.values()); + } + + if (enterTransition == null && exitTransition == null && sharedElementTransition == null) { + // don't call onSharedElementStart/End since there is no transition + return null; + } + + callSharedElementStartEnd(inFragment, outFragment, inIsPop, outSharedElements, true); + + final Rect inEpicenter; + if (sharedElementTransition != null) { + inEpicenter = new Rect(); + impl.setSharedElementTargets(sharedElementTransition, + nonExistentView, sharedElementsOut); + final boolean outIsPop = fragments.firstOutIsPop; + final BackStackRecord outTransaction = fragments.firstOutTransaction; + setOutEpicenter(impl, sharedElementTransition, exitTransition, outSharedElements, + outIsPop, outTransaction); + if (enterTransition != null) { + impl.setEpicenter(enterTransition, inEpicenter); + } + } else { + inEpicenter = null; + } + + + final Object finalSharedElementTransition = sharedElementTransition; + OneShotPreDrawListener.add(sceneRoot, new Runnable() { + @Override + public void run() { + ArrayMap inSharedElements = captureInSharedElements(impl, + nameOverrides, finalSharedElementTransition, fragments); + + if (inSharedElements != null) { + sharedElementsIn.addAll(inSharedElements.values()); + sharedElementsIn.add(nonExistentView); + } + + callSharedElementStartEnd(inFragment, outFragment, inIsPop, + inSharedElements, false); + if (finalSharedElementTransition != null) { + impl.swapSharedElementTargets( + finalSharedElementTransition, sharedElementsOut, + sharedElementsIn); + + final View inEpicenterView = getInEpicenterView(inSharedElements, + fragments, enterTransition, inIsPop); + if (inEpicenterView != null) { + impl.getBoundsOnScreen(inEpicenterView, + inEpicenter); + } + } + } + }); + + return sharedElementTransition; + } + + /** + * Finds the shared elements in the outgoing fragment. It also calls + * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control + * of the shared element mapping. {@code nameOverrides} is updated to match the + * actual transition name of the mapped shared elements. + * + * @param nameOverrides A map of the shared element names from the starting fragment to + * the final fragment's Views as given in + * {@link FragmentTransaction#addSharedElement(View, String)}. + * @param sharedElementTransition The shared element transition + * @param fragments A structure holding the transitioning fragments in this container. + * @return The mapping of shared element names to the Views in the hierarchy or null + * if there is no shared element transition. + */ + private static ArrayMap captureOutSharedElements(FragmentTransitionImpl impl, + ArrayMap nameOverrides, Object sharedElementTransition, + FragmentContainerTransition fragments) { + if (nameOverrides.isEmpty() || sharedElementTransition == null) { + nameOverrides.clear(); + return null; + } + final Fragment outFragment = fragments.firstOut; + final ArrayMap outSharedElements = new ArrayMap<>(); + impl.findNamedViews(outSharedElements, outFragment.requireView()); + + final SharedElementCallback sharedElementCallback; + final ArrayList names; + final BackStackRecord outTransaction = fragments.firstOutTransaction; + if (fragments.firstOutIsPop) { + sharedElementCallback = outFragment.getEnterTransitionCallback(); + names = outTransaction.mSharedElementTargetNames; + } else { + sharedElementCallback = outFragment.getExitTransitionCallback(); + names = outTransaction.mSharedElementSourceNames; + } + + outSharedElements.retainAll(names); + if (sharedElementCallback != null) { + sharedElementCallback.onMapSharedElements(names, outSharedElements); + for (int i = names.size() - 1; i >= 0; i--) { + String name = names.get(i); + View view = outSharedElements.get(name); + if (view == null) { + nameOverrides.remove(name); + } else if (!name.equals(ViewCompat.getTransitionName(view))) { + String targetValue = nameOverrides.remove(name); + nameOverrides.put(ViewCompat.getTransitionName(view), targetValue); + } + } + } else { + nameOverrides.retainAll(outSharedElements.keySet()); + } + return outSharedElements; + } + + /** + * Finds the shared elements in the incoming fragment. It also calls + * {@link SharedElementCallback#onMapSharedElements(List, Map)} to allow more control + * of the shared element mapping. {@code nameOverrides} is updated to match the + * actual transition name of the mapped shared elements. + * + * @param nameOverrides A map of the shared element names from the starting fragment to + * the final fragment's Views as given in + * {@link FragmentTransaction#addSharedElement(View, String)}. + * @param sharedElementTransition The shared element transition + * @param fragments A structure holding the transitioning fragments in this container. + * @return The mapping of shared element names to the Views in the hierarchy or null + * if there is no shared element transition. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static ArrayMap captureInSharedElements(FragmentTransitionImpl impl, + ArrayMap nameOverrides, Object sharedElementTransition, + FragmentContainerTransition fragments) { + Fragment inFragment = fragments.lastIn; + final View fragmentView = inFragment.getView(); + if (nameOverrides.isEmpty() || sharedElementTransition == null || fragmentView == null) { + nameOverrides.clear(); + return null; + } + final ArrayMap inSharedElements = new ArrayMap<>(); + impl.findNamedViews(inSharedElements, fragmentView); + + final SharedElementCallback sharedElementCallback; + final ArrayList names; + final BackStackRecord inTransaction = fragments.lastInTransaction; + if (fragments.lastInIsPop) { + sharedElementCallback = inFragment.getExitTransitionCallback(); + names = inTransaction.mSharedElementSourceNames; + } else { + sharedElementCallback = inFragment.getEnterTransitionCallback(); + names = inTransaction.mSharedElementTargetNames; + } + + if (names != null) { + inSharedElements.retainAll(names); + inSharedElements.retainAll(nameOverrides.values()); + } + if (sharedElementCallback != null) { + sharedElementCallback.onMapSharedElements(names, inSharedElements); + for (int i = names.size() - 1; i >= 0; i--) { + String name = names.get(i); + View view = inSharedElements.get(name); + if (view == null) { + String key = findKeyForValue(nameOverrides, name); + if (key != null) { + nameOverrides.remove(key); + } + } else if (!name.equals(ViewCompat.getTransitionName(view))) { + String key = findKeyForValue(nameOverrides, name); + if (key != null) { + nameOverrides.put(key, ViewCompat.getTransitionName(view)); + } + } + } + } else { + retainValues(nameOverrides, inSharedElements); + } + return inSharedElements; + } + + /** + * Utility to find the String key in {@code map} that maps to {@code value}. + */ + private static String findKeyForValue(ArrayMap map, String value) { + final int numElements = map.size(); + for (int i = 0; i < numElements; i++) { + if (value.equals(map.valueAt(i))) { + return map.keyAt(i); + } + } + return null; + } + + /** + * Returns the View in the incoming Fragment that should be used as the epicenter. + * + * @param inSharedElements The mapping of shared element names to Views in the + * incoming fragment. + * @param fragments A structure holding the transitioning fragments in this container. + * @param enterTransition The transition used for the incoming Fragment's views + * @param inIsPop Is the incoming fragment being added as a pop transaction? + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static View getInEpicenterView(ArrayMap inSharedElements, + FragmentContainerTransition fragments, + Object enterTransition, boolean inIsPop) { + BackStackRecord inTransaction = fragments.lastInTransaction; + if (enterTransition != null && inSharedElements != null + && inTransaction.mSharedElementSourceNames != null + && !inTransaction.mSharedElementSourceNames.isEmpty()) { + final String targetName = inIsPop + ? inTransaction.mSharedElementSourceNames.get(0) + : inTransaction.mSharedElementTargetNames.get(0); + return inSharedElements.get(targetName); + } + return null; + } + + /** + * Sets the epicenter for the exit transition. + * + * @param sharedElementTransition The shared element transition + * @param exitTransition The transition for the outgoing fragment's views + * @param outSharedElements Shared elements in the outgoing fragment + * @param outIsPop Is the outgoing fragment being removed as a pop transaction? + * @param outTransaction The transaction that caused the fragment to be removed. + */ + private static void setOutEpicenter(FragmentTransitionImpl impl, Object sharedElementTransition, + Object exitTransition, ArrayMap outSharedElements, boolean outIsPop, + BackStackRecord outTransaction) { + if (outTransaction.mSharedElementSourceNames != null + && !outTransaction.mSharedElementSourceNames.isEmpty()) { + final String sourceName = outIsPop + ? outTransaction.mSharedElementTargetNames.get(0) + : outTransaction.mSharedElementSourceNames.get(0); + final View outEpicenterView = outSharedElements.get(sourceName); + impl.setEpicenter(sharedElementTransition, outEpicenterView); + + if (exitTransition != null) { + impl.setEpicenter(exitTransition, outEpicenterView); + } + } + } + + /** + * A utility to retain only the mappings in {@code nameOverrides} that have a value + * that has a key in {@code namedViews}. This is a useful equivalent to + * {@link ArrayMap#retainAll(Collection)} for values. + */ + private static void retainValues(ArrayMap nameOverrides, + ArrayMap namedViews) { + for (int i = nameOverrides.size() - 1; i >= 0; i--) { + final String targetName = nameOverrides.valueAt(i); + if (!namedViews.containsKey(targetName)) { + nameOverrides.removeAt(i); + } + } + } + + /** + * Calls the {@link SharedElementCallback#onSharedElementStart(List, List, List)} or + * {@link SharedElementCallback#onSharedElementEnd(List, List, List)} on the appropriate + * incoming or outgoing fragment. + * + * @param inFragment The incoming fragment + * @param outFragment The outgoing fragment + * @param isPop Is the incoming fragment part of a pop transaction? + * @param sharedElements The shared element Views + * @param isStart Call the start or end call on the SharedElementCallback + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static void callSharedElementStartEnd(Fragment inFragment, Fragment outFragment, + boolean isPop, ArrayMap sharedElements, boolean isStart) { + SharedElementCallback sharedElementCallback = isPop + ? outFragment.getEnterTransitionCallback() + : inFragment.getEnterTransitionCallback(); + if (sharedElementCallback != null) { + ArrayList views = new ArrayList<>(); + ArrayList names = new ArrayList<>(); + final int count = sharedElements == null ? 0 : sharedElements.size(); + for (int i = 0; i < count; i++) { + names.add(sharedElements.keyAt(i)); + views.add(sharedElements.valueAt(i)); + } + if (isStart) { + sharedElementCallback.onSharedElementStart(names, views, null); + } else { + sharedElementCallback.onSharedElementEnd(names, views, null); + } + } + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static ArrayList configureEnteringExitingViews(FragmentTransitionImpl impl, + Object transition, + Fragment fragment, ArrayList sharedElements, View nonExistentView) { + ArrayList viewList = null; + if (transition != null) { + viewList = new ArrayList<>(); + View root = fragment.getView(); + if (root != null) { + impl.captureTransitioningViews(viewList, root); + } + if (sharedElements != null) { + viewList.removeAll(sharedElements); + } + if (!viewList.isEmpty()) { + viewList.add(nonExistentView); + impl.addTargets(transition, viewList); + } + } + return viewList; + } + + /** + * Sets the visibility of all Views in {@code views} to {@code visibility}. + */ + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static void setViewVisibility(ArrayList views, int visibility) { + if (views == null) { + return; + } + for (int i = views.size() - 1; i >= 0; i--) { + final View view = views.get(i); + view.setVisibility(visibility); + } + } + + /** + * Merges exit, shared element, and enter transitions so that they act together or + * sequentially as defined in the fragments. + */ + private static Object mergeTransitions(FragmentTransitionImpl impl, Object enterTransition, + Object exitTransition, Object sharedElementTransition, Fragment inFragment, + boolean isPop) { + boolean overlap = true; + if (enterTransition != null && exitTransition != null && inFragment != null) { + overlap = isPop ? inFragment.getAllowReturnTransitionOverlap() : + inFragment.getAllowEnterTransitionOverlap(); + } + + // Wrap the transitions. Explicit targets like in enter and exit will cause the + // views to be targeted regardless of excluded views. If that happens, then the + // excluded fragments views (hidden fragments) will still be in the transition. + + Object transition; + if (overlap) { + // Regular transition -- do it all together + transition = impl.mergeTransitionsTogether(exitTransition, + enterTransition, sharedElementTransition); + } else { + // First do exit, then enter, but allow shared element transition to happen + // during both. + transition = impl.mergeTransitionsInSequence(exitTransition, + enterTransition, sharedElementTransition); + } + return transition; + } + + /** + * Finds the first removed fragment and last added fragments when going forward. + * If none of the fragments have transitions, then both lists will be empty. + * + * @param transitioningFragments Keyed on the container ID, the first fragments to be removed, + * and last fragments to be added. This will be modified by + * this method. + */ + public static void calculateFragments(BackStackRecord transaction, + SparseArray transitioningFragments, + boolean isReordered) { + final int numOps = transaction.mOps.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + final BackStackRecord.Op op = transaction.mOps.get(opNum); + addToFirstInLastOut(transaction, op, transitioningFragments, false, isReordered); + } + } + + /** + * Finds the first removed fragment and last added fragments when popping the back stack. + * If none of the fragments have transitions, then both lists will be empty. + * + * @param transitioningFragments Keyed on the container ID, the first fragments to be removed, + * and last fragments to be added. This will be modified by + * this method. + */ + public static void calculatePopFragments(BackStackRecord transaction, + SparseArray transitioningFragments, boolean isReordered) { + if (!transaction.mManager.mContainer.onHasView()) { + return; // nothing to see, so no transitions + } + final int numOps = transaction.mOps.size(); + for (int opNum = numOps - 1; opNum >= 0; opNum--) { + final BackStackRecord.Op op = transaction.mOps.get(opNum); + addToFirstInLastOut(transaction, op, transitioningFragments, true, isReordered); + } + } + + static boolean supportsTransition() { + return PLATFORM_IMPL != null || SUPPORT_IMPL != null; + } + + /** + * Examines the {@code command} and may set the first out or last in fragment for the fragment's + * container. + * + * @param transaction The executing transaction + * @param op The operation being run. + * @param transitioningFragments A structure holding the first in and last out fragments + * for each fragment container. + * @param isPop Is the operation a pop? + * @param isReorderedTransaction True if the operations have been partially executed and the + * added fragments have Views in the hierarchy or false if the + * operations haven't been executed yet. + */ + @SuppressWarnings("ReferenceEquality") + private static void addToFirstInLastOut(BackStackRecord transaction, BackStackRecord.Op op, + SparseArray transitioningFragments, boolean isPop, + boolean isReorderedTransaction) { + final Fragment fragment = op.mFragment; + if (fragment == null) { + return; // no fragment, no transition + } + final int containerId = fragment.mContainerId; + if (containerId == 0) { + return; // no container, no transition + } + final int command = isPop ? INVERSE_OPS[op.mCmd] : op.mCmd; + boolean setLastIn = false; + boolean wasRemoved = false; + boolean setFirstOut = false; + boolean wasAdded = false; + switch (command) { + case BackStackRecord.OP_SHOW: + if (isReorderedTransaction) { + setLastIn = fragment.mHiddenChanged && !fragment.mHidden && fragment.mAdded; + } else { + setLastIn = fragment.mHidden; + } + wasAdded = true; + break; + case BackStackRecord.OP_ADD: + case BackStackRecord.OP_ATTACH: + if (isReorderedTransaction) { + setLastIn = fragment.mIsNewlyAdded; + } else { + setLastIn = !fragment.mAdded && !fragment.mHidden; + } + wasAdded = true; + break; + case BackStackRecord.OP_HIDE: + if (isReorderedTransaction) { + setFirstOut = fragment.mHiddenChanged && fragment.mAdded && fragment.mHidden; + } else { + setFirstOut = fragment.mAdded && !fragment.mHidden; + } + wasRemoved = true; + break; + case BackStackRecord.OP_REMOVE: + case BackStackRecord.OP_DETACH: + if (isReorderedTransaction) { + setFirstOut = !fragment.mAdded && fragment.mView != null + && fragment.mView.getVisibility() == View.VISIBLE + && fragment.mPostponedAlpha >= 0; + } else { + setFirstOut = fragment.mAdded && !fragment.mHidden; + } + wasRemoved = true; + break; + } + FragmentContainerTransition containerTransition = transitioningFragments.get(containerId); + if (setLastIn) { + containerTransition = + ensureContainer(containerTransition, transitioningFragments, containerId); + containerTransition.lastIn = fragment; + containerTransition.lastInIsPop = isPop; + containerTransition.lastInTransaction = transaction; + } + if (!isReorderedTransaction && wasAdded) { + if (containerTransition != null && containerTransition.firstOut == fragment) { + containerTransition.firstOut = null; + } + + /* + * Ensure that fragments that are entering are at least at the CREATED state + * so that they may load Transitions using TransitionInflater. + */ + FragmentManagerImpl manager = transaction.mManager; + if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED + && !transaction.mReorderingAllowed) { + manager.makeActive(fragment); + manager.moveToState(fragment, Fragment.CREATED, 0, 0, false); + } + } + if (setFirstOut && (containerTransition == null || containerTransition.firstOut == null)) { + containerTransition = + ensureContainer(containerTransition, transitioningFragments, containerId); + containerTransition.firstOut = fragment; + containerTransition.firstOutIsPop = isPop; + containerTransition.firstOutTransaction = transaction; + } + + if (!isReorderedTransaction && wasRemoved + && (containerTransition != null && containerTransition.lastIn == fragment)) { + containerTransition.lastIn = null; + } + } + + /** + * Ensures that a FragmentContainerTransition has been added to the SparseArray. If so, + * it returns the existing one. If not, one is created and added to the SparseArray and + * returned. + */ + private static FragmentContainerTransition ensureContainer( + FragmentContainerTransition containerTransition, + SparseArray transitioningFragments, int containerId) { + if (containerTransition == null) { + containerTransition = new FragmentContainerTransition(); + transitioningFragments.put(containerId, containerTransition); + } + return containerTransition; + } + + /** + * Tracks the last fragment added and first fragment removed for fragment transitions. + * This also tracks which fragments are changed by push or pop transactions. + */ + static class FragmentContainerTransition { + /** + * The last fragment added/attached/shown in its container + */ + public Fragment lastIn; + + /** + * true when lastIn was added during a pop transaction or false if added with a push + */ + public boolean lastInIsPop; + + /** + * The transaction that included the last in fragment + */ + public BackStackRecord lastInTransaction; + + /** + * The first fragment with a View that was removed/detached/hidden in its container. + */ + public Fragment firstOut; + + /** + * true when firstOut was removed during a pop transaction or false otherwise + */ + public boolean firstOutIsPop; + + /** + * The transaction that included the first out fragment + */ + public BackStackRecord firstOutTransaction; + } + + private FragmentTransition() { + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java new file mode 100644 index 000000000..edd8e6861 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionCompat21.java @@ -0,0 +1,318 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.graphics.Rect; +import android.transition.Transition; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.RequiresApi; + +import java.util.ArrayList; +import java.util.List; + +@RequiresApi(21) +class FragmentTransitionCompat21 extends FragmentTransitionImpl { + + @Override + public boolean canHandle(Object transition) { + return transition instanceof Transition; + } + + @Override + public Object cloneTransition(Object transition) { + Transition copy = null; + if (transition != null) { + copy = ((Transition) transition).clone(); + } + return copy; + } + + @Override + public Object wrapTransitionInSet(Object transition) { + if (transition == null) { + return null; + } + TransitionSet transitionSet = new TransitionSet(); + transitionSet.addTransition((Transition) transition); + return transitionSet; + } + + @Override + public void setSharedElementTargets(Object transitionObj, + View nonExistentView, ArrayList sharedViews) { + TransitionSet transition = (TransitionSet) transitionObj; + final List views = transition.getTargets(); + views.clear(); + final int count = sharedViews.size(); + for (int i = 0; i < count; i++) { + final View view = sharedViews.get(i); + bfsAddViewChildren(views, view); + } + views.add(nonExistentView); + sharedViews.add(nonExistentView); + addTargets(transition, sharedViews); + } + + @Override + public void setEpicenter(Object transitionObj, View view) { + if (view != null) { + Transition transition = (Transition) transitionObj; + final Rect epicenter = new Rect(); + getBoundsOnScreen(view, epicenter); + + transition.setEpicenterCallback(new Transition.EpicenterCallback() { + @Override + public Rect onGetEpicenter(Transition transition) { + return epicenter; + } + }); + } + } + + @Override + public void addTargets(Object transitionObj, ArrayList views) { + Transition transition = (Transition) transitionObj; + if (transition == null) { + return; + } + if (transition instanceof TransitionSet) { + TransitionSet set = (TransitionSet) transition; + int numTransitions = set.getTransitionCount(); + for (int i = 0; i < numTransitions; i++) { + Transition child = set.getTransitionAt(i); + addTargets(child, views); + } + } else if (!hasSimpleTarget(transition)) { + List targets = transition.getTargets(); + if (isNullOrEmpty(targets)) { + // We can just add the target views + int numViews = views.size(); + for (int i = 0; i < numViews; i++) { + transition.addTarget(views.get(i)); + } + } + } + } + + /** + * Returns true if there are any targets based on ID, transition or type. + */ + private static boolean hasSimpleTarget(Transition transition) { + return !isNullOrEmpty(transition.getTargetIds()) + || !isNullOrEmpty(transition.getTargetNames()) + || !isNullOrEmpty(transition.getTargetTypes()); + } + + @Override + public Object mergeTransitionsTogether(Object transition1, Object transition2, + Object transition3) { + TransitionSet transitionSet = new TransitionSet(); + if (transition1 != null) { + transitionSet.addTransition((Transition) transition1); + } + if (transition2 != null) { + transitionSet.addTransition((Transition) transition2); + } + if (transition3 != null) { + transitionSet.addTransition((Transition) transition3); + } + return transitionSet; + } + + @Override + public void scheduleHideFragmentView(Object exitTransitionObj, final View fragmentView, + final ArrayList exitingViews) { + Transition exitTransition = (Transition) exitTransitionObj; + exitTransition.addListener(new Transition.TransitionListener() { + @Override + public void onTransitionStart(Transition transition) { + } + + @Override + public void onTransitionEnd(Transition transition) { + transition.removeListener(this); + fragmentView.setVisibility(View.GONE); + final int numViews = exitingViews.size(); + for (int i = 0; i < numViews; i++) { + exitingViews.get(i).setVisibility(View.VISIBLE); + } + } + + @Override + public void onTransitionCancel(Transition transition) { + } + + @Override + public void onTransitionPause(Transition transition) { + } + + @Override + public void onTransitionResume(Transition transition) { + } + }); + } + + @Override + public Object mergeTransitionsInSequence(Object exitTransitionObj, + Object enterTransitionObj, Object sharedElementTransitionObj) { + // First do exit, then enter, but allow shared element transition to happen + // during both. + Transition staggered = null; + final Transition exitTransition = (Transition) exitTransitionObj; + final Transition enterTransition = (Transition) enterTransitionObj; + final Transition sharedElementTransition = (Transition) sharedElementTransitionObj; + if (exitTransition != null && enterTransition != null) { + staggered = new TransitionSet() + .addTransition(exitTransition) + .addTransition(enterTransition) + .setOrdering(TransitionSet.ORDERING_SEQUENTIAL); + } else if (exitTransition != null) { + staggered = exitTransition; + } else if (enterTransition != null) { + staggered = enterTransition; + } + if (sharedElementTransition != null) { + TransitionSet together = new TransitionSet(); + if (staggered != null) { + together.addTransition(staggered); + } + together.addTransition(sharedElementTransition); + return together; + } else { + return staggered; + } + } + + @Override + public void beginDelayedTransition(ViewGroup sceneRoot, Object transition) { + TransitionManager.beginDelayedTransition(sceneRoot, (Transition) transition); + } + + @Override + public void scheduleRemoveTargets(final Object overallTransitionObj, + final Object enterTransition, final ArrayList enteringViews, + final Object exitTransition, final ArrayList exitingViews, + final Object sharedElementTransition, final ArrayList sharedElementsIn) { + final Transition overallTransition = (Transition) overallTransitionObj; + overallTransition.addListener(new Transition.TransitionListener() { + @Override + public void onTransitionStart(Transition transition) { + if (enterTransition != null) { + replaceTargets(enterTransition, enteringViews, null); + } + if (exitTransition != null) { + replaceTargets(exitTransition, exitingViews, null); + } + if (sharedElementTransition != null) { + replaceTargets(sharedElementTransition, sharedElementsIn, null); + } + } + + @Override + public void onTransitionEnd(Transition transition) { + transition.removeListener(this); + } + + @Override + public void onTransitionCancel(Transition transition) { + } + + @Override + public void onTransitionPause(Transition transition) { + } + + @Override + public void onTransitionResume(Transition transition) { + } + }); + } + + @Override + public void swapSharedElementTargets(Object sharedElementTransitionObj, + ArrayList sharedElementsOut, ArrayList sharedElementsIn) { + TransitionSet sharedElementTransition = (TransitionSet) sharedElementTransitionObj; + if (sharedElementTransition != null) { + sharedElementTransition.getTargets().clear(); + sharedElementTransition.getTargets().addAll(sharedElementsIn); + replaceTargets(sharedElementTransition, sharedElementsOut, sharedElementsIn); + } + } + + @Override + public void replaceTargets(Object transitionObj, ArrayList oldTargets, + ArrayList newTargets) { + Transition transition = (Transition) transitionObj; + if (transition instanceof TransitionSet) { + TransitionSet set = (TransitionSet) transition; + int numTransitions = set.getTransitionCount(); + for (int i = 0; i < numTransitions; i++) { + Transition child = set.getTransitionAt(i); + replaceTargets(child, oldTargets, newTargets); + } + } else if (!hasSimpleTarget(transition)) { + List targets = transition.getTargets(); + if (targets != null && targets.size() == oldTargets.size() + && targets.containsAll(oldTargets)) { + // We have an exact match. We must have added these earlier in addTargets + final int targetCount = newTargets == null ? 0 : newTargets.size(); + for (int i = 0; i < targetCount; i++) { + transition.addTarget(newTargets.get(i)); + } + for (int i = oldTargets.size() - 1; i >= 0; i--) { + transition.removeTarget(oldTargets.get(i)); + } + } + } + } + + @Override + public void addTarget(Object transitionObj, View view) { + if (transitionObj != null) { + Transition transition = (Transition) transitionObj; + transition.addTarget(view); + } + } + + @Override + public void removeTarget(Object transitionObj, View view) { + if (transitionObj != null) { + Transition transition = (Transition) transitionObj; + transition.removeTarget(view); + } + } + + @Override + public void setEpicenter(Object transitionObj, final Rect epicenter) { + if (transitionObj != null) { + Transition transition = (Transition) transitionObj; + transition.setEpicenterCallback(new Transition.EpicenterCallback() { + @Override + public Rect onGetEpicenter(Transition transition) { + if (epicenter == null || epicenter.isEmpty()) { + return null; + } + return epicenter; + } + }); + } + } + +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java new file mode 100644 index 000000000..ccd121e30 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentTransitionImpl.java @@ -0,0 +1,368 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; + +import android.annotation.SuppressLint; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.RestrictTo; +import androidx.core.view.OneShotPreDrawListener; +import androidx.core.view.ViewCompat; +import androidx.core.view.ViewGroupCompat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + +/** + * @hide + */ +@RestrictTo(LIBRARY_GROUP_PREFIX) +@SuppressLint("UnknownNullness") +public abstract class FragmentTransitionImpl { + + /** + * Returns {@code true} if this implementation can handle the specified {@link transition}. + */ + public abstract boolean canHandle(Object transition); + + /** + * Returns a clone of a transition or null if it is null + */ + public abstract Object cloneTransition(Object transition); + + /** + * Wraps a transition in a TransitionSet and returns the set. If transition is null, null is + * returned. + */ + public abstract Object wrapTransitionInSet(Object transition); + + /** + * Finds all children of the shared elements and sets the wrapping TransitionSet + * targets to point to those. It also limits transitions that have no targets to the + * specific shared elements. This allows developers to target child views of the + * shared elements specifically, but this doesn't happen by default. + */ + public abstract void setSharedElementTargets(Object transitionObj, + View nonExistentView, ArrayList sharedViews); + + /** + * Sets a transition epicenter to the rectangle of a given View. + */ + public abstract void setEpicenter(Object transitionObj, View view); + + /** + * Replacement for view.getBoundsOnScreen because that is not public. This returns a rect + * containing the bounds relative to the screen that the view is in. + */ + protected void getBoundsOnScreen(View view, Rect epicenter) { + int[] loc = new int[2]; + view.getLocationOnScreen(loc); + epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight()); + } + + /** + * This method adds views as targets to the transition, but only if the transition + * doesn't already have a target. It is best for views to contain one View object + * that does not exist in the view hierarchy (state.nonExistentView) so that + * when they are removed later, a list match will suffice to remove the targets. + * Otherwise, if you happened to have targeted the exact views for the transition, + * the replaceTargets call will remove them unexpectedly. + */ + public abstract void addTargets(Object transitionObj, ArrayList views); + + /** + * Creates a TransitionSet that plays all passed transitions together. Any null + * transitions passed will not be added to the set. If all are null, then an empty + * TransitionSet will be returned. + */ + public abstract Object mergeTransitionsTogether(Object transition1, Object transition2, + Object transition3); + + /** + * After the transition completes, the fragment's view is set to GONE and the exiting + * views are set to VISIBLE. + */ + public abstract void scheduleHideFragmentView(Object exitTransitionObj, View fragmentView, + ArrayList exitingViews); + + /** + * Combines enter, exit, and shared element transition so that they play in the proper + * sequence. First the exit transition plays along with the shared element transition. + * When the exit transition completes, the enter transition starts. The shared element + * transition can continue running while the enter transition plays. + * + * @return A TransitionSet with all of enter, exit, and shared element transitions in + * it (modulo null values), ordered such that they play in the proper sequence. + */ + public abstract Object mergeTransitionsInSequence(Object exitTransitionObj, + Object enterTransitionObj, Object sharedElementTransitionObj); + + /** + * Calls {@code TransitionManager#beginDelayedTransition(ViewGroup, Transition)}. + */ + public abstract void beginDelayedTransition(ViewGroup sceneRoot, Object transition); + + /** + * Prepares for setting the shared element names by gathering the names of the incoming + * shared elements and clearing them. {@link #setNameOverridesReordered(View, ArrayList, + * ArrayList, ArrayList, Map)} must be called after this to complete setting the shared element + * name overrides. This must be called before + * {@link #beginDelayedTransition(ViewGroup, Object)}. + */ + ArrayList prepareSetNameOverridesReordered(ArrayList sharedElementsIn) { + final ArrayList names = new ArrayList<>(); + final int numSharedElements = sharedElementsIn.size(); + for (int i = 0; i < numSharedElements; i++) { + final View view = sharedElementsIn.get(i); + names.add(ViewCompat.getTransitionName(view)); + ViewCompat.setTransitionName(view, null); + } + return names; + } + + /** + * Changes the shared element names for the incoming shared elements to match those of the + * outgoing shared elements. This also temporarily clears the shared element names of the + * outgoing shared elements. Must be called after + * {@link #beginDelayedTransition(ViewGroup, Object)}. + */ + void setNameOverridesReordered(final View sceneRoot, + final ArrayList sharedElementsOut, final ArrayList sharedElementsIn, + final ArrayList inNames, final Map nameOverrides) { + final int numSharedElements = sharedElementsIn.size(); + final ArrayList outNames = new ArrayList<>(); + + for (int i = 0; i < numSharedElements; i++) { + final View view = sharedElementsOut.get(i); + final String name = ViewCompat.getTransitionName(view); + outNames.add(name); + if (name == null) { + continue; + } + ViewCompat.setTransitionName(view, null); + final String inName = nameOverrides.get(name); + for (int j = 0; j < numSharedElements; j++) { + if (inName.equals(inNames.get(j))) { + ViewCompat.setTransitionName(sharedElementsIn.get(j), name); + break; + } + } + } + + OneShotPreDrawListener.add(sceneRoot, new Runnable() { + @Override + public void run() { + for (int i = 0; i < numSharedElements; i++) { + ViewCompat.setTransitionName(sharedElementsIn.get(i), inNames.get(i)); + ViewCompat.setTransitionName(sharedElementsOut.get(i), outNames.get(i)); + } + } + }); + } + + /** + * Gets the Views in the hierarchy affected by entering and exiting Activity Scene transitions. + * + * @param transitioningViews This View will be added to transitioningViews if it is VISIBLE and + * a normal View or a ViewGroup with + * {@link android.view.ViewGroup#isTransitionGroup()} true. + * @param view The base of the view hierarchy to look in. + */ + void captureTransitioningViews(ArrayList transitioningViews, View view) { + if (view.getVisibility() == View.VISIBLE) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + if (ViewGroupCompat.isTransitionGroup(viewGroup)) { + transitioningViews.add(viewGroup); + } else { + int count = viewGroup.getChildCount(); + for (int i = 0; i < count; i++) { + View child = viewGroup.getChildAt(i); + captureTransitioningViews(transitioningViews, child); + } + } + } else { + transitioningViews.add(view); + } + } + } + + /** + * Finds all views that have transition names in the hierarchy under the given view and + * stores them in {@code namedViews} map with the name as the key. + */ + void findNamedViews(Map namedViews, @NonNull View view) { + if (view.getVisibility() == View.VISIBLE) { + String transitionName = ViewCompat.getTransitionName(view); + if (transitionName != null) { + namedViews.put(transitionName, view); + } + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + int count = viewGroup.getChildCount(); + for (int i = 0; i < count; i++) { + View child = viewGroup.getChildAt(i); + findNamedViews(namedViews, child); + } + } + } + } + + /** + *Applies the prepared {@code nameOverrides} to the view hierarchy. + */ + void setNameOverridesOrdered(final View sceneRoot, + final ArrayList sharedElementsIn, final Map nameOverrides) { + OneShotPreDrawListener.add(sceneRoot, new Runnable() { + @Override + public void run() { + final int numSharedElements = sharedElementsIn.size(); + for (int i = 0; i < numSharedElements; i++) { + View view = sharedElementsIn.get(i); + String name = ViewCompat.getTransitionName(view); + if (name != null) { + String inName = findKeyForValue(nameOverrides, name); + ViewCompat.setTransitionName(view, inName); + } + } + } + }); + } + + /** + * After the transition has started, remove all targets that we added to the transitions + * so that the transitions are left in a clean state. + */ + public abstract void scheduleRemoveTargets(Object overallTransitionObj, + Object enterTransition, ArrayList enteringViews, + Object exitTransition, ArrayList exitingViews, + Object sharedElementTransition, ArrayList sharedElementsIn); + + /** + * Swap the targets for the shared element transition from those Views in sharedElementsOut + * to those in sharedElementsIn + */ + public abstract void swapSharedElementTargets(Object sharedElementTransitionObj, + ArrayList sharedElementsOut, ArrayList sharedElementsIn); + + /** + * This method removes the views from transitions that target ONLY those views and + * replaces them with the new targets list. + * The views list should match those added in addTargets and should contain + * one view that is not in the view hierarchy (state.nonExistentView). + */ + public abstract void replaceTargets(Object transitionObj, ArrayList oldTargets, + ArrayList newTargets); + + /** + * Adds a View target to a transition. If transitionObj is null, nothing is done. + */ + public abstract void addTarget(Object transitionObj, View view); + + /** + * Remove a View target to a transition. If transitionObj is null, nothing is done. + */ + public abstract void removeTarget(Object transitionObj, View view); + + /** + * Sets the epicenter of a transition to a rect object. The object can be modified until + * the transition runs. + */ + public abstract void setEpicenter(Object transitionObj, Rect epicenter); + + void scheduleNameReset(final ViewGroup sceneRoot, + final ArrayList sharedElementsIn, final Map nameOverrides) { + OneShotPreDrawListener.add(sceneRoot, new Runnable() { + @Override + public void run() { + final int numSharedElements = sharedElementsIn.size(); + for (int i = 0; i < numSharedElements; i++) { + final View view = sharedElementsIn.get(i); + final String name = ViewCompat.getTransitionName(view); + final String inName = nameOverrides.get(name); + ViewCompat.setTransitionName(view, inName); + } + } + }); + } + + /** + * Uses a breadth-first scheme to add startView and all of its children to views. + * It won't add a child if it is already in views. + */ + protected static void bfsAddViewChildren(final List views, final View startView) { + final int startIndex = views.size(); + if (containedBeforeIndex(views, startView, startIndex)) { + return; // This child is already in the list, so all its children are also. + } + views.add(startView); + for (int index = startIndex; index < views.size(); index++) { + final View view = views.get(index); + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + final int childCount = viewGroup.getChildCount(); + for (int childIndex = 0; childIndex < childCount; childIndex++) { + final View child = viewGroup.getChildAt(childIndex); + if (!containedBeforeIndex(views, child, startIndex)) { + views.add(child); + } + } + } + } + } + + /** + * Does a linear search through views for view, limited to maxIndex. + */ + private static boolean containedBeforeIndex(final List views, final View view, + final int maxIndex) { + for (int i = 0; i < maxIndex; i++) { + if (views.get(i) == view) { + return true; + } + } + return false; + } + + /** + * Simple utility to detect if a list is null or has no elements. + */ + protected static boolean isNullOrEmpty(List list) { + return list == null || list.isEmpty(); + } + + /** + * Utility to find the String key in {@code map} that maps to {@code value}. + */ + @SuppressWarnings("WeakerAccess") + static String findKeyForValue(Map map, String value) { + for (Map.Entry entry : map.entrySet()) { + if (value.equals(entry.getValue())) { + return entry.getKey(); + } + } + return null; + } + +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java new file mode 100644 index 000000000..a62e0ca6c --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; + +class FragmentViewLifecycleOwner implements LifecycleOwner { + private LifecycleRegistry mLifecycleRegistry = null; + + /** + * Initializes the underlying Lifecycle if it hasn't already been created. + */ + void initialize() { + if (mLifecycleRegistry == null) { + mLifecycleRegistry = new LifecycleRegistry(this); + } + } + + /** + * @return True if the Lifecycle has been initialized. + */ + boolean isInitialized() { + return mLifecycleRegistry != null; + } + + @NonNull + @Override + public Lifecycle getLifecycle() { + initialize(); + return mLifecycleRegistry; + } + + void handleLifecycleEvent(@NonNull Lifecycle.Event event) { + mLifecycleRegistry.handleLifecycleEvent(event); + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/ListFragment.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/ListFragment.java new file mode 100644 index 000000000..adbcb6776 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/ListFragment.java @@ -0,0 +1,401 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Static library support version of the framework's {@link android.app.ListFragment}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class ListFragment extends Fragment { + static final int INTERNAL_EMPTY_ID = 0x00ff0001; + static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002; + static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003; + + final private Handler mHandler = new Handler(); + + final private Runnable mRequestFocus = new Runnable() { + @Override + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + final private AdapterView.OnItemClickListener mOnClickListener + = new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + onListItemClick((ListView)parent, v, position, id); + } + }; + + ListAdapter mAdapter; + ListView mList; + View mEmptyView; + TextView mStandardEmptyView; + View mProgressContainer; + View mListContainer; + CharSequence mEmptyText; + boolean mListShown; + + public ListFragment() { + } + + /** + * Provide default implementation to return a simple list view. Subclasses + * can override to replace with their own layout. If doing so, the + * returned view hierarchy must have a ListView whose id + * is {@link android.R.id#list android.R.id.list} and can optionally + * have a sibling view id {@link android.R.id#empty android.R.id.empty} + * that is to be shown when the list is empty. + * + *

If you are overriding this method with your own custom content, + * consider including the standard layout {@link android.R.layout#list_content} + * in your layout file, so that you continue to retain all of the standard + * behavior of ListFragment. In particular, this is currently the only + * way to have the built-in indeterminant progress state be shown. + */ + @Override + @Nullable + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + final Context context = requireContext(); + + FrameLayout root = new FrameLayout(context); + + // ------------------------------------------------------------------ + + LinearLayout pframe = new LinearLayout(context); + pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID); + pframe.setOrientation(LinearLayout.VERTICAL); + pframe.setVisibility(View.GONE); + pframe.setGravity(Gravity.CENTER); + + ProgressBar progress = new ProgressBar(context, null, + android.R.attr.progressBarStyleLarge); + pframe.addView(progress, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + root.addView(pframe, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // ------------------------------------------------------------------ + + FrameLayout lframe = new FrameLayout(context); + lframe.setId(INTERNAL_LIST_CONTAINER_ID); + + TextView tv = new TextView(context); + tv.setId(INTERNAL_EMPTY_ID); + tv.setGravity(Gravity.CENTER); + lframe.addView(tv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + ListView lv = new ListView(context); + lv.setId(android.R.id.list); + lv.setDrawSelectorOnTop(false); + lframe.addView(lv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + root.addView(lframe, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // ------------------------------------------------------------------ + + root.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + return root; + } + + /** + * Attach to list view once the view hierarchy has been created. + */ + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ensureList(); + } + + /** + * Detach from list view. + */ + @Override + public void onDestroyView() { + mHandler.removeCallbacks(mRequestFocus); + mList = null; + mListShown = false; + mEmptyView = mProgressContainer = mListContainer = null; + mStandardEmptyView = null; + super.onDestroyView(); + } + + /** + * This method will be called when an item in the list is selected. + * Subclasses should override. Subclasses can call + * getListView().getItemAtPosition(position) if they need to access the + * data associated with the selected item. + * + * @param l The ListView where the click happened + * @param v The view that was clicked within the ListView + * @param position The position of the view in the list + * @param id The row id of the item that was clicked + */ + public void onListItemClick(@NonNull ListView l, @NonNull View v, int position, long id) { + } + + /** + * Provide the cursor for the list view. + */ + public void setListAdapter(@Nullable ListAdapter adapter) { + boolean hadAdapter = mAdapter != null; + mAdapter = adapter; + if (mList != null) { + mList.setAdapter(adapter); + if (!mListShown && !hadAdapter) { + // The list was hidden, and previously didn't have an + // adapter. It is now time to show it. + setListShown(true, requireView().getWindowToken() != null); + } + } + } + + /** + * Set the currently selected list item to the specified + * position with the adapter's data + * + * @param position + */ + public void setSelection(int position) { + ensureList(); + mList.setSelection(position); + } + + /** + * Get the position of the currently selected list item. + */ + public int getSelectedItemPosition() { + ensureList(); + return mList.getSelectedItemPosition(); + } + + /** + * Get the cursor row ID of the currently selected list item. + */ + public long getSelectedItemId() { + ensureList(); + return mList.getSelectedItemId(); + } + + /** + * Get the fragment's list view widget. + */ + @NonNull + public ListView getListView() { + ensureList(); + return mList; + } + + /** + * The default content for a ListFragment has a TextView that can + * be shown when the list is empty. If you would like to have it + * shown, call this method to supply the text it should use. + */ + public void setEmptyText(@Nullable CharSequence text) { + ensureList(); + if (mStandardEmptyView == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + mStandardEmptyView.setText(text); + if (mEmptyText == null) { + mList.setEmptyView(mStandardEmptyView); + } + mEmptyText = text; + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + *

Applications do not normally need to use this themselves. The default + * behavior of ListFragment is to start with the list not being shown, only + * showing it once an adapter is given with {@link #setListAdapter(ListAdapter)}. + * If the list at that point had not been shown, when it does get shown + * it will be do without the user ever seeing the hidden state. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + */ + public void setListShown(boolean shown) { + setListShown(shown, true); + } + + /** + * Like {@link #setListShown(boolean)}, but no animation is used when + * transitioning from the previous state. + */ + public void setListShownNoAnimation(boolean shown) { + setListShown(shown, false); + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + * @param animate If true, an animation will be used to transition to the + * new state. + */ + private void setListShown(boolean shown, boolean animate) { + ensureList(); + if (mProgressContainer == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + if (mListShown == shown) { + return; + } + mListShown = shown; + if (shown) { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getContext(), android.R.anim.fade_out)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getContext(), android.R.anim.fade_in)); + } else { + mProgressContainer.clearAnimation(); + mListContainer.clearAnimation(); + } + mProgressContainer.setVisibility(View.GONE); + mListContainer.setVisibility(View.VISIBLE); + } else { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getContext(), android.R.anim.fade_in)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getContext(), android.R.anim.fade_out)); + } else { + mProgressContainer.clearAnimation(); + mListContainer.clearAnimation(); + } + mProgressContainer.setVisibility(View.VISIBLE); + mListContainer.setVisibility(View.GONE); + } + } + + /** + * Get the ListAdapter associated with this fragment's ListView. + * + * @see #requireListAdapter() + */ + @Nullable + public ListAdapter getListAdapter() { + return mAdapter; + } + + /** + * Get the ListAdapter associated with this fragment's ListView. + * + * @throws IllegalStateException if no ListAdapter has been set. + * @see #getListAdapter() + */ + @NonNull + public final ListAdapter requireListAdapter() { + ListAdapter listAdapter = getListAdapter(); + if (listAdapter == null) { + throw new IllegalStateException("ListFragment " + this + + " does not have a ListAdapter."); + } + return listAdapter; + } + + private void ensureList() { + if (mList != null) { + return; + } + View root = getView(); + if (root == null) { + throw new IllegalStateException("Content view not yet created"); + } + if (root instanceof ListView) { + mList = (ListView)root; + } else { + mStandardEmptyView = (TextView)root.findViewById(INTERNAL_EMPTY_ID); + if (mStandardEmptyView == null) { + mEmptyView = root.findViewById(android.R.id.empty); + } else { + mStandardEmptyView.setVisibility(View.GONE); + } + mProgressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID); + mListContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID); + View rawListView = root.findViewById(android.R.id.list); + if (!(rawListView instanceof ListView)) { + if (rawListView == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + + "'android.R.id.list'"); + } + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + mList = (ListView)rawListView; + if (mEmptyView != null) { + mList.setEmptyView(mEmptyView); + } else if (mEmptyText != null) { + mStandardEmptyView.setText(mEmptyText); + mList.setEmptyView(mStandardEmptyView); + } + } + mListShown = true; + mList.setOnItemClickListener(mOnClickListener); + if (mAdapter != null) { + ListAdapter adapter = mAdapter; + mAdapter = null; + setListAdapter(adapter); + } else { + // We are starting without an adapter, so assume we won't + // have our data right away and start with the progress indicator. + if (mProgressContainer != null) { + setListShown(false, false); + } + } + mHandler.post(mRequestFocus); + } +} diff --git a/fragment-1.1.0/src/main/java/androidx/fragment/app/SuperNotCalledException.java b/fragment-1.1.0/src/main/java/androidx/fragment/app/SuperNotCalledException.java new file mode 100644 index 000000000..467818a83 --- /dev/null +++ b/fragment-1.1.0/src/main/java/androidx/fragment/app/SuperNotCalledException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * 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 androidx.fragment.app; + +import android.util.AndroidRuntimeException; + +final class SuperNotCalledException extends AndroidRuntimeException { + public SuperNotCalledException(String msg) { + super(msg); + } +} diff --git a/leanback-1.0.0/build.gradle b/leanback-1.0.0/build.gradle index f4f37d916..fd4928b2b 100644 --- a/leanback-1.0.0/build.gradle +++ b/leanback-1.0.0/build.gradle @@ -34,11 +34,12 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.annotation:annotation:' + annotationXLibraryVersion - implementation 'androidx.core:core:1.6.0' - implementation 'androidx.interpolator:interpolator:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.0.0' - implementation 'androidx.fragment:fragment:1.1.0' - implementation 'androidx.media:media:1.2.0' + implementation 'androidx.core:core:' + kotlinCoreVersion + implementation 'androidx.recyclerview:recyclerview:' + recyclerviewXLibraryVersion + implementation 'androidx.fragment:fragment:' + annotationXLibraryVersion + implementation 'androidx.media:media:' + mediaXLibraryVersion + implementation 'androidx.interpolator:interpolator:' + leanbackXLibraryVersion + implementation project(':fragment-1.1.0') implementation project(':sharedutils') } diff --git a/settings.gradle b/settings.gradle index e62887ff9..a7c16ac39 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,4 @@ -include ':smarttubetv', ':common', ':chatkit', ':leanbackassistant', ':leanback-1.0.0' +include ':smarttubetv', ':common', ':chatkit', ':leanbackassistant', ':leanback-1.0.0', ':fragment-1.1.0' def rootDir = settingsDir diff --git a/smarttubetv/build.gradle b/smarttubetv/build.gradle index 9ee7d6e75..9cfe3fd3a 100644 --- a/smarttubetv/build.gradle +++ b/smarttubetv/build.gradle @@ -206,6 +206,7 @@ dependencies { androidTestImplementation 'androidx.test:rules:' + testXSupportLibraryVersion androidTestImplementation 'androidx.test.espresso:espresso-core:' + espressoVersion + implementation project(':fragment-1.1.0') implementation project(':leanback-1.0.0') implementation project(':common') implementation project(':sharedutils')