Merge branch 'release-candidate' into stable
diff --git a/.kokoro b/.kokoro
index 8d0dff6..d820c6d 100755
--- a/.kokoro
+++ b/.kokoro
@@ -1,4 +1,18 @@
 #!/bin/bash
+#
+# Copyright 2017-present The Material Motion Authors. All Rights Reserved.
+#
+# 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.
 
 # Fail on any error.
 set -e
@@ -6,30 +20,36 @@
 # Display commands to stderr.
 set -x
 
+KOKORO_RUNNER_VERSION="v3.*"
+
+fix_bazel_imports() {
+  if [ -z "$KOKORO_BUILD_NUMBER" ]; then
+    repo_prefix=""
+  else
+    repo_prefix="github/repo/"
+  fi
+
+  # Fixes a bug in bazel where objc_library targets have a _ prefix.
+  find "${repo_prefix}tests/unit" -type f -name '*.swift' -exec sed -i '' -E "s/import Motion(.+)/import _Motion\1/" {} + || true
+  stashed_dir=$(pwd)
+  reset_imports() {
+    # Undoes our source changes from above.
+    find "${stashed_dir}/${tests_dir_prefix}tests/unit" -type f -name '*.swift' -exec sed -i '' -E "s/import _Motion(.+)/import Motion\1/" {} + || true
+  }
+  trap reset_imports EXIT
+}
+
 if [ ! -d .kokoro-ios-runner ]; then
   git clone https://github.com/material-foundation/kokoro-ios-runner.git .kokoro-ios-runner
 fi
 
 pushd .kokoro-ios-runner
 git fetch > /dev/null
-TAG=$(git tag -l "v3*" | sort | tail -n1)
-git checkout $TAG > /dev/null
+TAG=$(git tag --sort=v:refname -l "$KOKORO_RUNNER_VERSION" | tail -n1)
+git checkout "$TAG" > /dev/null
 popd
 
-if [ -z "$KOKORO_BUILD_NUMBER" ]; then
-  tests_dir_prefix=""
-else
-  tests_dir_prefix="github/repo/"
-fi
-
-# Fixes a bug in bazel where objc_library targets have a _ prefix.
-find ${tests_dir_prefix}tests/unit -type f -name '*.swift' -exec sed -i '' -E "s/import Motion(.+)/import _Motion\1/" {} + || true
-stashed_dir=$(pwd)
-reset_imports() {
-  # Undoes our source changes from above.
-  find ${stashed_dir}/${tests_dir_prefix}tests/unit -type f -name '*.swift' -exec sed -i '' -E "s/import _Motion(.+)/import Motion\1/" {} + || true
-}
-trap reset_imports EXIT
+fix_bazel_imports
 
 ./.kokoro-ios-runner/bazel.sh test //:UnitTests 8.1.0
 
diff --git a/BUILD b/BUILD
index 6c9774f..30ee3e5 100644
--- a/BUILD
+++ b/BUILD
@@ -1,12 +1,28 @@
+# Copyright 2017-present The Material Motion Authors. All Rights Reserved.
+#
+# 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.
+#
 # Description:
 # Motion interchange format.
 
+load("@bazel_ios_warnings//:strict_warnings_objc_library.bzl", "strict_warnings_objc_library")
+load("@build_bazel_rules_apple//apple:swift.bzl", "swift_library")
+load("@build_bazel_rules_apple//apple:ios.bzl", "ios_unit_test")
+
 licenses(["notice"])  # Apache 2.0
 
 exports_files(["LICENSE"])
 
