Merge branch 'goog/pi-preview1-androidx-dev' into flatfoot-navigation
Change-Id: I1e4cf5b4050d64fe2e3a856f71f75a8610ee9ab8
diff --git a/.gitignore b/.gitignore
index ac3565c..6d32d4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@
**/gen
*.iml
**/out
+**/build
buildSrc/build
lifecycle/common/build
jacoco.exec
diff --git a/api/26.0.0-alpha1.txt b/api/26.0.0-alpha1.txt
index 7300fa5..d4699e7 100644
--- a/api/26.0.0-alpha1.txt
+++ b/api/26.0.0-alpha1.txt
@@ -5279,7 +5279,7 @@
method public abstract android.support.v4.app.FragmentTransaction hide(android.support.v4.app.Fragment);
method public abstract boolean isAddToBackStackAllowed();
method public abstract boolean isEmpty();
- method public abstract android.support.v4.app.FragmentTransaction postOnCommit(java.lang.Runnable);
+ method public abstract android.support.v4.app.FragmentTransaction runOnCommit(java.lang.Runnable);
method public abstract android.support.v4.app.FragmentTransaction remove(android.support.v4.app.Fragment);
method public abstract android.support.v4.app.FragmentTransaction replace(int, android.support.v4.app.Fragment);
method public abstract android.support.v4.app.FragmentTransaction replace(int, android.support.v4.app.Fragment, java.lang.String);
diff --git a/app-toolkit/init.gradle b/app-toolkit/init.gradle
index 6a91a4e..15c57c3 100644
--- a/app-toolkit/init.gradle
+++ b/app-toolkit/init.gradle
@@ -51,7 +51,7 @@
}
subprojects {
- project.tasks.whenTaskAdded { task ->
+ project.tasks.whenTaskAdded { task ->
if (task.name.startsWith("assembleAndroidTest")) {
buildServerAnchorTask.dependsOn task
}
diff --git a/app-toolkit/settings.gradle b/app-toolkit/settings.gradle
index d065af0..b674858 100644
--- a/app-toolkit/settings.gradle
+++ b/app-toolkit/settings.gradle
@@ -83,6 +83,20 @@
includeProject(":jetifier-standalone", new File(supportRoot, "jetifier/jetifier/standalone"))
includeProject(":jetifier-preprocessor", new File(supportRoot, "jetifier/jetifier/preprocessor"))
+includeProject(":navigation:navigation-common", new File(supportRoot, "navigation/common"))
+includeProject(":navigation:navigation-common-ktx", new File(supportRoot, "navigation/common/ktx"))
+includeProject(":navigation:navigation-runtime", new File(supportRoot, "navigation/runtime/"))
+includeProject(":navigation:navigation-runtime-ktx", new File(supportRoot, "navigation/runtime/ktx"))
+includeProject(":navigation:navigation-testing", new File(supportRoot, "navigation/testing"))
+includeProject(":navigation:navigation-testing-ktx", new File(supportRoot, "navigation/testing/ktx"))
+includeProject(":navigation:navigation-fragment", new File(supportRoot, "navigation/fragment"))
+includeProject(":navigation:navigation-fragment-ktx", new File(supportRoot, "navigation/fragment/ktx"))
+includeProject(":navigation:navigation-ui", new File(supportRoot, "navigation/ui"))
+includeProject(":navigation:navigation-ui-ktx", new File(supportRoot, "navigation/ui/ktx"))
+includeProject(":navigation:navigation-integration-tests:testapp", new File(supportRoot, "navigation/integration-tests/testapp"))
+includeProject(":navigation:navigation-safe-args-generator", new File(supportRoot, "navigation/safe-args-generator"))
+includeProject(":navigation:navigation-safe-args-gradle-plugin", new File(supportRoot, "navigation/safe-args-gradle-plugin"))
+
/////////////////////////////
//
// External
diff --git a/browser/src/main/java/androidx/browser/browseractions/BrowserActionsFallbackMenuDialog.java b/browser/src/main/java/androidx/browser/browseractions/BrowserActionsFallbackMenuDialog.java
index 09efc9c..d2fa0f7 100644
--- a/browser/src/main/java/androidx/browser/browseractions/BrowserActionsFallbackMenuDialog.java
+++ b/browser/src/main/java/androidx/browser/browseractions/BrowserActionsFallbackMenuDialog.java
@@ -39,7 +39,7 @@
private final View mContentView;
BrowserActionsFallbackMenuDialog(Context context, View contentView) {
- super(context);
+ super(context, android.support.customtabs.R.style.Theme_AppCompat_Light_Dialog);
mContentView = contentView;
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index f879a9f..6e0e7e6 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -80,6 +80,11 @@
val ARCH_CORE_TESTING = ARCH_CORE
/**
+ * Version code for Navigation
+ */
+ val NAVIGATION = Version("1.0.0-alpha01")
+
+ /**
* Version code for Jetifier
*/
val JETIFIER = Version("0.0.1")
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 7faa7fc..1d078ea 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -51,13 +51,25 @@
* https://github.com/xerial/sqlite-jdbc/issues/267
*/
const val XERIAL = "org.xerial:sqlite-jdbc:3.20.1"
+const val XPP3 = "xpp3:xpp3:1.1.4c"
+const val XMLPULL = "xmlpull:xmlpull:1.1.3.1"
const val ESPRESSO_CONTRIB_TMP = "com.android.temp.support.test.espresso:espresso-contrib:3.0.1"
const val ESPRESSO_CORE_TMP = "com.android.temp.support.test.espresso:espresso-core:3.0.1"
const val TEST_RUNNER_TMP = "com.android.temp.support.test:runner:1.0.1"
const val TEST_RULES_TMP = "com.android.temp.support.test:rules:1.0.1"
-// AndroidX libraries
+private const val NAV_SUPPORT_VERSION = "27.1.0"
+const val NAV_SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$NAV_SUPPORT_VERSION"
+const val NAV_SUPPORT_APPCOMPAT = "com.android.support:appcompat-v7:$NAV_SUPPORT_VERSION"
+const val NAV_SUPPORT_CARDVIEW = "com.android.support:cardview-v7:$NAV_SUPPORT_VERSION"
+const val NAV_SUPPORT_COMPAT = "com.android.support:support-compat:$NAV_SUPPORT_VERSION"
+const val NAV_SUPPORT_CORE_UTILS = "com.android.support:support-core-utils:$NAV_SUPPORT_VERSION"
+const val NAV_SUPPORT_DESIGN = "com.android.support:design:$NAV_SUPPORT_VERSION"
+const val NAV_SUPPORT_FRAGMENTS = "com.android.support:support-fragment:$NAV_SUPPORT_VERSION"
+const val NAV_SUPPORT_RECYCLERVIEW = "com.android.support:recyclerview-v7:$NAV_SUPPORT_VERSION"
+const val NAV_SUPPORT_V4 = "com.android.support:support-v4:$NAV_SUPPORT_VERSION"
+
private const val SUPPORT_VERSION = "1.0.0-alpha1"
const val SUPPORT_ANNOTATIONS = "androidx.annotation:annotation:$SUPPORT_VERSION"
const val SUPPORT_APPCOMPAT = "androidx.appcompat:appcompat:$SUPPORT_VERSION"
diff --git a/car/res/drawable/car_button_ripple_background_night.xml b/car/res/drawable/car_button_ripple_background_night.xml
deleted file mode 100644
index 6160768..0000000
--- a/car/res/drawable/car_button_ripple_background_night.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 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.
- -->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_light" />
diff --git a/car/res/layout/car_paged_scrollbar_buttons.xml b/car/res/layout/car_paged_scrollbar_buttons.xml
index cd62329..f455004 100644
--- a/car/res/layout/car_paged_scrollbar_buttons.xml
+++ b/car/res/layout/car_paged_scrollbar_buttons.xml
@@ -22,7 +22,7 @@
android:gravity="center"
android:orientation="vertical">
- <ImageButton
+ <ImageView
android:id="@+id/page_up"
android:layout_width="@dimen/car_scroll_bar_button_size"
android:layout_height="@dimen/car_scroll_bar_button_size"
@@ -63,7 +63,7 @@
android:background="@drawable/car_scrollbar_thumb" />
</FrameLayout>
- <ImageButton
+ <ImageView
android:id="@+id/page_down"
android:layout_width="@dimen/car_scroll_bar_button_size"
android:layout_height="@dimen/car_scroll_bar_button_size"
diff --git a/car/src/main/java/androidx/car/widget/PagedScrollBarView.java b/car/src/main/java/androidx/car/widget/PagedScrollBarView.java
index aa62813..ea681d3 100644
--- a/car/src/main/java/androidx/car/widget/PagedScrollBarView.java
+++ b/car/src/main/java/androidx/car/widget/PagedScrollBarView.java
@@ -258,22 +258,22 @@
case DayNightStyle.AUTO:
tintResId = R.color.car_tint;
thumbColorResId = R.color.car_scrollbar_thumb;
- upDownBackgroundResId = R.drawable.car_button_ripple_background;
+ upDownBackgroundResId = R.drawable.car_card_ripple_background;
break;
case DayNightStyle.AUTO_INVERSE:
tintResId = R.color.car_tint_inverse;
thumbColorResId = R.color.car_scrollbar_thumb_inverse;
- upDownBackgroundResId = R.drawable.car_button_ripple_background_inverse;
+ upDownBackgroundResId = R.drawable.car_card_ripple_background_inverse;
break;
case DayNightStyle.FORCE_NIGHT:
tintResId = R.color.car_tint_light;
thumbColorResId = R.color.car_scrollbar_thumb_light;
- upDownBackgroundResId = R.drawable.car_button_ripple_background_night;
+ upDownBackgroundResId = R.drawable.car_card_ripple_background_night;
break;
case DayNightStyle.FORCE_DAY:
tintResId = R.color.car_tint_dark;
thumbColorResId = R.color.car_scrollbar_thumb_dark;
- upDownBackgroundResId = R.drawable.car_button_ripple_background_day;
+ upDownBackgroundResId = R.drawable.car_card_ripple_background_day;
break;
default:
throw new IllegalArgumentException("Unknown DayNightStyle: " + mDayNightStyle);
diff --git a/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/src/main/java/androidx/fragment/app/Fragment.java
index ea99ddc..6c61009 100644
--- a/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -101,9 +101,6 @@
// When instantiated from saved state, this is the saved state.
Bundle mSavedFragmentState;
SparseArray<Parcelable> mSavedViewState;
- // If the userVisibleHint is changed before the state is set,
- // it is stored here
- @Nullable Boolean mSavedUserVisibleHint;
// Index into active fragment array.
int mIndex = -1;
diff --git a/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 9d637c4..bcfa0d9 100644
--- a/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -1366,13 +1366,8 @@
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);
- }
+ f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(
+ FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);
if (!f.mUserVisibleHint) {
f.mDeferStart = true;
if (newState > Fragment.STOPPED) {
diff --git a/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java b/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
index 99e6fc7..627d679 100644
--- a/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
+++ b/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
@@ -1391,150 +1391,6 @@
assertEquals(leftEdge, mGridView.getLayoutManager().findViewByPosition(0).getLeft());
}
- void testSetSelectedPosition(final boolean inSmoothScroll, final boolean layoutRequested,
- final boolean viewVisible, final boolean smooth,
- final boolean resultLayoutRequested, final boolean resultSmoothScroller,
- final int resultScrollState) throws Throwable {
- Intent intent = new Intent();
- intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_linear);
- intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 1500);
- intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
- mNumRows = 1;
- initActivity(intent);
- mOrientation = BaseGridView.VERTICAL;
-
- if (inSmoothScroll) {
- setSelectedPositionSmooth(500);
- }
- mActivityTestRule.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- if (layoutRequested) {
- mGridView.requestLayout();
- }
- final int position;
- if (viewVisible) {
- position = mGridView.getChildAdapterPosition(mGridView.getChildAt(
- mGridView.getChildCount() - 1));
- } else {
- position = 1000;
- }
- if (smooth) {
- mGridView.setSelectedPositionSmooth(position);
- } else {
- mGridView.setSelectedPosition(position);
- }
- assertEquals("isLayoutRequested", resultLayoutRequested,
- mGridView.isLayoutRequested());
- assertEquals("isSmoothScrolling", resultSmoothScroller,
- mGridView.getLayoutManager().isSmoothScrolling());
- if (!resultSmoothScroller) {
- // getScrollState() only matters when is not running smoothScroller
- assertEquals("getScrollState", resultScrollState,
- mGridView.getScrollState());
- }
- assertEquals("isLayoutRequested", resultLayoutRequested,
- mGridView.isLayoutRequested());
- }
- });
- }
-
- @Test
- public void testSelectedPosition01() throws Throwable {
- testSetSelectedPosition(false, false, false, false,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition02() throws Throwable {
- testSetSelectedPosition(false, false, false, true,
- false, true, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition03() throws Throwable {
- testSetSelectedPosition(false, false, true, false,
- false, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition04() throws Throwable {
- testSetSelectedPosition(false, false, true, true,
- false, false, RecyclerView.SCROLL_STATE_SETTLING);
- }
-
- @Test
- public void testSelectedPosition05() throws Throwable {
- testSetSelectedPosition(false, true, false, false,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition06() throws Throwable {
- testSetSelectedPosition(false, true, false, true,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition07() throws Throwable {
- testSetSelectedPosition(false, true, true, false,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition08() throws Throwable {
- testSetSelectedPosition(false, true, true, true,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition09() throws Throwable {
- testSetSelectedPosition(true, false, false, false,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition10() throws Throwable {
- testSetSelectedPosition(true, false, false, true,
- false, true, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition11() throws Throwable {
- testSetSelectedPosition(true, false, true, false,
- false, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition12() throws Throwable {
- testSetSelectedPosition(true, false, true, true,
- false, true, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition13() throws Throwable {
- testSetSelectedPosition(true, true, false, false,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition14() throws Throwable {
- testSetSelectedPosition(true, true, false, true,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition15() throws Throwable {
- testSetSelectedPosition(true, true, true, false,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
- @Test
- public void testSelectedPosition16() throws Throwable {
- testSetSelectedPosition(true, true, true, true,
- true, false, RecyclerView.SCROLL_STATE_IDLE);
- }
-
@Test
public void testScrollAndStuck() throws Throwable {
// see b/67370222 fastRelayout() may be stuck.
@@ -4734,6 +4590,7 @@
});
assertEquals(RecyclerView.SCROLL_STATE_IDLE, mGridView.getScrollState());
}
+
private boolean hasAction(AccessibilityNodeInfoCompat info, Object action) {
if (Build.VERSION.SDK_INT >= 21) {
AccessibilityNodeInfoCompat.AccessibilityActionCompat convertedAction =
diff --git a/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java b/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
index 62b7bb2..0669917 100644
--- a/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
+++ b/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
@@ -197,23 +197,17 @@
* Base class which scrolls to selected view in onStop().
*/
abstract class GridLinearSmoothScroller extends LinearSmoothScroller {
- boolean mSkipOnStopInternal;
-
GridLinearSmoothScroller() {
super(mBaseGridView.getContext());
}
@Override
protected void onStop() {
- super.onStop();
- if (!mSkipOnStopInternal) {
+ mFlag |= PF_IN_ONSTOP_SMOOTHSCROLLER;
+ try {
onStopInternal();
- }
- if (mCurrentSmoothScroller == this) {
- mCurrentSmoothScroller = null;
- }
- if (mPendingMoveSmoothScroller == this) {
- mPendingMoveSmoothScroller = null;
+ } finally {
+ mFlag &= ~PF_IN_ONSTOP_SMOOTHSCROLLER;
}
}
@@ -226,6 +220,7 @@
// to the target position.
scrollToSelection(getTargetPosition(), 0, false, 0);
}
+ super.onStop();
return;
}
if (mFocusPosition != getTargetPosition()) {
@@ -239,6 +234,7 @@
}
dispatchChildSelected();
dispatchChildSelectedAndPositioned();
+ super.onStop();
}
@Override
@@ -385,6 +381,7 @@
super.onStopInternal();
// if we hit wall, need clear the remaining pending moves.
mPendingMoves = 0;
+ mPendingMoveSmoothScroller = null;
View v = findViewByPosition(getTargetPosition());
if (v != null) scrollToView(v, true);
}
@@ -547,6 +544,12 @@
static final int PF_REVERSE_FLOW_MASK = PF_REVERSE_FLOW_PRIMARY | PF_REVERSE_FLOW_SECONDARY;
+ /**
+ * flag to prevent calling stop() in onStop() which will lead to stack overflow crash
+ * TODO: fix RecyclerView.SmoothScroller#stop() instead
+ */
+ static final int PF_IN_ONSTOP_SMOOTHSCROLLER = 1 << 20;
+
int mFlag = PF_LAYOUT_ENABLED
| PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END
| PF_PRUNE_CHILD | PF_SCROLL_ENABLED;
@@ -571,13 +574,7 @@
int mSubFocusPosition = 0;
/**
- * Current running SmoothScroller.
- */
- GridLinearSmoothScroller mCurrentSmoothScroller;
-
- /**
- * LinearSmoothScroller that consume pending DPAD movements. Can be same object as
- * mCurrentSmoothScroller when mCurrentSmoothScroller is PendingMoveSmoothScroller.
+ * LinearSmoothScroller that consume pending DPAD movements.
*/
PendingMoveSmoothScroller mPendingMoveSmoothScroller;
@@ -1255,7 +1252,7 @@
int start = 0;
// Iterate from left to right, which is a different index traversal
// in RTL flow
- if ((mFlag & PF_REVERSE_FLOW_SECONDARY) != 0) {
+ if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) {
for (int i = mNumRows-1; i > rowIndex; i--) {
start += getRowSizeSecondary(i) + mSpacingSecondary;
}
@@ -2668,7 +2665,8 @@
// is still valid and no layout is requested, otherwise defer to next layout pass.
// If it is still in smoothScrolling, we should either update smoothScroller or initiate
// a layout.
- final boolean notSmoothScrolling = !isSmoothScrolling();
+ final boolean notSmoothScrolling = !isSmoothScrolling()
+ || (mFlag & PF_IN_ONSTOP_SMOOTHSCROLLER) != 0;
if (notSmoothScrolling && !mBaseGridView.isLayoutRequested()
&& view != null && getAdapterPositionByView(view) == position) {
mFlag |= PF_IN_SELECTION;
@@ -2681,7 +2679,7 @@
mFocusPositionOffset = Integer.MIN_VALUE;
return;
}
- if (smooth && !mBaseGridView.isLayoutRequested()) {
+ if (smooth) {
mFocusPosition = position;
mSubFocusPosition = subposition;
mFocusPositionOffset = Integer.MIN_VALUE;
@@ -2700,21 +2698,13 @@
// stopScroll might change mFocusPosition, so call it before assign value to
// mFocusPosition
if (!notSmoothScrolling) {
- skipSmoothScrollerOnStopInternal();
mBaseGridView.stopScroll();
}
- if (!mBaseGridView.isLayoutRequested()
- && view != null && getAdapterPositionByView(view) == position) {
- mFlag |= PF_IN_SELECTION;
- scrollToView(view, smooth);
- mFlag &= ~PF_IN_SELECTION;
- } else {
- mFocusPosition = position;
- mSubFocusPosition = subposition;
- mFocusPositionOffset = Integer.MIN_VALUE;
- mFlag |= PF_FORCE_FULL_LAYOUT;
- requestLayout();
- }
+ mFocusPosition = position;
+ mSubFocusPosition = subposition;
+ mFocusPositionOffset = Integer.MIN_VALUE;
+ mFlag |= PF_FORCE_FULL_LAYOUT;
+ requestLayout();
}
}
if (TRACE) TraceCompat.endSection();
@@ -2747,33 +2737,6 @@
return linearSmoothScroller.getTargetPosition();
}
- /**
- * when start a new SmoothScroller or scroll to a different location, dont need
- * current SmoothScroller.onStopInternal() doing the scroll work.
- */
- void skipSmoothScrollerOnStopInternal() {
- if (mCurrentSmoothScroller != null) {
- mCurrentSmoothScroller.mSkipOnStopInternal = true;
- }
- }
-
- @Override
- public void startSmoothScroll(RecyclerView.SmoothScroller smoothScroller) {
- skipSmoothScrollerOnStopInternal();
- super.startSmoothScroll(smoothScroller);
- if (smoothScroller.isRunning() && smoothScroller instanceof GridLinearSmoothScroller) {
- mCurrentSmoothScroller = (GridLinearSmoothScroller) smoothScroller;
- if (mCurrentSmoothScroller instanceof PendingMoveSmoothScroller) {
- mPendingMoveSmoothScroller = (PendingMoveSmoothScroller) mCurrentSmoothScroller;
- } else {
- mPendingMoveSmoothScroller = null;
- }
- } else {
- mCurrentSmoothScroller = null;
- mPendingMoveSmoothScroller = null;
- }
- }
-
private void processPendingMovement(boolean forward) {
if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) {
return;
@@ -2785,6 +2748,9 @@
forward ? 1 : -1, mNumRows > 1);
mFocusPositionOffset = 0;
startSmoothScroll(linearSmoothScroller);
+ if (linearSmoothScroller.isRunning()) {
+ mPendingMoveSmoothScroller = linearSmoothScroller;
+ }
} else {
if (forward) {
mPendingMoveSmoothScroller.increasePendingMoves();
@@ -3723,10 +3689,6 @@
@Override
public boolean performAccessibilityAction(Recycler recycler, State state, int action,
Bundle args) {
- if (!isScrollEnabled()) {
- // eat action request so that talkback wont focus out of RV
- return true;
- }
saveContext(recycler, state);
int translatedAction = action;
boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0;
diff --git a/lifecycle/integration-tests/testapp/build.gradle b/lifecycle/integration-tests/testapp/build.gradle
index a842c45..6fd43c4 100644
--- a/lifecycle/integration-tests/testapp/build.gradle
+++ b/lifecycle/integration-tests/testapp/build.gradle
@@ -52,5 +52,3 @@
}
tasks['check'].dependsOn(tasks['connectedCheck'])
-
-uploadArchives.enabled = false
diff --git a/navigation/.gitignore b/navigation/.gitignore
new file mode 100644
index 0000000..be4e6f1
--- /dev/null
+++ b/navigation/.gitignore
@@ -0,0 +1,4 @@
+local.properties
+maven-repo/
+build/
+*.DS_Store
diff --git a/navigation/OWNERS b/navigation/OWNERS
new file mode 100644
index 0000000..c890670
--- /dev/null
+++ b/navigation/OWNERS
@@ -0,0 +1,2 @@
+ilake@google.com
+sergeyv@google.com
diff --git a/navigation/common/build.gradle b/navigation/common/build.gradle
new file mode 100644
index 0000000..eca1086
--- /dev/null
+++ b/navigation/common/build.gradle
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ api(NAV_SUPPORT_COMPAT)
+
+ testImplementation(JUNIT)
+ testImplementation(MOCKITO_CORE)
+ testImplementation(TEST_RUNNER)
+
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+//used by testCompile safe-args-generator
+android.libraryVariants.all { variant ->
+ def name = variant.buildType.name
+ def suffix = name.capitalize()
+ project.tasks.create(name: "jar${suffix}", type: Jar){
+ dependsOn variant.javaCompile
+ from variant.javaCompile.destinationDir
+ destinationDir new File(project.buildDir, "libJar")
+ }
+}
+
+supportLibrary {
+ name = "Android Navigation Common"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2017"
+ description = "Android Navigation-Common"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/common/ktx/build.gradle b/navigation/common/ktx/build.gradle
new file mode 100644
index 0000000..625c2dc
--- /dev/null
+++ b/navigation/common/ktx/build.gradle
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(project(":navigation:navigation-common"))
+ api(KOTLIN_STDLIB)
+ testImplementation(project(":navigation:navigation-testing"))
+ testImplementation(JUNIT)
+ testImplementation(TEST_RUNNER)
+ androidTestImplementation(project(":navigation:navigation-testing"))
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+ name = "Android Navigation Common Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2018"
+ description = "Android Navigation-Common-Ktx"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/common/ktx/src/androidTest/AndroidManifest.xml
similarity index 79%
rename from car/res/drawable/car_button_ripple_background_inverse.xml
rename to navigation/common/ktx/src/androidTest/AndroidManifest.xml
index 660dbcd..5b5eccf 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/common/ktx/src/androidTest/AndroidManifest.xml
@@ -14,6 +14,9 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.ktx.test">
+ <application>
+ </application>
+</manifest>
diff --git a/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
new file mode 100644
index 0000000..dd52934
--- /dev/null
+++ b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavDestinationBuilderTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.navigation
+
+import android.os.Bundle
+import android.support.annotation.IdRes
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import androidx.navigation.testing.TestNavigator
+import androidx.navigation.testing.TestNavigatorProvider
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NavDestinationTest {
+ private val provider = TestNavigatorProvider(InstrumentationRegistry.getTargetContext())
+
+ @Test
+ fun navDestination() {
+ val destination = provider.navDestination(DESTINATION_ID) { }
+ assertEquals("NavDestination should have id set",
+ DESTINATION_ID, destination.id)
+ }
+
+ @Test
+ fun navDestinationLabel() {
+ val destination = provider.navDestination(DESTINATION_ID) {
+ label = LABEL
+ }
+ assertEquals("NavDestination should have label set",
+ LABEL, destination.label)
+ }
+
+ @Test
+ fun navDestinationDefaultArguments() {
+ val arguments = Bundle()
+ val destination = provider.navDestination(DESTINATION_ID) {
+ defaultArguments = arguments
+ }
+ assertEquals("NavDestination should have default arguments set",
+ arguments, destination.defaultArguments)
+ }
+
+ @Test
+ fun navDestinationAction() {
+ val destination = provider.navDestination(DESTINATION_ID) {
+ action(ACTION_ID) {
+ destinationId = DESTINATION_ID
+ navOptions {
+ popUpTo = DESTINATION_ID
+ }
+ }
+ }
+ val action = destination.getAction(ACTION_ID)
+ assertNotNull("NavDestination should have action that was added", action)
+ assertEquals("NavAction should have NavOptions set",
+ DESTINATION_ID, action?.navOptions?.popUpTo)
+ }
+}
+
+private const val DESTINATION_ID = 1
+private const val LABEL = "TEST"
+private const val ACTION_ID = 1
+
+/**
+ * Instead of constructing a NavGraph from the NavigatorProvider, construct
+ * a NavDestination directly to allow for testing NavDestinationBuilder in
+ * isolation.
+ */
+fun NavigatorProvider.navDestination(
+ @IdRes id: Int,
+ block: NavDestinationBuilder<NavDestination>.() -> Unit
+): NavDestination = NavDestinationBuilder(this[TestNavigator::class], id).apply(block).build()
diff --git a/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
new file mode 100644
index 0000000..9ecffe9
--- /dev/null
+++ b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.navigation
+
+import android.support.annotation.IdRes
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import androidx.navigation.testing.TestNavigator
+import androidx.navigation.testing.TestNavigatorProvider
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NavGraphBuilderTest {
+ private val provider = TestNavigatorProvider(InstrumentationRegistry.getTargetContext())
+
+ @Test
+ fun navigation() {
+ val graph = provider.navigation(startDestination = DESTINATION_ID) {
+ navDestination(DESTINATION_ID) {}
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ }
+
+ @Test
+ fun navigationUnaryPlus() {
+ val graph = provider.navigation(startDestination = DESTINATION_ID) {
+ +NavDestination(provider[TestNavigator::class]).apply {
+ id = DESTINATION_ID
+ }
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ }
+
+ @Test
+ fun navigationAddDestination() {
+ val graph = provider.navigation(startDestination = DESTINATION_ID) {
+ val destination = NavDestination(provider[TestNavigator::class]).apply {
+ id = DESTINATION_ID
+ }
+ addDestination(destination)
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun navigationMissingStartDestination() {
+ provider.navigation(startDestination = 0) {
+ navDestination(DESTINATION_ID) {}
+ }
+ fail("NavGraph should throw IllegalStateException if startDestination is zero")
+ }
+
+ @Test
+ fun navigationNested() {
+ val graph = provider.navigation(startDestination = DESTINATION_ID) {
+ navigation(DESTINATION_ID, startDestination = SECOND_DESTINATION_ID) {
+ navDestination(SECOND_DESTINATION_ID) {}
+ }
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ }
+}
+
+private const val DESTINATION_ID = 1
+private const val SECOND_DESTINATION_ID = 2
+
+/**
+ * Create a base NavDestination. Generally, only subtypes of NavDestination should be
+ * added to a NavGraph (hence why this is not in the common-ktx library)
+ */
+fun NavGraphBuilder.navDestination(
+ @IdRes id: Int,
+ block: NavDestinationBuilder<NavDestination>.() -> Unit
+) = destination(NavDestinationBuilder(provider[TestNavigator::class], id).apply(block))
diff --git a/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphTest.kt b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphTest.kt
new file mode 100644
index 0000000..ebc11a7
--- /dev/null
+++ b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavGraphTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.navigation
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import androidx.navigation.testing.TestNavigator
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertSame
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NavGraphTest {
+ private val navGraphNavigator = NavGraphNavigator(InstrumentationRegistry.getTargetContext())
+ private val navigator = TestNavigator()
+
+ @Test
+ fun plusAssign() {
+ val graph = NavGraph(navGraphNavigator)
+ val destination = NavDestination(navigator).apply { id = DESTINATION_ID }
+ graph += destination
+ assertSame("plusAssign destination should be retrieved with get", destination,
+ graph[DESTINATION_ID])
+ }
+
+ @Test
+ fun minusAssign() {
+ val graph = NavGraph(navGraphNavigator)
+ val destination = NavDestination(navigator).apply { id = DESTINATION_ID }
+ graph += destination
+ assertSame("plusAssign destination should be retrieved with get", destination,
+ graph[DESTINATION_ID])
+ graph -= destination
+ assertFalse("Destination should be removed after minusAssign",
+ DESTINATION_ID in graph)
+ }
+
+ @Test
+ fun plusAssignGraph() {
+ val graph = NavGraph(navGraphNavigator)
+ val other = NavGraph(navGraphNavigator)
+ other += NavDestination(navigator).apply { id = DESTINATION_ID }
+ other += NavDestination(navigator).apply { id = SECOND_DESTINATION_ID }
+ graph += other
+ assertTrue("NavGraph should have destination1 from other",
+ DESTINATION_ID in graph)
+ assertFalse("other nav graph should not have destination1",
+ DESTINATION_ID in other)
+
+ assertTrue("NavGraph should have destination2 from other",
+ SECOND_DESTINATION_ID in graph)
+ assertFalse("other nav graph should not have destination2",
+ SECOND_DESTINATION_ID in other)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun getIllegalArgumentException() {
+ val graph = NavGraph(navGraphNavigator)
+ graph[DESTINATION_ID]
+ }
+}
+
+private const val DESTINATION_ID = 1
+private const val SECOND_DESTINATION_ID = 2
diff --git a/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavOptionsBuilderTest.kt b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavOptionsBuilderTest.kt
new file mode 100644
index 0000000..c33afe8
--- /dev/null
+++ b/navigation/common/ktx/src/androidTest/java/androidx/navigation/NavOptionsBuilderTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.navigation
+
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NavOptionsTest {
+
+ @Test
+ fun launchSingleTop() {
+ val navOptions = navOptions {
+ launchSingleTop = true
+ }
+ assertTrue("NavOptions should have launchSingleTop set",
+ navOptions.shouldLaunchSingleTop())
+ }
+
+ @Test
+ fun launchDocument() {
+ val navOptions = navOptions {
+ launchDocument = true
+ }
+ assertTrue("NavOptions should have launchDocument set",
+ navOptions.shouldLaunchDocument())
+ }
+
+ @Test
+ fun clearTask() {
+ val navOptions = navOptions {
+ clearTask = true
+ }
+ assertTrue("NavOptions should have clearTask set",
+ navOptions.shouldClearTask())
+ }
+
+ @Test
+ fun popUpTo() {
+ val navOptions = navOptions {
+ popUpTo = DESTINATION_ID
+ }
+ assertEquals("NavOptions should have popUpTo destination id set",
+ DESTINATION_ID, navOptions.popUpTo)
+ assertFalse("NavOptions should have isPopUpToInclusive false by default",
+ navOptions.isPopUpToInclusive)
+ }
+
+ @Test
+ fun popUpToInclusive() {
+ val navOptions = navOptions {
+ popUpTo(DESTINATION_ID) {
+ inclusive = true
+ }
+ }
+ assertEquals("NavOptions should have popUpTo destination id set",
+ DESTINATION_ID, navOptions.popUpTo)
+ assertTrue("NavOptions should have isPopUpToInclusive set",
+ navOptions.isPopUpToInclusive)
+ }
+
+ @Test
+ fun anim() {
+ val navOptions = navOptions {
+ anim {
+ enter = ENTER_ANIM_ID
+ exit = EXIT_ANIM_ID
+ popEnter = POP_ENTER_ANIM_ID
+ popExit = POP_EXIT_ANIM_ID
+ }
+ }
+ assertEquals("NavOptions should have enter animation set",
+ ENTER_ANIM_ID, navOptions.enterAnim)
+ assertEquals("NavOptions should have exit animation set",
+ EXIT_ANIM_ID, navOptions.exitAnim)
+ assertEquals("NavOptions should have pop enter animation set",
+ POP_ENTER_ANIM_ID, navOptions.popEnterAnim)
+ assertEquals("NavOptions should have pop exit animation set",
+ POP_EXIT_ANIM_ID, navOptions.popExitAnim)
+ }
+}
+
+private const val DESTINATION_ID = 1
+
+private const val ENTER_ANIM_ID = 10
+private const val EXIT_ANIM_ID = 11
+private const val POP_ENTER_ANIM_ID = 12
+private const val POP_EXIT_ANIM_ID = 13
diff --git a/car/res/drawable/car_button_ripple_background.xml b/navigation/common/ktx/src/main/AndroidManifest.xml
similarity index 83%
copy from car/res/drawable/car_button_ripple_background.xml
copy to navigation/common/ktx/src/main/AndroidManifest.xml
index 13d0a49..799fc93 100644
--- a/car/res/drawable/car_button_ripple_background.xml
+++ b/navigation/common/ktx/src/main/AndroidManifest.xml
@@ -14,6 +14,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background" />
+<manifest package="androidx.navigation.ktx"/>
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavDestinationBuilder.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavDestinationBuilder.kt
new file mode 100644
index 0000000..55cc97f
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavDestinationBuilder.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.navigation
+
+import android.os.Bundle
+import android.support.annotation.IdRes
+
+@DslMarker
+annotation class NavDestinationDsl
+
+/**
+ * DSL for constructing a new [NavDestination]
+ */
+@NavDestinationDsl
+open class NavDestinationBuilder<out D : NavDestination>(
+ protected val navigator: Navigator<out D>,
+ @IdRes val id: Int
+) {
+ /**
+ * The descriptive label of the destination
+ */
+ var label: CharSequence? = null
+
+ /**
+ * The default arguments that should be passed to the destination
+ */
+ var defaultArguments: Bundle? = null
+
+ private var deepLinks = mutableListOf<String>()
+
+ /**
+ * Add a deep link to this destination.
+ *
+ * In addition to a direct Uri match, the following features are supported:
+ *
+ * * Uris without a scheme are assumed as http and https. For example,
+ * `www.example.com` will match `http://www.example.com` and
+ * `https://www.example.com`.
+ * * Placeholders in the form of `{placeholder_name}` matches 1 or more
+ * characters. The String value of the placeholder will be available in the arguments
+ * [Bundle] with a key of the same name. For example,
+ * `http://www.example.com/users/{id}` will match
+ * `http://www.example.com/users/4`.
+ * * The `.*` wildcard can be used to match 0 or more characters.
+ *
+ * @param uriPattern The uri pattern to add as a deep link
+ */
+ fun deepLink(uriPattern: String) {
+ deepLinks.add(uriPattern)
+ }
+
+ private var actions = mutableMapOf<Int, NavAction>()
+
+ /**
+ * Adds a new [NavAction] to the destination
+ */
+ fun action(actionId: Int, block: NavActionBuilder.() -> Unit) {
+ actions[actionId] = NavActionBuilder().apply(block).build()
+ }
+
+ /**
+ * Build the NavDestination by calling [Navigator.createDestination].
+ */
+ open fun build(): D {
+ return navigator.createDestination().also { destination ->
+ destination.id = id
+ destination.label = label
+ destination.setDefaultArguments(defaultArguments)
+ deepLinks.forEach { deepLink ->
+ destination.addDeepLink(deepLink)
+ }
+ actions.forEach { actionId, action ->
+ destination.putAction(actionId, action)
+ }
+ }
+ }
+}
+
+/**
+ * DSL for building a [NavAction].
+ */
+@NavDestinationDsl
+class NavActionBuilder {
+ /**
+ * The ID of the destination that should be navigated to when this action is used
+ */
+ var destinationId: Int = 0
+
+ private var navOptions: NavOptions? = null
+
+ /**
+ * Sets the [NavOptions] for this action that should be used by default
+ */
+ fun navOptions(block: NavOptionsBuilder.() -> Unit) {
+ navOptions = NavOptionsBuilder().apply(block).build()
+ }
+
+ internal fun build() = NavAction(destinationId, navOptions)
+}
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavGraph.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavGraph.kt
new file mode 100644
index 0000000..eaf2775
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavGraph.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation
+
+import android.support.annotation.IdRes
+
+/**
+ * Returns the destination with `id`.
+ *
+ * @throws IllegalArgumentException if no destination is found with that id.
+ */
+inline operator fun NavGraph.get(@IdRes id: Int): NavDestination =
+ findNode(id) ?: throw IllegalArgumentException("No destination for $id was found in $this")
+
+/** Returns `true` if a destination with `id` is found in this navigation graph. */
+operator fun NavGraph.contains(@IdRes id: Int): Boolean = findNode(id) != null
+
+/**
+ * Adds a destination to this NavGraph. The destination must have an
+ * [id][NavDestination.getId] set.
+ *
+ * The destination must not have a [parent][NavDestination.getParent] set. If
+ * the destination is already part of a [NavGraph], call
+ * [NavGraph.remove] before calling this method.</p>
+ *
+ * @param node destination to add
+ */
+inline operator fun NavGraph.plusAssign(node: NavDestination) {
+ addDestination(node)
+}
+
+/**
+ * Add all destinations from another collection to this one. As each destination has at most
+ * one parent, the destinations will be removed from the given NavGraph.
+ *
+ * @param other collection of destinations to add. All destinations will be removed from the
+ * parameter graph after being added to this graph.
+ */
+inline operator fun NavGraph.plusAssign(other: NavGraph) {
+ addAll(other)
+}
+
+/** Removes `node` from this navigation graph. */
+inline operator fun NavGraph.minusAssign(node: NavDestination) {
+ remove(node)
+}
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavGraphBuilder.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavGraphBuilder.kt
new file mode 100644
index 0000000..97579ec
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavGraphBuilder.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.navigation
+
+import android.support.annotation.IdRes
+
+/**
+ * Construct a new [NavGraph]
+ */
+inline fun NavigatorProvider.navigation(
+ @IdRes id: Int = 0,
+ @IdRes startDestination: Int,
+ block: NavGraphBuilder.() -> Unit
+) = NavGraphBuilder(this, id, startDestination).apply(block).build()
+
+/**
+ * Construct a nested [NavGraph]
+ */
+inline fun NavGraphBuilder.navigation(
+ @IdRes id: Int,
+ @IdRes startDestination: Int,
+ block: NavGraphBuilder.() -> Unit
+) = destination(NavGraphBuilder(provider, id, startDestination).apply(block))
+
+/**
+ * DSL for constructing a new [NavGraph]
+ */
+@NavDestinationDsl
+class NavGraphBuilder(
+ val provider: NavigatorProvider,
+ @IdRes id: Int,
+ @IdRes private var startDestination: Int
+) : NavDestinationBuilder<NavGraph>(provider[NavGraphNavigator::class], id) {
+ private val destinations = mutableListOf<NavDestination>()
+
+ /**
+ * Build and add a new destination to the [NavGraphBuilder]
+ */
+ fun <D : NavDestination> destination(navDestination: NavDestinationBuilder<D>) {
+ destinations += navDestination.build()
+ }
+
+ /**
+ * Adds this destination to the [NavGraphBuilder]
+ */
+ operator fun NavDestination.unaryPlus() {
+ addDestination(this)
+ }
+
+ /**
+ * Add the destination to the [NavGraphBuilder]
+ */
+ fun addDestination(destination: NavDestination) {
+ destinations += destination
+ }
+
+ override fun build(): NavGraph = super.build().also { navGraph ->
+ navGraph.addDestinations(destinations)
+ if (startDestination == 0) {
+ throw IllegalStateException("You must set a startDestination")
+ }
+ navGraph.startDestination = startDestination
+ }
+}
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavOptionsBuilder.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavOptionsBuilder.kt
new file mode 100644
index 0000000..5b4781d
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavOptionsBuilder.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.navigation
+
+import android.support.annotation.AnimRes
+import android.support.annotation.AnimatorRes
+import android.support.annotation.IdRes
+
+@DslMarker
+annotation class NavOptionsDsl
+
+/**
+ * Construct a new [NavOptions]
+ */
+fun navOptions(block: NavOptionsBuilder.() -> Unit): NavOptions =
+ NavOptionsBuilder().apply(block).build()
+
+/**
+ * DSL for constructing a new [NavOptions]
+ */
+@NavOptionsDsl
+class NavOptionsBuilder {
+ private val builder = NavOptions.Builder()
+
+ /**
+ * Whether this navigation action should launch as single-top (i.e., there will be at most
+ * one copy of a given destination on the top of the back stack).
+ *
+ * This functions similarly to how [android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP]
+ * works with activites.
+ */
+ var launchSingleTop = false
+
+ /**
+ * Whether this navigation action should launch the destination in a new document.
+ *
+ * This functions similarly to how [android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT]
+ * works with activites.
+ */
+ var launchDocument = false
+
+ /**
+ * Whether this navigation action should clear the entire back stack
+ *
+ * This functions similarly to how [android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK]
+ * works with activites.
+ */
+ var clearTask = false
+
+ /**
+ * Pop up to a given destination before navigating. This pops all non-matching destinations
+ * from the back stack until this destination is found.
+ */
+ @IdRes
+ var popUpTo: Int = 0
+ set(value) {
+ field = value
+ inclusive = false
+ }
+ private var inclusive = false
+
+ /**
+ * Pop up to a given destination before navigating. This pops all non-matching destinations
+ * from the back stack until this destination is found.
+ */
+ fun popUpTo(@IdRes id: Int, block: PopUpToBuilder.() -> Unit) {
+ popUpTo = id
+ inclusive = PopUpToBuilder().apply(block).inclusive
+ }
+
+ /**
+ * Sets any custom Animation or Animator resources that should be used.
+ *
+ * Note: Animator resources are not supported for navigating to a new Activity
+ */
+ fun anim(block: AnimBuilder.() -> Unit) {
+ AnimBuilder().apply(block).run {
+ this@NavOptionsBuilder.builder.setEnterAnim(enter)
+ .setExitAnim(exit)
+ .setPopEnterAnim(popEnter)
+ .setPopExitAnim(popExit)
+ }
+ }
+
+ internal fun build() = builder.apply {
+ setLaunchSingleTop(launchSingleTop)
+ setLaunchDocument(launchDocument)
+ setClearTask(clearTask)
+ setPopUpTo(popUpTo, inclusive)
+ }.build()
+}
+
+/**
+ * DSL for customizing [NavOptionsBuilder.popUpTo] operations.
+ */
+@NavOptionsDsl
+class PopUpToBuilder {
+ /**
+ * Whether the `popUpTo` destination should be popped from the back stack.
+ */
+ var inclusive: Boolean = false
+}
+
+/**
+ * DSL for setting custom Animation or Animator resources on a [NavOptionsBuilder]
+ */
+@NavOptionsDsl
+class AnimBuilder {
+ /**
+ * The custom Animation or Animator resource for the enter animation.
+ *
+ * Note: Animator resources are not supported for navigating to a new Activity
+ */
+ @AnimRes
+ @AnimatorRes
+ var enter = -1
+
+ /**
+ * The custom Animation or Animator resource for the exit animation.
+ *
+ * Note: Animator resources are not supported for navigating to a new Activity
+ */
+ @AnimRes
+ @AnimatorRes
+ var exit = -1
+
+ /**
+ * The custom Animation or Animator resource for the enter animation
+ * when popping off the back stack.
+ *
+ * Note: Animator resources are not supported for navigating to a new Activity
+ */
+ @AnimRes
+ @AnimatorRes
+ var popEnter = -1
+
+ /**
+ * The custom Animation or Animator resource for the exit animation
+ * when popping off the back stack.
+ *
+ * Note: Animator resources are not supported for navigating to a new Activity
+ */
+ @AnimRes
+ @AnimatorRes
+ var popExit = -1
+}
diff --git a/navigation/common/ktx/src/main/java/androidx/navigation/NavigatorProvider.kt b/navigation/common/ktx/src/main/java/androidx/navigation/NavigatorProvider.kt
new file mode 100644
index 0000000..ec43d45
--- /dev/null
+++ b/navigation/common/ktx/src/main/java/androidx/navigation/NavigatorProvider.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation
+
+import kotlin.reflect.KClass
+
+/**
+ * Retrieves a registered [Navigator] by name.
+ *
+ * @throws IllegalStateException if the Navigator has not been added
+ */
+inline operator fun <D : NavDestination, T : Navigator<D>> NavigatorProvider.get(name: String): T =
+ getNavigator(name)
+
+/**
+ * Retrieves a registered [Navigator] using the name provided by the
+ * [Navigator.Name annotation][Navigator.Name].
+ *
+ * @throws IllegalStateException if the Navigator has not been added
+ */
+inline operator fun <D : NavDestination, T : Navigator<D>> NavigatorProvider.get(
+ clazz: KClass<T>
+): T = getNavigator(clazz.java)
+
+/**
+ * Register a [Navigator] by name. If a navigator by this name is already
+ * registered, this new navigator will replace it.
+ *
+ * @return the previously added [Navigator] for the given name, if any
+ */
+inline operator fun <D : NavDestination> NavigatorProvider.set(
+ name: String,
+ navigator: Navigator<D>
+) = addNavigator(name, navigator)
+
+/**
+ * Register a navigator using the name provided by the
+ * [Navigator.Name annotation][Navigator.Name].
+ */
+inline operator fun <D : NavDestination> NavigatorProvider.plusAssign(navigator: Navigator<D>) {
+ addNavigator(navigator)
+}
diff --git a/navigation/common/ktx/src/test/java/androidx/navigation/NavigatorProviderTest.kt b/navigation/common/ktx/src/test/java/androidx/navigation/NavigatorProviderTest.kt
new file mode 100644
index 0000000..95d7324
--- /dev/null
+++ b/navigation/common/ktx/src/test/java/androidx/navigation/NavigatorProviderTest.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.navigation
+
+import android.support.test.filters.SmallTest
+import androidx.navigation.testing.TestNavigator
+import org.junit.Assert.assertSame
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class NavigatorProviderTest {
+ private val provider = SimpleNavigatorProvider()
+
+ @Test
+ fun set() {
+ val navigator = TestNavigator()
+ provider[NAME] = navigator
+ val foundNavigator: Navigator<NavDestination> = provider[NAME]
+ assertSame("Set destination should be retrieved with get", navigator,
+ foundNavigator)
+ }
+
+ @Test
+ fun plusAssign() {
+ val navigator = TestNavigator()
+ provider += navigator
+ assertSame("Set destination should be retrieved with get", navigator,
+ provider[TestNavigator::class])
+ }
+}
+
+private const val NAME = "TEST"
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/common/src/androidTest/AndroidManifest.xml
similarity index 72%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/common/src/androidTest/AndroidManifest.xml
index 660dbcd..679327a 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/common/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,9 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.common.test">
+ <application>
+ </application>
+</manifest>
diff --git a/navigation/common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.java b/navigation/common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.java
new file mode 100644
index 0000000..55c1b00
--- /dev/null
+++ b/navigation/common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public class NavDeepLinkTest {
+ private static final String DEEP_LINK_EXACT_NO_SCHEME = "www.example.com";
+ private static final String DEEP_LINK_EXACT_HTTP = "http://" + DEEP_LINK_EXACT_NO_SCHEME;
+ private static final String DEEP_LINK_EXACT_HTTPS = "https://" + DEEP_LINK_EXACT_NO_SCHEME;
+
+ @Test
+ public void deepLinkExactMatch() {
+ NavDeepLink deepLink = new NavDeepLink(DEEP_LINK_EXACT_HTTP);
+
+ assertThat(deepLink.matches(Uri.parse(DEEP_LINK_EXACT_HTTP)), is(true));
+ assertThat(deepLink.matches(Uri.parse(DEEP_LINK_EXACT_HTTPS)), is(false));
+ }
+
+ @Test
+ public void deepLinkExactMatchWithHyphens() {
+ String deepLinkString = "android-app://com.example";
+ NavDeepLink deepLink = new NavDeepLink(deepLinkString);
+
+ assertThat(deepLink.matches(Uri.parse(deepLinkString)), is(true));
+ }
+
+ @Test
+ public void deepLinkExactMatchNoScheme() {
+ NavDeepLink deepLink = new NavDeepLink(DEEP_LINK_EXACT_NO_SCHEME);
+
+ assertThat(deepLink.matches(Uri.parse(DEEP_LINK_EXACT_HTTP)), is(true));
+ assertThat(deepLink.matches(Uri.parse(DEEP_LINK_EXACT_HTTPS)), is(true));
+ }
+
+ @Test
+ public void deepLinkArgumentMatch() {
+ String deepLinkArgument = DEEP_LINK_EXACT_HTTPS + "/users/{id}/posts";
+ NavDeepLink deepLink = new NavDeepLink(deepLinkArgument);
+
+ String id = "2";
+ Bundle matchArgs = deepLink.getMatchingArguments(
+ Uri.parse(deepLinkArgument.replace("{id}", id)));
+ assertThat(matchArgs, not(nullValue()));
+ assert matchArgs != null;
+ assertThat(matchArgs.getString("id"), is(id));
+ }
+
+ @Test
+ public void deepLinkMultipleArgumentMatch() {
+ String deepLinkArgument = DEEP_LINK_EXACT_HTTPS + "/users/{id}/posts/{postId}";
+ NavDeepLink deepLink = new NavDeepLink(deepLinkArgument);
+
+ String id = "2";
+ String postId = "42";
+ Bundle matchArgs = deepLink.getMatchingArguments(
+ Uri.parse(deepLinkArgument.replace("{id}", id).replace("{postId}", postId)));
+ assertThat(matchArgs, not(nullValue()));
+ assert matchArgs != null;
+ assertThat(matchArgs.getString("id"), is(id));
+ assertThat(matchArgs.getString("postId"), is(postId));
+ }
+
+ @Test
+ public void deepLinkEmptyArgumentNoMatch() {
+ String deepLinkArgument = DEEP_LINK_EXACT_HTTPS + "/users/{id}/posts";
+ NavDeepLink deepLink = new NavDeepLink(deepLinkArgument);
+
+ assertThat(deepLink.matches(
+ Uri.parse(deepLinkArgument.replace("{id}", ""))),
+ is(false));
+ }
+
+ @Test
+ public void deepLinkPrefixMatch() {
+ String deepLinkPrefix = DEEP_LINK_EXACT_HTTPS + "/posts/.*";
+ NavDeepLink deepLink = new NavDeepLink(deepLinkPrefix);
+
+ assertThat(deepLink.matches(
+ Uri.parse(deepLinkPrefix.replace(".*", "test"))),
+ is(true));
+ }
+
+ @Test
+ public void deepLinkWildcardMatch() {
+ String deepLinkWildcard = DEEP_LINK_EXACT_HTTPS + "/posts/.*/new";
+ NavDeepLink deepLink = new NavDeepLink(deepLinkWildcard);
+
+ assertThat(deepLink.matches(
+ Uri.parse(deepLinkWildcard.replace(".*", "test"))),
+ is(true));
+ }
+
+ @Test
+ public void deepLinkMultipleMatch() {
+ String deepLinkMultiple = DEEP_LINK_EXACT_HTTPS + "/users/{id}/posts/.*";
+ NavDeepLink deepLink = new NavDeepLink(deepLinkMultiple);
+
+ String id = "2";
+ Bundle matchArgs = deepLink.getMatchingArguments(
+ Uri.parse(deepLinkMultiple.replace("{id}", id)));
+ assertThat(matchArgs, not(nullValue()));
+ assert matchArgs != null;
+ assertThat(matchArgs.getString("id"), is(id));
+ }
+}
diff --git a/car/res/drawable/car_button_ripple_background_day.xml b/navigation/common/src/main/AndroidManifest.xml
similarity index 76%
copy from car/res/drawable/car_button_ripple_background_day.xml
copy to navigation/common/src/main/AndroidManifest.xml
index 16b1d0c..716a35e 100644
--- a/car/res/drawable/car_button_ripple_background_day.xml
+++ b/navigation/common/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_dark" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.common">
+
+</manifest>
diff --git a/navigation/common/src/main/java/androidx/navigation/NavAction.java b/navigation/common/src/main/java/androidx/navigation/NavAction.java
new file mode 100644
index 0000000..d233aa3
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/NavAction.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.Nullable;
+
+/**
+ * Navigation actions provide a level of indirection between your navigation code and the
+ * underlying destinations. This allows you to define common actions that change their destination
+ * or {@link NavOptions} based on the current {@link NavDestination}.
+ *
+ * <p>The {@link NavOptions} associated with a NavAction are used by default when navigating
+ * to this action via {@link NavController#navigate(int)} or
+ * {@link NavController#navigate(int, Bundle)}.</p>
+ *
+ * <p>Actions should be added via {@link NavDestination#putAction(int, int)} or
+ * {@link NavDestination#putAction(int, NavAction)}.</p>
+ */
+public class NavAction {
+ @IdRes
+ private final int mDestinationId;
+ private NavOptions mNavOptions;
+
+ /**
+ * Creates a new NavAction for the given destination.
+ *
+ * @param destinationId the ID of the destination that should be navigated to when this
+ * action is used.
+ */
+ public NavAction(@IdRes int destinationId) {
+ this(destinationId, null);
+ }
+
+ /**
+ * Creates a new NavAction for the given destination.
+ *
+ * @param destinationId the ID of the destination that should be navigated to when this
+ * action is used.
+ * @param navOptions special options for this action that should be used by default
+ */
+ public NavAction(@IdRes int destinationId, @Nullable NavOptions navOptions) {
+ mDestinationId = destinationId;
+ mNavOptions = navOptions;
+ }
+
+ /**
+ * Gets the ID of the destination that should be navigated to when this action is used
+ */
+ public int getDestinationId() {
+ return mDestinationId;
+ }
+
+ /**
+ * Sets the NavOptions to be used by default when navigating to this action.
+ *
+ * @param navOptions special options for this action that should be used by default
+ */
+ public void setNavOptions(@Nullable NavOptions navOptions) {
+ mNavOptions = navOptions;
+ }
+
+ /**
+ * Gets the NavOptions to be used by default when navigating to this action.
+ */
+ @Nullable
+ public NavOptions getNavOptions() {
+ return mNavOptions;
+ }
+}
diff --git a/navigation/common/src/main/java/androidx/navigation/NavDeepLink.java b/navigation/common/src/main/java/androidx/navigation/NavDeepLink.java
new file mode 100644
index 0000000..be0c67a
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/NavDeepLink.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * NavDeepLink encapsulates the parsing and matching of a navigation deep link.
+ */
+class NavDeepLink {
+ private static final Pattern SCHEME_PATTERN = Pattern.compile("^(\\w+-)*\\w+:");
+
+ private final ArrayList<String> mArguments = new ArrayList<>();
+ private final Pattern mPattern;
+
+ /**
+ * NavDestinations should be created via {@link Navigator#createDestination}.
+ */
+ NavDeepLink(@NonNull String uri) {
+ StringBuffer uriRegex = new StringBuffer("^");
+
+ if (!SCHEME_PATTERN.matcher(uri).find()) {
+ uriRegex.append("http[s]?://");
+ }
+ Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
+ Matcher matcher = fillInPattern.matcher(uri);
+ while (matcher.find()) {
+ String argName = matcher.group(1);
+ mArguments.add(argName);
+ matcher.appendReplacement(uriRegex, "");
+ uriRegex.append("(.+?)");
+ }
+ matcher.appendTail(uriRegex);
+ mPattern = Pattern.compile(uriRegex.toString());
+ }
+
+ boolean matches(@NonNull Uri deepLink) {
+ return mPattern.matcher(deepLink.toString()).matches();
+ }
+
+ @Nullable
+ Bundle getMatchingArguments(@NonNull Uri deepLink) {
+ Matcher matcher = mPattern.matcher(deepLink.toString());
+ if (!matcher.matches()) {
+ return null;
+ }
+ Bundle bundle = new Bundle();
+ int size = mArguments.size();
+ for (int index = 0; index < size; index++) {
+ String argument = mArguments.get(index);
+ bundle.putString(argument, matcher.group(index + 1));
+ }
+ return bundle;
+ }
+}
diff --git a/navigation/common/src/main/java/androidx/navigation/NavDestination.java b/navigation/common/src/main/java/androidx/navigation/NavDestination.java
new file mode 100644
index 0000000..993675b
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/NavDestination.java
@@ -0,0 +1,343 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.Pair;
+import android.support.v4.util.SparseArrayCompat;
+import android.util.AttributeSet;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+
+import androidx.navigation.common.R;
+
+/**
+ * NavDestination represents one node within an overall navigation graph.
+ *
+ * <p>Each destination is associated with a {@link Navigator} which knows how to navigate to this
+ * particular destination.</p>
+ *
+ * <p>Destinations declare a set of {@link #putAction(int, int) actions} that they
+ * support. These actions form a navigation API for the destination; the same actions declared
+ * on different destinations that fill similar roles allow application code to navigate based
+ * on semantic intent.</p>
+ *
+ * <p>Each destination has a set of {@link #getDefaultArguments() default arguments} that will
+ * be applied when {@link NavController#navigate(int, Bundle) navigating} to that destination.
+ * These arguments can be overridden at the time of navigation.</p>
+ */
+public class NavDestination {
+
+ /**
+ * Retrieve a suitable display name for a given id.
+ * @param context Context used to resolve a resource's name
+ * @param id The id to get a display name for
+ * @return The resource's name if it is a valid id or just the id itself if it is not
+ * a valid resource
+ */
+ @NonNull
+ static String getDisplayName(@NonNull Context context, int id) {
+ try {
+ return context.getResources().getResourceName(id);
+ } catch (Resources.NotFoundException e) {
+ return Integer.toString(id);
+ }
+ }
+
+ private final Navigator mNavigator;
+ private NavGraph mParent;
+ private int mId;
+ private CharSequence mLabel;
+ private Bundle mDefaultArgs;
+ private ArrayList<NavDeepLink> mDeepLinks;
+ private SparseArrayCompat<NavAction> mActions;
+
+ /**
+ * NavDestinations should be created via {@link Navigator#createDestination}.
+ */
+ public NavDestination(@NonNull Navigator<? extends NavDestination> navigator) {
+ mNavigator = navigator;
+ }
+
+ /**
+ * Called when inflating a destination from a resource.
+ *
+ * @param context local context performing inflation
+ * @param attrs attrs to parse during inflation
+ */
+ @CallSuper
+ public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
+ final TypedArray a = context.getResources().obtainAttributes(attrs,
+ R.styleable.Navigator);
+ setId(a.getResourceId(R.styleable.Navigator_android_id, 0));
+ setLabel(a.getText(R.styleable.Navigator_android_label));
+ a.recycle();
+ }
+
+ void setParent(NavGraph parent) {
+ mParent = parent;
+ }
+
+ /**
+ * Gets the {@link NavGraph} that contains this destination. This will be set when a
+ * destination is added to a NavGraph via {@link NavGraph#addDestination}.
+ * @return
+ */
+ @Nullable
+ public NavGraph getParent() {
+ return mParent;
+ }
+
+ /**
+ * Returns the destination's unique ID. This should be an ID resource generated by
+ * the Android resource system.
+ *
+ * @return this destination's ID
+ */
+ @IdRes
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Sets the destination's unique ID. This should be an ID resource generated by
+ * the Android resource system.
+ *
+ * @param id this destination's new ID
+ */
+ public void setId(@IdRes int id) {
+ mId = id;
+ }
+
+ /**
+ * Sets the descriptive label of this destination.
+ *
+ * @param label A descriptive label of this destination.
+ */
+ public void setLabel(@Nullable CharSequence label) {
+ mLabel = label;
+ }
+
+ /**
+ * Gets the descriptive label of this destination.
+ */
+ @Nullable
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ /**
+ * Returns the destination's {@link Navigator}.
+ *
+ * @return this destination's navigator
+ */
+ @NonNull
+ public Navigator getNavigator() {
+ return mNavigator;
+ }
+
+ /**
+ * Returns the destination's default arguments bundle.
+ *
+ * @return the default arguments bundle
+ */
+ public @NonNull Bundle getDefaultArguments() {
+ if (mDefaultArgs == null) {
+ mDefaultArgs = new Bundle();
+ }
+ return mDefaultArgs;
+ }
+
+ /**
+ * Sets the destination's default arguments bundle.
+ *
+ * @param args the new bundle to set
+ */
+ public void setDefaultArguments(@Nullable Bundle args) {
+ mDefaultArgs = args;
+ }
+
+ /**
+ * Merges a bundle of arguments into the current default arguments for this destination.
+ * New values with the same keys will replace old values with those keys.
+ *
+ * @param args arguments to add
+ */
+ public void addDefaultArguments(@NonNull Bundle args) {
+ getDefaultArguments().putAll(args);
+ }
+
+ /**
+ * Add a deep link to this destination. Matching Uris sent to
+ * {@link NavController#onHandleDeepLink(Intent)} will trigger navigating to this destination.
+ * <p>
+ * In addition to a direct Uri match, the following features are supported:
+ * <ul>
+ * <li>Uris without a scheme are assumed as http and https. For example,
+ * <code>www.example.com</code> will match <code>http://www.example.com</code> and
+ * <code>https://www.example.com</code>.</li>
+ * <li>Placeholders in the form of <code>{placeholder_name}</code> matches 1 or more
+ * characters. The String value of the placeholder will be available in the arguments
+ * {@link Bundle} with a key of the same name. For example,
+ * <code>http://www.example.com/users/{id}</code> will match
+ * <code>http://www.example.com/users/4</code>.</li>
+ * <li>The <code>.*</code> wildcard can be used to match 0 or more characters.</li>
+ * </ul>
+ * These Uris can be declared in your navigation XML files by adding one or more
+ * <code><deepLink app:uri="uriPattern" /></code> elements as
+ * a child to your destination.
+ * <p>
+ * Deep links added in navigation XML files will automatically replace instances of
+ * <code>${applicationId}</code> with the applicationId of your app.
+ * Programmatically added deep links should use {@link Context#getPackageName()} directly
+ * when constructing the uriPattern.
+ * @param uriPattern The uri pattern to add as a deep link
+ * @see NavController#onHandleDeepLink(Intent)
+ */
+ public void addDeepLink(@NonNull String uriPattern) {
+ if (mDeepLinks == null) {
+ mDeepLinks = new ArrayList<>();
+ }
+ mDeepLinks.add(new NavDeepLink(uriPattern));
+ }
+
+ /**
+ * Determines if this NavDestination has a deep link matching the given Uri.
+ * @param uri The Uri to match against all deep links added in {@link #addDeepLink(String)}
+ * @return The matching {@link NavDestination} and the appropriate {@link Bundle} of arguments
+ * extracted from the Uri, or null if no match was found.
+ */
+ @Nullable
+ Pair<NavDestination, Bundle> matchDeepLink(@NonNull Uri uri) {
+ if (mDeepLinks == null) {
+ return null;
+ }
+ for (NavDeepLink deepLink : mDeepLinks) {
+ Bundle matchingArguments = deepLink.getMatchingArguments(uri);
+ if (matchingArguments != null) {
+ return Pair.create(this, matchingArguments);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Build an array containing the hierarchy from the root down to this destination.
+ *
+ * @return An array containing all of the ids from the root to this destination
+ */
+ @NonNull
+ int[] buildDeepLinkIds() {
+ ArrayDeque<NavDestination> hierarchy = new ArrayDeque<>();
+ hierarchy.add(this);
+ while (hierarchy.peekFirst().getParent() != null) {
+ hierarchy.addFirst(hierarchy.peekFirst().getParent());
+ }
+ int[] deepLinkIds = new int[hierarchy.size()];
+ int index = 0;
+ for (NavDestination destination : hierarchy) {
+ deepLinkIds[index++] = destination.getId();
+ }
+ return deepLinkIds;
+ }
+
+ /**
+ * Returns the destination ID for a given action. This will recursively check the
+ * {@link #getParent() parent} of this destination if the action destination is not found in
+ * this destination.
+ *
+ * @param id action ID to fetch
+ * @return destination ID mapped to the given action id, or 0 if none
+ */
+ @Nullable
+ public NavAction getAction(@IdRes int id) {
+ NavAction destination = mActions == null ? null : mActions.get(id);
+ // Search the parent for the given action if it is not found in this destination
+ return destination != null
+ ? destination
+ : getParent() != null ? getParent().getAction(id) : null;
+ }
+
+ /**
+ * Sets a destination ID for an action ID.
+ *
+ * @param actionId action ID to bind
+ * @param destId destination ID for the given action
+ */
+ public void putAction(@IdRes int actionId, @IdRes int destId) {
+ putAction(actionId, new NavAction(destId));
+ }
+
+ /**
+ * Sets a destination ID for an action ID.
+ *
+ * @param actionId action ID to bind
+ * @param action action to associate with this action ID
+ */
+ public void putAction(@IdRes int actionId, @NonNull NavAction action) {
+ if (actionId == 0) {
+ throw new IllegalArgumentException("Cannot have an action with actionId 0");
+ }
+ if (mActions == null) {
+ mActions = new SparseArrayCompat<>();
+ }
+ mActions.put(actionId, action);
+ }
+
+ /**
+ * Unsets the destination ID for an action ID.
+ *
+ * @param actionId action ID to remove
+ */
+ public void removeAction(@IdRes int actionId) {
+ if (mActions == null) {
+ return;
+ }
+ mActions.delete(actionId);
+ }
+
+ /**
+ * Navigates to this destination.
+ *
+ * <p>Uses the {@link #getNavigator() configured navigator} to navigate to this destination.
+ * Apps should not call this directly, instead use {@link NavController}'s navigation methods
+ * to ensure consistent back stack tracking and behavior.</p>
+ *
+ * @param args arguments to the new destination
+ * @param navOptions options for navigation
+ */
+ public void navigate(@Nullable Bundle args, @Nullable NavOptions navOptions) {
+ Bundle defaultArgs = getDefaultArguments();
+ Bundle finalArgs = new Bundle();
+ finalArgs.putAll(defaultArgs);
+ if (args != null) {
+ finalArgs.putAll(args);
+ }
+ //noinspection unchecked
+ mNavigator.navigate(this, finalArgs, navOptions);
+ }
+}
diff --git a/navigation/common/src/main/java/androidx/navigation/NavDirections.java b/navigation/common/src/main/java/androidx/navigation/NavDirections.java
new file mode 100644
index 0000000..30422ac
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/NavDirections.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 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.navigation;
+
+
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.Nullable;
+
+/**
+ * An interface that describes a navigation operation: action's id and arguments
+ */
+public interface NavDirections {
+
+ /**
+ * Returns a action id to navigate with.
+ *
+ * @return id of an action
+ */
+ @IdRes
+ int getActionId();
+
+ /**
+ * Returns arguments to pass to the destination
+ */
+ @Nullable
+ Bundle getArguments();
+}
diff --git a/navigation/common/src/main/java/androidx/navigation/NavGraph.java b/navigation/common/src/main/java/androidx/navigation/NavGraph.java
new file mode 100644
index 0000000..4fc681d
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/NavGraph.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.Pair;
+import android.support.v4.util.SparseArrayCompat;
+import android.util.AttributeSet;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import androidx.navigation.common.R;
+
+/**
+ * NavGraph is a collection of {@link NavDestination} nodes fetchable by ID.
+ *
+ * <p>A NavGraph serves as a 'virtual' destination: while the NavGraph itself will not appear
+ * on the back stack, navigating to the NavGraph will cause the
+ * {@link #getStartDestination starting destination} to be added to the back stack.</p>
+ */
+public class NavGraph extends NavDestination implements Iterable<NavDestination> {
+ private final SparseArrayCompat<NavDestination> mNodes = new SparseArrayCompat<>();
+ private int mStartDestId;
+
+ /**
+ * Construct a new NavGraph. This NavGraph is not valid until you
+ * {@link #addDestination(NavDestination) add a destination} and
+ * {@link #setStartDestination(int) set the starting destination}.
+ *
+ * @param navigatorProvider The {@link NavController} which this NavGraph
+ * will be associated with.
+ */
+ public NavGraph(@NonNull NavigatorProvider navigatorProvider) {
+ this(navigatorProvider.getNavigator(NavGraphNavigator.class));
+ }
+
+ /**
+ * Construct a new NavGraph. This NavGraph is not valid until you
+ * {@link #addDestination(NavDestination) add a destination} and
+ * {@link #setStartDestination(int) set the starting destination}.
+ *
+ * @param navGraphNavigator The {@link NavGraphNavigator} which this destination
+ * will be associated with. Generally retrieved via a
+ * {@link NavController}'s
+ * {@link NavigatorProvider#getNavigator(Class)} method.
+ */
+ public NavGraph(@NonNull Navigator<? extends NavGraph> navGraphNavigator) {
+ super(navGraphNavigator);
+ }
+
+ @Override
+ public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
+ super.onInflate(context, attrs);
+ TypedArray a = context.getResources().obtainAttributes(attrs,
+ R.styleable.NavGraphNavigator);
+ setStartDestination(
+ a.getResourceId(R.styleable.NavGraphNavigator_startDestination, 0));
+ a.recycle();
+ }
+
+ @Override
+ @Nullable
+ Pair<NavDestination, Bundle> matchDeepLink(@NonNull Uri uri) {
+ // First search through any deep links directly added to this NavGraph
+ Pair<NavDestination, Bundle> result = super.matchDeepLink(uri);
+ if (result != null) {
+ return result;
+ }
+ // Then search through all child destinations for a matching deep link
+ for (NavDestination child : this) {
+ Pair<NavDestination, Bundle> childResult = child.matchDeepLink(uri);
+ if (childResult != null) {
+ return childResult;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a destination to this NavGraph. The destination must have an
+ * {@link NavDestination#getId()} id} set.
+ *
+ * <p>The destination must not have a {@link NavDestination#getParent() parent} set. If
+ * the destination is already part of a {@link NavGraph navigation graph}, call
+ * {@link #remove(NavDestination)} before calling this method.</p>
+ *
+ * @param node destination to add
+ */
+ public void addDestination(@NonNull NavDestination node) {
+ if (node.getId() == 0) {
+ throw new IllegalArgumentException("Destinations must have an id."
+ + " Call setId() or include an android:id in your navigation XML.");
+ }
+ NavDestination existingDestination = mNodes.get(node.getId());
+ if (existingDestination == node) {
+ return;
+ }
+ if (node.getParent() != null) {
+ throw new IllegalStateException("Destination already has a parent set."
+ + " Call NavGraph.remove() to remove the previous parent.");
+ }
+ if (existingDestination != null) {
+ existingDestination.setParent(null);
+ }
+ node.setParent(this);
+ mNodes.put(node.getId(), node);
+ }
+
+ /**
+ * Adds multiple destinations to this NavGraph. Each destination must have an
+ * {@link NavDestination#getId()} id} set.
+ *
+ * <p> Each destination must not have a {@link NavDestination#getParent() parent} set. If
+ * any destination is already part of a {@link NavGraph navigation graph}, call
+ * {@link #remove(NavDestination)} before calling this method.</p>
+ *
+ * @param nodes destinations to add
+ */
+ public void addDestinations(@NonNull Collection<NavDestination> nodes) {
+ for (NavDestination node : nodes) {
+ if (node == null) {
+ continue;
+ }
+ addDestination(node);
+ }
+ }
+
+ /**
+ * Adds multiple destinations to this NavGraph. Each destination must have an
+ * {@link NavDestination#getId()} id} set.
+ *
+ * <p> Each destination must not have a {@link NavDestination#getParent() parent} set. If
+ * any destination is already part of a {@link NavGraph navigation graph}, call
+ * {@link #remove(NavDestination)} before calling this method.</p>
+ *
+ * @param nodes destinations to add
+ */
+ public void addDestinations(@NonNull NavDestination... nodes) {
+ for (NavDestination node : nodes) {
+ if (node == null) {
+ continue;
+ }
+ addDestination(node);
+ }
+ }
+
+ /**
+ * Finds a destination in the collection by ID. This will recursively check the
+ * {@link #getParent() parent} of this navigation graph if node is not found in
+ * this navigation graph.
+ *
+ * @param resid ID to locate
+ * @return the node with ID resid
+ */
+ public NavDestination findNode(@IdRes int resid) {
+ return findNode(resid, true);
+ }
+
+ NavDestination findNode(@IdRes int resid, boolean searchParents) {
+ NavDestination destination = mNodes.get(resid);
+ // Search the parent for the NavDestination if it is not a child of this navigation graph
+ // and searchParents is true
+ return destination != null
+ ? destination
+ : searchParents && getParent() != null ? getParent().findNode(resid) : null;
+ }
+
+ @Override
+ public Iterator<NavDestination> iterator() {
+ return new Iterator<NavDestination>() {
+ private int mIndex = -1;
+ private boolean mWentToNext = false;
+
+ @Override
+ public boolean hasNext() {
+ return mIndex + 1 < mNodes.size();
+ }
+
+ @Override
+ public NavDestination next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ mWentToNext = true;
+ return mNodes.valueAt(++mIndex);
+ }
+
+ @Override
+ public void remove() {
+ if (!mWentToNext) {
+ throw new IllegalStateException(
+ "You must call next() before you can remove an element");
+ }
+ mNodes.valueAt(mIndex).setParent(null);
+ mNodes.removeAt(mIndex);
+ mIndex--;
+ mWentToNext = false;
+ }
+ };
+ }
+
+ /**
+ * Add all destinations from another collection to this one. As each destination has at most
+ * one parent, the destinations will be removed from the given NavGraph.
+ *
+ * @param other collection of destinations to add. All destinations will be removed from this
+ * graph after being added to this graph.
+ */
+ public void addAll(@NonNull NavGraph other) {
+ Iterator<NavDestination> iterator = other.iterator();
+ while (iterator.hasNext()) {
+ NavDestination destination = iterator.next();
+ iterator.remove();
+ addDestination(destination);
+ }
+ }
+
+ /**
+ * Remove a given destination from this NavGraph
+ *
+ * @param node the destination to remove.
+ */
+ public void remove(@NonNull NavDestination node) {
+ int index = mNodes.indexOfKey(node.getId());
+ if (index >= 0) {
+ mNodes.valueAt(index).setParent(null);
+ mNodes.removeAt(index);
+ }
+ }
+
+ /**
+ * Clear all destinations from this navigation graph.
+ */
+ public void clear() {
+ Iterator<NavDestination> iterator = iterator();
+ while (iterator.hasNext()) {
+ iterator.next();
+ iterator.remove();
+ }
+ }
+
+ /**
+ * Returns the starting destination for this NavGraph. When navigating to the NavGraph, this
+ * destination is the one the user will initially see.
+ * @return
+ */
+ @IdRes
+ public int getStartDestination() {
+ return mStartDestId;
+ }
+
+ /**
+ * Sets the starting destination for this NavGraph.
+ *
+ * @param startDestId The id of the destination to be shown when navigating to this NavGraph.
+ */
+ public void setStartDestination(@IdRes int startDestId) {
+ mStartDestId = startDestId;
+ }
+}
diff --git a/navigation/common/src/main/java/androidx/navigation/NavGraphNavigator.java b/navigation/common/src/main/java/androidx/navigation/NavGraphNavigator.java
new file mode 100644
index 0000000..99cf2f2
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/NavGraphNavigator.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/**
+ * A Navigator built specifically for {@link NavGraph} elements. Handles navigating to the
+ * correct destination when the NavGraph is the target of navigation actions.
+ */
+@Navigator.Name("navigation")
+public class NavGraphNavigator extends Navigator<NavGraph> {
+ private Context mContext;
+
+ /**
+ * Construct a Navigator capable of routing incoming navigation requests to the proper
+ * destination within a {@link NavGraph}.
+ * @param context
+ */
+ public NavGraphNavigator(@NonNull Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Creates a new {@link NavGraph} associated with this navigator.
+ * @return
+ */
+ @NonNull
+ @Override
+ public NavGraph createDestination() {
+ return new NavGraph(this);
+ }
+
+ @Override
+ public void navigate(@NonNull NavGraph destination, @Nullable Bundle args,
+ @Nullable NavOptions navOptions) {
+ int startId = destination.getStartDestination();
+ if (startId == 0) {
+ throw new IllegalStateException("no start destination defined via"
+ + " app:startDestination for "
+ + (destination.getId() != 0
+ ? NavDestination.getDisplayName(mContext, destination.getId())
+ : "the root navigation"));
+ }
+ NavDestination startDestination = destination.findNode(startId, false);
+ if (startDestination == null) {
+ final String dest = NavDestination.getDisplayName(mContext, startId);
+ throw new IllegalArgumentException("navigation destination " + dest
+ + " is not a direct child of this NavGraph");
+ }
+ dispatchOnNavigatorNavigated(destination.getId(), BACK_STACK_DESTINATION_ADDED);
+ startDestination.navigate(args, navOptions);
+ }
+
+ @Override
+ public boolean popBackStack() {
+ return false;
+ }
+}
diff --git a/navigation/common/src/main/java/androidx/navigation/NavOptions.java b/navigation/common/src/main/java/androidx/navigation/NavOptions.java
new file mode 100644
index 0000000..9439128
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/NavOptions.java
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.AnimRes;
+import android.support.annotation.AnimatorRes;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/**
+ * NavOptions stores special options for navigate actions
+ */
+public class NavOptions {
+ static final int LAUNCH_SINGLE_TOP = 0x1;
+ static final int LAUNCH_DOCUMENT = 0x2;
+ static final int LAUNCH_CLEAR_TASK = 0x4;
+
+ private static final String KEY_NAV_OPTIONS = "android-support-nav:navOptions";
+ private static final String KEY_LAUNCH_MODE = "launchMode";
+ private static final String KEY_POP_UP_TO = "popUpTo";
+ private static final String KEY_POP_UP_TO_INCLUSIVE = "popUpToInclusive";
+ private static final String KEY_ENTER_ANIM = "enterAnim";
+ private static final String KEY_EXIT_ANIM = "exitAnim";
+ private static final String KEY_POP_ENTER_ANIM = "popEnterAnim";
+ private static final String KEY_POP_EXIT_ANIM = "popExitAnim";
+
+ /**
+ * Add the {@link #getPopEnterAnim() pop enter} and {@link #getPopExitAnim() pop exit}
+ * animation to an Intent for later usage with
+ * {@link #applyPopAnimationsToPendingTransition(Activity)}.
+ * <p>
+ * This is automatically called for you by {@link ActivityNavigator}.
+ * </p>
+ *
+ * @param intent Intent being started with the given NavOptions
+ * @param navOptions NavOptions containing the pop animations.
+ * @see #applyPopAnimationsToPendingTransition(Activity)
+ * @see #getPopEnterAnim()
+ * @see #getPopExitAnim()
+ */
+ public static void addPopAnimationsToIntent(@NonNull Intent intent,
+ @Nullable NavOptions navOptions) {
+ if (navOptions != null) {
+ intent.putExtra(KEY_NAV_OPTIONS, navOptions.toBundle());
+ }
+ }
+
+ /**
+ * Apply any pop animations in the Intent of the given Activity to a pending transition.
+ * This should be used in place of {@link Activity#overridePendingTransition(int, int)}
+ * to get the appropriate pop animations.
+ * @param activity An activity started from the {@link ActivityNavigator}.
+ * @see #addPopAnimationsToIntent(Intent, NavOptions)
+ * @see #getPopEnterAnim()
+ * @see #getPopExitAnim()
+ */
+ public static void applyPopAnimationsToPendingTransition(@NonNull Activity activity) {
+ Intent intent = activity.getIntent();
+ if (intent == null) {
+ return;
+ }
+ Bundle bundle = intent.getBundleExtra(KEY_NAV_OPTIONS);
+ if (bundle != null) {
+ NavOptions navOptions = NavOptions.fromBundle(bundle);
+ int popEnterAnim = navOptions.getPopEnterAnim();
+ int popExitAnim = navOptions.getPopExitAnim();
+ if (popEnterAnim != -1 || popExitAnim != -1) {
+ popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
+ popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
+ activity.overridePendingTransition(popEnterAnim, popExitAnim);
+ }
+ }
+ }
+
+ private int mLaunchMode;
+ @IdRes
+ private int mPopUpTo;
+ private boolean mPopUpToInclusive;
+ @AnimRes @AnimatorRes
+ private int mEnterAnim;
+ @AnimRes @AnimatorRes
+ private int mExitAnim;
+ @AnimRes @AnimatorRes
+ private int mPopEnterAnim;
+ @AnimRes @AnimatorRes
+ private int mPopExitAnim;
+
+ NavOptions(int launchMode, @IdRes int popUpTo, boolean popUpToInclusive,
+ @AnimRes @AnimatorRes int enterAnim, @AnimRes @AnimatorRes int exitAnim,
+ @AnimRes @AnimatorRes int popEnterAnim, @AnimRes @AnimatorRes int popExitAnim) {
+ mLaunchMode = launchMode;
+ mPopUpTo = popUpTo;
+ mPopUpToInclusive = popUpToInclusive;
+ mEnterAnim = enterAnim;
+ mExitAnim = exitAnim;
+ mPopEnterAnim = popEnterAnim;
+ mPopExitAnim = popExitAnim;
+ }
+
+ /**
+ * Whether this navigation action should launch as single-top (i.e., there will be at most
+ * one copy of a given destination on the top of the back stack).
+ * <p>
+ * This functions similarly to how {@link android.content.Intent#FLAG_ACTIVITY_SINGLE_TOP}
+ * works with activites.
+ */
+ public boolean shouldLaunchSingleTop() {
+ return (mLaunchMode & LAUNCH_SINGLE_TOP) != 0;
+ }
+
+ /**
+ * Whether this navigation action should launch the destination in a new document.
+ * <p>
+ * This functions similarly to how {@link android.content.Intent#FLAG_ACTIVITY_NEW_DOCUMENT}
+ * works with activites.
+ */
+ public boolean shouldLaunchDocument() {
+ return (mLaunchMode & LAUNCH_DOCUMENT) != 0;
+ }
+
+ /**
+ * Whether this navigation action should clear the entire back stack
+ * <p>
+ * This functions similarly to how {@link android.content.Intent#FLAG_ACTIVITY_CLEAR_TASK}
+ * works with activites.
+ */
+ public boolean shouldClearTask() {
+ return (mLaunchMode & LAUNCH_CLEAR_TASK) != 0;
+ }
+
+ /**
+ * The destination to pop up to before navigating. When set, all non-matching destinations
+ * should be popped from the back stack.
+ * @return the destinationId to pop up to, clearing all intervening destinations
+ * @see Builder#setPopUpTo
+ * @see #isPopUpToInclusive
+ */
+ @IdRes
+ public int getPopUpTo() {
+ return mPopUpTo;
+ }
+
+ /**
+ * Whether the destination set in {@link #getPopUpTo} should be popped from the back stack.
+ * @see Builder#setPopUpTo
+ * @see #getPopUpTo
+ */
+ public boolean isPopUpToInclusive() {
+ return mPopUpToInclusive;
+ }
+
+ /**
+ * The custom enter Animation/Animator that should be run.
+ * @return the resource id of a Animation or Animator or -1 if none.
+ */
+ @AnimRes @AnimatorRes
+ public int getEnterAnim() {
+ return mEnterAnim;
+ }
+
+ /**
+ * The custom exit Animation/Animator that should be run.
+ * @return the resource id of a Animation or Animator or -1 if none.
+ */
+ @AnimRes @AnimatorRes
+ public int getExitAnim() {
+ return mExitAnim;
+ }
+
+ /**
+ * The custom enter Animation/Animator that should be run when this destination is
+ * popped from the back stack.
+ * @return the resource id of a Animation or Animator or -1 if none.
+ * @see #applyPopAnimationsToPendingTransition(Activity)
+ */
+ @AnimRes @AnimatorRes
+ public int getPopEnterAnim() {
+ return mPopEnterAnim;
+ }
+
+ /**
+ * The custom exit Animation/Animator that should be run when this destination is
+ * popped from the back stack.
+ * @return the resource id of a Animation or Animator or -1 if none.
+ * @see #applyPopAnimationsToPendingTransition(Activity)
+ */
+ @AnimRes @AnimatorRes
+ public int getPopExitAnim() {
+ return mPopExitAnim;
+ }
+
+ @NonNull
+ private Bundle toBundle() {
+ Bundle b = new Bundle();
+ b.putInt(KEY_LAUNCH_MODE, mLaunchMode);
+ b.putInt(KEY_POP_UP_TO, mPopUpTo);
+ b.putBoolean(KEY_POP_UP_TO_INCLUSIVE, mPopUpToInclusive);
+ b.putInt(KEY_ENTER_ANIM, mEnterAnim);
+ b.putInt(KEY_EXIT_ANIM, mExitAnim);
+ b.putInt(KEY_POP_ENTER_ANIM, mPopEnterAnim);
+ b.putInt(KEY_POP_EXIT_ANIM, mPopExitAnim);
+ return b;
+ }
+
+ @NonNull
+ private static NavOptions fromBundle(@NonNull Bundle b) {
+ return new NavOptions(b.getInt(KEY_LAUNCH_MODE, 0),
+ b.getInt(KEY_POP_UP_TO, 0), b.getBoolean(KEY_POP_UP_TO_INCLUSIVE, false),
+ b.getInt(KEY_ENTER_ANIM, -1), b.getInt(KEY_EXIT_ANIM, -1),
+ b.getInt(KEY_POP_ENTER_ANIM, -1), b.getInt(KEY_POP_EXIT_ANIM, -1));
+ }
+
+ /**
+ * Builder for constructing new instances of NavOptions.
+ */
+ public static class Builder {
+ int mLaunchMode;
+ @IdRes
+ int mPopUpTo;
+ boolean mPopUpToInclusive;
+ @AnimRes @AnimatorRes
+ int mEnterAnim = -1;
+ @AnimRes @AnimatorRes
+ int mExitAnim = -1;
+ @AnimRes @AnimatorRes
+ int mPopEnterAnim = -1;
+ @AnimRes @AnimatorRes
+ int mPopExitAnim = -1;
+
+ public Builder() {
+ }
+
+ /**
+ * Launch a navigation target as single-top if you are making a lateral navigation
+ * between instances of the same target (e.g. detail pages about similar data items)
+ * that should not preserve history.
+ *
+ * @param singleTop true to launch as single-top
+ */
+ @NonNull
+ public Builder setLaunchSingleTop(boolean singleTop) {
+ if (singleTop) {
+ mLaunchMode |= LAUNCH_SINGLE_TOP;
+ } else {
+ mLaunchMode &= ~LAUNCH_SINGLE_TOP;
+ }
+ return this;
+ }
+
+ /**
+ * Launch a navigation target as a document if you want it to appear as its own
+ * entry in the system Overview screen. If the same document is launched multiple times
+ * it will not create a new task, it will bring the existing document task to the front.
+ *
+ * <p>If the user presses the system Back key from a new document task they will land
+ * on their previous task. If the user reached the document task from the system Overview
+ * screen they will be taken to their home screen.</p>
+ *
+ * @param launchDocument true to launch a new document task
+ */
+ @NonNull
+ public Builder setLaunchDocument(boolean launchDocument) {
+ if (launchDocument) {
+ mLaunchMode |= LAUNCH_DOCUMENT;
+ } else {
+ mLaunchMode &= ~LAUNCH_DOCUMENT;
+ }
+ return this;
+ }
+
+ /**
+ * Clear the entire task before launching this target. If you are launching as a
+ * {@link #setLaunchDocument(boolean) document}, this will clear the document task.
+ * Otherwise it will clear the current task.
+ *
+ * @param clearTask
+ * @return
+ */
+ @NonNull
+ public Builder setClearTask(boolean clearTask) {
+ if (clearTask) {
+ mLaunchMode |= LAUNCH_CLEAR_TASK;
+ } else {
+ mLaunchMode &= ~LAUNCH_CLEAR_TASK;
+ }
+ return this;
+ }
+
+ /**
+ * Pop up to a given destination before navigating. This pops all non-matching destinations
+ * from the back stack until this destination is found.
+ *
+ * @param destinationId The destination to pop up to, clearing all intervening destinations.
+ * @param inclusive true to also pop the given destination from the back stack.
+ * @return this Builder
+ * @see NavOptions#getPopUpTo
+ * @see NavOptions#isPopUpToInclusive
+ */
+ @NonNull
+ public Builder setPopUpTo(@IdRes int destinationId, boolean inclusive) {
+ mPopUpTo = destinationId;
+ mPopUpToInclusive = inclusive;
+ return this;
+ }
+
+ /**
+ * Sets a custom Animation or Animator resource for the enter animation.
+ *
+ * <p>Note: Animator resources are not supported for navigating to a new Activity</p>
+ * @param enterAnim Custom animation to run
+ * @return this Builder
+ * @see NavOptions#getEnterAnim()
+ */
+ public Builder setEnterAnim(@AnimRes @AnimatorRes int enterAnim) {
+ mEnterAnim = enterAnim;
+ return this;
+ }
+
+ /**
+ * Sets a custom Animation or Animator resource for the exit animation.
+ *
+ * <p>Note: Animator resources are not supported for navigating to a new Activity</p>
+ * @param exitAnim Custom animation to run
+ * @return this Builder
+ * @see NavOptions#getExitAnim()
+ */
+ @NonNull
+ public Builder setExitAnim(@AnimRes @AnimatorRes int exitAnim) {
+ mExitAnim = exitAnim;
+ return this;
+ }
+
+ /**
+ * Sets a custom Animation or Animator resource for the enter animation
+ * when popping off the back stack.
+ *
+ * <p>Note: Animator resources are not supported for navigating to a new Activity</p>
+ * @param popEnterAnim Custom animation to run
+ * @return this Builder
+ * @see NavOptions#getPopEnterAnim()
+ */
+ @NonNull
+ public Builder setPopEnterAnim(@AnimRes @AnimatorRes int popEnterAnim) {
+ mPopEnterAnim = popEnterAnim;
+ return this;
+ }
+
+ /**
+ * Sets a custom Animation or Animator resource for the exit animation
+ * when popping off the back stack.
+ *
+ * <p>Note: Animator resources are not supported for navigating to a new Activity</p>
+ * @param popExitAnim Custom animation to run
+ * @return this Builder
+ * @see NavOptions#getPopExitAnim()
+ */
+ @NonNull
+ public Builder setPopExitAnim(@AnimRes @AnimatorRes int popExitAnim) {
+ mPopExitAnim = popExitAnim;
+ return this;
+ }
+
+ /**
+ * @return a constructed NavOptions
+ */
+ @NonNull
+ public NavOptions build() {
+ return new NavOptions(mLaunchMode, mPopUpTo, mPopUpToInclusive,
+ mEnterAnim, mExitAnim, mPopEnterAnim, mPopExitAnim);
+ }
+ }
+}
diff --git a/navigation/common/src/main/java/androidx/navigation/Navigator.java b/navigation/common/src/main/java/androidx/navigation/Navigator.java
new file mode 100644
index 0000000..f66acf5
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/Navigator.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Navigator defines a mechanism for navigating within an app.
+ *
+ * <p>Each Navigator sets the policy for a specific type of navigation, e.g.
+ * {@link ActivityNavigator} knows how to launch into {@link NavDestination destinations}
+ * backed by activities using {@link Context#startActivity(Intent) startActivity}.</p>
+ *
+ * <p>Navigators should be able to manage their own back stack when navigating between two
+ * destinations that belong to that navigator. The {@link NavController} manages a back stack of
+ * navigators representing the current navigation stack across all navigators.</p>
+ *
+ * <p>Each Navigator should add the {@link Name Navigator.Name annotation} to their class. Any
+ * custom attributes used by the associated {@link NavDestination destination} subclass should
+ * have a name corresponding with the name of the Navigator, e.g., {@link ActivityNavigator} uses
+ * <code><declare-styleable name="ActivityNavigator"></code></p>
+ *
+ * @param <D> the subclass of {@link NavDestination} used with this Navigator which can be used
+ * to hold any special data that will be needed to navigate to that destination.
+ * Examples include information about an intent to navigate to other activities,
+ * or a fragment class name to instantiate and swap to a new fragment.
+ */
+public abstract class Navigator<D extends NavDestination> {
+ /**
+ * This annotation should be added to each Navigator subclass to denote the default name used
+ * to register the Navigator with a {@link NavigatorProvider}.
+ *
+ * @see NavigatorProvider#addNavigator(Navigator)
+ * @see NavigatorProvider#getNavigator(Class)
+ */
+ @Retention(RUNTIME)
+ @Target({TYPE})
+ public @interface Name {
+ String value();
+ }
+
+ @Retention(SOURCE)
+ @IntDef({BACK_STACK_UNCHANGED, BACK_STACK_DESTINATION_ADDED, BACK_STACK_DESTINATION_POPPED})
+ @interface BackStackEffect {}
+
+ /**
+ * Indicator that the navigation event should not change the {@link NavController}'s back stack.
+ *
+ * <p>For example, a {@link NavOptions#shouldLaunchSingleTop() single top} navigation event may
+ * not result in a back stack change if the existing destination is on the top of the stack.</p>
+ *
+ * @see #dispatchOnNavigatorNavigated
+ */
+ public static final int BACK_STACK_UNCHANGED = 0;
+
+ /**
+ * Indicator that the navigation event has added a new entry to the back stack. Only
+ * destinations added with this flag will be handled by {@link NavController#navigateUp()}.
+ *
+ * @see #dispatchOnNavigatorNavigated
+ */
+ public static final int BACK_STACK_DESTINATION_ADDED = 1;
+
+ /**
+ * Indicator that the navigation event has popped an entry off the back stack.
+ *
+ * @see #dispatchOnNavigatorNavigated
+ */
+ public static final int BACK_STACK_DESTINATION_POPPED = 2;
+
+ private final CopyOnWriteArrayList<OnNavigatorNavigatedListener> mOnNavigatedListeners =
+ new CopyOnWriteArrayList<>();
+
+ /**
+ * Construct a new NavDestination associated with this Navigator.
+ *
+ * <p>Any initialization of the destination should be done in the destination's constructor as
+ * it is not guaranteed that every destination will be created through this method.</p>
+ * @return a new NavDestination
+ */
+ @NonNull
+ public abstract D createDestination();
+
+ /**
+ * Navigate to a destination.
+ *
+ * <p>Requests navigation to a given destination associated with this navigator in
+ * the navigation graph. This method generally should not be called directly;
+ * {@link NavController} will delegate to it when appropriate.</p>
+ *
+ * <p>Implementations should {@link #dispatchOnNavigatorNavigated} to notify
+ * listeners of the resulting navigation destination.</p>
+ *
+ * @param destination destination node to navigate to
+ * @param args arguments to use for navigation
+ * @param navOptions additional options for navigation
+ */
+ public abstract void navigate(@NonNull D destination, @Nullable Bundle args,
+ @Nullable NavOptions navOptions);
+
+ /**
+ * Attempt to pop this navigator's back stack, performing the appropriate navigation.
+ *
+ * <p>Implementations should {@link #dispatchOnNavigatorNavigated} to notify
+ * listeners of the resulting navigation destination and return {@code true} if navigation
+ * was successful. Implementations should return {@code false} if navigation could not
+ * be performed, for example if the navigator's back stack was empty.</p>
+ *
+ * @return {@code true} if pop was successful
+ */
+ public abstract boolean popBackStack();
+
+ /**
+ * Add a listener to be notified when this navigator changes navigation destinations.
+ *
+ * <p>Most application code should use
+ * {@link NavController#addOnNavigatedListener(NavController.OnNavigatedListener)} instead.
+ * </p>
+ *
+ * @param listener listener to add
+ */
+ public final void addOnNavigatorNavigatedListener(
+ @NonNull OnNavigatorNavigatedListener listener) {
+ mOnNavigatedListeners.add(listener);
+ }
+
+ /**
+ * Remove a listener so that it will no longer be notified when this navigator changes
+ * navigation destinations.
+ *
+ * @param listener listener to remove
+ */
+ public final void removeOnNavigatorNavigatedListener(
+ @NonNull OnNavigatorNavigatedListener listener) {
+ mOnNavigatedListeners.remove(listener);
+ }
+
+ /**
+ * Dispatch a navigated event to all registered {@link OnNavigatorNavigatedListener listeners}.
+ * Utility for navigator implementations.
+ *
+ * @param destId id of the new destination
+ * @param backStackEffect how the navigation event affects the back stack
+ */
+ public final void dispatchOnNavigatorNavigated(@IdRes int destId,
+ @BackStackEffect int backStackEffect) {
+ for (OnNavigatorNavigatedListener listener : mOnNavigatedListeners) {
+ listener.onNavigatorNavigated(this, destId, backStackEffect);
+ }
+ }
+
+ /**
+ * Listener for observing navigation events for this specific navigator. Most app code
+ * should use {@link NavController.OnNavigatedListener} instead.
+ */
+ public interface OnNavigatorNavigatedListener {
+ /**
+ * This method is called after the Navigator navigates to a new destination.
+ *
+ * @param navigator
+ * @param destId
+ * @param backStackEffect
+ */
+ void onNavigatorNavigated(@NonNull Navigator navigator, @IdRes int destId,
+ @BackStackEffect int backStackEffect);
+ }
+}
diff --git a/navigation/common/src/main/java/androidx/navigation/NavigatorProvider.java b/navigation/common/src/main/java/androidx/navigation/NavigatorProvider.java
new file mode 100644
index 0000000..792c894
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/NavigatorProvider.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.annotation.SuppressLint;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/**
+ * A NavigationProvider stores a set of {@link Navigator}s that are valid ways to navigate
+ * to a destination.
+ */
+@SuppressLint("TypeParameterUnusedInFormals")
+public interface NavigatorProvider {
+ /**
+ * Retrieves a registered {@link Navigator} using the name provided by the
+ * {@link Navigator.Name Navigator.Name annotation}.
+ *
+ * @param navigatorClass class of the navigator to return
+ * @return the registered navigator with the given {@link Navigator.Name}
+ *
+ * @throws IllegalArgumentException if the Navigator does not have a
+ * {@link Navigator.Name Navigator.Name annotation}
+ * @throws IllegalStateException if the Navigator has not been added
+ *
+ * @see #addNavigator(Navigator)
+ */
+ @NonNull
+ <D extends NavDestination, T extends Navigator<? extends D>> T getNavigator(
+ @NonNull Class<T> navigatorClass);
+
+ /**
+ * Retrieves a registered {@link Navigator} by name.
+ *
+ * @param name name of the navigator to return
+ * @return the registered navigator with the given name
+ *
+ * @throws IllegalStateException if the Navigator has not been added
+ *
+ * @see #addNavigator(String, Navigator)
+ */
+ @NonNull
+ <D extends NavDestination, T extends Navigator<? extends D>> T getNavigator(
+ @NonNull String name);
+
+ /**
+ * Register a navigator using the name provided by the
+ * {@link Navigator.Name Navigator.Name annotation}. {@link NavDestination destinations} may
+ * refer to any registered navigator by name for inflation. If a navigator by this name is
+ * already registered, this new navigator will replace it.
+ *
+ * @param navigator navigator to add
+ * @return the previously added Navigator for the name provided by the
+ * {@link Navigator.Name Navigator.Name annotation}, if any
+ */
+ @Nullable
+ Navigator<? extends NavDestination> addNavigator(
+ @NonNull Navigator<? extends NavDestination> navigator);
+
+ /**
+ * Register a navigator by name. {@link NavDestination destinations} may refer to any
+ * registered navigator by name for inflation. If a navigator by this name is already
+ * registered, this new navigator will replace it.
+ *
+ * @param name name for this navigator
+ * @param navigator navigator to add
+ * @return the previously added Navigator for the given name, if any
+ */
+ @Nullable
+ Navigator<? extends NavDestination> addNavigator(@NonNull String name,
+ @NonNull Navigator<? extends NavDestination> navigator);
+}
diff --git a/navigation/common/src/main/java/androidx/navigation/SimpleNavigatorProvider.java b/navigation/common/src/main/java/androidx/navigation/SimpleNavigatorProvider.java
new file mode 100644
index 0000000..ef1f222
--- /dev/null
+++ b/navigation/common/src/main/java/androidx/navigation/SimpleNavigatorProvider.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.annotation.SuppressLint;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+import java.util.HashMap;
+
+/**
+ * Simple implementation of a {@link NavigatorProvider} that stores instances of
+ * {@link Navigator navigators} by name, using the {@link Navigator.Name} when given a class name.
+ *
+ * @hide
+ */
+@SuppressLint("TypeParameterUnusedInFormals")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SimpleNavigatorProvider implements NavigatorProvider {
+ private static final HashMap<Class, String> sAnnotationNames = new HashMap<>();
+
+ private final HashMap<String, Navigator<? extends NavDestination>> mNavigators =
+ new HashMap<>();
+
+ @NonNull
+ private String getNameForNavigator(@NonNull Class<? extends Navigator> navigatorClass) {
+ String name = sAnnotationNames.get(navigatorClass);
+ if (name == null) {
+ Navigator.Name annotation = navigatorClass.getAnnotation(Navigator.Name.class);
+ name = annotation != null ? annotation.value() : null;
+ if (!validateName(name)) {
+ throw new IllegalArgumentException("No @Navigator.Name annotation found for "
+ + navigatorClass.getSimpleName());
+ }
+ sAnnotationNames.put(navigatorClass, name);
+ }
+ return name;
+ }
+
+ @NonNull
+ @Override
+ public <D extends NavDestination, T extends Navigator<? extends D>> T getNavigator(
+ @NonNull Class<T> navigatorClass) {
+ String name = getNameForNavigator(navigatorClass);
+ return getNavigator(name);
+ }
+
+ @NonNull
+ @Override
+ public <D extends NavDestination, T extends Navigator<? extends D>> T getNavigator(
+ @NonNull String name) {
+ if (!validateName(name)) {
+ throw new IllegalArgumentException("navigator name cannot be an empty string");
+ }
+
+ Navigator<? extends NavDestination> navigator = mNavigators.get(name);
+ if (navigator == null) {
+ throw new IllegalStateException("Could not find Navigator with name \"" + name
+ + "\". You must call NavController.addNavigator() for each navigation type.");
+ }
+ //noinspection unchecked
+ return (T) navigator;
+ }
+
+ @Nullable
+ @Override
+ public Navigator<? extends NavDestination> addNavigator(
+ @NonNull Navigator<? extends NavDestination> navigator) {
+ String name = getNameForNavigator(navigator.getClass());
+
+ return addNavigator(name, navigator);
+ }
+
+ @Nullable
+ @Override
+ public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
+ @NonNull Navigator<? extends NavDestination> navigator) {
+ if (!validateName(name)) {
+ throw new IllegalArgumentException("navigator name cannot be an empty string");
+ }
+ return mNavigators.put(name, navigator);
+ }
+
+ private boolean validateName(String name) {
+ return name != null && !name.isEmpty();
+ }
+}
diff --git a/navigation/common/src/main/res/values/attrs.xml b/navigation/common/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..2701cd6
--- /dev/null
+++ b/navigation/common/src/main/res/values/attrs.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<resources>
+ <declare-styleable name="Navigator">
+ <attr name="android:id"/>
+ <attr name="android:label" />
+ </declare-styleable>
+
+ <declare-styleable name="NavGraphNavigator">
+ <attr name="startDestination" format="reference" />
+ </declare-styleable>
+
+ <declare-styleable name="NavArgument">
+ <attr name="android:name" />
+ <attr name="android:defaultValue" />
+ <!--free format since in future it could be Parcelable-->
+ <attr name="type"/>
+ </declare-styleable>
+
+ <declare-styleable name="NavDeepLink">
+ <attr name="uri" format="string" />
+ <attr name="android:autoVerify" />
+ </declare-styleable>
+
+ <declare-styleable name="NavAction">
+ <attr name="android:id" />
+ <attr name="destination" format="reference" />
+ <attr name="launchSingleTop" format="boolean" />
+ <attr name="launchDocument" format="boolean" />
+ <attr name="clearTask" format="boolean" />
+ <attr name="popUpTo" format="reference" />
+ <attr name="popUpToInclusive" format="boolean" />
+ <attr name="enterAnim" format="reference" />
+ <attr name="exitAnim" format="reference" />
+ <attr name="popEnterAnim" format="reference" />
+ <attr name="popExitAnim" format="reference" />
+ </declare-styleable>
+</resources>
diff --git a/navigation/common/src/test/java/androidx/navigation/EmptyNavigator.java b/navigation/common/src/test/java/androidx/navigation/EmptyNavigator.java
new file mode 100644
index 0000000..2b2174b
--- /dev/null
+++ b/navigation/common/src/test/java/androidx/navigation/EmptyNavigator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.os.Bundle;
+
+/**
+ * An empty {@link Navigator} used to test {@link SimpleNavigatorProvider}.
+ */
+@Navigator.Name(EmptyNavigator.NAME)
+class EmptyNavigator extends Navigator<NavDestination> {
+ static final String NAME = "empty";
+
+ @Override
+ public NavDestination createDestination() {
+ return new NavDestination(this);
+ }
+
+ @Override
+ public void navigate(NavDestination destination, Bundle args, NavOptions navOptions) {
+ throw new IllegalStateException("navigate is not supported");
+ }
+
+ @Override
+ public boolean popBackStack() {
+ throw new IllegalStateException("popBackStack is not supported");
+ }
+}
diff --git a/navigation/common/src/test/java/androidx/navigation/NavActionTest.java b/navigation/common/src/test/java/androidx/navigation/NavActionTest.java
new file mode 100644
index 0000000..589a420
--- /dev/null
+++ b/navigation/common/src/test/java/androidx/navigation/NavActionTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.support.annotation.IdRes;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+@SmallTest
+public class NavActionTest {
+ @IdRes
+ private static final int DESTINATION_ID = 1;
+
+ @Test
+ public void createAction() {
+ NavAction action = new NavAction(DESTINATION_ID);
+
+ assertThat(action.getDestinationId(), is(DESTINATION_ID));
+ }
+
+ @Test
+ public void createActionWithNullNavOptions() {
+ NavAction action = new NavAction(DESTINATION_ID, null);
+
+ assertThat(action.getDestinationId(), is(DESTINATION_ID));
+ assertThat(action.getNavOptions(), nullValue());
+ }
+
+ @Test
+ public void setNavOptions() {
+ NavAction action = new NavAction(DESTINATION_ID);
+ NavOptions navOptions = new NavOptions.Builder().build();
+ action.setNavOptions(navOptions);
+
+ assertThat(action.getNavOptions(), is(navOptions));
+ }
+}
diff --git a/navigation/common/src/test/java/androidx/navigation/NavDestinationTest.java b/navigation/common/src/test/java/androidx/navigation/NavDestinationTest.java
new file mode 100644
index 0000000..ce5ad32
--- /dev/null
+++ b/navigation/common/src/test/java/androidx/navigation/NavDestinationTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.mock;
+
+import android.support.annotation.IdRes;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SuppressWarnings("unchecked")
+@RunWith(JUnit4.class)
+@SmallTest
+public class NavDestinationTest {
+ @IdRes
+ private static final int INVALID_ACTION_ID = 0;
+ @IdRes
+ private static final int ACTION_ID = 1;
+ @IdRes
+ private static final int DESTINATION_ID = 1;
+
+ @Test
+ public void buildDeepLinkIds() {
+ NavDestination destination = new NavDestination(mock(Navigator.class));
+ destination.setId(DESTINATION_ID);
+ int parentId = 2;
+ NavGraph parent = new NavGraph(mock(Navigator.class));
+ parent.setId(parentId);
+ destination.setParent(parent);
+ int[] deepLinkIds = destination.buildDeepLinkIds();
+ assertThat(deepLinkIds.length, is(2));
+ assertThat(deepLinkIds[0], is(parentId));
+ assertThat(deepLinkIds[1], is(DESTINATION_ID));
+ }
+
+ @Test
+ public void putActionByDestinationId() {
+ NavDestination destination = new NavDestination(mock(Navigator.class));
+ destination.putAction(ACTION_ID, DESTINATION_ID);
+
+ assertThat(destination.getAction(ACTION_ID), not(nullValue()));
+ assertThat(destination.getAction(ACTION_ID).getDestinationId(), is(DESTINATION_ID));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void putActionWithInvalidDestinationId() {
+ NavDestination destination = new NavDestination(mock(Navigator.class));
+ destination.putAction(INVALID_ACTION_ID, DESTINATION_ID);
+ }
+
+ @Test
+ public void putAction() {
+ NavDestination destination = new NavDestination(mock(Navigator.class));
+ NavAction action = new NavAction(DESTINATION_ID);
+ destination.putAction(ACTION_ID, action);
+
+ assertThat(destination.getAction(ACTION_ID), is(action));
+ }
+
+ @Test
+ public void removeAction() {
+ NavDestination destination = new NavDestination(mock(Navigator.class));
+ NavAction action = new NavAction(DESTINATION_ID);
+ destination.putAction(ACTION_ID, action);
+
+ assertThat(destination.getAction(ACTION_ID), is(action));
+
+ destination.removeAction(ACTION_ID);
+
+ assertThat(destination.getAction(ACTION_ID), nullValue());
+ }
+}
diff --git a/navigation/common/src/test/java/androidx/navigation/NavGraphTest.java b/navigation/common/src/test/java/androidx/navigation/NavGraphTest.java
new file mode 100644
index 0000000..beedfd9
--- /dev/null
+++ b/navigation/common/src/test/java/androidx/navigation/NavGraphTest.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.content.Context;
+import android.support.annotation.IdRes;
+import android.support.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+@SuppressWarnings("unchecked")
+@RunWith(JUnit4.class)
+@SmallTest
+public class NavGraphTest {
+ @IdRes
+ private static final int FIRST_DESTINATION_ID = 1;
+ @IdRes
+ private static final int SECOND_DESTINATION_ID = 2;
+
+ private NavGraphNavigator mNavGraphNavigator;
+
+ @Before
+ public void setup() {
+ mNavGraphNavigator = new NavGraphNavigator(mock(Context.class));
+ }
+
+ private NavDestination createFirstDestination() {
+ NavDestination destination = new NavDestination(mock(Navigator.class));
+ destination.setId(FIRST_DESTINATION_ID);
+ return destination;
+ }
+
+ private NavDestination createSecondDestination() {
+ NavDestination destination = new NavDestination(mock(Navigator.class));
+ destination.setId(SECOND_DESTINATION_ID);
+ return destination;
+ }
+
+ private NavGraph createGraphWithDestination(NavDestination destination) {
+ NavGraph graph = mNavGraphNavigator.createDestination();
+ graph.addDestination(destination);
+ return graph;
+ }
+
+ private NavGraph createGraphWithDestinations(NavDestination... destinations) {
+ NavGraph graph = mNavGraphNavigator.createDestination();
+ graph.addDestinations(destinations);
+ return graph;
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void addDestinationWithoutId() {
+ NavGraph graph = mNavGraphNavigator.createDestination();
+ NavDestination destination = new NavDestination(mock(Navigator.class));
+ graph.addDestination(destination);
+ }
+
+ @Test
+ public void addDestination() {
+ NavDestination destination = createFirstDestination();
+ NavGraph graph = createGraphWithDestination(destination);
+
+ assertThat(destination.getParent(), is(graph));
+ assertThat(graph.findNode(FIRST_DESTINATION_ID), is(destination));
+ }
+
+ @Test
+ public void addDestinationsAsCollection() {
+ NavGraph graph = mNavGraphNavigator.createDestination();
+ NavDestination destination = createFirstDestination();
+ NavDestination secondDestination = createSecondDestination();
+ graph.addDestinations(Arrays.asList(destination, secondDestination));
+
+ assertThat(destination.getParent(), is(graph));
+ assertThat(graph.findNode(FIRST_DESTINATION_ID), is(destination));
+ assertThat(secondDestination.getParent(), is(graph));
+ assertThat(graph.findNode(SECOND_DESTINATION_ID), is(secondDestination));
+ }
+
+ @Test
+ public void addDestinationsAsVarArgs() {
+ NavDestination destination = createFirstDestination();
+ NavDestination secondDestination = createSecondDestination();
+ NavGraph graph = createGraphWithDestinations(destination, secondDestination);
+
+ assertThat(destination.getParent(), is(graph));
+ assertThat(graph.findNode(FIRST_DESTINATION_ID), is(destination));
+ assertThat(secondDestination.getParent(), is(graph));
+ assertThat(graph.findNode(SECOND_DESTINATION_ID), is(secondDestination));
+ }
+
+ @Test
+ public void addReplacementDestination() {
+ NavDestination destination = createFirstDestination();
+ NavGraph graph = createGraphWithDestination(destination);
+
+ NavDestination replacementDestination = new NavDestination(mock(Navigator.class));
+ replacementDestination.setId(FIRST_DESTINATION_ID);
+ graph.addDestination(replacementDestination);
+
+ assertThat(destination.getParent(), nullValue());
+ assertThat(replacementDestination.getParent(), is(graph));
+ assertThat(graph.findNode(FIRST_DESTINATION_ID), is(replacementDestination));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void addDestinationWithExistingParent() {
+ NavDestination destination = createFirstDestination();
+ createGraphWithDestination(destination);
+
+ NavGraph other = mNavGraphNavigator.createDestination();
+ other.addDestination(destination);
+ }
+
+ @Test
+ public void addAll() {
+ NavDestination destination = createFirstDestination();
+ NavGraph other = createGraphWithDestination(destination);
+
+ NavGraph graph = mNavGraphNavigator.createDestination();
+ graph.addAll(other);
+
+ assertThat(destination.getParent(), is(graph));
+ assertThat(graph.findNode(FIRST_DESTINATION_ID), is(destination));
+ assertThat(other.findNode(FIRST_DESTINATION_ID), nullValue());
+ }
+
+ @Test
+ public void removeDestination() {
+ NavDestination destination = createFirstDestination();
+ NavGraph graph = createGraphWithDestination(destination);
+
+ graph.remove(destination);
+
+ assertThat(destination.getParent(), nullValue());
+ assertThat(graph.findNode(FIRST_DESTINATION_ID), nullValue());
+ }
+
+ @Test
+ public void iterator() {
+ NavDestination destination = createFirstDestination();
+ NavDestination secondDestination = createSecondDestination();
+ NavGraph graph = createGraphWithDestinations(destination, secondDestination);
+
+ Iterator<NavDestination> iterator = graph.iterator();
+ assertTrue(iterator.hasNext());
+ assertThat(iterator.next(), anyOf(is(destination), is(secondDestination)));
+ assertTrue(iterator.hasNext());
+ assertThat(iterator.next(), anyOf(is(destination), is(secondDestination)));
+ assertFalse(iterator.hasNext());
+ }
+
+ @Test(expected = NoSuchElementException.class)
+ public void iteratorNoSuchElement() {
+ NavDestination destination = createFirstDestination();
+ NavGraph graph = createGraphWithDestination(destination);
+
+ Iterator<NavDestination> iterator = graph.iterator();
+ iterator.next();
+ iterator.next();
+ }
+
+ @Test
+ public void iteratorRemove() {
+ NavDestination destination = createFirstDestination();
+ NavGraph graph = createGraphWithDestination(destination);
+
+ Iterator<NavDestination> iterator = graph.iterator();
+ NavDestination value = iterator.next();
+ iterator.remove();
+ assertThat(value.getParent(), nullValue());
+ assertThat(graph.findNode(value.getId()), nullValue());
+ }
+
+ @Test
+ public void iteratorDoubleRemove() {
+ NavDestination destination = createFirstDestination();
+ NavDestination secondDestination = createSecondDestination();
+ NavGraph graph = createGraphWithDestinations(destination, secondDestination);
+
+ Iterator<NavDestination> iterator = graph.iterator();
+ iterator.next();
+ iterator.remove();
+ NavDestination value = iterator.next();
+ iterator.remove();
+ assertThat(value.getParent(), nullValue());
+ assertThat(graph.findNode(value.getId()), nullValue());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void iteratorDoubleRemoveWithoutNext() {
+ NavDestination destination = createFirstDestination();
+ NavDestination secondDestination = createSecondDestination();
+ NavGraph graph = createGraphWithDestinations(destination, secondDestination);
+
+ Iterator<NavDestination> iterator = graph.iterator();
+ iterator.next();
+ iterator.remove();
+ iterator.remove();
+ }
+
+ @Test
+ public void clear() {
+ NavDestination destination = createFirstDestination();
+ NavDestination secondDestination = createSecondDestination();
+ NavGraph graph = createGraphWithDestinations(destination, secondDestination);
+
+ graph.clear();
+ assertThat(destination.getParent(), nullValue());
+ assertThat(graph.findNode(FIRST_DESTINATION_ID), nullValue());
+ assertThat(secondDestination.getParent(), nullValue());
+ assertThat(graph.findNode(SECOND_DESTINATION_ID), nullValue());
+ }
+}
diff --git a/navigation/common/src/test/java/androidx/navigation/NoNameNavigator.java b/navigation/common/src/test/java/androidx/navigation/NoNameNavigator.java
new file mode 100644
index 0000000..8b22459
--- /dev/null
+++ b/navigation/common/src/test/java/androidx/navigation/NoNameNavigator.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.os.Bundle;
+
+/**
+ * A {@link Navigator} that does not have a {@link Navigator.Name} used to test
+ * {@link SimpleNavigatorProvider}.
+ */
+class NoNameNavigator extends Navigator<NavDestination> {
+ @Override
+ public NavDestination createDestination() {
+ return new NavDestination(this);
+ }
+
+ @Override
+ public void navigate(NavDestination destination, Bundle args, NavOptions navOptions) {
+ throw new IllegalStateException("navigate is not supported");
+ }
+
+ @Override
+ public boolean popBackStack() {
+ throw new IllegalStateException("popBackStack is not supported");
+ }
+}
diff --git a/navigation/common/src/test/java/androidx/navigation/SimpleNavigatorProviderTest.java b/navigation/common/src/test/java/androidx/navigation/SimpleNavigatorProviderTest.java
new file mode 100644
index 0000000..ba7c648
--- /dev/null
+++ b/navigation/common/src/test/java/androidx/navigation/SimpleNavigatorProviderTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.fail;
+
+import android.support.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@SuppressWarnings("unchecked")
+@RunWith(JUnit4.class)
+@SmallTest
+public class SimpleNavigatorProviderTest {
+ @Test
+ public void addWithMissingAnnotationName() {
+ SimpleNavigatorProvider provider = new SimpleNavigatorProvider();
+ Navigator navigator = new NoNameNavigator();
+ try {
+ provider.addNavigator(navigator);
+ fail("Adding a provider with no @Navigator.Name should cause an "
+ + "IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void addWithMissingAnnotationNameGetWithExplicitName() {
+ SimpleNavigatorProvider provider = new SimpleNavigatorProvider();
+ Navigator navigator = new NoNameNavigator();
+ provider.addNavigator("name", navigator);
+ assertThat(provider.getNavigator("name"), is(navigator));
+ }
+
+ @Test
+ public void addWithExplicitNameGetWithExplicitName() {
+ SimpleNavigatorProvider provider = new SimpleNavigatorProvider();
+ Navigator navigator = new EmptyNavigator();
+ provider.addNavigator("name", navigator);
+ assertThat(provider.getNavigator("name"), is(navigator));
+ try {
+ provider.getNavigator(EmptyNavigator.class);
+ fail("getNavigator(Class) with an invalid name should cause an IllegalStateException");
+ } catch (IllegalStateException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void addWithExplicitNameGetWithMissingAnnotationName() {
+ SimpleNavigatorProvider provider = new SimpleNavigatorProvider();
+ Navigator navigator = new NoNameNavigator();
+ provider.addNavigator("name", navigator);
+ try {
+ provider.getNavigator(NoNameNavigator.class);
+ fail("getNavigator(Class) with no @Navigator.Name should cause "
+ + "an IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void addWithAnnotationNameGetWithAnnotationName() {
+ SimpleNavigatorProvider provider = new SimpleNavigatorProvider();
+ Navigator navigator = new EmptyNavigator();
+ provider.addNavigator(navigator);
+ assertThat(provider.getNavigator(EmptyNavigator.class), is(navigator));
+ }
+
+ @Test
+ public void addWithAnnotationNameGetWithExplicitName() {
+ SimpleNavigatorProvider provider = new SimpleNavigatorProvider();
+ Navigator navigator = new EmptyNavigator();
+ provider.addNavigator(navigator);
+ assertThat(provider.getNavigator(EmptyNavigator.NAME), is(navigator));
+ }
+}
diff --git a/navigation/fragment/build.gradle b/navigation/fragment/build.gradle
new file mode 100644
index 0000000..8014bf9
--- /dev/null
+++ b/navigation/fragment/build.gradle
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ api(NAV_SUPPORT_FRAGMENTS)
+ api(project(":navigation:navigation-runtime"))
+
+ testImplementation(JUNIT)
+ testImplementation(MOCKITO_CORE)
+ testImplementation(TEST_RUNNER)
+
+ androidTestImplementation(project(":navigation:navigation-testing"))
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+ name = "Android Navigation Fragment"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2017"
+ description = "Android Navigation-Fragment"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/fragment/ktx/build.gradle b/navigation/fragment/ktx/build.gradle
new file mode 100644
index 0000000..90d7e3b
--- /dev/null
+++ b/navigation/fragment/ktx/build.gradle
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(project(":navigation:navigation-fragment"))
+ // Ensure that the -ktx dependency graph mirrors the Java dependency graph
+ api(project(":navigation:navigation-runtime-ktx"))
+ api(KOTLIN_STDLIB)
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+ name = "Android Navigation Fragment Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2018"
+ description = "Android Navigation-Fragment-Ktx"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
+
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/fragment/ktx/src/androidTest/AndroidManifest.xml
similarity index 66%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/fragment/ktx/src/androidTest/AndroidManifest.xml
index 660dbcd..54df6d4 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/fragment/ktx/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.fragment.ktx.test">
+ <application>
+ <activity android:name="androidx.navigation.fragment.TestActivity" />
+ </application>
+</manifest>
diff --git a/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilderTest.kt b/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilderTest.kt
new file mode 100644
index 0000000..5c2a9c1
--- /dev/null
+++ b/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilderTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.navigation.fragment
+
+import android.support.test.annotation.UiThreadTest
+import android.support.test.filters.SmallTest
+import android.support.test.rule.ActivityTestRule
+import android.support.test.runner.AndroidJUnit4
+import android.support.v4.app.Fragment
+import androidx.navigation.contains
+import androidx.navigation.createGraph
+import androidx.navigation.get
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TestNavigatorDestinationBuilderTest {
+ @get:Rule
+ val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+ private val fragmentManager get() = activityRule.activity.supportFragmentManager
+
+ @UiThreadTest
+ @Test fun fragment() {
+ val navHostFragment = NavHostFragment()
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, navHostFragment)
+ .commitNow()
+ val graph = navHostFragment.createGraph(startDestination = DESTINATION_ID) {
+ fragment<BuilderTestFragment>(DESTINATION_ID)
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ assertEquals("Fragment class should be set to BuilderTestFragment",
+ BuilderTestFragment::class.java,
+ (graph[DESTINATION_ID] as FragmentNavigator.Destination).fragmentClass)
+ }
+
+ @UiThreadTest
+ @Test fun fragmentWithBody() {
+ val navHostFragment = NavHostFragment()
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, navHostFragment)
+ .commitNow()
+ val graph = navHostFragment.createGraph(startDestination = DESTINATION_ID) {
+ fragment<BuilderTestFragment>(DESTINATION_ID) {
+ label = LABEL
+ }
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ assertEquals("Fragment class should be set to BuilderTestFragment",
+ BuilderTestFragment::class.java,
+ (graph[DESTINATION_ID] as FragmentNavigator.Destination).fragmentClass)
+ assertEquals("Fragment should have label set",
+ LABEL, graph[DESTINATION_ID].label)
+ }
+}
+
+private const val DESTINATION_ID = 1
+private const val LABEL = "Test"
+class BuilderTestFragment : Fragment()
diff --git a/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentTest.kt b/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentTest.kt
new file mode 100644
index 0000000..2540852
--- /dev/null
+++ b/navigation/fragment/ktx/src/androidTest/java/androidx/navigation/fragment/FragmentTest.kt
@@ -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.navigation.fragment
+
+import android.support.test.annotation.UiThreadTest
+import android.support.test.filters.SmallTest
+import android.support.test.rule.ActivityTestRule
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentActivity
+import androidx.navigation.fragment.ktx.test.R
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+
+@SmallTest
+class ActivityTest {
+ @get:Rule val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+ private val fragmentManager get() = activityRule.activity.supportFragmentManager
+ private val contentFragment get() = fragmentManager.findFragmentById(android.R.id.content)
+
+ @UiThreadTest
+ @Test fun navController() {
+ val navHostFragment = NavHostFragment.create(R.navigation.test_graph)
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, navHostFragment)
+ .commitNow()
+ assertTrue("Fragment should have NavController set",
+ contentFragment.navController == navHostFragment.navController)
+ }
+
+ @UiThreadTest
+ @Test fun navControllerNull() {
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, TestFragment())
+ .commitNow()
+ try {
+ contentFragment.navController
+ fail("navController should throw IllegalStateException if a NavController was not set")
+ } catch (e: IllegalStateException) {
+ // Expected
+ }
+ }
+
+ @UiThreadTest
+ @Test fun findNavController() {
+ val navHostFragment = NavHostFragment.create(R.navigation.test_graph)
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, navHostFragment)
+ .commitNow()
+
+ val foundNavController = contentFragment.findNavController()
+ assertNotNull("findNavController should return non-null if a NavController was set",
+ foundNavController)
+ assertTrue("Fragment should have NavController set",
+ foundNavController == navHostFragment.navController)
+ }
+
+ @UiThreadTest
+ @Test fun findNavControllerNull() {
+ fragmentManager.beginTransaction()
+ .add(android.R.id.content, TestFragment())
+ .commitNow()
+ assertNull("findNavController should return null if a NavController was never set",
+ contentFragment.findNavController())
+ }
+}
+
+class TestActivity : FragmentActivity()
+class TestFragment : Fragment()
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/fragment/ktx/src/androidTest/res/navigation/test_graph.xml
similarity index 69%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/fragment/ktx/src/androidTest/res/navigation/test_graph.xml
index 660dbcd..5f1268a 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/fragment/ktx/src/androidTest/res/navigation/test_graph.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,11 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
+<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:startDestination="@+id/start">
+ <fragment
+ android:id="@+id/start"
+ android:name="androidx.navigation.fragment.TestFragment"/>
+</navigation>
diff --git a/car/res/drawable/car_button_ripple_background.xml b/navigation/fragment/ktx/src/main/AndroidManifest.xml
similarity index 83%
rename from car/res/drawable/car_button_ripple_background.xml
rename to navigation/fragment/ktx/src/main/AndroidManifest.xml
index 13d0a49..bd32886 100644
--- a/car/res/drawable/car_button_ripple_background.xml
+++ b/navigation/fragment/ktx/src/main/AndroidManifest.xml
@@ -14,6 +14,5 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background" />
+
+<manifest package="androidx.navigation.fragment.ktx"/>
diff --git a/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/Fragment.kt b/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/Fragment.kt
new file mode 100644
index 0000000..41e6e43
--- /dev/null
+++ b/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/Fragment.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.navigation.fragment
+
+import android.support.v4.app.Fragment
+import androidx.navigation.NavController
+
+/**
+ * Find a [NavController] given a [Fragment]
+ */
+fun Fragment.findNavController(): NavController? =
+ NavHostFragment.findNavController(this)
+
+/**
+ * Gets the [NavController] given a [Fragment].
+ *
+ * Calling this on a Fragment that is not a [NavHostFragment] or within a [NavHostFragment]
+ * will result in an [IllegalStateException]
+ */
+val Fragment.navController: NavController
+ get() = NavHostFragment.getNavController(this)
diff --git a/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt b/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt
new file mode 100644
index 0000000..a188bf0
--- /dev/null
+++ b/navigation/fragment/ktx/src/main/java/androidx/navigation/fragment/FragmentNavigatorDestinationBuilder.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.navigation.fragment
+
+import android.support.annotation.IdRes
+import android.support.v4.app.Fragment
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.get
+import kotlin.reflect.KClass
+
+/**
+ * Construct a new [FragmentNavigator.Destination]
+ */
+inline fun <reified F : Fragment> NavGraphBuilder.fragment(@IdRes id: Int) = fragment<F>(id) {}
+
+/**
+ * Construct a new [FragmentNavigator.Destination]
+ */
+inline fun <reified F : Fragment> NavGraphBuilder.fragment(
+ @IdRes id: Int,
+ block: FragmentNavigatorDestinationBuilder.() -> Unit
+) = destination(FragmentNavigatorDestinationBuilder(
+ provider[FragmentNavigator::class],
+ id,
+ F::class
+).apply(block))
+
+/**
+ * DSL for constructing a new [FragmentNavigator.Destination]
+ */
+@NavDestinationDsl
+class FragmentNavigatorDestinationBuilder(
+ navigator: FragmentNavigator,
+ @IdRes id: Int,
+ private val fragmentClass: KClass<out Fragment>
+) : NavDestinationBuilder<FragmentNavigator.Destination>(navigator, id) {
+
+ override fun build(): FragmentNavigator.Destination =
+ super.build().also { destination ->
+ destination.fragmentClass = fragmentClass.java
+ }
+}
diff --git a/navigation/fragment/src/androidTest/AndroidManifest.xml b/navigation/fragment/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..c9be5d4
--- /dev/null
+++ b/navigation/fragment/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.fragment.test">
+ <application>
+ <activity android:name="androidx.navigation.fragment.test.XmlNavigationActivity" />
+ <activity android:name="androidx.navigation.fragment.test.DynamicNavigationActivity" />
+ </application>
+</manifest>
diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/BaseNavControllerTest.java b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/BaseNavControllerTest.java
new file mode 100644
index 0000000..704731e
--- /dev/null
+++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/BaseNavControllerTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2017 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.navigation.fragment;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.app.Instrumentation;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.NavigationRes;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v4.app.TaskStackBuilder;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import androidx.navigation.NavController;
+import androidx.navigation.NavDeepLinkBuilder;
+import androidx.navigation.fragment.test.BaseNavigationActivity;
+import androidx.navigation.fragment.test.R;
+import androidx.navigation.testing.TestNavigator;
+
+@SmallTest
+public abstract class BaseNavControllerTest<A extends BaseNavigationActivity> {
+ private static final String TEST_ARG = "test";
+ private static final String TEST_ARG_VALUE = "value";
+ private static final String TEST_DEEP_LINK_ACTION = "deep_link";
+
+ /**
+ * @return The concrete Activity class under test
+ */
+ protected abstract Class<A> getActivityClass();
+
+ @Rule
+ public ActivityTestRule<A> mActivityRule =
+ new ActivityTestRule<>(getActivityClass(), false, false);
+
+ private Instrumentation mInstrumentation;
+
+ @Before
+ public void getInstrumentation() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ }
+
+ @Test
+ public void testDeeplink() throws Throwable {
+ BaseNavigationActivity activity = launchDeepLink(R.navigation.nav_deep_link,
+ R.id.deep_link_test, null);
+ NavController navController = activity.getNavController();
+
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.deep_link_test));
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navigator.mBackStack.size(), is(2));
+
+ // Test that the deep link Intent was passed through even though we don't pass in any args
+ Intent deepLinkIntent = navigator.mBackStack.peekLast().second
+ .getParcelable(NavController.KEY_DEEP_LINK_INTENT);
+ assertThat(deepLinkIntent, is(notNullValue(Intent.class)));
+ //noinspection ConstantConditions
+ assertThat(deepLinkIntent.getAction(), is(TEST_DEEP_LINK_ACTION));
+ }
+
+ @Test
+ public void testDeeplinkWithArgs() throws Throwable {
+ Bundle args = new Bundle();
+ args.putString(TEST_ARG, TEST_ARG_VALUE);
+ BaseNavigationActivity activity = launchDeepLink(R.navigation.nav_deep_link,
+ R.id.deep_link_test, args);
+ NavController navController = activity.getNavController();
+
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.deep_link_test));
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navigator.mBackStack.size(), is(2));
+ assertThat(navigator.mBackStack.peekLast().second.getString(TEST_ARG), is(TEST_ARG_VALUE));
+
+ // Test that the deep link Intent was passed in alongside our args
+ Intent deepLinkIntent = navigator.mBackStack.peekLast().second
+ .getParcelable(NavController.KEY_DEEP_LINK_INTENT);
+ assertThat(deepLinkIntent, is(notNullValue(Intent.class)));
+ //noinspection ConstantConditions
+ assertThat(deepLinkIntent.getAction(), is(TEST_DEEP_LINK_ACTION));
+ }
+
+ @Test
+ public void testUriDeepLink() throws Throwable {
+ Uri deepLinkUri = Uri.parse("http://www.example.com/" + TEST_ARG_VALUE);
+ Intent intent = new Intent(Intent.ACTION_VIEW, deepLinkUri)
+ .setComponent(new ComponentName(mInstrumentation.getContext(),
+ getActivityClass()))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ BaseNavigationActivity activity = launchActivity(intent);
+ NavController navController = activity.getNavController();
+ navController.setGraph(R.navigation.nav_deep_link);
+
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.deep_link_test));
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navigator.mBackStack.size(), is(2));
+ assertThat(navigator.mBackStack.peekLast().second.getString(TEST_ARG), is(TEST_ARG_VALUE));
+
+ // Test that the deep link Intent was passed in alongside our args
+ Intent deepLinkIntent = navigator.mBackStack.peekLast().second
+ .getParcelable(NavController.KEY_DEEP_LINK_INTENT);
+ assertThat(deepLinkIntent, is(notNullValue(Intent.class)));
+ //noinspection ConstantConditions
+ assertThat(deepLinkIntent.getData(), is(deepLinkUri));
+ }
+
+ private BaseNavigationActivity launchActivity(Intent intent) throws Throwable {
+ BaseNavigationActivity activity = mActivityRule.launchActivity(intent);
+ mInstrumentation.waitForIdleSync();
+ NavController navController = activity.getNavController();
+ assertThat(navController, is(notNullValue(NavController.class)));
+ TestNavigator navigator = new TestNavigator();
+ navController.getNavigatorProvider().addNavigator(navigator);
+ return activity;
+ }
+
+ private BaseNavigationActivity launchDeepLink(@NavigationRes int graphId, @IdRes int destId,
+ Bundle args) throws Throwable {
+ TaskStackBuilder intents = new NavDeepLinkBuilder(mInstrumentation.getTargetContext())
+ .setGraph(graphId)
+ .setDestination(destId)
+ .setArguments(args)
+ .createTaskStackBuilder();
+ Intent intent = intents.editIntentAt(0);
+ intent.setAction(TEST_DEEP_LINK_ACTION);
+
+ // Now launch the deeplink Intent
+ BaseNavigationActivity deeplinkActivity = launchActivity(intent);
+ NavController navController = deeplinkActivity.getNavController();
+ navController.setGraph(graphId);
+
+ return deeplinkActivity;
+ }
+}
diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/DynamicNavControllerTest.java b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/DynamicNavControllerTest.java
new file mode 100644
index 0000000..7996cd3
--- /dev/null
+++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/DynamicNavControllerTest.java
@@ -0,0 +1,29 @@
+/*
+ * 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.navigation.fragment;
+
+import android.support.test.filters.SmallTest;
+
+import androidx.navigation.fragment.test.DynamicNavigationActivity;
+
+@SmallTest
+public class DynamicNavControllerTest extends BaseNavControllerTest<DynamicNavigationActivity> {
+ @Override
+ protected Class<DynamicNavigationActivity> getActivityClass() {
+ return DynamicNavigationActivity.class;
+ }
+}
diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/XmlNavControllerTest.java b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/XmlNavControllerTest.java
new file mode 100644
index 0000000..d5e5a6e
--- /dev/null
+++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/XmlNavControllerTest.java
@@ -0,0 +1,29 @@
+/*
+ * 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.navigation.fragment;
+
+import android.support.test.filters.SmallTest;
+
+import androidx.navigation.fragment.test.XmlNavigationActivity;
+
+@SmallTest
+public class XmlNavControllerTest extends BaseNavControllerTest<XmlNavigationActivity> {
+ @Override
+ protected Class<XmlNavigationActivity> getActivityClass() {
+ return XmlNavigationActivity.class;
+ }
+}
diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/BaseNavigationActivity.java b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/BaseNavigationActivity.java
new file mode 100644
index 0000000..456b47a
--- /dev/null
+++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/BaseNavigationActivity.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 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.navigation.fragment.test;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.FragmentActivity;
+
+import androidx.navigation.NavController;
+import androidx.navigation.Navigation;
+
+/**
+ * Base Navigation Activity.
+ *
+ * <p>You must call {@link NavController#setGraph(int)}
+ * to set the appropriate graph for your test.</p>
+ */
+public abstract class BaseNavigationActivity extends FragmentActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @NonNull
+ public NavController getNavController() {
+ return Navigation.getNavController(this, R.id.nav_host);
+ }
+}
diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/DynamicNavigationActivity.java b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/DynamicNavigationActivity.java
new file mode 100644
index 0000000..b1be2c3
--- /dev/null
+++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/DynamicNavigationActivity.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 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.navigation.fragment.test;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+
+/**
+ * Test Navigation Activity that dynamically adds the {@link NavHostFragment}.
+ *
+ * <p>You must call {@link NavController#setGraph(int)}
+ * to set the appropriate graph for your test.</p>
+ */
+public class DynamicNavigationActivity extends BaseNavigationActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.dynamic_navigation_activity);
+
+ if (savedInstanceState == null) {
+ final NavHostFragment finalHost = new NavHostFragment();
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.nav_host, finalHost)
+ .setPrimaryNavigationFragment(finalHost)
+ .commit();
+ }
+ }
+}
diff --git a/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/XmlNavigationActivity.java b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/XmlNavigationActivity.java
new file mode 100644
index 0000000..c6a2575
--- /dev/null
+++ b/navigation/fragment/src/androidTest/java/androidx/navigation/fragment/test/XmlNavigationActivity.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 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.navigation.fragment.test;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+
+import androidx.navigation.NavController;
+import androidx.navigation.fragment.NavHostFragment;
+
+/**
+ * Test Navigation Activity that adds the {@link NavHostFragment} in XML.
+ *
+ * <p>You must call {@link NavController#setGraph(int)}
+ * to set the appropriate graph for your test.</p>
+ */
+public class XmlNavigationActivity extends BaseNavigationActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.navigation_activity);
+ }
+}
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/fragment/src/androidTest/res/layout/dynamic_navigation_activity.xml
similarity index 78%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/fragment/src/androidTest/res/layout/dynamic_navigation_activity.xml
index 660dbcd..df82bf4 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/fragment/src/androidTest/res/layout/dynamic_navigation_activity.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright 2017 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.
@@ -14,6 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
+
+<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/nav_host"
+/>
\ No newline at end of file
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/fragment/src/androidTest/res/layout/navigation_activity.xml
similarity index 66%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/fragment/src/androidTest/res/layout/navigation_activity.xml
index 660dbcd..e6a924d 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/fragment/src/androidTest/res/layout/navigation_activity.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,13 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
+
+<fragment
xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/nav_host"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ app:defaultNavHost="true"
+/>
\ No newline at end of file
diff --git a/navigation/fragment/src/androidTest/res/navigation/nav_deep_link.xml b/navigation/fragment/src/androidTest/res/navigation/nav_deep_link.xml
new file mode 100644
index 0000000..22c2ec3
--- /dev/null
+++ b/navigation/fragment/src/androidTest/res/navigation/nav_deep_link.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:startDestination="@+id/start_test">
+
+ <test android:id="@+id/start_test" />
+
+ <test android:id="@+id/deep_link_test">
+ <deepLink app:uri="www.example.com/{test}" />
+ </test>
+</navigation>
diff --git a/car/res/drawable/car_button_ripple_background_day.xml b/navigation/fragment/src/main/AndroidManifest.xml
similarity index 76%
copy from car/res/drawable/car_button_ripple_background_day.xml
copy to navigation/fragment/src/main/AndroidManifest.xml
index 16b1d0c..7b28b7d 100644
--- a/car/res/drawable/car_button_ripple_background_day.xml
+++ b/navigation/fragment/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_dark" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.fragment">
+
+</manifest>
diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.java
new file mode 100644
index 0000000..971a589
--- /dev/null
+++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/FragmentNavigator.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2017 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.navigation.fragment;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.util.AttributeSet;
+
+import java.util.HashMap;
+
+import androidx.navigation.NavController;
+import androidx.navigation.NavDestination;
+import androidx.navigation.NavOptions;
+import androidx.navigation.Navigator;
+import androidx.navigation.NavigatorProvider;
+
+/**
+ * Navigator that navigates through {@link FragmentTransaction fragment transactions}. Every
+ * destination using this Navigator must set a valid Fragment class name with
+ * <code>android:name</code> or {@link Destination#setFragmentClass}.
+ */
+@Navigator.Name("fragment")
+public class FragmentNavigator extends Navigator<FragmentNavigator.Destination> {
+ private Context mContext;
+ private FragmentManager mFragmentManager;
+ private int mContainerId;
+ private int mBackStackCount;
+
+ private final FragmentManager.OnBackStackChangedListener mOnBackStackChangedListener =
+ new FragmentManager.OnBackStackChangedListener() {
+ @Override
+ public void onBackStackChanged() {
+ int newCount = mFragmentManager.getBackStackEntryCount();
+ int backStackEffect;
+ if (newCount < mBackStackCount) {
+ backStackEffect = BACK_STACK_DESTINATION_POPPED;
+ } else if (newCount > mBackStackCount) {
+ backStackEffect = BACK_STACK_DESTINATION_ADDED;
+ } else {
+ backStackEffect = BACK_STACK_UNCHANGED;
+ }
+ mBackStackCount = newCount;
+
+ int destId = 0;
+ StateFragment state = getState();
+ if (state != null) {
+ destId = state.mCurrentDestId;
+ }
+ dispatchOnNavigatorNavigated(destId, backStackEffect);
+ }
+ };
+
+ public FragmentNavigator(@NonNull Context context, @NonNull FragmentManager manager,
+ int containerId) {
+ mContext = context;
+ mFragmentManager = manager;
+ mContainerId = containerId;
+
+ mBackStackCount = mFragmentManager.getBackStackEntryCount();
+ mFragmentManager.addOnBackStackChangedListener(mOnBackStackChangedListener);
+ }
+
+ @Override
+ public boolean popBackStack() {
+ return mFragmentManager.popBackStackImmediate();
+ }
+
+ @NonNull
+ @Override
+ public Destination createDestination() {
+ return new Destination(this);
+ }
+
+ @NonNull
+ private String getBackStackName(@IdRes int destinationId) {
+ // This gives us the resource name if it exists,
+ // or just the destinationId if it doesn't exist
+ try {
+ return mContext.getResources().getResourceName(destinationId);
+ } catch (Resources.NotFoundException e) {
+ return Integer.toString(destinationId);
+ }
+ }
+
+ @Override
+ public void navigate(@NonNull Destination destination, @Nullable Bundle args,
+ @Nullable NavOptions navOptions) {
+ final Fragment frag = destination.createFragment(args);
+ final FragmentTransaction ft = mFragmentManager.beginTransaction();
+
+ int enterAnim = navOptions != null ? navOptions.getEnterAnim() : -1;
+ int exitAnim = navOptions != null ? navOptions.getExitAnim() : -1;
+ int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : -1;
+ int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : -1;
+ if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
+ enterAnim = enterAnim != -1 ? enterAnim : 0;
+ exitAnim = exitAnim != -1 ? exitAnim : 0;
+ popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
+ popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
+ ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
+ }
+
+ ft.replace(mContainerId, frag);
+
+ final StateFragment oldState = getState();
+ if (oldState != null) {
+ ft.remove(oldState);
+ }
+
+ final @IdRes int destId = destination.getId();
+ final StateFragment newState = new StateFragment();
+ newState.mCurrentDestId = destId;
+ ft.add(newState, StateFragment.FRAGMENT_TAG);
+
+ final boolean initialNavigation = mFragmentManager.getFragments().isEmpty();
+ final boolean isClearTask = navOptions != null && navOptions.shouldClearTask();
+ // TODO Build first class singleTop behavior for fragments
+ final boolean isSingleTopReplacement = navOptions != null && oldState != null
+ && navOptions.shouldLaunchSingleTop()
+ && oldState.mCurrentDestId == destId;
+ if (!initialNavigation && !isClearTask && !isSingleTopReplacement) {
+ ft.addToBackStack(getBackStackName(destId));
+ } else {
+ ft.runOnCommit(new Runnable() {
+ @Override
+ public void run() {
+ dispatchOnNavigatorNavigated(destId, isSingleTopReplacement
+ ? BACK_STACK_UNCHANGED
+ : BACK_STACK_DESTINATION_ADDED);
+ }
+ });
+ }
+ ft.commit();
+ mFragmentManager.executePendingTransactions();
+ }
+
+ private StateFragment getState() {
+ return (StateFragment) mFragmentManager.findFragmentByTag(StateFragment.FRAGMENT_TAG);
+ }
+
+ /**
+ * NavDestination specific to {@link FragmentNavigator}
+ */
+ public static class Destination extends NavDestination {
+ private static final HashMap<String, Class<? extends Fragment>> sFragmentClasses =
+ new HashMap<>();
+
+ private Class<? extends Fragment> mFragmentClass;
+
+ /**
+ * Construct a new fragment destination. This destination is not valid until you set the
+ * Fragment via {@link #setFragmentClass(Class)}.
+ *
+ * @param navigatorProvider The {@link NavController} which this destination
+ * will be associated with.
+ */
+ public Destination(@NonNull NavigatorProvider navigatorProvider) {
+ this(navigatorProvider.getNavigator(FragmentNavigator.class));
+ }
+
+ /**
+ * Construct a new fragment destination. This destination is not valid until you set the
+ * Fragment via {@link #setFragmentClass(Class)}.
+ *
+ * @param fragmentNavigator The {@link FragmentNavigator} which this destination
+ * will be associated with. Generally retrieved via a
+ * {@link NavController}'s
+ * {@link NavigatorProvider#getNavigator(Class)} method.
+ */
+ public Destination(@NonNull Navigator<? extends Destination> fragmentNavigator) {
+ super(fragmentNavigator);
+ }
+
+ @Override
+ public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
+ super.onInflate(context, attrs);
+ TypedArray a = context.getResources().obtainAttributes(attrs,
+ R.styleable.FragmentNavigator);
+ setFragmentClass(getFragmentClassByName(context, a.getString(
+ R.styleable.FragmentNavigator_android_name)));
+ a.recycle();
+ }
+
+ private Class<? extends Fragment> getFragmentClassByName(Context context, String name) {
+ if (name != null && name.charAt(0) == '.') {
+ name = context.getPackageName() + name;
+ }
+ Class<? extends Fragment> clazz = sFragmentClasses.get(name);
+ if (clazz == null) {
+ try {
+ clazz = (Class<? extends Fragment>) Class.forName(name, true,
+ context.getClassLoader());
+ sFragmentClasses.put(name, clazz);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return clazz;
+ }
+
+ /**
+ * Set the Fragment associated with this destination
+ * @param clazz The class name of the Fragment to show when you navigate to this
+ * destination
+ * @return this {@link Destination}
+ */
+ public Destination setFragmentClass(Class<? extends Fragment> clazz) {
+ mFragmentClass = clazz;
+ return this;
+ }
+
+ /**
+ * Gets the Fragment associated with this destination
+ * @return
+ */
+ public Class<? extends Fragment> getFragmentClass() {
+ return mFragmentClass;
+ }
+
+ /**
+ * Create a new instance of the {@link Fragment} associated with this destination.
+ * @param args optional args to set on the new Fragment
+ * @return an instance of the {@link #getFragmentClass() Fragment class} associated
+ * with this destination
+ */
+ @SuppressWarnings("ClassNewInstance")
+ public Fragment createFragment(@Nullable Bundle args) {
+ Class<? extends Fragment> clazz = getFragmentClass();
+ if (clazz == null) {
+ throw new IllegalStateException("fragment class not set");
+ }
+
+ Fragment f;
+ try {
+ f = clazz.newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ if (args != null) {
+ f.setArguments(args);
+ }
+ return f;
+ }
+ }
+
+ /**
+ * An internal fragment used by FragmentNavigator to track additional navigation state.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static class StateFragment extends Fragment {
+ static final String FRAGMENT_TAG = "android-support-nav:FragmentNavigator.StateFragment";
+
+ private static final String KEY_CURRENT_DEST_ID = "currentDestId";
+
+ int mCurrentDestId;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mCurrentDestId = savedInstanceState.getInt(KEY_CURRENT_DEST_ID);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(KEY_CURRENT_DEST_ID, mCurrentDestId);
+ }
+ }
+}
diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
new file mode 100644
index 0000000..8f6dfe5
--- /dev/null
+++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2017 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.navigation.fragment;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.support.annotation.NavigationRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.navigation.NavController;
+import androidx.navigation.NavGraph;
+import androidx.navigation.NavHost;
+import androidx.navigation.Navigation;
+import androidx.navigation.Navigator;
+
+/**
+ * NavHostFragment provides an area within your layout for self-contained navigation to occur.
+ *
+ * <p>NavHostFragment is intended to be used as the content area within a layout resource
+ * defining your app's chrome around it, e.g.:</p>
+ *
+ * <pre class="prettyprint">
+ * <android.support.v4.widget.DrawerLayout
+ * xmlns:android="http://schemas.android.com/apk/res/android"
+ * xmlns:app="http://schemas.android.com/apk/res-auto"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent">
+ * <fragment
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent"
+ * android:id="@+id/my_nav_host_fragment"
+ * android:name="androidx.navigation.fragment.NavHostFragment"
+ * app:navGraph="@xml/nav_sample"
+ * app:defaultNavHost="true" />
+ * <android.support.design.widget.NavigationView
+ * android:layout_width="wrap_content"
+ * android:layout_height="match_parent"
+ * android:layout_gravity="start"/>
+ * </android.support.v4.widget.DrawerLayout>
+ * </pre>
+ *
+ * <p>Each NavHostFragment has a {@link NavController} that defines valid navigation within
+ * the navigation host. This includes the {@link NavGraph navigation graph} as well as navigation
+ * state such as current location and back stack that will be saved and restored along with the
+ * NavHostFragment itself.</p>
+ *
+ * <p>NavHostFragments register their navigation controller at the root of their view subtree
+ * such that any descendant can obtain the controller instance through the {@link Navigation}
+ * helper class's methods such as {@link Navigation#findNavController(View)}. View event listener
+ * implementations such as {@link android.view.View.OnClickListener} within navigation destination
+ * fragments can use these helpers to navigate based on user interaction without creating a tight
+ * coupling to the navigation host.</p>
+ */
+public class NavHostFragment extends Fragment implements NavHost {
+ private static final String KEY_GRAPH_ID = "android-support-nav:fragment:graphId";
+ private static final String KEY_NAV_CONTROLLER_STATE =
+ "android-support-nav:fragment:navControllerState";
+ private static final String KEY_DEFAULT_NAV_HOST = "android-support-nav:fragment:defaultHost";
+
+ /**
+ * Find a {@link NavController} given a local {@link Fragment}.
+ *
+ * <p>This method will locate the {@link NavController} associated with this Fragment,
+ * looking first for a {@link NavHostFragment} along the given Fragment's parent chain.
+ * If a {@link NavController} is not found, this method will look for one along this
+ * Fragment's {@link Fragment#getView() view hierarchy} as specified by
+ * {@link Navigation#findNavController(View)}.</p>
+ *
+ * @param fragment the locally scoped Fragment for navigation
+ * @return the locally scoped {@link NavController} for navigating from this {@link Fragment}
+ */
+ @Nullable
+ public static NavController findNavController(@Nullable Fragment fragment) {
+ if (fragment == null) {
+ return null;
+ }
+
+ Fragment findFragment = fragment;
+ while (findFragment != null) {
+ if (findFragment instanceof NavHostFragment) {
+ return ((NavHostFragment) findFragment).getNavController();
+ }
+ Fragment primaryNavFragment = findFragment.getFragmentManager()
+ .getPrimaryNavigationFragment();
+ if (primaryNavFragment instanceof NavHostFragment) {
+ return ((NavHostFragment) primaryNavFragment).getNavController();
+ }
+ findFragment = findFragment.getParentFragment();
+ }
+
+ // Try looking for one associated with the view instead, if applicable
+ View view = fragment.getView();
+ return view != null ? Navigation.findNavController(view) : null;
+ }
+
+ /**
+ * Gets the {@link NavController} given a local {@link Fragment}.
+ *
+ * <p>This method will locate the {@link NavController} associated with this Fragment,
+ * looking first for a {@link NavHostFragment} along the given Fragment's parent chain.
+ * If a {@link NavController} is not found, this method will look for one along this
+ * Fragment's {@link Fragment#getView() view hierarchy} as specified by
+ * {@link Navigation#findNavController(View)}.</p>
+ *
+ * @param fragment the locally scoped Fragment for navigation
+ * @return the locally scoped {@link NavController} for navigating from this {@link Fragment}
+ * @throws IllegalStateException if the given Fragment does not correspond with a
+ * {@link NavHost} or is not within a NavHost.
+ */
+ public static NavController getNavController(@Nullable Fragment fragment) {
+ NavController navController = findNavController(fragment);
+ if (navController == null) {
+ throw new IllegalStateException("Fragment " + fragment
+ + " does not have a NavController set");
+ }
+ return navController;
+ }
+
+ private NavController mNavController;
+
+ // State that will be saved and restored
+ private boolean mDefaultNavHost;
+
+ /**
+ * Create a new NavHostFragment instance with an inflated {@link NavGraph} resource.
+ *
+ * @param graphResId resource id of the navigation graph to inflate
+ * @return a new NavHostFragment instance
+ */
+ public static NavHostFragment create(@NavigationRes int graphResId) {
+ Bundle b = null;
+ if (graphResId != 0) {
+ b = new Bundle();
+ b.putInt(KEY_GRAPH_ID, graphResId);
+ }
+
+ final NavHostFragment result = new NavHostFragment();
+ if (b != null) {
+ result.setArguments(b);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the {@link NavController navigation controller} for this navigation host.
+ * This method will return null until this host fragment's {@link #onCreate(Bundle)}
+ * has been called and it has had an opportunity to restore from a previous instance state.
+ *
+ * @return this host's navigation controller
+ * @throws IllegalStateException if called before {@link #onCreate(Bundle)}
+ */
+ @NonNull
+ @Override
+ public NavController getNavController() {
+ if (mNavController == null) {
+ throw new IllegalStateException("NavController is not available before onCreate()");
+ }
+ return mNavController;
+ }
+
+ /**
+ * Set a {@link NavGraph} for this navigation host's {@link NavController} by resource id.
+ * The existing graph will be replaced.
+ *
+ * @param graphResId resource id of the navigation graph to inflate
+ */
+ public void setGraph(@NavigationRes int graphResId) {
+ if (mNavController == null) {
+ Bundle args = getArguments();
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putInt(KEY_GRAPH_ID, graphResId);
+ setArguments(args);
+ } else {
+ mNavController.setGraph(graphResId);
+ }
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ // TODO This feature should probably be a first-class feature of the Fragment system,
+ // but it can stay here until we can add the necessary attr resources to
+ // the fragment lib.
+ if (mDefaultNavHost) {
+ getFragmentManager().beginTransaction().setPrimaryNavigationFragment(this).commit();
+ }
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final Context context = getContext();
+
+ mNavController = new NavController(context);
+ mNavController.getNavigatorProvider().addNavigator(createFragmentNavigator());
+
+ Bundle navState = null;
+ if (savedInstanceState != null) {
+ navState = savedInstanceState.getBundle(KEY_NAV_CONTROLLER_STATE);
+ if (savedInstanceState.getBoolean(KEY_DEFAULT_NAV_HOST, false)) {
+ mDefaultNavHost = true;
+ getFragmentManager().beginTransaction().setPrimaryNavigationFragment(this).commit();
+ }
+ }
+
+ if (navState != null) {
+ // Navigation controller state overrides arguments
+ mNavController.restoreState(navState);
+ } else {
+ final Bundle args = getArguments();
+ final int graphId = args != null ? args.getInt(KEY_GRAPH_ID) : 0;
+ if (graphId != 0) {
+ mNavController.setGraph(graphId);
+ } else {
+ mNavController.setMetadataGraph();
+ }
+ }
+ }
+
+ /**
+ * Create the FragmentNavigator that this NavHostFragment will use. By default, this uses
+ * {@link FragmentNavigator}, which replaces the entire contents of the NavHostFragment.
+ * <p>
+ * This is only called once in {@link #onCreate(Bundle)} and should not be called directly by
+ * subclasses.
+ * @return a new instance of a FragmentNavigator
+ */
+ @NonNull
+ protected Navigator<? extends FragmentNavigator.Destination> createFragmentNavigator() {
+ return new FragmentNavigator(getContext(), getChildFragmentManager(), getId());
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ FrameLayout frameLayout = new FrameLayout(inflater.getContext());
+ // When added via XML, this has no effect (since this FrameLayout is given the ID
+ // automatically), but this ensures that the View exists as part of this Fragment's View
+ // hierarchy in cases where the NavHostFragment is added programmatically as is required
+ // for child fragment transactions
+ frameLayout.setId(getId());
+ return frameLayout;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ if (!(view instanceof ViewGroup)) {
+ throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
+ }
+ // When added via XML, the parent is null and our view is the root of the NavHostFragment
+ // but when added programmatically, we need to set the NavController on the parent - i.e.,
+ // the View that has the ID matching this NavHostFragment.
+ View rootView = view.getParent() != null ? (View) view.getParent() : view;
+ Navigation.setViewNavController(rootView, mNavController);
+ }
+
+ @Override
+ public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
+ super.onInflate(context, attrs, savedInstanceState);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NavHostFragment);
+ final int graphId = a.getResourceId(R.styleable.NavHostFragment_navGraph, 0);
+ final boolean defaultHost = a.getBoolean(R.styleable.NavHostFragment_defaultNavHost, false);
+
+ if (graphId != 0) {
+ setGraph(graphId);
+ }
+ if (defaultHost) {
+ mDefaultNavHost = true;
+ if (isAdded()) {
+ getFragmentManager().beginTransaction().setPrimaryNavigationFragment(this).commit();
+ }
+ }
+ a.recycle();
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ Bundle navState = mNavController.saveState();
+ if (navState != null) {
+ outState.putBundle(KEY_NAV_CONTROLLER_STATE, navState);
+ }
+ if (mDefaultNavHost) {
+ outState.putBoolean(KEY_DEFAULT_NAV_HOST, true);
+ }
+ }
+}
diff --git a/navigation/fragment/src/main/java/androidx/navigation/fragment/package-info.java b/navigation/fragment/src/main/java/androidx/navigation/fragment/package-info.java
new file mode 100644
index 0000000..0ff94ab
--- /dev/null
+++ b/navigation/fragment/src/main/java/androidx/navigation/fragment/package-info.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+/**
+ * The {@link androidx.navigation.fragment.NavHostFragment} provides a
+ * {@link androidx.navigation.NavHost} suitable for using
+ * {@link android.support.v4.app.Fragment Fragments} as destinations in your navigation graphs via
+ * <fragment%gt; elements. Navigating to a Fragment will replace the contents of the
+ * NavHostFragment.
+ * <p>
+ * Below is a minimal implementation.
+ * <pre class="prettyprint">
+ * // File: res/xml/main_navigation.xml
+ * <navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ * xmlns:app="http://schemas.android.com/apk/res-auto"
+ * app:startDestination="{@literal @}+id/home_fragment">
+ * <fragment android:id="{@literal @}+id/home_fragment"
+ * android:name="com.example.HomeFragment">
+ * <action android:id="{@literal @}+id/details"
+ * app:destination="{@literal @}+id/details_fragment" />
+ * <fragment />
+ * <fragment android:id="{@literal @}+id/details_fragment"
+ * android:name="com.example.DetailsFragment"/>
+ * <navigation />
+ *
+ * // File: activity_main.xml
+ * <fragment
+ * android:id="{@literal @}+id/my_nav_host_fragment"
+ * android:layout_width="match_parent"
+ * android:layout_height="match_parent"
+ * android:name="androidx.navigation.fragment.NavHostFragment"
+ * app:navGraph="{@literal @}xml/main_navigation"
+ * app:defaultNavHost="true"
+ * />
+ *
+ * // File: HomeFragment.java
+ * public void onViewCreated(View view, {@literal @}Nullable Bundle savedInstanceState) {
+ * // For example purposes, assume our layout created in onCreateView has a Button
+ * // that should navigate the user to a destination
+ * Button button = view.findViewById(R.id.view_details);
+ *
+ * // Retrieve the NavController from any Fragment created by a NavHostFragment by passing in
+ * // this
+ * final NavController navController = NavHostFragment.findNavController(this);
+ * // Alternatively, retrieve the NavController from any View within the NavHostFragment
+ * final NavController viewNavController = Navigation.findNavController(button);
+ *
+ * // And set the listener
+ * button.setOnClickListener(() -%gt; navController.navigate(R.id.details));
+ *
+ * // Or use the convenience method in Navigation to combine all of the previous steps
+ * button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.details));
+ * }
+ * </pre>
+ */
+package androidx.navigation.fragment;
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/fragment/src/main/res/values/attrs.xml
similarity index 62%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/fragment/src/main/res/values/attrs.xml
index 660dbcd..a5e9f55 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/fragment/src/main/res/values/attrs.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ 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.
@@ -14,6 +14,13 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+<resources>
+ <declare-styleable name="FragmentNavigator">
+ <attr name="android:name" />
+ </declare-styleable>
+
+ <declare-styleable name="NavHostFragment">
+ <attr name="navGraph" format="reference" />
+ <attr name="defaultNavHost" format="boolean" />
+ </declare-styleable>
+</resources>
diff --git a/navigation/integration-tests/safeargs-testapp/build.gradle b/navigation/integration-tests/safeargs-testapp/build.gradle
new file mode 100644
index 0000000..adc5d0c
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/build.gradle
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+import static android.support.dependencies.DependenciesKt.*
+import android.support.LibraryVersions
+import android.support.LibraryGroups
+
+buildscript {
+ ext.supportRootFolder = new File("../../../")
+ apply from: "${supportRootFolder}/buildSrc/repos.gradle"
+ apply from: "${supportRootFolder}/buildSrc/init.gradle"
+ apply from: "${supportRootFolder}/buildSrc/build_dependencies.gradle"
+ init.setSdkInLocalPropertiesFile()
+ repos.addMavenRepositories(repositories)
+
+ repositories {
+ maven {
+ url "$buildDir/localMaven/"
+ }
+ }
+ dependencies {
+ classpath build_libs.gradle
+ classpath "${LibraryGroups.NAVIGATION}:safe-args-gradle-plugin:${LibraryVersions.NAVIGATION}"
+ }
+}
+
+plugins {
+ id("SupportAndroidTestAppPlugin")
+}
+
+apply plugin: 'androidx.navigation.safeargs'
+apply from: "${supportRootFolder}/buildSrc/repos.gradle"
+repos.addMavenRepositories(repositories)
+repositories {
+ maven {
+ url "$buildDir/localMaven/"
+ }
+}
+
+android {
+ flavorDimensions "mode"
+ productFlavors {
+ foo {
+ dimension "mode"
+ applicationIdSuffix ".foo"
+ }
+ notfoo {
+ dimension "mode"
+ }
+ }
+}
+
+dependencies {
+ implementation "${LibraryGroups.NAVIGATION}:runtime:${LibraryVersions.NAVIGATION}"
+ testCompile(JUNIT)
+ testCompile(MOCKITO_CORE)
+}
\ No newline at end of file
diff --git a/navigation/integration-tests/safeargs-testapp/buildSrc/build.gradle b/navigation/integration-tests/safeargs-testapp/buildSrc/build.gradle
new file mode 100644
index 0000000..0941f4d
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/buildSrc/build.gradle
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+ext.supportRootFolder = new File("../../../..")
+apply from: "$supportRootFolder/buildSrc/repos.gradle"
+repos.addMavenRepositories(repositories)
+
+ext.runningInBuildServer = System.env.DIST_DIR != null && System.env.OUT_DIR != null
+ext.repoDir = runningInBuildServer ? new File(System.env.OUT_DIR + '/gradle/frameworks/support/build') :
+ new File("$supportRootFolder/../../out/host/gradle/frameworks/support/build")
+
+task createArchive(type: Exec) {
+ inputs.files(fileTree(dir: "$supportRootFolder/navigation",
+ includes: ['**/*.java', '**/*.kt'], exclude: 'integration-tests/*'))
+ outputs.dir(repoDir)
+ workingDir "$supportRootFolder/app-toolkit/"
+ commandLine "./gradlew", "createArchive"
+}
+
+task unzip(type: Copy, dependsOn: createArchive) {
+ from "$repoDir/support_repo"
+ into "../build/localMaven/"
+}
+
+apply plugin: 'java'
+dependencies {
+ compile project(":moar-buildSrc")
+}
+
+tasks["build"].dependsOn unzip
\ No newline at end of file
diff --git a/navigation/integration-tests/safeargs-testapp/buildSrc/settings.gradle b/navigation/integration-tests/safeargs-testapp/buildSrc/settings.gradle
new file mode 100644
index 0000000..ae61ad6
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/buildSrc/settings.gradle
@@ -0,0 +1,2 @@
+include ':moar-buildSrc'
+project(':moar-buildSrc').projectDir = new File(rootDir, "../../../../buildSrc/")
\ No newline at end of file
diff --git a/navigation/integration-tests/safeargs-testapp/gradle b/navigation/integration-tests/safeargs-testapp/gradle
new file mode 120000
index 0000000..84b694e
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/gradle
@@ -0,0 +1 @@
+../../../gradle
\ No newline at end of file
diff --git a/navigation/integration-tests/safeargs-testapp/gradle.properties b/navigation/integration-tests/safeargs-testapp/gradle.properties
new file mode 120000
index 0000000..591a8b3
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/gradle.properties
@@ -0,0 +1 @@
+../../../gradle.properties
\ No newline at end of file
diff --git a/navigation/integration-tests/safeargs-testapp/gradlew b/navigation/integration-tests/safeargs-testapp/gradlew
new file mode 120000
index 0000000..ab9334b
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/gradlew
@@ -0,0 +1 @@
+../../../gradlew
\ No newline at end of file
diff --git a/navigation/integration-tests/safeargs-testapp/src/foo/res/navigation/nav_test.xml b/navigation/integration-tests/safeargs-testapp/src/foo/res/navigation/nav_test.xml
new file mode 100644
index 0000000..70debb7
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/src/foo/res/navigation/nav_test.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ app:startDestination="@+id/first_screen">
+ <fragment android:id="@+id/first_screen"
+ android:name=".MainFragment">
+ <action android:id="@+id/foo" app:destination="@+id/foo_fragment">
+ <argument android:name="arg"/>
+ </action>
+ </fragment>
+ <fragment android:id="@+id/foo_fragment"
+ android:name="android.arch.navigation.FooFragment">
+ <argument android:name="myarg2" />
+ </fragment>
+ <fragment android:id="@+id/next_fragment"/>
+</navigation>
\ No newline at end of file
diff --git a/navigation/integration-tests/safeargs-testapp/src/main/AndroidManifest.xml b/navigation/integration-tests/safeargs-testapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c97e190
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="android.arch.navigation.safeargs.testapp">
+
+ <application
+ android:allowBackup="true"
+ android:supportsRtl="true"
+ tools:ignore="AllowBackup,GoogleAppIndexingWarning,MissingApplicationIcon">
+ </application>
+
+</manifest>
diff --git a/navigation/integration-tests/safeargs-testapp/src/main/java/android/arch/navigation/Foo.java b/navigation/integration-tests/safeargs-testapp/src/main/java/android/arch/navigation/Foo.java
new file mode 100644
index 0000000..a794771
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/src/main/java/android/arch/navigation/Foo.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 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 android.arch.navigation;
+
+/**
+ * Dummy file, because android gradle plugin requires not empty sources
+ */
+public class Foo {
+}
diff --git a/navigation/integration-tests/safeargs-testapp/src/main/res/navigation/nav_test.xml b/navigation/integration-tests/safeargs-testapp/src/main/res/navigation/nav_test.xml
new file mode 100644
index 0000000..4d4be46
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/src/main/res/navigation/nav_test.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ app:startDestination="@+id/first_screen">
+ <fragment android:id="@+id/first_screen"
+ android:name="android.arch.navigation.testapp.MainFragment">
+ <action android:id="@+id/next" app:destination="@+id/next_fragment">
+ <argument android:name="myarg2"/>
+ <argument android:name="randomArgument"/>
+ </action>
+ </fragment>
+ <fragment android:id="@+id/next_fragment"
+ android:name="android.arch.navigation.testapp.NextFragment">
+ <argument android:name="myarg2" />
+ </fragment>
+</navigation>
\ No newline at end of file
diff --git a/navigation/integration-tests/safeargs-testapp/src/test/java/android/arch/navigation/integration/MainDestinationTest.java b/navigation/integration-tests/safeargs-testapp/src/test/java/android/arch/navigation/integration/MainDestinationTest.java
new file mode 100644
index 0000000..0d3bf09
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/src/test/java/android/arch/navigation/integration/MainDestinationTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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 android.arch.navigation.integration;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.navigation.safeargs.testapp.R;
+import android.arch.navigation.testapp.MainFragmentDirections;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class MainDestinationTest {
+
+ @Test
+ public void destinationsTest() {
+ // test that we correctly generated R class references, so it compiles
+ MainFragmentDirections.Next directions = MainFragmentDirections.next("foo", "some");
+ assertThat(directions.getDestinationId(), is(R.id.next_fragment));
+ }
+}
diff --git a/navigation/integration-tests/safeargs-testapp/src/testFoo/java/android/arch/navigation/integration/flavor/foo/FlavorDestinationTest.java b/navigation/integration-tests/safeargs-testapp/src/testFoo/java/android/arch/navigation/integration/flavor/foo/FlavorDestinationTest.java
new file mode 100644
index 0000000..3229c58
--- /dev/null
+++ b/navigation/integration-tests/safeargs-testapp/src/testFoo/java/android/arch/navigation/integration/flavor/foo/FlavorDestinationTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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 android.arch.navigation.integration.flavor.foo;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.navigation.safeargs.testapp.R;
+import android.arch.navigation.safeargs.testapp.foo.MainFragmentDirections;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * this is teeeeeeeeeest
+ */
+@RunWith(JUnit4.class)
+public class FlavorDestinationTest {
+
+ /**
+ * teeeest
+ */
+ @Test
+ public void destinationsTest() {
+ MainFragmentDirections.Foo directions = MainFragmentDirections.foo("foo", "some");
+ assertThat(directions.getDestinationId(), is(R.id.foo_fragment));
+ }
+}
diff --git a/navigation/integration-tests/testapp/.gitignore b/navigation/integration-tests/testapp/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/navigation/integration-tests/testapp/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/navigation/integration-tests/testapp/build.gradle b/navigation/integration-tests/testapp/build.gradle
new file mode 100644
index 0000000..46bec7c
--- /dev/null
+++ b/navigation/integration-tests/testapp/build.gradle
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("SupportAndroidTestAppPlugin")
+}
+
+dependencies {
+ implementation(project(":navigation:navigation-fragment"))
+ implementation(project(":navigation:navigation-ui"))
+}
+
+tasks['check'].dependsOn(tasks['connectedCheck'])
diff --git a/navigation/integration-tests/testapp/src/main/AndroidManifest.xml b/navigation/integration-tests/testapp/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..cb53008
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="androidx.navigation.testapp">
+
+ <application
+ android:allowBackup="true"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme"
+ tools:ignore="AllowBackup,GoogleAppIndexingWarning,MissingApplicationIcon">
+ <activity android:name=".NavigationActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <!-- Handle the 'More information' link on www.example.com -->
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+ <data android:scheme="https"/>
+ <data android:scheme="http"/>
+ <data android:host="www.iana.org"/>
+ <data android:pathPrefix="/domains/"/>
+ </intent-filter>
+ </activity>
+ <activity
+ android:name=".HelpActivity"
+ android:label="@string/help"/>
+
+ <receiver android:name=".DeepLinkAppWidgetProvider" >
+ <intent-filter>
+ <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
+ </intent-filter>
+ <meta-data android:name="android.appwidget.provider"
+ android:resource="@xml/deep_link_appwidget_info" />
+ </receiver>
+ </application>
+
+</manifest>
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/AndroidFragment.java b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/AndroidFragment.java
new file mode 100644
index 0000000..1c8e768
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/AndroidFragment.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2017 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.navigation.testapp;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.NotificationCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import androidx.navigation.Navigation;
+
+/**
+ * Fragment used to show how to deep link to a destination
+ */
+public class AndroidFragment extends Fragment {
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.android_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ TextView tv = view.findViewById(R.id.text);
+ tv.setText(getArguments().getString("myarg"));
+
+ Button b = view.findViewById(R.id.send_notification);
+ b.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ EditText editArgs = view.findViewById(R.id.edit_args);
+ Bundle args = new Bundle();
+ args.putString("myarg", editArgs.getText().toString());
+ PendingIntent deeplink = Navigation.getNavController(v).createDeepLink()
+ .setDestination(R.id.android)
+ .setArguments(args)
+ .createPendingIntent();
+ NotificationManager notificationManager = (NotificationManager)
+ getContext().getSystemService(Context.NOTIFICATION_SERVICE);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notificationManager.createNotificationChannel(new NotificationChannel(
+ "deeplink", "Deep Links", NotificationManager.IMPORTANCE_HIGH));
+ }
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(
+ getContext(), "deeplink")
+ .setContentTitle("Navigation")
+ .setContentText("Deep link to Android")
+ .setSmallIcon(R.drawable.ic_android)
+ .setContentIntent(deeplink)
+ .setAutoCancel(true);
+ notificationManager.notify(0, builder.build());
+ }
+ });
+ }
+}
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/DeepLinkAppWidgetProvider.java b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/DeepLinkAppWidgetProvider.java
new file mode 100644
index 0000000..7db6516
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/DeepLinkAppWidgetProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 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.navigation.testapp;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+
+import androidx.navigation.NavDeepLinkBuilder;
+
+/**
+ * App Widget that deep links you to the {@link AndroidFragment}.
+ */
+public class DeepLinkAppWidgetProvider extends AppWidgetProvider {
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+ RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
+ R.layout.deep_link_appwidget);
+
+ Bundle args = new Bundle();
+ args.putString("myarg", "From Widget");
+ PendingIntent pendingIntent = new NavDeepLinkBuilder(context)
+ .setGraph(R.navigation.nav_main)
+ .setDestination(R.id.android)
+ .setArguments(args)
+ .createPendingIntent();
+
+ remoteViews.setOnClickPendingIntent(R.id.deep_link, pendingIntent);
+ appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);
+ }
+}
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/HelpActivity.java b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/HelpActivity.java
new file mode 100644
index 0000000..fc0e1ef
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/HelpActivity.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 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.navigation.testapp;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.widget.TextView;
+
+import androidx.navigation.NavOptions;
+
+/**
+ * Simple 'Help' activity that shows the data URI passed to it. In a real world app, it would
+ * load the chosen help article, etc.
+ */
+public class HelpActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_help);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ ((TextView) findViewById(R.id.data)).setText(getIntent().getData().toString());
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ finish();
+ NavOptions.applyPopAnimationsToPendingTransition(this);
+ return true;
+ }
+
+ @Override
+ public void onBackPressed() {
+ super.onBackPressed();
+ NavOptions.applyPopAnimationsToPendingTransition(this);
+ }
+}
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.java b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.java
new file mode 100644
index 0000000..2a4950e
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 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.navigation.testapp;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.navigation.Navigation;
+
+/**
+ * Fragment used to show how to navigate to another destination
+ */
+public class MainFragment extends Fragment {
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.main_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ TextView tv = (TextView) view.findViewById(R.id.text);
+ tv.setText(getArguments().getString("myarg"));
+
+ Button b = (Button) view.findViewById(R.id.next_button);
+ b.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next));
+ }
+}
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/NavigationActivity.java b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/NavigationActivity.java
new file mode 100644
index 0000000..039af32
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/NavigationActivity.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2017 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.navigation.testapp;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.design.widget.BottomNavigationView;
+import android.support.design.widget.NavigationView;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import androidx.navigation.NavController;
+import androidx.navigation.NavDestination;
+import androidx.navigation.Navigation;
+import androidx.navigation.fragment.NavHostFragment;
+import androidx.navigation.ui.NavigationUI;
+
+/**
+ * A simple activity demonstrating use of a NavHostFragment with a navigation drawer.
+ */
+public class NavigationActivity extends AppCompatActivity {
+ private DrawerLayout mDrawerLayout;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.navigation_activity);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ NavHostFragment host = (NavHostFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.my_nav_host_fragment);
+
+ if (host != null) {
+ NavController navController = host.getNavController();
+ mDrawerLayout = findViewById(R.id.drawer_layout);
+ NavigationUI.setupActionBarWithNavController(this, navController, mDrawerLayout);
+ NavigationView navigationView = findViewById(R.id.nav_view);
+ if (navigationView != null) {
+ NavigationUI.setupWithNavController(navigationView, navController);
+ }
+ BottomNavigationView bottomNavView = findViewById(R.id.bottom_nav_view);
+ if (bottomNavView != null) {
+ NavigationUI.setupWithNavController(bottomNavView, navController);
+ }
+ navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
+ @Override
+ public void onNavigated(NavController controller, NavDestination destination) {
+ String dest;
+ try {
+ dest = getResources().getResourceName(destination.getId());
+ } catch (Resources.NotFoundException e) {
+ dest = Integer.toString(destination.getId());
+ }
+ Toast.makeText(NavigationActivity.this, "Navigated to "
+ + dest,
+ Toast.LENGTH_SHORT).show();
+ Log.d("NavigationActivity", "Navigated to " + dest);
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ boolean retValue = super.onCreateOptionsMenu(menu);
+ NavigationView navigationView = findViewById(R.id.nav_view);
+ // The NavigationView already has these same navigation items, so we only add
+ // navigation items to the menu here if there isn't a NavigationView
+ if (navigationView == null) {
+ getMenuInflater().inflate(R.menu.menu_overflow, menu);
+ return true;
+ }
+ return retValue;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return NavigationUI.onNavDestinationSelected(
+ item, Navigation.getNavController(this, R.id.my_nav_host_fragment))
+ || super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ return NavigationUI.navigateUp(
+ mDrawerLayout, Navigation.getNavController(this, R.id.my_nav_host_fragment)
+ );
+ }
+}
diff --git a/navigation/integration-tests/testapp/src/main/res/drawable/ic_android.xml b/navigation/integration-tests/testapp/src/main/res/drawable/ic_android.xml
new file mode 100644
index 0000000..fccaeba
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/drawable/ic_android.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M6,18c0,0.55 0.45,1 1,1h1v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L11,19h2v3.5c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5L16,19h1c0.55,0 1,-0.45 1,-1L18,8L6,8v10zM3.5,8C2.67,8 2,8.67 2,9.5v7c0,0.83 0.67,1.5 1.5,1.5S5,17.33 5,16.5v-7C5,8.67 4.33,8 3.5,8zM20.5,8c-0.83,0 -1.5,0.67 -1.5,1.5v7c0,0.83 0.67,1.5 1.5,1.5s1.5,-0.67 1.5,-1.5v-7c0,-0.83 -0.67,-1.5 -1.5,-1.5zM15.53,2.16l1.3,-1.3c0.2,-0.2 0.2,-0.51 0,-0.71 -0.2,-0.2 -0.51,-0.2 -0.71,0l-1.48,1.48C13.85,1.23 12.95,1 12,1c-0.96,0 -1.86,0.23 -2.66,0.63L7.85,0.15c-0.2,-0.2 -0.51,-0.2 -0.71,0 -0.2,0.2 -0.2,0.51 0,0.71l1.31,1.31C6.97,3.26 6,5.01 6,7h12c0,-1.99 -0.97,-3.75 -2.47,-4.84zM10,5L9,5L9,4h1v1zM15,5h-1L14,4h1v1z"/>
+</vector>
diff --git a/navigation/integration-tests/testapp/src/main/res/drawable/ic_help.xml b/navigation/integration-tests/testapp/src/main/res/drawable/ic_help.xml
new file mode 100644
index 0000000..ec47054
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/drawable/ic_help.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
+</vector>
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/integration-tests/testapp/src/main/res/drawable/ic_home.xml
similarity index 60%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/integration-tests/testapp/src/main/res/drawable/ic_home.xml
index 660dbcd..fe1c541 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/integration-tests/testapp/src/main/res/drawable/ic_home.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +13,13 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
+</vector>
diff --git a/navigation/integration-tests/testapp/src/main/res/drawable/ic_settings.xml b/navigation/integration-tests/testapp/src/main/res/drawable/ic_settings.xml
new file mode 100644
index 0000000..aa7973b
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/drawable/ic_settings.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"/>
+</vector>
diff --git a/navigation/integration-tests/testapp/src/main/res/layout-h470dp/navigation_activity.xml b/navigation/integration-tests/testapp/src/main/res/layout-h470dp/navigation_activity.xml
new file mode 100644
index 0000000..186cc05
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/layout-h470dp/navigation_activity.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/colorPrimary"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark"/>
+ <fragment
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:id="@+id/my_nav_host_fragment"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ app:navGraph="@navigation/nav_main"
+ app:defaultNavHost="true"
+ />
+ <android.support.design.widget.BottomNavigationView
+ android:id="@+id/bottom_nav_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:menu="@menu/menu_bottom_nav"/>
+</LinearLayout>
diff --git a/navigation/integration-tests/testapp/src/main/res/layout-w960dp/navigation_activity.xml b/navigation/integration-tests/testapp/src/main/res/layout-w960dp/navigation_activity.xml
new file mode 100644
index 0000000..0db9801
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/layout-w960dp/navigation_activity.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <android.support.design.widget.NavigationView
+ android:id="@+id/nav_view"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ app:elevation="0dp"
+ app:headerLayout="@layout/nav_view_header"
+ app:menu="@menu/menu_nav_drawer"/>
+ <View
+ android:layout_width="1dp"
+ android:layout_height="match_parent"
+ android:layout_toRightOf="@id/nav_view"
+ android:layout_toEndOf="@id/nav_view"
+ android:background="?android:attr/listDivider"/>
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/colorPrimary"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark"
+ android:layout_toRightOf="@id/nav_view"
+ android:layout_toEndOf="@id/nav_view"
+ android:layout_alignParentTop="true"/>
+ <fragment
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/my_nav_host_fragment"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ app:navGraph="@navigation/nav_main"
+ app:defaultNavHost="true"
+ android:layout_toRightOf="@id/nav_view"
+ android:layout_toEndOf="@id/nav_view"
+ android:layout_below="@id/toolbar"/>
+</RelativeLayout>
diff --git a/navigation/integration-tests/testapp/src/main/res/layout/activity_help.xml b/navigation/integration-tests/testapp/src/main/res/layout/activity_help.xml
new file mode 100644
index 0000000..793e273
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/layout/activity_help.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/colorPrimary"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/data"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ </FrameLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/navigation/integration-tests/testapp/src/main/res/layout/android_fragment.xml b/navigation/integration-tests/testapp/src/main/res/layout/android_fragment.xml
new file mode 100644
index 0000000..3201c8b
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/layout/android_fragment.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical" android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="8dp">
+ <TextView android:id="@+id/text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ />
+ <EditText
+ android:id="@+id/edit_args"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="Type your deep link args"/>
+ <Button
+ android:id="@+id/send_notification"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Send deep link notification"/>
+</LinearLayout>
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/integration-tests/testapp/src/main/res/layout/deep_link_appwidget.xml
similarity index 68%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/integration-tests/testapp/src/main/res/layout/deep_link_appwidget.xml
index 660dbcd..e5661e2 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/integration-tests/testapp/src/main/res/layout/deep_link_appwidget.xml
@@ -1,19 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
- ~
+ ~ Copyright (C) 2017 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.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/deep_link"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/android" />
diff --git a/navigation/integration-tests/testapp/src/main/res/layout/main_fragment.xml b/navigation/integration-tests/testapp/src/main/res/layout/main_fragment.xml
new file mode 100644
index 0000000..9f797c9
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/layout/main_fragment.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical" android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="8dp">
+ <TextView android:id="@+id/text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ />
+ <Button
+ android:id="@+id/next_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Navigate next"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/navigation/integration-tests/testapp/src/main/res/layout/nav_view_header.xml b/navigation/integration-tests/testapp/src/main/res/layout/nav_view_header.xml
new file mode 100644
index 0000000..d7be09f
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/layout/nav_view_header.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize">
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_gravity="bottom"
+ android:background="?android:attr/listDivider"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/navigation/integration-tests/testapp/src/main/res/layout/navigation_activity.xml b/navigation/integration-tests/testapp/src/main/res/layout/navigation_activity.xml
new file mode 100644
index 0000000..7384802
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/layout/navigation_activity.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<android.support.v4.widget.DrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/colorPrimary"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark"/>
+ <fragment
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:id="@+id/my_nav_host_fragment"
+ android:name="androidx.navigation.fragment.NavHostFragment"
+ app:navGraph="@navigation/nav_main"
+ app:defaultNavHost="true"
+ />
+ </LinearLayout>
+ <android.support.design.widget.NavigationView
+ android:id="@+id/nav_view"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ app:menu="@menu/menu_nav_drawer"/>
+</android.support.v4.widget.DrawerLayout>
diff --git a/navigation/integration-tests/testapp/src/main/res/menu/menu_bottom_nav.xml b/navigation/integration-tests/testapp/src/main/res/menu/menu_bottom_nav.xml
new file mode 100644
index 0000000..2da6461
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/menu/menu_bottom_nav.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@id/launcher_home"
+ android:icon="@drawable/ic_home"
+ android:title="@string/home" />
+ <item
+ android:id="@id/android"
+ android:icon="@drawable/ic_android"
+ android:title="@string/android" />
+</menu>
\ No newline at end of file
diff --git a/navigation/integration-tests/testapp/src/main/res/menu/menu_nav_drawer.xml b/navigation/integration-tests/testapp/src/main/res/menu/menu_nav_drawer.xml
new file mode 100644
index 0000000..4ce2ffb
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/menu/menu_nav_drawer.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <group android:id="@+id/primary">
+ <item
+ android:id="@id/launcher_home"
+ android:icon="@drawable/ic_home"
+ android:title="@string/home" />
+ <item
+ android:id="@id/android"
+ android:icon="@drawable/ic_android"
+ android:title="@string/android" />
+ </group>
+ <item
+ android:id="@id/settings_activity"
+ android:icon="@drawable/ic_settings"
+ android:title="@string/settings" />
+ <item
+ android:id="@id/help_activity"
+ android:icon="@drawable/ic_help"
+ android:title="@string/help" />
+</menu>
\ No newline at end of file
diff --git a/navigation/integration-tests/testapp/src/main/res/menu/menu_overflow.xml b/navigation/integration-tests/testapp/src/main/res/menu/menu_overflow.xml
new file mode 100644
index 0000000..e07453f
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/menu/menu_overflow.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@id/settings_activity"
+ android:icon="@drawable/ic_settings"
+ android:title="@string/settings"
+ android:menuCategory="secondary" />
+ <item
+ android:id="@id/help_activity"
+ android:icon="@drawable/ic_help"
+ android:title="@string/help"
+ android:menuCategory="secondary" />
+</menu>
\ No newline at end of file
diff --git a/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
new file mode 100644
index 0000000..24fcf4f
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ app:startDestination="@+id/launcher_home"
+ tools:ignore="DuplicateIds">
+ <fragment android:id="@+id/launcher_home"
+ android:label="@string/home"
+ android:name=".MainFragment">
+ <argument android:name="myarg" android:defaultValue="Home" />
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
+ </fragment>
+ <fragment android:id="@+id/android"
+ android:label="@string/android"
+ android:name="androidx.navigation.testapp.AndroidFragment">
+ <argument android:name="myarg" android:defaultValue="Android!" />
+ <deepLink app:uri="www.iana.org/domains/{myarg}"/>
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
+ </fragment>
+ <fragment android:id="@+id/first_screen"
+ android:name="androidx.navigation.testapp.MainFragment">
+ <argument android:name="myarg" android:defaultValue="one" />
+ <action android:id="@+id/next" app:destination="@+id/next_fragment"/>
+ </fragment>
+ <fragment android:id="@+id/next_fragment"
+ android:name="androidx.navigation.testapp.MainFragment">
+ <argument android:name="myarg" android:defaultValue="two" />
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
+ </fragment>
+ <activity android:id="@+id/settings_activity"
+ app:action="android.settings.APPLICATION_DETAILS_SETTINGS"
+ app:data="package:androidx.navigation.testapp"/>
+ <activity android:id="@+id/help_activity"
+ android:name=".HelpActivity"
+ app:dataPattern="http://www.example.com/{topic}">
+ <argument android:name="topic" android:defaultValue="help_topic"/>
+ </activity>
+</navigation>
\ No newline at end of file
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/integration-tests/testapp/src/main/res/values/colors.xml
similarity index 72%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/integration-tests/testapp/src/main/res/values/colors.xml
index 660dbcd..d0709b5 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/integration-tests/testapp/src/main/res/values/colors.xml
@@ -1,19 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
- ~
+ ~ Copyright (C) 2017 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.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+<resources>
+ <color name="colorPrimary">#3F51B5</color>
+ <color name="colorPrimaryDark">#303F9F</color>
+ <color name="colorAccent">#FF4081</color>
+</resources>
diff --git a/navigation/integration-tests/testapp/src/main/res/values/strings.xml b/navigation/integration-tests/testapp/src/main/res/values/strings.xml
new file mode 100644
index 0000000..0fe66e3
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/values/strings.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<resources>
+ <string name="app_name">Navigation</string>
+ <string name="drawer_navigation">Drawer Nav</string>
+ <string name="drawer_open">Open Navigation Drawer</string>
+ <string name="drawer_close">Close Navigation Drawer</string>
+ <string name="bottom_navigation">Bottom Nav</string>
+ <string name="home">Home</string>
+ <string name="android">Android</string>
+ <string name="settings">Settings</string>
+ <string name="help">Help</string>
+</resources>
diff --git a/navigation/integration-tests/testapp/src/main/res/values/styles.xml b/navigation/integration-tests/testapp/src/main/res/values/styles.xml
new file mode 100644
index 0000000..41777e2
--- /dev/null
+++ b/navigation/integration-tests/testapp/src/main/res/values/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<resources>
+
+ <!-- Base application theme. -->
+ <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
+ <!-- Customize your theme here. -->
+ <item name="colorPrimary">@color/colorPrimary</item>
+ <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+ <item name="colorAccent">@color/colorAccent</item>
+ </style>
+
+</resources>
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/integration-tests/testapp/src/main/res/xml/deep_link_appwidget_info.xml
similarity index 67%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/integration-tests/testapp/src/main/res/xml/deep_link_appwidget_info.xml
index 660dbcd..78cb1fb 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/integration-tests/testapp/src/main/res/xml/deep_link_appwidget_info.xml
@@ -1,19 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
- ~
+ ~ Copyright (C) 2017 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.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="110dp"
+ android:minHeight="40dp"
+ android:updatePeriodMillis="0"
+ android:initialLayout="@layout/deep_link_appwidget"/>
diff --git a/navigation/runtime/build.gradle b/navigation/runtime/build.gradle
new file mode 100644
index 0000000..9b8cd0e
--- /dev/null
+++ b/navigation/runtime/build.gradle
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ api(NAV_SUPPORT_CORE_UTILS)
+ api(project(":navigation:navigation-common"))
+
+ testImplementation(JUNIT)
+ testImplementation(MOCKITO_CORE)
+ testImplementation(TEST_RUNNER)
+
+ androidTestImplementation(project(":navigation:navigation-testing"))
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+ name = "Android Navigation Runtime"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2017"
+ description = "Android Navigation-Runtime"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/runtime/ktx/build.gradle b/navigation/runtime/ktx/build.gradle
new file mode 100644
index 0000000..e4de0c6
--- /dev/null
+++ b/navigation/runtime/ktx/build.gradle
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(project(":navigation:navigation-runtime"))
+ // Ensure that the -ktx dependency graph mirrors the Java dependency graph
+ api(project(":navigation:navigation-common-ktx"))
+ api(KOTLIN_STDLIB)
+ androidTestImplementation(project(":navigation:navigation-testing-ktx"))
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+ name = "Android Navigation Runtime Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2018"
+ description = "Android Navigation-Runtime-Ktx"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/runtime/ktx/src/androidTest/AndroidManifest.xml
similarity index 67%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/runtime/ktx/src/androidTest/AndroidManifest.xml
index 660dbcd..a1a7c59 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/runtime/ktx/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.ktx.test">
+ <application>
+ <activity android:name="androidx.navigation.TestActivity"/>
+ </application>
+</manifest>
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityNavigatorDestinationBuilderTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityNavigatorDestinationBuilderTest.kt
new file mode 100644
index 0000000..53704eb
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityNavigatorDestinationBuilderTest.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.navigation
+
+import android.net.Uri
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import junit.framework.Assert.assertTrue
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ActivityNavigatorDestinationBuilderTest {
+ private val navController = NavController(InstrumentationRegistry.getTargetContext())
+
+ @Test
+ fun activity() {
+ val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+ activity(DESTINATION_ID) {
+ label = LABEL
+ }
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ assertEquals("Destination should have label set",
+ LABEL,
+ graph[DESTINATION_ID].label)
+ }
+
+ @Test
+ fun activityClass() {
+ val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+ activity(DESTINATION_ID) {
+ activityClass = TestActivity::class
+ }
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ assertEquals("Destination should have ComponentName set",
+ TestActivity::class.java.name,
+ (graph[DESTINATION_ID] as ActivityNavigator.Destination).component.className)
+ }
+
+ @Test
+ fun action() {
+ val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+ activity(DESTINATION_ID) {
+ action = ACTION
+ }
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ assertEquals("Destination should have action set",
+ ACTION,
+ (graph[DESTINATION_ID] as ActivityNavigator.Destination).action)
+ }
+
+ @Test
+ fun data() {
+ val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+ activity(DESTINATION_ID) {
+ data = DATA
+ }
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ assertEquals("Destination should have data set",
+ DATA,
+ (graph[DESTINATION_ID] as ActivityNavigator.Destination).data)
+ }
+
+ @Test
+ fun dataPattern() {
+ val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+ activity(DESTINATION_ID) {
+ dataPattern = DATA_PATTERN
+ }
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ assertEquals("Destination should have data pattern set",
+ DATA_PATTERN,
+ (graph[DESTINATION_ID] as ActivityNavigator.Destination).dataPattern)
+ }
+}
+
+private const val DESTINATION_ID = 1
+private const val LABEL = "Test"
+private const val ACTION = "ACTION_TEST"
+private val DATA = Uri.parse("http://www.example.com")
+private const val DATA_PATTERN = "http://www.example.com/{id}"
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityTest.kt
new file mode 100644
index 0000000..6468d9a
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ActivityTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.navigation
+
+import android.app.Activity
+import android.os.Bundle
+import android.support.test.filters.SmallTest
+import android.support.test.rule.ActivityTestRule
+import android.view.View
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+
+@SmallTest
+class ActivityTest {
+ @get:Rule val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+ private val view get() = activityRule.activity.findViewById<View>(VIEW_ID)
+
+ @Test fun navController() {
+ val navController = NavController(activityRule.activity)
+ view.navController = navController
+ assertTrue("View should have NavController set",
+ activityRule.activity.navController(VIEW_ID) == navController)
+ }
+
+ @Test fun navControllerNull() {
+ try {
+ activityRule.activity.navController(VIEW_ID)
+ fail("navController should throw IllegalStateException if a NavController was not set")
+ } catch (e: IllegalStateException) {
+ // Expected
+ }
+ }
+
+ @Test fun findNavController() {
+ val navController = NavController(activityRule.activity)
+ view.navController = navController
+
+ val foundNavController = activityRule.activity.findNavController(VIEW_ID)
+ assertNotNull("findNavController should return non-null if a NavController was set",
+ foundNavController)
+ assertTrue("View should have NavController set",
+ foundNavController == navController)
+ }
+
+ @Test fun findNavControllerNull() {
+ assertNull("findNavController should return null if a NavController was never set",
+ activityRule.activity.findNavController(VIEW_ID))
+ }
+}
+
+private const val VIEW_ID = 1
+
+class TestActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(View(this).apply {
+ id = VIEW_ID
+ })
+ }
+}
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavControllerTest.kt
new file mode 100644
index 0000000..bf6f332
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.navigation
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import androidx.navigation.testing.TestNavigator
+import androidx.navigation.testing.test
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class NavControllerTest {
+ private val navController = NavController(InstrumentationRegistry.getTargetContext()).apply {
+ navigatorProvider += TestNavigator()
+ }
+
+ @Test
+ fun createGraph() {
+ val graph = navController.createGraph(startDestination = DESTINATION_ID) {
+ test(DESTINATION_ID)
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ }
+}
+
+private const val DESTINATION_ID = 1
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavHostTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavHostTest.kt
new file mode 100644
index 0000000..7a74ab2
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/NavHostTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.navigation
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import androidx.navigation.testing.TestNavigator
+import androidx.navigation.testing.test
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@SmallTest
+class NavHostTest {
+ private val navController = NavController(InstrumentationRegistry.getTargetContext()).apply {
+ navigatorProvider += TestNavigator()
+ }
+ private val navHost = NavHost { this@NavHostTest.navController }
+
+ @Test
+ fun createGraph() {
+ val graph = navHost.createGraph(startDestination = DESTINATION_ID) {
+ test(DESTINATION_ID)
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ }
+}
+
+private const val DESTINATION_ID = 1
diff --git a/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ViewTest.kt b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ViewTest.kt
new file mode 100644
index 0000000..f6d1978
--- /dev/null
+++ b/navigation/runtime/ktx/src/androidTest/java/androidx/navigation/ViewTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.navigation
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import android.view.View
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ViewTest {
+
+ @Test fun navController() {
+ val view = View(InstrumentationRegistry.getTargetContext())
+ val navController = NavController(InstrumentationRegistry.getTargetContext())
+ view.navController = navController
+ assertTrue("View should have NavController set",
+ view.navController == navController)
+ }
+
+ @Test fun navControllerNull() {
+ val view = View(InstrumentationRegistry.getTargetContext())
+ try {
+ view.navController
+ fail("navController should throw IllegalStateException if a NavController was not set")
+ } catch (e: IllegalStateException) {
+ // Expected
+ }
+ }
+
+ @Test fun findNavController() {
+ val view = View(InstrumentationRegistry.getTargetContext())
+ val navController = NavController(InstrumentationRegistry.getTargetContext())
+ view.navController = navController
+
+ val foundNavController = view.findNavController()
+ assertNotNull("findNavController should return non-null if a NavController was set",
+ foundNavController)
+ assertTrue("View should have NavController set",
+ foundNavController == navController)
+ }
+
+ @Test fun findNavControllerNull() {
+ val view = View(InstrumentationRegistry.getTargetContext())
+ assertNull("findNavController should return null if a NavController was never set",
+ view.findNavController())
+ }
+}
diff --git a/car/res/drawable/car_button_ripple_background.xml b/navigation/runtime/ktx/src/main/AndroidManifest.xml
similarity index 83%
copy from car/res/drawable/car_button_ripple_background.xml
copy to navigation/runtime/ktx/src/main/AndroidManifest.xml
index 13d0a49..799fc93 100644
--- a/car/res/drawable/car_button_ripple_background.xml
+++ b/navigation/runtime/ktx/src/main/AndroidManifest.xml
@@ -14,6 +14,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background" />
+<manifest package="androidx.navigation.ktx"/>
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/Activity.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/Activity.kt
new file mode 100644
index 0000000..dff618a
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/Activity.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.navigation
+
+import android.app.Activity
+import android.support.annotation.IdRes
+
+/**
+ * Find a [NavController] given the id of a View and its containing
+ * [Activity].
+ */
+fun Activity.findNavController(@IdRes viewId: Int): NavController? =
+ Navigation.findNavController(this, viewId)
+
+/**
+ * Gets the [NavController] given the id of a View and its containing
+ * [Activity].
+ *
+ * Calling this on a View that is not a [NavHost] or within a [NavHost]
+ * will result in an [IllegalStateException]
+ */
+fun Activity.navController(@IdRes viewId: Int): NavController =
+ Navigation.getNavController(this, viewId)
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt
new file mode 100644
index 0000000..6de930d
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/ActivityNavigatorDestinationBuilder.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation
+
+import android.app.Activity
+import android.content.ComponentName
+import android.net.Uri
+import android.support.annotation.IdRes
+import kotlin.reflect.KClass
+
+/**
+ * Construct a new [ActivityNavigator.Destination]
+ */
+inline fun NavGraphBuilder.activity(
+ @IdRes id: Int,
+ block: ActivityNavigatorDestinationBuilder.() -> Unit
+) = destination(ActivityNavigatorDestinationBuilder(
+ provider[ActivityNavigator::class],
+ id
+).apply(block))
+
+/**
+ * DSL for constructing a new [ActivityNavigator.Destination]
+ */
+@NavDestinationDsl
+class ActivityNavigatorDestinationBuilder(
+ navigator: ActivityNavigator,
+ @IdRes id: Int
+) : NavDestinationBuilder<ActivityNavigator.Destination>(navigator, id) {
+ private val context = navigator.context
+
+ var activityClass: KClass<out Activity>? = null
+
+ var action: String? = null
+
+ var data: Uri? = null
+
+ var dataPattern: String? = null
+
+ override fun build(): ActivityNavigator.Destination =
+ super.build().also { destination ->
+ activityClass?.let { clazz ->
+ destination.setComponentName(ComponentName(context, clazz.java))
+ }
+ destination.action = action
+ destination.data = data
+ destination.dataPattern = dataPattern
+ }
+}
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/NavController.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/NavController.kt
new file mode 100644
index 0000000..76fb4dd
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/NavController.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.navigation
+
+import android.support.annotation.IdRes
+
+/**
+ * Construct a new [NavGraph]
+ */
+inline fun NavController.createGraph(
+ @IdRes id: Int = 0,
+ @IdRes startDestination: Int,
+ block: NavGraphBuilder.() -> Unit
+): NavGraph = navigatorProvider.navigation(id, startDestination, block)
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/NavHost.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/NavHost.kt
new file mode 100644
index 0000000..1a3bc05
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/NavHost.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.navigation
+
+import android.support.annotation.IdRes
+
+/**
+ * Construct a new [NavGraph]
+ */
+inline fun NavHost.createGraph(
+ @IdRes id: Int = 0,
+ @IdRes startDestination: Int,
+ block: NavGraphBuilder.() -> Unit
+): NavGraph = navController.createGraph(id, startDestination, block)
diff --git a/navigation/runtime/ktx/src/main/java/androidx/navigation/View.kt b/navigation/runtime/ktx/src/main/java/androidx/navigation/View.kt
new file mode 100644
index 0000000..2f5cd6c
--- /dev/null
+++ b/navigation/runtime/ktx/src/main/java/androidx/navigation/View.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.navigation
+
+import android.view.View
+
+/**
+ * Find a [NavController] associated with a [View].
+ */
+fun View.findNavController(): NavController? =
+ Navigation.findNavController(this)
+
+/**
+ * Property for the [NavController] associated with a [View].
+ *
+ * Calling view.navController on a View not within a [NavHost] will result in an
+ * [IllegalStateException]
+ */
+var View.navController: NavController
+ get() = Navigation.getNavController(this)
+ set(value) {
+ Navigation.setViewNavController(this, value)
+ }
diff --git a/navigation/runtime/lint-baseline.xml b/navigation/runtime/lint-baseline.xml
new file mode 100644
index 0000000..10e3040
--- /dev/null
+++ b/navigation/runtime/lint-baseline.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="4" by="lint 3.0.0">
+
+</issues>
\ No newline at end of file
diff --git a/navigation/runtime/proguard-rules.pro b/navigation/runtime/proguard-rules.pro
new file mode 100644
index 0000000..bb2cc55
--- /dev/null
+++ b/navigation/runtime/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /mnt/android-ssd/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/runtime/src/androidTest/AndroidManifest.xml
similarity index 73%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/runtime/src/androidTest/AndroidManifest.xml
index 660dbcd..394b021 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/runtime/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,9 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.test">
+ <application>
+ </application>
+</manifest>
diff --git a/navigation/runtime/src/androidTest/java/androidx/navigation/NavControllerTest.java b/navigation/runtime/src/androidTest/java/androidx/navigation/NavControllerTest.java
new file mode 100644
index 0000000..46c9705
--- /dev/null
+++ b/navigation/runtime/src/androidTest/java/androidx/navigation/NavControllerTest.java
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2017 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.navigation;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.v4.app.TaskStackBuilder;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import androidx.navigation.test.R;
+import androidx.navigation.testing.TestNavigator;
+
+@SmallTest
+public class NavControllerTest {
+ private static final String TEST_ARG = "test";
+ private static final String TEST_ARG_VALUE = "value";
+ private static final String TEST_OVERRIDDEN_VALUE_ARG = "test_overriden_value";
+ private static final String TEST_OVERRIDDEN_VALUE_ARG_VALUE = "override";
+
+ @Test
+ public void testStartDestination() {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_start_destination);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testMissingStartDestination() {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_missing_start_destination);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidStartDestination() {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_invalid_start_destination);
+ }
+
+ @Test
+ public void testNestedStartDestination() {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_nested_start_destination);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.nested_test));
+ }
+
+ @Test
+ public void testSetGraph() throws Throwable {
+ NavController navController = createNavController();
+ assertThat(navController.getGraph(), is(nullValue(NavGraph.class)));
+
+ navController.setGraph(R.navigation.nav_start_destination);
+ assertThat(navController.getGraph(), is(notNullValue(NavGraph.class)));
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ }
+
+ @Test
+ public void testNavigate() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ assertThat(navigator.mBackStack.size(), is(1));
+
+ navController.navigate(R.id.second_test);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ assertThat(navigator.mBackStack.size(), is(2));
+ }
+
+ @Test
+ public void testSaveRestoreStateXml() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ NavController navController = new NavController(context);
+ TestNavigator navigator = new TestNavigator();
+ navController.getNavigatorProvider().addNavigator(navigator);
+ navController.setGraph(R.navigation.nav_simple);
+ navController.navigate(R.id.second_test);
+
+ Bundle savedState = navController.saveState();
+ navController = new NavController(context);
+ navController.getNavigatorProvider().addNavigator(navigator);
+
+ // Restore state should automatically re-inflate the graph
+ // Since the graph has a set id
+ navController.restoreState(savedState);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ assertThat(navigator.mBackStack.size(), is(2));
+ }
+
+ @Test
+ public void testSaveRestoreStateProgrammatic() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ NavController navController = new NavController(context);
+ TestNavigator navigator = new TestNavigator();
+ navController.getNavigatorProvider().addNavigator(navigator);
+ NavGraph graph = new NavInflater(context, navController.getNavigatorProvider())
+ .inflate(R.navigation.nav_simple);
+ navController.setGraph(graph);
+ navController.navigate(R.id.second_test);
+
+ Bundle savedState = navController.saveState();
+ navController = new NavController(context);
+ navController.getNavigatorProvider().addNavigator(navigator);
+
+ // Restore state doesn't recreate any graph
+ navController.restoreState(savedState);
+ assertThat(navController.getGraph(), is(nullValue(NavGraph.class)));
+
+ // Explicitly setting a graph then restores the state
+ navController.setGraph(graph);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ assertThat(navigator.mBackStack.size(), is(2));
+ }
+
+ @Test
+ public void testNavigateWithNoDefaultValue() throws Throwable {
+ Bundle returnedArgs = navigateWithArgs(null);
+
+ // Test that arguments without a default value aren't passed through at all
+ assertThat(returnedArgs.containsKey("test_no_default_value"), is(false));
+ }
+
+ @Test
+ public void testNavigateWithDefaultArgs() throws Throwable {
+ Bundle returnedArgs = navigateWithArgs(null);
+
+ // Test that default values are passed through
+ assertThat(returnedArgs.getString("test_default_value"), is("default"));
+ }
+
+ @Test
+ public void testNavigateWithArgs() throws Throwable {
+ Bundle args = new Bundle();
+ args.putString(TEST_ARG, TEST_ARG_VALUE);
+ Bundle returnedArgs = navigateWithArgs(args);
+
+ // Test that programmatically constructed arguments are passed through
+ assertThat(returnedArgs.getString(TEST_ARG), is(TEST_ARG_VALUE));
+ }
+
+ @Test
+ public void testNavigateWithOverriddenDefaultArgs() throws Throwable {
+ Bundle args = new Bundle();
+ args.putString(TEST_OVERRIDDEN_VALUE_ARG, TEST_OVERRIDDEN_VALUE_ARG_VALUE);
+ Bundle returnedArgs = navigateWithArgs(args);
+
+ // Test that default values can be overridden by programmatic values
+ assertThat(returnedArgs.getString(TEST_OVERRIDDEN_VALUE_ARG),
+ is(TEST_OVERRIDDEN_VALUE_ARG_VALUE));
+ }
+
+ private Bundle navigateWithArgs(Bundle args) throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_arguments);
+
+ navController.navigate(R.id.second_test, args);
+
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ args = navigator.mBackStack.peekLast().second;
+ assertThat(args, is(notNullValue(Bundle.class)));
+
+ return args;
+ }
+
+ @Test
+ public void testNavigateThenPop() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ assertThat(navigator.mBackStack.size(), is(1));
+
+ navController.navigate(R.id.second_test);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ assertThat(navigator.mBackStack.size(), is(2));
+
+ navController.popBackStack();
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ assertThat(navigator.mBackStack.size(), is(1));
+ }
+
+ @Test
+ public void testNavigateThenNavigateUp() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ assertThat(navigator.mBackStack.size(), is(1));
+
+ navController.navigate(R.id.second_test);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ assertThat(navigator.mBackStack.size(), is(2));
+
+ // This should function identically to popBackStack()
+ navController.navigateUp();
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ assertThat(navigator.mBackStack.size(), is(1));
+ }
+
+ @Test
+ public void testNavigateViaAction() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navigator.mBackStack.size(), is(1));
+
+ navController.navigate(R.id.second);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ assertThat(navigator.mBackStack.size(), is(2));
+ }
+
+ @Test
+ public void testNavigateOptionSingleTop() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+ navController.navigate(R.id.second_test);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navigator.mBackStack.size(), is(2));
+
+ navController.navigate(R.id.self);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ assertThat(navigator.mBackStack.size(), is(2));
+ }
+
+ @Test
+ public void testNavigateOptionPopUpToInAction() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+ navController.navigate(R.id.second_test);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navigator.mBackStack.size(), is(2));
+
+ navController.navigate(R.id.finish);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ assertThat(navigator.mBackStack.size(), is(1));
+ }
+
+ @Test
+ public void testNavigateWithPopUpOptionsOnly() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+ navController.navigate(R.id.second_test);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navigator.mBackStack.size(), is(2));
+
+ NavOptions navOptions = new NavOptions.Builder().setPopUpTo(R.id.start_test, false).build();
+ // the same as to call .navigate(R.id.finish)
+ navController.navigate(0, null, navOptions);
+
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ assertThat(navigator.mBackStack.size(), is(1));
+ }
+
+ @Test
+ public void testNoDestinationNoPopUpTo() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+ NavOptions options = new NavOptions.Builder().build();
+ try {
+ navController.navigate(0, null, options);
+ Assert.fail("navController.navigate must throw");
+ } catch (IllegalArgumentException e) {
+ // expected exception
+ }
+ }
+
+ @Test
+ public void testNavigateOptionPopSelf() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+ navController.navigate(R.id.second_test);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.second_test));
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ assertThat(navigator.mBackStack.size(), is(2));
+
+ navController.navigate(R.id.finish_self);
+ assertThat(navController.getCurrentDestination().getId(), is(R.id.start_test));
+ assertThat(navigator.mBackStack.size(), is(1));
+ }
+
+ @Test
+ public void testNavigateViaActionWithArgs() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_arguments);
+
+ Bundle args = new Bundle();
+ args.putString(TEST_ARG, TEST_ARG_VALUE);
+ args.putString(TEST_OVERRIDDEN_VALUE_ARG, TEST_OVERRIDDEN_VALUE_ARG_VALUE);
+ navController.navigate(R.id.second, args);
+
+ TestNavigator navigator = navController.getNavigatorProvider()
+ .getNavigator(TestNavigator.class);
+ Bundle returnedArgs = navigator.mBackStack.peekLast().second;
+ assertThat(returnedArgs, is(notNullValue(Bundle.class)));
+
+ // Test that arguments without a default value aren't passed through at all
+ assertThat(returnedArgs.containsKey("test_no_default_value"), is(false));
+ // Test that default values are passed through
+ assertThat(returnedArgs.getString("test_default_value"), is("default"));
+ // Test that programmatically constructed arguments are passed through
+ assertThat(returnedArgs.getString(TEST_ARG), is(TEST_ARG_VALUE));
+ // Test that default values can be overridden by programmatic values
+ assertThat(returnedArgs.getString(TEST_OVERRIDDEN_VALUE_ARG),
+ is(TEST_OVERRIDDEN_VALUE_ARG_VALUE));
+ }
+
+ @Test
+ public void testDeepLinkFromNavGraph() throws Throwable {
+ NavController navController = createNavController();
+ navController.setGraph(R.navigation.nav_simple);
+
+ TaskStackBuilder taskStackBuilder = navController.createDeepLink()
+ .setDestination(R.id.second_test)
+ .createTaskStackBuilder();
+ assertThat(taskStackBuilder, is(notNullValue(TaskStackBuilder.class)));
+ assertThat(taskStackBuilder.getIntentCount(), is(1));
+ }
+
+ private NavController createNavController() {
+ NavController navController = new NavController(InstrumentationRegistry.getTargetContext());
+ TestNavigator navigator = new TestNavigator();
+ navController.getNavigatorProvider().addNavigator(navigator);
+ return navController;
+ }
+}
diff --git a/navigation/runtime/src/androidTest/java/androidx/navigation/NavInflaterTest.java b/navigation/runtime/src/androidTest/java/androidx/navigation/NavInflaterTest.java
new file mode 100644
index 0000000..db4101b
--- /dev/null
+++ b/navigation/runtime/src/androidTest/java/androidx/navigation/NavInflaterTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.v4.util.Pair;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import androidx.navigation.test.R;
+import androidx.navigation.testing.TestNavigatorProvider;
+
+@SmallTest
+public class NavInflaterTest {
+ private Instrumentation mInstrumentation;
+
+ @Before
+ public void getInstrumentation() {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ }
+
+ @Test
+ public void testInflateSimple() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ NavInflater navInflater = new NavInflater(context, new TestNavigatorProvider(context));
+ NavGraph graph = navInflater.inflate(R.navigation.nav_simple);
+
+ assertThat(graph, is(notNullValue(NavGraph.class)));
+ assertThat(graph.getStartDestination(), is(R.id.start_test));
+ }
+
+ @Test
+ public void testInflateDeepLinkWithApplicationId() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ NavInflater navInflater = new NavInflater(context, new TestNavigatorProvider(context));
+ NavGraph graph = navInflater.inflate(R.navigation.nav_simple);
+
+ assertThat(graph, is(notNullValue(NavGraph.class)));
+ Uri expectedUri = Uri.parse("android-app://"
+ + mInstrumentation.getTargetContext().getPackageName() + "/test");
+ Pair<NavDestination, Bundle> result = graph.matchDeepLink(expectedUri);
+ assertThat(result, is(notNullValue()));
+ assert result != null;
+ assertThat(result.first, is(notNullValue(NavDestination.class)));
+ assertThat(result.first.getId(), is(R.id.second_test));
+ }
+
+ @Test
+ public void testDefaultArgumentsInteger() {
+ Bundle defaultArguments = inflateDefaultArgumentsFromGraph();
+
+ assertThat(defaultArguments.getInt("test_int"), is(12));
+ }
+
+ @Test
+ public void testDefaultArgumentsDimen() {
+ Bundle defaultArguments = inflateDefaultArgumentsFromGraph();
+ Context context = InstrumentationRegistry.getTargetContext();
+ int expectedValue = context.getResources().getDimensionPixelSize(R.dimen.test_dimen_arg);
+
+ assertThat(defaultArguments.getInt("test_dimen"), is(expectedValue));
+ }
+
+ @Test
+ public void testDefaultArgumentsFloat() {
+ Bundle defaultArguments = inflateDefaultArgumentsFromGraph();
+
+ assertThat(defaultArguments.getFloat("test_float"), is(3.14f));
+ }
+
+ @Test
+ public void testDefaultArgumentsReference() {
+ Bundle defaultArguments = inflateDefaultArgumentsFromGraph();
+ Context context = InstrumentationRegistry.getTargetContext();
+ int expectedValue = context.getColor(R.color.test_reference_arg);
+
+ assertThat(defaultArguments.getInt("test_reference"), is(expectedValue));
+ }
+
+ private Bundle inflateDefaultArgumentsFromGraph() {
+ Context context = InstrumentationRegistry.getTargetContext();
+ NavInflater navInflater = new NavInflater(context, new TestNavigatorProvider(context));
+ NavGraph graph = navInflater.inflate(R.navigation.nav_default_arguments);
+
+ NavDestination startDestination = graph.findNode(graph.getStartDestination());
+ Bundle defaultArguments = startDestination.getDefaultArguments();
+
+ assertThat(defaultArguments, is(notNullValue(Bundle.class)));
+ return defaultArguments;
+ }
+}
diff --git a/navigation/runtime/src/androidTest/res/navigation/nav_arguments.xml b/navigation/runtime/src/androidTest/res/navigation/nav_arguments.xml
new file mode 100644
index 0000000..c264db2
--- /dev/null
+++ b/navigation/runtime/src/androidTest/res/navigation/nav_arguments.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:startDestination="@+id/start_test">
+
+ <test android:id="@+id/start_test">
+ <action android:id="@+id/second" app:destination="@+id/second_test" />
+ </test>
+
+ <test android:id="@+id/second_test">
+ <argument android:name="test_no_default_value" />
+ <argument android:name="test_default_value" android:defaultValue="default" />
+ <argument android:name="test_overridden_value" android:defaultValue="default" />
+ </test>
+</navigation>
diff --git a/navigation/runtime/src/androidTest/res/navigation/nav_default_arguments.xml b/navigation/runtime/src/androidTest/res/navigation/nav_default_arguments.xml
new file mode 100644
index 0000000..495d9e0
--- /dev/null
+++ b/navigation/runtime/src/androidTest/res/navigation/nav_default_arguments.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:startDestination="@+id/start_test">
+
+ <test android:id="@+id/start_test">
+ <argument android:name="test_int" android:defaultValue="12" />
+ <argument android:name="test_dimen" android:defaultValue="@dimen/test_dimen_arg" />
+ <argument android:name="test_float" android:defaultValue="3.14" />
+ <argument android:name="test_reference" android:defaultValue="@color/test_reference_arg" />
+ </test>
+</navigation>
diff --git a/navigation/runtime/src/androidTest/res/navigation/nav_invalid_start_destination.xml b/navigation/runtime/src/androidTest/res/navigation/nav_invalid_start_destination.xml
new file mode 100644
index 0000000..1d17431
--- /dev/null
+++ b/navigation/runtime/src/androidTest/res/navigation/nav_invalid_start_destination.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:startDestination="@+id/second_test">
+
+ <test android:id="@+id/start_test" />
+
+ <navigation android:id="@+id/embedded_nav">
+ <test android:id="@+id/second_test" />
+ </navigation>
+</navigation>
diff --git a/car/res/drawable/car_button_ripple_background_day.xml b/navigation/runtime/src/androidTest/res/navigation/nav_missing_start_destination.xml
similarity index 76%
copy from car/res/drawable/car_button_ripple_background_day.xml
copy to navigation/runtime/src/androidTest/res/navigation/nav_missing_start_destination.xml
index 16b1d0c..5e2a82a 100644
--- a/car/res/drawable/car_button_ripple_background_day.xml
+++ b/navigation/runtime/src/androidTest/res/navigation/nav_missing_start_destination.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,8 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_dark" />
+<navigation xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <test android:id="@+id/start_test" />
+
+</navigation>
diff --git a/navigation/runtime/src/androidTest/res/navigation/nav_nested_start_destination.xml b/navigation/runtime/src/androidTest/res/navigation/nav_nested_start_destination.xml
new file mode 100644
index 0000000..03d80cb
--- /dev/null
+++ b/navigation/runtime/src/androidTest/res/navigation/nav_nested_start_destination.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:startDestination="@+id/nested">
+
+ <navigation
+ android:id="@+id/nested"
+ app:startDestination="@+id/nested_test">
+
+ <test android:id="@+id/nested_test"/>
+ </navigation>
+</navigation>
\ No newline at end of file
diff --git a/navigation/runtime/src/androidTest/res/navigation/nav_simple.xml b/navigation/runtime/src/androidTest/res/navigation/nav_simple.xml
new file mode 100644
index 0000000..ff71fc3
--- /dev/null
+++ b/navigation/runtime/src/androidTest/res/navigation/nav_simple.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:startDestination="@+id/start_test">
+
+ <test android:id="@+id/start_test">
+ <action android:id="@+id/second" app:destination="@+id/second_test" />
+ </test>
+
+ <test android:id="@+id/second_test">
+ <action android:id="@+id/self" app:destination="@+id/second_test"
+ app:launchSingleTop="true" />
+ <action android:id="@+id/finish" app:popUpTo="@id/start_test" />
+ <action android:id="@+id/finish_self" app:popUpTo="@id/second_test"
+ app:popUpToInclusive="true" />
+ <deepLink app:uri="android-app://androidx.navigation.test/test" />
+ </test>
+</navigation>
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/runtime/src/androidTest/res/navigation/nav_start_destination.xml
similarity index 66%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/runtime/src/androidTest/res/navigation/nav_start_destination.xml
index 660dbcd..971ebb4 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/runtime/src/androidTest/res/navigation/nav_start_destination.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,9 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ app:startDestination="@+id/start_test">
+
+ <test android:id="@+id/start_test" />
+</navigation>
diff --git a/car/res/drawable/car_button_ripple_background_day.xml b/navigation/runtime/src/androidTest/res/values/colors.xml
similarity index 76%
copy from car/res/drawable/car_button_ripple_background_day.xml
copy to navigation/runtime/src/androidTest/res/values/colors.xml
index 16b1d0c..7e64182 100644
--- a/car/res/drawable/car_button_ripple_background_day.xml
+++ b/navigation/runtime/src/androidTest/res/values/colors.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_dark" />
+
+<resources>
+ <color name="test_reference_arg">#F00</color>
+</resources>
\ No newline at end of file
diff --git a/car/res/drawable/car_button_ripple_background_day.xml b/navigation/runtime/src/androidTest/res/values/dimens.xml
similarity index 76%
rename from car/res/drawable/car_button_ripple_background_day.xml
rename to navigation/runtime/src/androidTest/res/values/dimens.xml
index 16b1d0c..ce1a380 100644
--- a/car/res/drawable/car_button_ripple_background_day.xml
+++ b/navigation/runtime/src/androidTest/res/values/dimens.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_dark" />
+
+<resources>
+ <dimen name="test_dimen_arg">48dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car/res/drawable/car_button_ripple_background_day.xml b/navigation/runtime/src/main/AndroidManifest.xml
similarity index 76%
copy from car/res/drawable/car_button_ripple_background_day.xml
copy to navigation/runtime/src/main/AndroidManifest.xml
index 16b1d0c..db6466c 100644
--- a/car/res/drawable/car_button_ripple_background_day.xml
+++ b/navigation/runtime/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_dark" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation">
+
+</manifest>
diff --git a/navigation/runtime/src/main/java/androidx/navigation/ActivityNavigator.java b/navigation/runtime/src/main/java/androidx/navigation/ActivityNavigator.java
new file mode 100644
index 0000000..f8f9092
--- /dev/null
+++ b/navigation/runtime/src/main/java/androidx/navigation/ActivityNavigator.java
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * ActivityNavigator implements cross-activity navigation.
+ */
+@Navigator.Name("activity")
+public class ActivityNavigator extends Navigator<ActivityNavigator.Destination> {
+ private static final String EXTRA_NAV_SOURCE =
+ "android-support-navigation:ActivityNavigator:source";
+ private static final String EXTRA_NAV_CURRENT =
+ "android-support-navigation:ActivityNavigator:current";
+
+ private Context mContext;
+ private Activity mHostActivity;
+
+ public ActivityNavigator(@NonNull Context context) {
+ mContext = context;
+ while (context instanceof ContextWrapper) {
+ if (context instanceof Activity) {
+ mHostActivity = (Activity) context;
+ break;
+ }
+ context = ((ContextWrapper) context).getBaseContext();
+ }
+ }
+
+ @NonNull
+ Context getContext() {
+ return mContext;
+ }
+
+ @NonNull
+ @Override
+ public Destination createDestination() {
+ return new Destination(this);
+ }
+
+ @Override
+ public boolean popBackStack() {
+ if (mHostActivity != null) {
+ int destId = 0;
+ final Intent intent = mHostActivity.getIntent();
+ if (intent != null) {
+ destId = intent.getIntExtra(EXTRA_NAV_SOURCE, 0);
+ }
+ mHostActivity.finish();
+ dispatchOnNavigatorNavigated(destId, BACK_STACK_DESTINATION_POPPED);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void navigate(@NonNull Destination destination, @Nullable Bundle args,
+ @Nullable NavOptions navOptions) {
+ if (destination.getIntent() == null) {
+ throw new IllegalStateException("Destination " + destination.getId()
+ + " does not have an Intent set.");
+ }
+ Intent intent = new Intent(destination.getIntent());
+ if (args != null) {
+ intent.putExtras(args);
+ String dataPattern = destination.getDataPattern();
+ if (!TextUtils.isEmpty(dataPattern)) {
+ // Fill in the data pattern with the args to build a valid URI
+ StringBuffer data = new StringBuffer();
+ Pattern fillInPattern = Pattern.compile("\\{(.+?)\\}");
+ Matcher matcher = fillInPattern.matcher(dataPattern);
+ while (matcher.find()) {
+ String argName = matcher.group(1);
+ if (args.containsKey(argName)) {
+ matcher.appendReplacement(data, "");
+ data.append(Uri.encode(args.getString(argName)));
+ } else {
+ throw new IllegalArgumentException("Could not find " + argName + " in "
+ + args + " to fill data pattern " + dataPattern);
+ }
+ }
+ matcher.appendTail(data);
+ intent.setData(Uri.parse(data.toString()));
+ }
+ }
+ if (navOptions != null && navOptions.shouldClearTask()) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ }
+ if (navOptions != null && navOptions.shouldLaunchDocument()
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+ } else if (!(mContext instanceof Activity)) {
+ // If we're not launching from an Activity context we have to launch in a new task.
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ if (navOptions != null && navOptions.shouldLaunchSingleTop()) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ }
+ if (mHostActivity != null) {
+ final Intent hostIntent = mHostActivity.getIntent();
+ if (hostIntent != null) {
+ final int hostCurrentId = hostIntent.getIntExtra(EXTRA_NAV_CURRENT, 0);
+ if (hostCurrentId != 0) {
+ intent.putExtra(EXTRA_NAV_SOURCE, hostCurrentId);
+ }
+ }
+ }
+ final int destId = destination.getId();
+ intent.putExtra(EXTRA_NAV_CURRENT, destId);
+ NavOptions.addPopAnimationsToIntent(intent, navOptions);
+ mContext.startActivity(intent);
+ if (navOptions != null && mHostActivity != null) {
+ int enterAnim = navOptions.getEnterAnim();
+ int exitAnim = navOptions.getExitAnim();
+ if (enterAnim != -1 || exitAnim != -1) {
+ enterAnim = enterAnim != -1 ? enterAnim : 0;
+ exitAnim = exitAnim != -1 ? exitAnim : 0;
+ mHostActivity.overridePendingTransition(enterAnim, exitAnim);
+ }
+ }
+
+ // You can't pop the back stack from the caller of a new Activity,
+ // so we don't add this navigator to the controller's back stack
+ dispatchOnNavigatorNavigated(destId, BACK_STACK_UNCHANGED);
+ }
+
+ /**
+ * NavDestination for activity navigation
+ */
+ public static class Destination extends NavDestination {
+ private Intent mIntent;
+ private String mDataPattern;
+
+ /**
+ * Construct a new activity destination. This destination is not valid until you set the
+ * Intent via {@link #setIntent(Intent)} or one or more of the other set method.
+ *
+ *
+ * @param navigatorProvider The {@link NavController} which this destination
+ * will be associated with.
+ */
+ public Destination(@NonNull NavigatorProvider navigatorProvider) {
+ this(navigatorProvider.getNavigator(ActivityNavigator.class));
+ }
+
+ /**
+ * Construct a new activity destination. This destination is not valid until you set the
+ * Intent via {@link #setIntent(Intent)} or one or more of the other set method.
+ *
+ * @param activityNavigator The {@link ActivityNavigator} which this destination
+ * will be associated with. Generally retrieved via a
+ * {@link NavController}'s
+ * {@link NavigatorProvider#getNavigator(Class)} method.
+ */
+ public Destination(@NonNull Navigator<? extends Destination> activityNavigator) {
+ super(activityNavigator);
+ }
+
+ @Override
+ public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
+ super.onInflate(context, attrs);
+ TypedArray a = context.getResources().obtainAttributes(attrs,
+ R.styleable.ActivityNavigator);
+ String cls = a.getString(R.styleable.ActivityNavigator_android_name);
+ if (!TextUtils.isEmpty(cls)) {
+ // TODO Replace with ComponentName.createRelative() when minSdkVersion is 23
+ if (cls.charAt(0) == '.') {
+ cls = context.getPackageName() + cls;
+ }
+ setComponentName(new ComponentName(context, cls));
+ }
+ setAction(a.getString(R.styleable.ActivityNavigator_action));
+ String data = a.getString(R.styleable.ActivityNavigator_data);
+ if (data != null) {
+ setData(Uri.parse(data));
+ }
+ setDataPattern(a.getString(R.styleable.ActivityNavigator_dataPattern));
+ a.recycle();
+ }
+
+ /**
+ * Set the Intent to start when navigating to this destination.
+ * @param intent Intent to associated with this destination.
+ * @return this {@link Destination}
+ */
+ public Destination setIntent(Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ /**
+ * Gets the Intent associated with this destination.
+ * @return
+ */
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Set an explicit {@link ComponentName} to navigate to.
+ *
+ * @param name The component name of the Activity to start.
+ * @return this {@link Destination}
+ */
+ public Destination setComponentName(ComponentName name) {
+ if (mIntent == null) {
+ mIntent = new Intent();
+ }
+ mIntent.setComponent(name);
+ return this;
+ }
+
+ /**
+ * Get the explicit {@link ComponentName} associated with this destination, if any
+ * @return
+ */
+ public ComponentName getComponent() {
+ if (mIntent == null) {
+ return null;
+ }
+ return mIntent.getComponent();
+ }
+
+ /**
+ * Sets the action sent when navigating to this destination.
+ * @param action The action string to use.
+ * @return this {@link Destination}
+ */
+ public Destination setAction(String action) {
+ if (mIntent == null) {
+ mIntent = new Intent();
+ }
+ mIntent.setAction(action);
+ return this;
+ }
+
+ /**
+ * Get the action used to start the Activity, if any
+ */
+ public String getAction() {
+ if (mIntent == null) {
+ return null;
+ }
+ return mIntent.getAction();
+ }
+
+ /**
+ * Sets a static data URI that is sent when navigating to this destination.
+ *
+ * <p>To use a dynamic URI that changes based on the arguments passed in when navigating,
+ * use {@link #setDataPattern(String)}, which will take precedence when arguments are
+ * present.</p>
+ *
+ * @param data A static URI that should always be used.
+ * @see #setDataPattern(String)
+ * @return this {@link Destination}
+ */
+ public Destination setData(Uri data) {
+ if (mIntent == null) {
+ mIntent = new Intent();
+ }
+ mIntent.setData(data);
+ return this;
+ }
+
+ /**
+ * Get the data URI used to start the Activity, if any
+ */
+ public Uri getData() {
+ if (mIntent == null) {
+ return null;
+ }
+ return mIntent.getData();
+ }
+
+ /**
+ * Sets a dynamic data URI pattern that is sent when navigating to this destination.
+ *
+ * <p>If a non-null arguments Bundle is present when navigating, any segments in the form
+ * <code>{argName}</code> will be replaced with a URI encoded string from the arguments.</p>
+ * @param dataPattern A URI pattern with segments in the form of <code>{argName}</code> that
+ * will be replaced with URI encoded versions of the Strings in the
+ * arguments Bundle.
+ * @see #setData
+ * @return this {@link Destination}
+ */
+ public Destination setDataPattern(String dataPattern) {
+ mDataPattern = dataPattern;
+ return this;
+ }
+
+ /**
+ * Gets the dynamic data URI pattern, if any
+ */
+ public String getDataPattern() {
+ return mDataPattern;
+ }
+ }
+}
diff --git a/navigation/runtime/src/main/java/androidx/navigation/NavController.java b/navigation/runtime/src/main/java/androidx/navigation/NavController.java
new file mode 100644
index 0000000..cacb9fd
--- /dev/null
+++ b/navigation/runtime/src/main/java/androidx/navigation/NavController.java
@@ -0,0 +1,699 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.NavigationRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.TaskStackBuilder;
+import android.support.v4.util.Pair;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * NavController manages app navigation within a {@link NavHost}.
+ *
+ * <p>Apps will generally obtain a controller directly from a host, or by using one of the utility
+ * methods on the {@link Navigation} class rather than create a controller directly.</p>
+ *
+ * <p>Navigation flows and destinations are determined by the
+ * {@link NavGraph navigation graph} owned by the controller. These graphs are typically
+ * {@link #getNavInflater() inflated} from an Android resource, but, like views, they can also
+ * be constructed or combined programmatically or for the case of dynamic navigation structure.
+ * (For example, if the navigation structure of the application is determined by live data obtained'
+ * from a remote server.)</p>
+ */
+public class NavController {
+ private static final String KEY_GRAPH_ID = "android-support-nav:controller:graphId";
+ private static final String KEY_BACK_STACK_IDS = "android-support-nav:controller:backStackIds";
+ static final String KEY_DEEP_LINK_IDS = "android-support-nav:controller:deepLinkIds";
+ static final String KEY_DEEP_LINK_EXTRAS =
+ "android-support-nav:controller:deepLinkExtras";
+ /**
+ * The {@link Intent} that triggered a deep link to the current destination.
+ */
+ public static final String KEY_DEEP_LINK_INTENT =
+ "android-support-nav:controller:deepLinkIntent";
+
+ private final Context mContext;
+ private Activity mActivity;
+ private NavInflater mInflater;
+ private NavGraph mGraph;
+ private int mGraphId;
+ private int[] mBackStackToRestore;
+
+ private final Deque<NavDestination> mBackStack = new ArrayDeque<>();
+
+ private final SimpleNavigatorProvider mNavigatorProvider = new SimpleNavigatorProvider() {
+ @Nullable
+ @Override
+ public Navigator<? extends NavDestination> addNavigator(@NonNull String name,
+ @NonNull Navigator<? extends NavDestination> navigator) {
+ Navigator<? extends NavDestination> previousNavigator =
+ super.addNavigator(name, navigator);
+ if (previousNavigator != navigator) {
+ if (previousNavigator != null) {
+ previousNavigator.removeOnNavigatorNavigatedListener(mOnNavigatedListener);
+ }
+ navigator.addOnNavigatorNavigatedListener(mOnNavigatedListener);
+ }
+ return previousNavigator;
+ }
+ };
+
+ private final Navigator.OnNavigatorNavigatedListener mOnNavigatedListener =
+ new Navigator.OnNavigatorNavigatedListener() {
+ @Override
+ public void onNavigatorNavigated(Navigator navigator, @IdRes int destId,
+ @Navigator.BackStackEffect int backStackEffect) {
+ if (destId != 0) {
+ NavDestination newDest = findDestination(destId);
+ if (newDest == null) {
+ throw new IllegalArgumentException("Navigator " + navigator
+ + " reported navigation to unknown destination id "
+ + NavDestination.getDisplayName(mContext, destId));
+ }
+ switch (backStackEffect) {
+ case Navigator.BACK_STACK_DESTINATION_POPPED:
+ while (!mBackStack.isEmpty()
+ && mBackStack.peekLast().getId() != destId) {
+ mBackStack.removeLast();
+ }
+ break;
+ case Navigator.BACK_STACK_DESTINATION_ADDED:
+ mBackStack.add(newDest);
+ break;
+ case Navigator.BACK_STACK_UNCHANGED:
+ // Don't update the back stack and don't dispatchOnNavigated
+ return;
+ }
+ dispatchOnNavigated(newDest);
+ }
+ }
+ };
+
+ private final CopyOnWriteArrayList<OnNavigatedListener> mOnNavigatedListeners =
+ new CopyOnWriteArrayList<>();
+
+ /**
+ * OnNavigatorNavigatedListener receives a callback when the associated controller
+ * navigates to a new destination.
+ */
+ public interface OnNavigatedListener {
+ /**
+ * onNavigatorNavigated is called when the controller navigates to a new destination.
+ * This navigation may be to a destination that has not been seen before, or one that
+ * was previously on the back stack. This method is called after navigation is complete,
+ * but associated transitions may still be playing.
+ *
+ * @param controller the controller that navigated
+ * @param destination the new destination
+ */
+ void onNavigated(@NonNull NavController controller, @NonNull NavDestination destination);
+ }
+
+ /**
+ * Constructs a new controller for a given {@link Context}. Controllers should not be
+ * used outside of their context and retain a hard reference to the context supplied.
+ * If you need a global controller, pass {@link Context#getApplicationContext()}.
+ *
+ * <p>Apps should generally not construct controllers, instead obtain a relevant controller
+ * directly from a navigation host via {@link NavHost#getNavController()} or by using one of
+ * the utility methods on the {@link Navigation} class.</p>
+ *
+ * <p>Note that controllers that are not constructed with an {@link Activity} context
+ * (or a wrapped activity context) will only be able to navigate to
+ * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK new tasks} or
+ * {@link android.content.Intent#FLAG_ACTIVITY_NEW_DOCUMENT new document tasks} when
+ * navigating to new activities.</p>
+ *
+ * @param context context for this controller
+ */
+ public NavController(@NonNull Context context) {
+ mContext = context;
+ while (context instanceof ContextWrapper) {
+ if (context instanceof Activity) {
+ mActivity = (Activity) context;
+ break;
+ }
+ context = ((ContextWrapper) context).getBaseContext();
+ }
+ mNavigatorProvider.addNavigator(new NavGraphNavigator(mContext));
+ mNavigatorProvider.addNavigator(new ActivityNavigator(mContext));
+ }
+
+ @NonNull
+ Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Retrieve the NavController's {@link NavigatorProvider}. All {@link Navigator Navigators} used
+ * to construct the {@link NavGraph navigation graph} for this nav controller should be added
+ * to this navigator provider before the graph is constructed.
+ * <p>
+ * Generally, the Navigators are set for you by the {@link NavHost} hosting this NavController
+ * and you do not need to manually interact with the navigator provider.
+ * </p>
+ * @return The {@link NavigatorProvider} used by this NavController.
+ */
+ @NonNull
+ public NavigatorProvider getNavigatorProvider() {
+ return mNavigatorProvider;
+ }
+
+ /**
+ * Adds an {@link OnNavigatedListener} to this controller to receive events when
+ * the controller navigates to a new destination.
+ *
+ * <p>The current destination, if any, will be immediately sent to your listener.</p>
+ *
+ * @param listener the listener to receive events
+ */
+ public void addOnNavigatedListener(@NonNull OnNavigatedListener listener) {
+ // Inform the new listener of our current state, if any
+ if (!mBackStack.isEmpty()) {
+ listener.onNavigated(this, mBackStack.peekLast());
+ }
+ mOnNavigatedListeners.add(listener);
+ }
+
+ /**
+ * Removes an {@link OnNavigatedListener} from this controller. It will no longer
+ * receive navigation events.
+ *
+ * @param listener the listener to remove
+ */
+ public void removeOnNavigatedListener(@NonNull OnNavigatedListener listener) {
+ mOnNavigatedListeners.remove(listener);
+ }
+
+ /**
+ * Attempts to pop the controller's back stack. Analogous to when the user presses
+ * the system {@link android.view.KeyEvent#KEYCODE_BACK Back} button when the associated
+ * navigation host has focus.
+ *
+ * @return true if the stack was popped, false otherwise
+ */
+ public boolean popBackStack() {
+ if (mBackStack.isEmpty()) {
+ throw new IllegalArgumentException("NavController back stack is empty");
+ }
+ boolean popped = false;
+ while (!mBackStack.isEmpty()) {
+ popped = mBackStack.removeLast().getNavigator().popBackStack();
+ if (popped) {
+ break;
+ }
+ }
+ return popped;
+ }
+
+
+ /**
+ * Attempts to pop the controller's back stack back to a specific destination.
+ *
+ * @param destinationId The topmost destination to retain
+ * @param inclusive Whether the given destination should also be popped.
+ *
+ * @return true if the stack was popped at least once, false otherwise
+ */
+ public boolean popBackStack(@IdRes int destinationId, boolean inclusive) {
+ if (mBackStack.isEmpty()) {
+ throw new IllegalArgumentException("NavController back stack is empty");
+ }
+ ArrayList<NavDestination> destinationsToRemove = new ArrayList<>();
+ Iterator<NavDestination> iterator = mBackStack.descendingIterator();
+ while (iterator.hasNext()) {
+ NavDestination destination = iterator.next();
+ if (inclusive || destination.getId() != destinationId) {
+ destinationsToRemove.add(destination);
+ }
+ if (destination.getId() == destinationId) {
+ break;
+ }
+ }
+ boolean popped = false;
+ iterator = destinationsToRemove.iterator();
+ while (iterator.hasNext()) {
+ NavDestination destination = iterator.next();
+ // Skip destinations already removed by a previous popBackStack operation
+ while (!mBackStack.isEmpty() && mBackStack.peekLast().getId() != destination.getId()) {
+ if (iterator.hasNext()) {
+ destination = iterator.next();
+ } else {
+ destination = null;
+ break;
+ }
+ }
+ if (destination != null) {
+ popped = destination.getNavigator().popBackStack() || popped;
+ }
+ }
+ return popped;
+ }
+
+ /**
+ * Attempts to navigate up in the navigation hierarchy. Suitable for when the
+ * user presses the "Up" button marked with a left (or start)-facing arrow in the upper left
+ * (or starting) corner of the app UI.
+ *
+ * <p>The intended behavior of Up differs from {@link #popBackStack() Back} when the user
+ * did not reach the current destination from the application's own task. e.g. if the user
+ * is viewing a document or link in the current app in an activity hosted on another app's
+ * task where the user clicked the link. In this case the current activity (determined by the
+ * context used to create this NavController) will be {@link Activity#finish() finished} and
+ * the user will be taken to an appropriate destination in this app on its own task.</p>
+ *
+ * @return true if navigation was successful, false otherwise
+ */
+ public boolean navigateUp() {
+ if (mBackStack.size() == 1) {
+ // If there's only one entry, then we've deep linked into a specific destination
+ // on another task so we need to find the parent and start our task from there
+ NavDestination currentDestination = getCurrentDestination();
+ int destId = currentDestination.getId();
+ NavGraph parent = currentDestination.getParent();
+ while (parent != null) {
+ if (parent.getStartDestination() != destId) {
+ TaskStackBuilder parentIntents = new NavDeepLinkBuilder(NavController.this)
+ .setDestination(parent.getId())
+ .createTaskStackBuilder();
+ parentIntents.startActivities();
+ if (mActivity != null) {
+ mActivity.finish();
+ }
+ return true;
+ }
+ destId = parent.getId();
+ parent = parent.getParent();
+ }
+ // We're already at the startDestination of the graph so there's no 'Up' to go to
+ return false;
+ } else {
+ return popBackStack();
+ }
+ }
+
+ void dispatchOnNavigated(NavDestination destination) {
+ for (OnNavigatedListener listener : mOnNavigatedListeners) {
+ listener.onNavigated(this, destination);
+ }
+ }
+
+ /**
+ * Sets the {@link NavGraph navigation graph} as specified in the application manifest.
+ *
+ * <p>Applications may declare a graph resource in their manifest instead of declaring
+ * or passing this data to each host or controller:</p>
+ *
+ * <pre class="prettyprint">
+ * <meta-data android:name="android.nav.graph" android:resource="@xml/my_nav_graph" />
+ * </pre>
+ *
+ * <p>The inflated graph can be retrieved via {@link #getGraph()}.</p>
+ *
+ * @see NavInflater#METADATA_KEY_GRAPH
+ * @see NavInflater#inflateMetadataGraph()
+ * @see #getGraph
+ */
+ public void setMetadataGraph() {
+ setGraph(getNavInflater().inflateMetadataGraph());
+ }
+
+ /**
+ * Returns the {@link NavInflater inflater} for this controller.
+ *
+ * @return inflater for loading navigation resources
+ */
+ @NonNull
+ public NavInflater getNavInflater() {
+ if (mInflater == null) {
+ mInflater = new NavInflater(mContext, mNavigatorProvider);
+ }
+ return mInflater;
+ }
+
+ /**
+ * Sets the {@link NavGraph navigation graph} to the specified resource.
+ * Any current navigation graph data will be replaced.
+ *
+ * <p>The inflated graph can be retrieved via {@link #getGraph()}.</p>
+ *
+ * @param graphResId resource id of the navigation graph to inflate
+ *
+ * @see #getNavInflater()
+ * @see #setGraph(NavGraph)
+ * @see #getGraph
+ */
+ public void setGraph(@NavigationRes int graphResId) {
+ mGraph = getNavInflater().inflate(graphResId);
+ mGraphId = graphResId;
+ onGraphCreated();
+ }
+
+ /**
+ * Sets the {@link NavGraph navigation graph} to the specified graph.
+ * Any current navigation graph data will be replaced.
+ *
+ * <p>The graph can be retrieved later via {@link #getGraph()}.</p>
+ *
+ * @param graph graph to set
+ * @see #setGraph(int)
+ * @see #getGraph
+ */
+ public void setGraph(@NonNull NavGraph graph) {
+ mGraph = graph;
+ mGraphId = 0;
+ onGraphCreated();
+ }
+
+ private void onGraphCreated() {
+ if (mBackStackToRestore != null) {
+ for (int destinationId : mBackStackToRestore) {
+ NavDestination node = findDestination(destinationId);
+ if (node == null) {
+ throw new IllegalStateException("unknown destination during restore: "
+ + mContext.getResources().getResourceName(destinationId));
+ }
+ mBackStack.add(node);
+ }
+ mBackStackToRestore = null;
+ }
+ if (mGraph != null && mBackStack.isEmpty()) {
+ boolean deepLinked = mActivity != null && onHandleDeepLink(mActivity.getIntent());
+ if (!deepLinked) {
+ // Navigate to the first destination in the graph
+ // if we haven't deep linked to a destination
+ mGraph.navigate(null, null);
+ }
+ }
+ }
+
+ /**
+ * Checks the given Intent for a Navigation deep link and navigates to the deep link if present.
+ * This is called automatically for you the first time you set the graph if you've passed in an
+ * {@link Activity} as the context when constructing this NavController, but should be manually
+ * called if your Activity receives new Intents in {@link Activity#onNewIntent(Intent)}.
+ * <p>
+ * The types of Intents that are supported include:
+ * <ul>
+ * <ol>Intents created by {@link NavDeepLinkBuilder} or
+ * {@link #createDeepLink()}. This assumes that the current graph shares
+ * the same hierarchy to get to the deep linked destination as when the deep link was
+ * constructed.</ol>
+ * <ol>Intents that include a {@link Intent#getData() data Uri}. This Uri will be checked
+ * against the Uri patterns added via {@link NavDestination#addDeepLink(String)}.</ol>
+ * </ul>
+ * <p>The {@link #getGraph() navigation graph} should be set before calling this method.</p>
+ * @param intent The Intent that may contain a valid deep link
+ * @return True if the navigation controller found a valid deep link and navigated to it.
+ * @see NavDestination#addDeepLink(String)
+ */
+ public boolean onHandleDeepLink(@Nullable Intent intent) {
+ if (intent == null) {
+ return false;
+ }
+ Bundle extras = intent.getExtras();
+ int[] deepLink = extras != null ? extras.getIntArray(KEY_DEEP_LINK_IDS) : null;
+ Bundle bundle = extras != null ? extras.getBundle(KEY_DEEP_LINK_EXTRAS) : null;
+ if ((deepLink == null || deepLink.length == 0) && intent.getData() != null) {
+ Pair<NavDestination, Bundle> matchingDeepLink = mGraph.matchDeepLink(intent.getData());
+ if (matchingDeepLink != null) {
+ deepLink = matchingDeepLink.first.buildDeepLinkIds();
+ bundle = matchingDeepLink.second;
+ }
+ }
+ if (deepLink == null || deepLink.length == 0) {
+ return false;
+ }
+ if (bundle == null) {
+ bundle = new Bundle();
+ }
+ bundle.putParcelable(KEY_DEEP_LINK_INTENT, intent);
+ int flags = intent.getFlags();
+ if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0
+ && (flags & Intent.FLAG_ACTIVITY_CLEAR_TASK) == 0) {
+ // Someone called us with NEW_TASK, but we don't know what state our whole
+ // task stack is in, so we need to manually restart the whole stack to
+ // ensure we're in a predictably good state.
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ TaskStackBuilder taskStackBuilder = TaskStackBuilder
+ .create(mContext)
+ .addNextIntentWithParentStack(intent);
+ taskStackBuilder.startActivities();
+ if (mActivity != null) {
+ mActivity.finish();
+ }
+ return true;
+ }
+ if ((flags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) {
+ // Start with a cleared task starting at our root when we're on our own task
+ if (!mBackStack.isEmpty()) {
+ navigate(mGraph.getStartDestination(), bundle, new NavOptions.Builder()
+ .setClearTask(true).setEnterAnim(0).setExitAnim(0).build());
+ }
+ while (mBackStack.size() < deepLink.length) {
+ int destinationId = deepLink[mBackStack.size()];
+ NavDestination node = findDestination(destinationId);
+ if (node == null) {
+ throw new IllegalStateException("unknown destination during deep link: "
+ + NavDestination.getDisplayName(mContext, destinationId));
+ }
+ node.navigate(bundle,
+ new NavOptions.Builder().setEnterAnim(0).setExitAnim(0).build());
+ }
+ return true;
+ }
+ // Assume we're on another apps' task and only start the final destination
+ NavGraph graph = mGraph;
+ for (int i = 0; i < deepLink.length; i++) {
+ int destinationId = deepLink[i];
+ NavDestination node = i == 0 ? mGraph : graph.findNode(destinationId);
+ if (node == null) {
+ throw new IllegalStateException("unknown destination during deep link: "
+ + NavDestination.getDisplayName(mContext, destinationId));
+ }
+ if (i != deepLink.length - 1) {
+ // We're not at the final NavDestination yet, so keep going through the chain
+ graph = (NavGraph) node;
+ } else {
+ // Navigate to the last NavDestination, clearing any existing destinations
+ node.navigate(bundle, new NavOptions.Builder()
+ .setClearTask(true).setEnterAnim(0).setExitAnim(0).build());
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Gets the topmost navigation graph associated with this NavController.
+ *
+ * @see #setGraph(int)
+ * @see #setGraph(NavGraph)
+ * @see #setMetadataGraph()
+ */
+ public NavGraph getGraph() {
+ return mGraph;
+ }
+
+ /**
+ * Gets the current destination.
+ */
+ public NavDestination getCurrentDestination() {
+ return mBackStack.peekLast();
+ }
+
+ private NavDestination findDestination(@IdRes int destinationId) {
+ if (mGraph == null) {
+ return null;
+ }
+ if (mGraph.getId() == destinationId) {
+ return mGraph;
+ }
+ NavDestination currentNode = mBackStack.isEmpty() ? mGraph : mBackStack.peekLast();
+ NavGraph currentGraph = currentNode instanceof NavGraph
+ ? (NavGraph) currentNode
+ : currentNode.getParent();
+ return currentGraph.findNode(destinationId);
+ }
+
+ /**
+ * Navigate to a destination from the current navigation graph. This supports both navigating
+ * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
+ *
+ * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
+ * navigate to
+ */
+ public final void navigate(@IdRes int resId) {
+ navigate(resId, null);
+ }
+
+ /**
+ * Navigate to a destination from the current navigation graph. This supports both navigating
+ * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
+ *
+ * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
+ * navigate to
+ * @param args arguments to pass to the destination
+ */
+ public final void navigate(@IdRes int resId, @Nullable Bundle args) {
+ navigate(resId, args, null);
+ }
+
+ /**
+ * Navigate to a destination from the current navigation graph. This supports both navigating
+ * via an {@link NavDestination#getAction(int) action} and directly navigating to a destination.
+ *
+ * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
+ * navigate to
+ * @param args arguments to pass to the destination
+ * @param navOptions special options for this navigation operation
+ */
+ public void navigate(@IdRes int resId, @Nullable Bundle args, @Nullable NavOptions navOptions) {
+ NavDestination currentNode = mBackStack.isEmpty() ? mGraph : mBackStack.peekLast();
+ if (currentNode == null) {
+ throw new IllegalStateException("no current navigation node");
+ }
+ @IdRes int destId = resId;
+ final NavAction navAction = currentNode.getAction(resId);
+ if (navAction != null) {
+ if (navOptions == null) {
+ navOptions = navAction.getNavOptions();
+ }
+ destId = navAction.getDestinationId();
+ }
+ if (destId == 0 && navOptions != null && navOptions.getPopUpTo() != 0) {
+ popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
+ return;
+ }
+
+ if (destId == 0) {
+ throw new IllegalArgumentException("Destination id == 0 can only be used"
+ + " in conjunction with navOptions.popUpTo != 0");
+ }
+
+ NavDestination node = findDestination(destId);
+ if (node == null) {
+ final String dest = NavDestination.getDisplayName(mContext, destId);
+ throw new IllegalArgumentException("navigation destination " + dest
+ + (navAction != null
+ ? " referenced from action " + NavDestination.getDisplayName(mContext, resId)
+ : "")
+ + " is unknown to this NavController");
+ }
+ if (navOptions != null) {
+ if (navOptions.shouldClearTask()) {
+ // Start with a clean slate
+ popBackStack(0, true);
+ mBackStack.clear();
+ } else if (navOptions.getPopUpTo() != 0) {
+ popBackStack(navOptions.getPopUpTo(), navOptions.isPopUpToInclusive());
+ }
+ }
+ node.navigate(args, navOptions);
+ }
+
+ /**
+ * Navigate via the given {@link NavDirections}
+ *
+ * @param directions directions that describe this navigation operation
+ */
+ public void navigate(@NonNull NavDirections directions) {
+ navigate(directions.getActionId(), directions.getArguments());
+ }
+
+ /**
+ * Navigate via the given {@link NavDirections}
+ *
+ * @param directions directions that describe this navigation operation
+ */
+ public void navigate(@NonNull NavDirections directions, @Nullable NavOptions navOptions) {
+ navigate(directions.getActionId(), directions.getArguments(), navOptions);
+ }
+ /**
+ * Create a deep link to a destination within this NavController.
+ *
+ * @return a {@link NavDeepLinkBuilder} suitable for constructing a deep link
+ */
+ @NonNull
+ public NavDeepLinkBuilder createDeepLink() {
+ return new NavDeepLinkBuilder(this);
+ }
+
+ /**
+ * Saves all navigation controller state to a Bundle.
+ *
+ * <p>State may be restored from a bundle returned from this method by calling
+ * {@link #restoreState(Bundle)}. Saving controller state is the responsibility
+ * of a {@link NavHost}.</p>
+ *
+ * @return saved state for this controller
+ */
+ @Nullable
+ public Bundle saveState() {
+ Bundle b = null;
+ if (mGraphId != 0) {
+ b = new Bundle();
+ b.putInt(KEY_GRAPH_ID, mGraphId);
+ }
+ if (!mBackStack.isEmpty()) {
+ if (b == null) {
+ b = new Bundle();
+ }
+ int[] backStack = new int[mBackStack.size()];
+ int index = 0;
+ for (NavDestination destination : mBackStack) {
+ backStack[index++] = destination.getId();
+ }
+ b.putIntArray(KEY_BACK_STACK_IDS, backStack);
+ }
+ return b;
+ }
+
+ /**
+ * Restores all navigation controller state from a bundle.
+ *
+ * <p>State may be saved to a bundle by calling {@link #saveState()}.
+ * Restoring controller state is the responsibility of a {@link NavHost}.</p>
+ *
+ * @param navState state bundle to restore
+ */
+ public void restoreState(@Nullable Bundle navState) {
+ if (navState == null) {
+ return;
+ }
+
+ mGraphId = navState.getInt(KEY_GRAPH_ID);
+ mBackStackToRestore = navState.getIntArray(KEY_BACK_STACK_IDS);
+ if (mGraphId != 0) {
+ // Set the graph right away, onGraphCreated will re-add the back stack
+ // from mBackStackToRestore
+ setGraph(mGraphId);
+ }
+ }
+}
diff --git a/navigation/runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.java b/navigation/runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.java
new file mode 100644
index 0000000..d2a4480
--- /dev/null
+++ b/navigation/runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.NavigationRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.TaskStackBuilder;
+
+import java.util.ArrayDeque;
+
+/**
+ * Class used to construct deep links to a particular destination in a {@link NavGraph}.
+ *
+ * <p>When this deep link is triggered:
+ * <ol>
+ * <li>The task is cleared.</li>
+ * <li>The destination and all of its parents will be on the back stack.</li>
+ * <li>Calling {@link NavController#navigateUp()} will navigate to the parent of the
+ * destination.</li>
+ * </ol></p>
+ *
+ * The parent of the destination is the {@link NavGraph#getStartDestination() start destination}
+ * of the containing {@link NavGraph navigation graph}. In the cases where the destination is
+ * the start destination of its containing navigation graph, the start destination of its
+ * grandparent is used.
+ * <p>
+ * You can construct an instance directly with {@link #NavDeepLinkBuilder(Context)} or build one
+ * using an existing {@link NavController} via {@link NavController#createDeepLink()}.
+ */
+public class NavDeepLinkBuilder {
+ private final Context mContext;
+ private final Intent mIntent;
+
+ private NavGraph mGraph;
+ private int mDestId;
+
+ /**
+ * Construct a new NavDeepLinkBuilder.
+ *
+ * If the context passed in here is not an {@link Activity}, this method will use
+ * {@link android.content.pm.PackageManager#getLaunchIntentForPackage(String)} as the
+ * default activity to launch, if available.
+ *
+ * @param context Context used to create deep links
+ * @see #setComponentName
+ */
+ public NavDeepLinkBuilder(@NonNull Context context) {
+ mContext = context;
+ if (mContext instanceof Activity) {
+ mIntent = new Intent(mContext, mContext.getClass());
+ } else {
+ Intent launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(
+ mContext.getPackageName());
+ mIntent = launchIntent != null ? launchIntent : new Intent();
+ }
+ mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ }
+
+ /**
+ * @see NavController#createDeepLink()
+ */
+ NavDeepLinkBuilder(@NonNull NavController navController) {
+ this(navController.getContext());
+ mGraph = navController.getGraph();
+ }
+
+ /**
+ * Sets an explicit Activity to be started by the deep link created by this class.
+ *
+ * @param activityClass The Activity to start. This Activity should have a {@link NavController}
+ * which uses the same {@link NavGraph} used to construct this
+ * deep link.
+ * @return this object for chaining
+ */
+ @NonNull
+ public NavDeepLinkBuilder setComponentName(@NonNull Class<? extends Activity> activityClass) {
+ return setComponentName(new ComponentName(mContext, activityClass));
+ }
+
+ /**
+ * Sets an explicit Activity to be started by the deep link created by this class.
+ *
+ * @param componentName The Activity to start. This Activity should have a {@link NavController}
+ * which uses the same {@link NavGraph} used to construct this
+ * deep link.
+ * @return this object for chaining
+ */
+ @NonNull
+ public NavDeepLinkBuilder setComponentName(@NonNull ComponentName componentName) {
+ mIntent.setComponent(componentName);
+ return this;
+ }
+
+ /**
+ * Sets the graph that contains the {@link #setDestination(int) deep link destination}.
+ *
+ * @param navGraphId ID of the {@link NavGraph} containing the deep link destination
+ * @return this object for chaining
+ */
+ @NonNull
+ public NavDeepLinkBuilder setGraph(@NavigationRes int navGraphId) {
+ return setGraph(new NavInflater(mContext, new PermissiveNavigatorProvider(mContext))
+ .inflate(navGraphId));
+ }
+
+ /**
+ * Sets the graph that contains the {@link #setDestination(int) deep link destination}.
+ *
+ * @param navGraph The {@link NavGraph} containing the deep link destination
+ * @return this object for chaining
+ */
+ @NonNull
+ public NavDeepLinkBuilder setGraph(@NonNull NavGraph navGraph) {
+ mGraph = navGraph;
+ if (mDestId != 0) {
+ fillInIntent();
+ }
+ return this;
+ }
+
+ /**
+ * Sets the destination id to deep link to.
+ *
+ * @param destId destination ID to deep link to.
+ * @return this object for chaining
+ */
+ @NonNull
+ public NavDeepLinkBuilder setDestination(@IdRes int destId) {
+ mDestId = destId;
+ if (mGraph != null) {
+ fillInIntent();
+ }
+ return this;
+ }
+
+ private void fillInIntent() {
+ NavDestination node = null;
+ ArrayDeque<NavDestination> possibleDestinations = new ArrayDeque<>();
+ possibleDestinations.add(mGraph);
+ while (!possibleDestinations.isEmpty() && node == null) {
+ NavDestination destination = possibleDestinations.poll();
+ if (destination.getId() == mDestId) {
+ node = destination;
+ } else if (destination instanceof NavGraph) {
+ for (NavDestination child : (NavGraph) destination) {
+ possibleDestinations.add(child);
+ }
+ }
+ }
+ if (node == null) {
+ final String dest = NavDestination.getDisplayName(mContext, mDestId);
+ throw new IllegalArgumentException("navigation destination " + dest
+ + " is unknown to this NavController");
+ }
+ mIntent.putExtra(NavController.KEY_DEEP_LINK_IDS, node.buildDeepLinkIds());
+ }
+
+ /**
+ * Set optional arguments to send onto the destination
+ * @param args arguments to pass to the destination
+ * @return this object for chaining
+ */
+ @NonNull
+ public NavDeepLinkBuilder setArguments(@Nullable Bundle args) {
+ mIntent.putExtra(NavController.KEY_DEEP_LINK_EXTRAS, args);
+ return this;
+ }
+
+ /**
+ * Construct the full {@link TaskStackBuilder task stack} needed to deep link to the given
+ * destination.
+ * <p>
+ * You must have {@link #setGraph set a NavGraph} and {@link #setDestination set a destination}
+ * before calling this method.
+ * </p>
+ *
+ * @return a {@link TaskStackBuilder} which can be used to
+ * {@link TaskStackBuilder#startActivities() send the deep link} or
+ * {@link TaskStackBuilder#getPendingIntent(int, int) create a PendingIntent} to deep link to
+ * the given destination.
+ */
+ @NonNull
+ public TaskStackBuilder createTaskStackBuilder() {
+ if (mIntent.getIntArrayExtra(NavController.KEY_DEEP_LINK_IDS) == null) {
+ if (mGraph == null) {
+ throw new IllegalStateException("You must call setGraph() "
+ + "before constructing the deep link");
+ } else {
+ throw new IllegalStateException("You must call setDestination() "
+ + "before constructing the deep link");
+ }
+ }
+ // We create a copy of the Intent to ensure the Intent does not have itself
+ // as an extra. This also prevents developers from modifying the internal Intent
+ // via taskStackBuilder.editIntentAt()
+ TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext)
+ .addNextIntentWithParentStack(new Intent(mIntent));
+ for (int index = 0; index < taskStackBuilder.getIntentCount(); index++) {
+ // Attach the original Intent to each Activity so that they can know
+ // they were constructed in response to a deep link
+ taskStackBuilder.editIntentAt(index)
+ .putExtra(NavController.KEY_DEEP_LINK_INTENT, mIntent);
+ }
+ return taskStackBuilder;
+ }
+
+ /**
+ * Construct a {@link PendingIntent} to the {@link #setDestination(int) deep link destination}.
+ * <p>
+ * This constructs the entire {@link #createTaskStackBuilder() task stack} needed.
+ * <p>
+ * You must have {@link #setGraph set a NavGraph} and {@link #setDestination set a destination}
+ * before calling this method.
+ * </p>
+ *
+ * @return a PendingIntent constructed with
+ * {@link TaskStackBuilder#getPendingIntent(int, int)} to deep link to the
+ * given destination
+ */
+ @NonNull
+ public PendingIntent createPendingIntent() {
+ return createTaskStackBuilder()
+ .getPendingIntent(mDestId, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ /**
+ * A {@link NavigatorProvider} that only parses the basics: {@link NavGraph navigation graphs}
+ * and {@link NavDestination destinations}, effectively only getting the base destination
+ * information.
+ */
+ private static class PermissiveNavigatorProvider extends SimpleNavigatorProvider {
+ /**
+ * A Navigator that only parses the {@link NavDestination} attributes.
+ */
+ private final Navigator<NavDestination> mDestNavigator = new Navigator<NavDestination>() {
+ @Override
+ public NavDestination createDestination() {
+ return new NavDestination(this);
+ }
+
+ @Override
+ public void navigate(NavDestination destination, Bundle args, NavOptions navOptions) {
+ throw new IllegalStateException("navigate is not supported");
+ }
+
+ @Override
+ public boolean popBackStack() {
+ throw new IllegalStateException("popBackStack is not supported");
+ }
+ };
+
+ PermissiveNavigatorProvider(Context context) {
+ addNavigator(new NavGraphNavigator(context));
+ }
+
+ @NonNull
+ @Override
+ public Navigator<? extends NavDestination> getNavigator(@NonNull String name) {
+ try {
+ return super.getNavigator(name);
+ } catch (IllegalStateException e) {
+ return mDestNavigator;
+ }
+ }
+ }
+}
diff --git a/navigation/runtime/src/main/java/androidx/navigation/NavHost.java b/navigation/runtime/src/main/java/androidx/navigation/NavHost.java
new file mode 100644
index 0000000..ffa44db
--- /dev/null
+++ b/navigation/runtime/src/main/java/androidx/navigation/NavHost.java
@@ -0,0 +1,42 @@
+/*
+ * 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.navigation;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.view.View;
+
+/**
+ * A host is a single context or container for navigation via a {@link NavController}.
+ * <p>
+ * Navigation hosts must:
+ * <ul>
+ * <li>Handle {@link NavController#saveState() saving} and
+ * {@link NavController#restoreState(Bundle) restoring} their controller's state</li>
+ * <li>Call {@link Navigation#setViewNavController(View, NavController)} on their root view</li>
+ * </ul>
+ */
+public interface NavHost {
+
+ /**
+ * Returns the {@link NavController navigation controller} for this navigation host.
+ *
+ * @return this host's navigation controller
+ */
+ @NonNull
+ NavController getNavController();
+}
diff --git a/navigation/runtime/src/main/java/androidx/navigation/NavInflater.java b/navigation/runtime/src/main/java/androidx/navigation/NavInflater.java
new file mode 100644
index 0000000..e6c62dd
--- /dev/null
+++ b/navigation/runtime/src/main/java/androidx/navigation/NavInflater.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.Bundle;
+import android.support.annotation.NavigationRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * Class which translates a navigation XML file into a {@link NavGraph}
+ */
+public class NavInflater {
+ /**
+ * Metadata key for defining an app's default navigation graph.
+ *
+ * <p>Applications may declare a graph resource in their manifest instead of declaring
+ * or passing this data to each host or controller:</p>
+ *
+ * <pre class="prettyprint">
+ * <meta-data android:name="android.nav.graph" android:resource="@xml/my_nav_graph" />
+ * </pre>
+ *
+ * <p>A graph resource declared in this manner can be inflated into a controller by calling
+ * {@link NavController#setMetadataGraph()} or directly via {@link #inflateMetadataGraph()}.
+ * Navigation host implementations should do this automatically
+ * if no navigation resource is otherwise supplied during host configuration.</p>
+ */
+ public static final String METADATA_KEY_GRAPH = "android.nav.graph";
+
+ private static final String TAG_ARGUMENT = "argument";
+ private static final String TAG_DEEP_LINK = "deepLink";
+ private static final String TAG_ACTION = "action";
+ private static final String TAG_INCLUDE = "include";
+ private static final String APPLICATION_ID_PLACEHOLDER = "${applicationId}";
+
+ private static final ThreadLocal<TypedValue> sTmpValue = new ThreadLocal<>();
+
+ private Context mContext;
+ private NavigatorProvider mNavigatorProvider;
+
+ public NavInflater(@NonNull Context c, @NonNull NavigatorProvider navigatorProvider) {
+ mContext = c;
+ mNavigatorProvider = navigatorProvider;
+ }
+
+ /**
+ * Inflates {@link NavGraph navigation graph} as specified in the application manifest.
+ *
+ * <p>Applications may declare a graph resource in their manifest instead of declaring
+ * or passing this data to each host or controller:</p>
+ *
+ * <pre class="prettyprint">
+ * <meta-data android:name="android.nav.graph" android:resource="@xml/my_nav_graph" />
+ * </pre>
+ *
+ * @see #METADATA_KEY_GRAPH
+ */
+ @Nullable
+ public NavGraph inflateMetadataGraph() {
+ final Bundle metaData = mContext.getApplicationInfo().metaData;
+ if (metaData != null) {
+ final int resid = metaData.getInt(METADATA_KEY_GRAPH);
+ if (resid != 0) {
+ return inflate(resid);
+ }
+ }
+ return null;
+ }
+ /**
+ * Inflate a NavGraph from the given XML resource id.
+ *
+ * @param graphResId
+ * @return
+ */
+ @SuppressLint("ResourceType")
+ public NavGraph inflate(@NavigationRes int graphResId) {
+ Resources res = mContext.getResources();
+ XmlResourceParser parser = res.getXml(graphResId);
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ try {
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Empty loop
+ }
+ if (type != XmlPullParser.START_TAG) {
+ throw new XmlPullParserException("No start tag found");
+ }
+
+ String rootElement = parser.getName();
+ NavDestination destination = inflate(res, parser, attrs);
+ if (!(destination instanceof NavGraph)) {
+ throw new IllegalArgumentException("Root element <" + rootElement + ">"
+ + " did not inflate into a NavGraph");
+ }
+ return (NavGraph) destination;
+ } catch (Exception e) {
+ throw new RuntimeException("Exception inflating "
+ + res.getResourceName(graphResId) + " line "
+ + parser.getLineNumber(), e);
+ } finally {
+ parser.close();
+ }
+ }
+
+ private NavDestination inflate(Resources res, XmlResourceParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException {
+ Navigator navigator = mNavigatorProvider.getNavigator(parser.getName());
+ final NavDestination dest = navigator.createDestination();
+
+ dest.onInflate(mContext, attrs);
+
+ final int innerDepth = parser.getDepth() + 1;
+ int type;
+ int depth;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && ((depth = parser.getDepth()) >= innerDepth
+ || type != XmlPullParser.END_TAG)) {
+ if (type != XmlPullParser.START_TAG) {
+ continue;
+ }
+
+ if (depth > innerDepth) {
+ continue;
+ }
+
+ final String name = parser.getName();
+ if (TAG_ARGUMENT.equals(name)) {
+ inflateArgument(res, dest, attrs);
+ } else if (TAG_DEEP_LINK.equals(name)) {
+ inflateDeepLink(res, dest, attrs);
+ } else if (TAG_ACTION.equals(name)) {
+ inflateAction(res, dest, attrs);
+ } else if (TAG_INCLUDE.equals(name) && dest instanceof NavGraph) {
+ final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavInclude);
+ final int id = a.getResourceId(R.styleable.NavInclude_graph, 0);
+ ((NavGraph) dest).addDestination(inflate(id));
+ a.recycle();
+ } else if (dest instanceof NavGraph) {
+ ((NavGraph) dest).addDestination(inflate(res, parser, attrs));
+ }
+ }
+
+ return dest;
+ }
+
+ private void inflateArgument(Resources res, NavDestination dest, AttributeSet attrs)
+ throws XmlPullParserException {
+ final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavArgument);
+ String name = a.getString(R.styleable.NavArgument_android_name);
+
+ TypedValue value = sTmpValue.get();
+ if (value == null) {
+ value = new TypedValue();
+ sTmpValue.set(value);
+ }
+ if (a.getValue(R.styleable.NavArgument_android_defaultValue, value)) {
+ switch (value.type) {
+ case TypedValue.TYPE_STRING:
+ dest.getDefaultArguments().putString(name, value.string.toString());
+ break;
+ case TypedValue.TYPE_DIMENSION:
+ dest.getDefaultArguments().putInt(name,
+ (int) value.getDimension(res.getDisplayMetrics()));
+ break;
+ case TypedValue.TYPE_FLOAT:
+ dest.getDefaultArguments().putFloat(name, value.getFloat());
+ break;
+ case TypedValue.TYPE_REFERENCE:
+ dest.getDefaultArguments().putInt(name, value.data);
+ break;
+ default:
+ if (value.type >= TypedValue.TYPE_FIRST_INT
+ && value.type <= TypedValue.TYPE_LAST_INT) {
+ dest.getDefaultArguments().putInt(name, value.data);
+ } else {
+ throw new XmlPullParserException("unsupported argument type " + value.type);
+ }
+ }
+ }
+ a.recycle();
+ }
+
+ private void inflateDeepLink(Resources res, NavDestination dest, AttributeSet attrs) {
+ final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavDeepLink);
+ String uri = a.getString(R.styleable.NavDeepLink_uri);
+ if (TextUtils.isEmpty(uri)) {
+ throw new IllegalArgumentException("Every <" + TAG_DEEP_LINK
+ + "> must include an app:uri");
+ }
+ uri = uri.replace(APPLICATION_ID_PLACEHOLDER, mContext.getPackageName());
+ dest.addDeepLink(uri);
+ a.recycle();
+ }
+
+ private void inflateAction(Resources res, NavDestination dest, AttributeSet attrs) {
+ final TypedArray a = res.obtainAttributes(attrs, R.styleable.NavAction);
+ final int id = a.getResourceId(R.styleable.NavAction_android_id, 0);
+ final int destId = a.getResourceId(R.styleable.NavAction_destination, 0);
+ NavAction action = new NavAction(destId);
+
+ NavOptions.Builder builder = new NavOptions.Builder();
+ builder.setLaunchSingleTop(a.getBoolean(R.styleable.NavAction_launchSingleTop, false));
+ builder.setLaunchDocument(a.getBoolean(R.styleable.NavAction_launchDocument, false));
+ builder.setClearTask(a.getBoolean(R.styleable.NavAction_clearTask, false));
+ builder.setPopUpTo(a.getResourceId(R.styleable.NavAction_popUpTo, 0),
+ a.getBoolean(R.styleable.NavAction_popUpToInclusive, false));
+ builder.setEnterAnim(a.getResourceId(R.styleable.NavAction_enterAnim, -1));
+ builder.setExitAnim(a.getResourceId(R.styleable.NavAction_exitAnim, -1));
+ builder.setPopEnterAnim(a.getResourceId(R.styleable.NavAction_popEnterAnim, -1));
+ builder.setPopExitAnim(a.getResourceId(R.styleable.NavAction_popExitAnim, -1));
+ action.setNavOptions(builder.build());
+
+ dest.putAction(id, action);
+ a.recycle();
+ }
+}
diff --git a/navigation/runtime/src/main/java/androidx/navigation/Navigation.java b/navigation/runtime/src/main/java/androidx/navigation/Navigation.java
new file mode 100644
index 0000000..f1e3683
--- /dev/null
+++ b/navigation/runtime/src/main/java/androidx/navigation/Navigation.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2017 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.navigation;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.View;
+import android.view.ViewParent;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Entry point for navigation operations.
+ *
+ * <p>This class provides utilities for finding a relevant {@link NavController} instance from
+ * various common places in your application, or for performing navigation in response to
+ * UI events.</p>
+ */
+public class Navigation {
+ // No instances. Static utilities only.
+ private Navigation() {
+ }
+
+ /**
+ * Find a {@link NavController} given the id of a View and its containing
+ * {@link Activity}. This is a convenience wrapper around {@link #findNavController(View)}.
+ *
+ * <p>This method will locate the {@link NavController} associated with this view.
+ * This is automatically populated for the id of a {@link NavHost} and its children.</p>
+ *
+ * @param activity The Activity hosting the view
+ * @param viewId The id of the view to search from
+ * @return the {@link NavController} associated with the view referenced by id
+ */
+ @Nullable
+ public static NavController findNavController(@NonNull Activity activity, @IdRes int viewId) {
+ return Navigation.findNavController(activity.findViewById(viewId));
+ }
+
+ /**
+ * Gets the {@link NavController} given the id of a View and its containing
+ * {@link Activity}. This is a convenience wrapper around {@link #getNavController(View)}.
+ *
+ * <p>This method will locate the {@link NavController} associated with this view.
+ * This is automatically populated for the id of a {@link NavHost} and its children.</p>
+ *
+ * @param activity The Activity hosting the view
+ * @param viewId The id of the view to search from
+ * @return the {@link NavController} associated with the view referenced by id
+ * @throws IllegalStateException if the given viewId does not correspond with a
+ * {@link NavHost} or is not within a NavHost.
+ */
+ @NonNull
+ public static NavController getNavController(@NonNull Activity activity, @IdRes int viewId) {
+ NavController navController = findNavController(activity, viewId);
+ if (navController == null) {
+ throw new IllegalStateException("Activity " + activity
+ + " does not have a NavController set on " + viewId);
+ }
+ return navController;
+ }
+
+ /**
+ * Find a {@link NavController} given a local {@link View}.
+ *
+ * <p>This method will locate the {@link NavController} associated with this view.
+ * This is automatically populated for views that are managed by a {@link NavHost}
+ * and is intended for use by various {@link android.view.View.OnClickListener listener}
+ * interfaces.</p>
+ *
+ * @param view the view to search from
+ * @return the locally scoped {@link NavController} to the given view
+ */
+ @Nullable
+ public static NavController findNavController(@NonNull View view) {
+ while (view != null) {
+ NavController controller = getViewNavController(view);
+ if (controller != null) {
+ return controller;
+ }
+ ViewParent parent = view.getParent();
+ view = parent instanceof View ? (View) parent : null;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the {@link NavController} given a local {@link View}.
+ *
+ * <p>This method will locate the {@link NavController} associated with this view.
+ * This is automatically populated for views that are managed by a {@link NavHost}
+ * and is intended for use by various {@link android.view.View.OnClickListener listener}
+ * interfaces.</p>
+ *
+ * @param view the view to search from
+ * @return the locally scoped {@link NavController} to the given view
+ * @throws IllegalStateException if the given view does not correspond with a
+ * {@link NavHost} or is not within a NavHost.
+ */
+ @NonNull
+ public static NavController getNavController(@NonNull View view) {
+ NavController navController = findNavController(view);
+ if (navController == null) {
+ throw new IllegalStateException("View " + view + " does not have a NavController set");
+ }
+ return navController;
+ }
+
+ /**
+ * Create an {@link android.view.View.OnClickListener} for navigating
+ * to a destination. This supports both navigating via an
+ * {@link NavDestination#getAction(int) action} and directly navigating to a destination.
+ *
+ * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
+ * navigate to when the view is clicked
+ * @return a new click listener for setting on an arbitrary view
+ */
+ @NonNull
+ public static View.OnClickListener createNavigateOnClickListener(@IdRes final int resId) {
+ return createNavigateOnClickListener(resId, null);
+ }
+
+ /**
+ * Create an {@link android.view.View.OnClickListener} for navigating
+ * to a destination. This supports both navigating via an
+ * {@link NavDestination#getAction(int) action} and directly navigating to a destination.
+ *
+ * @param resId an {@link NavDestination#getAction(int) action} id or a destination id to
+ * navigate to when the view is clicked
+ * @param args arguments to pass to the final destination
+ * @return a new click listener for setting on an arbitrary view
+ */
+ @NonNull
+ public static View.OnClickListener createNavigateOnClickListener(@IdRes final int resId,
+ @Nullable final Bundle args) {
+ return new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ getNavController(view).navigate(resId, args);
+ }
+ };
+ }
+
+ /**
+ * Associates a NavController with the given View, allowing developers to use
+ * {@link #getNavController(View)} and {@link #getNavController(Activity, int)} with that
+ * View or any of its children to retrieve the NavController.
+ * <p>
+ * This is generally called for you by the hosting {@link NavHost}.
+ * @param view View that should be associated with the given NavController
+ * @param controller The controller you wish to later retrieve via
+ * {@link #getNavController(View)}
+ */
+ public static void setViewNavController(@NonNull View view,
+ @Nullable NavController controller) {
+ view.setTag(R.id.nav_controller_view_tag, controller);
+ }
+
+ static NavController getViewNavController(View view) {
+ Object tag = view.getTag(R.id.nav_controller_view_tag);
+ NavController controller = null;
+ if (tag instanceof WeakReference) {
+ controller = ((WeakReference<NavController>) tag).get();
+ } else if (tag instanceof NavController) {
+ controller = (NavController) tag;
+ }
+ return controller;
+ }
+}
diff --git a/navigation/runtime/src/main/java/androidx/navigation/package-info.java b/navigation/runtime/src/main/java/androidx/navigation/package-info.java
new file mode 100644
index 0000000..8814964
--- /dev/null
+++ b/navigation/runtime/src/main/java/androidx/navigation/package-info.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+/**
+ * Navigation is a framework for navigating between 'destinations' within an Android
+ * application that provides a consistent API whether destinations are implemented as
+ * {@link android.support.v4.app.Fragment Fragments}, {@link android.app.Activity Activities}, or
+ * other components.
+ * <p>
+ * There are 3 major components in Navigation.
+ * <ul>
+ * <li>{@link androidx.navigation.NavGraph}: A navigation graph encapsulates a set
+ * of {@link androidx.navigation.NavDestination destinations}. It can be created by
+ * inflating a navigation XML file, by constructing it programmatically,
+ * or a combination of the two.
+ * </li>
+ * <li>{@link androidx.navigation.NavController}: This is the main entry
+ * point for interacting with the Navigation Graph, translating calls to
+ * {@link androidx.navigation.NavController#navigate(int)},
+ * {@link androidx.navigation.NavController#popBackStack()}, and
+ * {@link androidx.navigation.NavController#navigateUp()} into the appropriate operations.
+ * </li>
+ * <li>{@link androidx.navigation.NavHost}:
+ * {@link androidx.navigation.NavController},
+ * {@link androidx.navigation.fragment.FragmentNavigator.Destination fragment destinations}.
+ * </li>
+ * </ul>
+ * Below is an example of working with a NavController.
+ * <pre class="prettyprint">
+ * // File: HomeFragment.java
+ * public void onViewCreated(View view, {@literal @}Nullable Bundle savedInstanceState) {
+ * // For example purposes, assume our layout created in onCreateView has a Button
+ * // that should navigate the user to a destination
+ * Button b = view.findViewById(R.id.view_details);
+ *
+ * // Retrieve the NavController from any View within a NavHost
+ * final NavController navController = Navigation.findNavController(this);
+ *
+ * // And set the listener
+ * b.setOnClickListener(() -%gt; navController.navigate(R.id.details));
+ *
+ * // Or use the convenience method in Navigation to combine all of the previous steps
+ * b.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.details));
+ * }
+ * </pre>
+ * <p>
+ * Please see the documentation of individual classes for details.
+ */
+package androidx.navigation;
diff --git a/navigation/runtime/src/main/res/values/attrs.xml b/navigation/runtime/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..b36e607
--- /dev/null
+++ b/navigation/runtime/src/main/res/values/attrs.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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.
+ -->
+<resources>
+ <declare-styleable name="ActivityNavigator">
+ <attr name="android:name" />
+ <attr name="action" format="string" />
+ <attr name="data" format="string" />
+ <attr name="dataPattern" format="string" />
+ </declare-styleable>
+
+ <declare-styleable name="NavInclude">
+ <attr name="graph" format="reference" />
+ </declare-styleable>
+</resources>
diff --git a/car/res/drawable/car_button_ripple_background.xml b/navigation/runtime/src/main/res/values/ids.xml
similarity index 75%
copy from car/res/drawable/car_button_ripple_background.xml
copy to navigation/runtime/src/main/res/values/ids.xml
index 13d0a49..d789f1c 100644
--- a/car/res/drawable/car_button_ripple_background.xml
+++ b/navigation/runtime/src/main/res/values/ids.xml
@@ -1,19 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
- ~
+ ~ Copyright (C) 2017 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.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background" />
+<resources>
+ <item type="id" name="nav_controller_view_tag" />
+</resources>
\ No newline at end of file
diff --git a/navigation/safe-args-generator/build.gradle b/navigation/safe-args-generator/build.gradle
new file mode 100644
index 0000000..1c4616b
--- /dev/null
+++ b/navigation/safe-args-generator/build.gradle
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+import androidx.build.SupportConfig
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+apply plugin: androidx.build.SupportKotlinLibraryPlugin
+
+
+sourceSets {
+ test.java.srcDirs += 'src/tests/kotlin'
+}
+
+dependencies {
+ compile(XPP3)
+ compile(XMLPULL)
+ compile(XPP3)
+ compile(XMLPULL)
+ compile(KOTLIN_STDLIB)
+
+ compile(JAVAPOET)
+
+ testCompile(JUNIT)
+ testCompile(GOOGLE_COMPILE_TESTING)
+ def logger = new com.android.build.gradle.internal.LoggerWrapper(project.logger)
+ def sdkHandler = new com.android.build.gradle.internal.SdkHandler(project, logger)
+ testCompile fileTree(dir: "${sdkHandler.sdkFolder}/platforms/android-$SupportConfig.CURRENT_SDK_VERSION/",
+ include : "android.jar")
+ testCompile fileTree(dir: "${new File(project(":navigation:navigation-common").buildDir, "libJar")}",
+ include : "*.jar")
+ testCompile files(org.gradle.internal.jvm.Jvm.current().getToolsJar())
+}
+
+tasks.findByName("test").doFirst {
+ // android.jar and xmlpull has the same classes, but android.jar has stubs instead of real
+ // implementation, so we move android.jar to end of classpath
+ def classpath = it.classpath.getFiles()
+ def androidJar = classpath.find { it.name == "android.jar" }
+ classpath.remove(androidJar)
+ classpath.add(androidJar)
+ it.classpath = files(classpath)
+}
+
+tasks.findByName("compileKotlin").dependsOn(":navigation:navigation-common:jarDebug")
+
+supportLibrary {
+ name = 'Android Navigation TypeSafe Arguments Generator'
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = '2017'
+ description = "Android Navigation TypeSafe Arguments Generator"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavArgumentResolver.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavArgumentResolver.kt
new file mode 100644
index 0000000..4f804a5
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavArgumentResolver.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.models.Argument
+import androidx.navigation.safe.args.generator.models.Destination
+import androidx.navigation.safe.args.generator.models.ResReference
+
+fun resolveArguments(rootDestination: Destination): Destination {
+ val destinations = mutableMapOf<ResReference, Destination>()
+
+ fun dfs(dest: Destination): Destination {
+ val nested = dest.nested.filter { it.id != null }.associateBy { it.id!! }
+ destinations.putAll(nested)
+ val resolvedActions = dest.actions.map { action ->
+ val actionDestination = destinations[action.destination]
+ if (actionDestination != null) {
+ action.copy(args = mergeArguments(action.args, actionDestination.args))
+ } else {
+ action
+ }
+ }
+ val result = dest.copy(nested = dest.nested.map(::dfs), actions = resolvedActions)
+ nested.keys.forEach { id -> destinations.remove(id) }
+ return result
+ }
+
+ return dfs(rootDestination)
+}
+
+private fun mergeArguments(args1: List<Argument>, args2: List<Argument>) =
+ args2.fold(args1) { result, arg ->
+ val duplicate = result.find { it.name == arg.name }
+ if (duplicate != null) {
+ if (duplicate.type != arg.type) {
+ // TODO: needs context to print better exception
+ throw IllegalArgumentException("Incompatible types ${duplicate.type} and " +
+ "${arg.type} of argument ${arg.name}")
+ }
+ result
+ } else {
+ result + arg
+ }
+ }
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParser.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParser.kt
new file mode 100644
index 0000000..9c47b29
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavParser.kt
@@ -0,0 +1,169 @@
+/*
+ * 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.models.Action
+import androidx.navigation.safe.args.generator.models.Argument
+import androidx.navigation.safe.args.generator.models.Destination
+import androidx.navigation.safe.args.generator.models.ResReference
+import org.xmlpull.v1.XmlPullParser
+import org.xmlpull.v1.XmlPullParserFactory
+import java.io.File
+import java.io.FileReader
+
+private const val TAG_NAVIGATION = "navigation"
+private const val TAG_ACTION = "action"
+private const val TAG_ARGUMENT = "argument"
+
+private const val ATTRIBUTE_ID = "id"
+private const val ATTRIBUTE_DESTINATION = "destination"
+private const val ATTRIBUTE_DEFAULT_VALUE = "defaultValue"
+private const val ATTRIBUTE_NAME = "name"
+private const val ATTRIBUTE_TYPE = "type"
+
+private const val NAMESPACE_RES_AUTO = "http://schemas.android.com/apk/res-auto"
+private const val NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"
+
+private fun parseDestination(
+ parser: XmlPullParser, rFilePackage: String,
+ applicationId: String): Destination {
+ val type = parser.name
+ val name = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_NAME) ?: ""
+ val idValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_ID)
+ val args = mutableListOf<Argument>()
+ val actions = mutableListOf<Action>()
+ val nested = mutableListOf<Destination>()
+ parser.traverseInnerStartTags {
+ when {
+ parser.name == TAG_ACTION -> actions.add(parseAction(parser, rFilePackage))
+ parser.name == TAG_ARGUMENT -> args.add(parseArgument(parser, rFilePackage))
+ type == TAG_NAVIGATION -> nested.add(parseDestination(parser, rFilePackage,
+ applicationId))
+ }
+ }
+
+ val id = parseNullableId(idValue, rFilePackage)
+ val className = Destination.createName(id, name, applicationId)
+ if (className == null && (actions.isNotEmpty() || args.isNotEmpty())) {
+ throw IllegalArgumentException("Destination with arguments or action mush have " +
+ "either name either id attributes")
+ }
+ return Destination(id, className, type, args, actions, nested)
+}
+
+private fun parseArgument(parser: XmlPullParser, rFilePackage: String): Argument {
+ val name = parser.attrValueOrThrow(NAMESPACE_ANDROID, ATTRIBUTE_NAME)
+ val defaultValue = parser.attrValue(NAMESPACE_ANDROID, ATTRIBUTE_DEFAULT_VALUE)
+ val typeString = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_TYPE)
+ if (typeString == null && defaultValue != null) {
+ return inferArgument(name, defaultValue, rFilePackage)
+ }
+
+ val (type, defaultTypedValue) = when (typeString) {
+ "integer" -> NavType.INT to defaultValue?.let { parseIntValue(defaultValue) }
+ "reference" -> NavType.REFERENCE to defaultValue?.let {
+ ReferenceValue(parseReference(defaultValue, rFilePackage))
+ }
+ else -> NavType.STRING to defaultValue?.let { StringValue(it) }
+ }
+ return Argument(name, type, defaultTypedValue)
+}
+
+internal fun inferArgument(name: String, defaultValue: String, rFilePackage: String): Argument {
+ val reference = tryToParseReference(defaultValue, rFilePackage)
+ if (reference != null) {
+ return Argument(name, NavType.REFERENCE, ReferenceValue(reference))
+ }
+ val intValue = tryToParseIntValue(defaultValue)
+ if (intValue != null) {
+ return Argument(name, NavType.INT, intValue)
+ }
+ return Argument(name, NavType.STRING, StringValue(defaultValue))
+}
+
+private fun parseAction(parser: XmlPullParser, rFilePackage: String): Action {
+ val idValue = parser.attrValueOrThrow(NAMESPACE_ANDROID, ATTRIBUTE_ID)
+ val destValue = parser.attrValue(NAMESPACE_RES_AUTO, ATTRIBUTE_DESTINATION)
+ val args = mutableListOf<Argument>()
+ parser.traverseInnerStartTags {
+ if (parser.name == TAG_ARGUMENT) {
+ args.add(parseArgument(parser, rFilePackage))
+ }
+ }
+ return Action(parseId(idValue, rFilePackage),
+ parseNullableId(destValue, rFilePackage), args)
+}
+
+fun parseNavigationFile(navigationXml: File, rFilePackage: String,
+ applicationId: String): Destination {
+ FileReader(navigationXml).use { reader ->
+ val parser = XmlPullParserFactory.newInstance().newPullParser().apply {
+ setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true)
+ setInput(reader)
+ }
+ parser.traverseStartTags { true }
+ return parseDestination(parser, rFilePackage, applicationId)
+ }
+}
+
+// @[+][package:]id/resource_name -> package.R.id.resource_name
+private val RESOURCE_REGEX = Regex("^@[+]?(.+?:)?(.+?)/(.+)$")
+
+internal fun parseReference(xmlValue: String, rFilePackage: String): ResReference {
+ return tryToParseReference(xmlValue, rFilePackage) ?:
+ throw IllegalArgumentException("id should be in format: " +
+ "@[+][package:]res_type/resource_name, but is: $xmlValue")
+}
+
+internal fun tryToParseReference(xmlValue: String, rFilePackage: String): ResReference? {
+ val matchEntire = RESOURCE_REGEX.matchEntire(xmlValue) ?: return null
+ val groups = matchEntire.groupValues
+ val resourceName = groups.last()
+ val resType = groups[groups.size - 2]
+ val packageName = if (groups[1].isNotEmpty()) groups[1].removeSuffix(":") else rFilePackage
+ return ResReference(packageName, resType, resourceName)
+}
+
+internal fun parseId(xmlId: String, rFilePackage: String): ResReference {
+ val ref = parseReference(xmlId, rFilePackage)
+ if (!ref.isId()) {
+ throw IllegalArgumentException("$xmlId was passed as id, but is ${ref.resType}")
+ }
+ return ref
+}
+
+internal fun parseNullableId(xmlId: String?, rFilePackage: String): ResReference? = xmlId?.let {
+ parseId(it, rFilePackage)
+}
+
+private fun tryToParseIntValue(value: String): IntValue? {
+ try {
+ if (value.startsWith("0x")) {
+ Integer.parseUnsignedInt(value.substring(2), 16)
+ } else {
+ Integer.parseInt(value)
+ }
+ } catch (ex: NumberFormatException) {
+ return null
+ }
+ return IntValue(value)
+}
+
+internal fun parseIntValue(value: String): IntValue {
+ return tryToParseIntValue(value)
+ ?: throw IllegalArgumentException("Failed to parse $value as int")
+}
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavSafeArgsGenerator.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavSafeArgsGenerator.kt
new file mode 100644
index 0000000..2e0c08a
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavSafeArgsGenerator.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.models.Destination
+import com.squareup.javapoet.JavaFile
+import java.io.File
+
+fun generateSafeArgs(
+ rFilePackage: String, applicationId: String,
+ navigationXml: File, outputDir: File): List<String> {
+ val rawDestination = parseNavigationFile(navigationXml, rFilePackage, applicationId)
+ val resolvedDestination = resolveArguments(rawDestination)
+ val javaFiles = mutableSetOf<JavaFile>()
+ fun writeJavaFiles(destination: Destination) {
+ if (destination.actions.isNotEmpty()) {
+ javaFiles.add(generateDirectionsJavaFile(destination))
+ }
+ if (destination.args.isNotEmpty()) {
+ javaFiles.add(generateArgsJavaFile(destination))
+ }
+ destination.nested.forEach(::writeJavaFiles)
+ }
+ writeJavaFiles(resolvedDestination)
+ javaFiles.forEach { javaFile -> javaFile.writeTo(outputDir) }
+ return javaFiles.map { javaFile -> "${javaFile.packageName}.${javaFile.typeSpec.name}" }
+}
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavWriter.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavWriter.kt
new file mode 100644
index 0000000..f072c54
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/NavWriter.kt
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2017 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.ext.N
+import androidx.navigation.safe.args.generator.ext.S
+import androidx.navigation.safe.args.generator.ext.T
+import androidx.navigation.safe.args.generator.models.Action
+import androidx.navigation.safe.args.generator.models.Argument
+import androidx.navigation.safe.args.generator.models.Destination
+import androidx.navigation.safe.args.generator.models.accessor
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+
+private const val NAVIGATION_PACKAGE = "androidx.navigation"
+private val NAV_DIRECTION_CLASSNAME: ClassName = ClassName.get(NAVIGATION_PACKAGE, "NavDirections")
+private val BUNDLE_CLASSNAME: ClassName = ClassName.get("android.os", "Bundle")
+
+private class ClassWithArgsSpecs(val args: List<Argument>) {
+
+ fun fieldSpecs() = args.map { arg ->
+ FieldSpec.builder(arg.type.typeName(), arg.name)
+ .apply {
+ addModifiers(Modifier.PRIVATE)
+ if (arg.isOptional()) {
+ initializer(arg.defaultValue!!.write())
+ }
+ }
+ .build()
+ }
+
+ fun setters(thisClassName: ClassName) = args.map { (name, type) ->
+ MethodSpec.methodBuilder("set${name.capitalize()}")
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(type.typeName(), name)
+ .addStatement("this.$N = $N", name, name)
+ .addStatement("return this")
+ .returns(thisClassName)
+ .build()
+ }
+
+ fun constructor() = MethodSpec.constructorBuilder().apply {
+ addModifiers(Modifier.PUBLIC)
+ args.filterNot(Argument::isOptional).forEach { (argName, type) ->
+ addParameter(type.typeName(), argName)
+ addStatement("this.$N = $N", argName, argName)
+ }
+ }.build()
+
+ fun toBundleMethod(name: String) = MethodSpec.methodBuilder(name).apply {
+ addModifiers(Modifier.PUBLIC)
+ returns(BUNDLE_CLASSNAME)
+ val bundleName = "__outBundle"
+ addStatement("$T $N = new $T()", BUNDLE_CLASSNAME, bundleName, BUNDLE_CLASSNAME)
+ args.forEach { (argName, type) ->
+ addStatement("$N.$N($S, this.$N)", bundleName, type.bundlePutMethod(), argName, argName)
+ }
+ addStatement("return $N", bundleName)
+ }.build()
+
+ fun copyProperties(to: String, from: String) = CodeBlock.builder()
+ .apply {
+ args.forEach { arg -> addStatement("$to.${arg.name} = $from.${arg.name}") }
+ }
+ .build()
+
+ fun getters() = args.map { arg ->
+ MethodSpec.methodBuilder("get${arg.name.capitalize()}")
+ .addModifiers(Modifier.PUBLIC)
+ .addStatement("return $N", arg.name)
+ .returns(arg.type.typeName())
+ .build()
+ }
+}
+
+fun generateDestinationDirectionsTypeSpec(
+ className: ClassName,
+ destination: Destination): TypeSpec {
+ val actionTypes = destination.actions.map { action ->
+ action to generateDirectionsTypeSpec(action)
+ }
+
+ val getters = actionTypes
+ .map { (action, actionType) ->
+ val constructor = actionType.methodSpecs.find(MethodSpec::isConstructor)!!
+ val params = constructor.parameters.joinToString(", ") { param -> param.name }
+ val actionTypeName = ClassName.get("", actionType.name)
+ MethodSpec.methodBuilder(action.id.name)
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .addParameters(constructor.parameters)
+ .returns(actionTypeName)
+ .addStatement("return new $T($params)", actionTypeName)
+ .build()
+ }
+
+ return TypeSpec.classBuilder(className)
+ .addModifiers(Modifier.PUBLIC)
+ .addTypes(actionTypes.map { (_, actionType) -> actionType })
+ .addMethods(getters)
+ .build()
+}
+
+fun generateDirectionsTypeSpec(action: Action): TypeSpec {
+ val specs = ClassWithArgsSpecs(action.args)
+
+ val getDestIdMethod = MethodSpec.methodBuilder("getActionId")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(Int::class.java)
+ .addStatement("return $N", action.id.accessor())
+ .build()
+
+ val className = ClassName.get("", action.id.name.capitalize())
+ return TypeSpec.classBuilder(className)
+ .addSuperinterface(NAV_DIRECTION_CLASSNAME)
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .addFields(specs.fieldSpecs())
+ .addMethod(specs.constructor())
+ .addMethods(specs.setters(className))
+ .addMethod(specs.toBundleMethod("getArguments"))
+ .addMethod(getDestIdMethod)
+ .build()
+}
+
+internal fun generateArgsJavaFile(destination: Destination): JavaFile {
+ val destName = destination.name
+ ?: throw IllegalStateException("Destination with arguments must have name")
+ val className = ClassName.get(destName.packageName(), "${destName.simpleName()}Args")
+ val args = destination.args
+ val specs = ClassWithArgsSpecs(args)
+
+ val fromBundleMethod = MethodSpec.methodBuilder("fromBundle").apply {
+ addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ val bundle = "bundle"
+ addParameter(BUNDLE_CLASSNAME, bundle)
+ returns(className)
+ val result = "result"
+ addStatement("$T $N = new $T()", className, result, className)
+ args.forEach { arg ->
+ beginControlFlow("if ($N.containsKey($S))", bundle, arg.name).apply {
+ addStatement("$N.$N = $N.$N($S)", result, arg.name, bundle,
+ arg.type.bundleGetMethod(), arg.name)
+ }
+ if (!arg.isOptional()) {
+ nextControlFlow("else")
+ addStatement("throw new $T($S)", IllegalArgumentException::class.java,
+ "Required argument \"${arg.name}\" is missing and does " +
+ "not have an android:defaultValue")
+ }
+ endControlFlow()
+ }
+ addStatement("return $N", result)
+ }.build()
+
+ val constructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build()
+
+ val copyConstructor = MethodSpec.constructorBuilder()
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(className, "original")
+ .addCode(specs.copyProperties("this", "original"))
+ .build()
+
+ val buildMethod = MethodSpec.methodBuilder("build")
+ .addModifiers(Modifier.PUBLIC)
+ .returns(className)
+ .addStatement("$T result = new $T()", className, className)
+ .addCode(specs.copyProperties("result", "this"))
+ .addStatement("return result")
+ .build()
+
+ val builderClassName = ClassName.get("", "Builder")
+ val builderTypeSpec = TypeSpec.classBuilder("Builder")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .addFields(specs.fieldSpecs())
+ .addMethod(copyConstructor)
+ .addMethod(specs.constructor())
+ .addMethod(buildMethod)
+ .addMethods(specs.setters(builderClassName))
+ .addMethods(specs.getters())
+ .build()
+
+ val typeSpec = TypeSpec.classBuilder(className)
+ .addModifiers(Modifier.PUBLIC)
+ .addFields(specs.fieldSpecs())
+ .addMethod(constructor)
+ .addMethod(fromBundleMethod)
+ .addMethods(specs.getters())
+ .addMethod(specs.toBundleMethod("toBundle"))
+ .addType(builderTypeSpec)
+ .build()
+
+ return JavaFile.builder(className.packageName(), typeSpec).build()
+}
+
+fun generateDirectionsJavaFile(destination: Destination): JavaFile {
+ val destName = destination.name
+ ?: throw IllegalStateException("Destination with actions must have name")
+ val className = ClassName.get(destName.packageName(), "${destName.simpleName()}Directions")
+ val typeSpec = generateDestinationDirectionsTypeSpec(className, destination)
+ return JavaFile.builder(className.packageName(), typeSpec).build()
+}
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/Types.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/Types.kt
new file mode 100644
index 0000000..bc0db04
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/Types.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.ext.S
+import androidx.navigation.safe.args.generator.models.ResReference
+import androidx.navigation.safe.args.generator.models.accessor
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.TypeName
+
+enum class NavType {
+
+ INT {
+ override fun typeName(): TypeName = TypeName.INT
+ override fun bundlePutMethod() = "putInt"
+ override fun bundleGetMethod() = "getInt"
+ },
+
+ STRING {
+ override fun typeName(): TypeName = ClassName.get(String::class.java)
+ override fun bundlePutMethod() = "putString"
+ override fun bundleGetMethod() = "getString"
+ },
+
+ REFERENCE {
+ // it is internally the same as INT, but we don't want to allow to
+ // assignment between int and reference args
+ override fun typeName(): TypeName = TypeName.INT
+
+ override fun bundlePutMethod() = "putInt"
+ override fun bundleGetMethod() = "getInt"
+ };
+
+ abstract fun typeName(): TypeName
+ abstract fun bundlePutMethod(): String
+ abstract fun bundleGetMethod(): String
+}
+
+sealed class WriteableValue {
+ abstract fun write(): CodeBlock
+}
+
+data class ReferenceValue(private val resReference: ResReference?) : WriteableValue() {
+ override fun write(): CodeBlock = CodeBlock.of(resReference.accessor())
+}
+
+data class StringValue(private val value: String) : WriteableValue() {
+ override fun write(): CodeBlock = CodeBlock.of(S, value)
+}
+
+// keeping value as String, it will help to preserve client format of it: hex, dec
+data class IntValue(private val value: String) : WriteableValue() {
+ override fun write(): CodeBlock = CodeBlock.of(value)
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/XmlPullParserExt.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/XmlPullParserExt.kt
new file mode 100644
index 0000000..cea993c
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/XmlPullParserExt.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 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.navigation.safe.args.generator
+
+import org.xmlpull.v1.XmlPullParser
+
+fun XmlPullParser.traverseStartTags(onStartTag: (XmlPullParser) -> Boolean) {
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ val processedLine = lineNumber
+ val processedColumn = columnNumber
+ if (eventType == XmlPullParser.START_TAG) {
+ if (onStartTag(this)) {
+ return
+ }
+ }
+ if (processedLine == lineNumber && processedColumn == columnNumber) {
+ // otherwise onStart already called next() and we need to try to process current node
+ next()
+ }
+ }
+}
+
+fun XmlPullParser.traverseInnerStartTags(onStartTag: (XmlPullParser) -> Unit) {
+ val innerDepth = depth + 1
+ next()
+ traverseStartTags {
+ if (innerDepth == it.depth) {
+ onStartTag(it)
+ }
+ it.depth < innerDepth
+ }
+}
+
+fun XmlPullParser.attrValue(namespace: String, name: String): String? =
+ (0 until this.attributeCount).find {
+ getAttributeNamespace(it) == namespace && name == getAttributeName(it)
+ }?.let { getAttributeValue(it) }
+
+fun XmlPullParser.attrValueOrThrow(namespace: String, name: String): String =
+ attrValue(namespace, name) ?:
+ throw IllegalStateException("attribute $namespace:$name is missing.")
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/ext/NavJavaPoet_ext.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/ext/NavJavaPoet_ext.kt
new file mode 100644
index 0000000..697998b
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/ext/NavJavaPoet_ext.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.navigation.safe.args.generator.ext
+
+const val N = "\$N"
+const val T = "\$T"
+const val S = "\$S"
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Action.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Action.kt
new file mode 100644
index 0000000..0361296
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Action.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 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.navigation.safe.args.generator.models
+
+data class Action(val id: ResReference,
+ val destination: ResReference?,
+ val args: List<Argument> = emptyList())
+
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Argument.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Argument.kt
new file mode 100644
index 0000000..fea1bdb
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Argument.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 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.navigation.safe.args.generator.models
+
+import androidx.navigation.safe.args.generator.NavType
+import androidx.navigation.safe.args.generator.WriteableValue
+
+data class Argument(val name: String, val type: NavType, val defaultValue: WriteableValue? = null) {
+ fun isOptional() = defaultValue != null
+}
+
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Destination.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Destination.kt
new file mode 100644
index 0000000..0c74908
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/Destination.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 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.navigation.safe.args.generator.models
+
+import com.squareup.javapoet.ClassName
+
+data class Destination(
+ val id: ResReference?,
+ val name: ClassName?,
+ val type: String,
+ val args: List<Argument>,
+ val actions: List<Action>,
+ val nested: List<Destination> = emptyList()) {
+
+ companion object {
+ fun createName(id: ResReference?, name: String, applicationId: String): ClassName? = when {
+ name.isNotEmpty() -> {
+ val specifiedPackage = name.substringBeforeLast('.', "")
+ val classPackage = if (name.startsWith(".")) {
+ "$applicationId$specifiedPackage"
+ } else {
+ specifiedPackage
+ }
+ ClassName.get(classPackage, name.substringAfterLast('.'))
+ }
+ id != null -> ClassName.get(id.packageName, id.name.capitalize())
+ else -> null
+ }
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/ResReference.kt b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/ResReference.kt
new file mode 100644
index 0000000..125fe18
--- /dev/null
+++ b/navigation/safe-args-generator/src/main/kotlin/androidx/navigation/safe/args/generator/models/ResReference.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.navigation.safe.args.generator.models
+
+data class ResReference(val packageName: String, val resType: String, val name: String) {
+ fun isId() = resType == "id"
+}
+
+fun ResReference?.accessor() = this?.let { "$packageName.R.$resType.$name" } ?: "0"
+
diff --git a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/DestinationTest.kt b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/DestinationTest.kt
new file mode 100644
index 0000000..2f2150d
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/DestinationTest.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.models.Destination
+import androidx.navigation.safe.args.generator.models.ResReference
+import com.squareup.javapoet.ClassName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class DestinationTest {
+
+ @Test
+ fun fullName() {
+ val name = Destination.createName(id("main"), "foo.sub.ClassName", "some.app")
+ assertThat(name, `is`(ClassName.get("foo.sub", "ClassName")))
+ }
+
+ @Test
+ fun relativeWithSubpackage() {
+ val name = Destination.createName(id("main"), ".sub.ClassName", "some.app")
+ assertThat(name, `is`(ClassName.get("some.app.sub", "ClassName")))
+ }
+
+ @Test
+ fun fullNameNoPackage() {
+ val name = Destination.createName(id("main"), "ClassName", "some.app")
+ assertThat(name, `is`(ClassName.get("", "ClassName")))
+ }
+
+ @Test
+ fun idOnly() {
+ val name = Destination.createName(id("main"), "", "some.app")
+ assertThat(name, `is`(ClassName.get("foo.bar", "Main")))
+ }
+}
+
+private fun id(name: String) = ResReference("foo.bar", "id", name)
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavArgumentResolverTest.kt b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavArgumentResolverTest.kt
new file mode 100644
index 0000000..a4906d2
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavArgumentResolverTest.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.NavType.INT
+import androidx.navigation.safe.args.generator.NavType.STRING
+import androidx.navigation.safe.args.generator.models.Action
+import androidx.navigation.safe.args.generator.models.Argument
+import androidx.navigation.safe.args.generator.models.Destination
+import androidx.navigation.safe.args.generator.models.ResReference
+import com.squareup.javapoet.ClassName
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.fail
+
+@RunWith(JUnit4::class)
+class NavArgumentResolverTest {
+
+ private fun id(id: String) = ResReference("a.b", "id", id)
+
+ private fun createTemplateDestination(name: String) =
+ Destination(
+ id(name), ClassName.get("foo", "Fragment${name.capitalize()}"), "test",
+ listOf(
+ Argument("arg1", STRING),
+ Argument("arg2", STRING, StringValue("foo"))
+ ), emptyList())
+
+ @Test
+ fun test() {
+ val dest1Template = createTemplateDestination("first")
+ val dest2Template = createTemplateDestination("second")
+ val outerScopeAction = Action(id("toOuterScope"), id("outerScope"),
+ listOf(Argument("boo", STRING)))
+ val dest1 = dest1Template.copy(actions = listOf(Action(id("action1"), dest2Template.id),
+ outerScopeAction))
+ val dest2 = dest2Template.copy(actions = listOf(Action(id("action2"), dest1Template.id,
+ listOf(Argument("arg1", STRING, StringValue("actionValue")),
+ Argument("actionArg", STRING)))))
+
+ val topLevel = Destination(null, null, "test",
+ emptyList(), emptyList(), listOf(dest1, dest2))
+
+ val resolveArguments = resolveArguments(topLevel)
+ assertThat(resolveArguments.nested.size, `is`(2))
+
+ val resolvedAction1 = Action(id("action1"), dest2Template.id, dest2.args)
+ assertThat(resolveArguments.nested[0].actions, `is`(listOf(resolvedAction1,
+ outerScopeAction)))
+
+ val resolvedAction2 = Action(id("action2"), dest1Template.id, listOf(
+ Argument("arg1", STRING, StringValue("actionValue")),
+ Argument("actionArg", STRING),
+ Argument("arg2", STRING, StringValue("foo"))
+ ))
+ assertThat(resolveArguments.nested[1].actions, `is`(listOf(resolvedAction2)))
+ }
+
+ @Test
+ fun testIncompatibleTypes() {
+ val dest1 = createTemplateDestination("first")
+ val invalidAction = Action(id("action"), dest1.id, listOf(
+ Argument("arg2", INT, IntValue("11")),
+ Argument("arg1", STRING)
+ ))
+
+ val topLevel = Destination(null, null, "test", emptyList(), listOf(invalidAction),
+ listOf(dest1))
+
+ try {
+ resolveArguments(topLevel)
+ fail()
+ } catch (ex: IllegalArgumentException) {
+ // expected error
+ }
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavGeneratorTest.kt b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavGeneratorTest.kt
new file mode 100644
index 0000000..48e3e3a
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavGeneratorTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.navigation.safe.args.generator
+
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+
+@RunWith(JUnit4::class)
+class NavGeneratorTest {
+
+ @Suppress("MemberVisibilityCanPrivate")
+ @get:Rule
+ val workingDir = TemporaryFolder()
+
+ @Test
+ fun test() {
+ val javaNames = generateSafeArgs("foo", "foo.flavor",
+ File("src/tests/test-data/naive_test.xml"), workingDir.root)
+
+ val expectedSet = setOf(
+ "androidx.navigation.testapp.MainFragmentDirections",
+ "foo.flavor.NextFragmentDirections",
+ "androidx.navigation.testapp.MainFragmentArgs",
+ "foo.flavor.NextFragmentArgs"
+ )
+ assertThat(javaNames.toSet(), `is`(expectedSet))
+ javaNames.forEach { name ->
+ val file = File(workingDir.root, "${name.replace('.', File.separatorChar)}.java")
+ assertThat(file.exists(), `is`(true))
+ }
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavParserTest.kt b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavParserTest.kt
new file mode 100644
index 0000000..2164674
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavParserTest.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.NavType.INT
+import androidx.navigation.safe.args.generator.NavType.REFERENCE
+import androidx.navigation.safe.args.generator.NavType.STRING
+import androidx.navigation.safe.args.generator.models.Action
+import androidx.navigation.safe.args.generator.models.Argument
+import androidx.navigation.safe.args.generator.models.Destination
+import androidx.navigation.safe.args.generator.models.ResReference
+import com.squareup.javapoet.ClassName
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.instanceOf
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+
+@RunWith(JUnit4::class)
+class NavParserTest {
+
+ @Test
+ fun test() {
+ val id: (String) -> ResReference = { id -> ResReference("a.b", "id", id) }
+ val navGraph = parseNavigationFile(File("src/tests/test-data/naive_test.xml"), "a.b",
+ "foo.app")
+
+ val nameFirst = ClassName.get("androidx.navigation.testapp", "MainFragment")
+ val nameNext = ClassName.get("foo.app", "NextFragment")
+ val expectedFirst = Destination(id("first_screen"), nameFirst, "fragment",
+ listOf(Argument("myarg1", STRING, StringValue("one"))),
+ listOf(Action(id("next"), id("next_fragment"), listOf(
+ Argument("myarg2", STRING),
+ Argument("randomArgument", STRING),
+ Argument("intArgument", INT, IntValue("261"))
+ ))))
+
+ val expectedNext = Destination(id("next_fragment"), nameNext, "fragment",
+ listOf(Argument("myarg2", STRING)),
+ listOf(Action(id("next"), id("first_screen")),
+ Action(id("finish"), null)))
+
+ val expectedGraph = Destination(null, null, "navigation", emptyList(), emptyList(),
+ listOf(expectedFirst, expectedNext))
+ assertThat(navGraph, `is`(expectedGraph))
+ }
+
+ @Test
+ fun testReferenceParsing() {
+ assertThat(parseReference("@+id/next", "a.b"), `is`(ResReference("a.b", "id", "next")))
+ assertThat(parseReference("@id/next", "a.b"), `is`(ResReference("a.b", "id", "next")))
+ assertThat(parseReference("@android:string/text", "a.b"),
+ `is`(ResReference("android", "string", "text")))
+ assertThat(parseReference("@android:id/text", "a.b"),
+ `is`(ResReference("android", "id", "text")))
+ assertThat(parseReference("@not.android:string/text", "a.b"),
+ `is`(ResReference("not.android", "string", "text")))
+ }
+
+ @Test
+ fun testIntValueParsing() {
+ val error = errorOf({ parseIntValue("foo") })
+ assertThat(error, instanceOf(IllegalArgumentException::class.java))
+ assertThat(parseIntValue("10"), `is`(IntValue("10")))
+ assertThat(parseIntValue("-10"), `is`(IntValue("-10")))
+ assertThat(parseIntValue("0xA"), `is`(IntValue("0xA")))
+ assertThat(parseIntValue("0xFFFFFFFF"), `is`(IntValue("0xFFFFFFFF")))
+ assertThat(errorOf({ parseIntValue("0x1FFFFFFFF") }),
+ instanceOf(IllegalArgumentException::class.java))
+ }
+
+ @Test
+ fun testArgInference() {
+ val infer = { value: String -> inferArgument("foo", value, "a.b") }
+ val intArg = { value: String -> Argument("foo", INT, IntValue(value)) }
+ val stringArg = { value: String -> Argument("foo", STRING, StringValue(value)) }
+ val referenceArg = { pName: String, type: String, value: String ->
+ Argument("foo", REFERENCE, ReferenceValue(ResReference(pName, type, value)))
+ }
+
+ assertThat(infer("spb"), `is`(stringArg("spb")))
+ assertThat(infer("10"), `is`(intArg("10")))
+ assertThat(infer("0x10"), `is`(intArg("0x10")))
+ assertThat(infer("@android:id/some_la"), `is`(referenceArg("android", "id", "some_la")))
+ assertThat(infer("@foo"), `is`(stringArg("@foo")))
+ assertThat(infer("@+id/foo"), `is`(referenceArg("a.b", "id", "foo")))
+ assertThat(infer("@foo:stuff"), `is`(stringArg("@foo:stuff")))
+ assertThat(infer("@/stuff"), `is`(stringArg("@/stuff")))
+ assertThat(infer("10101010100100"), `is`(stringArg("10101010100100")))
+ }
+
+ private fun errorOf(f: () -> Unit, message: String = ""): Exception {
+ try {
+ f()
+ Assert.fail(message)
+ throw Error()
+ } catch (e: Exception) {
+ return e
+ }
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavWriterTest.kt b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavWriterTest.kt
new file mode 100644
index 0000000..f4f3bc7
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/NavWriterTest.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.NavType.INT
+import androidx.navigation.safe.args.generator.NavType.STRING
+import androidx.navigation.safe.args.generator.NavType.REFERENCE
+
+import androidx.navigation.safe.args.generator.models.Action
+import androidx.navigation.safe.args.generator.models.Argument
+import androidx.navigation.safe.args.generator.models.Destination
+import androidx.navigation.safe.args.generator.models.ResReference
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubject
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.TypeSpec
+import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+import java.nio.charset.Charset
+import javax.tools.JavaFileObject
+
+@RunWith(JUnit4::class)
+class WriterTest {
+
+ @get:Rule
+ @Suppress("MemberVisibilityCanBePrivate")
+ val workingDir = TemporaryFolder()
+
+ private fun load(fullClassName: String, folder: String): JavaFileObject {
+ val folderPath = "src/tests/test-data/${if (folder.isEmpty()) "" else folder + "/"}"
+ val split = fullClassName.split(".")
+ val code = File("$folderPath/${split.last()}.java").readText(Charset.defaultCharset())
+ return JavaFileObjects.forSourceString(fullClassName, code)
+ }
+
+ private fun id(id: String) = ResReference("a.b", "id", id)
+
+ private fun wrappedInnerClass(spec: TypeSpec): JavaFileObject {
+ val wrappedSpec = TypeSpec.classBuilder("BoringWrapper").addType(spec).build()
+ return toJavaFileObject(JavaFile.builder("a.b", wrappedSpec).build())
+ }
+
+ private fun toJavaFileObject(javaFile: JavaFile): JavaFileObject {
+ val destination = workingDir.newFolder()
+ javaFile.writeTo(destination)
+ val path = javaFile.packageName.replace('.', '/')
+ val generated = File(destination, "$path/${javaFile.typeSpec.name}.java")
+ MatcherAssert.assertThat(generated.exists(), CoreMatchers.`is`(true))
+ return JavaFileObjects.forResource(generated.toURI().toURL())
+ }
+
+ private fun toJavaFileObject(spec: TypeSpec) =
+ toJavaFileObject(JavaFile.builder("a.b", spec).build())
+
+ private fun assertCompilesWithoutError(javaFileObject: JavaFileObject) {
+ JavaSourcesSubject.assertThat(load("a.b.R", "a/b"), javaFileObject).compilesWithoutError()
+ }
+
+ private fun JavaSourcesSubject.parsesAs(fullClassName: String) =
+ this.parsesAs(load(fullClassName, "expected"))
+
+ @Test
+ fun testDirectionClassGeneration() {
+ val actionSpec = generateDirectionsTypeSpec(Action(id("next"), id("destA"),
+ listOf(
+ Argument("main", STRING),
+ Argument("mainInt", INT),
+ Argument("optional", STRING, StringValue("bla")),
+ Argument("optionalInt", INT, IntValue("239")))))
+ val actual = toJavaFileObject(actionSpec)
+ JavaSourcesSubject.assertThat(actual).parsesAs("a.b.Next")
+ // actions spec must be inner class to be compiled, because of static modifier on class
+ assertCompilesWithoutError(wrappedInnerClass(actionSpec))
+ }
+
+ @Test
+ fun testDirectionNoIdClassGeneration() {
+ val actionSpec = generateDirectionsTypeSpec(Action(id("finish"), null, emptyList()))
+ val actual = toJavaFileObject(actionSpec)
+ JavaSourcesSubject.assertThat(actual).parsesAs("a.b.Finish")
+ // actions spec must be inner class to be compiled, because of static modifier on class
+ assertCompilesWithoutError(wrappedInnerClass(actionSpec))
+ }
+
+ @Test
+ fun testDirectionsClassGeneration() {
+ val nextAction = Action(id("next"), id("destA"),
+ listOf(
+ Argument("main", STRING),
+ Argument("optional", STRING, StringValue("bla"))))
+
+ val prevAction = Action(id("previous"), id("destB"),
+ listOf(
+ Argument("arg1", STRING),
+ Argument("arg2", STRING)))
+
+ val dest = Destination(null, ClassName.get("a.b", "MainFragment"), "fragment", listOf(),
+ listOf(prevAction, nextAction))
+
+ val actual = toJavaFileObject(generateDirectionsJavaFile(dest))
+ JavaSourcesSubject.assertThat(actual).parsesAs("a.b.MainFragmentDirections")
+ assertCompilesWithoutError(actual)
+ }
+
+ @Test
+ fun testArgumentsClassGeneration() {
+ val dest = Destination(null, ClassName.get("a.b", "MainFragment"), "fragment", listOf(
+ Argument("main", STRING),
+ Argument("optional", INT, IntValue("-1")),
+ Argument("reference", REFERENCE, ReferenceValue(ResReference("a.b", "drawable",
+ "background")))),
+ listOf())
+
+ val actual = toJavaFileObject(generateArgsJavaFile(dest))
+ JavaSourcesSubject.assertThat(actual).parsesAs("a.b.MainFragmentArgs")
+ assertCompilesWithoutError(actual)
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/WritableValueTest.kt b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/WritableValueTest.kt
new file mode 100644
index 0000000..4ab2627f
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/kotlin/androidx/navigation/safe/args/generator/WritableValueTest.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.navigation.safe.args.generator
+
+import androidx.navigation.safe.args.generator.models.ResReference
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class WritableValueTest {
+
+ @Test
+ fun testIntWrite() {
+ assertThat(IntValue("-10").write().toString(), `is`("-10"))
+ assertThat(IntValue("11").write().toString(), `is`("11"))
+ }
+
+ @Test
+ fun testStringWrite() {
+ assertThat(StringValue("foo").write().toString(), `is`("\"foo\""))
+ }
+
+ @Test
+ fun testReferenceWrite() {
+ assertThat(ReferenceValue(ResReference("foo", "id", "bla")).write().toString(),
+ `is`("foo.R.id.bla"))
+ assertThat(ReferenceValue(null).write().toString(), `is`("0"))
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/test-data/IGNORE_CHECKSTYLE b/navigation/safe-args-generator/src/tests/test-data/IGNORE_CHECKSTYLE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/IGNORE_CHECKSTYLE
diff --git a/navigation/safe-args-generator/src/tests/test-data/a/b/R.java b/navigation/safe-args-generator/src/tests/test-data/a/b/R.java
new file mode 100644
index 0000000..8d2fc90
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/a/b/R.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 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 a.b;
+
+// fake R class to compile against for WriterTest
+public class R {
+
+ public static final class id {
+ public static final int finish = 0x7f060000;
+ public static final int previous = 0x7f060001;
+ public static final int next = 0x7f060002;
+ }
+
+ public static final class drawable {
+ public static int background = 0x7f090001;
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/test-data/expected/Finish.java b/navigation/safe-args-generator/src/tests/test-data/expected/Finish.java
new file mode 100644
index 0000000..07e5ca5
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/expected/Finish.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 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 a.b;
+
+import android.os.Bundle;
+import androidx.navigation.NavDirections;
+
+public static class Finish implements NavDirections {
+
+ public Finish() {
+ }
+
+ public Bundle getArguments() {
+ Bundle __outBundle = new Bundle();
+ return __outBundle;
+ }
+
+ public int getActionId() {
+ return a.b.R.id.finish;
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/test-data/expected/MainFragmentArgs.java b/navigation/safe-args-generator/src/tests/test-data/expected/MainFragmentArgs.java
new file mode 100644
index 0000000..b62ed9c
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/expected/MainFragmentArgs.java
@@ -0,0 +1,105 @@
+package a.b;
+
+import android.os.Bundle;
+import java.lang.IllegalArgumentException;
+import java.lang.String;
+
+public class MainFragmentArgs {
+ private String main;
+
+ private int optional = -1;
+
+ private int reference = a.b.R.drawable.background;
+
+ private MainFragmentArgs() {
+ }
+
+ public static MainFragmentArgs fromBundle(Bundle bundle) {
+ MainFragmentArgs result = new MainFragmentArgs();
+ if (bundle.containsKey("main")) {
+ result.main = bundle.getString("main");
+ } else {
+ throw new IllegalArgumentException("Required argument \"main\" is missing and does not have an android:defaultValue");
+ }
+ if (bundle.containsKey("optional")) {
+ result.optional = bundle.getInt("optional");
+ }
+ if (bundle.containsKey("reference")) {
+ result.reference = bundle.getInt("reference");
+ }
+ return result;
+ }
+
+ public String getMain() {
+ return main;
+ }
+
+ public int getOptional() {
+ return optional;
+ }
+
+ public int getReference() {
+ return reference;
+ }
+
+ public Bundle toBundle() {
+ Bundle __outBundle = new Bundle();
+ __outBundle.putString("main", this.main);
+ __outBundle.putInt("optional", this.optional);
+ __outBundle.putInt("reference", this.reference);
+ return __outBundle;
+ }
+
+ public static class Builder {
+ private String main;
+
+ private int optional = -1;
+
+ private int reference = a.b.R.drawable.background;
+
+ public Builder(MainFragmentArgs original) {
+ this.main = original.main;
+ this.optional = original.optional;
+ this.reference = original.reference;
+ }
+
+ public Builder(String main) {
+ this.main = main;
+ }
+
+ public MainFragmentArgs build() {
+ MainFragmentArgs result = new MainFragmentArgs();
+ result.main = this.main;
+ result.optional = this.optional;
+ result.reference = this.reference;
+ return result;
+ }
+
+ public Builder setMain(String main) {
+ this.main = main;
+ return this;
+ }
+
+ public Builder setOptional(int optional) {
+ this.optional = optional;
+ return this;
+ }
+
+ public Builder setReference(int reference) {
+ this.reference = reference;
+ return this;
+ }
+
+ public String getMain() {
+ return main;
+ }
+
+ public int getOptional() {
+ return optional;
+ }
+
+ public int getReference() {
+ return reference;
+ }
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/test-data/expected/MainFragmentDirections.java b/navigation/safe-args-generator/src/tests/test-data/expected/MainFragmentDirections.java
new file mode 100644
index 0000000..a4747fb
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/expected/MainFragmentDirections.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 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 a.b;
+
+import android.os.Bundle;
+import androidx.navigation.NavDirections;
+import java.lang.String;
+
+public class MainFragmentDirections {
+ public static Previous previous(String arg1, String arg2) {
+ return new Previous(arg1, arg2);
+ }
+
+ public static Next next(String main) {
+ return new Next(main);
+ }
+
+ public static class Previous implements NavDirections {
+ private String arg1;
+
+ private String arg2;
+
+ public Previous(String arg1, String arg2) {
+ this.arg1 = arg1;
+ this.arg2 = arg2;
+ }
+
+ public Previous setArg1(String arg1) {
+ this.arg1 = arg1;
+ return this;
+ }
+
+ public Previous setArg2(String arg2) {
+ this.arg2 = arg2;
+ return this;
+ }
+
+ public Bundle getArguments() {
+ Bundle __outBundle = new Bundle();
+ __outBundle.putString("arg1", this.arg1);
+ __outBundle.putString("arg2", this.arg2);
+ return __outBundle;
+ }
+
+ public int getActionId() {
+ return a.b.R.id.previous;
+ }
+ }
+
+ public static class Next implements NavDirections {
+ private String main;
+
+ private String optional = "bla";
+
+ public Next(String main) {
+ this.main = main;
+ }
+
+ public Next setMain(String main) {
+ this.main = main;
+ return this;
+ }
+
+ public Next setOptional(String optional) {
+ this.optional = optional;
+ return this;
+ }
+
+ public Bundle getArguments() {
+ Bundle __outBundle = new Bundle();
+ __outBundle.putString("main", this.main);
+ __outBundle.putString("optional", this.optional);
+ return __outBundle;
+ }
+
+ public int getActionId() {
+ return a.b.R.id.next;
+ }
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/test-data/expected/Next.java b/navigation/safe-args-generator/src/tests/test-data/expected/Next.java
new file mode 100644
index 0000000..5bd185b
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/expected/Next.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 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 a.b;
+
+import android.os.Bundle;
+import androidx.navigation.NavDirections;
+import java.lang.String;
+
+public static class Next implements NavDirections {
+ private String main;
+
+ private int mainInt;
+
+ private String optional = "bla";
+
+ private int optionalInt = 239;
+
+ public Next(String main, int mainInt) {
+ this.main = main;
+ this.mainInt = mainInt;
+ }
+
+ public Next setMain(String main) {
+ this.main = main;
+ return this;
+ }
+
+ public Next setMainInt(int mainInt) {
+ this.mainInt = mainInt;
+ return this;
+ }
+
+ public Next setOptional(String optional) {
+ this.optional = optional;
+ return this;
+ }
+
+ public Next setOptionalInt(int optionalInt) {
+ this.optionalInt = optionalInt;
+ return this;
+ }
+
+ public Bundle getArguments() {
+ Bundle __outBundle = new Bundle();
+ __outBundle.putString("main", this.main);
+ __outBundle.putInt("mainInt", this.mainInt);
+ __outBundle.putString("optional", this.optional);
+ __outBundle.putInt("optionalInt", this.optionalInt);
+ return __outBundle;
+ }
+
+ public int getActionId() {
+ return a.b.R.id.next;
+ }
+}
\ No newline at end of file
diff --git a/navigation/safe-args-generator/src/tests/test-data/naive_test.xml b/navigation/safe-args-generator/src/tests/test-data/naive_test.xml
new file mode 100644
index 0000000..7cef9b0
--- /dev/null
+++ b/navigation/safe-args-generator/src/tests/test-data/naive_test.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2017 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ app:startDestination="@+id/first_screen">
+ <fragment android:id="@+id/first_screen"
+ android:name="androidx.navigation.testapp.MainFragment">
+ <argument android:name="myarg1" android:defaultValue="one" />
+ <action android:id="@+id/next" app:destination="@+id/next_fragment">
+ <argument android:name="myarg2" app:type="string"/>
+ <argument android:name="randomArgument"/>
+ <argument android:name="intArgument" app:type="integer" android:defaultValue="261"/>
+ </action>
+ </fragment>
+ <fragment android:id="@+id/next_fragment"
+ android:name=".NextFragment">
+ <argument android:name="myarg2" />
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
+ <action android:id="@+id/finish" app:popUpTo="@id/first_screen" />
+ </fragment>
+</navigation>
\ No newline at end of file
diff --git a/navigation/safe-args-gradle-plugin/build.gradle b/navigation/safe-args-gradle-plugin/build.gradle
new file mode 100644
index 0000000..62dc6e2
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+import androidx.build.SupportConfig
+
+apply plugin: androidx.build.SupportKotlinLibraryPlugin
+apply plugin: 'java-gradle-plugin'
+
+ext.generatedResources = "$buildDir/generated/resources"
+
+sourceSets {
+ test.java.srcDirs += 'src/tests/kotlin'
+ test.resources.srcDirs += generatedResources
+}
+
+dependencies {
+ compile build_libs.gradle
+ compile project(":navigation:navigation-safe-args-generator")
+ compile gradleApi()
+ compile(GSON)
+ testCompile gradleTestKit()
+ testCompile(JUNIT)
+}
+
+task generateSdkResource() {
+ inputs.property("compileSdkVersion", SupportConfig.CURRENT_SDK_VERSION)
+ inputs.property("buildToolsVersion", SupportConfig.BUILD_TOOLS_VERSION)
+ outputs.dir(generatedResources)
+ doLast {
+ // Properties.write will have a timestamp, that invalidates the task,
+ // so we don't use it and write a file manually
+ new File(generatedResources, "sdk.prop").withWriter('UTF-8') { writer ->
+ writer.write("compileSdkVersion=$SupportConfig.CURRENT_SDK_VERSION\n")
+ writer.write("buildToolsVersion=$SupportConfig.BUILD_TOOLS_VERSION\n")
+ }
+ }
+}
+
+test {
+ testLogging { showStandardStreams = true }
+}
+
+tasks["compileTestJava"].dependsOn generateSdkResource
+
+gradlePlugin {
+ plugins {
+ safeargs {
+ id = "androidx.navigation.safeargs"
+ implementationClass = "androidx.navigation.safeargs.gradle.SafeArgsPlugin"
+ }
+ }
+}
+
+supportLibrary {
+ name = "Android Navigation TypeSafe Arguments Gradle Plugin"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2017"
+ description = "Android Navigation TypeSafe Arguments Gradle Plugin"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/ArgumentsGenerationTask.kt b/navigation/safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/ArgumentsGenerationTask.kt
new file mode 100644
index 0000000..ee71478
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/ArgumentsGenerationTask.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.navigation.safeargs.gradle
+
+import androidx.navigation.safe.args.generator.generateSafeArgs
+import com.android.build.gradle.internal.tasks.IncrementalTask
+import com.android.ide.common.resources.FileStatus
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import org.gradle.api.GradleException
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputDirectory
+import java.io.File
+
+private const val MAPPING_FILE = "file_mappings.json"
+
+open class ArgumentsGenerationTask : IncrementalTask() {
+ @get:Input
+ lateinit var rFilePackage: String
+
+ @get:Input
+ lateinit var applicationId: String
+
+ @get:OutputDirectory
+ lateinit var outputDir: File
+
+ @get:InputFiles
+ var navigationFiles: List<File> = emptyList()
+
+ private fun generateArgs(navFiles: Collection<File>, out: File) = navFiles.map { file ->
+ Mapping(file.relativeTo(project.projectDir).path,
+ generateSafeArgs(rFilePackage, applicationId, file, out))
+ }
+
+ private fun writeMappings(mappings: List<Mapping>) {
+ File(incrementalFolder, MAPPING_FILE).writer().use { Gson().toJson(mappings, it) }
+ }
+
+ private fun readMappings(): List<Mapping> {
+ val type = object : TypeToken<List<Mapping>>() {}.type
+ val mappingsFile = File(incrementalFolder, MAPPING_FILE)
+ if (mappingsFile.exists()) {
+ return mappingsFile.reader().use { Gson().fromJson(it, type) }
+ } else {
+ return emptyList()
+ }
+ }
+
+ override fun doFullTaskAction() {
+ if (outputDir.exists() && !outputDir.deleteRecursively()) {
+ project.logger.warn("Failed to clear directory for navigation arguments")
+ }
+ if (!outputDir.exists() && !outputDir.mkdirs()) {
+ throw GradleException("Failed to create directory for navigation arguments")
+ }
+ val mappings = generateArgs(navigationFiles, outputDir)
+ writeMappings(mappings)
+ }
+
+ override fun doIncrementalTaskAction(changedInputs: MutableMap<File, FileStatus>) {
+ super.doIncrementalTaskAction(changedInputs)
+ val oldMapping = readMappings()
+ val navFiles = changedInputs.filter { (_, status) -> status != FileStatus.REMOVED }.keys
+ val newMapping = generateArgs(navFiles, outputDir)
+ val newJavaFiles = newMapping.flatMap { it.javaFiles }.toSet()
+ val (modified, unmodified) = oldMapping.partition {
+ File(project.projectDir, it.navFile) in changedInputs
+ }
+ modified.flatMap { it.javaFiles }
+ .filter { name -> name !in newJavaFiles }
+ .forEach { javaName ->
+ val fileName = "${javaName.replace('.', File.separatorChar)}.java"
+ val file = File(outputDir, fileName)
+ if (file.exists()) {
+ file.delete()
+ }
+ }
+ writeMappings(unmodified + newMapping)
+ }
+
+ override fun isIncremental() = true
+}
+
+private data class Mapping(val navFile: String, val javaFiles: List<String>)
diff --git a/navigation/safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt b/navigation/safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
new file mode 100644
index 0000000..686b6b9
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
@@ -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.navigation.safeargs.gradle
+
+import com.android.build.gradle.AppExtension
+import com.android.build.gradle.api.BaseVariant
+import groovy.util.XmlSlurper
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import java.io.File
+
+private const val PLUGIN_DIRNAME = "navigation-args"
+internal const val GENERATED_PATH = "generated/source/$PLUGIN_DIRNAME"
+internal const val INTERMEDIATES_PATH = "intermediates/$PLUGIN_DIRNAME"
+
+@Suppress("unused")
+class SafeArgsPlugin : Plugin<Project> {
+
+ override fun apply(project: Project) {
+ val appExtension = project.extensions.findByType(AppExtension::class.java)
+ ?: throw GradleException("safeargs plugin must be used with android plugin")
+ appExtension.applicationVariants.all { variant ->
+ val task = project.tasks.create("generateSafeArgs${variant.name.capitalize()}",
+ ArgumentsGenerationTask::class.java) { task ->
+ task.rFilePackage = variant.rFilePackage()
+ task.applicationId = variant.applicationId
+ task.navigationFiles = navigationFiles(variant)
+ task.outputDir = File(project.buildDir, "$GENERATED_PATH/${variant.dirName}")
+ task.incrementalFolder = File(project.buildDir,
+ "$INTERMEDIATES_PATH/${variant.dirName}")
+ task.variantName = variant.name
+ }
+ variant.registerJavaGeneratingTask(task, task.outputDir)
+ }
+ }
+}
+
+private fun navigationFiles(variant: BaseVariant) = variant.sourceSets
+ .flatMap { it.resDirectories }
+ .mapNotNull {
+ File(it, "navigation").let { navFolder ->
+ if (navFolder.exists() && navFolder.isDirectory) navFolder else null
+ }
+ }
+ .flatMap { navFolder -> navFolder.listFiles().asIterable() }
+ .groupBy { file -> file.name }
+ .map { entry -> entry.value.last() }
+
+private fun BaseVariant.rFilePackage(): String {
+ val mainSourceSet = sourceSets.find { it.name == "main" }
+ val sourceSet = mainSourceSet ?: sourceSets[0]
+ val manifest = sourceSet.manifestFile
+ val parsed = XmlSlurper(false, false).parse(manifest)
+ return parsed.getProperty("@package").toString()
+}
\ No newline at end of file
diff --git a/navigation/safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/PluginTest.kt b/navigation/safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/PluginTest.kt
new file mode 100644
index 0000000..f490951
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/src/test/kotlin/androidx/navigation/safeargs/gradle/PluginTest.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.navigation.safeargs.gradle
+
+import org.gradle.testkit.runner.BuildResult
+import org.gradle.testkit.runner.GradleRunner
+import org.gradle.testkit.runner.TaskOutcome
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.CoreMatchers.not
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.File
+import java.util.Properties
+
+private const val MAIN_DIR = "androidx/navigation/testapp"
+
+private const val NEXT_DIRECTIONS = "$MAIN_DIR/NextFragmentDirections.java"
+private const val MAIN_DIRECTIONS = "$MAIN_DIR/MainFragmentDirections.java"
+private const val MODIFIED_NEXT_DIRECTIONS = "$MAIN_DIR/ModifiedNextFragmentDirections.java"
+private const val ADDITIONAL_DIRECTIONS = "$MAIN_DIR/AdditionalFragmentDirections.java"
+private const val FOO_DIRECTIONS = "safe/gradle/test/app/foo/FooFragmentDirections.java"
+
+private const val NAV_RESOURCES = "src/main/res/navigation"
+private const val SEC = 1000L
+
+// Does not work in the Android Studio
+@RunWith(JUnit4::class)
+class PluginTest {
+
+ @Suppress("MemberVisibilityCanPrivate")
+ @get:Rule
+ val testProjectDir = TemporaryFolder()
+
+ private var buildFile: File = File("")
+ private var compileSdkVersion = ""
+ private var buildToolsVersion = ""
+
+ private fun projectRoot(): File = testProjectDir.root
+
+ private fun assertGenerated(name: String) = assertExists(name, true)
+
+ private fun assertNotGenerated(name: String) = assertExists(name, false)
+
+ private fun assertExists(name: String, ex: Boolean): File {
+ val generatedFile = File(projectRoot(), "build/$GENERATED_PATH/$name")
+ assertThat(generatedFile.exists(), `is`(ex))
+ return generatedFile
+ }
+
+ private fun navResource(name: String) = File(projectRoot(), "$NAV_RESOURCES/$name")
+
+ private fun runGradle(vararg args: String) = GradleRunner.create()
+ .withProjectDir(projectRoot()).withPluginClasspath().withArguments(*args).build()
+
+ @Before
+ fun setup() {
+ projectRoot().mkdirs()
+ buildFile = File(projectRoot(), "build.gradle")
+ buildFile.createNewFile()
+ // copy local.properties
+ File("../../app-toolkit/local.properties").copyTo(File(projectRoot(),
+ "local.properties"), overwrite = true)
+ val stream = PluginTest::class.java.classLoader.getResourceAsStream("sdk.prop")
+ val properties = Properties()
+ properties.load(stream)
+ compileSdkVersion = properties.getProperty("compileSdkVersion")
+ buildToolsVersion = properties.getProperty("buildToolsVersion")
+ testData("app-project").copyRecursively(projectRoot())
+ }
+
+ private fun setupSimpleBuildGradle() {
+ buildFile.writeText("""
+ plugins {
+ id('com.android.application')
+ id('androidx.navigation.safeargs')
+ }
+
+ android {
+ compileSdkVersion $compileSdkVersion
+ buildToolsVersion "$buildToolsVersion"
+ }
+ """.trimIndent())
+ }
+
+ @Test
+ fun runGenerateTask() {
+ buildFile.writeText("""
+ plugins {
+ id('com.android.application')
+ id('androidx.navigation.safeargs')
+ }
+
+ android {
+ compileSdkVersion $compileSdkVersion
+ buildToolsVersion "$buildToolsVersion"
+ flavorDimensions "mode"
+ productFlavors {
+ foo {
+ dimension "mode"
+ applicationIdSuffix ".foo"
+ }
+ notfoo {
+ dimension "mode"
+ }
+
+ }
+ }
+ """.trimIndent())
+
+ runGradle("generateSafeArgsNotfooDebug", "generateSafeArgsFooDebug")
+ .assertSuccessfulTask("generateSafeArgsNotfooDebug")
+ .assertSuccessfulTask("generateSafeArgsFooDebug")
+
+ assertGenerated("notfoo/debug/$NEXT_DIRECTIONS")
+ assertNotGenerated("foo/debug/$NEXT_DIRECTIONS")
+ assertGenerated("foo/debug/$FOO_DIRECTIONS")
+ }
+
+ @Test
+ fun incrementalAdd() {
+ setupSimpleBuildGradle()
+ runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
+ val nextLastMod = assertGenerated("debug/$NEXT_DIRECTIONS").lastModified()
+
+ testData("incremental-test-data/add_nav.xml").copyTo(navResource("add_nav.xml"))
+
+ // lastModified has one second precision on certain platforms and jdk versions
+ // so sleep for a second
+ Thread.sleep(SEC)
+ runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
+ assertGenerated("debug/$ADDITIONAL_DIRECTIONS")
+ val newNextLastMod = assertGenerated("debug/$NEXT_DIRECTIONS").lastModified()
+ assertThat(newNextLastMod, `is`(nextLastMod))
+ }
+
+ @Test
+ fun incrementalModify() {
+ setupSimpleBuildGradle()
+ testData("incremental-test-data/add_nav.xml").copyTo(navResource("add_nav.xml"))
+
+ runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
+ val mainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
+ val additionalLastMod = assertGenerated("debug/$ADDITIONAL_DIRECTIONS").lastModified()
+ assertGenerated("debug/$NEXT_DIRECTIONS")
+
+ testData("incremental-test-data/modified_nav.xml").copyTo(navResource("nav_test.xml"), true)
+
+ // lastModified has one second precision on certain platforms and jdk versions
+ // so sleep for a second
+ Thread.sleep(SEC)
+ runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
+ val newMainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
+ // main directions were regenerated
+ assertThat(newMainLastMod, not(mainLastMod))
+
+ // but additional directions weren't touched
+ val newAdditionalLastMod = assertGenerated("debug/$ADDITIONAL_DIRECTIONS").lastModified()
+ assertThat(newAdditionalLastMod, `is`(additionalLastMod))
+
+ assertGenerated("debug/$MODIFIED_NEXT_DIRECTIONS")
+ assertNotGenerated("debug/$NEXT_DIRECTIONS")
+ }
+
+ @Test
+ fun incrementalRemove() {
+ setupSimpleBuildGradle()
+ testData("incremental-test-data/add_nav.xml").copyTo(navResource("add_nav.xml"))
+
+ runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
+ val mainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
+ assertGenerated("debug/$ADDITIONAL_DIRECTIONS")
+
+ val wasRemoved = navResource("add_nav.xml").delete()
+ assertThat(wasRemoved, `is`(true))
+
+ // lastModified has one second precision on certain platforms and jdk versions
+ // so sleep for a second
+ Thread.sleep(SEC)
+ runGradle("generateSafeArgsDebug").assertSuccessfulTask("generateSafeArgsDebug")
+ val newMainLastMod = assertGenerated("debug/$MAIN_DIRECTIONS").lastModified()
+ // main directions weren't touched
+ assertThat(newMainLastMod, `is`(mainLastMod))
+
+ // but additional directions are removed
+ assertNotGenerated("debug/$ADDITIONAL_DIRECTIONS")
+ }
+}
+
+private fun testData(name: String) = File("src/test/test-data", name)
+
+private fun BuildResult.assertSuccessfulTask(name: String): BuildResult {
+ assertThat(task(":$name")!!.outcome, `is`(TaskOutcome.SUCCESS))
+ return this
+}
\ No newline at end of file
diff --git a/navigation/safe-args-gradle-plugin/src/test/test-data/IGNORE_CHECKSTYLE b/navigation/safe-args-gradle-plugin/src/test/test-data/IGNORE_CHECKSTYLE
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/src/test/test-data/IGNORE_CHECKSTYLE
diff --git a/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/foo/res/navigation/nav_test.xml b/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/foo/res/navigation/nav_test.xml
new file mode 100644
index 0000000..a3f8210
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/foo/res/navigation/nav_test.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ app:startDestination="@+id/first_screen">
+ <fragment android:id="@+id/first_screen"
+ android:name="androidx.navigation.testapp.MainFragment">
+ <argument android:name="myarg1" android:defaultValue="one" />
+ <action android:id="@+id/next" app:destination="@+id/next_fragment">
+ <argument android:name="myarg2"/>
+ <argument android:name="randomArgument"/>
+ </action>
+ </fragment>
+ <fragment android:id="@+id/next_fragment"
+ android:name=".FooFragment">
+ <argument android:name="myarg2" />
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
+ <action android:id="@+id/finish" app:popUpTo="@id/first_screen" />
+ </fragment>
+</navigation>
\ No newline at end of file
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/main/AndroidManifest.xml
similarity index 68%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/main/AndroidManifest.xml
index 660dbcd..92b9c99 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ 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.
@@ -14,6 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="safe.gradle.test.app">
+ <application
+ android:allowBackup="true"
+ android:supportsRtl="true">
+ </application>
+</manifest>
diff --git a/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/main/java/foo/Bar.java b/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/main/java/foo/Bar.java
new file mode 100644
index 0000000..9410272
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/main/java/foo/Bar.java
@@ -0,0 +1,5 @@
+package foo;
+
+class Bar {
+
+}
\ No newline at end of file
diff --git a/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/main/res/navigation/nav_test.xml b/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/main/res/navigation/nav_test.xml
new file mode 100644
index 0000000..1e38990
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/src/test/test-data/app-project/src/main/res/navigation/nav_test.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ app:startDestination="@+id/first_screen">
+ <fragment android:id="@+id/first_screen"
+ android:name="androidx.navigation.testapp.MainFragment">
+ <argument android:name="myarg1" android:defaultValue="one" />
+ <action android:id="@+id/next" app:destination="@+id/next_fragment">
+ <argument android:name="myarg2"/>
+ <argument android:name="randomArgument"/>
+ </action>
+ </fragment>
+ <fragment android:id="@+id/next_fragment"
+ android:name="androidx.navigation.testapp.NextFragment">
+ <argument android:name="myarg2" />
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
+ <action android:id="@+id/finish" app:popUpTo="@id/first_screen" />
+ </fragment>
+</navigation>
\ No newline at end of file
diff --git a/navigation/safe-args-gradle-plugin/src/test/test-data/incremental-test-data/add_nav.xml b/navigation/safe-args-gradle-plugin/src/test/test-data/incremental-test-data/add_nav.xml
new file mode 100644
index 0000000..695f195
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/src/test/test-data/incremental-test-data/add_nav.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ app:startDestination="@+id/additional_screen">
+ <fragment android:id="@+id/additional_screen"
+ android:name="androidx.navigation.testapp.AdditionalFragment">
+ <argument android:name="bar" android:defaultValue="one" />
+ <action android:id="@+id/foo_action" app:destination="@+id/next_fragment">
+ <argument android:name="foo" />
+ </action>
+ </fragment>
+</navigation>
\ No newline at end of file
diff --git a/navigation/safe-args-gradle-plugin/src/test/test-data/incremental-test-data/modified_nav.xml b/navigation/safe-args-gradle-plugin/src/test/test-data/incremental-test-data/modified_nav.xml
new file mode 100644
index 0000000..9c27169
--- /dev/null
+++ b/navigation/safe-args-gradle-plugin/src/test/test-data/incremental-test-data/modified_nav.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ app:startDestination="@+id/first_screen">
+ <fragment android:id="@+id/first_screen"
+ android:name="androidx.navigation.testapp.MainFragment">
+ <argument android:name="myarg1" android:defaultValue="one" />
+ <action android:id="@+id/next" app:destination="@+id/next_fragment">
+ <argument android:name="randomArgument"/>
+ </action>
+ </fragment>
+ <fragment android:id="@+id/next_fragment"
+ android:name="androidx.navigation.testapp.ModifiedNextFragment">
+ <action android:id="@+id/next" app:destination="@+id/first_screen"/>
+ <action android:id="@+id/finish" app:popUpTo="@id/first_screen" />
+ </fragment>
+</navigation>
\ No newline at end of file
diff --git a/navigation/testing/build.gradle b/navigation/testing/build.gradle
new file mode 100644
index 0000000..923da88
--- /dev/null
+++ b/navigation/testing/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ api(project(":navigation:navigation-common"))
+
+ testImplementation(JUNIT)
+ testImplementation(MOCKITO_CORE)
+ testImplementation(TEST_RUNNER)
+
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+ name = "Android Navigation Testing"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2017"
+ description = "Android Navigation-Testing"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/testing/ktx/build.gradle b/navigation/testing/ktx/build.gradle
new file mode 100644
index 0000000..d37cbef
--- /dev/null
+++ b/navigation/testing/ktx/build.gradle
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(project(":navigation:navigation-testing"))
+ // Ensure that the -ktx dependency graph mirrors the Java dependency graph
+ api(project(":navigation:navigation-common-ktx"))
+ api(KOTLIN_STDLIB)
+
+ testImplementation(JUNIT)
+ testImplementation(TEST_RUNNER)
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+ name = "Android Navigation Testing Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2018"
+ description = "Android Navigation-Testing-Ktx"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/testing/ktx/src/androidTest/AndroidManifest.xml
similarity index 75%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/testing/ktx/src/androidTest/AndroidManifest.xml
index 660dbcd..d963bb6 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/testing/ktx/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.testing.ktx.test">
+
+</manifest>
diff --git a/navigation/testing/ktx/src/androidTest/java/androidx/navigation/testing/TestNavigatorDestinationBuilderTest.kt b/navigation/testing/ktx/src/androidTest/java/androidx/navigation/testing/TestNavigatorDestinationBuilderTest.kt
new file mode 100644
index 0000000..20bba5a
--- /dev/null
+++ b/navigation/testing/ktx/src/androidTest/java/androidx/navigation/testing/TestNavigatorDestinationBuilderTest.kt
@@ -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.navigation.testing
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.filters.SmallTest
+import android.support.test.runner.AndroidJUnit4
+import androidx.navigation.contains
+import androidx.navigation.get
+import androidx.navigation.navigation
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TestNavigatorDestinationBuilderTest {
+ private val provider = TestNavigatorProvider(InstrumentationRegistry.getTargetContext())
+
+ @Test
+ fun test() {
+ val graph = provider.navigation(startDestination = DESTINATION_ID) {
+ test(DESTINATION_ID)
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ }
+
+ @Test
+ fun testWithBody() {
+ val graph = provider.navigation(startDestination = DESTINATION_ID) {
+ test(DESTINATION_ID) {
+ label = LABEL
+ }
+ }
+ assertTrue("Destination should be added to the graph",
+ DESTINATION_ID in graph)
+ assertEquals("Destination should have label set",
+ LABEL, graph[DESTINATION_ID].label)
+ }
+}
+
+private const val DESTINATION_ID = 1
+private const val LABEL = "Test"
diff --git a/car/res/drawable/car_button_ripple_background.xml b/navigation/testing/ktx/src/main/AndroidManifest.xml
similarity index 83%
copy from car/res/drawable/car_button_ripple_background.xml
copy to navigation/testing/ktx/src/main/AndroidManifest.xml
index 13d0a49..35a5d64 100644
--- a/car/res/drawable/car_button_ripple_background.xml
+++ b/navigation/testing/ktx/src/main/AndroidManifest.xml
@@ -14,6 +14,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background" />
+<manifest package="androidx.navigation.testing.ktx"/>
diff --git a/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigator.kt b/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigator.kt
new file mode 100644
index 0000000..fc46653
--- /dev/null
+++ b/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigator.kt
@@ -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.navigation.testing
+
+import android.os.Bundle
+
+/**
+ * Get the [TestNavigator] back stack as a [List] of [destination and argument pairs][Pair].
+ */
+val TestNavigator.backStack: List<Pair<TestNavigator.Destination, Bundle?>>
+ get() = mBackStack.map { Pair(it.first!!, it.second) }
diff --git a/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigatorDestinationBuilder.kt b/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigatorDestinationBuilder.kt
new file mode 100644
index 0000000..ce8ae41
--- /dev/null
+++ b/navigation/testing/ktx/src/main/java/androidx/navigation/testing/TestNavigatorDestinationBuilder.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation.testing
+
+import android.support.annotation.IdRes
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.get
+
+/**
+ * Construct a new [TestNavigator.Destination]
+ */
+inline fun NavGraphBuilder.test(@IdRes id: Int) = test(id) {}
+
+/**
+ * Construct a new [TestNavigator.Destination]
+ */
+inline fun NavGraphBuilder.test(
+ @IdRes id: Int,
+ block: TestNavigatorDestinationBuilder.() -> Unit
+) = destination(TestNavigatorDestinationBuilder(provider[TestNavigator::class], id).apply(block))
+
+/**
+ * DSL for constructing a new [TestNavigator.Destination]
+ */
+@NavDestinationDsl
+class TestNavigatorDestinationBuilder(
+ navigator: TestNavigator,
+ @IdRes id: Int
+) : NavDestinationBuilder<TestNavigator.Destination>(navigator, id)
diff --git a/navigation/testing/ktx/src/test/java/androidx/navigation/testing/TestNavigatorTest.kt b/navigation/testing/ktx/src/test/java/androidx/navigation/testing/TestNavigatorTest.kt
new file mode 100644
index 0000000..80cdb4f
--- /dev/null
+++ b/navigation/testing/ktx/src/test/java/androidx/navigation/testing/TestNavigatorTest.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 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.navigation.testing
+
+import android.os.Bundle
+import android.support.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+@SmallTest
+class TestNavigatorTest {
+
+ @Test
+ fun backStack() {
+ val testNavigator = TestNavigator()
+ val destination = testNavigator.createDestination()
+ val args = Bundle()
+ testNavigator.navigate(destination, args, null)
+ assertEquals("TestNavigator back stack size is 1 after navigate",
+ 1,
+ testNavigator.backStack.size)
+ val (foundDestination, foundArgs) = testNavigator.backStack.last()
+ assertEquals("last() returns last destination navigated to",
+ destination, foundDestination)
+ assertEquals("last() returns arguments Bundle",
+ args, foundArgs)
+ }
+}
diff --git a/car/res/drawable/car_button_ripple_background_day.xml b/navigation/testing/src/main/AndroidManifest.xml
similarity index 76%
copy from car/res/drawable/car_button_ripple_background_day.xml
copy to navigation/testing/src/main/AndroidManifest.xml
index 16b1d0c..248fd5a 100644
--- a/car/res/drawable/car_button_ripple_background_day.xml
+++ b/navigation/testing/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_dark" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.testing">
+
+</manifest>
diff --git a/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigator.java b/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigator.java
new file mode 100644
index 0000000..68a4994
--- /dev/null
+++ b/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigator.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 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.navigation.testing;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.util.Pair;
+
+import androidx.navigation.NavDestination;
+import androidx.navigation.NavOptions;
+import androidx.navigation.Navigator;
+
+import java.util.ArrayDeque;
+
+/**
+ * A simple Navigator that doesn't actually navigate anywhere, but does dispatch correctly
+ */
+@Navigator.Name("test")
+public class TestNavigator extends Navigator<TestNavigator.Destination> {
+
+ public final ArrayDeque<Pair<Destination, Bundle>> mBackStack = new ArrayDeque<>();
+
+ @NonNull
+ @Override
+ public Destination createDestination() {
+ return new Destination(this);
+ }
+
+ @Override
+ public void navigate(@NonNull Destination destination, Bundle args,
+ NavOptions navOptions) {
+ if (navOptions != null && navOptions.shouldLaunchSingleTop() && !mBackStack.isEmpty()
+ && mBackStack.peekLast().first.getId() == destination.getId()) {
+ mBackStack.pop();
+ mBackStack.add(new Pair<>(destination, args));
+ dispatchOnNavigatorNavigated(destination.getId(), BACK_STACK_UNCHANGED);
+ } else {
+ mBackStack.add(new Pair<>(destination, args));
+ dispatchOnNavigatorNavigated(destination.getId(), BACK_STACK_DESTINATION_ADDED);
+ }
+ }
+
+ @Override
+ public boolean popBackStack() {
+ boolean popped = mBackStack.pollLast() != null;
+ if (popped) {
+ dispatchOnNavigatorNavigated(mBackStack.isEmpty()
+ ? 0
+ : mBackStack.peekLast().first.getId(),
+ BACK_STACK_DESTINATION_POPPED);
+ }
+ return popped;
+ }
+
+ /**
+ * A simple Test destination
+ */
+ public static class Destination extends NavDestination {
+ /**
+ * NavDestinations should be created via {@link Navigator#createDestination}.
+ */
+ Destination(@NonNull Navigator<? extends NavDestination> navigator) {
+ super(navigator);
+ }
+ }
+}
diff --git a/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigatorProvider.java b/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigatorProvider.java
new file mode 100644
index 0000000..5608632
--- /dev/null
+++ b/navigation/testing/src/main/java/androidx/navigation/testing/TestNavigatorProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 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.navigation.testing;
+
+import android.content.Context;
+
+import androidx.navigation.NavGraphNavigator;
+import androidx.navigation.SimpleNavigatorProvider;
+
+/**
+ * Simple NavigatorProvider that only supports <navigation> and <test> navigation
+ * elements.
+ */
+public class TestNavigatorProvider extends SimpleNavigatorProvider {
+
+ public TestNavigatorProvider(Context context) {
+ addNavigator(new NavGraphNavigator(context));
+ addNavigator(new TestNavigator());
+ }
+}
diff --git a/navigation/ui/build.gradle b/navigation/ui/build.gradle
new file mode 100644
index 0000000..deb21c0
--- /dev/null
+++ b/navigation/ui/build.gradle
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+}
+
+dependencies {
+ api(project(":navigation:navigation-runtime"))
+ api(NAV_SUPPORT_DESIGN)
+
+ testImplementation(JUNIT)
+ testImplementation(MOCKITO_CORE)
+ testImplementation(TEST_RUNNER)
+
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+ name = "Android Navigation UI"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2018"
+ description = "Android Navigation-UI"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/navigation/ui/ktx/build.gradle b/navigation/ui/ktx/build.gradle
new file mode 100644
index 0000000..6de2e66
--- /dev/null
+++ b/navigation/ui/ktx/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
+import androidx.build.SupportLibraryExtension
+
+plugins {
+ id("SupportAndroidLibraryPlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ buildTypes {
+ debug {
+ testCoverageEnabled = false // Breaks Kotlin compiler.
+ }
+ }
+}
+
+dependencies {
+ api(project(":navigation:navigation-ui"))
+ // Ensure that the -ktx dependency graph mirrors the Java dependency graph
+ api(project(":navigation:navigation-runtime-ktx"))
+ api(KOTLIN_STDLIB)
+ androidTestImplementation(TEST_RUNNER)
+ androidTestImplementation(ESPRESSO_CORE)
+}
+
+supportLibrary {
+ name = "Android Navigation UI Kotlin Extensions"
+ publish = true
+ mavenVersion = LibraryVersions.NAVIGATION
+ mavenGroup = LibraryGroups.NAVIGATION
+ inceptionYear = "2018"
+ description = "Android Navigation-UI-Ktx"
+ url = SupportLibraryExtension.ARCHITECTURE_URL
+}
diff --git a/car/res/drawable/car_button_ripple_background.xml b/navigation/ui/ktx/src/main/AndroidManifest.xml
similarity index 83%
copy from car/res/drawable/car_button_ripple_background.xml
copy to navigation/ui/ktx/src/main/AndroidManifest.xml
index 13d0a49..120cc3f 100644
--- a/car/res/drawable/car_button_ripple_background.xml
+++ b/navigation/ui/ktx/src/main/AndroidManifest.xml
@@ -14,6 +14,4 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background" />
+<manifest package="androidx.navigation.ui.ktx"/>
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/Activity.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/Activity.kt
new file mode 100644
index 0000000..6e530c6
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/Activity.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.navigation.ui
+
+import android.support.v4.widget.DrawerLayout
+import android.support.v7.app.AppCompatActivity
+import androidx.navigation.NavController
+
+/**
+ * Sets up the ActionBar returned by [AppCompatActivity.getSupportActionBar] for use
+ * with a [NavController].
+ *
+ * By calling this method, the title in the action bar will automatically be updated when
+ * the destination changes (assuming there is a valid
+ * [label][androidx.navigation.NavDestination.getLabel]).
+ *
+ * The action bar will also display the Up button when you are on a non-root destination and
+ * the drawer icon when on the root destination, automatically animating between them.
+ * Call [DrawerLayout.navigateUp] to handle the Up button.
+ *
+ * @param navController The NavController whose navigation actions will be reflected
+ * in the title of the action bar.
+ * @param drawerLayout The DrawerLayout that should be toggled from the home button
+ */
+fun AppCompatActivity.setupActionBarWithNavController(
+ navController: NavController,
+ drawerLayout: DrawerLayout? = null
+) {
+ NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
+}
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/BottomNavigationView.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/BottomNavigationView.kt
new file mode 100644
index 0000000..a23285b
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/BottomNavigationView.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.navigation.ui
+
+import android.support.design.widget.BottomNavigationView
+import androidx.navigation.NavController
+
+/**
+ * Sets up a [BottomNavigationView] for use with a [NavController]. This will call
+ * [android.view.MenuItem.onNavDestinationSelected] when a menu item is selected.
+ *
+ * The selected item in the NavigationView will automatically be updated when the destination
+ * changes.
+ */
+fun BottomNavigationView.setupWithNavController(navController: NavController) {
+ NavigationUI.setupWithNavController(this, navController)
+}
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/DrawerLayout.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/DrawerLayout.kt
new file mode 100644
index 0000000..c87acdc
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/DrawerLayout.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.navigation.ui
+
+import android.support.v4.widget.DrawerLayout
+import androidx.navigation.NavController
+
+/**
+ * Handles the Up button by delegating its behavior to the given [NavController].
+ *
+ * This is equivalent to calling [NavController.navigateUp] if the [DrawerLayout] is null.
+ *
+ * @return True if the [NavController] was able to navigate up.
+ */
+fun DrawerLayout?.navigateUp(navController: NavController): Boolean =
+ NavigationUI.navigateUp(this, navController)
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/MenuItem.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/MenuItem.kt
new file mode 100644
index 0000000..a989ee3
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/MenuItem.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.navigation.ui
+
+import android.view.MenuItem
+import androidx.navigation.NavController
+
+/**
+ * Attempt to navigate to the [NavDestination] associated with this [MenuItem].
+ *
+ * Importantly, it assumes the [menu item id][getItemId] matches a valid
+ * [action id][androidx.navigation.NavDestination.getAction] or
+ * [destination id][androidx.navigation.NavDestination.getId] to be navigated to.
+ *
+ * @return True if the [NavController] was able to navigate to the destination.
+ */
+fun MenuItem.onNavDestinationSelected(navController: NavController): Boolean =
+ NavigationUI.onNavDestinationSelected(this, navController)
diff --git a/navigation/ui/ktx/src/main/java/androidx/navigation/ui/NavigationView.kt b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/NavigationView.kt
new file mode 100644
index 0000000..71e1db5
--- /dev/null
+++ b/navigation/ui/ktx/src/main/java/androidx/navigation/ui/NavigationView.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.navigation.ui
+
+import android.support.design.widget.NavigationView
+import androidx.navigation.NavController
+
+/**
+ * Sets up a [NavigationView] for use with a [NavController]. This will call
+ * [android.view.MenuItem.onNavDestinationSelected] when a menu item is selected.
+ *
+ * The selected item in the NavigationView will automatically be updated when the destination
+ * changes.
+ */
+fun NavigationView.setupWithNavController(navController: NavController) {
+ NavigationUI.setupWithNavController(this, navController)
+}
diff --git a/car/res/drawable/car_button_ripple_background_day.xml b/navigation/ui/src/main/AndroidManifest.xml
similarity index 76%
copy from car/res/drawable/car_button_ripple_background_day.xml
copy to navigation/ui/src/main/AndroidManifest.xml
index 16b1d0c..4db5212 100644
--- a/car/res/drawable/car_button_ripple_background_day.xml
+++ b/navigation/ui/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,7 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_dark" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.navigation.ui">
+
+</manifest>
diff --git a/navigation/ui/src/main/java/androidx/navigation/ui/NavigationUI.java b/navigation/ui/src/main/java/androidx/navigation/ui/NavigationUI.java
new file mode 100644
index 0000000..c34dc65
--- /dev/null
+++ b/navigation/ui/src/main/java/androidx/navigation/ui/NavigationUI.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2017 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.navigation.ui;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.BottomNavigationView;
+import android.support.design.widget.NavigationView;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.graphics.drawable.DrawerArrowDrawable;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.ViewParent;
+
+import androidx.navigation.NavController;
+import androidx.navigation.NavDestination;
+import androidx.navigation.NavOptions;
+
+/**
+ * Class which hooks up elements typically in the 'chrome' of your application such as global
+ * navigation patterns like a navigation drawer or bottom nav bar with your {@link NavController}.
+ */
+public class NavigationUI {
+
+ // No instances. Static utilities only.
+ private NavigationUI() {
+ }
+
+ /**
+ * Attempt to navigate to the {@link NavDestination} associated with the given MenuItem. This
+ * MenuItem should have been added via one of the helper methods in this class.
+ *
+ * <p>Importantly, it assumes the {@link MenuItem#getItemId() menu item id} matches a valid
+ * {@link NavDestination#getAction(int) action id} or
+ * {@link NavDestination#getId() destination id} to be navigated to.</p>
+ *
+ * @param item The selected MenuItem.
+ * @param navController The NavController that hosts the destination.
+ * @return True if the {@link NavController} was able to navigate to the destination
+ * associated with the given MenuItem.
+ */
+ public static boolean onNavDestinationSelected(@NonNull MenuItem item,
+ @NonNull NavController navController) {
+ return onNavDestinationSelected(item, navController, false);
+ }
+
+ private static boolean onNavDestinationSelected(@NonNull MenuItem item,
+ @NonNull NavController navController, boolean popUp) {
+ NavOptions.Builder builder = new NavOptions.Builder()
+ .setPopUpTo(navController.getGraph().getStartDestination(), false)
+ .setLaunchSingleTop(true)
+ .setEnterAnim(R.anim.nav_default_enter_anim)
+ .setExitAnim(R.anim.nav_default_exit_anim)
+ .setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
+ .setPopExitAnim(R.anim.nav_default_pop_exit_anim);
+ if (popUp) {
+ builder.setPopUpTo(navController.getGraph().getStartDestination(), false);
+ }
+ NavOptions options = builder.build();
+ try {
+ //TODO provide proper API instead of using Exceptions as Control-Flow.
+ navController.navigate(item.getItemId(), null, options);
+ return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Handles the Up button by delegating its behavior to the given NavController. This should
+ * generally be called from {@link AppCompatActivity#onSupportNavigateUp()}.
+ * <p>If you do not have a {@link DrawerLayout}, you should call
+ * {@link NavController#navigateUp()} directly.
+ *
+ * @param drawerLayout The DrawerLayout that should be opened if you are on the topmost level
+ * of the app.
+ * @param navController The NavController that hosts your content.
+ * @return True if the {@link NavController} was able to navigate up.
+ */
+ public static boolean navigateUp(@Nullable DrawerLayout drawerLayout,
+ @NonNull NavController navController) {
+ if (drawerLayout != null && navController.getCurrentDestination().getId()
+ == navController.getGraph().getStartDestination()) {
+ drawerLayout.openDrawer(GravityCompat.START);
+ return true;
+ } else {
+ return navController.navigateUp();
+ }
+ }
+
+ /**
+ * Sets up the ActionBar returned by {@link AppCompatActivity#getSupportActionBar()} for use
+ * with a {@link NavController}.
+ *
+ * <p>By calling this method, the title in the action bar will automatically be updated when
+ * the destination changes (assuming there is a valid {@link NavDestination#getLabel label}).
+ *
+ * <p>The action bar will also display the Up button when you are on a non-root destination.
+ * Call {@link #navigateUp(DrawerLayout, NavController)} to handle the Up button.
+ *
+ * @param activity The activity hosting the action bar that should be kept in sync with changes
+ * to the NavController.
+ * @param navController The NavController that supplies the secondary menu. Navigation actions
+ * on this NavController will be reflected in the title of the action bar.
+ */
+ public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
+ @NonNull NavController navController) {
+ setupActionBarWithNavController(activity, navController, null);
+ }
+
+ /**
+ * Sets up the ActionBar returned by {@link AppCompatActivity#getSupportActionBar()} for use
+ * with a {@link NavController}.
+ *
+ * <p>By calling this method, the title in the action bar will automatically be updated when
+ * the destination changes (assuming there is a valid {@link NavDestination#getLabel label}).
+ *
+ * <p>The action bar will also display the Up button when you are on a non-root destination and
+ * the drawer icon when on the root destination, automatically animating between them.
+ * Call {@link #navigateUp(DrawerLayout, NavController)} to handle the Up button.
+ * @param activity The activity hosting the action bar that should be kept in sync with changes
+ * to the NavController.
+ * @param navController The NavController whose navigation actions will be reflected
+ * in the title of the action bar.
+ * @param drawerLayout The DrawerLayout that should be toggled from the home button
+ */
+ public static void setupActionBarWithNavController(@NonNull AppCompatActivity activity,
+ @NonNull NavController navController,
+ @Nullable DrawerLayout drawerLayout) {
+ navController.addOnNavigatedListener(
+ new ActionBarOnNavigatedListener(activity, drawerLayout));
+ }
+
+ /**
+ * Sets up a {@link NavigationView} for use with a {@link NavController}. This will call
+ * {@link #onNavDestinationSelected(MenuItem, NavController)} when a menu item is selected.
+ * The selected item in the NavigationView will automatically be updated when the destination
+ * changes.
+ *
+ * @param navigationView The NavigationView that should be kept in sync with changes to the
+ * NavController.
+ * @param navController The NavController that supplies the primary and secondary menu.
+ * Navigation actions on this NavController will be reflected in the
+ * selected item in the NavigationView.
+ */
+ public static void setupWithNavController(@NonNull final NavigationView navigationView,
+ @NonNull final NavController navController) {
+ navigationView.setNavigationItemSelectedListener(
+ new NavigationView.OnNavigationItemSelectedListener() {
+ @Override
+ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+ boolean handled = onNavDestinationSelected(item, navController, true);
+ if (handled) {
+ ViewParent parent = navigationView.getParent();
+ if (parent instanceof DrawerLayout) {
+ ((DrawerLayout) parent).closeDrawer(navigationView);
+ }
+ }
+ return handled;
+ }
+ });
+ navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
+ @Override
+ public void onNavigated(@NonNull NavController controller,
+ @NonNull NavDestination destination) {
+ int destinationId = destination.getId();
+ Menu menu = navigationView.getMenu();
+ for (int h = 0, size = menu.size(); h < size; h++) {
+ MenuItem item = menu.getItem(h);
+ item.setChecked(item.getItemId() == destinationId);
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets up a {@link BottomNavigationView} for use with a {@link NavController}. This will call
+ * {@link #onNavDestinationSelected(MenuItem, NavController)} when a menu item is selected. The
+ * selected item in the BottomNavigationView will automatically be updated when the destination
+ * changes.
+ *
+ * @param bottomNavigationView The BottomNavigationView that should be kept in sync with
+ * changes to the NavController.
+ * @param navController The NavController that supplies the primary menu.
+ * Navigation actions on this NavController will be reflected in the
+ * selected item in the BottomNavigationView.
+ */
+ public static void setupWithNavController(
+ @NonNull final BottomNavigationView bottomNavigationView,
+ @NonNull final NavController navController) {
+ bottomNavigationView.setOnNavigationItemSelectedListener(
+ new BottomNavigationView.OnNavigationItemSelectedListener() {
+ @Override
+ public boolean onNavigationItemSelected(@NonNull MenuItem item) {
+ return onNavDestinationSelected(item, navController, true);
+ }
+ });
+ navController.addOnNavigatedListener(new NavController.OnNavigatedListener() {
+ @Override
+ public void onNavigated(@NonNull NavController controller,
+ @NonNull NavDestination destination) {
+ int destinationId = destination.getId();
+ Menu menu = bottomNavigationView.getMenu();
+ for (int h = 0, size = menu.size(); h < size; h++) {
+ MenuItem item = menu.getItem(h);
+ if (item.getItemId() == destinationId) {
+ item.setChecked(true);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * The OnNavigatedListener specifically for keeping the ActionBar updated. This handles both
+ * updating the title and updating the Up Indicator transitioning between the
+ */
+ private static class ActionBarOnNavigatedListener implements NavController.OnNavigatedListener {
+ private final AppCompatActivity mActivity;
+ @Nullable
+ private final DrawerLayout mDrawerLayout;
+ private DrawerArrowDrawable mArrowDrawable;
+ private ValueAnimator mAnimator;
+
+ ActionBarOnNavigatedListener(
+ @NonNull AppCompatActivity activity, @Nullable DrawerLayout drawerLayout) {
+ mActivity = activity;
+ mDrawerLayout = drawerLayout;
+ }
+
+ @Override
+ public void onNavigated(@NonNull NavController controller,
+ @NonNull NavDestination destination) {
+ ActionBar actionBar = mActivity.getSupportActionBar();
+ CharSequence title = destination.getLabel();
+ if (!TextUtils.isEmpty(title)) {
+ actionBar.setTitle(title);
+ }
+ boolean isStartDestination =
+ controller.getGraph().getStartDestination() == destination.getId();
+ actionBar.setDisplayHomeAsUpEnabled(mDrawerLayout != null || !isStartDestination);
+ setActionBarUpIndicator(mDrawerLayout != null && isStartDestination);
+ }
+
+ void setActionBarUpIndicator(boolean showAsDrawerIndicator) {
+ ActionBarDrawerToggle.Delegate delegate = mActivity.getDrawerToggleDelegate();
+ boolean animate = true;
+ if (mArrowDrawable == null) {
+ mArrowDrawable = new DrawerArrowDrawable(
+ delegate.getActionBarThemedContext());
+ delegate.setActionBarUpIndicator(mArrowDrawable, 0);
+ // We're setting the initial state, so skip the animation
+ animate = false;
+ }
+ float endValue = showAsDrawerIndicator ? 0f : 1f;
+ if (animate) {
+ float startValue = mArrowDrawable.getProgress();
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ mAnimator = ObjectAnimator.ofFloat(mArrowDrawable, "progress",
+ startValue, endValue);
+ mAnimator.start();
+ } else {
+ mArrowDrawable.setProgress(endValue);
+ }
+ }
+ }
+}
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/ui/src/main/res/anim/nav_default_enter_anim.xml
similarity index 69%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/ui/src/main/res/anim/nav_default_enter_anim.xml
index 660dbcd..ef3b3dc 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/ui/src/main/res/anim/nav_default_enter_anim.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha
+ android:duration="@android:integer/config_mediumAnimTime"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0"/>
+</set>
\ No newline at end of file
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/ui/src/main/res/anim/nav_default_exit_anim.xml
similarity index 69%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/ui/src/main/res/anim/nav_default_exit_anim.xml
index 660dbcd..f18bbd5 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/ui/src/main/res/anim/nav_default_exit_anim.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha
+ android:duration="@android:integer/config_mediumAnimTime"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"/>
+</set>
\ No newline at end of file
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/ui/src/main/res/anim/nav_default_pop_enter_anim.xml
similarity index 69%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/ui/src/main/res/anim/nav_default_pop_enter_anim.xml
index 660dbcd..ef3b3dc 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/ui/src/main/res/anim/nav_default_pop_enter_anim.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha
+ android:duration="@android:integer/config_mediumAnimTime"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0"/>
+</set>
\ No newline at end of file
diff --git a/car/res/drawable/car_button_ripple_background_inverse.xml b/navigation/ui/src/main/res/anim/nav_default_pop_exit_anim.xml
similarity index 69%
copy from car/res/drawable/car_button_ripple_background_inverse.xml
copy to navigation/ui/src/main/res/anim/nav_default_pop_exit_anim.xml
index 660dbcd..f18bbd5 100644
--- a/car/res/drawable/car_button_ripple_background_inverse.xml
+++ b/navigation/ui/src/main/res/anim/nav_default_pop_exit_anim.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- ~ Copyright (C) 2018 The Android Open Source Project
+ ~ Copyright (C) 2017 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.
@@ -14,6 +14,10 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
-<ripple
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@color/car_card_ripple_background_inverse" />
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha
+ android:duration="@android:integer/config_mediumAnimTime"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"/>
+</set>
\ No newline at end of file
diff --git a/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/AsyncListDifferTest.kt b/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/AsyncListDifferTest.kt
index 8d4415e..0e0656b 100644
--- a/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/AsyncListDifferTest.kt
+++ b/v7/recyclerview/src/androidTest/java/androidx/recyclerview/widget/AsyncListDifferTest.kt
@@ -20,7 +20,6 @@
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertSame
-import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -58,7 +57,7 @@
private val mMainThread = TestExecutor()
private val mBackgroundThread = TestExecutor()
- private fun <T> createDiffer(listUpdateCallback: ListUpdateCallback,
+ private fun <T> createHelper(listUpdateCallback: ListUpdateCallback,
diffCallback: DiffUtil.ItemCallback<T>): AsyncListDiffer<T> {
return AsyncListDiffer(listUpdateCallback,
AsyncDifferConfig.Builder<T>(diffCallback)
@@ -70,72 +69,72 @@
@Test
fun initialState() {
val callback = mock(ListUpdateCallback::class.java)
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
- assertEquals(0, differ.currentList.size)
+ val helper = createHelper(callback, STRING_DIFF_CALLBACK)
+ assertEquals(0, helper.currentList.size)
verifyZeroInteractions(callback)
}
@Test(expected = IndexOutOfBoundsException::class)
fun getEmpty() {
- val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
- differ.currentList[0]
+ val helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
+ helper.currentList[0]
}
@Test(expected = IndexOutOfBoundsException::class)
fun getNegative() {
- val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
- differ.submitList(listOf("a", "b"))
- differ.currentList[-1]
+ val helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
+ helper.submitList(listOf("a", "b"))
+ helper.currentList[-1]
}
@Test(expected = IndexOutOfBoundsException::class)
fun getPastEnd() {
- val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
- differ.submitList(listOf("a", "b"))
- differ.currentList[2]
+ val helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
+ helper.submitList(listOf("a", "b"))
+ helper.currentList[2]
}
@Test
fun getCurrentList() {
- val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
+ val helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
// null is emptyList
- assertSame(emptyList<String>(), differ.currentList)
+ assertSame(emptyList<String>(), helper.currentList)
// other list is wrapped
val list = listOf("a", "b")
- differ.submitList(list)
- assertEquals(list, differ.currentList)
- assertNotSame(list, differ.currentList)
+ helper.submitList(list)
+ assertEquals(list, helper.currentList)
+ assertNotSame(list, helper.currentList)
// null again, empty again
- differ.submitList(null)
- assertSame(emptyList<String>(), differ.currentList)
+ helper.submitList(null)
+ assertSame(emptyList<String>(), helper.currentList)
}
@Test(expected = UnsupportedOperationException::class)
fun mutateCurrentListEmpty() {
- val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
- differ.currentList[0] = ""
+ val helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
+ helper.currentList[0] = ""
}
@Test(expected = UnsupportedOperationException::class)
fun mutateCurrentListNonEmpty() {
- val differ = createDiffer(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
- differ.submitList(listOf("a"))
- differ.currentList[0] = ""
+ val helper = createHelper(IGNORE_CALLBACK, STRING_DIFF_CALLBACK)
+ helper.submitList(listOf("a"))
+ helper.currentList[0] = ""
}
@Test
fun submitListSimple() {
val callback = mock(ListUpdateCallback::class.java)
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
+ val helper = createHelper(callback, STRING_DIFF_CALLBACK)
- differ.submitList(listOf("a", "b"))
+ helper.submitList(listOf("a", "b"))
- assertEquals(2, differ.currentList.size)
- assertEquals("a", differ.currentList[0])
- assertEquals("b", differ.currentList[1])
+ assertEquals(2, helper.currentList.size)
+ assertEquals("a", helper.currentList[0])
+ assertEquals("b", helper.currentList[1])
verify(callback).onInserted(0, 2)
verifyNoMoreInteractions(callback)
@@ -170,24 +169,24 @@
@Test
fun submitListUpdate() {
val callback = mock(ListUpdateCallback::class.java)
- val differ = createDiffer(callback, STRING_DIFF_CALLBACK)
+ val helper = createHelper(callback, STRING_DIFF_CALLBACK)
// initial list (immediate)
- differ.submitList(listOf("a", "b"))
+ helper.submitList(listOf("a", "b"))
verify(callback).onInserted(0, 2)
verifyNoMoreInteractions(callback)
drain()
verifyNoMoreInteractions(callback)
// update (deferred)
- differ.submitList(listOf("a", "b", "c"))
+ helper.submitList(listOf("a", "b", "c"))
verifyNoMoreInteractions(callback)
drain()
verify(callback).onInserted(2, 1)
verifyNoMoreInteractions(callback)
// clear (immediate)
- differ.submitList(null)
+ helper.submitList(null)
verify(callback).onRemoved(0, 3)
verifyNoMoreInteractions(callback)
drain()
@@ -292,21 +291,12 @@
companion object {
private val STRING_DIFF_CALLBACK = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
- // items are the same if first char is the same
- return oldItem[0] == newItem[0]
+ return oldItem == newItem
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
-
- override fun getChangePayload(oldItem: String, newItem: String): Any? {
- if (newItem.startsWith(oldItem)) {
- // new string is appended, return added portion on the end
- return newItem.subSequence(oldItem.length, newItem.length)
- }
- return null
- }
}
private val IGNORE_CALLBACK = object : ListUpdateCallback {
diff --git a/v7/recyclerview/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java b/v7/recyclerview/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java
index 26020e0..91ca484 100644
--- a/v7/recyclerview/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java
+++ b/v7/recyclerview/src/main/java/androidx/recyclerview/widget/AsyncListDiffer.java
@@ -194,23 +194,19 @@
// incrementing generation means any currently-running diffs are discarded when they finish
final int runGeneration = ++mMaxScheduledGeneration;
- // fast simple remove all
if (newList == null) {
//noinspection ConstantConditions
- int countRemoved = mList.size();
+ mUpdateCallback.onRemoved(0, mList.size());
mList = null;
mReadOnlyList = Collections.emptyList();
- // notify last, after list is updated
- mUpdateCallback.onRemoved(0, countRemoved);
return;
}
- // fast simple first insert
if (mList == null) {
+ // fast simple first insert
+ mUpdateCallback.onInserted(0, newList.size());
mList = newList;
mReadOnlyList = Collections.unmodifiableList(newList);
- // notify last, after list is updated
- mUpdateCallback.onInserted(0, newList.size());
return;
}
@@ -286,9 +282,8 @@
}
private void latchList(@NonNull List<T> newList, @NonNull DiffUtil.DiffResult diffResult) {
- mList = newList;
- // notify last, after list is updated
- mReadOnlyList = Collections.unmodifiableList(newList);
diffResult.dispatchUpdatesTo(mUpdateCallback);
+ mList = newList;
+ mReadOnlyList = Collections.unmodifiableList(newList);
}
}
diff --git a/v7/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java b/v7/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
index 08196eb..b044710 100644
--- a/v7/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
+++ b/v7/recyclerview/src/main/java/androidx/recyclerview/widget/RecyclerView.java
@@ -11519,12 +11519,12 @@
if (!mRunning) {
return;
}
- mRunning = false;
onStop();
mRecyclerView.mState.mTargetPosition = RecyclerView.NO_POSITION;
mTargetView = null;
mTargetPosition = RecyclerView.NO_POSITION;
mPendingInitialRun = false;
+ mRunning = false;
// trigger a cleanup
mLayoutManager.onSmoothScrollerStopped(this);
// clear references to avoid any potential leak by a custom smooth scroller