-load("@bazel_ios_warnings//:strict_warnings_objc_library.bzl", "strict_warnings_objc_library")
-
 strict_warnings_objc_library(
     name = "MotionInterchange",
     srcs = glob([
@@ -22,8 +38,6 @@
     visibility = ["//visibility:public"],
 )
 
-load("@build_bazel_rules_apple//apple:swift.bzl", "swift_library")
-
 swift_library(
     name = "UnitTestsSwiftLib",
     srcs = glob([
@@ -42,8 +56,6 @@
     visibility = ["//visibility:private"],
 )
 
-load("@build_bazel_rules_apple//apple:ios.bzl", "ios_unit_test")
-
 ios_unit_test(
     name = "UnitTests",
     deps = [
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f4ac5c5..c25b775 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,29 @@
+# 1.2.0
+
+This minor release introduces a new API for reversing cubic beziers and a unit test for
+`MDMModalMovementTiming`.
+
+## New features
+
+`MDMMotionCurveReversedBezier` reverses cubic bezier curves. Intended for use when building mirrored
+bi-directional transitions.
+
+## Source changes
+
+* [Add a unit test for MDMModalMovementTiming. (#12)](https://github.com/material-motion/motion-interchange-objc/commit/a0c3566ad52a45365657e0591701afa7989eb822) (featherless)
+* [Add MDMMotionCurveReversed for reversing timing curves. (#11)](https://github.com/material-motion/motion-interchange-objc/commit/a54a5ffa49052a198b4bb5beedce737bb61ebc91) (featherless)
+
+## API changes
+
+### MDMMotionCurveReversedBezier
+
+**new** function: `MDMMotionCurveReversedBezier`.
+
+## Non-source changes
+
+* [Standardize the kokoro and bazel files. (#13)](https://github.com/material-motion/motion-interchange-objc/commit/a009d3f7d08d8b2d087891a86eb1e298714198b4) (featherless)
+* [Use the v1.0.0 tag for bazel_ios_warnings. (#10)](https://github.com/material-motion/motion-interchange-objc/commit/545b6a448ddb235279318dc262f051d653a48ed4) (featherless)
+
 # 1.1.1
 
 This patch release migrates the project's continuous integration pipeline from arc to bazel and
diff --git a/MotionInterchange.podspec b/MotionInterchange.podspec
index 7d96972..5d5cc55 100644
--- a/MotionInterchange.podspec
+++ b/MotionInterchange.podspec
@@ -1,7 +1,7 @@
 Pod::Spec.new do |s|
   s.name         = "MotionInterchange"
   s.summary      = "Motion interchange format."
-  s.version      = "1.1.1"
+  s.version      = "1.2.0"
   s.authors      = "The Material Motion Authors"
   s.license      = "Apache 2.0"
   s.homepage     = "https://github.com/material-motion/motion-interchange-objc"
diff --git a/Podfile.lock b/Podfile.lock
index 734cbe9..ef0bf5f 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -1,6 +1,6 @@
 PODS:
   - CatalogByConvention (2.1.1)
-  - MotionInterchange (1.1.1)
+  - MotionInterchange (1.2.0)
 
 DEPENDENCIES:
   - CatalogByConvention
@@ -12,7 +12,7 @@
 
 SPEC CHECKSUMS:
   CatalogByConvention: c3a5319de04250a7cd4649127fcfca5fe3322a43
-  MotionInterchange: 3556642f403549449a49f05653da3bd932a5e072
+  MotionInterchange: 499c98e7628a8a078905749734dbfedbfae54cca
 
 PODFILE CHECKSUM: 09090d12db5aab00a13fe82da94f338ebd03f5dc
 
diff --git a/WORKSPACE b/WORKSPACE
index c01a93d..722a7a4 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,3 +1,17 @@
+# Copyright 2017-present The Material Motion Authors. All Rights Reserved.
+#
+# 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.
+
 git_repository(
     name = "build_bazel_rules_apple",
     remote = "https://github.com/bazelbuild/rules_apple.git",
@@ -7,5 +21,5 @@
 git_repository(
     name = "bazel_ios_warnings",
     remote = "https://github.com/material-foundation/bazel_ios_warnings.git",
-    commit = "3e61cb5b60f52c8b9c77b5d62364d8b4d25e528f",
+    tag = "v1.0.1",
 )
diff --git a/examples/apps/Catalog/MotionInterchangeCatalog.xcodeproj/project.pbxproj b/examples/apps/Catalog/MotionInterchangeCatalog.xcodeproj/project.pbxproj
index 29dcd40..c0139ca 100644
--- a/examples/apps/Catalog/MotionInterchangeCatalog.xcodeproj/project.pbxproj
+++ b/examples/apps/Catalog/MotionInterchangeCatalog.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		6619E1D91FA0ED0300F3AB25 /* MDMModalMovementTimingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6619E1D81FA0ED0300F3AB25 /* MDMModalMovementTimingTests.m */; };
 		663ED7C51EDF1F0C0096B2A9 /* ExampleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663ED7C01EDF1F0C0096B2A9 /* ExampleViewController.swift */; };
 		663ED7C61EDF1F0C0096B2A9 /* ExampleViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663ED7C11EDF1F0C0096B2A9 /* ExampleViews.swift */; };
 		663ED7C71EDF1F0C0096B2A9 /* HexColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663ED7C21EDF1F0C0096B2A9 /* HexColor.swift */; };
@@ -46,6 +47,7 @@
 		09CEA5DEA01BA723D08D84E6 /* Pods-UnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.release.xcconfig"; sourceTree = "<group>"; };
 		2DE76D4D35953D836F578CDE /* Pods-MotionInterchangeCatalog.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MotionInterchangeCatalog.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-MotionInterchangeCatalog/Pods-MotionInterchangeCatalog.debug.xcconfig"; sourceTree = "<group>"; };
 		4AAB8EBB088513D48896641A /* Pods-MotionInterchangeCatalog.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MotionInterchangeCatalog.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-MotionInterchangeCatalog/Pods-MotionInterchangeCatalog.release.xcconfig"; sourceTree = "<group>"; };
+		6619E1D81FA0ED0300F3AB25 /* MDMModalMovementTimingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MDMModalMovementTimingTests.m; sourceTree = "<group>"; };
 		663ED7C01EDF1F0C0096B2A9 /* ExampleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleViewController.swift; sourceTree = "<group>"; };
 		663ED7C11EDF1F0C0096B2A9 /* ExampleViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleViews.swift; sourceTree = "<group>"; };
 		663ED7C21EDF1F0C0096B2A9 /* HexColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HexColor.swift; sourceTree = "<group>"; };
@@ -162,6 +164,7 @@
 			children = (
 				663ED8001EE628BA0096B2A9 /* MDMMotionCurveTests.swift */,
 				663ED8021EE6299A0096B2A9 /* MDMMotionCurveTests.m */,
+				6619E1D81FA0ED0300F3AB25 /* MDMModalMovementTimingTests.m */,
 			);
 			name = tests;
 			path = ../../../tests/unit;
@@ -477,6 +480,7 @@
 			files = (
 				663ED8031EE6299A0096B2A9 /* MDMMotionCurveTests.m in Sources */,
 				663ED8011EE628BA0096B2A9 /* MDMMotionCurveTests.swift in Sources */,
+				6619E1D91FA0ED0300F3AB25 /* MDMModalMovementTimingTests.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
diff --git a/src/MDMMotionCurve.h b/src/MDMMotionCurve.h
index f93f808..6eace63 100644
--- a/src/MDMMotionCurve.h
+++ b/src/MDMMotionCurve.h
@@ -95,6 +95,15 @@
 // clang-format on
 
 /**
+ For cubic bezier curves, returns a reversed cubic bezier curve. For all other curve types, a copy
+ of the original curve is returned.
+ */
+// clang-format off
+FOUNDATION_EXTERN MDMMotionCurve MDMMotionCurveReversedBezier(MDMMotionCurve motionCurve)
+    NS_SWIFT_NAME(MotionCurveReversedBezier(fromMotionCurve:));
+// clang-format on
+
+/**
  Named indices for the bezier motion curve's data array.
  */
 typedef NS_ENUM(NSUInteger, MDMBezierMotionCurveDataIndex) {
diff --git a/src/MDMMotionCurve.m b/src/MDMMotionCurve.m
index 111846c..7cb7b3c 100644
--- a/src/MDMMotionCurve.m
+++ b/src/MDMMotionCurve.m
@@ -31,3 +31,14 @@
   [timingFunction getControlPointAtIndex:2 values:pt2];
   return MDMMotionCurveMakeBezier(pt1[0], pt1[1], pt2[0], pt2[1]);
 }
+
+MDMMotionCurve MDMMotionCurveReversedBezier(MDMMotionCurve motionCurve) {
+  MDMMotionCurve reversed = motionCurve;
+  if (motionCurve.type == MDMMotionCurveTypeBezier) {
+    reversed.data[0] = 1 - motionCurve.data[2];
+    reversed.data[1] = 1 - motionCurve.data[3];
+    reversed.data[2] = 1 - motionCurve.data[0];
+    reversed.data[3] = 1 - motionCurve.data[1];
+  }
+  return reversed;
+}
diff --git a/tests/unit/MDMModalMovementTimingTests.m b/tests/unit/MDMModalMovementTimingTests.m
new file mode 100644
index 0000000..f955a9a
--- /dev/null
+++ b/tests/unit/MDMModalMovementTimingTests.m
@@ -0,0 +1,87 @@
+/*
+ Copyright 2017-present The Material Motion Authors. All Rights Reserved.
+
+ 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 <XCTest/XCTest.h>
+
+#import "MotionInterchange.h"
+
+@interface MDMModalMovementTimingTests : XCTestCase
+@property(nonatomic, strong) UIWindow *window;
+@end
+
+@interface ModalPresentationExtractionViewController : UIViewController
+@property(nonatomic, strong) CAAnimation *presentationPositionAnimation;
+@end
+
+@implementation ModalPresentationExtractionViewController
+
+- (void)viewDidLayoutSubviews {
+  [super viewDidLayoutSubviews];
+
+  // We just want the first position key path animation that affects this view controller.
+  if (!self.presentationPositionAnimation) {
+    self.presentationPositionAnimation = [self.view.layer animationForKey:@"position"];
+  }
+}
+
+@end
+
+@implementation MDMModalMovementTimingTests
+
+- (void)setUp {
+  [super setUp];
+
+  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
+  self.window.rootViewController = [[UIViewController alloc] initWithNibName:nil bundle:nil];
+  [self.window makeKeyAndVisible];
+}
+
+- (void)tearDown {
+  self.window = nil;
+
+  [super tearDown];
+}
+
+- (void)testSystemModalMovementTimingCurveMatchesModalMovementTiming {
+  ModalPresentationExtractionViewController *presentedViewController =
+      [[ModalPresentationExtractionViewController alloc] initWithNibName:nil bundle:nil];
+  XCTestExpectation *didComplete = [self expectationWithDescription:@"Animation completed"];
+  [self.window.rootViewController presentViewController:presentedViewController
+                                               animated:YES
+                                             completion:^{
+                                               [didComplete fulfill];
+                                             }];
+
+  [self waitForExpectationsWithTimeout:1 handler:nil];
+
+  XCTAssertTrue([presentedViewController.presentationPositionAnimation
+                 isKindOfClass:[CASpringAnimation class]]);
+  CASpringAnimation *springAnimation =
+      (CASpringAnimation *)presentedViewController.presentationPositionAnimation;
+
+  MDMMotionTiming timing = MDMModalMovementTiming;
+  XCTAssertEqualWithAccuracy(timing.curve.data[MDMSpringMotionCurveDataIndexMass],
+                             springAnimation.mass,
+                             0.001);
+  XCTAssertEqualWithAccuracy(timing.curve.data[MDMSpringMotionCurveDataIndexTension],
+                             springAnimation.stiffness,
+                             0.001);
+  XCTAssertEqualWithAccuracy(timing.curve.data[MDMSpringMotionCurveDataIndexFriction],
+                             springAnimation.damping,
+                             0.001);
+}
+
+@end
diff --git a/tests/unit/MDMMotionCurveTests.swift b/tests/unit/MDMMotionCurveTests.swift
index eaf59b7..0f1fb27 100644
--- a/tests/unit/MDMMotionCurveTests.swift
+++ b/tests/unit/MDMMotionCurveTests.swift
@@ -43,4 +43,23 @@
     XCTAssertEqualWithAccuracy(curve.data.2, 0.3, accuracy: 0.001) // friction
     XCTAssertEqualWithAccuracy(curve.data.3, 0.0, accuracy: 0.001)
   }
+
+  func testReversedBezierCurve() {
+    let curve = MotionCurveMakeBezier(p1x: 0.1, p1y: 0.2, p2x: 0.3, p2y: 0.4)
+    let reversed = MotionCurveReversedBezier(fromMotionCurve: curve)
+    XCTAssertEqualWithAccuracy(curve.data.0, 1 - reversed.data.2, accuracy: 0.001)
+    XCTAssertEqualWithAccuracy(curve.data.1, 1 - reversed.data.3, accuracy: 0.001)
+    XCTAssertEqualWithAccuracy(curve.data.2, 1 - reversed.data.0, accuracy: 0.001)
+    XCTAssertEqualWithAccuracy(curve.data.3, 1 - reversed.data.1, accuracy: 0.001)
+  }
+
+  func testReversingBezierCurveTwiceGivesSameResult() {
+    let curve = MotionCurveMakeBezier(p1x: 0.1, p1y: 0.2, p2x: 0.3, p2y: 0.4)
+    let reversed = MotionCurveReversedBezier(fromMotionCurve: curve)
+    let reversedAgain = MotionCurveReversedBezier(fromMotionCurve: reversed)
+    XCTAssertEqualWithAccuracy(curve.data.0, reversedAgain.data.0, accuracy: 0.001)
+    XCTAssertEqualWithAccuracy(curve.data.1, reversedAgain.data.1, accuracy: 0.001)
+    XCTAssertEqualWithAccuracy(curve.data.2, reversedAgain.data.2, accuracy: 0.001)
+    XCTAssertEqualWithAccuracy(curve.data.3, reversedAgain.data.3, accuracy: 0.001)
+  }
 }