diff --git a/.github/ISSUE_TEMPLATE/--general-question.md b/.github/ISSUE_TEMPLATE/--general-question.md new file mode 100644 index 0000000000..f354c77a4f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--general-question.md @@ -0,0 +1,28 @@ +--- +name: "❓ General question" +about: Describe this issue template's purpose here. +title: "[Question] " +labels: 'new, type: question' +assignees: '' + +--- + + + +### [READ] For Firebase Unity SDK question, please report to [Firebase Unity Sample](https://github.com/firebase/quickstart-unity/issues/new/choose) + +Once you've read this section and determined that your issue is appropriate for this repository, please delete this section. + +### [REQUIRED] Please fill in the following fields: + + * Pre-built SDK from the [website](https://firebase.google.com/download/cpp) or open-source from this repo: _____ + * Firebase C++ SDK version: _____ + * Main Firebase Components in concern: _____ (Auth, Database, etc.) + * Other Firebase Components in use: _____ (Auth, Database, etc.) + * Platform you are using the C++ SDK on: _____ (Mac, Windows, or Linux) + * Platform you are targeting: _____ (iOS, Android, and/or desktop) + +### [REQUIRED] Please describe the question here: diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000000..d20d8a8c44 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,30 @@ +--- +name: "➕ Feature request" +about: If you have a feature request for the Firebase C++ SDK, file it here. +title: '' +labels: 'new, type: feature request' +assignees: '' + +--- + + +### [READ] Guidelines + +When filing a feature request please make sure the issue title starts with "FR:". + +A good feature request ideally +* is either immediately obvious (i.e. "Add Sign in with Apple support"), or +* starts with a use case that is not achievable with the existing Firebase API and + includes an API proposal that would make the use case possible. The proposed API + change does not need to be very specific. + +Once you've read this section, please delete it and fill out the rest of the template. + +### Feature proposal + +* Firebase Component: _____ (Auth, Core, Database, Firestore, Messaging, Storage, etc) + +Describe your use case and/or feature request here. diff --git a/.github/ISSUE_TEMPLATE/firebase-cpp-sdk-issue.md b/.github/ISSUE_TEMPLATE/firebase-cpp-sdk-issue.md deleted file mode 100644 index 916a4ce47f..0000000000 --- a/.github/ISSUE_TEMPLATE/firebase-cpp-sdk-issue.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: Firebase C++ SDK issue -about: Please use this template to report issues with the Firebase C++ SDK. -title: '' -labels: new -assignees: '' - ---- - -### Please fill in the following fields: -Pre-built SDK from the [website](https://firebase.google.com/download/cpp) or open-source from this repo: -Firebase C++ SDK version: -Firebase plugins in use (Auth, Database, etc.): -Additional SDKs you are using (Facebook, AdMob, etc.): -Platform you are using the C++ SDK on (Mac, Windows, or Linux): -Platform you are targeting (iOS, Android, and/or desktop): - -### Please describe the issue here: -(Please list the full steps to reproduce the issue. Include device logs, Unity logs, and stack traces if available.) - -### Please answer the following, if applicable: -Have you been able to reproduce this issue with just the [Firebase C++ quickstarts](https://github.com/firebase/quickstart-cpp) ? - -What's the issue repro rate? (eg 100%, 1/5 etc) diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md new file mode 100644 index 0000000000..45eb75e9d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -0,0 +1,43 @@ +--- +name: "\U0001F41E Bug report" +about: Please use this template to report bugs with the Firebase C++ SDK. +title: '' +labels: new +assignees: '' + +--- + + + +### [REQUIRED] Please fill in the following fields: + + * Pre-built SDK from the [website](https://firebase.google.com/download/cpp) or open-source from this repo: _____ + * Firebase C++ SDK version: _____ + * Problematic Firebase Component: _____ (Auth, Database, etc.) + * Other Firebase Components in use: _____ (Auth, Database, etc.) + * Platform you are using the C++ SDK on: _____ (Mac, Windows, or Linux) + * Platform you are targeting: _____ (iOS, Android, and/or desktop) + +### [REQUIRED] Please describe the issue here: + +(Please list the full steps to reproduce the issue. Include device logs, Unity logs, and stack traces if available.) + +#### Steps to reproduce: + +Have you been able to reproduce this issue with just the [Firebase C++ quickstarts](https://github.com/firebase/quickstart-cpp) ? +What's the issue repro rate? (eg 100%, 1/5 etc) + +What happened? How can we make the problem occur? +This could be a description, log/console output, etc. + +If you have a downloadable sample project that reproduces the bug you're reporting, you will +likely receive a faster response on your issue. + +#### Relevant Code: + +``` +// TODO(you): code here to reproduce the problem +``` diff --git a/Android/firebase_dependencies.gradle b/Android/firebase_dependencies.gradle index 4ae98c201c..8c2ff4d638 100644 --- a/Android/firebase_dependencies.gradle +++ b/Android/firebase_dependencies.gradle @@ -16,23 +16,27 @@ import org.gradle.util.ConfigureUtil; // A map of library to the dependencies that need to be added for it. def firebaseDependenciesMap = [ - 'app' : ['com.google.firebase:firebase-analytics:17.4.4'], - 'admob' : ['com.google.firebase:firebase-ads:19.2.0', - 'com.google.android.gms:play-services-measurement-sdk-api:17.4.4'], - 'analytics' : ['com.google.firebase:firebase-analytics:17.4.4'], + 'app' : ['com.google.firebase:firebase-analytics:17.5.0'], + 'admob' : ['com.google.firebase:firebase-ads:19.3.0', + 'com.google.android.gms:play-services-measurement-sdk-api:17.5.0', + 'com.google.android.gms:play-services-base:17.4.0'], + 'analytics' : ['com.google.firebase:firebase-analytics:17.5.0', + 'com.google.android.gms:play-services-base:17.4.0'], 'auth' : ['com.google.firebase:firebase-auth:19.3.2'], - 'database' : ['com.google.firebase:firebase-database:19.3.1'], + 'database' : ['com.google.firebase:firebase-database:19.4.0'], 'dynamic_links' : ['com.google.firebase:firebase-dynamic-links:19.1.0'], - 'firestore' : ['com.google.firebase:firebase-firestore:21.5.0'], - 'functions' : ['com.google.firebase:firebase-functions:19.0.2'], - 'instance_id' : ['com.google.firebase:firebase-iid:20.2.3'], + 'firestore' : ['com.google.firebase:firebase-firestore:21.6.0'], + 'functions' : ['com.google.firebase:firebase-functions:19.1.0'], + 'instance_id' : ['com.google.firebase:firebase-iid:20.2.4'], 'invites' : ['com.google.firebase:firebase-invites:17.0.0'], // Messaging has an additional local dependency to include. - 'messaging' : ['com.google.firebase:firebase-messaging:20.2.3', - 'firebase_cpp_sdk.messaging:messaging_java'], - 'performance' : ['com.google.firebase:firebase-perf:19.0.7'], - 'remote_config' : ['com.google.firebase:firebase-config:19.2.0'], - 'storage' : ['com.google.firebase:firebase-storage:19.1.1'] + 'messaging' : ['com.google.firebase:firebase-messaging:20.2.4', + 'firebase_cpp_sdk.messaging:messaging_java', + 'androidx.core:core:1.0.1'], + 'performance' : ['com.google.firebase:firebase-perf:19.0.8'], + 'remote_config' : ['com.google.firebase:firebase-config:19.2.0', + 'com.google.android.gms:play-services-base:17.4.0'], + 'storage' : ['com.google.firebase:firebase-storage:19.2.0'] ] // A map of library to the gradle resources that they depend upon. @@ -157,4 +161,4 @@ project.afterEvaluate { } } } -} \ No newline at end of file +} diff --git a/admob/admob_resources/build.gradle b/admob/admob_resources/build.gradle index 4597cc77d0..ecab34f893 100644 --- a/admob/admob_resources/build.gradle +++ b/admob/admob_resources/build.gradle @@ -45,8 +45,8 @@ android { } dependencies { - implementation 'com.google.firebase:firebase-analytics:17.4.4' - implementation 'com.google.firebase:firebase-ads:19.2.0' + implementation 'com.google.firebase:firebase-analytics:17.5.0' + implementation 'com.google.firebase:firebase-ads:19.3.0' } afterEvaluate { @@ -54,4 +54,4 @@ afterEvaluate { } apply from: "$rootDir/android_build_files/extract_and_dex.gradle" -extractAndDexAarFile('admob_resources') \ No newline at end of file +extractAndDexAarFile('admob_resources') diff --git a/admob/src/include/firebase/admob/types.h b/admob/src/include/firebase/admob/types.h index b9f04cebb4..d1e3d28e75 100644 --- a/admob/src/include/firebase/admob/types.h +++ b/admob/src/include/firebase/admob/types.h @@ -72,6 +72,8 @@ enum AdMobError { /// An attempt has been made to show an ad on an Android Activity that has /// no window token (such as one that's not done initializing). kAdMobErrorNoWindowToken, + /// Fallback error for any unidentified cases. + kAdMobErrorUnknown, }; #ifdef INTERNAL_EXPERIMENTAL // LINT.ThenChange(//depot_firebase_cpp/admob/client/cpp/src_java/com/google/firebase/admob/internal/cpp/ConstantsHelper.java) diff --git a/admob/src_java/com/google/firebase/admob/internal/cpp/ConstantsHelper.java b/admob/src_java/com/google/firebase/admob/internal/cpp/ConstantsHelper.java index d226ea8ad3..0687ba7f52 100644 --- a/admob/src_java/com/google/firebase/admob/internal/cpp/ConstantsHelper.java +++ b/admob/src_java/com/google/firebase/admob/internal/cpp/ConstantsHelper.java @@ -40,6 +40,8 @@ public final class ConstantsHelper { public static final int CALLBACK_ERROR_NO_FILL = 7; public static final int CALLBACK_ERROR_NO_WINDOW_TOKEN = 8; + + public static final int CALLBACK_ERROR_UNKNOWN = 9; // LINT.ThenChange(//depot_firebase_cpp/admob/client/cpp/src/include/firebase/admob/types.h) /** @@ -68,6 +70,8 @@ public final class ConstantsHelper { public static final String CALLBACK_ERROR_MESSAGE_NO_WINDOW_TOKEN = "Android Activity does not have a window token."; + + public static final String CALLBACK_ERROR_MESSAGE_UNKNOWN = "Unknown error occurred."; // LINT.ThenChange(//depot_firebase_cpp/admob/client/cpp/src/include/firebase/admob/types.h) /** Types of notifications to send back to the C++ side for listeners updates. */ diff --git a/admob/src_java/com/google/firebase/admob/internal/cpp/InterstitialAdHelper.java b/admob/src_java/com/google/firebase/admob/internal/cpp/InterstitialAdHelper.java index eee1149e08..60b7183de7 100644 --- a/admob/src_java/com/google/firebase/admob/internal/cpp/InterstitialAdHelper.java +++ b/admob/src_java/com/google/firebase/admob/internal/cpp/InterstitialAdHelper.java @@ -92,14 +92,22 @@ public void run() { int errorCode; String errorMessage; if (mInterstitial == null) { - errorCode = ConstantsHelper.CALLBACK_ERROR_NONE; - errorMessage = ConstantsHelper.CALLBACK_ERROR_MESSAGE_NONE; - mInterstitial = new InterstitialAd(mActivity); - mInterstitial.setAdUnitId(mAdUnitId); - mInterstitial.setAdListener(new InterstitialAdListener()); + try { + mInterstitial = new InterstitialAd(mActivity); + mInterstitial.setAdUnitId(mAdUnitId); + mInterstitial.setAdListener(new InterstitialAdListener()); + errorCode = ConstantsHelper.CALLBACK_ERROR_NONE; + errorMessage = ConstantsHelper.CALLBACK_ERROR_MESSAGE_NONE; + } catch (IllegalStateException e) { + mInterstitial = null; + // This exception can be thrown if the ad unit ID was already set. + errorCode = ConstantsHelper.CALLBACK_ERROR_ALREADY_INITIALIZED; + errorMessage = ConstantsHelper.CALLBACK_ERROR_MESSAGE_ALREADY_INITIALIZED; + } } else { errorCode = ConstantsHelper.CALLBACK_ERROR_ALREADY_INITIALIZED; errorMessage = ConstantsHelper.CALLBACK_ERROR_MESSAGE_ALREADY_INITIALIZED; + } completeInterstitialAdFutureCallback(callbackDataPtr, errorCode, errorMessage); @@ -152,7 +160,17 @@ public void run() { mLoadAdCallbackDataPtr = CPP_NULLPTR; } } else { - mInterstitial.loadAd(request); + try { + mInterstitial.loadAd(request); + } catch (IllegalStateException e) { + synchronized (mInterstitialLock) { + completeInterstitialAdFutureCallback( + mLoadAdCallbackDataPtr, + ConstantsHelper.CALLBACK_ERROR_UNINITIALIZED, + ConstantsHelper.CALLBACK_ERROR_MESSAGE_UNINITIALIZED); + mLoadAdCallbackDataPtr = CPP_NULLPTR; + } + } } } }); @@ -222,6 +240,9 @@ public void onAdFailedToLoad(int errorCode) { callbackErrorCode = ConstantsHelper.CALLBACK_ERROR_NO_FILL; callbackErrorMessage = ConstantsHelper.CALLBACK_ERROR_MESSAGE_NO_FILL; break; + default: + callbackErrorCode = ConstantsHelper.CALLBACK_ERROR_UNKNOWN; + callbackErrorMessage = ConstantsHelper.CALLBACK_ERROR_MESSAGE_UNKNOWN; } synchronized (mInterstitialLock) { @@ -233,11 +254,6 @@ public void onAdFailedToLoad(int errorCode) { super.onAdFailedToLoad(errorCode); } - @Override - public void onAdLeftApplication() { - super.onAdLeftApplication(); - } - @Override public void onAdLoaded() { synchronized (mInterstitialLock) { diff --git a/analytics/ios_headers/FIREventNames.h b/analytics/ios_headers/FIREventNames.h index 9458782579..f7984ca777 100644 --- a/analytics/ios_headers/FIREventNames.h +++ b/analytics/ios_headers/FIREventNames.h @@ -69,6 +69,21 @@ static NSString *const kFIREventAddToCart NS_SWIFT_NAME(AnalyticsEventAddToCart) static NSString *const kFIREventAddToWishlist NS_SWIFT_NAME(AnalyticsEventAddToWishlist) = @"add_to_wishlist"; +/// Ad Impression event. This event signifies when a user sees an ad impression. Note: If you supply +/// the @c kFIRParameterValue parameter, you must also supply the @c kFIRParameterCurrency parameter +/// so that revenue metrics can be computed accurately. Params: +/// +/// +static NSString *const kFIREventAdImpression NS_SWIFT_NAME(AnalyticsEventAdImpression) = + @"ad_impression"; + /// App Open event. By logging this event when an App becomes active, developers can understand how /// often users leave and return during the course of a Session. Although Sessions are automatically /// reported, this event can provide further clarification around the continuous engagement of diff --git a/analytics/ios_headers/FIRParameterNames.h b/analytics/ios_headers/FIRParameterNames.h index 515232e9b5..6047f5684a 100644 --- a/analytics/ios_headers/FIRParameterNames.h +++ b/analytics/ios_headers/FIRParameterNames.h @@ -39,6 +39,17 @@ static NSString *const kFIRParameterAchievementID NS_SWIFT_NAME(AnalyticsParameterAchievementID) = @"achievement_id"; +/// The ad format (e.g. Banner, Interstitial, Rewarded, Native, Rewarded Interstitial, Instream). +/// (NSString). +///
+///     NSDictionary *params = @{
+///       kFIRParameterAdFormat : @"Banner",
+///       // ...
+///     };
+/// 
+static NSString *const kFIRParameterAdFormat NS_SWIFT_NAME(AnalyticsParameterAdFormat) = + @"ad_format"; + /// Ad Network Click ID (NSString). Used for network-specific click IDs which vary in format. ///
 ///     NSDictionary *params = @{
@@ -49,6 +60,36 @@ static NSString *const kFIRParameterAchievementID NS_SWIFT_NAME(AnalyticsParamet
 static NSString *const kFIRParameterAdNetworkClickID
     NS_SWIFT_NAME(AnalyticsParameterAdNetworkClickID) = @"aclid";
 
+/// The ad platform (e.g. MoPub, IronSource) (NSString).
+/// 
+///     NSDictionary *params = @{
+///       kFIRParameterAdPlatform : @"MoPub",
+///       // ...
+///     };
+/// 
+static NSString *const kFIRParameterAdPlatform NS_SWIFT_NAME(AnalyticsParameterAdPlatform) = + @"ad_platform"; + +/// The ad source (e.g. AdColony) (NSString). +///
+///     NSDictionary *params = @{
+///       kFIRParameterAdSource : @"AdColony",
+///       // ...
+///     };
+/// 
+static NSString *const kFIRParameterAdSource NS_SWIFT_NAME(AnalyticsParameterAdSource) = + @"ad_source"; + +/// The ad unit name (e.g. Banner_03) (NSString). +///
+///     NSDictionary *params = @{
+///       kFIRParameterAdUnitName : @"Banner_03",
+///       // ...
+///     };
+/// 
+static NSString *const kFIRParameterAdUnitName NS_SWIFT_NAME(AnalyticsParameterAdUnitName) = + @"ad_unit_name"; + /// A product affiliation to designate a supplying company or brick and mortar store location /// (NSString).
 ///     NSDictionary *params = @{
diff --git a/app/app_resources/build.gradle b/app/app_resources/build.gradle
index f4110eedf5..d6d13928c2 100644
--- a/app/app_resources/build.gradle
+++ b/app/app_resources/build.gradle
@@ -46,7 +46,7 @@ android {
 }
 
 dependencies {
-  implementation 'com.google.firebase:firebase-analytics:17.4.4'
+  implementation 'com.google.firebase:firebase-analytics:17.5.0'
 }
 
 afterEvaluate {
@@ -54,4 +54,4 @@ afterEvaluate {
 }
 
 apply from: "$rootDir/android_build_files/extract_and_dex.gradle"
-extractAndDexAarFile('app_resources')
\ No newline at end of file
+extractAndDexAarFile('app_resources')
diff --git a/app/google_api_resources/build.gradle b/app/google_api_resources/build.gradle
index 0d3076b981..f54e91e465 100644
--- a/app/google_api_resources/build.gradle
+++ b/app/google_api_resources/build.gradle
@@ -49,7 +49,7 @@ android {
 }
 
 dependencies {
-  implementation 'com.google.firebase:firebase-analytics:17.4.4'
+  implementation 'com.google.firebase:firebase-analytics:17.5.0'
   implementation 'com.google.android.gms:play-services-base:17.0.0'
   implementation project(':app:app_resources')
 }
@@ -59,4 +59,4 @@ afterEvaluate {
 }
 
 apply from: "$rootDir/android_build_files/extract_and_dex.gradle"
-extractAndDexAarFile('google_api_resources')
\ No newline at end of file
+extractAndDexAarFile('google_api_resources')
diff --git a/app/invites_resources/build.gradle b/app/invites_resources/build.gradle
index c5db176719..8215747a6e 100644
--- a/app/invites_resources/build.gradle
+++ b/app/invites_resources/build.gradle
@@ -45,7 +45,7 @@ android {
 }
 
 dependencies {
-  implementation 'com.google.firebase:firebase-analytics:17.4.4'
+  implementation 'com.google.firebase:firebase-analytics:17.5.0'
   implementation 'com.google.firebase:firebase-dynamic-links:19.1.0'
   implementation project(':app:app_resources')
 }
@@ -55,4 +55,4 @@ afterEvaluate {
 }
 
 apply from: "$rootDir/android_build_files/extract_and_dex.gradle"
-extractAndDexAarFile('invites_resources')
\ No newline at end of file
+extractAndDexAarFile('invites_resources')
diff --git a/app/src/include/firebase/variant.h b/app/src/include/firebase/variant.h
index 77a0796e6b..86aa6bed88 100644
--- a/app/src/include/firebase/variant.h
+++ b/app/src/include/firebase/variant.h
@@ -131,7 +131,7 @@ class Variant {
   template 
   Variant(T value)  // NOLINT
     : type_(kInternalTypeNull) {
-    set_value_t(value);
+    set_value_t(value);
   }
 
   /// @brief Construct a Variant containing the given string value (makes a
@@ -1104,14 +1104,11 @@ class Variant {
     value_.blob_value.size = size;
   }
 
-  // Templated helper function to ensure the value 0 is constructed as int
-  // instead of nullptr char*
-  //
   // If you hit a compiler error here it means you are trying to construct a
   // variant with unsupported type. Ether cast to correct type or add support
   // below.
   template 
-  void set_value_t(T value);
+  void set_value_t(T value) = delete;
 
   // Get whether this Variant contains a small string.
   bool is_small_string() const { return type_ == kInternalTypeSmallString; }
diff --git a/auth/auth_resources/build.gradle b/auth/auth_resources/build.gradle
index 25e0263958..aa9640bb74 100644
--- a/auth/auth_resources/build.gradle
+++ b/auth/auth_resources/build.gradle
@@ -45,7 +45,7 @@ android {
 }
 
 dependencies {
-  implementation 'com.google.firebase:firebase-analytics:17.4.4'
+  implementation 'com.google.firebase:firebase-analytics:17.5.0'
   implementation 'com.google.firebase:firebase-auth:19.3.2'
   implementation project(':app:app_resources')
 }
diff --git a/auth/src/include/firebase/auth.h b/auth/src/include/firebase/auth.h
index c852486248..043f531720 100644
--- a/auth/src/include/firebase/auth.h
+++ b/auth/src/include/firebase/auth.h
@@ -47,7 +47,7 @@ class PhoneAuthProvider;
 struct AuthCompletionHandle;
 class FederatedAuthProvider;
 class FederatedOAuthProvider;
-class SignInResult;
+struct SignInResult;
 
 /// @brief Firebase authentication object.
 ///
diff --git a/cpp_sdk_version.json b/cpp_sdk_version.json
index 5f4404eb40..a32751a9ab 100644
--- a/cpp_sdk_version.json
+++ b/cpp_sdk_version.json
@@ -1,5 +1,5 @@
 {
-  "released": "6.15.1",
-  "stable": "6.15.1",
-  "head": "6.15.1"
+  "released": "6.16.0",
+  "stable": "6.16.0",
+  "head": "6.16.0"
 }
diff --git a/database/database_resources/build.gradle b/database/database_resources/build.gradle
index b5040464df..0231afd804 100644
--- a/database/database_resources/build.gradle
+++ b/database/database_resources/build.gradle
@@ -45,8 +45,8 @@ android {
 }
 
 dependencies {
-  implementation 'com.google.firebase:firebase-analytics:17.4.4'
-  implementation 'com.google.firebase:firebase-database:19.3.1'
+  implementation 'com.google.firebase:firebase-analytics:17.5.0'
+  implementation 'com.google.firebase:firebase-database:19.4.0'
   //implementation project(':app:app_resources')
 }
 
diff --git a/firestore/CMakeLists.txt b/firestore/CMakeLists.txt
index cddeb2ce3b..df77ae105d 100644
--- a/firestore/CMakeLists.txt
+++ b/firestore/CMakeLists.txt
@@ -71,16 +71,14 @@ set(android_SRCS
     src/android/document_snapshot_android.h
     src/android/event_listener_android.cc
     src/android/event_listener_android.h
+    src/android/exception_android.cc
+    src/android/exception_android.h
     src/android/field_path_android.cc
     src/android/field_path_android.h
     src/android/field_path_portable.cc
     src/android/field_path_portable.h
     src/android/field_value_android.cc
     src/android/field_value_android.h
-    src/android/firebase_firestore_exception_android.cc
-    src/android/firebase_firestore_exception_android.h
-    src/android/firebase_firestore_settings_android.cc
-    src/android/firebase_firestore_settings_android.h
     src/android/firestore_android.cc
     src/android/firestore_android.h
     src/android/geo_point_android.cc
@@ -101,6 +99,8 @@ set(android_SRCS
     src/android/server_timestamp_behavior_android.h
     src/android/set_options_android.cc
     src/android/set_options_android.h
+    src/android/settings_android.cc
+    src/android/settings_android.h
     src/android/snapshot_metadata_android.cc
     src/android/snapshot_metadata_android.h
     src/android/source_android.cc
diff --git a/firestore/firestore_resources/build.gradle b/firestore/firestore_resources/build.gradle
index 357394b0cc..04ed413046 100644
--- a/firestore/firestore_resources/build.gradle
+++ b/firestore/firestore_resources/build.gradle
@@ -43,16 +43,11 @@ android {
       }
     }
   }
-
-  lintOptions {
-    abortOnError false
-  }
 }
 
-
 dependencies {
-  implementation 'com.google.firebase:firebase-analytics:17.4.4'
-  implementation 'com.google.firebase:firebase-firestore:21.5.0'
+  implementation 'com.google.firebase:firebase-analytics:17.5.0'
+  implementation 'com.google.firebase:firebase-firestore:21.6.0'
 }
 
 afterEvaluate {
diff --git a/firestore/run_local_tests.sh b/firestore/run_local_tests.sh
index 07a1ff0898..b0ef454e40 100755
--- a/firestore/run_local_tests.sh
+++ b/firestore/run_local_tests.sh
@@ -3,9 +3,99 @@
 # TAP or Guitar at the moment (see b/135205911). Execute this script before
 # submitting.
 
+set -euo pipefail
+
+# Executes blaze to run a Kokoro test.
+#
+# The first argument is the name of an associative array variable. A key/value
+# pair will be inserted into this associative array with information about the
+# launched blaze process. The key will be the PID of the blaze process and the
+# value will be the full blaze command that was executed.
+#
+# The second argument is the "name" of the test, and is used in the subdirectory
+# name of the directory specified to blaze's --output_base argument.
+#
+# The remaining arguments, if any, will be specified to the blaze command after
+# the "test" argument.
+function run_blaze {
+  if [[ $# -lt 2 ]] ; then
+    echo "INTERNAL ERROR: run_blaze invalid arguments: $*" >&2
+    exit 1
+  fi
+
+  local -n readonly blaze_process_map="$1"
+  local readonly blaze_build_name="$2"
+  shift 2
+
+  local readonly blaze_args=(
+    "blaze"
+    "--blazerc=/dev/null"
+    "--output_base=/tmp/run_local_tests_blaze_output_bases/${blaze_build_name}"
+    "test"
+    "$@"
+    "//firebase/firestore/client/cpp:kokoro_build_test"
+  )
+
+  echo "${blaze_args[*]}"
+  "${blaze_args[@]}" &
+  blaze_process_map["$!"]="${blaze_args[*]}"
+}
+
+# Waits for blaze commands started by `run_blaze` to complete.
+#
+# If all blaze commands complete successfully then a "success" message is
+# printed; otherwise, if one or more of the blaze commands fail, then an error
+# message is printed.
+#
+# The first (and only) argument is the name of an associative array variable.
+# This variable should be the same that was was specified to `run_blaze` and
+# specifies the blaze commands for which to wait.
+#
+# The return value is 0 if all blaze processes completed successfully or 1 if
+# one or more of the blaze processes failed.
+function wait_for_blazes_to_complete {
+  if [[ $# -ne 1 ]] ; then
+    echo "INTERNAL ERROR: wait_for_blazes_to_complete invalid arguments: $*" >&2
+    exit 1
+  fi
+
+  local -n blaze_process_map="$1"
+
+  local blaze_pid
+  local -a blaze_failed_pids=()
+
+  for blaze_pid in "${!blaze_process_map[@]}" ; do
+    if ! wait "${blaze_pid}" ; then
+      blaze_failed_pids+=("${blaze_pid}")
+    fi
+  done
+
+  local readonly num_failed_blazes="${#blaze_failed_pids[@]}"
+  if [[ ${num_failed_blazes} -eq 0 ]] ; then
+    echo "All blaze commands completed successfully."
+  else
+    echo "ERROR: The following ${num_failed_blazes} blaze invocation(s) failed:"
+    for blaze_pid in "${blaze_failed_pids[@]}" ; do
+      echo "${blaze_process_map[${blaze_pid}]}"
+    done
+    echo "Go to http://sponge2 to see the results."
+    return 1
+  fi
+}
+
+# The main program begins here.
+if [[ $# -gt 0 ]] ; then
+  echo "ERROR: $0 does not accept any arguments, but got: $*" >&2
+  exit 2
+fi
+
+declare -A BLAZE_PROCESS_MAP
+
 # LINT.IfChange
-blaze test --config=android_arm //firebase/firestore/client/cpp:kokoro_build_test && \
-blaze test --config=darwin_x86_64 //firebase/firestore/client/cpp:kokoro_build_test && \
-blaze test //firebase/firestore/client/cpp:kokoro_build_test && \
-blaze test --config=msvc //firebase/firestore/client/cpp:kokoro_build_test
+run_blaze "BLAZE_PROCESS_MAP" "android_arm" "--config=android_arm"
+run_blaze "BLAZE_PROCESS_MAP" "darwin_x86_64" "--config=darwin_x86_64"
+run_blaze "BLAZE_PROCESS_MAP" "linux" "--define=force_regular_grpc=1"
+run_blaze "BLAZE_PROCESS_MAP" "msvc" "--config=msvc"
 # LINT.ThenChange(//depot_firebase_cpp/firestore/client/cpp/METADATA)
+
+wait_for_blazes_to_complete "BLAZE_PROCESS_MAP"
diff --git a/firestore/src/android/blob_android.cc b/firestore/src/android/blob_android.cc
index 8e7091b6d3..ac68a1d173 100644
--- a/firestore/src/android/blob_android.cc
+++ b/firestore/src/android/blob_android.cc
@@ -1,62 +1,48 @@
 #include "firestore/src/android/blob_android.h"
 
-#include "app/src/util_android.h"
-#include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/array.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
-
-// clang-format off
-#define BLOB_METHODS(X)                                                        \
-  X(Constructor, "", "(Lcom/google/protobuf/ByteString;)V",              \
-    util::kMethodTypeInstance),                                                \
-  X(FromBytes, "fromBytes", "([B)Lcom/google/firebase/firestore/Blob;",        \
-    util::kMethodTypeStatic),                                                  \
-  X(ToBytes, "toBytes", "()[B")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(blob, BLOB_METHODS)
-METHOD_LOOKUP_DEFINITION(blob,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/Blob",
-                         BLOB_METHODS)
-
-/* static */
-jobject BlobInternal::BlobToJavaBlob(JNIEnv* env, const uint8_t* value,
-                                     size_t size) {
-  jobject byte_array = util::ByteBufferToJavaByteArray(env, value, size);
-  jobject result = env->CallStaticObjectMethod(
-      blob::GetClass(), blob::GetMethodId(blob::kFromBytes), byte_array);
-  env->DeleteLocalRef(byte_array);
-  CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-jbyteArray BlobInternal::JavaBlobToJbyteArray(JNIEnv* env, jobject obj) {
-  jbyteArray result = static_cast(
-      env->CallObjectMethod(obj, blob::GetMethodId(blob::kToBytes)));
-  CheckAndClearJniExceptions(env);
-  return result;
+namespace {
+
+using jni::Array;
+using jni::Class;
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+using jni::StaticMethod;
+
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/Blob";
+jclass g_class = nullptr;
+
+Method kConstructor("", "(Lcom/google/protobuf/ByteString;)V");
+StaticMethod kFromBytes(
+    "fromBytes", "([B)Lcom/google/firebase/firestore/Blob;");
+Method> kToBytes("toBytes", "()[B");
+
+}  // namespace
+
+void BlobInternal::Initialize(jni::Loader& loader) {
+  g_class = loader.LoadClass(kClass);
+  loader.LoadAll(kConstructor, kFromBytes, kToBytes);
 }
 
-/* static */
-jclass BlobInternal::GetClass() { return blob::GetClass(); }
+Class BlobInternal::GetClass() { return Class(g_class); }
 
-/* static */
-bool BlobInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = blob::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
+Local BlobInternal::Create(Env& env, const uint8_t* value,
+                                         size_t size) {
+  Local> byte_array = env.NewArray(size);
+  env.SetArrayRegion(byte_array, 0, size, value);
+  return env.Call(kFromBytes, byte_array);
 }
 
-/* static */
-void BlobInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  blob::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+Local> BlobInternal::ToBytes(Env& env) const {
+  return env.Call(*this, kToBytes);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/blob_android.h b/firestore/src/android/blob_android.h
index 96e0c43a14..187072a4c4 100644
--- a/firestore/src/android/blob_android.h
+++ b/firestore/src/android/blob_android.h
@@ -1,26 +1,24 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_BLOB_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_BLOB_ANDROID_H_
 
-#include 
-
-#include "app/src/include/firebase/app.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
 
 namespace firebase {
 namespace firestore {
 
-class BlobInternal {
+class BlobInternal : public jni::Object {
  public:
-  static jobject BlobToJavaBlob(JNIEnv* env, const uint8_t* value, size_t size);
+  using Object::Object;
 
-  static jbyteArray JavaBlobToJbyteArray(JNIEnv* env, jobject obj);
+  static void Initialize(jni::Loader& loader);
 
-  static jclass GetClass();
+  static jni::Class GetClass();
 
- private:
-  friend class FirestoreInternal;
+  static jni::Local Create(jni::Env& env, const uint8_t* value,
+                                         size_t size);
 
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  jni::Local> ToBytes(jni::Env& env) const;
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/collection_reference_android.cc b/firestore/src/android/collection_reference_android.cc
index 4170892604..20637cb4be 100644
--- a/firestore/src/android/collection_reference_android.cc
+++ b/firestore/src/android/collection_reference_android.cc
@@ -1,6 +1,5 @@
 #include "firestore/src/android/collection_reference_android.h"
 
-
 #include 
 #include 
 
@@ -9,133 +8,89 @@
 #include "firestore/src/android/field_value_android.h"
 #include "firestore/src/android/promise_android.h"
 #include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
-
-// clang-format off
-#define COLLECTION_REFERENCE_METHODS(X)                               \
-  X(GetId, "getId", "()Ljava/lang/String;"),                          \
-  X(GetPath, "getPath", "()Ljava/lang/String;"),                      \
-  X(GetParent, "getParent",                                           \
-    "()Lcom/google/firebase/firestore/DocumentReference;"),           \
-  X(DocumentAutoId, "document",                                       \
-    "()Lcom/google/firebase/firestore/DocumentReference;"),           \
-  X(Document, "document", "(Ljava/lang/String;)"                      \
-    "Lcom/google/firebase/firestore/DocumentReference;"),             \
-  X(Add, "add",                                                       \
-    "(Ljava/lang/Object;)Lcom/google/android/gms/tasks/Task;")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(collection_reference, COLLECTION_REFERENCE_METHODS)
-METHOD_LOOKUP_DEFINITION(collection_reference,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/CollectionReference",
-                         COLLECTION_REFERENCE_METHODS)
+namespace {
+
+using jni::Class;
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+using jni::String;
+
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/CollectionReference";
+
+Method kGetId("getId", "()Ljava/lang/String;");
+Method kGetPath("getPath", "()Ljava/lang/String;");
+Method kGetParent(
+    "getParent", "()Lcom/google/firebase/firestore/DocumentReference;");
+Method kDocumentAutoId(
+    "document", "()Lcom/google/firebase/firestore/DocumentReference;");
+Method kDocument("document",
+                         "(Ljava/lang/String;)"
+                         "Lcom/google/firebase/firestore/DocumentReference;");
+Method kAdd("add",
+                    "(Ljava/lang/Object;)Lcom/google/android/gms/tasks/Task;");
+
+}  // namespace
+
+void CollectionReferenceInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kGetId, kGetPath, kGetParent, kDocumentAutoId,
+                   kDocument, kAdd);
+}
 
 const std::string& CollectionReferenceInternal::id() const {
-  if (!cached_id_.empty()) {
-    return cached_id_;
+  if (cached_id_.empty()) {
+    Env env = GetEnv();
+    cached_id_ = env.Call(obj_, kGetId).ToString(env);
   }
-
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jstring id = static_cast(env->CallObjectMethod(
-      obj_, collection_reference::GetMethodId(collection_reference::kGetId)));
-  cached_id_ = util::JniStringToString(env, id);
-  CheckAndClearJniExceptions(env);
-
   return cached_id_;
 }
 
 const std::string& CollectionReferenceInternal::path() const {
-  if (!cached_path_.empty()) {
-    return cached_path_;
+  if (cached_path_.empty()) {
+    Env env = GetEnv();
+    cached_path_ = env.Call(obj_, kGetPath).ToString(env);
   }
-
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jstring path = static_cast(env->CallObjectMethod(
-      obj_, collection_reference::GetMethodId(collection_reference::kGetPath)));
-  cached_path_ = util::JniStringToString(env, path);
-  CheckAndClearJniExceptions(env);
-
   return cached_path_;
 }
 
 DocumentReference CollectionReferenceInternal::Parent() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject parent = env->CallObjectMethod(
-      obj_,
-      collection_reference::GetMethodId(collection_reference::kGetParent));
-  CheckAndClearJniExceptions(env);
-  if (parent == nullptr) {
-    return DocumentReference();
-  } else {
-    DocumentReferenceInternal* internal =
-        new DocumentReferenceInternal{firestore_, parent};
-    env->DeleteLocalRef(parent);
-    return DocumentReference(internal);
-  }
+  Env env = GetEnv();
+  Local parent = env.Call(obj_, kGetParent);
+  return firestore_->NewDocumentReference(env, parent);
 }
 
 DocumentReference CollectionReferenceInternal::Document() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject document = env->CallObjectMethod(
-      obj_,
-      collection_reference::GetMethodId(collection_reference::kDocumentAutoId));
-  DocumentReferenceInternal* internal =
-      new DocumentReferenceInternal{firestore_, document};
-  env->DeleteLocalRef(document);
-  CheckAndClearJniExceptions(env);
-  return DocumentReference(internal);
+  Env env = GetEnv();
+  Local document = env.Call(obj_, kDocumentAutoId);
+  return firestore_->NewDocumentReference(env, document);
 }
 
 DocumentReference CollectionReferenceInternal::Document(
     const std::string& document_path) const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jstring path_string = env->NewStringUTF(document_path.c_str());
-  jobject document = env->CallObjectMethod(
-      obj_, collection_reference::GetMethodId(collection_reference::kDocument),
-      path_string);
-  env->DeleteLocalRef(path_string);
-  CheckAndClearJniExceptions(env);
-  DocumentReferenceInternal* internal =
-      new DocumentReferenceInternal{firestore_, document};
-  env->DeleteLocalRef(document);
-  CheckAndClearJniExceptions(env);
-  return DocumentReference(internal);
+  Env env = GetEnv();
+  Local java_path = env.NewStringUtf(document_path);
+  Local document = env.Call(obj_, kDocument, java_path);
+  return firestore_->NewDocumentReference(env, document);
 }
 
 Future CollectionReferenceInternal::Add(
     const MapFieldValue& data) {
   FieldValueInternal map_value(data);
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject task = env->CallObjectMethod(
-      obj_, collection_reference::GetMethodId(collection_reference::kAdd),
-      map_value.java_object());
-  CheckAndClearJniExceptions(env);
+
+  Env env = GetEnv();
+  Local task = env.Call(obj_, kAdd, map_value.java_object());
 
   auto promise = promises_.MakePromise();
-  promise.RegisterForTask(CollectionReferenceFn::kAdd, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
+  promise.RegisterForTask(AsyncFn::kAdd, task.get());
   return promise.GetFuture();
 }
 
-/* static */
-bool CollectionReferenceInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = collection_reference::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void CollectionReferenceInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  collection_reference::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-}
-
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/android/collection_reference_android.h b/firestore/src/android/collection_reference_android.h
index 3b1073be2b..44429793b9 100644
--- a/firestore/src/android/collection_reference_android.h
+++ b/firestore/src/android/collection_reference_android.h
@@ -5,24 +5,27 @@
 
 #include "firestore/src/android/firestore_android.h"
 #include "firestore/src/android/query_android.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
 
-// To make things simple, CollectionReferenceInternal uses the Future management
-// from its base class, QueryInternal. Each API of CollectionReference that
-// returns a Future needs to define an enum value to QueryFn. For example, a
-// Future-returning method Foo() relies on the enum value QueryFn::kFoo. The
-// enum values are used to identify and manage Future in the Firestore Future
-// manager.
-using CollectionReferenceFn = QueryFn;
-
 // This is the Android implementation of CollectionReference.
 class CollectionReferenceInternal : public QueryInternal {
  public:
   using ApiType = CollectionReference;
   using QueryInternal::QueryInternal;
 
+  // To make things simple, CollectionReferenceInternal uses the Future
+  // management from its base class, QueryInternal. Each API of
+  // CollectionReference that returns a Future needs to define an enum value in
+  // QueryFn. For example, a Future-returning method Foo() relies on the enum
+  // value AsyncFn::kFoo. The enum values are used to identify and manage Future
+  // in the Firestore Future manager.
+  using AsyncFn = QueryInternal::AsyncFn;
+
+  static void Initialize(jni::Loader& loader);
+
   const std::string& id() const;
   const std::string& path() const;
   DocumentReference Parent() const;
@@ -33,9 +36,6 @@ class CollectionReferenceInternal : public QueryInternal {
  private:
   friend class FirestoreInternal;
 
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
-
   // Below are cached call results.
   mutable std::string cached_id_;
   mutable std::string cached_path_;
diff --git a/firestore/src/android/direction_android.cc b/firestore/src/android/direction_android.cc
index bb45dfc810..4aaef50e43 100644
--- a/firestore/src/android/direction_android.cc
+++ b/firestore/src/android/direction_android.cc
@@ -1,75 +1,37 @@
 #include "firestore/src/android/direction_android.h"
 
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-// clang-format off
-#define DIRECTION_METHODS(X)                               \
-  X(Name, "name", "()Ljava/lang/String;")
-#define DIRECTION_FIELDS(X)                                \
-  X(Ascending, "ASCENDING",                                \
-    "Lcom/google/firebase/firestore/Query$Direction;",     \
-    util::kFieldTypeStatic),                               \
-  X(Descending, "DESCENDING",                              \
-    "Lcom/google/firebase/firestore/Query$Direction;",     \
-    util::kFieldTypeStatic)
-// clang-format on
+using jni::Env;
+using jni::Local;
+using jni::Object;
+using jni::StaticField;
 
-METHOD_LOOKUP_DECLARATION(direction, DIRECTION_METHODS, DIRECTION_FIELDS)
-METHOD_LOOKUP_DEFINITION(direction,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/Query$Direction",
-                         DIRECTION_METHODS, DIRECTION_FIELDS)
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/Query$Direction";
+StaticField kAscending(
+    "ASCENDING", "Lcom/google/firebase/firestore/Query$Direction;");
+StaticField kDescending(
+    "DESCENDING", "Lcom/google/firebase/firestore/Query$Direction;");
 
-jobject DirectionInternal::ascending_ = nullptr;
-jobject DirectionInternal::descending_ = nullptr;
+}  // namespace
 
-/* static */
-jobject DirectionInternal::ToJavaObject(JNIEnv* env,
-                                        Query::Direction direction) {
+void DirectionInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kAscending, kDescending);
+}
+
+Local DirectionInternal::Create(Env& env, Query::Direction direction) {
   if (direction == Query::Direction::kAscending) {
-    return ascending_;
+    return env.Get(kAscending);
   } else {
-    return descending_;
+    return env.Get(kDescending);
   }
 }
 
-/* static */
-bool DirectionInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = direction::CacheMethodIds(env, activity) &&
-                direction::CacheFieldIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-
-  // Cache Java enum values.
-  jobject value = env->GetStaticObjectField(
-      direction::GetClass(), direction::GetFieldId(direction::kAscending));
-  ascending_ = env->NewGlobalRef(value);
-  env->DeleteLocalRef(value);
-
-  value = env->GetStaticObjectField(
-      direction::GetClass(), direction::GetFieldId(direction::kDescending));
-  descending_ = env->NewGlobalRef(value);
-  env->DeleteLocalRef(value);
-  util::CheckAndClearJniExceptions(env);
-
-  return result;
-}
-
-/* static */
-void DirectionInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  direction::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-
-  // Uncache Java enum values.
-  env->DeleteGlobalRef(ascending_);
-  ascending_ = nullptr;
-  env->DeleteGlobalRef(descending_);
-  descending_ = nullptr;
-}
-
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/android/direction_android.h b/firestore/src/android/direction_android.h
index 505c09cdae..88f567ef99 100644
--- a/firestore/src/android/direction_android.h
+++ b/firestore/src/android/direction_android.h
@@ -1,25 +1,18 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_DIRECTION_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_DIRECTION_ANDROID_H_
 
-#include "app/src/include/firebase/app.h"
-#include "app/src/util_android.h"
 #include "firestore/src/android/query_android.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
 
 class DirectionInternal {
  public:
-  static jobject ToJavaObject(JNIEnv* env, Query::Direction direction);
+  static void Initialize(jni::Loader& loader);
 
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
-
-  static jobject ascending_;
-  static jobject descending_;
+  static jni::Local Create(jni::Env& env,
+                                        Query::Direction direction);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/document_change_android.cc b/firestore/src/android/document_change_android.cc
index 323d4df0c6..5f31bcecee 100644
--- a/firestore/src/android/document_change_android.cc
+++ b/firestore/src/android/document_change_android.cc
@@ -1,92 +1,56 @@
 #include "firestore/src/android/document_change_android.h"
 
-#include 
-
-#include "app/src/util_android.h"
 #include "firestore/src/android/document_change_type_android.h"
 #include "firestore/src/android/document_snapshot_android.h"
-#include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-using Type = DocumentChange::Type;
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
 
-// clang-format off
-#define DOCUMENT_CHANGE_METHODS(X)                                             \
-  X(Type, "getType", "()Lcom/google/firebase/firestore/DocumentChange$Type;"), \
-  X(Document, "getDocument",                                                   \
-    "()Lcom/google/firebase/firestore/QueryDocumentSnapshot;"),                \
-  X(OldIndex, "getOldIndex", "()I"),                                           \
-  X(NewIndex, "getNewIndex", "()I")
-// clang-format on
+using Type = DocumentChange::Type;
 
-METHOD_LOOKUP_DECLARATION(document_change, DOCUMENT_CHANGE_METHODS)
-METHOD_LOOKUP_DEFINITION(document_change,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/DocumentChange",
-                         DOCUMENT_CHANGE_METHODS)
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/DocumentChange";
+Method kType(
+    "getType", "()Lcom/google/firebase/firestore/DocumentChange$Type;");
+Method kDocument(
+    "getDocument", "()Lcom/google/firebase/firestore/QueryDocumentSnapshot;");
+Method kOldIndex("getOldIndex", "()I");
+Method kNewIndex("getNewIndex", "()I");
 
-Type DocumentChangeInternal::type() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
+}  // namespace
 
-  jobject type = env->CallObjectMethod(
-      obj_, document_change::GetMethodId(document_change::kType));
-  Type result =
-      DocumentChangeTypeInternal::JavaDocumentChangeTypeToDocumentChangeType(
-          env, type);
-  env->DeleteLocalRef(type);
+void DocumentChangeInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kDocument, kOldIndex, kNewIndex);
+}
 
-  CheckAndClearJniExceptions(env);
-  return result;
+Type DocumentChangeInternal::type() const {
+  Env env = GetEnv();
+  Local type = env.Call(obj_, kType);
+  return type.GetType(env);
 }
 
 DocumentSnapshot DocumentChangeInternal::document() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  jobject snapshot = env->CallObjectMethod(
-      obj_, document_change::GetMethodId(document_change::kDocument));
-  CheckAndClearJniExceptions(env);
-
-  DocumentSnapshot result{new DocumentSnapshotInternal{firestore_, snapshot}};
-  env->DeleteLocalRef(snapshot);
-  return result;
+  Env env = GetEnv();
+  Local snapshot = env.Call(obj_, kDocument);
+  return firestore_->NewDocumentSnapshot(env, snapshot);
 }
 
 std::size_t DocumentChangeInternal::old_index() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  jint index = env->CallIntMethod(
-      obj_, document_change::GetMethodId(document_change::kOldIndex));
-  CheckAndClearJniExceptions(env);
-
-  return static_cast(index);
+  Env env = GetEnv();
+  return env.Call(obj_, kOldIndex);
 }
 
 std::size_t DocumentChangeInternal::new_index() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  jint index = env->CallIntMethod(
-      obj_, document_change::GetMethodId(document_change::kNewIndex));
-  CheckAndClearJniExceptions(env);
-
-  return static_cast(index);
-}
-
-/* static */
-bool DocumentChangeInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = document_change::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void DocumentChangeInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  document_change::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  return env.Call(obj_, kNewIndex);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/document_change_android.h b/firestore/src/android/document_change_android.h
index 787356d384..938a415d8c 100644
--- a/firestore/src/android/document_change_android.h
+++ b/firestore/src/android/document_change_android.h
@@ -2,10 +2,10 @@
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_DOCUMENT_CHANGE_ANDROID_H_
 
 #include 
-#include 
 
 #include "firestore/src/android/wrapper.h"
 #include "firestore/src/include/firebase/firestore/document_change.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
@@ -15,16 +15,12 @@ class DocumentChangeInternal : public Wrapper {
   using ApiType = DocumentChange;
   using Wrapper::Wrapper;
 
+  static void Initialize(jni::Loader& loader);
+
   DocumentChange::Type type() const;
   DocumentSnapshot document() const;
   std::size_t old_index() const;
   std::size_t new_index() const;
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/document_change_type_android.cc b/firestore/src/android/document_change_type_android.cc
index 2cd87eeb4a..90ae7c4eae 100644
--- a/firestore/src/android/document_change_type_android.cc
+++ b/firestore/src/android/document_change_type_android.cc
@@ -1,85 +1,41 @@
 #include "firestore/src/android/document_change_type_android.h"
 
-#include "app/src/util_android.h"
+#include "../include/firebase/firestore/document_change.h"
+#include "app/src/assert.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-using Type = DocumentChange::Type;
-
-// clang-format off
-#define DOCUMENT_CHANGE_TYPE_METHODS(X) X(Name, "name", "()Ljava/lang/String;")
-#define DOCUMENT_CHANGE_TYPE_FIELDS(X)                                         \
-  X(Added, "ADDED", "Lcom/google/firebase/firestore/DocumentChange$Type;",     \
-    util::kFieldTypeStatic),                                                   \
-  X(Modified, "MODIFIED",                                                      \
-    "Lcom/google/firebase/firestore/DocumentChange$Type;",                     \
-    util::kFieldTypeStatic),                                                   \
-  X(Removed, "REMOVED", "Lcom/google/firebase/firestore/DocumentChange$Type;", \
-    util::kFieldTypeStatic)
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(document_change_type, DOCUMENT_CHANGE_TYPE_METHODS,
-                          DOCUMENT_CHANGE_TYPE_FIELDS)
-METHOD_LOOKUP_DEFINITION(document_change_type,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/DocumentChange$Type",
-                         DOCUMENT_CHANGE_TYPE_METHODS,
-                         DOCUMENT_CHANGE_TYPE_FIELDS)
-
-std::map* DocumentChangeTypeInternal::cpp_enum_to_java_ =
-    nullptr;
+using jni::Env;
+using jni::Method;
+using jni::Object;
 
-/* static */
-Type DocumentChangeTypeInternal::JavaDocumentChangeTypeToDocumentChangeType(
-    JNIEnv* env, jobject type) {
-  for (const auto& kv : *cpp_enum_to_java_) {
-    if (env->IsSameObject(type, kv.second)) {
-      return kv.first;
-    }
-  }
-  FIREBASE_ASSERT_MESSAGE(false, "Unknown DocumentChange type.");
-  return Type::kAdded;
-}
+using Type = DocumentChange::Type;
 
-/* static */
-bool DocumentChangeTypeInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = document_change_type::CacheMethodIds(env, activity) &&
-                document_change_type::CacheFieldIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/DocumentChange$Type";
+Method kOrdinal("ordinal", "()I");
 
-  // Cache Java enum values.
-  cpp_enum_to_java_ = new std::map();
-  const auto add_enum = [env](Type type, document_change_type::Field field) {
-    jobject value =
-        env->GetStaticObjectField(document_change_type::GetClass(),
-                                  document_change_type::GetFieldId(field));
-    (*cpp_enum_to_java_)[type] = env->NewGlobalRef(value);
-    env->DeleteLocalRef(value);
-    util::CheckAndClearJniExceptions(env);
-  };
-  add_enum(Type::kAdded, document_change_type::kAdded);
-  add_enum(Type::kModified, document_change_type::kModified);
-  add_enum(Type::kRemoved, document_change_type::kRemoved);
+}  // namespace
 
-  return result;
+void DocumentChangeTypeInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kOrdinal);
 }
 
-/* static */
-void DocumentChangeTypeInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  document_change_type::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+Type DocumentChangeTypeInternal::GetType(Env& env) const {
+  static constexpr int32_t kMinType = static_cast(Type::kAdded);
+  static constexpr int32_t kMaxType = static_cast(Type::kRemoved);
 
-  // Uncache Java enum values.
-  for (auto& kv : *cpp_enum_to_java_) {
-    env->DeleteGlobalRef(kv.second);
+  int32_t ordinal = env.Call(*this, kOrdinal);
+  if (ordinal >= kMinType && ordinal <= kMaxType) {
+    return static_cast(ordinal);
+  } else {
+    FIREBASE_ASSERT_MESSAGE(false, "Unknown DocumentChange type.");
+    return Type::kAdded;
   }
-  util::CheckAndClearJniExceptions(env);
-  delete cpp_enum_to_java_;
-  cpp_enum_to_java_ = nullptr;
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/document_change_type_android.h b/firestore/src/android/document_change_type_android.h
index 786885913b..cba99b531d 100644
--- a/firestore/src/android/document_change_type_android.h
+++ b/firestore/src/android/document_change_type_android.h
@@ -1,28 +1,20 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_DOCUMENT_CHANGE_TYPE_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_DOCUMENT_CHANGE_TYPE_ANDROID_H_
 
-#include 
-
-#include 
-
-#include "app/src/include/firebase/app.h"
 #include "firestore/src/include/firebase/firestore/document_change.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
 
 namespace firebase {
 namespace firestore {
 
-class DocumentChangeTypeInternal {
+class DocumentChangeTypeInternal : public jni::Object {
  public:
-  static DocumentChange::Type JavaDocumentChangeTypeToDocumentChangeType(
-      JNIEnv* env, jobject type);
-
- private:
-  friend class FirestoreInternal;
+  using jni::Object::Object;
 
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  static void Initialize(jni::Loader& loader);
 
-  static std::map* cpp_enum_to_java_;
+  DocumentChange::Type GetType(jni::Env& env) const;
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/document_reference_android.cc b/firestore/src/android/document_reference_android.cc
index cb51943ad9..5cf3c26620 100644
--- a/firestore/src/android/document_reference_android.cc
+++ b/firestore/src/android/document_reference_android.cc
@@ -15,42 +15,60 @@
 #include "firestore/src/android/source_android.h"
 #include "firestore/src/android/util_android.h"
 #include "firestore/src/include/firebase/firestore.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
-
-// clang-format off
-#define DOCUMENT_REFERENCE_METHODS(X)                                 \
-  X(GetId, "getId", "()Ljava/lang/String;"),                          \
-  X(GetPath, "getPath", "()Ljava/lang/String;"),                      \
-  X(GetParent, "getParent",                                           \
-    "()Lcom/google/firebase/firestore/CollectionReference;"),         \
-  X(Collection, "collection", "(Ljava/lang/String;)"                  \
-    "Lcom/google/firebase/firestore/CollectionReference;"),           \
-  X(Get, "get",                                                       \
-    "(Lcom/google/firebase/firestore/Source;)"                        \
-    "Lcom/google/android/gms/tasks/Task;"),                           \
-  X(Set, "set",                                                       \
-   "(Ljava/lang/Object;Lcom/google/firebase/firestore/SetOptions;)"   \
-   "Lcom/google/android/gms/tasks/Task;"),                            \
-  X(Update, "update",                                                 \
-   "(Ljava/util/Map;)Lcom/google/android/gms/tasks/Task;"),           \
-  X(UpdateVarargs, "update",                                          \
-   "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;"     \
-   "[Ljava/lang/Object;)Lcom/google/android/gms/tasks/Task;"),        \
-  X(Delete, "delete", "()Lcom/google/android/gms/tasks/Task;"),       \
-  X(AddSnapshotListener, "addSnapshotListener",                       \
-    "(Ljava/util/concurrent/Executor;"                                \
-    "Lcom/google/firebase/firestore/MetadataChanges;"                 \
-    "Lcom/google/firebase/firestore/EventListener;)"                  \
-    "Lcom/google/firebase/firestore/ListenerRegistration;")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(document_reference, DOCUMENT_REFERENCE_METHODS)
-METHOD_LOOKUP_DEFINITION(document_reference,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/DocumentReference",
-                         DOCUMENT_REFERENCE_METHODS)
+namespace {
+
+using jni::Class;
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+using jni::String;
+
+constexpr char kClassName[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/DocumentReference";
+jclass clazz = nullptr;
+
+Method kGetId("getId", "()Ljava/lang/String;");
+Method kGetPath("getPath", "()Ljava/lang/String;");
+Method kGetParent(
+    "getParent", "()Lcom/google/firebase/firestore/CollectionReference;");
+Method kCollection(
+    "collection",
+    "(Ljava/lang/String;)"
+    "Lcom/google/firebase/firestore/CollectionReference;");
+Method kGet("get",
+                    "(Lcom/google/firebase/firestore/Source;)"
+                    "Lcom/google/android/gms/tasks/Task;");
+Method kSet(
+    "set",
+    "(Ljava/lang/Object;Lcom/google/firebase/firestore/SetOptions;)"
+    "Lcom/google/android/gms/tasks/Task;");
+Method kUpdate("update",
+                       "(Ljava/util/Map;)Lcom/google/android/gms/tasks/Task;");
+Method kUpdateVarargs(
+    "update",
+    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;"
+    "[Ljava/lang/Object;)Lcom/google/android/gms/tasks/Task;");
+Method kDelete("delete", "()Lcom/google/android/gms/tasks/Task;");
+Method kAddSnapshotListener(
+    "addSnapshotListener",
+    "(Ljava/util/concurrent/Executor;"
+    "Lcom/google/firebase/firestore/MetadataChanges;"
+    "Lcom/google/firebase/firestore/EventListener;)"
+    "Lcom/google/firebase/firestore/ListenerRegistration;");
+
+}  // namespace
+
+void DocumentReferenceInternal::Initialize(jni::Loader& loader) {
+  clazz = loader.LoadClass(kClassName);
+  loader.LoadAll(kGetId, kGetPath, kGetParent, kCollection, kGet, kSet, kUpdate,
+                 kUpdateVarargs, kDelete, kAddSnapshotListener);
+}
 
 Firestore* DocumentReferenceInternal::firestore() {
   FIREBASE_ASSERT(firestore_->firestore_public() != nullptr);
@@ -58,106 +76,56 @@ Firestore* DocumentReferenceInternal::firestore() {
 }
 
 const std::string& DocumentReferenceInternal::id() const {
-  if (!cached_id_.empty()) {
-    return cached_id_;
+  if (cached_id_.empty()) {
+    Env env = GetEnv();
+    cached_id_ = env.Call(obj_, kGetId).ToString(env);
   }
-
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jstring id = static_cast(env->CallObjectMethod(
-      obj_, document_reference::GetMethodId(document_reference::kGetId)));
-  cached_id_ = util::JniStringToString(env, id);
-  CheckAndClearJniExceptions(env);
-
   return cached_id_;
 }
 
 const std::string& DocumentReferenceInternal::path() const {
-  if (!cached_path_.empty()) {
-    return cached_path_;
+  if (cached_path_.empty()) {
+    Env env = GetEnv();
+    cached_path_ = env.Call(obj_, kGetPath).ToString(env);
   }
-
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jstring path = static_cast(env->CallObjectMethod(
-      obj_, document_reference::GetMethodId(document_reference::kGetPath)));
-  cached_path_ = util::JniStringToString(env, path);
-  CheckAndClearJniExceptions(env);
-
   return cached_path_;
 }
 
 CollectionReference DocumentReferenceInternal::Parent() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject parent = env->CallObjectMethod(
-      obj_, document_reference::GetMethodId(document_reference::kGetParent));
-  CollectionReferenceInternal* internal =
-      new CollectionReferenceInternal{firestore_, parent};
-  env->DeleteLocalRef(parent);
-  CheckAndClearJniExceptions(env);
-  return CollectionReference(internal);
+  Env env = GetEnv();
+  Local parent = env.Call(obj_, kGetParent);
+  return firestore_->NewCollectionReference(env, parent);
 }
 
 CollectionReference DocumentReferenceInternal::Collection(
     const std::string& collection_path) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jstring path_string = env->NewStringUTF(collection_path.c_str());
-  jobject collection = env->CallObjectMethod(
-      obj_, document_reference::GetMethodId(document_reference::kCollection),
-      path_string);
-  env->DeleteLocalRef(path_string);
-  CheckAndClearJniExceptions(env);
-  CollectionReferenceInternal* internal =
-      new CollectionReferenceInternal{firestore_, collection};
-  env->DeleteLocalRef(collection);
-  CheckAndClearJniExceptions(env);
-  return CollectionReference(internal);
+  Env env = GetEnv();
+  Local java_path = env.NewStringUtf(collection_path);
+  Local collection = env.Call(obj_, kCollection, java_path);
+  return firestore_->NewCollectionReference(env, collection);
 }
 
 Future DocumentReferenceInternal::Get(Source source) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject task = env->CallObjectMethod(
-      obj_, document_reference::GetMethodId(document_reference::kGet),
-      SourceInternal::ToJavaObject(env, source));
-  CheckAndClearJniExceptions(env);
-
-  auto promise = promises_.MakePromise();
-  promise.RegisterForTask(DocumentReferenceFn::kGet, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
+  Env env = GetEnv();
+  Local java_source = SourceInternal::Create(env, source);
+  Local task = env.Call(obj_, kGet, java_source);
+  return promises_.NewFuture(env, AsyncFn::kGet, task);
 }
 
 Future DocumentReferenceInternal::Set(const MapFieldValue& data,
                                             const SetOptions& options) {
+  Env env = GetEnv();
   FieldValueInternal map_value(data);
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject java_options = SetOptionsInternal::ToJavaObject(env, options);
-  CheckAndClearJniExceptions(env);
-  jobject task = env->CallObjectMethod(
-      obj_, document_reference::GetMethodId(document_reference::kSet),
-      map_value.java_object(), java_options);
-  env->DeleteLocalRef(java_options);
-  CheckAndClearJniExceptions(env);
-
-  auto promise = promises_.MakePromise();
-  promise.RegisterForTask(DocumentReferenceFn::kSet, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
+  Local java_options = SetOptionsInternal::Create(env, options);
+  Local task = env.Call(obj_, kSet, map_value, java_options);
+  return promises_.NewFuture(env, AsyncFn::kSet, task);
 }
 
 Future DocumentReferenceInternal::Update(const MapFieldValue& data) {
+  Env env = GetEnv();
   FieldValueInternal map_value(data);
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject task = env->CallObjectMethod(
-      obj_, document_reference::GetMethodId(document_reference::kUpdate),
-      map_value.java_object());
-  CheckAndClearJniExceptions(env);
-
-  auto promise = promises_.MakePromise();
-  promise.RegisterForTask(DocumentReferenceFn::kUpdate, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
+  Local task = env.Call(obj_, kUpdate, map_value);
+  return promises_.NewFuture(env, AsyncFn::kUpdate, task);
 }
 
 Future DocumentReferenceInternal::Update(const MapFieldPathValue& data) {
@@ -165,48 +133,26 @@ Future DocumentReferenceInternal::Update(const MapFieldPathValue& data) {
     return Update(MapFieldValue{});
   }
 
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  auto iter = data.begin();
-  jobject first_field = FieldPathConverter::ToJavaObject(env, iter->first);
-  jobject first_value = iter->second.internal_->java_object();
-  ++iter;
-
-  // Make the varargs
-  jobjectArray more_fields_and_values =
-      MapFieldPathValueToJavaArray(firestore_, iter, data.end());
+  Env env = GetEnv();
+  UpdateFieldPathArgs args = MakeUpdateFieldPathArgs(env, data);
+  Local task = env.Call(obj_, kUpdateVarargs, args.first_field,
+                                args.first_value, args.varargs);
 
-  jobject task = env->CallObjectMethod(
-      obj_, document_reference::GetMethodId(document_reference::kUpdateVarargs),
-      first_field, first_value, more_fields_and_values);
-  env->DeleteLocalRef(first_field);
-  env->DeleteLocalRef(more_fields_and_values);
-  CheckAndClearJniExceptions(env);
-
-  auto promise = promises_.MakePromise();
-  promise.RegisterForTask(DocumentReferenceFn::kUpdate, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
+  return promises_.NewFuture(env, AsyncFn::kUpdate, task);
 }
 
 Future DocumentReferenceInternal::Delete() {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject task = env->CallObjectMethod(
-      obj_, document_reference::GetMethodId(document_reference::kDelete));
-  CheckAndClearJniExceptions(env);
-
-  auto promise = promises_.MakePromise();
-  promise.RegisterForTask(DocumentReferenceFn::kDelete, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
+  Env env = GetEnv();
+  Local task = env.Call(obj_, kDelete);
+  return promises_.NewFuture(env, AsyncFn::kDelete, task);
 }
 
 #if defined(FIREBASE_USE_STD_FUNCTION)
 
 ListenerRegistration DocumentReferenceInternal::AddSnapshotListener(
     MetadataChanges metadata_changes,
-    std::function callback) {
+    std::function
+        callback) {
   LambdaEventListener* listener =
       new LambdaEventListener(firebase::Move(callback));
   return AddSnapshotListener(metadata_changes, listener,
@@ -218,51 +164,22 @@ ListenerRegistration DocumentReferenceInternal::AddSnapshotListener(
 ListenerRegistration DocumentReferenceInternal::AddSnapshotListener(
     MetadataChanges metadata_changes, EventListener* listener,
     bool passing_listener_ownership) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  // Create listener.
-  jobject java_listener =
-      EventListenerInternal::EventListenerToJavaEventListener(env, firestore_,
-                                                              listener);
-  jobject java_metadata =
-      MetadataChangesInternal::ToJavaObject(env, metadata_changes);
-
-  // Register listener.
-  jobject java_registration = env->CallObjectMethod(
-      obj_,
-      document_reference::GetMethodId(document_reference::kAddSnapshotListener),
-      firestore_->user_callback_executor(), java_metadata, java_listener);
-  env->DeleteLocalRef(java_listener);
-  CheckAndClearJniExceptions(env);
-
-  // Wrapping
-  ListenerRegistrationInternal* registration = new ListenerRegistrationInternal{
-      firestore_, listener, passing_listener_ownership, java_registration};
-  env->DeleteLocalRef(java_registration);
-
-  return ListenerRegistration{registration};
+  Env env = GetEnv();
+  Local java_metadata =
+      MetadataChangesInternal::Create(env, metadata_changes);
+  Local java_listener =
+      EventListenerInternal::Create(env, firestore_, listener);
+
+  Local java_registration =
+      env.Call(obj_, kAddSnapshotListener, firestore_->user_callback_executor(),
+               java_metadata, java_listener);
+
+  if (!env.ok() || !java_registration) return {};
+  return ListenerRegistration(new ListenerRegistrationInternal(
+      firestore_, listener, passing_listener_ownership, java_registration));
 }
 
-/* static */
-jclass DocumentReferenceInternal::GetClass() {
-  return document_reference::GetClass();
-}
-
-/* static */
-bool DocumentReferenceInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = document_reference::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void DocumentReferenceInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  document_reference::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-}
+Class DocumentReferenceInternal::GetClass() { return Class(clazz); }
 
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/android/document_reference_android.h b/firestore/src/android/document_reference_android.h
index 5fa010e4ea..65d72c3a3b 100644
--- a/firestore/src/android/document_reference_android.h
+++ b/firestore/src/android/document_reference_android.h
@@ -10,29 +10,30 @@
 #include "firestore/src/android/promise_factory_android.h"
 #include "firestore/src/android/wrapper.h"
 #include "firestore/src/include/firebase/firestore/collection_reference.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
 
 class Firestore;
 
-// Each API of DocumentReference that returns a Future needs to define an enum
-// value here. For example, a Future-returning method Foo() relies on the enum
-// value kFoo. The enum values are used to identify and manage Future in the
-// Firestore Future manager.
-enum class DocumentReferenceFn {
-  kGet = 0,
-  kSet,
-  kUpdate,
-  kDelete,
-  kCount,  // Must be the last enum value.
-};
-
 // This is the Android implementation of DocumentReference.
 class DocumentReferenceInternal : public Wrapper {
  public:
   using ApiType = DocumentReference;
 
+  // Each API of DocumentReference that returns a Future needs to define an enum
+  // value here. For example, a Future-returning method Foo() relies on the enum
+  // value kFoo. The enum values are used to identify and manage Future in the
+  // Firestore Future manager.
+  enum class AsyncFn {
+    kGet = 0,
+    kSet,
+    kUpdate,
+    kDelete,
+    kCount,  // Must be the last enum value.
+  };
+
   DocumentReferenceInternal(FirestoreInternal* firestore, jobject object)
       : Wrapper(firestore, object), promises_(firestore) {}
 
@@ -145,7 +146,8 @@ class DocumentReferenceInternal : public Wrapper {
    */
   ListenerRegistration AddSnapshotListener(
       MetadataChanges metadata_changes,
-      std::function callback);
+      std::function
+          callback);
 #endif  // defined(FIREBASE_USE_STD_FUNCTION)
 
   /**
@@ -170,15 +172,14 @@ class DocumentReferenceInternal : public Wrapper {
       bool passing_listener_ownership = false);
 
   /** @brief Gets the class object of Java DocumentReference class. */
-  static jclass GetClass();
+  static jni::Class GetClass();
 
  private:
   friend class FirestoreInternal;
 
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  static void Initialize(jni::Loader& loader);
 
-  PromiseFactory promises_;
+  PromiseFactory promises_;
 
   // Below are cached call results.
   mutable std::string cached_id_;
diff --git a/firestore/src/android/document_snapshot_android.cc b/firestore/src/android/document_snapshot_android.cc
index 54ab29abd8..0d2d05116c 100644
--- a/firestore/src/android/document_snapshot_android.cc
+++ b/firestore/src/android/document_snapshot_android.cc
@@ -4,44 +4,53 @@
 
 #include 
 
-#include "app/src/util_android.h"
 #include "firestore/src/android/document_reference_android.h"
 #include "firestore/src/android/field_path_android.h"
 #include "firestore/src/android/field_value_android.h"
 #include "firestore/src/android/server_timestamp_behavior_android.h"
 #include "firestore/src/android/snapshot_metadata_android.h"
-#include "firestore/src/android/util_android.h"
 #include "firestore/src/include/firebase/firestore.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
+
+using jni::Env;
+using jni::Local;
+using jni::Map;
+using jni::Method;
+using jni::Object;
+using jni::String;
 
 using ServerTimestampBehavior = DocumentSnapshot::ServerTimestampBehavior;
 
-// clang-format off
-#define DOCUMENT_SNAPSHOT_METHODS(X)                            \
-  X(GetId, "getId", "()Ljava/lang/String;"),                    \
-  X(GetReference, "getReference",                               \
-    "()Lcom/google/firebase/firestore/DocumentReference;"),     \
-  X(GetMetadata, "getMetadata",                                 \
-    "()Lcom/google/firebase/firestore/SnapshotMetadata;"),      \
-  X(Exists, "exists", "()Z"),                                   \
-  X(GetData, "getData",                                         \
-    "(Lcom/google/firebase/firestore/DocumentSnapshot$"         \
-    "ServerTimestampBehavior;)Ljava/util/Map;"),                \
-  X(Contains, "contains",                                       \
-    "(Lcom/google/firebase/firestore/FieldPath;)Z"),            \
-  X(Get, "get",                                                 \
-    "(Lcom/google/firebase/firestore/FieldPath;"                \
-    "Lcom/google/firebase/firestore/DocumentSnapshot$"          \
-    "ServerTimestampBehavior;)Ljava/lang/Object;")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(document_snapshot, DOCUMENT_SNAPSHOT_METHODS)
-METHOD_LOOKUP_DEFINITION(document_snapshot,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/DocumentSnapshot",
-                         DOCUMENT_SNAPSHOT_METHODS)
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/DocumentSnapshot";
+
+Method kGetId("getId", "()Ljava/lang/String;");
+Method kGetReference(
+    "getReference", "()Lcom/google/firebase/firestore/DocumentReference;");
+Method kGetMetadata(
+    "getMetadata", "()Lcom/google/firebase/firestore/SnapshotMetadata;");
+Method kExists("exists", "()Z");
+Method kGetData("getData",
+                        "(Lcom/google/firebase/firestore/DocumentSnapshot$"
+                        "ServerTimestampBehavior;)Ljava/util/Map;");
+Method kContains("contains",
+                       "(Lcom/google/firebase/firestore/FieldPath;)Z");
+Method kGet("get",
+                    "(Lcom/google/firebase/firestore/FieldPath;"
+                    "Lcom/google/firebase/firestore/DocumentSnapshot$"
+                    "ServerTimestampBehavior;)Ljava/lang/Object;");
+
+}  // namespace
+
+void DocumentSnapshotInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kGetId, kGetReference, kGetMetadata, kExists,
+                   kGetData, kContains, kGet);
+}
 
 Firestore* DocumentSnapshotInternal::firestore() const {
   FIREBASE_ASSERT(firestore_->firestore_public() != nullptr);
@@ -49,110 +58,56 @@ Firestore* DocumentSnapshotInternal::firestore() const {
 }
 
 const std::string& DocumentSnapshotInternal::id() const {
-  if (!cached_id_.empty()) {
-    return cached_id_;
+  if (cached_id_.empty()) {
+    Env env = GetEnv();
+    cached_id_ = env.Call(obj_, kGetId).ToString(env);
   }
-
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jstring id = static_cast(env->CallObjectMethod(
-      obj_, document_snapshot::GetMethodId(document_snapshot::kGetId)));
-  cached_id_ = util::JniStringToString(env, id);
-
-  CheckAndClearJniExceptions(env);
   return cached_id_;
 }
 
 DocumentReference DocumentSnapshotInternal::reference() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject reference = env->CallObjectMethod(
-      obj_, document_snapshot::GetMethodId(document_snapshot::kGetReference));
-  DocumentReferenceInternal* internal =
-      new DocumentReferenceInternal{firestore_, reference};
-  env->DeleteLocalRef(reference);
-
-  CheckAndClearJniExceptions(env);
-  return DocumentReference{internal};
+  Env env = GetEnv();
+  Local reference = env.Call(obj_, kGetReference);
+  return firestore_->NewDocumentReference(env, reference);
 }
 
 SnapshotMetadata DocumentSnapshotInternal::metadata() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject metadata = env->CallObjectMethod(
-      obj_, document_snapshot::GetMethodId(document_snapshot::kGetMetadata));
-  SnapshotMetadata result =
-      SnapshotMetadataInternal::JavaSnapshotMetadataToSnapshotMetadata(
-          env, metadata);
-
-  CheckAndClearJniExceptions(env);
-  return result;
+  Env env = GetEnv();
+  auto java_metadata = env.Call(obj_, kGetMetadata);
+  return java_metadata.ToPublic(env);
 }
 
 bool DocumentSnapshotInternal::exists() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jboolean exists = env->CallBooleanMethod(
-      obj_, document_snapshot::GetMethodId(document_snapshot::kExists));
-
-  CheckAndClearJniExceptions(env);
-  return static_cast(exists);
+  Env env = GetEnv();
+  return env.Call(obj_, kExists);
 }
 
 MapFieldValue DocumentSnapshotInternal::GetData(
     ServerTimestampBehavior stb) const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject stb_enum = ServerTimestampBehaviorInternal::ToJavaObject(env, stb);
-
-  jobject map_value = env->CallObjectMethod(
-      obj_, document_snapshot::GetMethodId(document_snapshot::kGetData),
-      stb_enum);
-  CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  Local java_stb = ServerTimestampBehaviorInternal::Create(env, stb);
+  Local java_data = env.Call(obj_, kGetData, java_stb);
 
-  FieldValueInternal value(firestore_, map_value);
-  env->DeleteLocalRef(map_value);
+  FieldValueInternal value(firestore_, java_data.get());
   return value.map_value();
 }
 
 FieldValue DocumentSnapshotInternal::Get(const FieldPath& field,
                                          ServerTimestampBehavior stb) const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject field_path = FieldPathConverter::ToJavaObject(env, field);
+  Env env = GetEnv();
+  Local java_field = FieldPathConverter::Create(env, field);
 
   // Android returns null for both null fields and nonexistent fields, so first
   // use contains() to check if the field exists.
-  jboolean contains_field = env->CallBooleanMethod(
-      obj_, document_snapshot::GetMethodId(document_snapshot::kContains),
-      field_path);
-  CheckAndClearJniExceptions(env);
+  bool contains_field = env.Call(obj_, kContains, java_field);
   if (!contains_field) {
-    env->DeleteLocalRef(field_path);
     return FieldValue();
-  } else {
-    jobject stb_enum = ServerTimestampBehaviorInternal::ToJavaObject(env, stb);
-
-    jobject field_value = env->CallObjectMethod(
-        obj_, document_snapshot::GetMethodId(document_snapshot::kGet),
-        field_path, stb_enum);
-    CheckAndClearJniExceptions(env);
-    env->DeleteLocalRef(field_path);
-
-    FieldValue result{new FieldValueInternal{firestore_, field_value}};
-    env->DeleteLocalRef(field_value);
-    return result;
   }
-}
 
-/* static */
-bool DocumentSnapshotInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = document_snapshot::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
+  Local java_stb = ServerTimestampBehaviorInternal::Create(env, stb);
+  Local field_value = env.Call(obj_, kGet, java_field, java_stb);
 
-/* static */
-void DocumentSnapshotInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  document_snapshot::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+  return FieldValue(new FieldValueInternal(firestore_, field_value.get()));
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/document_snapshot_android.h b/firestore/src/android/document_snapshot_android.h
index 30563f3726..5d3f10b808 100644
--- a/firestore/src/android/document_snapshot_android.h
+++ b/firestore/src/android/document_snapshot_android.h
@@ -1,10 +1,8 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_DOCUMENT_SNAPSHOT_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_DOCUMENT_SNAPSHOT_ANDROID_H_
 
-#include 
 #include 
 
-#include "app/src/include/firebase/app.h"
 #include "firestore/src/android/firestore_android.h"
 #include "firestore/src/android/wrapper.h"
 #include "firestore/src/include/firebase/firestore/document_reference.h"
@@ -21,6 +19,8 @@ class DocumentSnapshotInternal : public Wrapper {
   using ApiType = DocumentSnapshot;
   using Wrapper::Wrapper;
 
+  static void Initialize(jni::Loader& loader);
+
   ~DocumentSnapshotInternal() override {}
 
   /** Gets the Firestore instance associated with this document snapshot. */
@@ -46,12 +46,6 @@ class DocumentSnapshotInternal : public Wrapper {
                  DocumentSnapshot::ServerTimestampBehavior stb) const;
 
  private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
-
-  // Below are cached call results.
   mutable std::string cached_id_;
 };
 
diff --git a/firestore/src/android/event_listener_android.cc b/firestore/src/android/event_listener_android.cc
index 17d9e0d081..16986e5a95 100644
--- a/firestore/src/android/event_listener_android.cc
+++ b/firestore/src/android/event_listener_android.cc
@@ -7,196 +7,150 @@
 #include "app/src/include/firebase/internal/common.h"
 #include "app/src/util_android.h"
 #include "firestore/src/android/document_snapshot_android.h"
-#include "firestore/src/android/firebase_firestore_exception_android.h"
+#include "firestore/src/android/exception_android.h"
 #include "firestore/src/android/query_snapshot_android.h"
 #include "firestore/src/android/util_android.h"
+#include "firestore/src/common/util.h"
 #include "firestore/src/include/firebase/firestore/query_snapshot.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 #include "firebase/firestore/firestore_errors.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-#define CPP_EVENT_LISTENER_METHODS(X) \
-  X(DiscardPointers, "discardPointers", "()V")
-METHOD_LOOKUP_DECLARATION(cpp_event_listener, CPP_EVENT_LISTENER_METHODS)
-METHOD_LOOKUP_DEFINITION(
-    cpp_event_listener,
-    "com/google/firebase/firestore/internal/cpp/CppEventListener",
-    CPP_EVENT_LISTENER_METHODS)
-
-#define DOCUMENT_EVENT_LISTENER_METHODS(X) X(Constructor, "", "(JJ)V")
-METHOD_LOOKUP_DECLARATION(document_event_listener,
-                          DOCUMENT_EVENT_LISTENER_METHODS)
-METHOD_LOOKUP_DEFINITION(
-    document_event_listener,
-    "com/google/firebase/firestore/internal/cpp/DocumentEventListener",
-    DOCUMENT_EVENT_LISTENER_METHODS)
-
-#define QUERY_EVENT_LISTENER_METHODS(X) X(Constructor, "", "(JJ)V")
-METHOD_LOOKUP_DECLARATION(query_event_listener, QUERY_EVENT_LISTENER_METHODS)
-METHOD_LOOKUP_DEFINITION(
-    query_event_listener,
-    "com/google/firebase/firestore/internal/cpp/QueryEventListener",
-    QUERY_EVENT_LISTENER_METHODS)
-
-#define VOID_EVENT_LISTENER_METHODS(X) X(Constructor, "", "(J)V")
-METHOD_LOOKUP_DECLARATION(void_event_listener, VOID_EVENT_LISTENER_METHODS)
-METHOD_LOOKUP_DEFINITION(
-    void_event_listener,
-    "com/google/firebase/firestore/internal/cpp/VoidEventListener",
-    VOID_EVENT_LISTENER_METHODS)
+using jni::Constructor;
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+
+constexpr char kCppEventListenerClassName[] =
+    "com/google/firebase/firestore/internal/cpp/CppEventListener";
+Method kDiscardPointers("discardPointers", "()V");
+
+constexpr char kDocumentEventListenerClassName[] =
+    "com/google/firebase/firestore/internal/cpp/DocumentEventListener";
+Constructor kNewDocumentEventListener("(JJ)V");
+
+constexpr char kQueryEventListenerClassName[] =
+    "com/google/firebase/firestore/internal/cpp/QueryEventListener";
+Constructor kNewQueryEventListener("(JJ)V");
+
+constexpr char kVoidEventListenerClassName[] =
+    "com/google/firebase/firestore/internal/cpp/VoidEventListener";
+Constructor kNewVoidEventListener("(J)V");
+
+}  // namespace
 
 /* static */
+void EventListenerInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kCppEventListenerClassName, kDiscardPointers);
+
+  static const JNINativeMethod kDocumentEventListenerNatives[] = {
+      {"nativeOnEvent",
+       "(JJLjava/lang/Object;Lcom/google/firebase/firestore/"
+       "FirebaseFirestoreException;)V",
+       reinterpret_cast(
+           &EventListenerInternal::DocumentEventListenerNativeOnEvent)}};
+  loader.LoadClass(kDocumentEventListenerClassName, kNewDocumentEventListener);
+  loader.RegisterNatives(kDocumentEventListenerNatives,
+                         FIREBASE_ARRAYSIZE(kDocumentEventListenerNatives));
+
+  static const JNINativeMethod kQueryEventListenerNatives[] = {
+      {"nativeOnEvent",
+       "(JJLjava/lang/Object;Lcom/google/firebase/firestore/"
+       "FirebaseFirestoreException;)V",
+       reinterpret_cast(
+           &EventListenerInternal::QueryEventListenerNativeOnEvent)}};
+  loader.LoadClass(kQueryEventListenerClassName, kNewQueryEventListener);
+  loader.RegisterNatives(kQueryEventListenerNatives,
+                         FIREBASE_ARRAYSIZE(kQueryEventListenerNatives));
+
+  static const JNINativeMethod kVoidEventListenerNatives[] = {
+      {"nativeOnEvent", "(J)V",
+       reinterpret_cast(
+           &EventListenerInternal::VoidEventListenerNativeOnEvent)}};
+  loader.LoadClass(kVoidEventListenerClassName, kNewVoidEventListener);
+  loader.RegisterNatives(kVoidEventListenerNatives,
+                         FIREBASE_ARRAYSIZE(kVoidEventListenerNatives));
+}
+
 void EventListenerInternal::DocumentEventListenerNativeOnEvent(
-    JNIEnv* env, jclass clazz, jlong firestore_ptr, jlong listener_ptr,
-    jobject value, jobject error) {
+    JNIEnv* raw_env, jclass, jlong firestore_ptr, jlong listener_ptr,
+    jobject value, jobject raw_error) {
   if (firestore_ptr == 0 || listener_ptr == 0) {
     return;
   }
-  EventListener* listener =
+  auto* listener =
       reinterpret_cast*>(listener_ptr);
-  Error error_code =
-      FirebaseFirestoreExceptionInternal::ToErrorCode(env, error);
+  auto* firestore = reinterpret_cast(firestore_ptr);
+
+  Env env(raw_env);
+  Object error(raw_error);
+  Error error_code = ExceptionInternal::GetErrorCode(env, error);
+  std::string error_message = ExceptionInternal::ToString(env, error);
   if (error_code != Error::kErrorOk) {
-    listener->OnEvent(DocumentSnapshot{}, error_code);
+    listener->OnEvent({}, error_code, error_message);
     return;
   }
 
-  FirestoreInternal* firestore =
-      reinterpret_cast(firestore_ptr);
-  DocumentSnapshot snapshot(new DocumentSnapshotInternal{firestore, value});
-  listener->OnEvent(snapshot, error_code);
+  auto snapshot = firestore->NewDocumentSnapshot(env, Object(value));
+  listener->OnEvent(snapshot, error_code, error_message);
 }
 
 /* static */
 void EventListenerInternal::QueryEventListenerNativeOnEvent(
-    JNIEnv* env, jclass clazz, jlong firestore_ptr, jlong listener_ptr,
-    jobject value, jobject error) {
+    JNIEnv* raw_env, jclass, jlong firestore_ptr, jlong listener_ptr,
+    jobject value, jobject raw_error) {
   if (firestore_ptr == 0 || listener_ptr == 0) {
     return;
   }
-  EventListener* listener =
+  auto* listener =
       reinterpret_cast*>(listener_ptr);
-  Error error_code =
-      FirebaseFirestoreExceptionInternal::ToErrorCode(env, error);
+  auto* firestore = reinterpret_cast(firestore_ptr);
+
+  Env env(raw_env);
+  Object error(raw_error);
+  Error error_code = ExceptionInternal::GetErrorCode(env, error);
+  std::string error_message = ExceptionInternal::ToString(env, error);
   if (error_code != Error::kErrorOk) {
-    listener->OnEvent(QuerySnapshot{}, error_code);
+    listener->OnEvent({}, error_code, error_message);
     return;
   }
 
-  FirestoreInternal* firestore =
-      reinterpret_cast(firestore_ptr);
-  QuerySnapshot snapshot(new QuerySnapshotInternal{firestore, value});
-  listener->OnEvent(snapshot, error_code);
+  auto snapshot = firestore->NewQuerySnapshot(env, Object(value));
+  listener->OnEvent(snapshot, error_code, error_message);
 }
 
 /* static */
-void EventListenerInternal::VoidEventListenerNativeOnEvent(JNIEnv* env,
-                                                           jclass clazz,
+void EventListenerInternal::VoidEventListenerNativeOnEvent(JNIEnv*, jclass,
                                                            jlong listener_ptr) {
   if (listener_ptr == 0) {
     return;
   }
-  EventListener* listener =
-      reinterpret_cast*>(listener_ptr);
-
-  listener->OnEvent(Error::kErrorOk);
+  auto* listener = reinterpret_cast*>(listener_ptr);
+  listener->OnEvent(Error::kErrorOk, EmptyString());
 }
 
-/* static */
-jobject EventListenerInternal::EventListenerToJavaEventListener(
-    JNIEnv* env, FirestoreInternal* firestore,
+Local EventListenerInternal::Create(
+    Env& env, FirestoreInternal* firestore,
     EventListener* listener) {
-  jobject result = env->NewObject(document_event_listener::GetClass(),
-                                  document_event_listener::GetMethodId(
-                                      document_event_listener::kConstructor),
-                                  reinterpret_cast(firestore),
-                                  reinterpret_cast(listener));
-  CheckAndClearJniExceptions(env);
-  return result;
+  return env.New(kNewDocumentEventListener, reinterpret_cast(firestore),
+                 reinterpret_cast(listener));
 }
 
-/* static */
-jobject EventListenerInternal::EventListenerToJavaEventListener(
-    JNIEnv* env, FirestoreInternal* firestore,
+Local EventListenerInternal::Create(
+    Env& env, FirestoreInternal* firestore,
     EventListener* listener) {
-  jobject result = env->NewObject(
-      query_event_listener::GetClass(),
-      query_event_listener::GetMethodId(query_event_listener::kConstructor),
-      reinterpret_cast(firestore), reinterpret_cast(listener));
-  CheckAndClearJniExceptions(env);
-  return result;
+  return env.New(kNewQueryEventListener, reinterpret_cast(firestore),
+                 reinterpret_cast(listener));
 }
 
-/* static */
-jobject EventListenerInternal::EventListenerToJavaRunnable(
-    JNIEnv* env, EventListener* listener) {
-  jobject result = env->NewObject(
-      void_event_listener::GetClass(),
-      void_event_listener::GetMethodId(void_event_listener::kConstructor),
-      reinterpret_cast(listener));
-  CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-bool EventListenerInternal::InitializeEmbeddedClasses(
-    App* app, const std::vector* embedded_files) {
-  static const JNINativeMethod kDocumentEventListenerNatives[] = {
-      {"nativeOnEvent",
-       "(JJLjava/lang/Object;Lcom/google/firebase/firestore/"
-       "FirebaseFirestoreException;)V",
-       reinterpret_cast(
-           &EventListenerInternal::DocumentEventListenerNativeOnEvent)}};
-  static const JNINativeMethod kQueryEventListenerNatives[] = {
-      {"nativeOnEvent",
-       "(JJLjava/lang/Object;Lcom/google/firebase/firestore/"
-       "FirebaseFirestoreException;)V",
-       reinterpret_cast(
-           &EventListenerInternal::QueryEventListenerNativeOnEvent)}};
-  static const JNINativeMethod kVoidEventListenerNatives[] = {
-      {"nativeOnEvent", "(J)V",
-       reinterpret_cast(
-           &EventListenerInternal::VoidEventListenerNativeOnEvent)}};
-
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result =
-      // Cache classes
-      cpp_event_listener::CacheClassFromFiles(env, activity, embedded_files) &&
-      document_event_listener::CacheClassFromFiles(env, activity,
-                                                   embedded_files) &&
-      query_event_listener::CacheClassFromFiles(env, activity,
-                                                embedded_files) &&
-      void_event_listener::CacheClassFromFiles(env, activity, embedded_files) &&
-      // Cache method-ids
-      cpp_event_listener::CacheMethodIds(env, activity) &&
-      document_event_listener::CacheMethodIds(env, activity) &&
-      query_event_listener::CacheMethodIds(env, activity) &&
-      void_event_listener::CacheMethodIds(env, activity) &&
-      // Register natives
-      document_event_listener::RegisterNatives(
-          env, kDocumentEventListenerNatives,
-          FIREBASE_ARRAYSIZE(kDocumentEventListenerNatives)) &&
-      query_event_listener::RegisterNatives(
-          env, kQueryEventListenerNatives,
-          FIREBASE_ARRAYSIZE(kQueryEventListenerNatives)) &&
-      void_event_listener::RegisterNatives(
-          env, kVoidEventListenerNatives,
-          FIREBASE_ARRAYSIZE(kVoidEventListenerNatives));
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void EventListenerInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  // Release embedded classes.
-  cpp_event_listener::ReleaseClass(env);
-  document_event_listener::ReleaseClass(env);
-  query_event_listener::ReleaseClass(env);
-  void_event_listener::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+Local EventListenerInternal::Create(Env& env,
+                                            EventListener* listener) {
+  return env.New(kNewVoidEventListener, reinterpret_cast(listener));
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/event_listener_android.h b/firestore/src/android/event_listener_android.h
index 7907deba72..b0f1dcf7b0 100644
--- a/firestore/src/android/event_listener_android.h
+++ b/firestore/src/android/event_listener_android.h
@@ -1,18 +1,18 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_EVENT_LISTENER_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_EVENT_LISTENER_ANDROID_H_
 
-#include 
-
-#include "app/src/embedded_file.h"
 #include "firestore/src/android/firestore_android.h"
 #include "firestore/src/include/firebase/firestore/document_snapshot.h"
 #include "firestore/src/include/firebase/firestore/event_listener.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
 
 class EventListenerInternal {
  public:
+  static void Initialize(jni::Loader& loader);
+
   static void DocumentEventListenerNativeOnEvent(JNIEnv* env, jclass clazz,
                                                  jlong firestore_ptr,
                                                  jlong listener_ptr,
@@ -24,23 +24,27 @@ class EventListenerInternal {
   static void VoidEventListenerNativeOnEvent(JNIEnv* env, jclass clazz,
                                              jlong listener_ptr);
 
+  static jni::Local Create(
+      jni::Env& env, FirestoreInternal* firestore,
+      EventListener* listener);
+
   static jobject EventListenerToJavaEventListener(
       JNIEnv* env, FirestoreInternal* firestore,
       EventListener* listener);
 
+  static jni::Local Create(jni::Env& env,
+                                        FirestoreInternal* firestore,
+                                        EventListener* listener);
+
   static jobject EventListenerToJavaEventListener(
       JNIEnv* env, FirestoreInternal* firestore,
       EventListener* listener);
 
+  static jni::Local Create(jni::Env& env,
+                                        EventListener* listener);
+
   static jobject EventListenerToJavaRunnable(JNIEnv* env,
                                              EventListener* listener);
-
- private:
-  friend class FirestoreInternal;
-
-  static bool InitializeEmbeddedClasses(
-      App* app, const std::vector* embedded_files);
-  static void Terminate(App* app);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/exception_android.cc b/firestore/src/android/exception_android.cc
new file mode 100644
index 0000000000..afe5e7a85e
--- /dev/null
+++ b/firestore/src/android/exception_android.cc
@@ -0,0 +1,137 @@
+#include "firestore/src/android/exception_android.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+#include "firestore/src/jni/throwable.h"
+
+namespace firebase {
+namespace firestore {
+namespace {
+
+using jni::Constructor;
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+using jni::StaticMethod;
+using jni::String;
+using jni::Throwable;
+
+// FirebaseFirestoreException
+constexpr char kFirestoreExceptionClassName[] = PROGUARD_KEEP_CLASS
+    "com/google/firebase/firestore/FirebaseFirestoreException";
+
+Constructor kNewFirestoreException(
+    "(Ljava/lang/String;"
+    "Lcom/google/firebase/firestore/FirebaseFirestoreException$Code;)V");
+Method kGetCode(
+    "getCode",
+    "()Lcom/google/firebase/firestore/FirebaseFirestoreException$Code;");
+
+jclass g_firestore_exception_class = nullptr;
+
+// FirebaseFirestoreException$Code
+constexpr char kCodeClassName[] = PROGUARD_KEEP_CLASS
+    "com/google/firebase/firestore/FirebaseFirestoreException$Code";
+
+Method kValue("value", "()I");
+StaticMethod kFromValue(
+    "fromValue",
+    "(I)Lcom/google/firebase/firestore/FirebaseFirestoreException$Code;");
+
+// IllegalStateException
+constexpr char kIllegalStateExceptionClassName[] =
+    PROGUARD_KEEP_CLASS "java/lang/IllegalStateException";
+Constructor kNewIllegalStateException("()V");
+
+jclass g_illegal_state_exception_class = nullptr;
+
+}  // namespace
+
+/* static */
+void ExceptionInternal::Initialize(jni::Loader& loader) {
+  g_firestore_exception_class = loader.LoadClass(
+      kFirestoreExceptionClassName, kNewFirestoreException, kGetCode);
+
+  loader.LoadClass(kCodeClassName, kValue, kFromValue);
+
+  g_illegal_state_exception_class = loader.LoadClass(
+      kIllegalStateExceptionClassName, kNewIllegalStateException);
+}
+
+Error ExceptionInternal::GetErrorCode(Env& env, const Object& exception) {
+  if (!exception) {
+    return Error::kErrorOk;
+  }
+
+  if (IsIllegalStateException(env, exception)) {
+    // Some of the Precondition failure is thrown as IllegalStateException
+    // instead of a FirebaseFirestoreException. Convert those into a more
+    // meaningful code.
+    return Error::kErrorFailedPrecondition;
+  } else if (!IsFirestoreException(env, exception)) {
+    return Error::kErrorUnknown;
+  }
+
+  Local java_code = env.Call(exception, kGetCode);
+  int32_t code = env.Call(java_code, kValue);
+
+  if (code > Error::kErrorUnauthenticated || code < Error::kErrorOk) {
+    return Error::kErrorUnknown;
+  }
+  return static_cast(code);
+}
+
+std::string ExceptionInternal::ToString(Env& env, const Object& exception) {
+  return util::GetMessageFromException(env.get(), exception.get());
+}
+
+Local ExceptionInternal::Create(Env& env, Error code,
+                                           const std::string& message) {
+  if (code == Error::kErrorOk) {
+    return {};
+  }
+
+  Local java_message;
+  if (message.empty()) {
+    // FirebaseFirestoreException requires message to be non-empty. If the
+    // caller does not bother to give details, we assign an arbitrary message
+    // here.
+    java_message = env.NewStringUtf("Unknown Exception");
+  } else {
+    java_message = env.NewStringUtf(message);
+  }
+
+  Local java_code = env.Call(kFromValue, static_cast(code));
+  return env.New(kNewFirestoreException, java_message, java_code);
+}
+
+Local ExceptionInternal::Wrap(Env& env,
+                                         Local&& exception) {
+  if (IsFirestoreException(env, exception)) {
+    return Move(exception);
+  } else {
+    return Create(env, GetErrorCode(env, exception),
+                  ToString(env, exception).c_str());
+  }
+}
+
+bool ExceptionInternal::IsFirestoreException(Env& env,
+                                             const Object& exception) {
+  return env.IsInstanceOf(exception, g_firestore_exception_class);
+}
+
+bool ExceptionInternal::IsIllegalStateException(Env& env,
+                                                const Object& exception) {
+  return env.IsInstanceOf(exception, g_illegal_state_exception_class);
+}
+
+bool ExceptionInternal::IsAnyExceptionThrownByFirestore(
+    Env& env, const Object& exception) {
+  return IsFirestoreException(env, exception) ||
+         IsIllegalStateException(env, exception);
+}
+
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/android/exception_android.h b/firestore/src/android/exception_android.h
new file mode 100644
index 0000000000..738e234e4d
--- /dev/null
+++ b/firestore/src/android/exception_android.h
@@ -0,0 +1,43 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_EXCEPTION_ANDROID_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_EXCEPTION_ANDROID_H_
+
+#include 
+
+#include "app/src/include/firebase/app.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firebase/firestore/firestore_errors.h"
+
+namespace firebase {
+namespace firestore {
+
+class ExceptionInternal {
+ public:
+  static void Initialize(jni::Loader& loader);
+
+  static Error GetErrorCode(jni::Env& env, const jni::Object& exception);
+  static std::string ToString(jni::Env& env, const jni::Object& exception);
+
+  static jni::Local Create(jni::Env& env, Error code,
+                                           const std::string& message);
+  static jni::Local Wrap(
+      jni::Env& env, jni::Local&& exception);
+
+  /** Returns true if the given object is a FirestoreException. */
+  static bool IsFirestoreException(jni::Env& env, const jni::Object& exception);
+
+  /** Returns true if the given object is an IllegalStateException. */
+  static bool IsIllegalStateException(jni::Env& env,
+                                      const jni::Object& exception);
+
+  /**
+   * Returns true if the given object is a FirestoreException or any other type
+   * of exception thrown by a Firestore API.
+   */
+  static bool IsAnyExceptionThrownByFirestore(jni::Env& env,
+                                              const jni::Object& exception);
+};
+
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_EXCEPTION_ANDROID_H_
diff --git a/firestore/src/android/field_path_android.cc b/firestore/src/android/field_path_android.cc
index dd35310139..2db98584ca 100644
--- a/firestore/src/android/field_path_android.cc
+++ b/firestore/src/android/field_path_android.cc
@@ -2,6 +2,9 @@
 
 #include "app/src/util_android.h"
 #include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/array.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 #if defined(__ANDROID__)
 #include "firestore/src/android/field_path_portable.h"
@@ -11,70 +14,44 @@
 
 namespace firebase {
 namespace firestore {
+namespace {
+
+using jni::Array;
+using jni::Env;
+using jni::Local;
+using jni::Object;
+using jni::StaticMethod;
+using jni::String;
+
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/FieldPath";
+StaticMethod kOf(
+    "of", "([Ljava/lang/String;)Lcom/google/firebase/firestore/FieldPath;");
+StaticMethod kDocumentId("documentId",
+                                 "()Lcom/google/firebase/firestore/FieldPath;");
+
+}  // namespace
+
+void FieldPathConverter::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kOf, kDocumentId);
+}
 
-// clang-format off
-#define FIELD_PATH_METHODS(X)                                                  \
-  X(Of, "of",                                                                  \
-    "([Ljava/lang/String;)Lcom/google/firebase/firestore/FieldPath;",          \
-    firebase::util::kMethodTypeStatic),                                        \
-  X(DocumentId, "documentId",                                                  \
-    "()Lcom/google/firebase/firestore/FieldPath;",                             \
-    firebase::util::kMethodTypeStatic)
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(field_path, FIELD_PATH_METHODS)
-METHOD_LOOKUP_DEFINITION(field_path,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/FieldPath",
-                         FIELD_PATH_METHODS)
+Local FieldPathConverter::Create(Env& env, const FieldPath& path) {
+  FieldPath::FieldPathInternal& internal = *path.internal_;
 
-/* static */
-jobject FieldPathConverter::ToJavaObject(JNIEnv* env, const FieldPath& path) {
-  FieldPath::FieldPathInternal* internal = path.internal_;
   // If the path is key (i.e. __name__).
-  if (internal->IsKeyFieldPath()) {
-    jobject result = env->CallStaticObjectMethod(
-        field_path::GetClass(),
-        field_path::GetMethodId(field_path::kDocumentId));
-    CheckAndClearJniExceptions(env);
-    return result;
+  if (internal.IsKeyFieldPath()) {
+    return env.Call(kDocumentId);
   }
 
   // Prepare call arguments.
-  jsize size = static_cast(internal->size());
-  jobjectArray args =
-      env->NewObjectArray(size, firebase::util::string::GetClass(),
-                          /*initialElement=*/nullptr);
-  for (jsize i = 0; i < size; ++i) {
-    jobject segment = env->NewStringUTF((*internal)[i].c_str());
-    env->SetObjectArrayElement(args, i, segment);
-    env->DeleteLocalRef(segment);
-    CheckAndClearJniExceptions(env);
+  size_t size = internal.size();
+  Local> args = env.NewArray(size, String::GetClass());
+  for (size_t i = 0; i < size; ++i) {
+    args.Set(env, i, env.NewStringUtf(internal[i]));
   }
 
-  // Make JNI call and check for error.
-  jobject result = env->CallStaticObjectMethod(
-      field_path::GetClass(), field_path::GetMethodId(field_path::kOf), args);
-  CheckAndClearJniExceptions(env);
-  env->DeleteLocalRef(args);
-
-  return result;
-}
-
-/* static */
-bool FieldPathConverter::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = field_path::CacheMethodIds(env, activity);
-  firebase::util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void FieldPathConverter::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  field_path::ReleaseClass(env);
-  firebase::util::CheckAndClearJniExceptions(env);
+  return env.Call(kOf, args);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/field_path_android.h b/firestore/src/android/field_path_android.h
index 9cb718f052..fa08f322c3 100644
--- a/firestore/src/android/field_path_android.h
+++ b/firestore/src/android/field_path_android.h
@@ -1,10 +1,8 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_FIELD_PATH_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_FIELD_PATH_ANDROID_H_
 
-#include 
-
-#include "app/src/include/firebase/app.h"
 #include "firestore/src/include/firebase/firestore/field_path.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
@@ -16,17 +14,13 @@ class FieldPathConverter {
  public:
   using ApiType = FieldPath;
 
-  // Convert a C++ FieldPath to a Java FieldPath.
-  static jobject ToJavaObject(JNIEnv* env, const FieldPath& path);
+  static void Initialize(jni::Loader& loader);
+
+  /** Creates a Java FieldPath from  a C++ FieldPath. */
+  static jni::Local Create(jni::Env& env, const FieldPath& path);
 
   // We do not need to convert Java FieldPath back to C++ FieldPath since there
   // is no public API that returns a FieldPath yet.
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/field_value_android.cc b/firestore/src/android/field_value_android.cc
index 52ada216c1..218bdd7216 100644
--- a/firestore/src/android/field_value_android.cc
+++ b/firestore/src/android/field_value_android.cc
@@ -9,12 +9,22 @@
 #include "firestore/src/android/geo_point_android.h"
 #include "firestore/src/android/timestamp_android.h"
 #include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/class.h"
+#include "firestore/src/jni/env.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
+
+using jni::Array;
+using jni::Env;
+using jni::Local;
+using jni::Object;
 
 using Type = FieldValue::Type;
 
+}  // namespace
+
 // com.google.firebase.firestore.FieldValue is the public type which contains
 // static methods to build sentinel values.
 // clang-format off
@@ -89,10 +99,9 @@ FieldValueInternal::FieldValueInternal(double value)
 
 FieldValueInternal::FieldValueInternal(Timestamp value)
     : cached_type_(Type::kTimestamp) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject obj = TimestampInternal::TimestampToJavaTimestamp(env, value);
-  obj_ = env->NewGlobalRef(obj);
-  env->DeleteLocalRef(obj);
+  Env env;
+  Local obj = TimestampInternal::Create(env, value);
+  obj_ = env.get()->NewGlobalRef(obj.get());
 }
 
 FieldValueInternal::FieldValueInternal(std::string value)
@@ -111,11 +120,9 @@ FieldValueInternal::FieldValueInternal(std::string value)
 // blob_value().
 FieldValueInternal::FieldValueInternal(const uint8_t* value, size_t size)
     : cached_type_(Type::kBlob) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject obj = BlobInternal::BlobToJavaBlob(env, value, size);
-  obj_ = env->NewGlobalRef(obj);
-  env->DeleteLocalRef(obj);
-  CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  Local obj = BlobInternal::Create(env, value, size);
+  obj_ = env.get()->NewGlobalRef(obj.get());
   FIREBASE_ASSERT(obj_ != nullptr);
 }
 
@@ -124,10 +131,9 @@ FieldValueInternal::FieldValueInternal(DocumentReference value)
 
 FieldValueInternal::FieldValueInternal(GeoPoint value)
     : cached_type_(Type::kGeoPoint) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject obj = GeoPointInternal::GeoPointToJavaGeoPoint(env, value);
-  obj_ = env->NewGlobalRef(obj);
-  env->DeleteLocalRef(obj);
+  Env env = GetEnv();
+  Local obj = GeoPointInternal::Create(env, value);
+  obj_ = env.get()->NewGlobalRef(obj.get());
 }
 
 FieldValueInternal::FieldValueInternal(std::vector value)
@@ -186,7 +192,7 @@ Type FieldValueInternal::type() const {
     cached_type_ = Type::kDouble;
     return Type::kDouble;
   }
-  if (env->IsInstanceOf(obj_, TimestampInternal::GetClass())) {
+  if (env->IsInstanceOf(obj_, TimestampInternal::GetClass().get())) {
     cached_type_ = Type::kTimestamp;
     return Type::kTimestamp;
   }
@@ -194,15 +200,15 @@ Type FieldValueInternal::type() const {
     cached_type_ = Type::kString;
     return Type::kString;
   }
-  if (env->IsInstanceOf(obj_, BlobInternal::GetClass())) {
+  if (env->IsInstanceOf(obj_, BlobInternal::GetClass().get())) {
     cached_type_ = Type::kBlob;
     return Type::kBlob;
   }
-  if (env->IsInstanceOf(obj_, DocumentReferenceInternal::GetClass())) {
+  if (env->IsInstanceOf(obj_, DocumentReferenceInternal::GetClass().get())) {
     cached_type_ = Type::kReference;
     return Type::kReference;
   }
-  if (env->IsInstanceOf(obj_, GeoPointInternal::GetClass())) {
+  if (env->IsInstanceOf(obj_, GeoPointInternal::GetClass().get())) {
     cached_type_ = Type::kGeoPoint;
     return Type::kGeoPoint;
   }
@@ -263,17 +269,17 @@ double FieldValueInternal::double_value() const {
 }
 
 Timestamp FieldValueInternal::timestamp_value() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
+  Env env;
 
   // Make sure this instance is of correct type.
   if (cached_type_ == Type::kNull) {
-    FIREBASE_ASSERT(env->IsInstanceOf(obj_, TimestampInternal::GetClass()));
+    FIREBASE_ASSERT(env.IsInstanceOf(obj_, TimestampInternal::GetClass()));
     cached_type_ = Type::kTimestamp;
   } else {
     FIREBASE_ASSERT(cached_type_ == Type::kTimestamp);
   }
 
-  return TimestampInternal::JavaTimestampToTimestamp(env, obj_);
+  return TimestampInternal(obj_).ToPublic(env);
 }
 
 std::string FieldValueInternal::string_value() const {
@@ -291,55 +297,57 @@ std::string FieldValueInternal::string_value() const {
 }
 
 const uint8_t* FieldValueInternal::blob_value() const {
-  if (blob_size() == 0) {
-    // Doesn't matter what we return. Not return &(cached_blob_.get()->front())
-    // to avoid going into undefined-behavior world. Once we drop the support of
-    // STLPort, we might just combine this case into the logic below by calling
-    // cached_blob_.get()->data().
+  static_assert(sizeof(uint8_t) == sizeof(jbyte),
+                "uint8_t and jbyte must be of same size");
+
+  Env env = GetEnv();
+  EnsureCachedBlob(env);
+  if (!env.ok() || cached_blob_.get() == nullptr) {
     return nullptr;
   }
 
-  if (cached_blob_.get()) {
-    return &(cached_blob_.get()->front());
+  if (cached_blob_->empty()) {
+    // The return value doesn't matter, but we can't return
+    // `&cached_blob->front()` because calling `front` on an empty vector is
+    // undefined behavior. When we drop support for STLPort, we can use `data`
+    // instead which is defined, even for empty vectors.
+    // TODO(b/163140650): remove this special case.
+    return nullptr;
   }
 
-  size_t size = blob_size();
-  // firebase::SharedPtr does not have set() API.
-  cached_blob_ =
-      SharedPtr>{new std::vector(size)};
-
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jbyteArray bytes = BlobInternal::JavaBlobToJbyteArray(env, obj_);
-  static_assert(sizeof(uint8_t) == sizeof(jbyte),
-                "uint8_t and jbyte must be of same size");
-  env->GetByteArrayRegion(
-      bytes, 0, size, reinterpret_cast(&(cached_blob_.get()->front())));
-  env->DeleteLocalRef(bytes);
-
-  CheckAndClearJniExceptions(env);
-  return &(cached_blob_.get()->front());
+  return &(cached_blob_->front());
 }
 
 size_t FieldValueInternal::blob_size() const {
-  if (cached_blob_.get()) {
-    return cached_blob_.get()->size();
+  Env env = GetEnv();
+  EnsureCachedBlob(env);
+  if (!env.ok() || cached_blob_.get() == nullptr) {
+    return 0;
   }
 
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
+  return cached_blob_->size();
+}
 
-  // Make sure this instance is of correct type.
+void FieldValueInternal::EnsureCachedBlob(Env& env) const {
   if (cached_type_ == Type::kNull) {
-    FIREBASE_ASSERT(env->IsInstanceOf(obj_, BlobInternal::GetClass()));
+    FIREBASE_ASSERT(env.IsInstanceOf(Object(obj_), BlobInternal::GetClass()));
     cached_type_ = Type::kBlob;
   } else {
     FIREBASE_ASSERT(cached_type_ == Type::kBlob);
   }
+  if (cached_blob_.get() != nullptr) {
+    return;
+  }
 
-  jbyteArray bytes = BlobInternal::JavaBlobToJbyteArray(env, obj_);
-  jsize result = env->GetArrayLength(bytes);
-  env->DeleteLocalRef(bytes);
-  CheckAndClearJniExceptions(env);
-  return static_cast(result);
+  Local> bytes = BlobInternal(obj_).ToBytes(env);
+  size_t size = bytes.Size(env);
+
+  auto result = MakeShared>(size);
+  env.GetArrayRegion(bytes, 0, size, &(result->front()));
+
+  if (env.ok()) {
+    cached_blob_ = Move(result);
+  }
 }
 
 DocumentReference FieldValueInternal::reference_value() const {
@@ -348,7 +356,7 @@ DocumentReference FieldValueInternal::reference_value() const {
   // Make sure this instance is of correct type.
   if (cached_type_ == Type::kNull) {
     FIREBASE_ASSERT(
-        env->IsInstanceOf(obj_, DocumentReferenceInternal::GetClass()));
+        env->IsInstanceOf(obj_, DocumentReferenceInternal::GetClass().get()));
     cached_type_ = Type::kReference;
   } else {
     FIREBASE_ASSERT(cached_type_ == Type::kReference);
@@ -362,17 +370,17 @@ DocumentReference FieldValueInternal::reference_value() const {
 }
 
 GeoPoint FieldValueInternal::geo_point_value() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
+  Env env = GetEnv();
 
   // Make sure this instance is of correct type.
   if (cached_type_ == Type::kNull) {
-    FIREBASE_ASSERT(env->IsInstanceOf(obj_, GeoPointInternal::GetClass()));
+    FIREBASE_ASSERT(env.IsInstanceOf(obj_, GeoPointInternal::GetClass()));
     cached_type_ = Type::kGeoPoint;
   } else {
     FIREBASE_ASSERT(cached_type_ == Type::kGeoPoint);
   }
 
-  return GeoPointInternal::JavaGeoPointToGeoPoint(env, obj_);
+  return GeoPointInternal(obj_).ToPublic(env);
 }
 
 std::vector FieldValueInternal::array_value() const {
@@ -605,17 +613,8 @@ void FieldValueInternal::Terminate(App* app) {
 }
 
 bool operator==(const FieldValueInternal& lhs, const FieldValueInternal& rhs) {
-  // Most likely only happens when comparing one with itself or both are Null.
-  if (lhs.obj_ == rhs.obj_) {
-    return true;
-  }
-
-  // If only one of them is Null, then they cannot equal.
-  if (lhs.obj_ == nullptr || rhs.obj_ == nullptr) {
-    return false;
-  }
-
-  return lhs.EqualsJavaObject(rhs);
+  Env env = FirestoreInternal::GetEnv();
+  return Object::Equals(env, lhs.ToJava(), rhs.ToJava());
 }
 
 jobject FieldValueInternal::TryGetJobject(const FieldValue& value) {
diff --git a/firestore/src/android/field_value_android.h b/firestore/src/android/field_value_android.h
index c307cc5c17..07f18a37c3 100644
--- a/firestore/src/android/field_value_android.h
+++ b/firestore/src/android/field_value_android.h
@@ -9,6 +9,7 @@
 #include "firestore/src/android/wrapper.h"
 #include "firestore/src/include/firebase/firestore/document_reference.h"
 #include "firestore/src/include/firebase/firestore/field_value.h"
+#include "firestore/src/jni/jni_fwd.h"
 #include "firebase/firestore/geo_point.h"
 #include "firebase/firestore/timestamp.h"
 
@@ -68,6 +69,8 @@ class FieldValueInternal : public Wrapper {
   static bool Initialize(App* app);
   static void Terminate(App* app);
 
+  void EnsureCachedBlob(jni::Env& env) const;
+
   static jobject TryGetJobject(const FieldValue& value);
 
   static jobject delete_;
@@ -81,6 +84,14 @@ class FieldValueInternal : public Wrapper {
 
 bool operator==(const FieldValueInternal& lhs, const FieldValueInternal& rhs);
 
+inline jobject ToJni(const FieldValueInternal* value) {
+  return value->java_object();
+}
+
+inline jobject ToJni(const FieldValueInternal& value) {
+  return value.java_object();
+}
+
 }  // namespace firestore
 }  // namespace firebase
 
diff --git a/firestore/src/android/firebase_firestore_exception_android.cc b/firestore/src/android/firebase_firestore_exception_android.cc
deleted file mode 100644
index 6424999cc0..0000000000
--- a/firestore/src/android/firebase_firestore_exception_android.cc
+++ /dev/null
@@ -1,161 +0,0 @@
-#include "firestore/src/android/firebase_firestore_exception_android.h"
-
-#include 
-
-#include "firestore/src/android/util_android.h"
-
-namespace firebase {
-namespace firestore {
-
-// clang-format off
-#define FIRESTORE_EXCEPTION_METHODS(X)                                      \
-  X(Constructor, "",                                                  \
-    "(Ljava/lang/String;"                                                   \
-    "Lcom/google/firebase/firestore/FirebaseFirestoreException$Code;)V"),   \
-  X(GetCode, "getCode",                                                     \
-    "()Lcom/google/firebase/firestore/FirebaseFirestoreException$Code;")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(firestore_exception, FIRESTORE_EXCEPTION_METHODS)
-METHOD_LOOKUP_DEFINITION(
-    firestore_exception,
-    PROGUARD_KEEP_CLASS
-    "com/google/firebase/firestore/FirebaseFirestoreException",
-    FIRESTORE_EXCEPTION_METHODS)
-
-// clang-format off
-#define FIRESTORE_EXCEPTION_CODE_METHODS(X)                                \
-  X(Value, "value", "()I"),                                                \
-  X(FromValue, "fromValue",                                                \
-    "(I)Lcom/google/firebase/firestore/FirebaseFirestoreException$Code;",  \
-    util::kMethodTypeStatic)
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(firestore_exception_code,
-                          FIRESTORE_EXCEPTION_CODE_METHODS)
-METHOD_LOOKUP_DEFINITION(
-    firestore_exception_code,
-    PROGUARD_KEEP_CLASS
-    "com/google/firebase/firestore/FirebaseFirestoreException$Code",
-    FIRESTORE_EXCEPTION_CODE_METHODS)
-
-#define ILLEGAL_STATE_EXCEPTION_METHODS(X) X(Constructor, "", "()V")
-
-METHOD_LOOKUP_DECLARATION(illegal_state_exception,
-                          ILLEGAL_STATE_EXCEPTION_METHODS)
-METHOD_LOOKUP_DEFINITION(illegal_state_exception,
-                         PROGUARD_KEEP_CLASS "java/lang/IllegalStateException",
-                         ILLEGAL_STATE_EXCEPTION_METHODS)
-
-/* static */
-Error FirebaseFirestoreExceptionInternal::ToErrorCode(JNIEnv* env,
-                                                      jobject exception) {
-  if (exception == nullptr) {
-    return Error::kErrorOk;
-  }
-
-  // Some of the Precondition failure is thrown as IllegalStateException instead
-  // of a FirebaseFirestoreException. So we convert them into a more meaningful
-  // code.
-  if (env->IsInstanceOf(exception, illegal_state_exception::GetClass())) {
-    return Error::kErrorFailedPrecondition;
-  } else if (!IsInstance(env, exception)) {
-    return Error::kErrorUnknown;
-  }
-
-  jobject code = env->CallObjectMethod(
-      exception,
-      firestore_exception::GetMethodId(firestore_exception::kGetCode));
-  jint code_value = env->CallIntMethod(
-      code,
-      firestore_exception_code::GetMethodId(firestore_exception_code::kValue));
-  env->DeleteLocalRef(code);
-  CheckAndClearJniExceptions(env);
-
-  if (code_value > Error::kErrorUnauthenticated ||
-      code_value < Error::kErrorOk) {
-    return Error::kErrorUnknown;
-  }
-  return static_cast(code_value);
-}
-
-/* static */
-std::string FirebaseFirestoreExceptionInternal::ToString(JNIEnv* env,
-                                                         jobject exception) {
-  return util::GetMessageFromException(env, exception);
-}
-
-/* static */
-jthrowable FirebaseFirestoreExceptionInternal::ToException(
-    JNIEnv* env, Error code, const char* message) {
-  if (code == Error::kErrorOk) {
-    return nullptr;
-  }
-  // FirebaseFirestoreException requires message to be non-empty. If the caller
-  // does not bother to give details, we assign an arbitrary message here.
-  if (message == nullptr || strlen(message) == 0) {
-    message = "Unknown Exception";
-  }
-
-  jstring exception_message = env->NewStringUTF(message);
-  jobject exception_code =
-      env->CallStaticObjectMethod(firestore_exception_code::GetClass(),
-                                  firestore_exception_code::GetMethodId(
-                                      firestore_exception_code::kFromValue),
-                                  static_cast(code));
-  jthrowable result = static_cast(env->NewObject(
-      firestore_exception::GetClass(),
-      firestore_exception::GetMethodId(firestore_exception::kConstructor),
-      exception_message, exception_code));
-  env->DeleteLocalRef(exception_message);
-  env->DeleteLocalRef(exception_code);
-  CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-jthrowable FirebaseFirestoreExceptionInternal::ToException(
-    JNIEnv* env, jthrowable exception) {
-  if (IsInstance(env, exception)) {
-    return static_cast(env->NewLocalRef(exception));
-  } else {
-    return ToException(env, ToErrorCode(env, exception),
-                       ToString(env, exception).c_str());
-  }
-}
-
-/* static */
-bool FirebaseFirestoreExceptionInternal::IsInstance(JNIEnv* env,
-                                                    jobject exception) {
-  return env->IsInstanceOf(exception, firestore_exception::GetClass());
-}
-
-/* static */
-bool FirebaseFirestoreExceptionInternal::IsFirestoreException(
-    JNIEnv* env, jobject exception) {
-  return IsInstance(env, exception) ||
-         env->IsInstanceOf(exception, illegal_state_exception::GetClass());
-}
-
-/* static */
-bool FirebaseFirestoreExceptionInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = firestore_exception::CacheMethodIds(env, activity) &&
-                firestore_exception_code::CacheMethodIds(env, activity) &&
-                illegal_state_exception::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void FirebaseFirestoreExceptionInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  firestore_exception::ReleaseClass(env);
-  firestore_exception_code::ReleaseClass(env);
-  illegal_state_exception::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-}
-
-}  // namespace firestore
-}  // namespace firebase
diff --git a/firestore/src/android/firebase_firestore_exception_android.h b/firestore/src/android/firebase_firestore_exception_android.h
deleted file mode 100644
index 04c61307b1..0000000000
--- a/firestore/src/android/firebase_firestore_exception_android.h
+++ /dev/null
@@ -1,32 +0,0 @@
-#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_FIREBASE_FIRESTORE_EXCEPTION_ANDROID_H_
-#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_FIREBASE_FIRESTORE_EXCEPTION_ANDROID_H_
-
-#include 
-
-#include "app/src/include/firebase/app.h"
-#include "app/src/util_android.h"
-#include "firebase/firestore/firestore_errors.h"
-
-namespace firebase {
-namespace firestore {
-
-class FirebaseFirestoreExceptionInternal {
- public:
-  static Error ToErrorCode(JNIEnv* env, jobject exception);
-  static std::string ToString(JNIEnv* env, jobject exception);
-  static jthrowable ToException(JNIEnv* env, Error code, const char* message);
-  static jthrowable ToException(JNIEnv* env, jthrowable exception);
-  static bool IsInstance(JNIEnv* env, jobject exception);
-  static bool IsFirestoreException(JNIEnv* env, jobject exception);
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
-};
-
-}  // namespace firestore
-}  // namespace firebase
-
-#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_FIREBASE_FIRESTORE_EXCEPTION_ANDROID_H_
diff --git a/firestore/src/android/firebase_firestore_settings_android.cc b/firestore/src/android/firebase_firestore_settings_android.cc
deleted file mode 100644
index 05c1f4a8e7..0000000000
--- a/firestore/src/android/firebase_firestore_settings_android.cc
+++ /dev/null
@@ -1,135 +0,0 @@
-#include "firestore/src/android/firebase_firestore_settings_android.h"
-
-#include "firestore/src/android/util_android.h"
-
-namespace firebase {
-namespace firestore {
-
-// clang-format off
-#define SETTINGS_BUILDER_METHODS(X)                                            \
-  X(Constructor, "", "()V", util::kMethodTypeInstance),                  \
-  X(SetHost, "setHost", "(Ljava/lang/String;)"                                 \
-    "Lcom/google/firebase/firestore/FirebaseFirestoreSettings$Builder;"),      \
-  X(SetSslEnabled, "setSslEnabled", "(Z)"                                      \
-    "Lcom/google/firebase/firestore/FirebaseFirestoreSettings$Builder;"),      \
-  X(SetPersistenceEnabled, "setPersistenceEnabled", "(Z)"                      \
-    "Lcom/google/firebase/firestore/FirebaseFirestoreSettings$Builder;"),      \
-  X(SetTimestampsInSnapshotsEnabled, "setTimestampsInSnapshotsEnabled", "(Z)"  \
-    "Lcom/google/firebase/firestore/FirebaseFirestoreSettings$Builder;"),      \
-  X(Build, "build",                                              \
-    "()Lcom/google/firebase/firestore/FirebaseFirestoreSettings;")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(settings_builder, SETTINGS_BUILDER_METHODS)
-METHOD_LOOKUP_DEFINITION(
-    settings_builder,
-    PROGUARD_KEEP_CLASS
-    "com/google/firebase/firestore/FirebaseFirestoreSettings$Builder",
-    SETTINGS_BUILDER_METHODS)
-
-// clang-format off
-#define SETTINGS_METHODS(X)                                \
-  X(GetHost, "getHost", "()Ljava/lang/String;"),           \
-  X(IsSslEnabled, "isSslEnabled", "()Z"),                  \
-  X(IsPersistenceEnabled, "isPersistenceEnabled", "()Z")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(settings, SETTINGS_METHODS)
-METHOD_LOOKUP_DEFINITION(
-    settings,
-    PROGUARD_KEEP_CLASS
-    "com/google/firebase/firestore/FirebaseFirestoreSettings",
-    SETTINGS_METHODS)
-
-/* static */
-jobject FirebaseFirestoreSettingsInternal::SettingToJavaSetting(
-    JNIEnv* env, const Settings& settings) {
-  jobject builder = env->NewObject(
-      settings_builder::GetClass(),
-      settings_builder::GetMethodId(settings_builder::kConstructor));
-
-  // Always set Timestamps-in-Snapshots enabled to true.
-  jobject builder_timestamp = env->CallObjectMethod(
-      builder,
-      settings_builder::GetMethodId(
-          settings_builder::kSetTimestampsInSnapshotsEnabled),
-      static_cast(true));
-  env->DeleteLocalRef(builder);
-  builder = builder_timestamp;
-
-  // Set host
-  jstring host = env->NewStringUTF(settings.host().c_str());
-  jobject builder_host = env->CallObjectMethod(
-      builder, settings_builder::GetMethodId(settings_builder::kSetHost), host);
-  env->DeleteLocalRef(builder);
-  env->DeleteLocalRef(host);
-  builder = builder_host;
-
-  // Set SSL enabled
-  jobject builder_ssl = env->CallObjectMethod(
-      builder, settings_builder::GetMethodId(settings_builder::kSetSslEnabled),
-      static_cast(settings.is_ssl_enabled()));
-  env->DeleteLocalRef(builder);
-  builder = builder_ssl;
-
-  // Set Persistence enabled
-  jobject builder_persistence = env->CallObjectMethod(
-      builder,
-      settings_builder::GetMethodId(settings_builder::kSetPersistenceEnabled),
-      static_cast(settings.is_persistence_enabled()));
-  env->DeleteLocalRef(builder);
-  builder = builder_persistence;
-
-  // Build
-  jobject settings_jobj = env->CallObjectMethod(
-      builder, settings_builder::GetMethodId(settings_builder::kBuild));
-  env->DeleteLocalRef(builder);
-  CheckAndClearJniExceptions(env);
-  return settings_jobj;
-}
-
-/* static */
-Settings FirebaseFirestoreSettingsInternal::JavaSettingToSetting(JNIEnv* env,
-                                                                 jobject obj) {
-  Settings result;
-
-  // Set host
-  jstring host = static_cast(
-      env->CallObjectMethod(obj, settings::GetMethodId(settings::kGetHost)));
-  result.set_host(util::JStringToString(env, host));
-  env->DeleteLocalRef(host);
-
-  // Set SSL enabled
-  jboolean ssl_enabled = env->CallBooleanMethod(
-      obj, settings::GetMethodId(settings::kIsSslEnabled));
-  result.set_ssl_enabled(ssl_enabled);
-
-  // Set Persistence enabled
-  jboolean persistence_enabled = env->CallBooleanMethod(
-      obj, settings::GetMethodId(settings::kIsPersistenceEnabled));
-  result.set_persistence_enabled(persistence_enabled);
-
-  CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-bool FirebaseFirestoreSettingsInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = settings_builder::CacheMethodIds(env, activity) &&
-                settings::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void FirebaseFirestoreSettingsInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  settings_builder::ReleaseClass(env);
-  settings::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-}
-
-}  // namespace firestore
-}  // namespace firebase
diff --git a/firestore/src/android/firebase_firestore_settings_android.h b/firestore/src/android/firebase_firestore_settings_android.h
deleted file mode 100644
index cb38634333..0000000000
--- a/firestore/src/android/firebase_firestore_settings_android.h
+++ /dev/null
@@ -1,31 +0,0 @@
-#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_FIREBASE_FIRESTORE_SETTINGS_ANDROID_H_
-#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_FIREBASE_FIRESTORE_SETTINGS_ANDROID_H_
-
-#include 
-
-#include "app/src/include/firebase/app.h"
-#include "app/src/util_android.h"
-#include "firestore/src/include/firebase/firestore/settings.h"
-
-namespace firebase {
-namespace firestore {
-
-class FirebaseFirestoreSettingsInternal {
- public:
-  using ApiType = Settings;
-
-  static jobject SettingToJavaSetting(JNIEnv* env, const Settings& settings);
-
-  static Settings JavaSettingToSetting(JNIEnv* env, jobject obj);
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
-};
-
-}  // namespace firestore
-}  // namespace firebase
-
-#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_FIREBASE_FIRESTORE_SETTINGS_ANDROID_H_
diff --git a/firestore/src/android/firestore_android.cc b/firestore/src/android/firestore_android.cc
index b61e43c6c7..2adb8fc4d5 100644
--- a/firestore/src/android/firestore_android.cc
+++ b/firestore/src/android/firestore_android.cc
@@ -17,19 +17,20 @@
 #include "firestore/src/android/document_reference_android.h"
 #include "firestore/src/android/document_snapshot_android.h"
 #include "firestore/src/android/event_listener_android.h"
+#include "firestore/src/android/exception_android.h"
 #include "firestore/src/android/field_path_android.h"
 #include "firestore/src/android/field_value_android.h"
-#include "firestore/src/android/firebase_firestore_exception_android.h"
-#include "firestore/src/android/firebase_firestore_settings_android.h"
 #include "firestore/src/android/geo_point_android.h"
 #include "firestore/src/android/lambda_event_listener.h"
 #include "firestore/src/android/lambda_transaction_function.h"
+#include "firestore/src/android/listener_registration_android.h"
 #include "firestore/src/android/metadata_changes_android.h"
 #include "firestore/src/android/promise_android.h"
 #include "firestore/src/android/query_android.h"
 #include "firestore/src/android/query_snapshot_android.h"
 #include "firestore/src/android/server_timestamp_behavior_android.h"
 #include "firestore/src/android/set_options_android.h"
+#include "firestore/src/android/settings_android.h"
 #include "firestore/src/android/snapshot_metadata_android.h"
 #include "firestore/src/android/source_android.h"
 #include "firestore/src/android/timestamp_android.h"
@@ -38,89 +39,117 @@
 #include "firestore/src/android/wrapper.h"
 #include "firestore/src/android/write_batch_android.h"
 #include "firestore/src/include/firebase/firestore.h"
+#include "firestore/src/jni/array_list.h"
+#include "firestore/src/jni/boolean.h"
+#include "firestore/src/jni/collection.h"
+#include "firestore/src/jni/double.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/hash_map.h"
+#include "firestore/src/jni/integer.h"
+#include "firestore/src/jni/iterator.h"
 #include "firestore/src/jni/jni.h"
+#include "firestore/src/jni/list.h"
+#include "firestore/src/jni/loader.h"
+#include "firestore/src/jni/long.h"
+#include "firestore/src/jni/map.h"
+#include "firestore/src/jni/set.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
+
+using jni::Constructor;
+using jni::Env;
+using jni::Loader;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+using jni::StaticMethod;
+using jni::String;
+
+constexpr char kFirestoreClassName[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/FirebaseFirestore";
+
+Method kCollection(
+    "collection",
+    "(Ljava/lang/String;)"
+    "Lcom/google/firebase/firestore/CollectionReference;");
+Method kDocument("document",
+                         "(Ljava/lang/String;)"
+                         "Lcom/google/firebase/firestore/DocumentReference;");
+Method kCollectionGroup("collectionGroup",
+                                "(Ljava/lang/String;)"
+                                "Lcom/google/firebase/firestore/Query;");
+Method kGetSettings(
+    "getFirestoreSettings",
+    "()Lcom/google/firebase/firestore/FirebaseFirestoreSettings;");
+StaticMethod kGetInstance(
+    "getInstance",
+    "(Lcom/google/firebase/FirebaseApp;)"
+    "Lcom/google/firebase/firestore/FirebaseFirestore;");
+StaticMethod kSetLoggingEnabled("setLoggingEnabled", "(Z)V");
+StaticMethod kSetClientLanguage("setClientLanguage",
+                                      "(Ljava/lang/String;)V");
+Method kSetSettings(
+    "setFirestoreSettings",
+    "(Lcom/google/firebase/firestore/FirebaseFirestoreSettings;)V");
+Method kBatch("batch", "()Lcom/google/firebase/firestore/WriteBatch;");
+Method kRunTransaction(
+    "runTransaction",
+    "(Lcom/google/firebase/firestore/Transaction$Function;)"
+    "Lcom/google/android/gms/tasks/Task;");
+Method kEnableNetwork("enableNetwork",
+                              "()Lcom/google/android/gms/tasks/Task;");
+Method kDisableNetwork("disableNetwork",
+                               "()Lcom/google/android/gms/tasks/Task;");
+Method kTerminate("terminate", "()Lcom/google/android/gms/tasks/Task;");
+Method kWaitForPendingWrites("waitForPendingWrites",
+                                     "()Lcom/google/android/gms/tasks/Task;");
+Method kClearPersistence("clearPersistence",
+                                 "()Lcom/google/android/gms/tasks/Task;");
+Method kAddSnapshotsInSyncListener(
+    "addSnapshotsInSyncListener",
+    "(Ljava/util/concurrent/Executor;Ljava/lang/Runnable;)"
+    "Lcom/google/firebase/firestore/ListenerRegistration;");
+
+void InitializeFirestore(Loader& loader) {
+  loader.LoadClass(kFirestoreClassName, kCollection, kDocument,
+                   kCollectionGroup, kGetSettings, kGetInstance,
+                   kSetLoggingEnabled, kSetClientLanguage, kSetSettings, kBatch,
+                   kRunTransaction, kEnableNetwork, kDisableNetwork, kTerminate,
+                   kWaitForPendingWrites, kClearPersistence,
+                   kAddSnapshotsInSyncListener);
+}
 
-const char kApiIdentifier[] = "Firestore";
+constexpr char kUserCallbackExecutorClassName[] = PROGUARD_KEEP_CLASS
+    "com/google/firebase/firestore/internal/cpp/"
+    "SilentRejectionSingleThreadExecutor";
+Constructor kNewUserCallbackExecutor("()V");
+Method kExecutorShutdown("shutdown", "()V");
+
+void InitializeUserCallbackExecutor(Loader& loader) {
+  loader.LoadClass(kUserCallbackExecutorClassName, kNewUserCallbackExecutor,
+                   kExecutorShutdown);
+}
+
+}  // namespace
 
-// clang-format off
-#define FIREBASE_FIRESTORE_METHODS(X)                                   \
-  X(Collection, "collection",                                           \
-    "(Ljava/lang/String;)"                                              \
-    "Lcom/google/firebase/firestore/CollectionReference;"),             \
-  X(Document, "document",                                               \
-    "(Ljava/lang/String;)"                                              \
-    "Lcom/google/firebase/firestore/DocumentReference;"),               \
-  X(CollectionGroup, "collectionGroup",                                 \
-    "(Ljava/lang/String;)"                                              \
-    "Lcom/google/firebase/firestore/Query;"),                           \
-  X(GetSettings, "getFirestoreSettings",                                \
-    "()Lcom/google/firebase/firestore/FirebaseFirestoreSettings;"),     \
-  X(GetInstance, "getInstance",                                         \
-    "(Lcom/google/firebase/FirebaseApp;)"                               \
-    "Lcom/google/firebase/firestore/FirebaseFirestore;",                \
-    util::kMethodTypeStatic),                                           \
-  X(SetLoggingEnabled, "setLoggingEnabled",                             \
-    "(Z)V", util::kMethodTypeStatic),                                   \
-  X(SetSettings, "setFirestoreSettings",                                \
-    "(Lcom/google/firebase/firestore/FirebaseFirestoreSettings;)V"),    \
-  X(Batch, "batch",                                                     \
-    "()Lcom/google/firebase/firestore/WriteBatch;"),                    \
-  X(RunTransaction, "runTransaction",                                   \
-    "(Lcom/google/firebase/firestore/Transaction$Function;)"            \
-    "Lcom/google/android/gms/tasks/Task;"),                             \
-  X(EnableNetwork, "enableNetwork",                                     \
-    "()Lcom/google/android/gms/tasks/Task;"),                           \
-  X(DisableNetwork, "disableNetwork",                                   \
-    "()Lcom/google/android/gms/tasks/Task;"),                           \
-  X(Terminate, "terminate",                                             \
-    "()Lcom/google/android/gms/tasks/Task;"),                           \
-  X(WaitForPendingWrites, "waitForPendingWrites",                       \
-    "()Lcom/google/android/gms/tasks/Task;"),                           \
-  X(ClearPersistence, "clearPersistence",                               \
-    "()Lcom/google/android/gms/tasks/Task;"),                           \
-  X(AddSnapshotsInSyncListener, "addSnapshotsInSyncListener",           \
-    "(Ljava/util/concurrent/Executor;Ljava/lang/Runnable;)"             \
-    "Lcom/google/firebase/firestore/ListenerRegistration;")
-
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(firebase_firestore, FIREBASE_FIRESTORE_METHODS)
-METHOD_LOOKUP_DEFINITION(firebase_firestore,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/FirebaseFirestore",
-                         FIREBASE_FIRESTORE_METHODS)
-
-#define SILENT_REJECTION_EXECUTOR_METHODS(X) X(Constructor, "", "()V")
-METHOD_LOOKUP_DECLARATION(silent_rejection_executor,
-                          SILENT_REJECTION_EXECUTOR_METHODS)
-METHOD_LOOKUP_DEFINITION(silent_rejection_executor,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/internal/cpp/"
-                         "SilentRejectionSingleThreadExecutor",
-                         SILENT_REJECTION_EXECUTOR_METHODS)
+const char kApiIdentifier[] = "Firestore";
 
 Mutex FirestoreInternal::init_mutex_;  // NOLINT
 int FirestoreInternal::initialize_count_ = 0;
+Loader* FirestoreInternal::loader_ = nullptr;
 
 FirestoreInternal::FirestoreInternal(App* app) {
   FIREBASE_ASSERT(app != nullptr);
   if (!Initialize(app)) return;
   app_ = app;
 
-  JNIEnv* env = app_->GetJNIEnv();
-  jobject platform_app = app_->GetPlatformApp();
-  jobject firestore_obj = env->CallStaticObjectMethod(
-      firebase_firestore::GetClass(),
-      firebase_firestore::GetMethodId(firebase_firestore::kGetInstance),
-      platform_app);
-  util::CheckAndClearJniExceptions(env);
-  env->DeleteLocalRef(platform_app);
-  FIREBASE_ASSERT(firestore_obj != nullptr);
-  obj_ = env->NewGlobalRef(firestore_obj);
-  env->DeleteLocalRef(firestore_obj);
+  Env env = GetEnv();
+  Local platform_app(env.get(), app_->GetPlatformApp());
+  Local java_firestore = env.Call(kGetInstance, platform_app);
+  FIREBASE_ASSERT(java_firestore.get() != nullptr);
+  obj_ = java_firestore;
 
   // Mainly for enabling TimestampsInSnapshotsEnabled. The rest comes from the
   // default in native SDK. The C++ implementation relies on that for reading
@@ -128,17 +157,12 @@ FirestoreInternal::FirestoreInternal(App* app) {
   // default, we may safely remove the calls below.
   set_settings(settings());
 
-  jobject user_callback_executor_obj =
-      env->NewObject(silent_rejection_executor::GetClass(),
-                     silent_rejection_executor::GetMethodId(
-                         silent_rejection_executor::kConstructor));
+  Local java_user_callback_executor = env.New(kNewUserCallbackExecutor);
 
-  CheckAndClearJniExceptions(env);
-  FIREBASE_ASSERT(user_callback_executor_obj != nullptr);
-  user_callback_executor_ = env->NewGlobalRef(user_callback_executor_obj);
-  env->DeleteLocalRef(user_callback_executor_obj);
+  FIREBASE_ASSERT(java_user_callback_executor.get() != nullptr);
+  user_callback_executor_ = java_user_callback_executor;
 
-  future_manager_.AllocFutureApi(this, static_cast(FirestoreFn::kCount));
+  future_manager_.AllocFutureApi(this, static_cast(AsyncFn::kCount));
 }
 
 /* static */
@@ -147,99 +171,75 @@ bool FirestoreInternal::Initialize(App* app) {
   if (initialize_count_ == 0) {
     jni::Initialize(app->java_vm());
 
-    JNIEnv* env = app->GetJNIEnv();
-    jobject activity = app->activity();
-    if (!(firebase_firestore::CacheMethodIds(env, activity) &&
-          // Call Initialize on each Firestore internal class.
-          BlobInternal::Initialize(app) &&
-          CollectionReferenceInternal::Initialize(app) &&
-          DirectionInternal::Initialize(app) &&
-          DocumentChangeInternal::Initialize(app) &&
-          DocumentChangeTypeInternal::Initialize(app) &&
-          DocumentReferenceInternal::Initialize(app) &&
-          DocumentSnapshotInternal::Initialize(app) &&
-          FieldPathConverter::Initialize(app) &&
-          FieldValueInternal::Initialize(app) &&
-          FirebaseFirestoreExceptionInternal::Initialize(app) &&
-          FirebaseFirestoreSettingsInternal::Initialize(app) &&
-          GeoPointInternal::Initialize(app) &&
-          ListenerRegistrationInternal::Initialize(app) &&
-          MetadataChangesInternal::Initialize(app) &&
-          QueryInternal::Initialize(app) &&
-          QuerySnapshotInternal::Initialize(app) &&
-          ServerTimestampBehaviorInternal::Initialize(app) &&
-          SetOptionsInternal::Initialize(app) &&
-          SnapshotMetadataInternal::Initialize(app) &&
-          SourceInternal::Initialize(app) &&
-          TimestampInternal::Initialize(app) &&
-          TransactionInternal::Initialize(app) && Wrapper::Initialize(app) &&
-          WriteBatchInternal::Initialize(app) &&
-          // Initialize those embedded Firestore internal classes.
-          InitializeEmbeddedClasses(app))) {
+    Loader loader(app);
+    loader.AddEmbeddedFile(::firebase_firestore::firestore_resources_filename,
+                           ::firebase_firestore::firestore_resources_data,
+                           ::firebase_firestore::firestore_resources_size);
+    loader.CacheEmbeddedFiles();
+
+    if (!FieldValueInternal::Initialize(app)) {
+      ReleaseClasses(app);
+      return false;
+    }
+
+    jni::Object::Initialize(loader);
+
+    jni::ArrayList::Initialize(loader);
+    jni::Boolean::Initialize(loader);
+    jni::Collection::Initialize(loader);
+    jni::Double::Initialize(loader);
+    jni::Integer::Initialize(loader);
+    jni::Iterator::Initialize(loader);
+    jni::HashMap::Initialize(loader);
+    jni::List::Initialize(loader);
+    jni::Long::Initialize(loader);
+    jni::Map::Initialize(loader);
+
+    InitializeFirestore(loader);
+    InitializeUserCallbackExecutor(loader);
+
+    BlobInternal::Initialize(loader);
+    CollectionReferenceInternal::Initialize(loader);
+    DirectionInternal::Initialize(loader);
+    DocumentChangeInternal::Initialize(loader);
+    DocumentChangeTypeInternal::Initialize(loader);
+    DocumentReferenceInternal::Initialize(loader);
+    DocumentSnapshotInternal::Initialize(loader);
+    EventListenerInternal::Initialize(loader);
+    ExceptionInternal::Initialize(loader);
+    FieldPathConverter::Initialize(loader);
+    GeoPointInternal::Initialize(loader);
+    ListenerRegistrationInternal::Initialize(loader);
+    MetadataChangesInternal::Initialize(loader);
+    QueryInternal::Initialize(loader);
+    QuerySnapshotInternal::Initialize(loader);
+    ServerTimestampBehaviorInternal::Initialize(loader);
+    SetOptionsInternal::Initialize(loader);
+    SettingsInternal::Initialize(loader);
+    SnapshotMetadataInternal::Initialize(loader);
+    SourceInternal::Initialize(loader);
+    TimestampInternal::Initialize(loader);
+    TransactionInternal::Initialize(loader);
+    WriteBatchInternal::Initialize(loader);
+    if (!loader.ok()) {
       ReleaseClasses(app);
       return false;
     }
 
-    util::CheckAndClearJniExceptions(env);
+    FIREBASE_DEV_ASSERT(loader_ == nullptr);
+    loader_ = new Loader(Move(loader));
   }
   initialize_count_++;
   return true;
 }
 
-/* static */
-bool FirestoreInternal::InitializeEmbeddedClasses(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  // Terminate() handles tearing this down.
-  // Load embedded classes.
-  const std::vector embedded_files =
-      util::CacheEmbeddedFiles(
-          env, activity,
-          internal::EmbeddedFile::ToVector(
-              ::firebase_firestore::firestore_resources_filename,
-              ::firebase_firestore::firestore_resources_data,
-              ::firebase_firestore::firestore_resources_size));
-  return EventListenerInternal::InitializeEmbeddedClasses(app,
-                                                          &embedded_files) &&
-         TransactionInternal::InitializeEmbeddedClasses(app, &embedded_files) &&
-         silent_rejection_executor::CacheClassFromFiles(env, activity,
-                                                        &embedded_files) &&
-         silent_rejection_executor::CacheMethodIds(env, activity);
-}
-
 /* static */
 void FirestoreInternal::ReleaseClasses(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  firebase_firestore::ReleaseClass(env);
-  silent_rejection_executor::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+  delete loader_;
+  loader_ = nullptr;
 
   // Call Terminate on each Firestore internal class.
-  BlobInternal::Terminate(app);
-  CollectionReferenceInternal::Terminate(app);
-  DirectionInternal::Terminate(app);
-  DocumentChangeInternal::Terminate(app);
-  DocumentChangeTypeInternal::Terminate(app);
-  DocumentReferenceInternal::Terminate(app);
-  DocumentSnapshotInternal::Terminate(app);
-  EventListenerInternal::Terminate(app);
-  FieldPathConverter::Terminate(app);
   FieldValueInternal::Terminate(app);
-  FirebaseFirestoreExceptionInternal::Terminate(app);
-  FirebaseFirestoreSettingsInternal::Terminate(app);
-  GeoPointInternal::Terminate(app);
-  ListenerRegistrationInternal::Terminate(app);
-  MetadataChangesInternal::Terminate(app);
-  QueryInternal::Terminate(app);
-  QuerySnapshotInternal::Terminate(app);
-  ServerTimestampBehaviorInternal::Terminate(app);
-  SetOptionsInternal::Terminate(app);
-  SnapshotMetadataInternal::Terminate(app);
-  SourceInternal::Terminate(app);
-  TimestampInternal::Terminate(app);
-  TransactionInternal::Terminate(app);
-  Wrapper::Terminate(app);
-  WriteBatchInternal::Terminate(app);
 }
 
 /* static */
@@ -252,11 +252,8 @@ void FirestoreInternal::Terminate(App* app) {
   }
 }
 
-void FirestoreInternal::ShutdownUserCallbackExecutor() {
-  JNIEnv* env = app_->GetJNIEnv();
-  auto shutdown_method = env->GetMethodID(
-      env->GetObjectClass(user_callback_executor_), "shutdown", "()V");
-  env->CallVoidMethod(user_callback_executor_, shutdown_method);
+void FirestoreInternal::ShutdownUserCallbackExecutor(Env& env) {
+  env.Call(user_callback_executor_, kExecutorShutdown);
 }
 
 FirestoreInternal::~FirestoreInternal() {
@@ -274,239 +271,130 @@ FirestoreInternal::~FirestoreInternal() {
     listener_registrations_.clear();
   }
 
-  future_manager_.ReleaseFutureApi(this);
+  Env env = GetEnv();
+  ShutdownUserCallbackExecutor(env);
 
-  ShutdownUserCallbackExecutor();
+  future_manager_.ReleaseFutureApi(this);
 
-  JNIEnv* env = app_->GetJNIEnv();
-  env->DeleteGlobalRef(user_callback_executor_);
-  user_callback_executor_ = nullptr;
-  env->DeleteGlobalRef(obj_);
-  obj_ = nullptr;
   Terminate(app_);
   app_ = nullptr;
-
-  util::CheckAndClearJniExceptions(env);
 }
 
 CollectionReference FirestoreInternal::Collection(
     const char* collection_path) const {
-  JNIEnv* env = app_->GetJNIEnv();
-  jstring path_string = env->NewStringUTF(collection_path);
-  jobject collection_reference = env->CallObjectMethod(
-      obj_, firebase_firestore::GetMethodId(firebase_firestore::kCollection),
-      path_string);
-  env->DeleteLocalRef(path_string);
-  CheckAndClearJniExceptions(env);
-  FIREBASE_ASSERT(collection_reference != nullptr);
-  CollectionReferenceInternal* internal = new CollectionReferenceInternal{
-      const_cast(this), collection_reference};
-  env->DeleteLocalRef(collection_reference);
-  CheckAndClearJniExceptions(env);
-  return CollectionReference{internal};
+  Env env = GetEnv();
+  Local java_path = env.NewStringUtf(collection_path);
+  Local result = env.Call(obj_, kCollection, java_path);
+  return NewCollectionReference(env, result);
 }
 
 DocumentReference FirestoreInternal::Document(const char* document_path) const {
-  JNIEnv* env = app_->GetJNIEnv();
-  jstring path_string = env->NewStringUTF(document_path);
-  jobject document_reference = env->CallObjectMethod(
-      obj_, firebase_firestore::GetMethodId(firebase_firestore::kDocument),
-      path_string);
-  env->DeleteLocalRef(path_string);
-  CheckAndClearJniExceptions(env);
-  FIREBASE_ASSERT(document_reference != nullptr);
-  DocumentReferenceInternal* internal = new DocumentReferenceInternal{
-      const_cast(this), document_reference};
-  env->DeleteLocalRef(document_reference);
-  CheckAndClearJniExceptions(env);
-  return DocumentReference{internal};
+  Env env = GetEnv();
+  Local java_path = env.NewStringUtf(document_path);
+  Local result = env.Call(obj_, kDocument, java_path);
+  return NewDocumentReference(env, result);
 }
 
 Query FirestoreInternal::CollectionGroup(const char* collection_id) const {
-  JNIEnv* env = app_->GetJNIEnv();
-  jstring collection_id_string = env->NewStringUTF(collection_id);
-
-  jobject query = env->CallObjectMethod(
-      obj_,
-      firebase_firestore::GetMethodId(firebase_firestore::kCollectionGroup),
-      collection_id_string);
-  env->DeleteLocalRef(collection_id_string);
-  CheckAndClearJniExceptions(env);
-  FIREBASE_ASSERT(query != nullptr);
-
-  QueryInternal* internal =
-      new QueryInternal{const_cast(this), query};
-  env->DeleteLocalRef(query);
-
-  CheckAndClearJniExceptions(env);
-  return Query{internal};
+  Env env = GetEnv();
+  Local java_collection_id = env.NewStringUtf(collection_id);
+  Local query = env.Call(obj_, kCollectionGroup, java_collection_id);
+  return NewQuery(env, query);
 }
 
 Settings FirestoreInternal::settings() const {
-  JNIEnv* env = app_->GetJNIEnv();
-  jobject settings = env->CallObjectMethod(
-      obj_, firebase_firestore::GetMethodId(firebase_firestore::kGetSettings));
-  FIREBASE_ASSERT(settings != nullptr);
+  Env env = GetEnv();
+  Local settings = env.Call(obj_, kGetSettings);
 
-  Settings result =
-      FirebaseFirestoreSettingsInternal::JavaSettingToSetting(env, settings);
-  env->DeleteLocalRef(settings);
-  CheckAndClearJniExceptions(env);
-  return result;
+  if (!env.ok()) return {};
+  return settings.ToPublic(env);
 }
 
 void FirestoreInternal::set_settings(Settings settings) {
-  JNIEnv* env = app_->GetJNIEnv();
-  jobject settings_jobj =
-      FirebaseFirestoreSettingsInternal::SettingToJavaSetting(env, settings);
-  env->CallVoidMethod(
-      obj_, firebase_firestore::GetMethodId(firebase_firestore::kSetSettings),
-      settings_jobj);
-  env->DeleteLocalRef(settings_jobj);
-  CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  auto java_settings = SettingsInternal::Create(env, settings);
+  env.Call(obj_, kSetSettings, java_settings);
 }
 
 WriteBatch FirestoreInternal::batch() const {
-  JNIEnv* env = app_->GetJNIEnv();
-  jobject write_batch = env->CallObjectMethod(
-      obj_, firebase_firestore::GetMethodId(firebase_firestore::kBatch));
-  FIREBASE_ASSERT(write_batch != nullptr);
+  Env env = GetEnv();
+  Local result = env.Call(obj_, kBatch);
 
-  WriteBatchInternal* internal =
-      new WriteBatchInternal{const_cast(this), write_batch};
-  env->DeleteLocalRef(write_batch);
-  CheckAndClearJniExceptions(env);
-  return WriteBatch{internal};
+  if (!env.ok()) return {};
+  return WriteBatch(new WriteBatchInternal(mutable_this(), result.get()));
 }
 
 Future FirestoreInternal::RunTransaction(TransactionFunction* update,
                                                bool is_lambda) {
-  JNIEnv* env = app_->GetJNIEnv();
-  jobject transaction_function =
-      TransactionInternal::ToJavaObject(env, this, update);
-  jobject task = env->CallObjectMethod(
-      obj_,
-      firebase_firestore::GetMethodId(firebase_firestore::kRunTransaction),
-      transaction_function);
-  env->DeleteLocalRef(transaction_function);
-  CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  Local transaction_function =
+      TransactionInternal::Create(env, this, update);
+  Local task = env.Call(obj_, kRunTransaction, transaction_function);
+
+  if (!env.ok()) return {};
 
 #if defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
   auto* completion =
       static_cast(is_lambda ? update : nullptr);
-  Promise promise{ref_future(), this, completion};
-#else   // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
-  Promise promise{ref_future(), this};
+  Promise promise(ref_future(), this, completion);
+#else  // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
+  Promise promise(ref_future(), this);
 #endif  // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
 
-  promise.RegisterForTask(FirestoreFn::kRunTransaction, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
+  promise.RegisterForTask(env, AsyncFn::kRunTransaction, task);
   return promise.GetFuture();
 }
 
 #if defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
 Future FirestoreInternal::RunTransaction(
     std::function update) {
-  LambdaTransactionFunction* lambda_update =
-      new LambdaTransactionFunction(firebase::Move(update));
+  auto* lambda_update = new LambdaTransactionFunction(Move(update));
   return RunTransaction(lambda_update, /*is_lambda=*/true);
 }
 #endif  // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
 
 Future FirestoreInternal::DisableNetwork() {
-  JNIEnv* env = app_->GetJNIEnv();
-  jobject task = env->CallObjectMethod(
-      obj_,
-      firebase_firestore::GetMethodId(firebase_firestore::kDisableNetwork));
-  CheckAndClearJniExceptions(env);
-
-  Promise promise{ref_future(), this};
-  promise.RegisterForTask(FirestoreFn::kDisableNetwork, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
+  Env env = GetEnv();
+  Local task = env.Call(obj_, kDisableNetwork);
+  return NewFuture(env, AsyncFn::kDisableNetwork, task);
 }
 
 Future FirestoreInternal::EnableNetwork() {
-  JNIEnv* env = app_->GetJNIEnv();
-  jobject task = env->CallObjectMethod(
-      obj_,
-      firebase_firestore::GetMethodId(firebase_firestore::kEnableNetwork));
-  CheckAndClearJniExceptions(env);
-
-  Promise promise{ref_future(), this};
-  promise.RegisterForTask(FirestoreFn::kEnableNetwork, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
+  Env env = GetEnv();
+  Local task = env.Call(obj_, kEnableNetwork);
+  return NewFuture(env, AsyncFn::kEnableNetwork, task);
 }
 
 Future FirestoreInternal::Terminate() {
-  JNIEnv* env = app_->GetJNIEnv();
-
-  jobject task = env->CallObjectMethod(
-      obj_, firebase_firestore::GetMethodId(firebase_firestore::kTerminate));
-  CheckAndClearJniExceptions(env);
-
-  Promise promise{ref_future(), this};
-  promise.RegisterForTask(FirestoreFn::kTerminate, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-
-  return promise.GetFuture();
+  Env env = GetEnv();
+  Local task = env.Call(obj_, kTerminate);
+  return NewFuture(env, AsyncFn::kTerminate, task);
 }
 
 Future FirestoreInternal::WaitForPendingWrites() {
-  JNIEnv* env = app_->GetJNIEnv();
-  jobject task = env->CallObjectMethod(
-      obj_, firebase_firestore::GetMethodId(
-                firebase_firestore::kWaitForPendingWrites));
-  CheckAndClearJniExceptions(env);
-
-  Promise promise{ref_future(), this};
-  promise.RegisterForTask(FirestoreFn::kWaitForPendingWrites, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
+  Env env = GetEnv();
+  Local task = env.Call(obj_, kWaitForPendingWrites);
+  return NewFuture(env, AsyncFn::kWaitForPendingWrites, task);
 }
 
 Future FirestoreInternal::ClearPersistence() {
-  JNIEnv* env = app_->GetJNIEnv();
-  jobject task = env->CallObjectMethod(
-      obj_,
-      firebase_firestore::GetMethodId(firebase_firestore::kClearPersistence));
-  CheckAndClearJniExceptions(env);
-
-  Promise promise{ref_future(), this};
-  promise.RegisterForTask(FirestoreFn::kClearPersistence, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
+  Env env = GetEnv();
+  Local task = env.Call(obj_, kClearPersistence);
+  return NewFuture(env, AsyncFn::kClearPersistence, task);
 }
 
 ListenerRegistration FirestoreInternal::AddSnapshotsInSyncListener(
     EventListener* listener, bool passing_listener_ownership) {
-  JNIEnv* env = app_->GetJNIEnv();
-
-  // Create listener.
-  jobject java_runnable =
-      EventListenerInternal::EventListenerToJavaRunnable(env, listener);
+  Env env = GetEnv();
+  Local java_runnable = EventListenerInternal::Create(env, listener);
 
-  // Register listener.
-  jobject java_registration = env->CallObjectMethod(
-      obj_,
-      firebase_firestore::GetMethodId(
-          firebase_firestore::kAddSnapshotsInSyncListener),
-      user_callback_executor(), java_runnable);
-  env->DeleteLocalRef(java_runnable);
-  CheckAndClearJniExceptions(env);
+  Local java_registration =
+      env.Call(obj_, kAddSnapshotsInSyncListener, user_callback_executor(),
+               java_runnable);
 
-  // Wrapping
-  ListenerRegistrationInternal* registration = new ListenerRegistrationInternal{
-      this, listener, passing_listener_ownership, java_registration};
-  env->DeleteLocalRef(java_registration);
-  return ListenerRegistration{registration};
+  if (!env.ok() || !java_registration) return {};
+  return ListenerRegistration(new ListenerRegistrationInternal(
+      this, listener, passing_listener_ownership, java_registration));
 }
 
 #if defined(FIREBASE_USE_STD_FUNCTION)
@@ -536,17 +424,63 @@ void FirestoreInternal::UnregisterListenerRegistration(
   }
 }
 
+jni::Env FirestoreInternal::GetEnv() {
+  jni::Env env;
+  env.SetUnhandledExceptionHandler(GlobalUnhandledExceptionHandler, nullptr);
+  return env;
+}
+
+CollectionReference FirestoreInternal::NewCollectionReference(
+    jni::Env& env, const jni::Object& reference) const {
+  if (!env.ok() || !reference) return {};
+
+  return CollectionReference(
+      new CollectionReferenceInternal(mutable_this(), reference.get()));
+}
+
+DocumentReference FirestoreInternal::NewDocumentReference(
+    jni::Env& env, const jni::Object& reference) const {
+  if (!env.ok() || !reference) return {};
+
+  return DocumentReference(
+      new DocumentReferenceInternal(mutable_this(), reference.get()));
+}
+
+DocumentSnapshot FirestoreInternal::NewDocumentSnapshot(
+    jni::Env& env, const jni::Object& snapshot) const {
+  if (!env.ok() || !snapshot) return {};
+
+  return DocumentSnapshot(
+      new DocumentSnapshotInternal(mutable_this(), snapshot.get()));
+}
+
+Query FirestoreInternal::NewQuery(jni::Env& env,
+                                  const jni::Object& query) const {
+  if (!env.ok() || !query) return {};
+  return Query(new QueryInternal(mutable_this(), query.get()));
+}
+
+QuerySnapshot FirestoreInternal::NewQuerySnapshot(
+    jni::Env& env, const jni::Object& snapshot) const {
+  if (!env.ok() || !snapshot) return {};
+  return QuerySnapshot(
+      new QuerySnapshotInternal(mutable_this(), snapshot.get()));
+}
+
 /* static */
 void Firestore::set_log_level(LogLevel log_level) {
   // "Verbose" and "debug" map to logging enabled.
   // "Info", "warning", "error", and "assert" map to logging disabled.
   bool logging_enabled = log_level < LogLevel::kLogLevelInfo;
-  JNIEnv* env = firebase::util::GetJNIEnvFromApp();
-  env->CallStaticVoidMethod(
-      firebase_firestore::GetClass(),
-      firebase_firestore::GetMethodId(firebase_firestore::kSetLoggingEnabled),
-      logging_enabled);
-  CheckAndClearJniExceptions(env);
+
+  Env env = FirestoreInternal::GetEnv();
+  env.Call(kSetLoggingEnabled, logging_enabled);
+}
+
+void FirestoreInternal::SetClientLanguage(const std::string& language_token) {
+  Env env = FirestoreInternal::GetEnv();
+  Local java_language_token = env.NewStringUtf(language_token);
+  env.Call(kSetClientLanguage, java_language_token);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/firestore_android.h b/firestore/src/android/firestore_android.h
index 2ab261a68a..643d535d94 100644
--- a/firestore/src/android/firestore_android.h
+++ b/firestore/src/android/firestore_android.h
@@ -13,11 +13,14 @@
 #include "app/src/cleanup_notifier.h"
 #include "app/src/future_manager.h"
 #include "app/src/include/firebase/app.h"
-#include "app/src/util_android.h"
-#include "firestore/src/android/listener_registration_android.h"
+#include "firestore/src/common/type_mapping.h"
 #include "firestore/src/include/firebase/firestore/collection_reference.h"
 #include "firestore/src/include/firebase/firestore/document_reference.h"
 #include "firestore/src/include/firebase/firestore/settings.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+#include "firestore/src/jni/ownership.h"
 
 namespace firebase {
 namespace firestore {
@@ -28,24 +31,13 @@ class Transaction;
 class TransactionFunction;
 class WriteBatch;
 
+template 
+class Promise;
+
 // Used for registering global callbacks. See
 // firebase::util::RegisterCallbackOnTask for context.
 extern const char kApiIdentifier[];
 
-// Each API of Firestore that returns a Future needs to define an enum
-// value here. For example, a Future-returning method Foo() relies on the enum
-// value kFoo. The enum values are used to identify and manage Future in the
-// Firestore Future manager.
-enum class FirestoreFn {
-  kEnableNetwork = 0,
-  kDisableNetwork,
-  kRunTransaction,
-  kTerminate,
-  kWaitForPendingWrites,
-  kClearPersistence,
-  kCount,  // Must be the last enum value.
-};
-
 // This is the Android implementation of Firestore. Cannot inherit WrapperFuture
 // as a valid FirestoreInternal is required to construct a WrapperFuture. So we
 // will implement the Future APIs on the fly.
@@ -53,6 +45,20 @@ class FirestoreInternal {
  public:
   using ApiType = Firestore;
 
+  // Each API of Firestore that returns a Future needs to define an enum
+  // value here. For example, a Future-returning method Foo() relies on the enum
+  // value kFoo. The enum values are used to identify and manage Future in the
+  // Firestore Future manager.
+  enum class AsyncFn {
+    kEnableNetwork = 0,
+    kDisableNetwork,
+    kRunTransaction,
+    kTerminate,
+    kWaitForPendingWrites,
+    kClearPersistence,
+    kCount,  // Must be the last enum value.
+  };
+
   // Note: call `set_firestore_public` immediately after construction.
   explicit FirestoreInternal(App* app);
   ~FirestoreInternal();
@@ -119,11 +125,23 @@ class FirestoreInternal {
   void UnregisterListenerRegistration(
       ListenerRegistrationInternal* registration);
 
-  // The constructor explicit Foo(FooInternal*) is protected in public API. But
-  // we want it to be public-usable in internal implementation code mainly for
-  // those general utility functions. So we provide this helper to allow any
-  // internal code to use that constructor. Here we assume FirestoreInternal is
-  // a friend class of InternalType::ApiType.
+  static jni::Env GetEnv();
+
+  CollectionReference NewCollectionReference(
+      jni::Env& env, const jni::Object& reference) const;
+  DocumentReference NewDocumentReference(jni::Env& env,
+                                         const jni::Object& reference) const;
+  DocumentSnapshot NewDocumentSnapshot(jni::Env& env,
+                                       const jni::Object& snapshot) const;
+  Query NewQuery(jni::Env& env, const jni::Object& query) const;
+  QuerySnapshot NewQuerySnapshot(jni::Env& env,
+                                 const jni::Object& snapshot) const;
+
+  // The constructor explicit Foo(FooInternal*) is protected in public API.
+  // But we want it to be public-usable in internal implementation code
+  // mainly for those general utility functions. So we provide this helper
+  // to allow any internal code to use that constructor. Here we assume
+  // FirestoreInternal is a friend class of InternalType::ApiType.
   template 
   static typename InternalType::ApiType Wrap(InternalType* internal) {
     return typename InternalType::ApiType{internal};
@@ -145,7 +163,11 @@ class FirestoreInternal {
   Firestore* firestore_public() { return firestore_public_; }
   const Firestore* firestore_public() const { return firestore_public_; }
 
-  jobject user_callback_executor() const { return user_callback_executor_; }
+  const jni::Global& user_callback_executor() const {
+    return user_callback_executor_;
+  }
+
+  static void SetClientLanguage(const std::string& language_token);
 
  private:
   // Gets the reference-counted Future implementation of this instance, which
@@ -154,24 +176,37 @@ class FirestoreInternal {
     return future_manager_.GetFutureApi(this);
   }
 
-  void ShutdownUserCallbackExecutor();
+  FirestoreInternal* mutable_this() const {
+    return const_cast(this);
+  }
+
+  template >
+  Future NewFuture(jni::Env& env, AsyncFn op,
+                            const jni::Object& task) const {
+    if (!env.ok()) return {};
+
+    FirestoreInternal* self = mutable_this();
+    Promise promise(self->ref_future(), self);
+    promise.RegisterForTask(env, op, task);
+    return promise.GetFuture();
+  }
+
+  void ShutdownUserCallbackExecutor(jni::Env& env);
 
   static bool Initialize(App* app);
   static void ReleaseClasses(App* app);
   static void Terminate(App* app);
 
-  // Initialize classes loaded from embedded files.
-  static bool InitializeEmbeddedClasses(App* app);
-
   static Mutex init_mutex_;
   static int initialize_count_;
+  static jni::Loader* loader_;
 
-  jobject user_callback_executor_;
+  jni::Global user_callback_executor_;
 
   App* app_ = nullptr;
   Firestore* firestore_public_ = nullptr;
   // Java Firestore global ref.
-  jobject obj_;
+  jni::Global obj_;
 
   Mutex listener_registration_mutex_;  // For registering listener-registrations
 #if defined(_STLPORT_VERSION)
diff --git a/firestore/src/android/geo_point_android.cc b/firestore/src/android/geo_point_android.cc
index e484345330..5d7800e52e 100644
--- a/firestore/src/android/geo_point_android.cc
+++ b/firestore/src/android/geo_point_android.cc
@@ -1,65 +1,44 @@
 #include "firestore/src/android/geo_point_android.h"
 
-#include 
-
-#include "app/src/util_android.h"
-#include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-// clang-format off
-#define GEO_POINT_METHODS(X)                                    \
-  X(Constructor, "", "(DD)V", util::kMethodTypeInstance), \
-  X(GetLatitude, "getLatitude", "()D"),                         \
-  X(GetLongitude, "getLongitude", "()D")
-// clang-format on
+using jni::Class;
+using jni::Constructor;
+using jni::Env;
+using jni::Local;
+using jni::Method;
 
-METHOD_LOOKUP_DECLARATION(geo_point, GEO_POINT_METHODS)
-METHOD_LOOKUP_DEFINITION(geo_point,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/GeoPoint",
-                         GEO_POINT_METHODS)
+constexpr char kClassName[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/GeoPoint";
+Constructor kConstructor("(DD)V");
+Method kGetLatitude("getLatitude", "()D");
+Method kGetLongitude("getLongitude", "()D");
 
-/* static */
-jobject GeoPointInternal::GeoPointToJavaGeoPoint(JNIEnv* env,
-                                                 const GeoPoint& point) {
-  jobject result = env->NewObject(
-      geo_point::GetClass(), geo_point::GetMethodId(geo_point::kConstructor),
-      static_cast(point.latitude()),
-      static_cast(point.longitude()));
-  CheckAndClearJniExceptions(env);
-  return result;
-}
+jclass g_clazz = nullptr;
 
-/* static */
-GeoPoint GeoPointInternal::JavaGeoPointToGeoPoint(JNIEnv* env, jobject obj) {
-  jdouble latitude = env->CallDoubleMethod(
-      obj, geo_point::GetMethodId(geo_point::kGetLatitude));
-  jdouble longitude = env->CallDoubleMethod(
-      obj, geo_point::GetMethodId(geo_point::kGetLongitude));
-  CheckAndClearJniExceptions(env);
-  return GeoPoint{static_cast(latitude),
-                  static_cast(longitude)};
+}  // namespace
+
+void GeoPointInternal::Initialize(jni::Loader& loader) {
+  g_clazz =
+      loader.LoadClass(kClassName, kConstructor, kGetLatitude, kGetLongitude);
 }
 
-/* static */
-jclass GeoPointInternal::GetClass() { return geo_point::GetClass(); }
+Class GeoPointInternal::GetClass() { return Class(g_clazz); }
 
-/* static */
-bool GeoPointInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = geo_point::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
+Local GeoPointInternal::Create(Env& env,
+                                                 const GeoPoint& point) {
+  return env.New(kConstructor, point.latitude(), point.longitude());
 }
 
-/* static */
-void GeoPointInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  geo_point::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+GeoPoint GeoPointInternal::ToPublic(Env& env) const {
+  double latitude = env.Call(*this, kGetLatitude);
+  double longitude = env.Call(*this, kGetLongitude);
+  return GeoPoint(latitude, longitude);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/geo_point_android.h b/firestore/src/android/geo_point_android.h
index b11c87b03a..7525eee2fe 100644
--- a/firestore/src/android/geo_point_android.h
+++ b/firestore/src/android/geo_point_android.h
@@ -1,37 +1,30 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_GEO_POINT_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_GEO_POINT_ANDROID_H_
 
-#include 
-
-#include "app/src/include/firebase/app.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
 #include "firebase/firestore/geo_point.h"
 
 namespace firebase {
 namespace firestore {
 
-// This is the non-wrapper Android implementation of GeoPoint. Since GeoPoint
-// has most methods inlined, we use it directly instead of wrapping around a
-// Java GeoPoint object. We still need the helper functions to convert between
-// the two types. In addition, we also need proper initializer and terminator
-// for the Java class cache/uncache.
-class GeoPointInternal {
+/** A C++ proxy for a Java `GeoPoint`. */
+class GeoPointInternal : public jni::Object {
  public:
   using ApiType = GeoPoint;
 
-  // Convert a C++ GeoPoint into a Java GeoPoint.
-  static jobject GeoPointToJavaGeoPoint(JNIEnv* env, const GeoPoint& point);
+  using jni::Object::Object;
 
-  // Convert a Java GeoPoint into a C++ GeoPoint.
-  static GeoPoint JavaGeoPointToGeoPoint(JNIEnv* env, jobject obj);
+  static void Initialize(jni::Loader& loader);
 
-  // Gets the class object of Java GeoPoint class.
-  static jclass GetClass();
+  static jni::Class GetClass();
 
- private:
-  friend class FirestoreInternal;
+  /** Creates a C++ proxy for a Java `GeoPoint` object. */
+  static jni::Local Create(jni::Env& env,
+                                             const GeoPoint& point);
 
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  /** Converts a Java GeoPoint to a public C++ GeoPoint. */
+  GeoPoint ToPublic(jni::Env& env) const;
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/lambda_event_listener.h b/firestore/src/android/lambda_event_listener.h
index b1a10ee0ae..d14d835c46 100644
--- a/firestore/src/android/lambda_event_listener.h
+++ b/firestore/src/android/lambda_event_listener.h
@@ -18,17 +18,19 @@ namespace firestore {
 template 
 class LambdaEventListener : public EventListener {
  public:
-  LambdaEventListener(std::function callback)
+  LambdaEventListener(
+      std::function callback)
       : callback_(firebase::Move(callback)) {
-    FIREBASE_ASSERT(callback);
+    FIREBASE_ASSERT(callback_);
   }
 
-  void OnEvent(const T& value, Error error) override {
-    callback_(value, error);
+  void OnEvent(const T& value, Error error_code,
+               const std::string& error_message) override {
+    callback_(value, error_code, error_message);
   }
 
  private:
-  std::function callback_;
+  std::function callback_;
 };
 
 template <>
@@ -36,10 +38,10 @@ class LambdaEventListener : public EventListener {
  public:
   LambdaEventListener(std::function callback)
       : callback_(firebase::Move(callback)) {
-    FIREBASE_ASSERT(callback);
+    FIREBASE_ASSERT(callback_);
   }
 
-  void OnEvent(Error) override { callback_(); }
+  void OnEvent(Error, const std::string&) override { callback_(); }
 
  private:
   std::function callback_;
diff --git a/firestore/src/android/lambda_transaction_function.h b/firestore/src/android/lambda_transaction_function.h
index 26c0defbb0..e4934fcc5c 100644
--- a/firestore/src/android/lambda_transaction_function.h
+++ b/firestore/src/android/lambda_transaction_function.h
@@ -25,7 +25,7 @@ namespace firestore {
  */
 class LambdaTransactionFunction
     : public TransactionFunction,
-      public Promise::Completion {
+      public Promise::Completion {
  public:
   LambdaTransactionFunction(
       std::function update)
diff --git a/firestore/src/android/listener_registration_android.cc b/firestore/src/android/listener_registration_android.cc
index b5c7e422be..5e782820d5 100644
--- a/firestore/src/android/listener_registration_android.cc
+++ b/firestore/src/android/listener_registration_android.cc
@@ -1,61 +1,68 @@
 #include "firestore/src/android/listener_registration_android.h"
 
 #include "app/src/assert.h"
-#include "app/src/util_android.h"
+#include "firestore/src/android/firestore_android.h"
 #include "firestore/src/include/firebase/firestore/listener_registration.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-#define LISTENER_REGISTRATION_METHODS(X) X(Remove, "remove", "()V")
-METHOD_LOOKUP_DECLARATION(listener_registration, LISTENER_REGISTRATION_METHODS)
-METHOD_LOOKUP_DEFINITION(listener_registration,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/ListenerRegistration",
-                         LISTENER_REGISTRATION_METHODS)
+using jni::Env;
+using jni::Object;
+
+constexpr char kClassName[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/ListenerRegistration";
+
+jni::Method kRemove("remove", "()V");
+
+}  // namespace
+
+void ListenerRegistrationInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClassName, kRemove);
+}
 
 ListenerRegistrationInternal::ListenerRegistrationInternal(
     FirestoreInternal* firestore,
     EventListener* event_listener, bool owning_event_listener,
-    jobject listener_registration)
+    const Object& listener_registration)
     : firestore_(firestore),
-      listener_registration_(
-          firestore->app()->GetJNIEnv()->NewGlobalRef(listener_registration)),
+      listener_registration_(listener_registration),
       document_event_listener_(event_listener),
       owning_event_listener_(owning_event_listener) {
   FIREBASE_ASSERT(firestore != nullptr);
   FIREBASE_ASSERT(event_listener != nullptr);
-  FIREBASE_ASSERT(listener_registration != nullptr);
+  FIREBASE_ASSERT(listener_registration);
 
   firestore->RegisterListenerRegistration(this);
 }
 
 ListenerRegistrationInternal::ListenerRegistrationInternal(
     FirestoreInternal* firestore, EventListener* event_listener,
-    bool owning_event_listener, jobject listener_registration)
+    bool owning_event_listener, const Object& listener_registration)
     : firestore_(firestore),
-      listener_registration_(
-          firestore->app()->GetJNIEnv()->NewGlobalRef(listener_registration)),
+      listener_registration_(listener_registration),
       query_event_listener_(event_listener),
       owning_event_listener_(owning_event_listener) {
   FIREBASE_ASSERT(firestore != nullptr);
   FIREBASE_ASSERT(event_listener != nullptr);
-  FIREBASE_ASSERT(listener_registration != nullptr);
+  FIREBASE_ASSERT(listener_registration);
 
   firestore->RegisterListenerRegistration(this);
 }
 
 ListenerRegistrationInternal::ListenerRegistrationInternal(
     FirestoreInternal* firestore, EventListener* event_listener,
-    bool owning_event_listener, jobject listener_registration)
+    bool owning_event_listener, const Object& listener_registration)
     : firestore_(firestore),
-      listener_registration_(
-          firestore->app()->GetJNIEnv()->NewGlobalRef(listener_registration)),
+      listener_registration_(listener_registration),
       void_event_listener_(event_listener),
       owning_event_listener_(owning_event_listener) {
   FIREBASE_ASSERT(firestore != nullptr);
   FIREBASE_ASSERT(event_listener != nullptr);
-  FIREBASE_ASSERT(listener_registration != nullptr);
+  FIREBASE_ASSERT(listener_registration);
 
   firestore->RegisterListenerRegistration(this);
 }
@@ -64,18 +71,14 @@ ListenerRegistrationInternal::ListenerRegistrationInternal(
 // FirestoreInternal will hold the lock and unregister all of them. So we do not
 // call UnregisterListenerRegistration explicitly here.
 ListenerRegistrationInternal::~ListenerRegistrationInternal() {
-  if (listener_registration_ == nullptr) {
+  if (!listener_registration_) {
     return;
   }
 
   // Remove listener and release java ListenerRegistration object.
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  env->CallVoidMethod(
-      listener_registration_,
-      listener_registration::GetMethodId(listener_registration::kRemove));
-  env->DeleteGlobalRef(listener_registration_);
-  util::CheckAndClearJniExceptions(env);
-  listener_registration_ = nullptr;
+  Env env = GetEnv();
+  env.Call(listener_registration_, kRemove);
+  listener_registration_.clear();
 
   // de-allocate owning EventListener object.
   if (owning_event_listener_) {
@@ -85,21 +88,7 @@ ListenerRegistrationInternal::~ListenerRegistrationInternal() {
   }
 }
 
-/* static */
-bool ListenerRegistrationInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = listener_registration::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void ListenerRegistrationInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  listener_registration::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-}
+jni::Env ListenerRegistrationInternal::GetEnv() { return firestore_->GetEnv(); }
 
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/android/listener_registration_android.h b/firestore/src/android/listener_registration_android.h
index d2495333d7..2abcdd0460 100644
--- a/firestore/src/android/listener_registration_android.h
+++ b/firestore/src/android/listener_registration_android.h
@@ -1,11 +1,13 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_LISTENER_REGISTRATION_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_LISTENER_REGISTRATION_ANDROID_H_
 
-#include 
 #include "firestore/src/android/firestore_android.h"
 #include "firestore/src/include/firebase/firestore/document_snapshot.h"
 #include "firestore/src/include/firebase/firestore/event_listener.h"
 #include "firestore/src/include/firebase/firestore/query_snapshot.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+#include "firestore/src/jni/ownership.h"
 
 namespace firebase {
 namespace firestore {
@@ -22,21 +24,23 @@ class ListenerRegistrationInternal {
  public:
   using ApiType = ListenerRegistration;
 
+  static void Initialize(jni::Loader& loader);
+
   // Global references will be created from the jobjects. The caller is
   // responsible for cleaning up any local references to jobjects after the
   // constructor returns.
   ListenerRegistrationInternal(FirestoreInternal* firestore,
                                EventListener* event_listener,
                                bool owning_event_listener,
-                               jobject listener_registration);
+                               const jni::Object& listener_registration);
   ListenerRegistrationInternal(FirestoreInternal* firestore,
                                EventListener* event_listener,
                                bool owning_event_listener,
-                               jobject listener_registration);
+                               const jni::Object& listener_registration);
   ListenerRegistrationInternal(FirestoreInternal* firestore,
                                EventListener* event_listener,
                                bool owning_event_listener,
-                               jobject listener_registration);
+                               const jni::Object& listener_registration);
 
   // Delete the default one to make the ownership more obvious i.e.
   // FirestoreInternal owns each instance and forbid anyone else to make copy.
@@ -57,13 +61,11 @@ class ListenerRegistrationInternal {
 
  private:
   friend class DocumentReferenceInternal;
-  friend class FirestoreInternal;
 
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  jni::Env GetEnv();
 
   FirestoreInternal* firestore_ = nullptr;  // not owning
-  jobject listener_registration_ = nullptr;
+  jni::Global listener_registration_;
 
   // May own it, see owning_event_listener_. If user pass in an EventListener
   // directly, then the registration does not own it. If user pass in a lambda,
diff --git a/firestore/src/android/metadata_changes_android.cc b/firestore/src/android/metadata_changes_android.cc
index 2fc5379318..1e3d07caf9 100644
--- a/firestore/src/android/metadata_changes_android.cc
+++ b/firestore/src/android/metadata_changes_android.cc
@@ -1,72 +1,38 @@
 #include "firestore/src/android/metadata_changes_android.h"
 
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-// clang-format off
-#define METADATA_CHANGES_METHODS(X) X(Name, "name", "()Ljava/lang/String;")
-#define METADATA_CHANGES_FIELDS(X)                                          \
-  X(Exclude, "EXCLUDE", "Lcom/google/firebase/firestore/MetadataChanges;",  \
-    util::kFieldTypeStatic),                                                \
-  X(Include, "INCLUDE", "Lcom/google/firebase/firestore/MetadataChanges;",  \
-    util::kFieldTypeStatic)
-// clang-format on
-METHOD_LOOKUP_DECLARATION(metadata_changes, METADATA_CHANGES_METHODS,
-                          METADATA_CHANGES_FIELDS)
-METHOD_LOOKUP_DEFINITION(metadata_changes,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/MetadataChanges",
-                         METADATA_CHANGES_METHODS, METADATA_CHANGES_FIELDS)
+using jni::Env;
+using jni::Local;
+using jni::Object;
+using jni::StaticField;
 
-jobject MetadataChangesInternal::exclude_ = nullptr;
-jobject MetadataChangesInternal::include_ = nullptr;
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/MetadataChanges";
+StaticField kExclude("EXCLUDE",
+                             "Lcom/google/firebase/firestore/MetadataChanges;");
+StaticField kInclude("INCLUDE",
+                             "Lcom/google/firebase/firestore/MetadataChanges;");
 
-/* static */
-jobject MetadataChangesInternal::ToJavaObject(
-    JNIEnv* env, MetadataChanges metadata_changes) {
-  if (metadata_changes == MetadataChanges::kExclude) {
-    return exclude_;
-  } else {
-    return include_;
-  }
-}
-
-/* static */
-bool MetadataChangesInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = metadata_changes::CacheMethodIds(env, activity) &&
-                metadata_changes::CacheFieldIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-
-  // Cache Java enum values.
-  jobject value = env->GetStaticObjectField(
-      metadata_changes::GetClass(),
-      metadata_changes::GetFieldId(metadata_changes::kExclude));
-  exclude_ = env->NewGlobalRef(value);
-  env->DeleteLocalRef(value);
+}  // namespace
 
-  value = env->GetStaticObjectField(
-      metadata_changes::GetClass(),
-      metadata_changes::GetFieldId(metadata_changes::kInclude));
-  include_ = env->NewGlobalRef(value);
-  env->DeleteLocalRef(value);
-
-  return result;
+void MetadataChangesInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kExclude, kInclude);
 }
 
-/* static */
-void MetadataChangesInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  metadata_changes::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-
-  // Uncache Java enum values.
-  env->DeleteGlobalRef(exclude_);
-  exclude_ = nullptr;
-  env->DeleteGlobalRef(include_);
-  include_ = nullptr;
+Local MetadataChangesInternal::Create(
+    Env& env, MetadataChanges metadata_changes) {
+  switch (metadata_changes) {
+    case MetadataChanges::kExclude:
+      return env.Get(kExclude);
+    case MetadataChanges::kInclude:
+      return env.Get(kInclude);
+  }
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/metadata_changes_android.h b/firestore/src/android/metadata_changes_android.h
index 8e2ecda649..2ec6180c7a 100644
--- a/firestore/src/android/metadata_changes_android.h
+++ b/firestore/src/android/metadata_changes_android.h
@@ -1,11 +1,8 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_METADATA_CHANGES_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_METADATA_CHANGES_ANDROID_H_
 
-#include 
-
-#include "app/src/include/firebase/app.h"
-#include "app/src/util_android.h"
 #include "firestore/src/include/firebase/firestore/metadata_changes.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
@@ -14,16 +11,10 @@ class MetadataChangesInternal {
  public:
   using ApiType = MetadataChanges;
 
-  static jobject ToJavaObject(JNIEnv* env, MetadataChanges metadata_changes);
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  static void Initialize(jni::Loader& loader);
 
-  static jobject exclude_;
-  static jobject include_;
+  static jni::Local Create(jni::Env& env,
+                                        MetadataChanges metadata_changes);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/promise_android.h b/firestore/src/android/promise_android.h
index de9b149a8a..7b63de4378 100644
--- a/firestore/src/android/promise_android.h
+++ b/firestore/src/android/promise_android.h
@@ -3,12 +3,15 @@
 
 #include 
 
+#include "app/memory/unique_ptr.h"
 #include "app/src/reference_counted_future_impl.h"
 #include "app/src/util_android.h"
 #include "firestore/src/android/document_snapshot_android.h"
-#include "firestore/src/android/firebase_firestore_exception_android.h"
+#include "firestore/src/android/exception_android.h"
 #include "firestore/src/android/firestore_android.h"
 #include "firestore/src/android/query_snapshot_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/object.h"
 
 namespace firebase {
 namespace firestore {
@@ -40,24 +43,43 @@ class Promise {
 
   Promise(ReferenceCountedFutureImpl* impl, FirestoreInternal* firestore,
           Completion* completion = nullptr)
-      : completer_{new Completer{impl, firestore,
-                                                           completion}},
+      : completer_(MakeUnique>(
+            impl, firestore, completion)),
         impl_(impl) {}
 
-  ~Promise() { delete completer_; }
+  ~Promise() {}
+
+  Promise(const Promise&) = delete;
+  Promise& operator=(const Promise&) = delete;
+
+  Promise(Promise&& other) = default;
+  Promise& operator=(Promise&& other) = default;
+
+  void RegisterForTask(FnEnumType op, const jni::Object& task) {
+    return RegisterForTask(op, task.get());
+  }
 
   void RegisterForTask(FnEnumType op, jobject task) {
     JNIEnv* env = completer_->firestore()->app()->GetJNIEnv();
     handle_ = completer_->Alloc(static_cast(op));
 
     // Ownership of the completer will pass to to RegisterCallbackOnTask
-    Completer* completer = completer_;
-    completer_ = nullptr;
+    auto* completer = completer_.release();
 
     util::RegisterCallbackOnTask(env, task, ResultCallback, completer,
                                  kApiIdentifier);
   }
 
+  void RegisterForTask(jni::Env& env, FnEnumType op, const jni::Object& task) {
+    handle_ = completer_->Alloc(static_cast(op));
+
+    // Ownership of the completer will pass to to RegisterCallbackOnTask
+    auto* completer = completer_.release();
+
+    util::RegisterCallbackOnTask(env.get(), task.get(), ResultCallback,
+                                 completer, kApiIdentifier);
+  }
+
   Future GetFuture() { return MakeFuture(impl_, handle_); }
 
  private:
@@ -77,15 +99,17 @@ class Promise {
       return handle_;
     }
 
-    virtual void CompleteWithResult(jobject result,
+    virtual void CompleteWithResult(jobject raw_result,
                                     util::FutureResult result_code,
                                     const char* status_message) {
       // result can be either the resolved object or exception, depending on
       // result_code.
+      jni::Env env;
+      jni::Object result(raw_result);
 
       if (result_code == util::kFutureResultSuccess) {
         // When succeeded, result is the resolved object of the Future.
-        SucceedWithResult(result);
+        SucceedWithResult(env, result);
         return;
       }
 
@@ -93,8 +117,7 @@ class Promise {
       switch (result_code) {
         case util::kFutureResultFailure:
           // When failed, result is the exception raised.
-          error_code = FirebaseFirestoreExceptionInternal::ToErrorCode(
-              this->firestore_->app()->GetJNIEnv(), result);
+          error_code = ExceptionInternal::GetErrorCode(env, result);
           break;
         case util::kFutureResultCancelled:
           error_code = Error::kErrorCancelled;
@@ -111,7 +134,8 @@ class Promise {
       delete this;
     }
 
-    virtual void SucceedWithResult(jobject result) = 0;
+    virtual void SucceedWithResult(jni::Env& env,
+                                   const jni::Object& result) = 0;
 
    protected:
     SafeFutureHandle handle_;
@@ -128,9 +152,9 @@ class Promise {
    public:
     using CompleterBase::CompleterBase;
 
-    void SucceedWithResult(jobject result) override {
+    void SucceedWithResult(jni::Env& env, const jni::Object& result) override {
       PublicT future_result = FirestoreInternal::Wrap(
-          new InternalT(this->firestore_, result));
+          new InternalT(this->firestore_, result.get()));
 
       this->impl_->CompleteWithResult(this->handle_, Error::kErrorOk,
                                       /*error_msg=*/"", future_result);
@@ -147,7 +171,7 @@ class Promise {
    public:
     using CompleterBase::CompleterBase;
 
-    void SucceedWithResult(jobject result) override {
+    void SucceedWithResult(jni::Env& env, const jni::Object& result) override {
       this->impl_->Complete(this->handle_, Error::kErrorOk, /*error_msg=*/"");
       if (this->completion_ != nullptr) {
         this->completion_->CompleteWith(Error::kErrorOk, /*error_message*/ "",
@@ -167,7 +191,7 @@ class Promise {
     }
   }
 
-  Completer* completer_;
+  UniquePtr> completer_;
 
   // Keep these values separate from the Completer in case completion happens
   // before the future is constructed.
diff --git a/firestore/src/android/promise_factory_android.h b/firestore/src/android/promise_factory_android.h
index 438f038da1..2826d63098 100644
--- a/firestore/src/android/promise_factory_android.h
+++ b/firestore/src/android/promise_factory_android.h
@@ -40,6 +40,15 @@ class PromiseFactory {
     return Promise{future_api(), firestore_};
   }
 
+  template >
+  Future NewFuture(jni::Env& env, EnumT op, const jni::Object& task) {
+    if (!env.ok()) return {};
+
+    auto promise = MakePromise();
+    promise.RegisterForTask(env, op, task);
+    return promise.GetFuture();
+  }
+
  private:
   // Gets the reference-counted Future implementation of this instance, which
   // can be used to create a Future.
diff --git a/firestore/src/android/query_android.cc b/firestore/src/android/query_android.cc
index 238c4086b1..933bf23896 100644
--- a/firestore/src/android/query_android.cc
+++ b/firestore/src/android/query_android.cc
@@ -9,178 +9,270 @@
 #include "firestore/src/android/field_value_android.h"
 #include "firestore/src/android/firestore_android.h"
 #include "firestore/src/android/lambda_event_listener.h"
+#include "firestore/src/android/listener_registration_android.h"
 #include "firestore/src/android/metadata_changes_android.h"
 #include "firestore/src/android/promise_android.h"
 #include "firestore/src/android/source_android.h"
-#include "firestore/src/android/util_android.h"
 #include "firestore/src/include/firebase/firestore.h"
+#include "firestore/src/jni/array_list.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
-
-METHOD_LOOKUP_DEFINITION(query,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/Query",
-                         QUERY_METHODS)
+namespace {
+
+using jni::Array;
+using jni::ArrayList;
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+
+constexpr char kClassName[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/Query";
+Method kEqualTo(
+    "whereEqualTo",
+    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kLessThan(
+    "whereLessThan",
+    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kLessThanOrEqualTo(
+    "whereLessThanOrEqualTo",
+    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kGreaterThan(
+    "whereGreaterThan",
+    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kGreaterThanOrEqualTo(
+    "whereGreaterThanOrEqualTo",
+    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kArrayContains(
+    "whereArrayContains",
+    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kArrayContainsAny(
+    "whereArrayContainsAny",
+    "(Lcom/google/firebase/firestore/FieldPath;Ljava/util/List;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kIn("whereIn",
+                   "(Lcom/google/firebase/firestore/FieldPath;Ljava/util/List;)"
+                   "Lcom/google/firebase/firestore/Query;");
+Method kOrderBy("orderBy",
+                        "(Lcom/google/firebase/firestore/FieldPath;"
+                        "Lcom/google/firebase/firestore/Query$Direction;)"
+                        "Lcom/google/firebase/firestore/Query;");
+Method kLimit("limit", "(J)Lcom/google/firebase/firestore/Query;");
+Method kLimitToLast("limitToLast",
+                            "(J)Lcom/google/firebase/firestore/Query;");
+Method kStartAtSnapshot(
+    "startAt",
+    "(Lcom/google/firebase/firestore/DocumentSnapshot;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kStartAt(
+    "startAt", "([Ljava/lang/Object;)Lcom/google/firebase/firestore/Query;");
+Method kStartAfterSnapshot(
+    "startAfter",
+    "(Lcom/google/firebase/firestore/DocumentSnapshot;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kStartAfter(
+    "startAfter", "([Ljava/lang/Object;)Lcom/google/firebase/firestore/Query;");
+Method kEndBeforeSnapshot(
+    "endBefore",
+    "(Lcom/google/firebase/firestore/DocumentSnapshot;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kEndBefore(
+    "endBefore", "([Ljava/lang/Object;)Lcom/google/firebase/firestore/Query;");
+Method kEndAtSnapshot(
+    "endAt",
+    "(Lcom/google/firebase/firestore/DocumentSnapshot;)"
+    "Lcom/google/firebase/firestore/Query;");
+Method kEndAt(
+    "endAt", "([Ljava/lang/Object;)Lcom/google/firebase/firestore/Query;");
+Method kGet("get",
+                    "(Lcom/google/firebase/firestore/Source;)"
+                    "Lcom/google/android/gms/tasks/Task;");
+Method kAddSnapshotListener(
+    "addSnapshotListener",
+    "(Ljava/util/concurrent/Executor;"
+    "Lcom/google/firebase/firestore/MetadataChanges;"
+    "Lcom/google/firebase/firestore/EventListener;)"
+    "Lcom/google/firebase/firestore/ListenerRegistration;");
+
+}  // namespace
+
+void QueryInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClassName, kEqualTo, kLessThan, kLessThanOrEqualTo,
+                   kGreaterThan, kGreaterThanOrEqualTo, kArrayContains,
+                   kArrayContainsAny, kIn, kOrderBy, kLimit, kLimitToLast,
+                   kStartAtSnapshot, kStartAt, kStartAfterSnapshot, kStartAfter,
+                   kEndBeforeSnapshot, kEndBefore, kEndAtSnapshot, kEndAt, kGet,
+                   kAddSnapshotListener);
+}
 
 Firestore* QueryInternal::firestore() {
   FIREBASE_ASSERT(firestore_->firestore_public() != nullptr);
   return firestore_->firestore_public();
 }
 
+Query QueryInternal::WhereEqualTo(const FieldPath& field,
+                                  const FieldValue& value) {
+  return Where(field, kEqualTo, value);
+}
+
+Query QueryInternal::WhereLessThan(const FieldPath& field,
+                                   const FieldValue& value) {
+  return Where(field, kLessThan, value);
+}
+
+Query QueryInternal::WhereLessThanOrEqualTo(const FieldPath& field,
+                                            const FieldValue& value) {
+  return Where(field, kLessThanOrEqualTo, value);
+}
+
+Query QueryInternal::WhereGreaterThan(const FieldPath& field,
+                                      const FieldValue& value) {
+  return Where(field, kGreaterThan, value);
+}
+
+Query QueryInternal::WhereGreaterThanOrEqualTo(const FieldPath& field,
+                                               const FieldValue& value) {
+  return Where(field, kGreaterThanOrEqualTo, value);
+}
+
+Query QueryInternal::WhereArrayContains(const FieldPath& field,
+                                        const FieldValue& value) {
+  return Where(field, kArrayContains, value);
+}
+
+Query QueryInternal::WhereArrayContainsAny(
+    const FieldPath& field, const std::vector& values) {
+  return Where(field, kArrayContainsAny, values);
+}
+
+Query QueryInternal::WhereIn(const FieldPath& field,
+                             const std::vector& values) {
+  return Where(field, kIn, values);
+}
+
 Query QueryInternal::OrderBy(const FieldPath& field,
                              Query::Direction direction) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject j_field = FieldPathConverter::ToJavaObject(env, field);
-  jobject j_direction = DirectionInternal::ToJavaObject(env, direction);
-  jobject query = env->CallObjectMethod(
-      obj_, query::GetMethodId(query::kOrderBy), j_field, j_direction);
-  CheckAndClearJniExceptions(env);
-  QueryInternal* internal = new QueryInternal{firestore_, query};
-  env->DeleteLocalRef(j_field);
-  env->DeleteLocalRef(query);
-
-  CheckAndClearJniExceptions(env);
-  return Query{internal};
+  Env env = GetEnv();
+  Local java_field = FieldPathConverter::Create(env, field);
+  Local java_direction = DirectionInternal::Create(env, direction);
+  Local query =
+      env.Call(obj_, kOrderBy, java_field.get(), java_direction.get());
+  return firestore_->NewQuery(env, query);
 }
 
 Query QueryInternal::Limit(int32_t limit) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  // Although the backend supports signed int32, Android client SDK uses long
-  // as parameter type. So we cast it to jlong instead of jint here.
-  jobject query = env->CallObjectMethod(obj_, query::GetMethodId(query::kLimit),
-                                        static_cast(limit));
-  CheckAndClearJniExceptions(env);
-  QueryInternal* internal = new QueryInternal{firestore_, query};
-  env->DeleteLocalRef(query);
+  Env env = GetEnv();
 
-  CheckAndClearJniExceptions(env);
-  return Query{internal};
+  // Although the backend only supports int32, the Android client SDK uses long
+  // as parameter type.
+  auto java_limit = static_cast(limit);
+  Local query = env.Call(obj_, kLimit, java_limit);
+  return firestore_->NewQuery(env, query);
 }
 
 Query QueryInternal::LimitToLast(int32_t limit) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  // Although the backend supports signed int32, Android client SDK uses long
-  // as parameter type. So we cast it to jlong instead of jint here.
-  jobject query = env->CallObjectMethod(
-      obj_, query::GetMethodId(query::kLimitToLast), static_cast(limit));
-  CheckAndClearJniExceptions(env);
-  QueryInternal* internal = new QueryInternal{firestore_, query};
-  env->DeleteLocalRef(query);
+  Env env = GetEnv();
 
-  CheckAndClearJniExceptions(env);
-  return Query{internal};
+  // Although the backend only supports int32, the Android client SDK uses long
+  // as parameter type.
+  auto java_limit = static_cast(limit);
+  Local query = env.Call(obj_, kLimitToLast, java_limit);
+  return firestore_->NewQuery(env, query);
 }
 
-Future QueryInternal::Get(Source source) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject task =
-      env->CallObjectMethod(obj_, query::GetMethodId(query::kGet),
-                            SourceInternal::ToJavaObject(env, source));
-  CheckAndClearJniExceptions(env);
-
-  auto promise = promises_.MakePromise();
-  promise.RegisterForTask(QueryFn::kGet, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
-}
-
-/* static */
-bool QueryInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = query::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
+Query QueryInternal::StartAt(const DocumentSnapshot& snapshot) {
+  return WithBound(kStartAtSnapshot, snapshot);
 }
 
-/* static */
-void QueryInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  query::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+Query QueryInternal::StartAt(const std::vector& values) {
+  return WithBound(kStartAt, values);
 }
 
-Query QueryInternal::Where(const FieldPath& field, query::Method method,
-                           const FieldValue& value) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject path = FieldPathConverter::ToJavaObject(env, field);
-  jobject query = env->CallObjectMethod(obj_, query::GetMethodId(method), path,
-                                        value.internal_->java_object());
-  CheckAndClearJniExceptions(env);
-  QueryInternal* internal = new QueryInternal{firestore_, query};
-  env->DeleteLocalRef(path);
-  env->DeleteLocalRef(query);
+Query QueryInternal::StartAfter(const DocumentSnapshot& snapshot) {
+  return WithBound(kStartAfterSnapshot, snapshot);
+}
 
-  CheckAndClearJniExceptions(env);
-  return Query{internal};
+Query QueryInternal::StartAfter(const std::vector& values) {
+  return WithBound(kStartAfter, values);
 }
 
-Query QueryInternal::Where(const FieldPath& field, query::Method method,
-                           const std::vector& values) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  // Convert std::vector into java.util.List object.
-  // TODO(chenbrian): Refactor this into a helper function.
-  jobject converted_values = env->NewObject(
-      util::array_list::GetClass(),
-      util::array_list::GetMethodId(util::array_list::kConstructor));
-  jmethodID add_method = util::array_list::GetMethodId(util::array_list::kAdd);
-  jsize size = static_cast(values.size());
-  for (jsize i = 0; i < size; ++i) {
-    // ArrayList.Add() always returns true, which we have no use for.
-    env->CallBooleanMethod(converted_values, add_method,
-                           values[i].internal_->java_object());
-    CheckAndClearJniExceptions(env);
-  }
+Query QueryInternal::EndBefore(const DocumentSnapshot& snapshot) {
+  return WithBound(kEndBeforeSnapshot, snapshot);
+}
+
+Query QueryInternal::EndBefore(const std::vector& values) {
+  return WithBound(kEndBefore, values);
+}
 
-  jobject path = FieldPathConverter::ToJavaObject(env, field);
-  jobject query = env->CallObjectMethod(obj_, query::GetMethodId(method), path,
-                                        converted_values);
-  CheckAndClearJniExceptions(env);
-  QueryInternal* internal = new QueryInternal{firestore_, query};
-  env->DeleteLocalRef(path);
-  env->DeleteLocalRef(query);
-  env->DeleteLocalRef(converted_values);
+Query QueryInternal::EndAt(const DocumentSnapshot& snapshot) {
+  return WithBound(kEndAtSnapshot, snapshot);
+}
 
-  return Query{internal};
+Query QueryInternal::EndAt(const std::vector& values) {
+  return WithBound(kEndAt, values);
 }
 
-Query QueryInternal::WithBound(query::Method method,
-                               const DocumentSnapshot& snapshot) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject query = env->CallObjectMethod(obj_, query::GetMethodId(method),
-                                        snapshot.internal_->java_object());
-  CheckAndClearJniExceptions(env);
-  QueryInternal* internal = new QueryInternal{firestore_, query};
-  env->DeleteLocalRef(query);
+Future QueryInternal::Get(Source source) {
+  Env env = GetEnv();
+  Local java_source = SourceInternal::Create(env, source);
+  Local task = env.Call(obj_, kGet, java_source);
+  return promises_.NewFuture(env, AsyncFn::kGet, task);
+}
 
-  CheckAndClearJniExceptions(env);
-  return Query{internal};
+Query QueryInternal::Where(const FieldPath& field, const Method& method,
+                           const FieldValue& value) {
+  Env env = GetEnv();
+  Local java_field = FieldPathConverter::Create(env, field);
+  Local query =
+      env.Call(obj_, method, java_field, value.internal_->ToJava());
+  return firestore_->NewQuery(env, query);
 }
 
-Query QueryInternal::WithBound(query::Method method,
-                               const std::vector& values) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
+Query QueryInternal::Where(const FieldPath& field, const Method& method,
+                           const std::vector& values) {
+  Env env = GetEnv();
+
+  size_t size = values.size();
+  Local java_values = ArrayList::Create(env, size);
+  for (size_t i = 0; i < size; ++i) {
+    java_values.Add(env, values[i].internal_->ToJava());
+  }
+
+  Local java_field = FieldPathConverter::Create(env, field);
+  Local query = env.Call(obj_, method, java_field, java_values);
+  return firestore_->NewQuery(env, query);
+}
 
-  jobjectArray converted_values = ConvertFieldValues(env, values);
-  jobject query =
-      env->CallObjectMethod(obj_, query::GetMethodId(method), converted_values);
-  CheckAndClearJniExceptions(env);
-  env->DeleteLocalRef(converted_values);
-  QueryInternal* internal = new QueryInternal{firestore_, query};
-  env->DeleteLocalRef(query);
+Query QueryInternal::WithBound(const Method& method,
+                               const DocumentSnapshot& snapshot) {
+  Env env = GetEnv();
+  Local query = env.Call(obj_, method, snapshot.internal_->ToJava());
+  return firestore_->NewQuery(env, query);
+}
 
-  CheckAndClearJniExceptions(env);
-  return Query{internal};
+Query QueryInternal::WithBound(const Method& method,
+                               const std::vector& values) {
+  Env env = GetEnv();
+  Local> java_values = ConvertFieldValues(env, values);
+  Local query = env.Call(obj_, method, java_values);
+  return firestore_->NewQuery(env, query);
 }
 
 #if defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
 ListenerRegistration QueryInternal::AddSnapshotListener(
     MetadataChanges metadata_changes,
-    std::function callback) {
-  LambdaEventListener* listener =
+    std::function
+        callback) {
+  auto* listener =
       new LambdaEventListener(firebase::Move(callback));
   return AddSnapshotListener(metadata_changes, listener,
                              /*passing_listener_ownership=*/true);
@@ -190,45 +282,35 @@ ListenerRegistration QueryInternal::AddSnapshotListener(
 ListenerRegistration QueryInternal::AddSnapshotListener(
     MetadataChanges metadata_changes, EventListener* listener,
     bool passing_listener_ownership) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  // Create listener.
-  jobject java_listener =
-      EventListenerInternal::EventListenerToJavaEventListener(env, firestore_,
-                                                              listener);
-  jobject java_metadata =
-      MetadataChangesInternal::ToJavaObject(env, metadata_changes);
-
-  // Register listener.
-  jobject java_registration = env->CallObjectMethod(
-      obj_, query::GetMethodId(query::kAddSnapshotListener),
-      firestore_->user_callback_executor(), java_metadata, java_listener);
-  env->DeleteLocalRef(java_listener);
-  CheckAndClearJniExceptions(env);
-
-  // Wrapping
-  ListenerRegistrationInternal* registration = new ListenerRegistrationInternal{
-      firestore_, listener, passing_listener_ownership, java_registration};
-  env->DeleteLocalRef(java_registration);
-
-  return ListenerRegistration{registration};
-}
-
-jobjectArray QueryInternal::ConvertFieldValues(
-    JNIEnv* env, const std::vector& field_values) {
-  jsize size = static_cast(field_values.size());
-  jobjectArray result = env->NewObjectArray(size, util::object::GetClass(),
-                                            /*initialElement=*/nullptr);
-  for (jsize i = 0; i < size; ++i) {
-    env->SetObjectArrayElement(result, i,
-                               field_values[i].internal_->java_object());
-  }
+  Env env = GetEnv();
+
+  Local java_listener =
+      EventListenerInternal::Create(env, firestore_, listener);
+  Local java_metadata =
+      MetadataChangesInternal::Create(env, metadata_changes);
+
+  Local java_registration =
+      env.Call(obj_, kAddSnapshotListener, firestore_->user_callback_executor(),
+               java_metadata, java_listener);
 
+  if (!env.ok()) return {};
+  return ListenerRegistration(new ListenerRegistrationInternal(
+      firestore_, listener, passing_listener_ownership, java_registration));
+}
+
+Local> QueryInternal::ConvertFieldValues(
+    Env& env, const std::vector& field_values) {
+  size_t size = field_values.size();
+  Local> result = env.NewArray(size, Object::GetClass());
+  for (size_t i = 0; i < size; ++i) {
+    result.Set(env, i, field_values[i].internal_->ToJava());
+  }
   return result;
 }
 
 bool operator==(const QueryInternal& lhs, const QueryInternal& rhs) {
-  return lhs.EqualsJavaObject(rhs);
+  Env env = FirestoreInternal::GetEnv();
+  return Object::Equals(env, lhs.ToJava(), rhs.ToJava());
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/query_android.h b/firestore/src/android/query_android.h
index 8006c0c6ca..4f8f1ee2cb 100644
--- a/firestore/src/android/query_android.h
+++ b/firestore/src/android/query_android.h
@@ -6,7 +6,6 @@
 #include 
 
 #include "app/src/reference_counted_future_impl.h"
-#include "app/src/util_android.h"
 #include "firestore/src/android/promise_factory_android.h"
 #include "firestore/src/android/wrapper.h"
 #include "firestore/src/include/firebase/firestore/field_path.h"
@@ -17,89 +16,26 @@ namespace firestore {
 
 class Firestore;
 
-// Each API of Query that returns a Future needs to define an enum value here.
-// For example, a Future-returning method Foo() relies on the enum value kFoo.
-// The enum values are used to identify and manage Future in the Firestore
-// Future manager.
-enum class QueryFn {
-  // Enum values for the baseclass Query.
-  kGet = 0,
+class QueryInternal : public Wrapper {
+ public:
+  using ApiType = Query;
 
-  // Enum values below are for the subclass CollectionReference.
-  kAdd,
+  // Each API of Query that returns a Future needs to define an enum value here.
+  // For example, a Future-returning method Foo() relies on the enum value kFoo.
+  // The enum values are used to identify and manage Future in the Firestore
+  // Future manager.
+  enum class AsyncFn {
+    // Enum values for the baseclass Query.
+    kGet = 0,
 
-  // Must be the last enum value.
-  kCount,
-};
+    // Enum values below are for the subclass CollectionReference.
+    kAdd,
 
-// clang-format off
-#define QUERY_METHODS(X)                                            \
-  X(EqualTo, "whereEqualTo",                                        \
-    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)" \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(LessThan, "whereLessThan",                                      \
-    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)" \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(LessThanOrEqualTo, "whereLessThanOrEqualTo",                    \
-    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)" \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(GreaterThan, "whereGreaterThan",                                \
-    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)" \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(GreaterThanOrEqualTo, "whereGreaterThanOrEqualTo",              \
-    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)" \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(ArrayContains, "whereArrayContains",                            \
-    "(Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;)" \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(ArrayContainsAny, "whereArrayContainsAny",                      \
-    "(Lcom/google/firebase/firestore/FieldPath;Ljava/util/List;)"   \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(In, "whereIn",                                                  \
-    "(Lcom/google/firebase/firestore/FieldPath;Ljava/util/List;)"   \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(OrderBy, "orderBy",                                             \
-    "(Lcom/google/firebase/firestore/FieldPath;"                    \
-    "Lcom/google/firebase/firestore/Query$Direction;)"              \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(Limit, "limit", "(J)Lcom/google/firebase/firestore/Query;"),    \
-  X(LimitToLast, "limitToLast",                                     \
-      "(J)Lcom/google/firebase/firestore/Query;"),                  \
-  X(StartAtSnapshot, "startAt",                                     \
-    "(Lcom/google/firebase/firestore/DocumentSnapshot;)"            \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(StartAt, "startAt",                                             \
-    "([Ljava/lang/Object;)Lcom/google/firebase/firestore/Query;"),  \
-  X(StartAfterSnapshot, "startAfter",                               \
-    "(Lcom/google/firebase/firestore/DocumentSnapshot;)"            \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(StartAfter, "startAfter",                                       \
-    "([Ljava/lang/Object;)Lcom/google/firebase/firestore/Query;"),  \
-  X(EndBeforeSnapshot, "endBefore",                                 \
-    "(Lcom/google/firebase/firestore/DocumentSnapshot;)"            \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(EndBefore, "endBefore",                                         \
-    "([Ljava/lang/Object;)Lcom/google/firebase/firestore/Query;"),  \
-  X(EndAtSnapshot, "endAt",                                         \
-    "(Lcom/google/firebase/firestore/DocumentSnapshot;)"            \
-    "Lcom/google/firebase/firestore/Query;"),                       \
-  X(EndAt, "endAt",                                                 \
-    "([Ljava/lang/Object;)Lcom/google/firebase/firestore/Query;"),  \
-  X(Get, "get",                                                     \
-    "(Lcom/google/firebase/firestore/Source;)"                      \
-    "Lcom/google/android/gms/tasks/Task;"),                         \
-  X(AddSnapshotListener, "addSnapshotListener",                     \
-    "(Ljava/util/concurrent/Executor;"                              \
-    "Lcom/google/firebase/firestore/MetadataChanges;"               \
-    "Lcom/google/firebase/firestore/EventListener;)"                \
-    "Lcom/google/firebase/firestore/ListenerRegistration;")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(query, QUERY_METHODS)
+    // Must be the last enum value.
+    kCount,
+  };
 
-class QueryInternal : public Wrapper {
- public:
-  using ApiType = Query;
+  static void Initialize(jni::Loader& loader);
 
   QueryInternal(FirestoreInternal* firestore, jobject object)
       : Wrapper(firestore, object), promises_(firestore) {}
@@ -117,9 +53,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query WhereEqualTo(const FieldPath& field, const FieldValue& value) {
-    return Where(field, query::kEqualTo, value);
-  }
+  Query WhereEqualTo(const FieldPath& field, const FieldValue& value);
 
   /**
    * @brief Creates and returns a new Query with the additional filter that
@@ -131,9 +65,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query WhereLessThan(const FieldPath& field, const FieldValue& value) {
-    return Where(field, query::kLessThan, value);
-  }
+  Query WhereLessThan(const FieldPath& field, const FieldValue& value);
 
   /**
    * @brief Creates and returns a new Query with the additional filter that
@@ -145,10 +77,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query WhereLessThanOrEqualTo(const FieldPath& field,
-                               const FieldValue& value) {
-    return Where(field, query::kLessThanOrEqualTo, value);
-  }
+  Query WhereLessThanOrEqualTo(const FieldPath& field, const FieldValue& value);
 
   /**
    * @brief Creates and returns a new Query with the additional filter that
@@ -160,9 +89,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query WhereGreaterThan(const FieldPath& field, const FieldValue& value) {
-    return Where(field, query::kGreaterThan, value);
-  }
+  Query WhereGreaterThan(const FieldPath& field, const FieldValue& value);
 
   /**
    * @brief Creates and returns a new Query with the additional filter that
@@ -175,9 +102,7 @@ class QueryInternal : public Wrapper {
    * @return The created Query.
    */
   Query WhereGreaterThanOrEqualTo(const FieldPath& field,
-                                  const FieldValue& value) {
-    return Where(field, query::kGreaterThanOrEqualTo, value);
-  }
+                                  const FieldValue& value);
 
   /**
    * @brief Creates and returns a new Query with the additional filter that
@@ -191,9 +116,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query WhereArrayContains(const FieldPath& field, const FieldValue& value) {
-    return Where(field, query::kArrayContains, value);
-  }
+  Query WhereArrayContains(const FieldPath& field, const FieldValue& value);
 
   /**
    * @brief Creates and returns a new Query with the additional filter that
@@ -209,9 +132,7 @@ class QueryInternal : public Wrapper {
    * @return The created Query.
    */
   Query WhereArrayContainsAny(const FieldPath& field,
-                              const std::vector& values) {
-    return Where(field, query::kArrayContainsAny, values);
-  }
+                              const std::vector& values);
 
   /**
    * @brief Creates and returns a new Query with the additional filter that
@@ -226,9 +147,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query WhereIn(const FieldPath& field, const std::vector& values) {
-    return Where(field, query::kIn, values);
-  }
+  Query WhereIn(const FieldPath& field, const std::vector& values);
 
   /**
    * @brief Creates and returns a new Query that's additionally sorted by the
@@ -273,9 +192,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query StartAt(const DocumentSnapshot& snapshot) {
-    return WithBound(query::kStartAtSnapshot, snapshot);
-  }
+  Query StartAt(const DocumentSnapshot& snapshot);
 
   /**
    * @brief Creates and returns a new Query that starts at the provided fields
@@ -287,9 +204,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query StartAt(const std::vector& values) {
-    return WithBound(query::kStartAt, values);
-  }
+  Query StartAt(const std::vector& values);
 
   /**
    * @brief Creates and returns a new Query that starts after the provided
@@ -301,9 +216,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query StartAfter(const DocumentSnapshot& snapshot) {
-    return WithBound(query::kStartAfterSnapshot, snapshot);
-  }
+  Query StartAfter(const DocumentSnapshot& snapshot);
 
   /**
    * @brief Creates and returns a new Query that starts after the provided
@@ -315,9 +228,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query StartAfter(const std::vector& values) {
-    return WithBound(query::kStartAfter, values);
-  }
+  Query StartAfter(const std::vector& values);
 
   /**
    * @brief Creates and returns a new Query that ends before the provided
@@ -329,9 +240,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query EndBefore(const DocumentSnapshot& snapshot) {
-    return WithBound(query::kEndBeforeSnapshot, snapshot);
-  }
+  Query EndBefore(const DocumentSnapshot& snapshot);
 
   /**
    * @brief Creates and returns a new Query that ends before the provided fields
@@ -343,9 +252,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query EndBefore(const std::vector& values) {
-    return WithBound(query::kEndBefore, values);
-  }
+  Query EndBefore(const std::vector& values);
 
   /**
    * @brief Creates and returns a new Query that ends at the provided document
@@ -357,9 +264,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query EndAt(const DocumentSnapshot& snapshot) {
-    return WithBound(query::kEndAtSnapshot, snapshot);
-  }
+  Query EndAt(const DocumentSnapshot& snapshot);
 
   /**
    * @brief Creates and returns a new Query that ends at the provided fields
@@ -371,9 +276,7 @@ class QueryInternal : public Wrapper {
    *
    * @return The created Query.
    */
-  Query EndAt(const std::vector& values) {
-    return WithBound(query::kEndAt, values);
-  }
+  Query EndAt(const std::vector& values);
 
   /**
    * @brief Executes the query and returns the results as a QuerySnapshot.
@@ -406,7 +309,8 @@ class QueryInternal : public Wrapper {
    */
   ListenerRegistration AddSnapshotListener(
       MetadataChanges metadata_changes,
-      std::function callback);
+      std::function
+          callback);
 
 #endif  // defined(FIREBASE_USE_STD_FUNCTION)
 
@@ -429,27 +333,26 @@ class QueryInternal : public Wrapper {
       bool passing_listener_ownership = false);
 
  protected:
-  PromiseFactory promises_;
+  PromiseFactory promises_;
 
  private:
   friend class FirestoreInternal;
 
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
-
   // A generalized function for all WhereFoo calls.
-  Query Where(const FieldPath& field, query::Method method,
+  Query Where(const FieldPath& field, const jni::Method& method,
               const FieldValue& value);
-  Query Where(const FieldPath& field, query::Method method,
+  Query Where(const FieldPath& field, const jni::Method& method,
               const std::vector& values);
 
   // A generalized function for all {Start|End}{Before|After|At} calls.
-  Query WithBound(query::Method method, const DocumentSnapshot& snapshot);
-  Query WithBound(query::Method method, const std::vector& values);
+  Query WithBound(const jni::Method& method,
+                  const DocumentSnapshot& snapshot);
+  Query WithBound(const jni::Method& method,
+                  const std::vector& values);
 
   // A helper function to convert std::vector to Java FieldValue[].
-  jobjectArray ConvertFieldValues(JNIEnv* env,
-                                  const std::vector& field_values);
+  jni::Local> ConvertFieldValues(
+      jni::Env& env, const std::vector& field_values);
 };
 
 bool operator==(const QueryInternal& lhs, const QueryInternal& rhs);
diff --git a/firestore/src/android/query_snapshot_android.cc b/firestore/src/android/query_snapshot_android.cc
index 36b43e72e8..6e37a31702 100644
--- a/firestore/src/android/query_snapshot_android.cc
+++ b/firestore/src/android/query_snapshot_android.cc
@@ -3,107 +3,71 @@
 #include 
 
 #include "app/src/assert.h"
-#include "app/src/util_android.h"
 #include "firestore/src/android/document_change_android.h"
 #include "firestore/src/android/document_snapshot_android.h"
 #include "firestore/src/android/metadata_changes_android.h"
 #include "firestore/src/android/query_android.h"
 #include "firestore/src/android/snapshot_metadata_android.h"
-#include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
-
-// clang-format off
-#define QUERY_SNAPSHOT_METHODS(X)                                          \
-  X(Query, "getQuery", "()Lcom/google/firebase/firestore/Query;"),         \
-  X(Metadata, "getMetadata",                                               \
-    "()Lcom/google/firebase/firestore/SnapshotMetadata;"),                 \
-  X(DocumentChanges, "getDocumentChanges",                                 \
-    "(Lcom/google/firebase/firestore/MetadataChanges;)Ljava/util/List;"),  \
-  X(Documents, "getDocuments", "()Ljava/util/List;"),                      \
-  X(Size, "size", "()I")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(query_snapshot, QUERY_SNAPSHOT_METHODS)
-METHOD_LOOKUP_DEFINITION(query_snapshot,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/QuerySnapshot",
-                         QUERY_SNAPSHOT_METHODS)
+namespace {
+
+using jni::Env;
+using jni::List;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+
+constexpr char kClassName[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/QuerySnapshot";
+Method kGetQuery("getQuery", "()Lcom/google/firebase/firestore/Query;");
+Method kGetMetadata(
+    "getMetadata", "()Lcom/google/firebase/firestore/SnapshotMetadata;");
+Method kGetDocumentChanges(
+    "getDocumentChanges",
+    "(Lcom/google/firebase/firestore/MetadataChanges;)Ljava/util/List;");
+Method kGetDocuments("getDocuments", "()Ljava/util/List;");
+Method kSize("size", "()I");
+
+}  // namespace
+
+void QuerySnapshotInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClassName, kGetQuery, kGetMetadata, kGetDocumentChanges,
+                   kGetDocuments, kSize);
+}
 
 Query QuerySnapshotInternal::query() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject query = env->CallObjectMethod(
-      obj_, query_snapshot::GetMethodId(query_snapshot::kQuery));
-  QueryInternal* internal = new QueryInternal{firestore_, query};
-  env->DeleteLocalRef(query);
-
-  CheckAndClearJniExceptions(env);
-  return Query{internal};
+  Env env = GetEnv();
+  Local query = env.Call(obj_, kGetQuery);
+  return firestore_->NewQuery(env, query);
 }
 
 SnapshotMetadata QuerySnapshotInternal::metadata() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject metadata = env->CallObjectMethod(
-      obj_, query_snapshot::GetMethodId(query_snapshot::kMetadata));
-  SnapshotMetadata result =
-      SnapshotMetadataInternal::JavaSnapshotMetadataToSnapshotMetadata(
-          env, metadata);
-
-  CheckAndClearJniExceptions(env);
-  return result;
+  Env env = GetEnv();
+  return env.Call(obj_, kGetMetadata).ToPublic(env);
 }
 
 std::vector QuerySnapshotInternal::DocumentChanges(
     MetadataChanges metadata_changes) const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject j_metadata_changes =
-      MetadataChangesInternal::ToJavaObject(env, metadata_changes);
-  jobject change_list = env->CallObjectMethod(
-      obj_, query_snapshot::GetMethodId(query_snapshot::kDocumentChanges),
-      j_metadata_changes);
-  CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  auto java_metadata = MetadataChangesInternal::Create(env, metadata_changes);
 
-  std::vector result;
-  JavaListToStdVector(firestore_, change_list, &result);
-  return result;
+  Local change_list = env.Call(obj_, kGetDocumentChanges, java_metadata);
+  return MakeVector(env, change_list);
 }
 
 std::vector QuerySnapshotInternal::documents() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject document_list = env->CallObjectMethod(
-      obj_, query_snapshot::GetMethodId(query_snapshot::kDocuments));
-  CheckAndClearJniExceptions(env);
-
-  std::vector result;
-  JavaListToStdVector(firestore_, document_list, &result);
-  env->DeleteLocalRef(document_list);
-  return result;
+  Env env = GetEnv();
+  Local document_list = env.Call(obj_, kGetDocuments);
+  return MakeVector(env, document_list);
 }
 
 std::size_t QuerySnapshotInternal::size() const {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jint result = env->CallIntMethod(
-      obj_, query_snapshot::GetMethodId(query_snapshot::kSize));
-
-  CheckAndClearJniExceptions(env);
-  return static_cast(result);
-}
-
-/* static */
-bool QuerySnapshotInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = query_snapshot::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void QuerySnapshotInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  query_snapshot::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  return env.Call(obj_, kSize);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/query_snapshot_android.h b/firestore/src/android/query_snapshot_android.h
index 51f0261413..2a9f8dbfbb 100644
--- a/firestore/src/android/query_snapshot_android.h
+++ b/firestore/src/android/query_snapshot_android.h
@@ -10,6 +10,7 @@
 #include "firestore/src/include/firebase/firestore/query.h"
 #include "firestore/src/include/firebase/firestore/query_snapshot.h"
 #include "firestore/src/include/firebase/firestore/snapshot_metadata.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
@@ -19,6 +20,8 @@ class QuerySnapshotInternal : public Wrapper {
   using ApiType = QuerySnapshot;
   using Wrapper::Wrapper;
 
+  static void Initialize(jni::Loader& loader);
+
   /**
    * @brief The query from which you get this QuerySnapshot.
    */
@@ -59,12 +62,6 @@ class QuerySnapshotInternal : public Wrapper {
    * @return The number of documents in the QuerySnapshot.
    */
   std::size_t size() const;
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/server_timestamp_behavior_android.cc b/firestore/src/android/server_timestamp_behavior_android.cc
index cf1330c4be..cfca646c69 100644
--- a/firestore/src/android/server_timestamp_behavior_android.cc
+++ b/firestore/src/android/server_timestamp_behavior_android.cc
@@ -1,94 +1,52 @@
 #include "firestore/src/android/server_timestamp_behavior_android.h"
 
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
 namespace firebase {
 namespace firestore {
+namespace {
 
-using ServerTimestampBehavior = DocumentSnapshot::ServerTimestampBehavior;
-
-// clang-format off
-#define SERVER_TIMESTAMP_BEHAVIOR_METHODS(X)                \
-  X(Name, "name", "()Ljava/lang/String;")
-#define SERVER_TIMESTAMP_BEHAVIOR_FIELDS(X)                 \
-  X(None, "NONE",                                           \
-    "Lcom/google/firebase/firestore/DocumentSnapshot$"      \
-    "ServerTimestampBehavior;",                             \
-    util::kFieldTypeStatic),                                \
-  X(Estimate, "ESTIMATE",                                   \
-    "Lcom/google/firebase/firestore/DocumentSnapshot$"      \
-    "ServerTimestampBehavior;",                             \
-    util::kFieldTypeStatic),                                \
-  X(Previous, "PREVIOUS",                                   \
-    "Lcom/google/firebase/firestore/DocumentSnapshot$"      \
-    "ServerTimestampBehavior;",                             \
-    util::kFieldTypeStatic),                                \
-  X(Default, "DEFAULT",                                     \
-    "Lcom/google/firebase/firestore/DocumentSnapshot$"      \
-    "ServerTimestampBehavior;",                             \
-    util::kFieldTypeStatic)
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(server_timestamp_behavior,
-                          SERVER_TIMESTAMP_BEHAVIOR_METHODS,
-                          SERVER_TIMESTAMP_BEHAVIOR_FIELDS)
-METHOD_LOOKUP_DEFINITION(server_timestamp_behavior,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/DocumentSnapshot$"
-                         "ServerTimestampBehavior",
-                         SERVER_TIMESTAMP_BEHAVIOR_METHODS,
-                         SERVER_TIMESTAMP_BEHAVIOR_FIELDS)
-
-std::map*
-    ServerTimestampBehaviorInternal::cpp_enum_to_java_ = nullptr;
-
-/* static */
-jobject ServerTimestampBehaviorInternal::ToJavaObject(
-    JNIEnv* env, ServerTimestampBehavior stb) {
-  return (*cpp_enum_to_java_)[stb];
-}
-
-/* static */
-bool ServerTimestampBehaviorInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = server_timestamp_behavior::CacheMethodIds(env, activity) &&
-                server_timestamp_behavior::CacheFieldIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
+using jni::Env;
+using jni::Local;
+using jni::Object;
+using jni::StaticField;
 
-  // Cache Java enum values.
-  cpp_enum_to_java_ = new std::map();
-  const auto add_enum = [env](ServerTimestampBehavior stb,
-                              server_timestamp_behavior::Field field) {
-    jobject value =
-        env->GetStaticObjectField(server_timestamp_behavior::GetClass(),
-                                  server_timestamp_behavior::GetFieldId(field));
-    (*cpp_enum_to_java_)[stb] = env->NewGlobalRef(value);
-    env->DeleteLocalRef(value);
-    util::CheckAndClearJniExceptions(env);
-  };
-  add_enum(ServerTimestampBehavior::kDefault,
-           server_timestamp_behavior::kDefault);
-  add_enum(ServerTimestampBehavior::kNone, server_timestamp_behavior::kNone);
-  add_enum(ServerTimestampBehavior::kEstimate,
-           server_timestamp_behavior::kEstimate);
-  add_enum(ServerTimestampBehavior::kPrevious,
-           server_timestamp_behavior::kPrevious);
+using ServerTimestampBehavior = DocumentSnapshot::ServerTimestampBehavior;
 
-  return result;
+constexpr char kClass[] = PROGUARD_KEEP_CLASS
+    "com/google/firebase/firestore/DocumentSnapshot$ServerTimestampBehavior";
+
+StaticField kNone(
+    "NONE",
+    "Lcom/google/firebase/firestore/DocumentSnapshot$ServerTimestampBehavior;");
+StaticField kEstimate(
+    "ESTIMATE",
+    "Lcom/google/firebase/firestore/DocumentSnapshot$ServerTimestampBehavior;");
+StaticField kPrevious(
+    "PREVIOUS",
+    "Lcom/google/firebase/firestore/DocumentSnapshot$ServerTimestampBehavior;");
+
+}  // namespace
+
+Local ServerTimestampBehaviorInternal::Create(
+    Env& env, ServerTimestampBehavior stb) {
+  static_assert(
+      ServerTimestampBehavior::kDefault == ServerTimestampBehavior::kNone,
+      "default should be the same as none");
+
+  switch (stb) {
+    case ServerTimestampBehavior::kNone:
+      return env.Get(kNone);
+    case ServerTimestampBehavior::kEstimate:
+      return env.Get(kEstimate);
+    case ServerTimestampBehavior::kPrevious:
+      return env.Get(kPrevious);
+  }
 }
 
-/* static */
-void ServerTimestampBehaviorInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  server_timestamp_behavior::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-
-  // Uncache Java enum values.
-  for (auto& kv : *cpp_enum_to_java_) {
-    env->DeleteGlobalRef(kv.second);
-  }
-  util::CheckAndClearJniExceptions(env);
-  delete cpp_enum_to_java_;
-  cpp_enum_to_java_ = nullptr;
+void ServerTimestampBehaviorInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kNone, kEstimate, kPrevious);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/server_timestamp_behavior_android.h b/firestore/src/android/server_timestamp_behavior_android.h
index dddfbcb911..638ff2f5d7 100644
--- a/firestore/src/android/server_timestamp_behavior_android.h
+++ b/firestore/src/android/server_timestamp_behavior_android.h
@@ -1,28 +1,18 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SERVER_TIMESTAMP_BEHAVIOR_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SERVER_TIMESTAMP_BEHAVIOR_ANDROID_H_
 
-#include 
-
-#include "app/src/include/firebase/app.h"
-#include "app/src/util_android.h"
 #include "firestore/src/include/firebase/firestore/document_snapshot.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
 
 class ServerTimestampBehaviorInternal {
  public:
-  static jobject ToJavaObject(JNIEnv* env,
-                              DocumentSnapshot::ServerTimestampBehavior stb);
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  static void Initialize(jni::Loader& loader);
 
-  static std::map*
-      cpp_enum_to_java_;
+  static jni::Local Create(
+      jni::Env& env, DocumentSnapshot::ServerTimestampBehavior stb);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/set_options_android.cc b/firestore/src/android/set_options_android.cc
index 96287d04cd..fbf8bb256a 100644
--- a/firestore/src/android/set_options_android.cc
+++ b/firestore/src/android/set_options_android.cc
@@ -7,100 +7,63 @@
 #include "app/src/util_android.h"
 #include "firestore/src/android/field_path_android.h"
 #include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/array_list.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
+
+using jni::ArrayList;
+using jni::Env;
+using jni::Local;
+using jni::Object;
+using jni::StaticField;
+using jni::StaticMethod;
 
 using Type = SetOptions::Type;
 
-// clang-format off
-#define SET_OPTIONS_METHODS(X)                                                 \
-  X(Merge, "merge",                                                            \
-    "()Lcom/google/firebase/firestore/SetOptions;",                            \
-    firebase::util::kMethodTypeStatic),                                        \
-  X(MergeFieldPaths, "mergeFieldPaths",                                        \
-    "(Ljava/util/List;)Lcom/google/firebase/firestore/SetOptions;",            \
-    firebase::util::kMethodTypeStatic)
-#define SET_OPTIONS_FIELDS(X)                                                  \
-  X(Overwrite, "OVERWRITE", "Lcom/google/firebase/firestore/SetOptions;",      \
-    util::kFieldTypeStatic)
-// clang-format on
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/SetOptions";
 
-METHOD_LOOKUP_DECLARATION(set_options, SET_OPTIONS_METHODS, SET_OPTIONS_FIELDS)
-METHOD_LOOKUP_DEFINITION(set_options,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/SetOptions",
-                         SET_OPTIONS_METHODS, SET_OPTIONS_FIELDS)
+StaticMethod kMerge("merge",
+                            "()Lcom/google/firebase/firestore/SetOptions;");
+StaticMethod kMergeFieldPaths(
+    "mergeFieldPaths",
+    "(Ljava/util/List;)Lcom/google/firebase/firestore/SetOptions;");
 
-/* static */
-jobject SetOptionsInternal::Overwrite(JNIEnv* env) {
-  jobject result = env->GetStaticObjectField(
-      set_options::GetClass(),
-      set_options::GetFieldId(set_options::kOverwrite));
-  CheckAndClearJniExceptions(env);
-  return result;
-}
+StaticField kOverwrite("OVERWRITE",
+                               "Lcom/google/firebase/firestore/SetOptions;");
+
+}  // namespace
 
-/* static */
-jobject SetOptionsInternal::Merge(JNIEnv* env) {
-  jobject result = env->CallStaticObjectMethod(
-      set_options::GetClass(), set_options::GetMethodId(set_options::kMerge));
-  CheckAndClearJniExceptions(env);
-  return result;
+void SetOptionsInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kMerge, kMergeFieldPaths, kOverwrite);
 }
 
-/* static */
-jobject SetOptionsInternal::ToJavaObject(JNIEnv* env,
+Local SetOptionsInternal::Create(Env& env,
                                          const SetOptions& set_options) {
   switch (set_options.type_) {
     case Type::kOverwrite:
-      return Overwrite(env);
+      return env.Get(kOverwrite);
     case Type::kMergeAll:
-      return Merge(env);
+      return env.Call(kMerge);
     case Type::kMergeSpecific:
       // Do below this switch.
       break;
     default:
       FIREBASE_ASSERT_MESSAGE(false, "Unknown SetOptions type.");
-      return nullptr;
+      return {};
   }
 
   // Now we deal with options to merge specific fields.
-  // Construct call arguments.
-  jobject fields = env->NewObject(
-      util::array_list::GetClass(),
-      util::array_list::GetMethodId(util::array_list::kConstructor));
-  jmethodID add_method = util::array_list::GetMethodId(util::array_list::kAdd);
+  Local fields = ArrayList::Create(env);
   for (const FieldPath& field : set_options.fields_) {
-    jobject field_converted = FieldPathConverter::ToJavaObject(env, field);
-    env->CallBooleanMethod(fields, add_method, field_converted);
-    CheckAndClearJniExceptions(env);
-    env->DeleteLocalRef(field_converted);
+    fields.Add(env, FieldPathConverter::Create(env, field));
   }
-  // Make the call to Android SDK.
-  jobject result = env->CallStaticObjectMethod(
-      set_options::GetClass(),
-      set_options::GetMethodId(set_options::kMergeFieldPaths), fields);
-  CheckAndClearJniExceptions(env);
-  env->DeleteLocalRef(fields);
-  return result;
-}
-
-/* static */
-bool SetOptionsInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = set_options::CacheMethodIds(env, activity) &&
-                set_options::CacheFieldIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
 
-/* static */
-void SetOptionsInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  set_options::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+  return env.Call(kMergeFieldPaths, fields);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/set_options_android.h b/firestore/src/android/set_options_android.h
index fbc700f871..96c62e41ad 100644
--- a/firestore/src/android/set_options_android.h
+++ b/firestore/src/android/set_options_android.h
@@ -1,10 +1,8 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SET_OPTIONS_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SET_OPTIONS_ANDROID_H_
 
-#include 
-
-#include "app/src/include/firebase/app.h"
 #include "firestore/src/include/firebase/firestore/set_options.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
@@ -18,23 +16,14 @@ class SetOptionsInternal {
  public:
   using ApiType = SetOptions;
 
-  /** Get a jobject for overwrite option. */
-  static jobject Overwrite(JNIEnv* env);
-
-  /** Get a jobject for merge all option. */
-  static jobject Merge(JNIEnv* env);
+  static void Initialize(jni::Loader& loader);
 
   /** Convert a C++ SetOptions to a Java SetOptions. */
-  static jobject ToJavaObject(JNIEnv* env, const SetOptions& set_options);
+  static jni::Local Create(jni::Env& env,
+                                        const SetOptions& set_options);
 
   // We do not need to convert Java SetOptions back to C++ SetOptions since
   // there is no public API that returns a SetOptions yet.
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/settings_android.cc b/firestore/src/android/settings_android.cc
new file mode 100644
index 0000000000..9c6cde0b5a
--- /dev/null
+++ b/firestore/src/android/settings_android.cc
@@ -0,0 +1,87 @@
+#include "firestore/src/android/settings_android.h"
+
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace {
+
+using jni::Constructor;
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+using jni::String;
+
+// class FirebaseFirestoreSettings.Builder
+constexpr char kSettingsBuilderClass[] = PROGUARD_KEEP_CLASS
+    "com/google/firebase/firestore/FirebaseFirestoreSettings$Builder";
+Constructor kNewBuilder("()V");
+Method kSetHost(
+    "setHost",
+    "(Ljava/lang/String;)"
+    "Lcom/google/firebase/firestore/FirebaseFirestoreSettings$Builder;");
+Method kSetSslEnabled(
+    "setSslEnabled",
+    "(Z)"
+    "Lcom/google/firebase/firestore/FirebaseFirestoreSettings$Builder;");
+Method kSetPersistenceEnabled(
+    "setPersistenceEnabled",
+    "(Z)"
+    "Lcom/google/firebase/firestore/FirebaseFirestoreSettings$Builder;");
+Method kBuild(
+    "build", "()Lcom/google/firebase/firestore/FirebaseFirestoreSettings;");
+
+// class FirebaseFirestoreSettings
+constexpr char kSettingsClass[] = PROGUARD_KEEP_CLASS
+    "com/google/firebase/firestore/FirebaseFirestoreSettings";
+Method kGetHost("getHost", "()Ljava/lang/String;");
+Method kIsSslEnabled("isSslEnabled", "()Z");
+Method kIsPersistenceEnabled("isPersistenceEnabled", "()Z");
+
+}  // namespace
+
+void SettingsInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kSettingsBuilderClass, kNewBuilder, kSetHost, kSetSslEnabled,
+                   kSetPersistenceEnabled, kBuild);
+
+  loader.LoadClass(kSettingsClass, kGetHost, kIsSslEnabled,
+                   kIsPersistenceEnabled);
+}
+
+Local SettingsInternal::Create(Env& env,
+                                                 const Settings& settings) {
+  Local builder = env.New(kNewBuilder);
+
+  Local host = env.NewStringUtf(settings.host());
+  builder = env.Call(builder, kSetHost, host);
+
+  builder = env.Call(builder, kSetSslEnabled, settings.is_ssl_enabled());
+
+  builder = env.Call(builder, kSetPersistenceEnabled,
+                     settings.is_persistence_enabled());
+
+  return env.Call(builder, kBuild);
+}
+
+Settings SettingsInternal::ToPublic(Env& env) const {
+  Settings result;
+
+  // Set host
+  Local host = env.Call(*this, kGetHost);
+  result.set_host(host.ToString(env));
+
+  // Set SSL enabled
+  bool ssl_enabled = env.Call(*this, kIsSslEnabled);
+  result.set_ssl_enabled(ssl_enabled);
+
+  // Set Persistence enabled
+  bool persistence_enabled = env.Call(*this, kIsPersistenceEnabled);
+  result.set_persistence_enabled(persistence_enabled);
+
+  return result;
+}
+
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/android/settings_android.h b/firestore/src/android/settings_android.h
new file mode 100644
index 0000000000..1c4d579bb2
--- /dev/null
+++ b/firestore/src/android/settings_android.h
@@ -0,0 +1,28 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SETTINGS_ANDROID_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SETTINGS_ANDROID_H_
+
+#include "firestore/src/include/firebase/firestore/settings.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+
+namespace firebase {
+namespace firestore {
+
+class SettingsInternal : public jni::Object {
+ public:
+  using ApiType = Settings;
+
+  using jni::Object::Object;
+
+  static void Initialize(jni::Loader& loader);
+
+  static jni::Local Create(jni::Env& env,
+                                             const Settings& settings);
+
+  Settings ToPublic(jni::Env& env) const;
+};
+
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SETTINGS_ANDROID_H_
diff --git a/firestore/src/android/snapshot_metadata_android.cc b/firestore/src/android/snapshot_metadata_android.cc
index c94b96e4cd..2f12bd0593 100644
--- a/firestore/src/android/snapshot_metadata_android.cc
+++ b/firestore/src/android/snapshot_metadata_android.cc
@@ -1,67 +1,32 @@
 #include "firestore/src/android/snapshot_metadata_android.h"
 
-#include 
-
-#include "app/src/util_android.h"
-#include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-// clang-format off
-#define SNAPSHOT_METADATA_METHODS(X)                            \
-  X(Constructor, "", "(ZZ)V", util::kMethodTypeInstance), \
-  X(HasPendingWrites, "hasPendingWrites", "()Z"),               \
-  X(IsFromCache, "isFromCache", "()Z")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(snapshot_metadata, SNAPSHOT_METADATA_METHODS)
-METHOD_LOOKUP_DEFINITION(snapshot_metadata,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/SnapshotMetadata",
-                         SNAPSHOT_METADATA_METHODS)
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
 
-/* static */
-jobject SnapshotMetadataInternal::SnapshotMetadataToJavaSnapshotMetadata(
-    JNIEnv* env, const SnapshotMetadata& metadata) {
-  jobject result = env->NewObject(
-      snapshot_metadata::GetClass(),
-      snapshot_metadata::GetMethodId(snapshot_metadata::kConstructor),
-      static_cast(metadata.has_pending_writes()),
-      static_cast(metadata.is_from_cache()));
-  CheckAndClearJniExceptions(env);
-  return result;
-}
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/SnapshotMetadata";
+Method kHasPendingWrites("hasPendingWrites", "()Z");
+Method kIsFromCache("isFromCache", "()Z");
 
-/* static */
-SnapshotMetadata
-SnapshotMetadataInternal::JavaSnapshotMetadataToSnapshotMetadata(JNIEnv* env,
-                                                                 jobject obj) {
-  jboolean has_pending_writes = env->CallBooleanMethod(
-      obj,
-      snapshot_metadata::GetMethodId(snapshot_metadata::kHasPendingWrites));
-  jboolean is_from_cache = env->CallBooleanMethod(
-      obj, snapshot_metadata::GetMethodId(snapshot_metadata::kIsFromCache));
-  env->DeleteLocalRef(obj);
-  CheckAndClearJniExceptions(env);
-  return SnapshotMetadata{static_cast(has_pending_writes),
-                          static_cast(is_from_cache)};
-}
+}  // namespace
 
-/* static */
-bool SnapshotMetadataInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = snapshot_metadata::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
+void SnapshotMetadataInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kHasPendingWrites, kIsFromCache);
 }
 
-/* static */
-void SnapshotMetadataInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  snapshot_metadata::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+SnapshotMetadata SnapshotMetadataInternal::ToPublic(Env& env) const {
+  bool has_pending_writes = env.Call(*this, kHasPendingWrites);
+  bool is_from_cache = env.Call(*this, kIsFromCache);
+  return SnapshotMetadata(has_pending_writes, is_from_cache);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/snapshot_metadata_android.h b/firestore/src/android/snapshot_metadata_android.h
index c7ad70c6f8..6bbc8bebb7 100644
--- a/firestore/src/android/snapshot_metadata_android.h
+++ b/firestore/src/android/snapshot_metadata_android.h
@@ -1,39 +1,30 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SNAPSHOT_METADATA_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SNAPSHOT_METADATA_ANDROID_H_
 
-#include 
-
-#include "app/src/include/firebase/app.h"
 #include "firestore/src/include/firebase/firestore/snapshot_metadata.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
 
 namespace firebase {
 namespace firestore {
 
-// This is the non-wrapper Android implementation of SnapshotMetadata. Since
-// SnapshotMetadata has all methods inlined, we use it directly instead of
-// wrapping around a Java SnapshotMetadata object. We still need the helper
-// functions to convert between the two types. In addition, we also need proper
-// initializer and terminator for the Java class cache/uncache.
-class SnapshotMetadataInternal {
+/**
+ * A C++ proxy for a Java `SnapshotMetadata` object.
+ */
+class SnapshotMetadataInternal : public jni::Object {
  public:
   using ApiType = SnapshotMetadata;
 
-  // Convert a C++ SnapshotMetadata into a Java SnapshotMetadata.
-  static jobject SnapshotMetadataToJavaSnapshotMetadata(
-      JNIEnv* env, const SnapshotMetadata& metadata);
+  using jni::Object::Object;
+
+  static void Initialize(jni::Loader& loader);
 
   // Convert a Java SnapshotMetadata into a C++ SnapshotMetadata and release the
   // reference to the jobject.
-  static SnapshotMetadata JavaSnapshotMetadataToSnapshotMetadata(JNIEnv* env,
-                                                                 jobject obj);
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  SnapshotMetadata ToPublic(jni::Env& env) const;
 };
 
 }  // namespace firestore
 }  // namespace firebase
+
 #endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SNAPSHOT_METADATA_ANDROID_H_
diff --git a/firestore/src/android/source_android.cc b/firestore/src/android/source_android.cc
index c94cb97d92..0294a511dc 100644
--- a/firestore/src/android/source_android.cc
+++ b/firestore/src/android/source_android.cc
@@ -1,70 +1,41 @@
 #include "firestore/src/android/source_android.h"
 
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
 namespace firebase {
 namespace firestore {
-
-// clang-format off
-#define SOURCE_METHODS(X)                \
-  X(Name, "name", "()Ljava/lang/String;")
-#define SOURCE_FIELDS(X)                                          \
-  X(Default, "DEFAULT", "Lcom/google/firebase/firestore/Source;", \
-    util::kFieldTypeStatic),                                      \
-  X(Server, "SERVER", "Lcom/google/firebase/firestore/Source;",   \
-    util::kFieldTypeStatic),                                      \
-  X(Cache, "CACHE", "Lcom/google/firebase/firestore/Source;",     \
-    util::kFieldTypeStatic)
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(source, SOURCE_METHODS, SOURCE_FIELDS)
-METHOD_LOOKUP_DEFINITION(source,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/Source",
-                         SOURCE_METHODS, SOURCE_FIELDS)
-
-std::map* SourceInternal::cpp_enum_to_java_ = nullptr;
-
-/* static */
-jobject SourceInternal::ToJavaObject(JNIEnv* env, Source source) {
-  return (*cpp_enum_to_java_)[source];
+namespace {
+
+using jni::Env;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+using jni::StaticField;
+using jni::String;
+
+constexpr char kClass[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/Source";
+StaticField kDefault("DEFAULT",
+                             "Lcom/google/firebase/firestore/Source;");
+StaticField kServer("SERVER", "Lcom/google/firebase/firestore/Source;");
+StaticField kCache("CACHE", "Lcom/google/firebase/firestore/Source;");
+
+}  // namespace
+
+void SourceInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClass, kDefault, kServer, kCache);
 }
 
-/* static */
-bool SourceInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = source::CacheMethodIds(env, activity) &&
-                source::CacheFieldIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-
-  // Cache Java enum values.
-  cpp_enum_to_java_ = new std::map();
-  const auto add_enum = [env](Source source, source::Field field) {
-    jobject value = env->GetStaticObjectField(source::GetClass(),
-                                              source::GetFieldId(field));
-    (*cpp_enum_to_java_)[source] = env->NewGlobalRef(value);
-    env->DeleteLocalRef(value);
-    util::CheckAndClearJniExceptions(env);
-  };
-  add_enum(Source::kDefault, source::kDefault);
-  add_enum(Source::kServer, source::kServer);
-  add_enum(Source::kCache, source::kCache);
-
-  return result;
-}
-
-/* static */
-void SourceInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  source::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-
-  // Uncache Java enum values.
-  for (auto& kv : *cpp_enum_to_java_) {
-    env->DeleteGlobalRef(kv.second);
+Local SourceInternal::Create(Env& env, Source source) {
+  switch (source) {
+    case Source::kDefault:
+      return env.Get(kDefault);
+    case Source::kServer:
+      return env.Get(kServer);
+    case Source::kCache:
+      return env.Get(kCache);
   }
-  util::CheckAndClearJniExceptions(env);
-  delete cpp_enum_to_java_;
-  cpp_enum_to_java_ = nullptr;
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/source_android.h b/firestore/src/android/source_android.h
index 5e36ad0c5a..9a4f020ac7 100644
--- a/firestore/src/android/source_android.h
+++ b/firestore/src/android/source_android.h
@@ -1,26 +1,17 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SOURCE_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_SOURCE_ANDROID_H_
 
-#include 
-
-#include "app/src/include/firebase/app.h"
-#include "app/src/util_android.h"
 #include "firestore/src/include/firebase/firestore/source.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 namespace firebase {
 namespace firestore {
 
 class SourceInternal {
  public:
-  static jobject ToJavaObject(JNIEnv* env, Source source);
-
- private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  static void Initialize(jni::Loader& loader);
 
-  static std::map* cpp_enum_to_java_;
+  static jni::Local Create(jni::Env& env, Source source);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/timestamp_android.cc b/firestore/src/android/timestamp_android.cc
index b9fd4b253a..db72af0081 100644
--- a/firestore/src/android/timestamp_android.cc
+++ b/firestore/src/android/timestamp_android.cc
@@ -1,65 +1,44 @@
 #include "firestore/src/android/timestamp_android.h"
 
-#include 
-
-#include "app/src/util_android.h"
-#include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-// clang-format off
-#define TIMESTAMP_METHODS(X)                                     \
-  X(Constructor, "", "(JI)V", util::kMethodTypeInstance),  \
-  X(GetSeconds, "getSeconds", "()J"),                            \
-  X(GetNanoseconds, "getNanoseconds", "()I")
-// clang-format on
+using jni::Class;
+using jni::Constructor;
+using jni::Env;
+using jni::Local;
+using jni::Method;
 
-METHOD_LOOKUP_DECLARATION(timestamp, TIMESTAMP_METHODS)
-METHOD_LOOKUP_DEFINITION(timestamp,
-                         PROGUARD_KEEP_CLASS "com/google/firebase/Timestamp",
-                         TIMESTAMP_METHODS)
+constexpr char kClassName[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/Timestamp";
+Constructor kConstructor("(JI)V");
+Method kGetSeconds("getSeconds", "()J");
+Method kGetNanoseconds("getNanoseconds", "()I");
 
-/* static */
-jobject TimestampInternal::TimestampToJavaTimestamp(
-    JNIEnv* env, const Timestamp& timestamp) {
-  jobject result = env->NewObject(
-      timestamp::GetClass(), timestamp::GetMethodId(timestamp::kConstructor),
-      static_cast(timestamp.seconds()),
-      static_cast(timestamp.nanoseconds()));
-  CheckAndClearJniExceptions(env);
-  return result;
-}
+jclass g_clazz = nullptr;
+
+}  // namespace
 
-/* static */
-Timestamp TimestampInternal::JavaTimestampToTimestamp(JNIEnv* env,
-                                                      jobject obj) {
-  jlong seconds =
-      env->CallLongMethod(obj, timestamp::GetMethodId(timestamp::kGetSeconds));
-  jint nanoseconds = env->CallIntMethod(
-      obj, timestamp::GetMethodId(timestamp::kGetNanoseconds));
-  CheckAndClearJniExceptions(env);
-  return Timestamp{static_cast(seconds),
-                   static_cast(nanoseconds)};
+void TimestampInternal::Initialize(jni::Loader& loader) {
+  g_clazz =
+      loader.LoadClass(kClassName, kConstructor, kGetSeconds, kGetNanoseconds);
 }
 
-/* static */
-jclass TimestampInternal::GetClass() { return timestamp::GetClass(); }
+Class TimestampInternal::GetClass() { return Class(g_clazz); }
 
-/* static */
-bool TimestampInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = timestamp::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
+Local TimestampInternal::Create(Env& env,
+                                                   const Timestamp& timestamp) {
+  return env.New(kConstructor, timestamp.seconds(), timestamp.nanoseconds());
 }
 
-/* static */
-void TimestampInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  timestamp::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+Timestamp TimestampInternal::ToPublic(Env& env) const {
+  int64_t seconds = env.Call(*this, kGetSeconds);
+  int32_t nanoseconds = env.Call(*this, kGetNanoseconds);
+  return Timestamp(seconds, nanoseconds);
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/timestamp_android.h b/firestore/src/android/timestamp_android.h
index 51d73490d2..51484113cd 100644
--- a/firestore/src/android/timestamp_android.h
+++ b/firestore/src/android/timestamp_android.h
@@ -1,38 +1,30 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_TIMESTAMP_ANDROID_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_TIMESTAMP_ANDROID_H_
 
-#include 
-
-#include "app/src/include/firebase/app.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
 #include "firebase/firestore/timestamp.h"
 
 namespace firebase {
 namespace firestore {
 
-// This is the non-wrapper Android implementation of Timestamp. Since Timestamp
-// has most of their method inlined, we use it directly instead of wrapping
-// around a Java Timestamp object. We still need the helper functions to convert
-// between the two types. In addition, we also need proper initializer and
-// terminator for the Java class cache/uncache.
-class TimestampInternal {
+/** A C++ proxy for a Java `Timestamp`. */
+class TimestampInternal : public jni::Object {
  public:
   using ApiType = Timestamp;
 
-  // Convert a C++ Timestamp into a Java Timestamp.
-  static jobject TimestampToJavaTimestamp(JNIEnv* env,
-                                          const Timestamp& timestamp);
+  using jni::Object::Object;
 
-  // Convert a Java Timestamp into a C++ Timestamp.
-  static Timestamp JavaTimestampToTimestamp(JNIEnv* env, jobject obj);
+  static void Initialize(jni::Loader& loader);
 
-  // Gets the class object of Java Timestamp class.
-  static jclass GetClass();
+  static jni::Class GetClass();
 
- private:
-  friend class FirestoreInternal;
+  /** Creates a C++ proxy for a Java `Timestamp` object. */
+  static jni::Local Create(jni::Env& env,
+                                              const Timestamp& timestamp);
 
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
+  /** Converts a Java `Timestamp` into a public C++ `Timestamp`. */
+  Timestamp ToPublic(jni::Env& env) const;
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/transaction_android.cc b/firestore/src/android/transaction_android.cc
index 4e5dab3e2a..500223380f 100644
--- a/firestore/src/android/transaction_android.cc
+++ b/firestore/src/android/transaction_android.cc
@@ -4,75 +4,87 @@
 
 #include 
 
-#include "app/src/embedded_file.h"
+#include "app/meta/move.h"
 #include "app/src/include/firebase/internal/common.h"
-#include "app/src/util_android.h"
 #include "firestore/src/android/document_reference_android.h"
+#include "firestore/src/android/exception_android.h"
 #include "firestore/src/android/field_path_android.h"
 #include "firestore/src/android/field_value_android.h"
-#include "firestore/src/android/firebase_firestore_exception_android.h"
 #include "firestore/src/android/set_options_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/hash_map.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
+
+using jni::Constructor;
+using jni::Env;
+using jni::HashMap;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+using jni::Throwable;
+
+constexpr char kTransactionClassName[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/Transaction";
+Method kSet(
+    "set",
+    "(Lcom/google/firebase/firestore/DocumentReference;Ljava/lang/Object;"
+    "Lcom/google/firebase/firestore/SetOptions;)"
+    "Lcom/google/firebase/firestore/Transaction;");
+Method kUpdate(
+    "update",
+    "(Lcom/google/firebase/firestore/DocumentReference;Ljava/util/Map;)"
+    "Lcom/google/firebase/firestore/Transaction;");
+Method kUpdateVarargs(
+    "update",
+    "(Lcom/google/firebase/firestore/DocumentReference;"
+    "Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;"
+    "[Ljava/lang/Object;)Lcom/google/firebase/firestore/Transaction;");
+Method kDelete("delete",
+                       "(Lcom/google/firebase/firestore/DocumentReference;)"
+                       "Lcom/google/firebase/firestore/Transaction;");
+Method kGet("get",
+                    "(Lcom/google/firebase/firestore/DocumentReference;)"
+                    "Lcom/google/firebase/firestore/DocumentSnapshot;");
+
+constexpr char kTransactionFunctionClassName[] = PROGUARD_KEEP_CLASS
+    "com/google/firebase/firestore/internal/cpp/TransactionFunction";
+
+Constructor kNewTransactionFunction("(JJ)V");
+
+}  // namespace
+
+void TransactionInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kTransactionClassName, kSet, kUpdate, kUpdateVarargs,
+                   kDelete, kGet);
 
-// clang-format off
-#define TRANSACTION_METHODS(X)                                                 \
-  X(Set, "set",                                                                \
-    "(Lcom/google/firebase/firestore/DocumentReference;Ljava/lang/Object;"     \
-    "Lcom/google/firebase/firestore/SetOptions;)"                              \
-    "Lcom/google/firebase/firestore/Transaction;"),                            \
-  X(Update, "update",                                                          \
-    "(Lcom/google/firebase/firestore/DocumentReference;Ljava/util/Map;)"       \
-    "Lcom/google/firebase/firestore/Transaction;"),                            \
-  X(UpdateVarargs, "update",                                                   \
-    "(Lcom/google/firebase/firestore/DocumentReference;"                       \
-    "Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;"              \
-    "[Ljava/lang/Object;)Lcom/google/firebase/firestore/Transaction;"),        \
-  X(Delete, "delete", "(Lcom/google/firebase/firestore/DocumentReference;)"    \
-    "Lcom/google/firebase/firestore/Transaction;"),                            \
-  X(Get, "get", "(Lcom/google/firebase/firestore/DocumentReference;)"          \
-    "Lcom/google/firebase/firestore/DocumentSnapshot;")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(transaction, TRANSACTION_METHODS)
-METHOD_LOOKUP_DEFINITION(transaction,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/Transaction",
-                         TRANSACTION_METHODS)
-
-#define TRANSACTION_FUNCTION_METHODS(X) X(Constructor, "", "(JJ)V")
-METHOD_LOOKUP_DECLARATION(transaction_function, TRANSACTION_FUNCTION_METHODS)
-METHOD_LOOKUP_DEFINITION(
-    transaction_function,
-    PROGUARD_KEEP_CLASS
-    "com/google/firebase/firestore/internal/cpp/TransactionFunction",
-    TRANSACTION_FUNCTION_METHODS)
+  static const JNINativeMethod kTransactionFunctionNatives[] = {
+      {"nativeApply",
+       "(JJLcom/google/firebase/firestore/Transaction;)Ljava/lang/Exception;",
+       reinterpret_cast(
+           &TransactionInternal::TransactionFunctionNativeApply)}};
+  loader.LoadClass(kTransactionFunctionClassName, kNewTransactionFunction);
+  loader.RegisterNatives(kTransactionFunctionNatives,
+                         FIREBASE_ARRAYSIZE(kTransactionFunctionNatives));
+}
 
 void TransactionInternal::Set(const DocumentReference& document,
                               const MapFieldValue& data,
                               const SetOptions& options) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  jobject data_map = MapFieldValueToJavaMap(firestore_, data);
-  jobject java_options = SetOptionsInternal::ToJavaObject(env, options);
-  env->CallObjectMethod(obj_, transaction::GetMethodId(transaction::kSet),
-                        document.internal_->java_object(), data_map,
-                        java_options);
-  env->DeleteLocalRef(data_map);
-  env->DeleteLocalRef(java_options);
-  CheckAndClearJniExceptions();
+  Env env = GetEnv();
+  Local java_data = MakeJavaMap(env, data);
+  Local java_options = SetOptionsInternal::Create(env, options);
+  env.Call(obj_, kSet, document.internal_->ToJava(), java_data, java_options);
 }
 
 void TransactionInternal::Update(const DocumentReference& document,
                                  const MapFieldValue& data) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  jobject data_map = MapFieldValueToJavaMap(firestore_, data);
-  env->CallObjectMethod(obj_, transaction::GetMethodId(transaction::kUpdate),
-                        document.internal_->java_object(), data_map);
-  env->DeleteLocalRef(data_map);
-  CheckAndClearJniExceptions();
+  Env env = GetEnv();
+  Local java_data = MakeJavaMap(env, data);
+  env.Call(obj_, kUpdate, document.internal_->ToJava(), java_data);
 }
 
 void TransactionInternal::Update(const DocumentReference& document,
@@ -81,65 +93,42 @@ void TransactionInternal::Update(const DocumentReference& document,
     Update(document, MapFieldValue{});
     return;
   }
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  auto iter = data.begin();
-  jobject first_field = FieldPathConverter::ToJavaObject(env, iter->first);
-  jobject first_value = iter->second.internal_->java_object();
-  ++iter;
-
-  // Make the varargs
-  jobjectArray more_fields_and_values =
-      MapFieldPathValueToJavaArray(firestore_, iter, data.end());
-
-  env->CallObjectMethod(obj_,
-                        transaction::GetMethodId(transaction::kUpdateVarargs),
-                        document.internal_->java_object(), first_field,
-                        first_value, more_fields_and_values);
-  env->DeleteLocalRef(first_field);
-  env->DeleteLocalRef(more_fields_and_values);
-  CheckAndClearJniExceptions();
+
+  Env env = GetEnv();
+  UpdateFieldPathArgs args = MakeUpdateFieldPathArgs(env, data);
+  env.Call(obj_, kUpdateVarargs, document.internal_->ToJava(), args.first_field,
+           args.first_value, args.varargs);
 }
 
 void TransactionInternal::Delete(const DocumentReference& document) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  env->CallObjectMethod(obj_, transaction::GetMethodId(transaction::kDelete),
-                        document.internal_->java_object());
-  CheckAndClearJniExceptions();
+  Env env = GetEnv();
+  env.Call(obj_, kDelete, document.internal_->ToJava());
 }
 
 DocumentSnapshot TransactionInternal::Get(const DocumentReference& document,
                                           Error* error_code,
                                           std::string* error_message) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
+  Env env = GetEnv();
 
-  jobject snapshot =
-      env->CallObjectMethod(obj_, transaction::GetMethodId(transaction::kGet),
-                            document.internal_->java_object());
-  jthrowable exception = env->ExceptionOccurred();
+  Local snapshot = env.Call(obj_, kGet, document.internal_->ToJava());
+  Local exception = env.ClearExceptionOccurred();
 
-  // Now deal with exceptions, if any. Do not preserve yet. Do not call
-  // this->CheckAndClearJniExceptions().
-  util::CheckAndClearJniExceptions(env);
-  if (exception != nullptr) {
+  if (exception) {
     if (error_code != nullptr) {
-      *error_code =
-          FirebaseFirestoreExceptionInternal::ToErrorCode(env, exception);
+      *error_code = ExceptionInternal::GetErrorCode(env, exception);
     }
     if (error_message != nullptr) {
-      *error_message =
-          FirebaseFirestoreExceptionInternal::ToString(env, exception);
+      *error_message = ExceptionInternal::ToString(env, exception);
     }
 
-    if (!FirebaseFirestoreExceptionInternal::IsInstance(env, exception)) {
+    if (!ExceptionInternal::IsFirestoreException(env, exception)) {
       // We would only preserve exception if it is not
       // FirebaseFirestoreException. The user should decide whether to raise the
       // error or let the transaction succeed.
-      PreserveException(exception);
+      PreserveException(env, Move(exception));
     }
-
-    env->DeleteLocalRef(exception);
     return DocumentSnapshot{};
+
   } else {
     if (error_code != nullptr) {
       *error_code = Error::kErrorOk;
@@ -148,62 +137,52 @@ DocumentSnapshot TransactionInternal::Get(const DocumentReference& document,
       *error_message = "";
     }
   }
-  DocumentSnapshot result{new DocumentSnapshotInternal{firestore_, snapshot}};
-  env->DeleteLocalRef(snapshot);
-  return result;
+
+  return firestore_->NewDocumentSnapshot(env, snapshot);
 }
 
-void TransactionInternal::PreserveException(jthrowable exception) {
-  if (*first_exception_ != nullptr || exception == nullptr) {
-    return;
-  }
+Env TransactionInternal::GetEnv() {
+  Env env;
+  env.SetUnhandledExceptionHandler(ExceptionHandler, this);
+  return env;
+}
 
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  if (FirebaseFirestoreExceptionInternal::IsFirestoreException(env,
-                                                               exception)) {
-    jobject firestore_exception =
-        FirebaseFirestoreExceptionInternal::ToException(env, exception);
-    *first_exception_ =
-        static_cast(env->NewGlobalRef(firestore_exception));
-    env->DeleteLocalRef(firestore_exception);
-  } else {
-    *first_exception_ = static_cast(env->NewGlobalRef(exception));
-  }
+void TransactionInternal::ExceptionHandler(Env& env,
+                                           Local&& exception,
+                                           void* context) {
+  auto* transaction = static_cast(context);
+  env.ExceptionClear();
+  transaction->PreserveException(env, Move(exception));
 }
 
-void TransactionInternal::ClearException() {
-  if (*first_exception_) {
-    firestore_->app()->GetJNIEnv()->DeleteGlobalRef(*first_exception_);
-    *first_exception_ = nullptr;
+void TransactionInternal::PreserveException(jni::Env& env,
+                                            Local&& exception) {
+  // Only preserve the first real exception.
+  if (*first_exception_ || !exception) {
+    return;
+  }
+
+  if (ExceptionInternal::IsAnyExceptionThrownByFirestore(env, exception)) {
+    exception = ExceptionInternal::Wrap(env, Move(exception));
   }
+  *first_exception_ = Move(exception);
 }
 
-bool TransactionInternal::CheckAndClearJniExceptions() {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jthrowable exception = env->ExceptionOccurred();
-  // We need to clear the exception before we can do anything useful. Only limit
-  // JNI APIs have defined behavior when there is an active exception.
-  bool result = util::CheckAndClearJniExceptions(env);
-  PreserveException(exception);
-  return result;
+Local TransactionInternal::ClearExceptionOccurred() {
+  if (!*first_exception_) return {};
+  return Move(*first_exception_);
 }
 
-/* static */
-jobject TransactionInternal::ToJavaObject(JNIEnv* env,
+Local TransactionInternal::Create(Env& env,
                                           FirestoreInternal* firestore,
                                           TransactionFunction* function) {
-  jobject result = env->NewObject(
-      transaction_function::GetClass(),
-      transaction_function::GetMethodId(transaction_function::kConstructor),
-      reinterpret_cast(firestore), reinterpret_cast(function));
-  util::CheckAndClearJniExceptions(env);
-  return result;
+  return env.New(kNewTransactionFunction, reinterpret_cast(firestore),
+                 reinterpret_cast(function));
 }
 
-/* static */
 jobject TransactionInternal::TransactionFunctionNativeApply(
-    JNIEnv* env, jclass clazz, jlong firestore_ptr,
-    jlong transaction_function_ptr, jobject transaction) {
+    JNIEnv* raw_env, jclass clazz, jlong firestore_ptr,
+    jlong transaction_function_ptr, jobject java_transaction) {
   if (firestore_ptr == 0 || transaction_function_ptr == 0) {
     return nullptr;
   }
@@ -212,59 +191,22 @@ jobject TransactionInternal::TransactionFunctionNativeApply(
       reinterpret_cast(firestore_ptr);
   TransactionFunction* transaction_function =
       reinterpret_cast(transaction_function_ptr);
-  Transaction cpp_transaction(new TransactionInternal{firestore, transaction});
-  cpp_transaction.internal_->ClearException();
+
+  Transaction transaction(new TransactionInternal(firestore, java_transaction));
+
   std::string message;
-  Error code = transaction_function->Apply(cpp_transaction, message);
+  Error code = transaction_function->Apply(transaction, message);
 
-  jobject first_exception =
-      env->NewLocalRef(*(cpp_transaction.internal_->first_exception_));
+  Local first_exception =
+      transaction.internal_->ClearExceptionOccurred();
 
   if (first_exception) {
-    cpp_transaction.internal_->ClearException();
-    return first_exception;
+    return first_exception.release();
   } else {
-    return FirebaseFirestoreExceptionInternal::ToException(env, code,
-                                                           message.c_str());
+    Env env(raw_env);
+    return ExceptionInternal::Create(env, code, message).release();
   }
 }
 
-/* static */
-bool TransactionInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = transaction::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-bool TransactionInternal::InitializeEmbeddedClasses(
-    App* app, const std::vector* embedded_files) {
-  static const JNINativeMethod kTransactionFunctionNatives[] = {
-      {"nativeApply",
-       "(JJLcom/google/firebase/firestore/Transaction;)Ljava/lang/Exception;",
-       reinterpret_cast(
-           &TransactionInternal::TransactionFunctionNativeApply)}};
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = transaction_function::CacheClassFromFiles(env, activity,
-                                                          embedded_files) &&
-                transaction_function::CacheMethodIds(env, activity) &&
-                transaction_function::RegisterNatives(
-                    env, kTransactionFunctionNatives,
-                    FIREBASE_ARRAYSIZE(kTransactionFunctionNatives));
-  util::CheckAndClearJniExceptions(env);
-  return result;
-}
-
-/* static */
-void TransactionInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  transaction::ReleaseClass(env);
-  transaction_function::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-}
-
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/android/transaction_android.h b/firestore/src/android/transaction_android.h
index f1997b5592..86d0d8b166 100644
--- a/firestore/src/android/transaction_android.h
+++ b/firestore/src/android/transaction_android.h
@@ -10,6 +10,9 @@
 #include "firestore/src/include/firebase/firestore/document_reference.h"
 #include "firestore/src/include/firebase/firestore/map_field_value.h"
 #include "firestore/src/include/firebase/firestore/transaction.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/ownership.h"
+#include "firestore/src/jni/throwable.h"
 
 namespace firebase {
 namespace firestore {
@@ -18,14 +21,18 @@ class TransactionInternal : public Wrapper {
  public:
   using ApiType = Transaction;
 
+  static void Initialize(jni::Loader& loader);
+
   TransactionInternal(FirestoreInternal* firestore, jobject obj)
-      : Wrapper(firestore, obj), first_exception_{new jthrowable{nullptr}} {}
+      : Wrapper(firestore, obj),
+        first_exception_(MakeShared>()) {}
 
   TransactionInternal(const TransactionInternal& rhs)
       : Wrapper(rhs), first_exception_(rhs.first_exception_) {}
 
   TransactionInternal(TransactionInternal&& rhs)
-      : Wrapper(firebase::Move(rhs)), first_exception_(rhs.first_exception_) {}
+      : Wrapper(firebase::Move(rhs)),
+        first_exception_(Move(rhs.first_exception_)) {}
 
   void Set(const DocumentReference& document, const MapFieldValue& data,
            const SetOptions& options);
@@ -39,8 +46,9 @@ class TransactionInternal : public Wrapper {
   DocumentSnapshot Get(const DocumentReference& document, Error* error_code,
                        std::string* error_message);
 
-  static jobject ToJavaObject(JNIEnv* env, FirestoreInternal* firestore,
-                              TransactionFunction* function);
+  static jni::Local Create(jni::Env& env,
+                                        FirestoreInternal* firestore,
+                                        TransactionFunction* function);
 
   static jobject TransactionFunctionNativeApply(JNIEnv* env, jclass clazz,
                                                 jlong firestore_ptr,
@@ -48,35 +56,25 @@ class TransactionInternal : public Wrapper {
                                                 jobject transaction);
 
  private:
-  // If this is the first exception, then store it. Otherwise, preserve the
-  // current exception. Passing nullptr has no effect.
-  void PreserveException(jthrowable exception);
+  jni::Env GetEnv();
 
-  // Clear the global reference of the first exception, if any. The SharedPtr
-  // does not support custom deleters and thus we must call this explicitly.
-  // The workflow of RunTransaction allows us to do so.
-  void ClearException();
+  static void ExceptionHandler(jni::Env& env,
+                               jni::Local&& exception,
+                               void* context);
 
-  // A helper function to replace util::CheckAndClearJniExceptions(env). It will
-  // check and clear all exceptions just as what the one under util is doing. In
-  // addition, it also preserves the first ever exception during a Transaction.
-  // We do not preserve each and every exception. Only the first one matters and
-  // more than likely the subsequent exception is caused by the first one.
-  bool CheckAndClearJniExceptions();
-
-  friend class FirestoreInternal;
+  // If this is the first exception, then store it. Otherwise, preserve the
+  // current exception. Passing nullptr has no effect.
+  void PreserveException(jni::Env& env, jni::Local&& exception);
 
-  static bool Initialize(App* app);
-  static bool InitializeEmbeddedClasses(
-      App* app, const std::vector* embedded_files);
-  static void Terminate(App* app);
+  // Returns and clears the global reference of the first exception, if any.
+  jni::Local ClearExceptionOccurred();
 
   // The first exception that occurred. Because exceptions must be cleared
   // before calling other JNI methods, we cannot rely on the Java exception
   // mechanism to properly handle native calls via JNI. The first exception is
   // shared by a transaction and its copies. User is allowed to make copy and
   // call transaction operation on the copy.
-  SharedPtr first_exception_;
+  SharedPtr> first_exception_;
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/util_android.cc b/firestore/src/android/util_android.cc
new file mode 100644
index 0000000000..2326252f52
--- /dev/null
+++ b/firestore/src/android/util_android.cc
@@ -0,0 +1,24 @@
+#include "firestore/src/android/util_android.h"
+
+#include "firestore/src/jni/env.h"
+
+namespace firebase {
+namespace firestore {
+
+void GlobalUnhandledExceptionHandler(jni::Env& env,
+                                     jni::Local&& exception,
+                                     void* context) {
+#if __cpp_exceptions
+  // TODO(b/149105903): Handle different underlying Java exceptions differently.
+  env.ExceptionClear();
+  throw FirestoreException(exception.GetMessage(env));
+
+#else
+  // Just clear the pending exception. The exception was already logged when
+  // first caught.
+  env.ExceptionClear();
+#endif
+}
+
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/android/util_android.h b/firestore/src/android/util_android.h
index f42194f3c3..f9969f7a0e 100644
--- a/firestore/src/android/util_android.h
+++ b/firestore/src/android/util_android.h
@@ -2,8 +2,11 @@
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_ANDROID_UTIL_ANDROID_H_
 
 #include 
+
 #include 
+
 #include "app/src/util_android.h"
+#include "firestore/src/jni/jni_fwd.h"
 
 #if __cpp_exceptions
 #include 
@@ -28,7 +31,7 @@ class FirestoreException : public std::exception {
 };
 
 inline bool CheckAndClearJniExceptions(JNIEnv* env) {
-  jobject java_exception = env->ExceptionOccurred();
+  jthrowable java_exception = env->ExceptionOccurred();
   if (java_exception == nullptr) {
     return false;
   }
@@ -49,6 +52,10 @@ inline bool CheckAndClearJniExceptions(JNIEnv* env) {
 
 #endif  // __cpp_exceptions
 
+void GlobalUnhandledExceptionHandler(jni::Env& env,
+                                     jni::Local&& exception,
+                                     void* context);
+
 }  // namespace firestore
 }  // namespace firebase
 
diff --git a/firestore/src/android/wrapper.cc b/firestore/src/android/wrapper.cc
index 2cfd37e9ce..945205d6af 100644
--- a/firestore/src/android/wrapper.cc
+++ b/firestore/src/android/wrapper.cc
@@ -7,16 +7,21 @@
 #include "firestore/src/android/field_value_android.h"
 #include "firestore/src/android/util_android.h"
 #include "firestore/src/include/firebase/firestore.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/hash_map.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
 
-// clang-format off
-#define OBJECT_METHOD(X) X(Equals, "equals", "(Ljava/lang/Object;)Z")
-// clang-format on
+using jni::Array;
+using jni::Env;
+using jni::HashMap;
+using jni::Local;
+using jni::Object;
+using jni::String;
 
-METHOD_LOOKUP_DECLARATION(object, OBJECT_METHOD)
-METHOD_LOOKUP_DEFINITION(object, "java/lang/Object", OBJECT_METHOD)
+}  // namespace
 
 Wrapper::Wrapper(FirestoreInternal* firestore, jobject obj)
     : Wrapper(firestore, obj, AllowNullObject::Yes) {
@@ -81,85 +86,40 @@ Wrapper::~Wrapper() {
   }
 }
 
-bool Wrapper::EqualsJavaObject(const Wrapper& other) const {
-  if (obj_ == other.obj_) {
-    return true;
-  }
-
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jboolean result = env->CallBooleanMethod(
-      obj_, object::GetMethodId(object::kEquals), other.obj_);
-  CheckAndClearJniExceptions(env);
-  return static_cast(result);
-}
-
-/* static */
-jobject Wrapper::MapFieldValueToJavaMap(FirestoreInternal* firestore,
-                                        const MapFieldValue& data) {
-  JNIEnv* env = firestore->app()->GetJNIEnv();
-
-  // Creates an empty Java HashMap (implementing Map) object.
-  jobject result =
-      env->NewObject(util::hash_map::GetClass(),
-                     util::hash_map::GetMethodId(util::hash_map::kConstructor));
-  CheckAndClearJniExceptions(env);
-
-  // Adds each mapping.
-  jmethodID put_method = util::map::GetMethodId(util::map::kPut);
+Local Wrapper::MakeJavaMap(Env& env, const MapFieldValue& data) const {
+  Local result = HashMap::Create(env);
   for (const auto& kv : data) {
-    jobject key = env->NewStringUTF(kv.first.c_str());
-    // Map::Put() returns previously associated value or null, which we have
-    // no use of.
-    env->CallObjectMethod(result, put_method, key,
-                          kv.second.internal_->java_object());
-    env->DeleteLocalRef(key);
-    CheckAndClearJniExceptions(env);
+    Local key = env.NewStringUtf(kv.first);
+    const Object& value = kv.second.internal_->ToJava();
+    result.Put(env, key, value);
   }
-
   return result;
 }
 
-/* static */
-jobjectArray Wrapper::MapFieldPathValueToJavaArray(
-    FirestoreInternal* firestore, MapFieldPathValue::const_iterator begin,
-    MapFieldPathValue::const_iterator end) {
-  JNIEnv* env = firestore->app()->GetJNIEnv();
+Wrapper::UpdateFieldPathArgs Wrapper::MakeUpdateFieldPathArgs(
+    Env& env, const MapFieldPathValue& data) const {
+  auto iter = data.begin();
+  auto end = data.end();
+  FIREBASE_DEV_ASSERT_MESSAGE(iter != end, "data must be non-empty");
 
-  const auto size = std::distance(begin, end) * 2;
-  jobjectArray result = env->NewObjectArray(size, util::object::GetClass(),
-                                            /*initialElement=*/nullptr);
-  CheckAndClearJniExceptions(env);
+  Local first_field = FieldPathConverter::Create(env, iter->first);
+  const Object& first_value = iter->second.internal_->ToJava();
+  ++iter;
+
+  const auto size = std::distance(iter, data.end()) * 2;
+  Local> varargs = env.NewArray(size, Object::GetClass());
 
   int index = 0;
-  for (auto iter = begin; iter != end; ++iter) {
-    jobject field = FieldPathConverter::ToJavaObject(env, iter->first);
-    env->SetObjectArrayElement(result, index, field);
-    env->DeleteLocalRef(field);
-    ++index;
-
-    env->SetObjectArrayElement(result, index,
-                               iter->second.internal_->java_object());
-    ++index;
-    CheckAndClearJniExceptions(env);
-  }
+  for (; iter != end; ++iter) {
+    Local field = FieldPathConverter::Create(env, iter->first);
+    const Object& value = iter->second.internal_->ToJava();
 
-  return result;
-}
+    varargs.Set(env, index++, field);
+    varargs.Set(env, index++, value);
+  }
 
-/* static */
-bool Wrapper::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = object::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
+  return UpdateFieldPathArgs{Move(first_field), first_value, Move(varargs)};
 }
 
-/* static */
-void Wrapper::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  object::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
-}
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/android/wrapper.h b/firestore/src/android/wrapper.h
index 67e1d3f880..98ebc0fc8e 100644
--- a/firestore/src/android/wrapper.h
+++ b/firestore/src/android/wrapper.h
@@ -12,6 +12,11 @@
 #include "firestore/src/include/firebase/firestore/field_path.h"
 #include "firestore/src/include/firebase/firestore/field_value.h"
 #include "firestore/src/include/firebase/firestore/map_field_value.h"
+#include "firestore/src/jni/array.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/list.h"
+#include "firestore/src/jni/object.h"
+#include "firestore/src/jni/ownership.h"
 
 namespace firebase {
 namespace firestore {
@@ -48,10 +53,8 @@ class Wrapper {
   Wrapper& operator=(Wrapper&& wrapper) = delete;
 
   FirestoreInternal* firestore_internal() { return firestore_; }
-  jobject java_object() { return obj_; }
-
-  // Tests the equality of the wrapped Java Object.
-  bool EqualsJavaObject(const Wrapper& other) const;
+  jobject java_object() const { return obj_; }
+  jni::Object ToJava() const { return jni::Object(obj_); }
 
  protected:
   enum class AllowNullObject { Yes };
@@ -66,51 +69,63 @@ class Wrapper {
   // Similar to a copy constructor, but can handle the case where `rhs` is null.
   explicit Wrapper(Wrapper* rhs);
 
+  jni::Env GetEnv() const { return firestore_->GetEnv(); }
+
   // Converts a java list of java type e.g. java.util.List to
   // a C++ vector of equivalent type e.g. std::vector.
   template >
-  static void JavaListToStdVector(FirestoreInternal* firestore, jobject from,
-                                  std::vector* to) {
-    JNIEnv* env = firestore->app()->GetJNIEnv();
-    int size =
-        env->CallIntMethod(from, util::list::GetMethodId(util::list::kSize));
-    CheckAndClearJniExceptions(env);
-    to->clear();
-    to->reserve(size);
+  std::vector MakeVector(jni::Env& env, const jni::List& from) const {
+    size_t size = from.Size(env);
+    std::vector result;
+    result.reserve(size);
+
     for (int i = 0; i < size; ++i) {
-      jobject element = env->CallObjectMethod(
-          from, util::list::GetMethodId(util::list::kGet), i);
-      CheckAndClearJniExceptions(env);
-      // Cannot call with emplace_back since the constructor is protected.
-      to->push_back(PublicT{new InternalT{firestore, element}});
-      env->DeleteLocalRef(element);
+      jni::Local element = from.Get(env, i);
+
+      // Avoid creating a partially valid public object on failure.
+      // TODO(b/163140650): Use `return {}`
+      // Clang 5 with STLPort gives a "chosen constructor is explicit in
+      // copy-initialization" error because the default constructor is explicit.
+      if (!env.ok()) return std::vector();
+
+      // Use push_back because emplace_back requires a public constructor.
+      result.push_back(PublicT{new InternalT{firestore_, element.get()}});
     }
+    return result;
   }
 
-  // Converts a MapFieldValue to a java Map object that maps String to Object.
-  // The caller is responsible for freeing the returned jobject via
-  // JNIEnv::DeleteLocalRef().
-  static jobject MapFieldValueToJavaMap(FirestoreInternal* firestore,
-                                        const MapFieldValue& data);
-
-  // Makes a variadic parameters from C++ MapFieldPathValue iterators up to the
-  // given size. The caller is responsible for freeing the returned jobject via
-  // JNIENV::DeleteLocalRef(). This helper takes iterators instead of
-  // MapFieldPathValue directly because the Android native client API may
-  // require passing the first pair explicit and thus the variadic starting from
-  // the second pair.
-  static jobjectArray MapFieldPathValueToJavaArray(
-      FirestoreInternal* firestore, MapFieldPathValue::const_iterator begin,
-      MapFieldPathValue::const_iterator end);
+  // Converts a MapFieldValue to a Java Map object that maps String to Object.
+  jni::Local MakeJavaMap(jni::Env& env,
+                                       const MapFieldValue& data) const;
+
+  /**
+   * The result of parsing a `MapFieldPathValue` object into its equivalent
+   * arguments, prepared for calling a Firestore Java `update` method. `update`
+   * takes its first two arguments separate from a varargs array.
+   *
+   * An `UpdateFieldPathArgs` object is only valid as long as the
+   * `MapFieldPathValue` object from which it is created is valid because these
+   * are not new references. This is reflected in the fact that `first_value`
+   * is not an owning reference to its `jni::Object`.
+   */
+  struct UpdateFieldPathArgs {
+    jni::Local first_field;
+    jni::Object first_value;
+    jni::Local> varargs;
+  };
+
+  // Creates the variadic parameters for a call to Java `update` from a C++
+  // MapFieldPathValue. The result separates the first field and value because
+  // Android Java API requires passing the first pair separately. The caller
+  // is responsible for verifying that `data` has at least one element.
+  UpdateFieldPathArgs MakeUpdateFieldPathArgs(
+      jni::Env& env, const MapFieldPathValue& data) const;
 
   FirestoreInternal* firestore_ = nullptr;  // not owning
   jobject obj_ = nullptr;
 
  private:
   friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
 };
 
 }  // namespace firestore
diff --git a/firestore/src/android/write_batch_android.cc b/firestore/src/android/write_batch_android.cc
index 67edff79ee..0512dadb68 100644
--- a/firestore/src/android/write_batch_android.cc
+++ b/firestore/src/android/write_batch_android.cc
@@ -1,67 +1,66 @@
 #include "firestore/src/android/write_batch_android.h"
 
-#include 
-
-#include 
-
-#include "app/src/util_android.h"
 #include "firestore/src/android/document_reference_android.h"
 #include "firestore/src/android/field_path_android.h"
 #include "firestore/src/android/field_value_android.h"
 #include "firestore/src/android/set_options_android.h"
-#include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/hash_map.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
-
-// clang-format off
-#define WRITE_BATCH_METHODS(X)                                                 \
-  X(Set, "set",                                                                \
-    "(Lcom/google/firebase/firestore/DocumentReference;Ljava/lang/Object;"     \
-    "Lcom/google/firebase/firestore/SetOptions;)"                              \
-    "Lcom/google/firebase/firestore/WriteBatch;"),                             \
-  X(Update, "update",                                                          \
-    "(Lcom/google/firebase/firestore/DocumentReference;Ljava/util/Map;)"       \
-    "Lcom/google/firebase/firestore/WriteBatch;"),                             \
-  X(UpdateVarargs, "update",                                                   \
-    "(Lcom/google/firebase/firestore/DocumentReference;"                       \
-    "Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;"              \
-    "[Ljava/lang/Object;)Lcom/google/firebase/firestore/WriteBatch;"),         \
-  X(Delete, "delete", "(Lcom/google/firebase/firestore/DocumentReference;)"    \
-    "Lcom/google/firebase/firestore/WriteBatch;"),                             \
-  X(Commit, "commit", "()Lcom/google/android/gms/tasks/Task;")
-// clang-format on
-
-METHOD_LOOKUP_DECLARATION(write_batch, WRITE_BATCH_METHODS)
-METHOD_LOOKUP_DEFINITION(write_batch,
-                         PROGUARD_KEEP_CLASS
-                         "com/google/firebase/firestore/WriteBatch",
-                         WRITE_BATCH_METHODS)
+namespace {
+
+using jni::Env;
+using jni::HashMap;
+using jni::Local;
+using jni::Method;
+using jni::Object;
+
+constexpr char kClassName[] =
+    PROGUARD_KEEP_CLASS "com/google/firebase/firestore/WriteBatch";
+Method kSet(
+    "set",
+    "(Lcom/google/firebase/firestore/DocumentReference;Ljava/lang/Object;"
+    "Lcom/google/firebase/firestore/SetOptions;)"
+    "Lcom/google/firebase/firestore/WriteBatch;");
+Method kUpdate(
+    "update",
+    "(Lcom/google/firebase/firestore/DocumentReference;Ljava/util/Map;)"
+    "Lcom/google/firebase/firestore/WriteBatch;");
+Method kUpdateVarargs(
+    "update",
+    "(Lcom/google/firebase/firestore/DocumentReference;"
+    "Lcom/google/firebase/firestore/FieldPath;Ljava/lang/Object;"
+    "[Ljava/lang/Object;)Lcom/google/firebase/firestore/WriteBatch;");
+Method kDelete("delete",
+                       "(Lcom/google/firebase/firestore/DocumentReference;)"
+                       "Lcom/google/firebase/firestore/WriteBatch;");
+Method kCommit("commit", "()Lcom/google/android/gms/tasks/Task;");
+
+}  // namespace
+
+void WriteBatchInternal::Initialize(jni::Loader& loader) {
+  loader.LoadClass(kClassName, kSet, kUpdate, kUpdateVarargs, kDelete, kCommit);
+}
 
 void WriteBatchInternal::Set(const DocumentReference& document,
                              const MapFieldValue& data,
                              const SetOptions& options) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  jobject data_map = MapFieldValueToJavaMap(firestore_, data);
-  jobject java_options = SetOptionsInternal::ToJavaObject(env, options);
-  env->CallObjectMethod(obj_, write_batch::GetMethodId(write_batch::kSet),
-                        document.internal_->java_object(), data_map,
-                        java_options);
-  env->DeleteLocalRef(data_map);
-  env->DeleteLocalRef(java_options);
-  CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  Local java_data = MakeJavaMap(env, data);
+  Local java_options = SetOptionsInternal::Create(env, options);
+
+  env.Call(obj_, kSet, ToJni(document), java_data, java_options);
 }
 
 void WriteBatchInternal::Update(const DocumentReference& document,
                                 const MapFieldValue& data) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
+  Env env = GetEnv();
+  Local java_data = MakeJavaMap(env, data);
 
-  jobject data_map = MapFieldValueToJavaMap(firestore_, data);
-  env->CallObjectMethod(obj_, write_batch::GetMethodId(write_batch::kUpdate),
-                        document.internal_->java_object(), data_map);
-  env->DeleteLocalRef(data_map);
-  CheckAndClearJniExceptions(env);
+  env.Call(obj_, kUpdate, ToJni(document), java_data);
 }
 
 void WriteBatchInternal::Update(const DocumentReference& document,
@@ -71,60 +70,26 @@ void WriteBatchInternal::Update(const DocumentReference& document,
     return;
   }
 
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  auto iter = data.begin();
-  jobject first_field = FieldPathConverter::ToJavaObject(env, iter->first);
-  jobject first_value = iter->second.internal_->java_object();
-  ++iter;
-
-  // Make the varargs
-  jobjectArray more_fields_and_values =
-      MapFieldPathValueToJavaArray(firestore_, iter, data.end());
-
-  env->CallObjectMethod(obj_,
-                        write_batch::GetMethodId(write_batch::kUpdateVarargs),
-                        document.internal_->java_object(), first_field,
-                        first_value, more_fields_and_values);
-  env->DeleteLocalRef(first_field);
-  env->DeleteLocalRef(more_fields_and_values);
-  CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  UpdateFieldPathArgs args = MakeUpdateFieldPathArgs(env, data);
+
+  env.Call(obj_, kUpdateVarargs, ToJni(document), args.first_field,
+           args.first_value, args.varargs);
 }
 
 void WriteBatchInternal::Delete(const DocumentReference& document) {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-
-  env->CallObjectMethod(obj_, write_batch::GetMethodId(write_batch::kDelete),
-                        document.internal_->java_object());
-  CheckAndClearJniExceptions(env);
+  Env env = GetEnv();
+  env.Call(obj_, kDelete, ToJni(document));
 }
 
 Future WriteBatchInternal::Commit() {
-  JNIEnv* env = firestore_->app()->GetJNIEnv();
-  jobject task = env->CallObjectMethod(
-      obj_, write_batch::GetMethodId(write_batch::kCommit));
-  CheckAndClearJniExceptions(env);
-
-  auto promise = promises_.MakePromise();
-  promise.RegisterForTask(WriteBatchFn::kCommit, task);
-  env->DeleteLocalRef(task);
-  CheckAndClearJniExceptions(env);
-  return promise.GetFuture();
-}
-
-/* static */
-bool WriteBatchInternal::Initialize(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  jobject activity = app->activity();
-  bool result = write_batch::CacheMethodIds(env, activity);
-  util::CheckAndClearJniExceptions(env);
-  return result;
+  Env env = GetEnv();
+  Local task = env.Call(obj_, kCommit);
+  return promises_.NewFuture(env, AsyncFn::kCommit, task);
 }
 
-/* static */
-void WriteBatchInternal::Terminate(App* app) {
-  JNIEnv* env = app->GetJNIEnv();
-  write_batch::ReleaseClass(env);
-  util::CheckAndClearJniExceptions(env);
+jni::Object WriteBatchInternal::ToJni(const DocumentReference& reference) {
+  return reference.internal_ ? reference.internal_->ToJava() : jni::Object();
 }
 
 }  // namespace firestore
diff --git a/firestore/src/android/write_batch_android.h b/firestore/src/android/write_batch_android.h
index 1d5130dbe6..d814975f02 100644
--- a/firestore/src/android/write_batch_android.h
+++ b/firestore/src/android/write_batch_android.h
@@ -10,19 +10,21 @@
 namespace firebase {
 namespace firestore {
 
-// Each API of WriteBatch that returns a Future needs to define an enum value
-// here. For example, a Future-returning method Foo() relies on the enum value
-// kFoo. The enum values are used to identify and manage Future in the Firestore
-// Future manager.
-enum class WriteBatchFn {
-  kCommit = 0,
-  kCount,  // Must be the last enum value.
-};
-
 class WriteBatchInternal : public Wrapper {
  public:
   using ApiType = WriteBatch;
 
+  // Each API of WriteBatch that returns a Future needs to define an enum value
+  // here. For example, a Future-returning method Foo() relies on the enum value
+  // kFoo. The enum values are used to identify and manage Future in the
+  // Firestore Future manager.
+  enum class AsyncFn {
+    kCommit = 0,
+    kCount,  // Must be the last enum value.
+  };
+
+  static void Initialize(jni::Loader& loader);
+
   WriteBatchInternal(FirestoreInternal* firestore, jobject object)
       : Wrapper(firestore, object), promises_(firestore) {}
 
@@ -38,12 +40,18 @@ class WriteBatchInternal : public Wrapper {
   Future Commit();
 
  private:
-  friend class FirestoreInternal;
-
-  static bool Initialize(App* app);
-  static void Terminate(App* app);
-
-  PromiseFactory promises_;
+  /**
+   * Converts a public DocumentReference to a non-owning proxy for its backing
+   * Java object. The Java object is owned by the DocumentReference.
+   *
+   * Note: this method is not visible to `Env`, so this must still be invoked
+   * manually for arguments passed to `Env` methods.
+   */
+  // TODO(mcg): Move this out of WriteBatchInternal
+  // This needs to be here now because of existing friend relationships.
+  static jni::Object ToJni(const DocumentReference& reference);
+
+  PromiseFactory promises_;
 };
 
 }  // namespace firestore
diff --git a/firestore/src/common/compiler_info.cc b/firestore/src/common/compiler_info.cc
new file mode 100644
index 0000000000..6dac679eb5
--- /dev/null
+++ b/firestore/src/common/compiler_info.cc
@@ -0,0 +1,213 @@
+// Copyright 2020 Google LLC
+//
+// 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.
+
+#include "firestore/src/common/compiler_info.h"
+
+#include 
+#include 
+
+#include "app/meta/move.h"
+
+namespace firebase {
+namespace firestore {
+
+namespace {
+
+// The code in this file is adapted from
+// https://github.com/googleapis/google-cloud-cpp/blob/7f418183fc4f07cd76995bd12f7c1971b8e057ac/google/cloud/internal/compiler_info.cc
+// and
+// https://github.com/googleapis/google-cloud-cpp/blob/7f418183fc4f07cd76995bd12f7c1971b8e057ac/google/cloud/internal/port_platform.h.
+
+/**
+ * Returns the compiler ID.
+ *
+ * The Compiler ID is a string like "GNU" or "Clang", as described by
+ * https://cmake.org/cmake/help/v3.5/variable/CMAKE_LANG_COMPILER_ID.html
+ */
+struct CompilerId {};
+
+std::ostream& operator<<(std::ostream& os, CompilerId) {
+  // The macros for determining the compiler ID are taken from:
+  // https://gitlab.kitware.com/cmake/cmake/tree/v3.5.0/Modules/Compiler/\*-DetermineCompiler.cmake
+  // We do not care to detect every single compiler possible and only target
+  // the most popular ones.
+  //
+  // Order is significant as some compilers can define the same macros.
+
+#if defined(__apple_build_version__) && defined(__clang__)
+  os << "AppleClang";
+#elif defined(__clang__)
+  os << "Clang";
+#elif defined(__GNUC__)
+  os << "GNU";
+#elif defined(_MSC_VER)
+  os << "MSVC";
+#else
+  os << "Unknown";
+#endif
+  return os;
+}
+
+/** Returns the compiler version. This string will be something like "9.1.1". */
+struct CompilerVersion {};
+
+std::ostream& operator<<(std::ostream& os, CompilerVersion) {
+#if defined(__apple_build_version__) && defined(__clang__)
+  os << __clang_major__ << "." << __clang_minor__ << "." << __clang_patchlevel__
+     << "." << __apple_build_version__;
+
+#elif defined(__clang__)
+  os << __clang_major__ << "." << __clang_minor__ << "."
+     << __clang_patchlevel__;
+
+#elif defined(__GNUC__)
+  os << __GNUC__ << "." << __GNUC_MINOR__ << "." << __GNUC_PATCHLEVEL__;
+
+#elif defined(_MSC_VER)
+  os << _MSC_VER / 100 << ".";
+  os << _MSC_VER % 100;
+#if defined(_MSC_FULL_VER)
+#if _MSC_VER >= 1400
+  os << "." << _MSC_FULL_VER % 100000;
+#else
+  os << "." << _MSC_FULL_VER % 10000;
+#endif  // _MSC_VER >= 1400
+#endif  // defined(_MSC_VER)
+
+#else
+  os << "Unknown";
+
+#endif  // defined(__apple_build_version__) && defined(__clang__)
+
+  return os;
+}
+
+// Discover if exceptions are enabled and define them as needed.
+#if defined(__clang__)
+#if defined(__EXCEPTIONS) && __has_feature(cxx_exceptions)
+#define FIRESTORE_HAVE_EXCEPTIONS 1
+#endif  // defined(__EXCEPTIONS) && __has_feature(cxx_exceptions)
+
+#elif defined(_MSC_VER)
+#if defined(_CPPUNWIND)
+#define FIRESTORE_HAVE_EXCEPTIONS 1
+#endif  // defined(_CPPUNWIND)
+
+#elif defined(__GNUC__)
+#if (__GNUC__ < 5) && defined(__EXCEPTIONS)
+#define FIRESTORE_HAVE_EXCEPTIONS 1
+#elif (__GNUC__ >= 5) && defined(__cpp_exceptions)
+#define FIRESTORE_HAVE_EXCEPTIONS 1
+#endif  // (__GNUC__ >= 5) && defined(__cpp_exceptions)
+
+#elif defined(__cpp_exceptions)
+// This should work in increasingly more and more compilers.
+// https://isocpp.org/std/standing-documents/sd-6-sg10-feature-test-recommendations
+#define FIRESTORE_HAVE_EXCEPTIONS 1
+#endif  // FIRESTORE_HAVE_EXCEPTIONS
+
+/**
+ * Returns certain interesting compiler features.
+ *
+ * Currently this returns one of "ex" or "noex" to indicate whether or not
+ * C++ exceptions are enabled.
+ */
+struct CompilerFeatures {};
+
+std::ostream& operator<<(std::ostream& os, CompilerFeatures) {
+#if FIRESTORE_HAVE_EXCEPTIONS
+  os << "ex";
+#else
+  os << "noex";
+#endif  // FIRESTORE_HAVE_EXCEPTIONS
+  return os;
+}
+
+// Microsoft Visual Studio does not define `__cplusplus` correctly by default:
+// https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus
+// Instead, `_MSVC_LANG` macro can be used which uses the same version numbers
+// as the standard `__cplusplus` macro (except when the `/std:c++latest` option
+// is used, in which case it will be higher).
+#ifdef _MSC_VER
+#define FIRESTORE__CPLUSPLUS _MSVC_LANG
+#else
+#define FIRESTORE__CPLUSPLUS __cplusplus
+#endif  // _MSC_VER
+
+/** Returns the 4-digit year of the C++ language standard. */
+struct LanguageVersion {};
+
+std::ostream& operator<<(std::ostream& os, LanguageVersion) {
+  switch (FIRESTORE__CPLUSPLUS) {
+    case 199711L:
+      os << "1998";
+      break;
+    case 201103L:
+      os << "2011";
+      break;
+    case 201402L:
+      os << "2014";
+      break;
+    case 201703L:
+      os << "2017";
+      break;
+    case 202002L:
+      os << "2020";
+      break;
+    default:
+#ifdef _MSC_VER
+      // According to
+      // https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros,
+      // _MSVC_LANG is "set to a higher, unspecified value when the
+      // `/std:c++latest` option is specified".
+      if (FIRESTORE__CPLUSPLUS > 202002L) {
+        os << "latest";
+        break;
+      }
+#endif  // _MSC_VER
+      os << "unknown";
+      break;
+  }
+
+  return os;
+}
+
+struct StandardLibraryVendor {};
+
+std::ostream& operator<<(std::ostream& os, StandardLibraryVendor) {
+#if defined(_STLPORT_VERSION)
+  os << "stlport";
+#elif defined(__GLIBCXX__) || defined(__GLIBCPP__)
+  os << "gnustl";
+#elif defined(_LIBCPP_STD_VER)
+  os << "libcpp";
+#elif defined(_MSC_VER)
+  os << "msvc";
+#else
+  os << "unknown";
+#endif
+  return os;
+}
+
+}  // namespace
+
+std::string GetFullCompilerInfo() {
+  std::ostringstream os;
+  os << CompilerId() << "-" << CompilerVersion() << "-" << CompilerFeatures()
+     << "-" << LanguageVersion() << "-" << StandardLibraryVendor();
+  return Move(os).str();
+}
+
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/common/compiler_info.h b/firestore/src/common/compiler_info.h
new file mode 100644
index 0000000000..bc3ac6663b
--- /dev/null
+++ b/firestore/src/common/compiler_info.h
@@ -0,0 +1,38 @@
+// Copyright 2020 Google LLC
+//
+// 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.
+
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_COMPILER_INFO_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_COMPILER_INFO_H_
+
+#include 
+
+namespace firebase {
+namespace firestore {
+
+// Returns a string describing the compiler version and settings in the
+// following format:
+//
+//   ----
+//
+// e.g. "AppleClang-11.0.3.11030032-ex-2011-libcpp".
+//
+// The format is based on what is used by Cloud C++ libraries:
+// https://github.com/googleapis/google-cloud-cpp/blob/211006b86c841f2226fedf2f7ae6ced482aa2cc0/google/cloud/internal/api_client_header.cc#L23-L29
+// with the addition of  (e.g. "libcpp").
+std::string GetFullCompilerInfo();
+
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_COMMON_COMPILER_INFO_H_
diff --git a/firestore/src/common/document_reference.cc b/firestore/src/common/document_reference.cc
index 93e1db66fd..ac02b67540 100644
--- a/firestore/src/common/document_reference.cc
+++ b/firestore/src/common/document_reference.cc
@@ -152,14 +152,16 @@ Future DocumentReference::Delete() {
 
 #if defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
 ListenerRegistration DocumentReference::AddSnapshotListener(
-    std::function callback) {
+    std::function
+        callback) {
   return AddSnapshotListener(MetadataChanges::kExclude,
                              firebase::Move(callback));
 }
 
 ListenerRegistration DocumentReference::AddSnapshotListener(
     MetadataChanges metadata_changes,
-    std::function callback) {
+    std::function
+        callback) {
   FIREBASE_ASSERT_MESSAGE(callback, "invalid callback parameter is passed in.");
   if (!internal_) return {};
   return internal_->AddSnapshotListener(metadata_changes,
diff --git a/firestore/src/common/firestore.cc b/firestore/src/common/firestore.cc
index a6fc59c25a..4d30f91bd7 100644
--- a/firestore/src/common/firestore.cc
+++ b/firestore/src/common/firestore.cc
@@ -9,6 +9,7 @@
 #include "app/src/include/firebase/version.h"
 #include "app/src/log.h"
 #include "app/src/util.h"
+#include "firestore/src/common/compiler_info.h"
 #include "firestore/src/common/futures.h"
 
 #if defined(__ANDROID__)
@@ -19,6 +20,10 @@
 #include "firestore/src/ios/firestore_ios.h"
 #endif  // defined(__ANDROID__)
 
+#ifdef __APPLE__
+#include "TargetConditionals.h"
+#endif  // __APPLE__
+
 namespace firebase {
 namespace firestore {
 
@@ -26,6 +31,22 @@ DEFINE_FIREBASE_VERSION_STRING(FirebaseFirestore);
 
 namespace {
 
+const char* GetPlatform() {
+#if defined(__ANDROID__)
+  return "gl-android/";
+#elif TARGET_OS_IOS
+  return "gl-ios/";
+#elif TARGET_OS_OSX
+  return "gl-macos/";
+#elif defined(_WIN32)
+  return "gl-windows/";
+#elif defined(__linux__)
+  return "gl-linux/";
+#else
+  return "";
+#endif
+}
+
 Mutex g_firestores_lock;  // NOLINT
 std::map* g_firestores = nullptr;
 
@@ -123,6 +144,12 @@ Firestore::Firestore(FirestoreInternal* internal)
     : internal_(internal) {
   internal_->set_firestore_public(this);
 
+  // Note: because Firestore libraries are currently distributed in
+  // a precompiled form, `GetFullCompilerInfo` will reflect the compiler used to
+  // produce the binaries. Unfortunately, there is no clear way to avoid that
+  // without breaking ODR.
+  SetClientLanguage(std::string("gl-cpp/") + GetFullCompilerInfo());
+
   if (internal_->initialized()) {
     CleanupNotifier* app_notifier = CleanupNotifier::FindByOwner(app());
     assert(app_notifier);
@@ -279,5 +306,15 @@ ListenerRegistration Firestore::AddSnapshotsInSyncListener(
 }
 #endif  // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
 
+void Firestore::SetClientLanguage(const std::string& language_token) {
+  // TODO(b/135633112): this is a temporary measure until the Firestore backend
+  // rolls out Firebase platform logging.
+  // Note: this implementation lumps together the language and platform tokens,
+  // relying on the fact that `SetClientLanguage` doesn't validate or parse its
+  // input in any way. This is deemed acceptable because reporting the platform
+  // this way is a temporary measure.
+  FirestoreInternal::SetClientLanguage(language_token + " " + GetPlatform());
+}
+
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/common/query.cc b/firestore/src/common/query.cc
index 4dbaa175d1..96ca77cf2a 100644
--- a/firestore/src/common/query.cc
+++ b/firestore/src/common/query.cc
@@ -244,14 +244,16 @@ Future Query::Get(Source source) const {
 
 #if defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
 ListenerRegistration Query::AddSnapshotListener(
-    std::function callback) {
+    std::function
+        callback) {
   return AddSnapshotListener(MetadataChanges::kExclude,
                              firebase::Move(callback));
 }
 
 ListenerRegistration Query::AddSnapshotListener(
     MetadataChanges metadata_changes,
-    std::function callback) {
+    std::function
+        callback) {
   FIREBASE_ASSERT_MESSAGE(callback, "invalid callback parameter is passed in.");
   if (!internal_) return {};
   return internal_->AddSnapshotListener(metadata_changes,
diff --git a/firestore/src/common/wrapper_assertions.h b/firestore/src/common/wrapper_assertions.h
index 68aefd697a..4272765ae4 100644
--- a/firestore/src/common/wrapper_assertions.h
+++ b/firestore/src/common/wrapper_assertions.h
@@ -7,6 +7,7 @@
 #if defined(__ANDROID__)
 #include 
 
+#include "app/src/util_android.h"
 #include "firestore/src/android/firestore_android.h"
 #elif defined(FIRESTORE_STUB_BUILD)
 #include "firestore/src/stub/firestore_stub.h"
diff --git a/firestore/src/include/firebase/csharp/api_headers.h b/firestore/src/include/firebase/csharp/api_headers.h
new file mode 100644
index 0000000000..5ede195c7b
--- /dev/null
+++ b/firestore/src/include/firebase/csharp/api_headers.h
@@ -0,0 +1,23 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_API_HEADERS_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_API_HEADERS_H_
+
+#include "firebase/firestore.h"
+
+namespace firebase {
+namespace firestore {
+namespace csharp {
+
+// This class allows limited access to private functions of `Firestore` related
+// to sending Cloud headers.
+class ApiHeaders {
+ public:
+  static void SetClientLanguage(const std::string& language_token) {
+    Firestore::SetClientLanguage(language_token);
+  }
+};
+
+}  // namespace csharp
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_API_HEADERS_H_
diff --git a/firestore/src/include/firebase/csharp/document_event_listener.cc b/firestore/src/include/firebase/csharp/document_event_listener.cc
index 46eae53789..440e60a367 100644
--- a/firestore/src/include/firebase/csharp/document_event_listener.cc
+++ b/firestore/src/include/firebase/csharp/document_event_listener.cc
@@ -1,30 +1,21 @@
 #include "firebase/csharp/document_event_listener.h"
 
-#include "app/src/assert.h"
 #include "firebase/firestore/document_reference.h"
 
 namespace firebase {
 namespace firestore {
 namespace csharp {
 
-void DocumentEventListener::OnEvent(const DocumentSnapshot& value,
-                                    Error error) {
-  // Ownership of this pointer is passed into the C# handler
-  auto* copy = new DocumentSnapshot(value);
-
-  callback_(callback_id_, copy, error);
-}
-
-/* static */
-ListenerRegistration DocumentEventListener::AddListenerTo(
+ListenerRegistration AddDocumentSnapshotListener(
     DocumentReference* reference, MetadataChanges metadata_changes,
     int32_t callback_id, DocumentEventListenerCallback callback) {
-  DocumentEventListener listener(callback_id, callback);
-
   return reference->AddSnapshotListener(
       metadata_changes,
-      [listener](const DocumentSnapshot& value, Error error) mutable {
-        listener.OnEvent(value, error);
+      [callback, callback_id](const DocumentSnapshot& value, Error error_code,
+                              const std::string& error_message) {
+        // Ownership of the DocumentSnapshot pointer is passed to C#.
+        callback(callback_id, new DocumentSnapshot(value), error_code,
+                 error_message.c_str());
       });
 }
 
diff --git a/firestore/src/include/firebase/csharp/document_event_listener.h b/firestore/src/include/firebase/csharp/document_event_listener.h
index b87362f976..301485b479 100644
--- a/firestore/src/include/firebase/csharp/document_event_listener.h
+++ b/firestore/src/include/firebase/csharp/document_event_listener.h
@@ -1,14 +1,10 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_DOCUMENT_EVENT_LISTENER_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_DOCUMENT_EVENT_LISTENER_H_
 
-#include 
-
-#include "app/src/callback.h"
-#include "app/src/mutex.h"
 #include "firebase/firestore/document_snapshot.h"
-#include "firebase/firestore/event_listener.h"
 #include "firebase/firestore/listener_registration.h"
 #include "firebase/firestore/metadata_changes.h"
+#include "firebase/firestore/firestore_errors.h"
 
 namespace firebase {
 namespace firestore {
@@ -25,33 +21,19 @@ namespace csharp {
 #endif
 
 // The callbacks that are used by the listener, that need to reach back to C#
-// callbacks.
-typedef void(SWIGSTDCALL* DocumentEventListenerCallback)(int callback_id,
-                                                         void* snapshot,
-                                                         Error error);
-
-// Provide a C++ implementation of the EventListener for DocumentSnapshot that
-// can forward the calls back to the C# delegates.
-class DocumentEventListener : public EventListener {
- public:
-  explicit DocumentEventListener(int32_t callback_id,
-                                 DocumentEventListenerCallback callback)
-      : callback_id_(callback_id), callback_(callback) {}
-
-  void OnEvent(const DocumentSnapshot& value, Error error) override;
-
-  // This method is a proxy to DocumentReference::AddSnapshotListener()
-  // that can be easily called from C#. It allows our C# wrapper to
-  // track user callbacks in a dictionary keyed off of a unique int
-  // for each user callback and then raise the correct one later.
-  static ListenerRegistration AddListenerTo(
-      DocumentReference* reference, MetadataChanges metadata_changes,
-      int32_t callback_id, DocumentEventListenerCallback callback);
-
- private:
-  int32_t callback_id_;
-  DocumentEventListenerCallback callback_;
-};
+// callbacks. The error_message pointer is only valid for the duration of the
+// callback.
+typedef void(SWIGSTDCALL* DocumentEventListenerCallback)(
+    int callback_id, DocumentSnapshot* snapshot, Error error_code,
+    const char* error_message);
+
+// This method is a proxy to DocumentReference::AddSnapshotListener()
+// that can be easily called from C#. It allows our C# wrapper to
+// track user callbacks in a dictionary keyed off of a unique int
+// for each user callback and then raise the correct one later.
+ListenerRegistration AddDocumentSnapshotListener(
+    DocumentReference* reference, MetadataChanges metadata_changes,
+    int32_t callback_id, DocumentEventListenerCallback callback);
 
 }  // namespace csharp
 }  // namespace firestore
diff --git a/firestore/src/include/firebase/csharp/query_event_listener.cc b/firestore/src/include/firebase/csharp/query_event_listener.cc
index e3207c1db8..15c5808117 100644
--- a/firestore/src/include/firebase/csharp/query_event_listener.cc
+++ b/firestore/src/include/firebase/csharp/query_event_listener.cc
@@ -1,29 +1,21 @@
 #include "firebase/csharp/query_event_listener.h"
 
-#include "app/src/assert.h"
 #include "firebase/firestore/query.h"
 
 namespace firebase {
 namespace firestore {
 namespace csharp {
 
-void QueryEventListener::OnEvent(const QuerySnapshot& value, Error error) {
-  // Ownership of this pointer is passed into the C# handler
-  auto* copy = new QuerySnapshot(value);
-
-  callback_(callback_id_, copy, error);
-}
-
-/* static */
-ListenerRegistration QueryEventListener::AddListenerTo(
+ListenerRegistration AddQuerySnapshotListener(
     Query* query, MetadataChanges metadata_changes, int32_t callback_id,
     QueryEventListenerCallback callback) {
-  QueryEventListener listener(callback_id, callback);
-
   return query->AddSnapshotListener(
       metadata_changes,
-      [listener](const QuerySnapshot& value, Error error) mutable {
-        listener.OnEvent(value, error);
+      [callback, callback_id](const QuerySnapshot& value, Error error_code,
+                              const std::string& error_message) {
+        // Ownership of the QuerySnapshot pointer is passed to C#.
+        callback(callback_id, new QuerySnapshot(value), error_code,
+                 error_message.c_str());
       });
 }
 
diff --git a/firestore/src/include/firebase/csharp/query_event_listener.h b/firestore/src/include/firebase/csharp/query_event_listener.h
index 38968e28b7..0b2f1c0322 100644
--- a/firestore/src/include/firebase/csharp/query_event_listener.h
+++ b/firestore/src/include/firebase/csharp/query_event_listener.h
@@ -1,14 +1,9 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_QUERY_EVENT_LISTENER_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_QUERY_EVENT_LISTENER_H_
 
-#include 
-
-#include "app/src/callback.h"
-#include "app/src/mutex.h"
-#include "firebase/firestore/event_listener.h"
 #include "firebase/firestore/listener_registration.h"
-#include "firebase/firestore/metadata_changes.h"
 #include "firebase/firestore/query_snapshot.h"
+#include "firebase/firestore/firestore_errors.h"
 
 namespace firebase {
 namespace firestore {
@@ -24,33 +19,19 @@ namespace csharp {
 #endif
 
 // The callbacks that are used by the listener, that need to reach back to C#
-// callbacks.
-typedef void(SWIGSTDCALL* QueryEventListenerCallback)(int callback_id,
-                                                      void* snapshot,
-                                                      Error error);
-
-// Provide a C++ implementation of the EventListener for QuerySnapshot that
-// can forward the calls back to the C# delegates.
-class QueryEventListener : public EventListener {
- public:
-  explicit QueryEventListener(int32_t callback_id,
-                              QueryEventListenerCallback callback)
-      : callback_id_(callback_id), callback_(callback) {}
-
-  void OnEvent(const QuerySnapshot& value, Error error) override;
-
-  // This method is a proxy to Query::AddSnapshotsListener()
-  // that can be easily called from C#. It allows our C# wrapper to
-  // track user callbacks in a dictionary keyed off of a unique int
-  // for each user callback and then raise the correct one later.
-  static ListenerRegistration AddListenerTo(
-      Query* query, MetadataChanges metadata_changes, int32_t callback_id,
-      QueryEventListenerCallback callback);
-
- private:
-  int32_t callback_id_;
-  QueryEventListenerCallback callback_;
-};
+// callbacks. The error_message pointer is only valid for the duration of the
+// callback.
+typedef void(SWIGSTDCALL* QueryEventListenerCallback)(
+    int32_t callback_id, QuerySnapshot* snapshot, Error error_code,
+    const char* error_message);
+
+// This method is a proxy to Query::AddSnapshotsListener()
+// that can be easily called from C#. It allows our C# wrapper to
+// track user callbacks in a dictionary keyed off of a unique int
+// for each user callback and then raise the correct one later.
+ListenerRegistration AddQuerySnapshotListener(
+    Query* query, MetadataChanges metadata_changes, int32_t callback_id,
+    QueryEventListenerCallback callback);
 
 }  // namespace csharp
 }  // namespace firestore
diff --git a/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.cc b/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.cc
index b5f3e7766a..0fa5b67ba0 100644
--- a/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.cc
+++ b/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.cc
@@ -1,21 +1,14 @@
 #include "firebase/csharp/snapshots_in_sync_listener.h"
 
-#include "app/src/assert.h"
-
 namespace firebase {
 namespace firestore {
 namespace csharp {
 
-void SnapshotsInSyncListener::OnEvent(Error error) { callback_(callback_id_); }
-
-/* static */
-ListenerRegistration SnapshotsInSyncListener::AddListenerTo(
+ListenerRegistration AddSnapshotsInSyncListener(
     Firestore* firestore, int32_t callback_id,
     SnapshotsInSyncCallback callback) {
-  SnapshotsInSyncListener listener(callback_id, callback);
-
   return firestore->AddSnapshotsInSyncListener(
-      [listener]() mutable { listener.OnEvent(Error::kErrorOk); });
+      [callback, callback_id]() { callback(callback_id); });
 }
 
 }  // namespace csharp
diff --git a/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.h b/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.h
index 353c5488ed..374031abd1 100644
--- a/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.h
+++ b/firestore/src/include/firebase/csharp/snapshots_in_sync_listener.h
@@ -1,13 +1,7 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_SNAPSHOTS_IN_SYNC_LISTENER_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_SNAPSHOTS_IN_SYNC_LISTENER_H_
 
-#include 
-
-#include "app/src/callback.h"
 #include "firebase/firestore.h"
-#include "firebase/firestore/event_listener.h"
-#include "firebase/firestore/listener_registration.h"
-#include "firebase/firestore/firestore_errors.h"
 
 namespace firebase {
 namespace firestore {
@@ -27,28 +21,13 @@ namespace csharp {
 // callbacks.
 typedef void(SWIGSTDCALL* SnapshotsInSyncCallback)(int callback_id);
 
-// Provides a C++ implementation of the EventListener for
-// ListenForSnapshotsInSync that can forward the calls back to the C# delegates.
-class SnapshotsInSyncListener : public EventListener {
- public:
-  SnapshotsInSyncListener(int32_t callback_id,
-                          SnapshotsInSyncCallback callback)
-      : callback_id_(callback_id), callback_(callback) {}
-
-  void OnEvent(Error error) override;
-
-  // This method is a proxy to Firestore::AddSnapshotsInSyncListener()
-  // that can be easily called from C#. It allows our C# wrapper to
-  // track user callbacks in a dictionary keyed off of a unique int
-  // for each user callback and then raise the correct one later.
-  static ListenerRegistration AddListenerTo(Firestore* firestore,
-                                            int32_t callback_id,
-                                            SnapshotsInSyncCallback callback);
-
- private:
-  int32_t callback_id_;
-  SnapshotsInSyncCallback callback_;
-};
+// This method is a proxy to Firestore::AddSnapshotsInSyncListener()
+// that can be easily called from C#. It allows our C# wrapper to
+// track user callbacks in a dictionary keyed off of a unique int
+// for each user callback and then raise the correct one later.
+ListenerRegistration AddSnapshotsInSyncListener(
+    Firestore* firestore, int32_t callback_id,
+    SnapshotsInSyncCallback callback);
 
 }  // namespace csharp
 }  // namespace firestore
diff --git a/firestore/src/include/firebase/csharp/typemap_helper.h b/firestore/src/include/firebase/csharp/typemap_helper.h
index 9137f0b11e..ba51ddb3d6 100644
--- a/firestore/src/include/firebase/csharp/typemap_helper.h
+++ b/firestore/src/include/firebase/csharp/typemap_helper.h
@@ -2,6 +2,7 @@
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_CSHARP_TYPEMAP_HELPER_H_
 
 #include 
+#include 
 
 #include "firebase/firestore/document_reference.h"
 #include "firebase/firestore/document_snapshot.h"
@@ -16,18 +17,34 @@ namespace firebase {
 namespace firestore {
 namespace csharp {
 
-struct TransactionGetResult {
-  DocumentSnapshot snapshot;
-  Error error_code = Error::kErrorUnknown;
-  std::string error_message;
+class TransactionGetResult {
+ public:
+  TransactionGetResult(DocumentSnapshot snapshot, Error error_code,
+                       std::string error_message)
+      : snapshot_(std::move(snapshot)),
+        error_code_(error_code),
+        error_message_(std::move(error_message)) {}
+
+  DocumentSnapshot TakeSnapshot() { return std::move(snapshot_); }
+
+  Error error_code() const { return error_code_; }
+
+  const std::string& error_message() const { return error_message_; }
+
+ private:
+  DocumentSnapshot snapshot_;
+  Error error_code_ = Error::kErrorUnknown;
+  std::string error_message_;
 };
 
 TransactionGetResult TransactionGet(Transaction* transaction,
                                     const DocumentReference& document) {
-  TransactionGetResult result;
-  result.snapshot =
-      transaction->Get(document, &result.error_code, &result.error_message);
-  return result;
+  Error error_code = Error::kErrorUnknown;
+  std::string error_message;
+  DocumentSnapshot snapshot =
+      transaction->Get(document, &error_code, &error_message);
+  return TransactionGetResult(std::move(snapshot), error_code,
+                              std::move(error_message));
 }
 
 }  // namespace csharp
diff --git a/firestore/src/include/firebase/firestore.h b/firestore/src/include/firebase/firestore.h
index 6b63d3afdd..16e82c2385 100644
--- a/firestore/src/include/firebase/firestore.h
+++ b/firestore/src/include/firebase/firestore.h
@@ -62,6 +62,8 @@ namespace firestore {
 
 class FirestoreInternal;
 
+namespace csharp { class ApiHeaders; }
+
 /**
  * @brief Entry point for the Firebase Firestore C++ SDK.
  *
@@ -414,6 +416,8 @@ class Firestore {
   template 
   friend struct CleanupFn;
 
+  friend class csharp::ApiHeaders;
+
   explicit Firestore(::firebase::App* app);
   explicit Firestore(FirestoreInternal* internal);
 
@@ -423,6 +427,8 @@ class Firestore {
   static Firestore* AddFirestoreToCache(Firestore* firestore,
                                         InitResult* init_result_out);
 
+  static void SetClientLanguage(const std::string& language_token);
+
   // Delete the internal_ data.
   void DeleteInternal();
 
diff --git a/firestore/src/include/firebase/firestore/document_reference.h b/firestore/src/include/firebase/firestore/document_reference.h
index f85b1313ec..3b49975e06 100644
--- a/firestore/src/include/firebase/firestore/document_reference.h
+++ b/firestore/src/include/firebase/firestore/document_reference.h
@@ -257,6 +257,8 @@ class DocumentReference {
    *
    * @param[in] callback The std::function to call. When this function is
    * called, snapshot value is valid if and only if error is Error::kErrorOk.
+   * The std::string is an error message; the value may be empty if an error
+   * message is not available.
    *
    * @return A registration object that can be used to remove the listener.
    *
@@ -264,7 +266,8 @@ class DocumentReference {
    * library.
    */
   virtual ListenerRegistration AddSnapshotListener(
-      std::function callback);
+      std::function
+          callback);
 
   /**
    * @brief Starts listening to the document referenced by this
@@ -275,6 +278,8 @@ class DocumentReference {
    * events.
    * @param[in] callback The std::function to call. When this function is
    * called, snapshot value is valid if and only if error is Error::kErrorOk.
+   * The std::string is an error message; the value may be empty if an error
+   * message is not available.
    *
    * @return A registration object that can be used to remove the listener.
    *
@@ -283,7 +288,8 @@ class DocumentReference {
    */
   virtual ListenerRegistration AddSnapshotListener(
       MetadataChanges metadata_changes,
-      std::function callback);
+      std::function
+          callback);
 #endif  // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
 
 #if !defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
diff --git a/firestore/src/include/firebase/firestore/event_listener.h b/firestore/src/include/firebase/firestore/event_listener.h
index c9c12825c7..bfe5657fde 100644
--- a/firestore/src/include/firebase/firestore/event_listener.h
+++ b/firestore/src/include/firebase/firestore/event_listener.h
@@ -17,6 +17,8 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_FIRESTORE_EVENT_LISTENER_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_INCLUDE_FIREBASE_FIRESTORE_EVENT_LISTENER_H_
 
+#include 
+
 #include "firebase/firestore/firestore_errors.h"
 
 namespace firebase {
@@ -45,9 +47,13 @@ class EventListener {
    * Error::kErrorOk.
    *
    * @param value The value of the event. Invalid if there was an error.
-   * @param error The error if there was error. Error::kErrorOk otherwise.
+   * @param error_code The error code if there was an error. Error::kErrorOk
+   * otherwise.
+   * @param error_message The error message if there was an error. An empty
+   * string otherwise.
    */
-  virtual void OnEvent(const T& value, Error error) = 0;
+  virtual void OnEvent(const T& value, Error error_code,
+                       const std::string& error_message) = 0;
 };
 
 /**
@@ -68,9 +74,12 @@ class EventListener {
   /**
    * @brief OnEvent will be called with the error if an error occurred.
    *
-   * @param error The error if there was error. Error::kErrorOk otherwise.
+   * @param error_code The error code if there was an error. Error::kErrorOk
+   * otherwise.
+   * @param error_message The error message if there was an error. An empty
+   * string otherwise.
    */
-  virtual void OnEvent(Error error) = 0;
+  virtual void OnEvent(Error error_code, const std::string& error_message) = 0;
 };
 
 }  // namespace firestore
diff --git a/firestore/src/include/firebase/firestore/query.h b/firestore/src/include/firebase/firestore/query.h
index 8c59f541d5..5691465368 100644
--- a/firestore/src/include/firebase/firestore/query.h
+++ b/firestore/src/include/firebase/firestore/query.h
@@ -531,6 +531,8 @@ class Query {
    *
    * @param[in] callback The std::function to call. When this function is
    * called, snapshot value is valid if and only if error is Error::kErrorOk.
+   * The std::string is an error message; the value may be empty if an error
+   * message is not available.
    *
    * @return A registration object that can be used to remove the listener.
    *
@@ -538,7 +540,8 @@ class Query {
    * library.
    */
   virtual ListenerRegistration AddSnapshotListener(
-      std::function callback);
+      std::function
+          callback);
 
   /**
    * @brief Starts listening to the QuerySnapshot events referenced by this
@@ -549,6 +552,8 @@ class Query {
    * events.
    * @param[in] callback The std::function to call. When this function is
    * called, snapshot value is valid if and only if error is Error::kErrorOk.
+   * The std::string is an error message; the value may be empty if an error
+   * message is not available.
    *
    * @return A registration object that can be used to remove the listener.
    *
@@ -557,7 +562,8 @@ class Query {
    */
   virtual ListenerRegistration AddSnapshotListener(
       MetadataChanges metadata_changes,
-      std::function callback);
+      std::function
+          callback);
 #endif  // defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
 
 #if !defined(FIREBASE_USE_STD_FUNCTION) || defined(DOXYGEN)
diff --git a/firestore/src/ios/document_reference_ios.cc b/firestore/src/ios/document_reference_ios.cc
index 503e75a536..26ea4ed717 100644
--- a/firestore/src/ios/document_reference_ios.cc
+++ b/firestore/src/ios/document_reference_ios.cc
@@ -91,7 +91,8 @@ ListenerRegistration DocumentReferenceInternal::AddSnapshotListener(
 
 ListenerRegistration DocumentReferenceInternal::AddSnapshotListener(
     MetadataChanges metadata_changes,
-    std::function callback) {
+    std::function
+        callback) {
   auto options = core::ListenOptions::FromIncludeMetadataChanges(
       metadata_changes == MetadataChanges::kInclude);
   auto result = reference_.AddSnapshotListener(
diff --git a/firestore/src/ios/document_reference_ios.h b/firestore/src/ios/document_reference_ios.h
index 911b549380..47f0f93e7a 100644
--- a/firestore/src/ios/document_reference_ios.h
+++ b/firestore/src/ios/document_reference_ios.h
@@ -53,7 +53,8 @@ class DocumentReferenceInternal {
 
   ListenerRegistration AddSnapshotListener(
       MetadataChanges metadata_changes,
-      std::function callback);
+      std::function
+          callback);
 
   const api::DocumentReference& document_reference_core() const {
     return reference_;
diff --git a/firestore/src/ios/firestore_ios.cc b/firestore/src/ios/firestore_ios.cc
index 647c062de6..738526b16e 100644
--- a/firestore/src/ios/firestore_ios.cc
+++ b/firestore/src/ios/firestore_ios.cc
@@ -5,6 +5,7 @@
 #include "app/src/include/firebase/future.h"
 #include "app/src/reference_counted_future_impl.h"
 #include "auth/src/include/firebase/auth.h"
+#include "firestore/src/common/util.h"
 #include "firestore/src/include/firebase/firestore.h"
 #include "firestore/src/ios/converter_ios.h"
 #include "firestore/src/ios/credentials_provider_ios.h"
@@ -227,7 +228,7 @@ void FirestoreInternal::ClearListeners() {
 ListenerRegistration FirestoreInternal::AddSnapshotsInSyncListener(
     EventListener* listener) {
   std::function listener_function = [listener] {
-    listener->OnEvent(Error::kErrorOk);
+    listener->OnEvent(Error::kErrorOk, EmptyString());
   };
   auto result = firestore_core_->AddSnapshotsInSyncListener(
       ListenerWithCallback(std::move(listener_function)));
@@ -294,5 +295,9 @@ void Firestore::set_log_level(LogLevel log_level) {
                                                     : log_level);
 }
 
+void FirestoreInternal::SetClientLanguage(const std::string& language_token) {
+  api::Firestore::SetClientLanguage(language_token);
+}
+
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/ios/firestore_ios.h b/firestore/src/ios/firestore_ios.h
index 605a10900e..6473b28221 100644
--- a/firestore/src/ios/firestore_ios.h
+++ b/firestore/src/ios/firestore_ios.h
@@ -102,6 +102,8 @@ class FirestoreInternal {
     return firestore_core_;
   }
 
+  static void SetClientLanguage(const std::string& language_token);
+
  private:
   friend class TestFriend;
 
diff --git a/firestore/src/ios/listener_ios.h b/firestore/src/ios/listener_ios.h
index 99ac11155c..0db6188313 100644
--- a/firestore/src/ios/listener_ios.h
+++ b/firestore/src/ios/listener_ios.h
@@ -5,6 +5,7 @@
 #include 
 #include 
 
+#include "firestore/src/common/util.h"
 #include "firestore/src/ios/converter_ios.h"
 #include "firestore/src/ios/promise_ios.h"
 #include "firebase/firestore/firestore_errors.h"
@@ -42,16 +43,16 @@ std::unique_ptr> ListenerWithPromise(
 //   a public API type (`To`).
 template 
 std::unique_ptr> ListenerWithCallback(
-    std::function callback) {
+    std::function callback) {
   return core::EventListener::Create(
       [callback](util::StatusOr maybe_value) mutable {
         if (maybe_value.ok()) {
           From from = std::move(maybe_value).ValueOrDie();
           To to = MakePublic(std::move(from));
-          callback(std::move(to), Error::kErrorOk);
-
+          callback(std::move(to), Error::kErrorOk, EmptyString());
         } else {
-          callback(To{}, maybe_value.status().code());
+          callback(To{}, maybe_value.status().code(),
+                   maybe_value.status().error_message());
         }
       });
 }
@@ -74,7 +75,10 @@ template 
 std::unique_ptr> ListenerWithEventListener(
     EventListener* listener) {
   return ListenerWithCallback(
-      [listener](To result, Error error) { listener->OnEvent(result, error); });
+      [listener](To result, Error error_code,
+                 const std::string& error_message) {
+        listener->OnEvent(result, error_code, error_message);
+      });
 }
 
 inline util::StatusCallback StatusCallbackWithPromise(Promise promise) {
diff --git a/firestore/src/ios/query_ios.cc b/firestore/src/ios/query_ios.cc
index 2e1c172a1d..c3a4dc1581 100644
--- a/firestore/src/ios/query_ios.cc
+++ b/firestore/src/ios/query_ios.cc
@@ -109,7 +109,8 @@ ListenerRegistration QueryInternal::AddSnapshotListener(
 
 ListenerRegistration QueryInternal::AddSnapshotListener(
     MetadataChanges metadata_changes,
-    std::function callback) {
+    std::function
+        callback) {
   auto options = core::ListenOptions::FromIncludeMetadataChanges(
       metadata_changes == MetadataChanges::kInclude);
   auto result = query_.AddSnapshotListener(
diff --git a/firestore/src/ios/query_ios.h b/firestore/src/ios/query_ios.h
index fbf56fdad2..ef527a7c21 100644
--- a/firestore/src/ios/query_ios.h
+++ b/firestore/src/ios/query_ios.h
@@ -39,7 +39,8 @@ class QueryInternal {
 
   ListenerRegistration AddSnapshotListener(
       MetadataChanges metadata_changes,
-      std::function callback);
+      std::function
+          callback);
 
   // Delegating methods
 
diff --git a/firestore/src/jni/array.h b/firestore/src/jni/array.h
new file mode 100644
index 0000000000..9923b14613
--- /dev/null
+++ b/firestore/src/jni/array.h
@@ -0,0 +1,38 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ARRAY_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ARRAY_H_
+
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+#include "firestore/src/jni/traits.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+template 
+class Array : public Object {
+ public:
+  using jni_type = JniType>;
+
+  Array() = default;
+  explicit Array(jni_type array) : Object(array) {}
+
+  jni_type get() const override { return static_cast(Object::get()); }
+
+  size_t Size(Env& env) const { return env.GetArrayLength(*this); }
+
+  Local Get(Env& env, size_t i) const {
+    return env.GetArrayElement(*this, i);
+  }
+
+  void Set(Env& env, size_t i, const T& value) {
+    env.SetArrayElement(*this, i, value);
+  }
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ARRAY_H_
diff --git a/firestore/src/jni/array_list.cc b/firestore/src/jni/array_list.cc
new file mode 100644
index 0000000000..aeb75f4fc2
--- /dev/null
+++ b/firestore/src/jni/array_list.cc
@@ -0,0 +1,31 @@
+#include "firestore/src/jni/array_list.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+Constructor kConstructor("()V");
+Constructor kConstructorWithSize("(I)V");
+
+}  // namespace
+
+void ArrayList::Initialize(Loader& loader) {
+  loader.LoadFromExistingClass("java/util/ArrayList",
+                               util::array_list::GetClass(), kConstructor,
+                               kConstructorWithSize);
+}
+
+Local ArrayList::Create(Env& env) { return env.New(kConstructor); }
+
+Local ArrayList::Create(Env& env, size_t size) {
+  return env.New(kConstructorWithSize, size);
+}
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/array_list.h b/firestore/src/jni/array_list.h
new file mode 100644
index 0000000000..4687f04218
--- /dev/null
+++ b/firestore/src/jni/array_list.h
@@ -0,0 +1,25 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ARRAY_LIST_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ARRAY_LIST_H_
+
+#include "firestore/src/jni/list.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/** A C++ proxy for a Java `ArrayList`. */
+class ArrayList : public List {
+ public:
+  using List::List;
+
+  static void Initialize(Loader& loader);
+
+  static Local Create(Env& env);
+  static Local Create(Env& env, size_t size);
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ARRAY_LIST_H_
diff --git a/firestore/src/jni/boolean.cc b/firestore/src/jni/boolean.cc
new file mode 100644
index 0000000000..72293cee36
--- /dev/null
+++ b/firestore/src/jni/boolean.cc
@@ -0,0 +1,37 @@
+#include "firestore/src/jni/boolean.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+constexpr char kClassName[] = "java/lang/Boolean";
+Constructor kConstructor("(Z)V");
+Method kBooleanValue("booleanValue", "()Z");
+jclass g_clazz = nullptr;
+
+}  // namespace
+
+void Boolean::Initialize(Loader& loader) {
+  g_clazz = util::boolean_class::GetClass();
+  loader.LoadFromExistingClass(kClassName, g_clazz, kConstructor,
+                               kBooleanValue);
+}
+
+Class Boolean::GetClass() { return Class(g_clazz); }
+
+Local Boolean::Create(Env& env, bool value) {
+  return env.New(kConstructor, value);
+}
+
+bool Boolean::BooleanValue(Env& env) const {
+  return env.Call(*this, kBooleanValue);
+}
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/boolean.h b/firestore/src/jni/boolean.h
new file mode 100644
index 0000000000..97621348c0
--- /dev/null
+++ b/firestore/src/jni/boolean.h
@@ -0,0 +1,29 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_BOOLEAN_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_BOOLEAN_H_
+
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/** A C++ proxy for a Java `Boolean`. */
+class Boolean : public Object {
+ public:
+  using Object::Object;
+
+  static void Initialize(Loader& loader);
+
+  static Class GetClass();
+
+  static Local Create(Env& env, bool value);
+
+  bool BooleanValue(Env& env) const;
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_BOOLEAN_H_
diff --git a/firestore/src/jni/call_traits.h b/firestore/src/jni/call_traits.h
index d0e0d8814d..d89e8e74e3 100644
--- a/firestore/src/jni/call_traits.h
+++ b/firestore/src/jni/call_traits.h
@@ -21,6 +21,7 @@ struct CallTraits {
   static constexpr auto kCall = &JNIEnv::CallObjectMethod;
   static constexpr auto kGetStaticField = &JNIEnv::GetStaticObjectField;
   static constexpr auto kCallStatic = &JNIEnv::CallStaticObjectMethod;
+  static constexpr auto kNewArray = &JNIEnv::NewObjectArray;
 };
 
 template <>
@@ -28,6 +29,9 @@ struct CallTraits {
   static constexpr auto kCall = &JNIEnv::CallBooleanMethod;
   static constexpr auto kGetStaticField = &JNIEnv::GetStaticBooleanField;
   static constexpr auto kCallStatic = &JNIEnv::CallStaticBooleanMethod;
+  static constexpr auto kNewArray = &JNIEnv::NewBooleanArray;
+  static constexpr auto kGetArrayRegion = &JNIEnv::GetBooleanArrayRegion;
+  static constexpr auto kSetArrayRegion = &JNIEnv::SetBooleanArrayRegion;
 };
 
 template <>
@@ -35,6 +39,9 @@ struct CallTraits {
   static constexpr auto kCall = &JNIEnv::CallByteMethod;
   static constexpr auto kGetStaticField = &JNIEnv::GetStaticByteField;
   static constexpr auto kCallStatic = &JNIEnv::CallStaticByteMethod;
+  static constexpr auto kNewArray = &JNIEnv::NewByteArray;
+  static constexpr auto kGetArrayRegion = &JNIEnv::GetByteArrayRegion;
+  static constexpr auto kSetArrayRegion = &JNIEnv::SetByteArrayRegion;
 };
 
 template <>
@@ -42,6 +49,9 @@ struct CallTraits {
   static constexpr auto kCall = &JNIEnv::CallCharMethod;
   static constexpr auto kGetStaticField = &JNIEnv::GetStaticCharField;
   static constexpr auto kCallStatic = &JNIEnv::CallStaticCharMethod;
+  static constexpr auto kNewArray = &JNIEnv::NewCharArray;
+  static constexpr auto kGetArrayRegion = &JNIEnv::GetCharArrayRegion;
+  static constexpr auto kSetArrayRegion = &JNIEnv::SetCharArrayRegion;
 };
 
 template <>
@@ -49,6 +59,9 @@ struct CallTraits {
   static constexpr auto kCall = &JNIEnv::CallShortMethod;
   static constexpr auto kGetStaticField = &JNIEnv::GetStaticShortField;
   static constexpr auto kCallStatic = &JNIEnv::CallStaticShortMethod;
+  static constexpr auto kNewArray = &JNIEnv::NewShortArray;
+  static constexpr auto kGetArrayRegion = &JNIEnv::GetShortArrayRegion;
+  static constexpr auto kSetArrayRegion = &JNIEnv::SetShortArrayRegion;
 };
 
 template <>
@@ -56,6 +69,9 @@ struct CallTraits {
   static constexpr auto kCall = &JNIEnv::CallIntMethod;
   static constexpr auto kGetStaticField = &JNIEnv::GetStaticIntField;
   static constexpr auto kCallStatic = &JNIEnv::CallStaticIntMethod;
+  static constexpr auto kNewArray = &JNIEnv::NewIntArray;
+  static constexpr auto kGetArrayRegion = &JNIEnv::GetIntArrayRegion;
+  static constexpr auto kSetArrayRegion = &JNIEnv::SetIntArrayRegion;
 };
 
 template <>
@@ -63,6 +79,9 @@ struct CallTraits {
   static constexpr auto kCall = &JNIEnv::CallLongMethod;
   static constexpr auto kGetStaticField = &JNIEnv::GetStaticLongField;
   static constexpr auto kCallStatic = &JNIEnv::CallStaticLongMethod;
+  static constexpr auto kNewArray = &JNIEnv::NewLongArray;
+  static constexpr auto kGetArrayRegion = &JNIEnv::GetLongArrayRegion;
+  static constexpr auto kSetArrayRegion = &JNIEnv::SetLongArrayRegion;
 };
 
 template <>
@@ -70,6 +89,9 @@ struct CallTraits {
   static constexpr auto kCall = &JNIEnv::CallFloatMethod;
   static constexpr auto kGetStaticField = &JNIEnv::GetStaticFloatField;
   static constexpr auto kCallStatic = &JNIEnv::CallStaticFloatMethod;
+  static constexpr auto kNewArray = &JNIEnv::NewFloatArray;
+  static constexpr auto kGetArrayRegion = &JNIEnv::GetFloatArrayRegion;
+  static constexpr auto kSetArrayRegion = &JNIEnv::SetFloatArrayRegion;
 };
 
 template <>
@@ -77,6 +99,9 @@ struct CallTraits {
   static constexpr auto kCall = &JNIEnv::CallDoubleMethod;
   static constexpr auto kGetStaticField = &JNIEnv::GetStaticDoubleField;
   static constexpr auto kCallStatic = &JNIEnv::CallStaticDoubleMethod;
+  static constexpr auto kNewArray = &JNIEnv::NewDoubleArray;
+  static constexpr auto kGetArrayRegion = &JNIEnv::GetDoubleArrayRegion;
+  static constexpr auto kSetArrayRegion = &JNIEnv::SetDoubleArrayRegion;
 };
 
 template <>
diff --git a/firestore/src/jni/class.cc b/firestore/src/jni/class.cc
new file mode 100644
index 0000000000..4a79a9680c
--- /dev/null
+++ b/firestore/src/jni/class.cc
@@ -0,0 +1,35 @@
+#include "firestore/src/jni/class.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+constexpr char kClass[] = "java/lang/Class";
+Method kGetName("getName", "()Ljava/lang/String;");
+Method kIsArray("isArray", "()Z");
+
+}  // namespace
+
+void Class::Initialize(Loader& loader) {
+  loader.LoadFromExistingClass(kClass, util::class_class::GetClass(), kGetName,
+                               kIsArray);
+}
+
+std::string Class::GetName(Env& env) const {
+  return env.Call(*this, kGetName).ToString(env);
+}
+
+std::string Class::GetClassName(Env& env, const Object& object) {
+  return util::JObjectClassName(env.get(), object.get());
+}
+
+bool Class::IsArray(Env& env) const { return env.Call(*this, kIsArray); }
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/class.h b/firestore/src/jni/class.h
index e85edc86d9..9892a3839c 100644
--- a/firestore/src/jni/class.h
+++ b/firestore/src/jni/class.h
@@ -1,6 +1,9 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_CLASS_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_CLASS_H_
 
+#include 
+
+#include "firestore/src/jni/jni_fwd.h"
 #include "firestore/src/jni/object.h"
 
 namespace firebase {
@@ -21,6 +24,20 @@ class Class : public Object {
   explicit Class(jclass clazz) : Object(clazz) {}
 
   jclass get() const override { return static_cast(object_); }
+
+  /**
+   * Returns the name of the class, as returned by the Java `Class.name` method.
+   */
+  std::string GetName(Env& env) const;
+
+  static std::string GetClassName(Env& env, const Object& object);
+
+  bool IsArray(Env& env) const;
+
+ private:
+  friend class Loader;
+
+  static void Initialize(Loader& loader);
 };
 
 }  // namespace jni
diff --git a/firestore/src/jni/collection.cc b/firestore/src/jni/collection.cc
new file mode 100644
index 0000000000..a3ca074398
--- /dev/null
+++ b/firestore/src/jni/collection.cc
@@ -0,0 +1,36 @@
+#include "firestore/src/jni/collection.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/iterator.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+constexpr char kClass[] = "java/util/Collection";
+Method kAdd("add", "(Ljava/lang/Object;)Z");
+Method kIterator("iterator", "()Ljava/util/Iterator;");
+Method kSize("size", "()I");
+
+}  // namespace
+
+void Collection::Initialize(Loader& loader) {
+  loader.LoadClass(kClass, kAdd, kIterator, kSize);
+}
+
+bool Collection::Add(Env& env, const Object& object) {
+  return env.Call(*this, kAdd, object);
+}
+
+Local Collection::Iterator(Env& env) {
+  return env.Call(*this, kIterator);
+}
+
+size_t Collection::Size(Env& env) const { return env.Call(*this, kSize); }
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/collection.h b/firestore/src/jni/collection.h
new file mode 100644
index 0000000000..db2fe33359
--- /dev/null
+++ b/firestore/src/jni/collection.h
@@ -0,0 +1,29 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_COLLECTION_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_COLLECTION_H_
+
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+class Iterator;
+
+/** A C++ proxy for a Java `Collection`. */
+class Collection : public Object {
+ public:
+  using Object::Object;
+
+  static void Initialize(Loader& loader);
+
+  bool Add(Env& env, const Object& object);
+  Local Iterator(Env& env);
+  size_t Size(Env& env) const;
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_COLLECTION_H_
diff --git a/firestore/src/jni/declaration.h b/firestore/src/jni/declaration.h
new file mode 100644
index 0000000000..88a9f78a5b
--- /dev/null
+++ b/firestore/src/jni/declaration.h
@@ -0,0 +1,181 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_DECLARATION_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_DECLARATION_H_
+
+#include 
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+class Class;
+class Env;
+
+/**
+ * A base class containing the details of a Java constructor. See
+ * Constructor.
+ */
+class ConstructorBase {
+ public:
+  /**
+   * Creates a new method descriptor from an argument signature. The argument
+   * should be a string literal--this class does not take ownership of nor copy
+   * the string. The internal method ID should be populated later with a call to
+   * `Loader::Load`.
+   */
+  constexpr explicit ConstructorBase(const char* sig) : sig_(sig) {}
+
+  ConstructorBase(jclass clazz, jmethodID id) : clazz_(clazz), id_(id) {}
+
+  jclass clazz() const { return clazz_; }
+  jmethodID id() const { return id_; }
+
+ private:
+  friend class Loader;
+
+  const char* sig_ = nullptr;
+
+  jclass clazz_ = nullptr;
+  jmethodID id_ = nullptr;
+};
+
+/**
+ * A declaration of a Java constructor. This is intended to be used as a global
+ * variable and loaded once the JavaVM is available.
+ *
+ * @tparam T The C++ type of the new object.
+ */
+template 
+class Constructor : public ConstructorBase {
+ public:
+  using ConstructorBase::ConstructorBase;
+};
+
+/**
+ * A base class containing the details of a Java method. See Method.
+ */
+class MethodBase {
+ public:
+  /**
+   * Creates a new method descriptor from a name and signature. These arguments
+   * should be string literals--this class does not take ownership of nor copy
+   * these strings. The internal method ID should be populated later with a call
+   * to `Loader::Load`.
+   */
+  constexpr MethodBase(const char* name, const char* sig)
+      : name_(name), sig_(sig) {}
+
+  explicit MethodBase(jmethodID id) : id_(id) {}
+
+  jmethodID id() const { return id_; }
+
+ private:
+  friend class Loader;
+
+  const char* name_ = nullptr;
+  const char* sig_ = nullptr;
+
+  jmethodID id_ = nullptr;
+};
+
+/**
+ * A declaration of a Java method. This is intended to be used as a global
+ * variable and loaded once the JavaVM is available.
+ *
+ * @tparam T The C++ return type of the method.
+ */
+template 
+class Method : public MethodBase {
+ public:
+  using MethodBase::MethodBase;
+};
+
+/**
+ * A base class containing the details of a Java static field. See
+ * StaticField.
+ */
+class StaticFieldBase {
+ public:
+  /**
+   * Creates a new static field descriptor from a name and signature. These
+   * arguments should be string literals--this class does not take ownership of
+   * these strings. The internal class and field ID should be populated later
+   * with a call to `Loader::Load`.
+   */
+  constexpr StaticFieldBase(const char* name, const char* sig)
+      : name_(name), sig_(sig) {}
+
+  explicit StaticFieldBase(jfieldID id) : id_(id) {}
+
+  jclass clazz() const { return clazz_; }
+
+  jfieldID id() const { return id_; }
+
+ private:
+  friend class Loader;
+
+  const char* name_ = nullptr;
+  const char* sig_ = nullptr;
+
+  jclass clazz_ = nullptr;
+  jfieldID id_ = nullptr;
+};
+
+/**
+ * A declaration of a Java static field. This is intended to be used as a global
+ * variable and loaded once the JavaVM is available.
+ *
+ * @tparam T The C++ type of the field.
+ */
+template 
+class StaticField : public StaticFieldBase {
+ public:
+  using StaticFieldBase::StaticFieldBase;
+};
+
+/**
+ * A base class containing the details of a Java static method. See
+ * StaticMethod.
+ */
+class StaticMethodBase {
+ public:
+  /**
+   * Creates a new static method descriptor from a name and signature. These
+   * arguments should be string literals--this class does not take ownership of
+   * nor copy these strings. The internal method ID should be populated later
+   * with a call to `Loader::Load`.
+   */
+  constexpr StaticMethodBase(const char* name, const char* sig)
+      : name_(name), sig_(sig) {}
+
+  StaticMethodBase(jclass clazz, jmethodID id) : clazz_(clazz), id_(id) {}
+
+  jclass clazz() const { return clazz_; }
+  jmethodID id() const { return id_; }
+
+ private:
+  friend class Loader;
+
+  const char* name_ = nullptr;
+  const char* sig_ = nullptr;
+
+  jclass clazz_ = nullptr;
+  jmethodID id_ = nullptr;
+};
+
+/**
+ * A declaration of a Java static method. This is intended to be used as a
+ * global variable and loaded once the JavaVM is available.
+ *
+ * @tparam T The C++ return type of the method.
+ */
+template 
+class StaticMethod : public StaticMethodBase {
+ public:
+  using StaticMethodBase::StaticMethodBase;
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_DECLARATION_H_
diff --git a/firestore/src/jni/double.cc b/firestore/src/jni/double.cc
new file mode 100644
index 0000000000..4bfd0e1c3f
--- /dev/null
+++ b/firestore/src/jni/double.cc
@@ -0,0 +1,36 @@
+#include "firestore/src/jni/double.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+constexpr char kClassName[] = "java/lang/Double";
+Constructor kConstructor("(D)V");
+Method kDoubleValue("doubleValue", "()D");
+jclass g_clazz = nullptr;
+
+}  // namespace
+
+void Double::Initialize(Loader& loader) {
+  g_clazz = util::double_class::GetClass();
+  loader.LoadFromExistingClass(kClassName, g_clazz, kConstructor, kDoubleValue);
+}
+
+Class Double::GetClass() { return Class(g_clazz); }
+
+Local Double::Create(Env& env, double value) {
+  return env.New(kConstructor, value);
+}
+
+double Double::DoubleValue(Env& env) const {
+  return env.Call(*this, kDoubleValue);
+}
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/double.h b/firestore/src/jni/double.h
new file mode 100644
index 0000000000..59432ee9a4
--- /dev/null
+++ b/firestore/src/jni/double.h
@@ -0,0 +1,29 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_DOUBLE_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_DOUBLE_H_
+
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/** A C++ proxy for a Java `Double`. */
+class Double : public Object {
+ public:
+  using Object::Object;
+
+  static void Initialize(Loader& loader);
+
+  static Class GetClass();
+
+  static Local Create(Env& env, double value);
+
+  double DoubleValue(Env& env) const;
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_DOUBLE_H_
diff --git a/firestore/src/jni/env.cc b/firestore/src/jni/env.cc
index 32c265bb68..6d791f84ff 100644
--- a/firestore/src/jni/env.cc
+++ b/firestore/src/jni/env.cc
@@ -1,15 +1,121 @@
 #include "firestore/src/jni/env.h"
 
+#include "app/src/assert.h"
+#include "app/src/log.h"
+
+// Add constant declarations missing from the NDK's jni.h
+#ifndef JNI_ENOMEM
+#define JNI_ENOMEM -4
+#define JNI_EEXIST -5
+#define JNI_EINVAL -6
+#endif
+
 namespace firebase {
 namespace firestore {
 namespace jni {
+namespace {
+
+/**
+ * Returns the number of currently pending exceptions. This can be more than
+ * one if an exception is thrown in a try-catch block in a destructor. Returns
+ * zero if exceptions are disabled.
+ */
+int CurrentExceptionCount() {
+#if !__cpp_exceptions
+  return 0;
+
+#elif __cplusplus >= 201703L
+  return std::uncaught_exceptions();
+
+#else
+  return std::uncaught_exception() ? 1 : 0;
+#endif  // !__cpp_exceptions
+}
+
+}  // namespace
+
+Env::Env() : Env(GetEnv()) {}
+
+Env::Env(JNIEnv* env)
+    : env_(env), initial_pending_exceptions_(CurrentExceptionCount()) {}
+
+Env::~Env() noexcept(false) {
+  if (ok()) return;
+
+  if (exception_handler_ &&
+      CurrentExceptionCount() == initial_pending_exceptions_) {
+    exception_handler_(*this, ExceptionOccurred(), context_);
+  }
+
+  // If no unhandled exception handler is registered, leave the exception
+  // pending in the JNIEnv. This will either propagate out to another Env
+  // instance that does have a handler installed or will propagate out to the
+  // JVM.
+}
 
 Local Env::FindClass(const char* name) {
+  if (!ok()) return {};
+
   jclass result = env_->FindClass(name);
   RecordException();
   return Local(env_, result);
 }
 
+void Env::Throw(const Throwable& throwable) {
+  if (!ok()) return;
+
+  jint result = env_->Throw(throwable.get());
+  FIREBASE_ASSERT_MESSAGE(
+      result == JNI_OK, "Failed to throw an exception %s: %s",
+      ErrorDescription(throwable).c_str(), ErrorName(result));
+}
+
+void Env::ThrowNew(const Class& clazz, const char* message) {
+  if (!ok()) return;
+
+  jint result = env_->ThrowNew(clazz.get(), message);
+  FIREBASE_ASSERT_MESSAGE(
+      result == JNI_OK, "Failed to throw %s with message %s: %s",
+      ErrorDescription(clazz).c_str(), message, ErrorName(result));
+}
+
+Local Env::ExceptionOccurred() {
+  jthrowable exception = env_->ExceptionOccurred();
+  return Local(env_, exception);
+}
+
+void Env::ExceptionClear() { env_->ExceptionClear(); }
+
+Local Env::ClearExceptionOccurred() {
+  Local result = ExceptionOccurred();
+  ExceptionClear();
+  return result;
+}
+
+Local Env::GetObjectClass(const Object& object) {
+  if (!ok()) return {};
+
+  jclass result = env_->GetObjectClass(object.get());
+  RecordException();
+  return Local(env_, result);
+}
+
+bool Env::IsInstanceOf(const Object& object, const Class& clazz) {
+  if (!ok()) return false;
+
+  jboolean result = env_->IsInstanceOf(object.get(), clazz.get());
+  RecordException();
+  return result;
+}
+
+bool Env::IsSameObject(const Object& object1, const Object& object2) {
+  if (!ok()) return false;
+
+  jboolean result = env_->IsSameObject(object1.get(), object2.get());
+  RecordException();
+  return result;
+}
+
 jmethodID Env::GetMethodId(const Class& clazz, const char* name,
                            const char* sig) {
   if (!ok()) return nullptr;
@@ -45,7 +151,8 @@ Local Env::NewStringUtf(const char* bytes) {
   return Local(env_, result);
 }
 
-std::string Env::GetStringUtfRegion(jstring string, size_t start, size_t len) {
+std::string Env::GetStringUtfRegion(const String& string, size_t start,
+                                    size_t len) {
   if (!ok()) return "";
 
   // Copy directly into the std::string buffer. This is guaranteed to work as
@@ -53,7 +160,7 @@ std::string Env::GetStringUtfRegion(jstring string, size_t start, size_t len) {
   std::string result;
   result.resize(len);
 
-  env_->GetStringUTFRegion(string, ToJni(start), ToJni(len), &result[0]);
+  env_->GetStringUTFRegion(string.get(), ToJni(start), ToJni(len), &result[0]);
   RecordException();
 
   // Ensure that if there was an exception, the contents of the buffer are
@@ -63,12 +170,45 @@ std::string Env::GetStringUtfRegion(jstring string, size_t start, size_t len) {
 }
 
 void Env::RecordException() {
-  if (last_exception_ || !env_->ExceptionCheck()) return;
+  if (ok()) return;
 
   env_->ExceptionDescribe();
+}
+
+std::string Env::ErrorDescription(const Object& object) {
+  ExceptionClearGuard block(*this);
+  std::string result = object.ToString(*this);
+  if (ok()) {
+    return result;
+  }
+
+  auto exception = ExceptionOccurred();
+
+  ExceptionClearGuard block2(*this);
+  std::string message = exception.GetMessage(*this);
+  return std::string("(unknown object: failed trying to describe it: ") +
+         message + ")";
+}
 
-  last_exception_ = env_->ExceptionOccurred();
-  env_->ExceptionClear();
+const char* Env::ErrorName(jint error) {
+  switch (error) {
+    case JNI_OK:
+      return "no error (JNI_OK)";
+    case JNI_ERR:
+      return "general JNI failure (JNI_ERR)";
+    case JNI_EDETACHED:
+      return "thread detached from the VM (JNI_EDETACHED)";
+    case JNI_EVERSION:
+      return "JNI version error (JNI_EVERSION)";
+    case JNI_ENOMEM:
+      return "not enough memory (JNI_ENOMEM)";
+    case JNI_EEXIST:
+      return "VM already created (JNI_EEXIST)";
+    case JNI_EINVAL:
+      return "invalid arguments (JNI_EINVAL)";
+    default:
+      return "unexpected error code";
+  }
 }
 
 }  // namespace jni
diff --git a/firestore/src/jni/env.h b/firestore/src/jni/env.h
index a77d64e9cc..e1bca9bb69 100644
--- a/firestore/src/jni/env.h
+++ b/firestore/src/jni/env.h
@@ -4,13 +4,17 @@
 #include 
 
 #include 
+#include 
+#include 
 
 #include "app/meta/move.h"
 #include "firestore/src/jni/call_traits.h"
 #include "firestore/src/jni/class.h"
+#include "firestore/src/jni/declaration.h"
 #include "firestore/src/jni/object.h"
 #include "firestore/src/jni/ownership.h"
 #include "firestore/src/jni/string.h"
+#include "firestore/src/jni/throwable.h"
 #include "firestore/src/jni/traits.h"
 
 namespace firebase {
@@ -38,12 +42,40 @@ namespace jni {
  */
 class Env {
  public:
-  Env() : env_(GetEnv()) {}
+  /**
+   * An unhandled exception handler for Java exceptions that can be registered
+   * by calling `SetUnhandledExceptionHandler`. The unhandled exception handler
+   * is not invoked immediately after a Java exception is observed via JNI.
+   * Instead it is invoked if `Env` starts destruction with a Java exception
+   * pending.
+   *
+   * When calling the `UncaughtExceptionHandler`, `Env` does not automatically
+   * clear any pending exceptions. The handler should call `Env::ExceptionClear`
+   * if it wishes to clear the pending exception or use `ExceptionClearGuard` to
+   * temporarily clear the pending exception.
+   */
+  using UnhandledExceptionHandler = void (*)(
+      jni::Env& env, jni::Local&& exception, void* context);
+
+  Env();
+
+  explicit Env(JNIEnv* env);
 
-  explicit Env(JNIEnv* env) : env_(env) {}
+  /**
+   * Destroys the Env instance. This can throw if an `UnhandledExceptionHandler`
+   * has been registered and that function itself throws and there is no
+   * exception currently being handled.
+   */
+  ~Env() noexcept(false);
+
+  Env(const Env&) = delete;
+  Env& operator=(const Env&) = delete;
+
+  Env(Env&&) noexcept = default;
+  Env& operator=(Env&&) noexcept = default;
 
   /** Returns true if the Env has not encountered an exception. */
-  bool ok() const { return last_exception_ == nullptr; }
+  bool ok() const { return !env_->ExceptionCheck(); }
 
   /** Returns the underlying JNIEnv pointer. */
   JNIEnv* get() const { return env_; }
@@ -56,6 +88,39 @@ class Env {
    */
   Local FindClass(const char* name);
 
+  // MARK: Exceptions
+
+  void Throw(const Throwable& throwable);
+
+  void ThrowNew(const Class& clazz, const char* message);
+  void ThrowNew(const Class& clazz, const std::string& message) {
+    ThrowNew(clazz, message.c_str());
+  }
+
+  /**
+   * Returns the last Java exception to occur or an invalid reference. The
+   * exception is cleared with a call to `ExceptionClear`.
+   */
+  Local ExceptionOccurred();
+
+  /** Clears the last exception. */
+  void ExceptionClear();
+
+  /**
+   * Returns the last Java exception to occur and clears the pending exception.
+   */
+  Local ClearExceptionOccurred();
+
+  /**
+   * Sets the exception handler to automatically invoke if there's a pending
+   * exception when `Env` is being destroyed.
+   */
+  void SetUnhandledExceptionHandler(UnhandledExceptionHandler handler,
+                                    void* context) {
+    exception_handler_ = handler;
+    context_ = context;
+  }
+
   // MARK: Object Operations
 
   /**
@@ -80,6 +145,31 @@ class Env {
     return MakeResult(result);
   }
 
+  template 
+  Local New(const Constructor& ctor, Args&&... args) {
+    if (!ok()) return {};
+
+    auto result =
+        env_->NewObject(ctor.clazz(), ctor.id(), ToJni(Forward(args))...);
+    RecordException();
+    return MakeResult(result);
+  }
+
+  Local GetObjectClass(const Object& object);
+
+  bool IsInstanceOf(const Object& object, const Class& clazz);
+  bool IsInstanceOf(const Object& object, jclass clazz) {
+    return IsInstanceOf(object, Class(clazz));
+  }
+  bool IsInstanceOf(jobject object, const Class& clazz) {
+    return IsInstanceOf(Object(object), clazz);
+  }
+  bool IsInstanceOf(jobject object, jclass clazz) {
+    return IsInstanceOf(Object(object), Class(clazz));
+  }
+
+  bool IsSameObject(const Object& object1, const Object& object2);
+
   // MARK: Calling Instance Methods
 
   /**
@@ -107,6 +197,32 @@ class Env {
                          ToJni(Forward(args))...);
   }
 
+  // Temporarily allow passing the object parameter with type jobject.
+  // TODO(mcg): Remove once migration is complete
+  template 
+  ResultType Call(jobject object, jmethodID method, Args&&... args) {
+    auto env_method = CallTraits>::kCall;
+    return CallHelper(env_method, object, method,
+                         ToJni(Forward(args))...);
+  }
+
+  template 
+  ResultType Call(const Object& object, const Method& method,
+                     Args&&... args) {
+    auto env_method = CallTraits>::kCall;
+    return CallHelper(env_method, object.get(), method.id(),
+                         ToJni(Forward(args))...);
+  }
+
+  // Temporarily allow passing the object parameter with type jobject.
+  // TODO(mcg): Remove once migration is complete
+  template 
+  ResultType Call(jobject object, const Method& method, Args&&... args) {
+    auto env_method = CallTraits>::kCall;
+    return CallHelper(env_method, object, method.id(),
+                         ToJni(Forward(args))...);
+  }
+
   // MARK: Accessing Static Fields
 
   jfieldID GetStaticFieldId(const Class& clazz, const char* name,
@@ -131,6 +247,16 @@ class Env {
     return MakeResult(result);
   }
 
+  template 
+  ResultType Get(const StaticField& field) {
+    if (!ok()) return {};
+
+    auto env_method = CallTraits>::kGetStaticField;
+    auto result = INVOKE(env_, env_method, field.clazz(), field.id());
+    RecordException();
+    return MakeResult(result);
+  }
+
   // MARK: Calling Static Methods
 
   /**
@@ -161,6 +287,13 @@ class Env {
                          ToJni(Forward(args))...);
   }
 
+  template 
+  ResultType Call(const StaticMethod& method, Args&&... args) {
+    auto env_method = CallTraits>::kCallStatic;
+    return CallHelper(env_method, method.clazz(), method.id(),
+                         ToJni(Forward(args))...);
+  }
+
   // MARK: String Operations
 
   /**
@@ -173,23 +306,149 @@ class Env {
   }
 
   /** Returns the length of the string in modified UTF-8 bytes. */
-  size_t GetStringUtfLength(jstring string) {
-    jsize result = env_->GetStringUTFLength(string);
+  size_t GetStringUtfLength(const String& string) {
+    if (!ok()) return 0;
+
+    jsize result = env_->GetStringUTFLength(string.get());
     RecordException();
     return static_cast(result);
   }
-  size_t GetStringUtfLength(const String& string) {
-    return GetStringUtfLength(string.get());
-  }
 
   /**
    * Copies the contents of a region of a Java string to a C++ string. The
    * resulting string has a modified UTF-8 encoding.
    */
-  std::string GetStringUtfRegion(jstring string, size_t start, size_t len);
   std::string GetStringUtfRegion(const String& string, size_t start,
-                                 size_t len) {
-    return GetStringUtfRegion(string.get(), start, len);
+                                 size_t len);
+
+  // MARK: Array Operations
+
+  /**
+   * Returns the length of the given Java Array.
+   */
+  template 
+  size_t GetArrayLength(const Array& array) {
+    if (!ok()) return 0;
+
+    jsize result = env_->GetArrayLength(array.get());
+    RecordException();
+    return static_cast(result);
+  }
+
+  /**
+   * Creates a new object array where `element_class` is the required type for
+   * each element.
+   *
+   * @tparam T The type of the C++ proxy for elements of the array.
+   * @param size The fixed size of the array.
+   * @param element_class The required class of each element, equivalent to the
+   *     literal Java type before the square brackets. That is, to create a Java
+   *     `String[]`, pass `String::GetClass()` for `element_class`.
+   */
+  template 
+  EnableForReference>> NewArray(size_t size,
+                                                  jclass element_class) {
+    if (!ok()) return {};
+
+    jobjectArray result =
+        env_->NewObjectArray(ToJni(size), element_class, nullptr);
+    RecordException();
+    return MakeResult>(result);
+  }
+
+  template 
+  EnableForReference>> NewArray(size_t size,
+                                               const Class& element_class) {
+    return NewArray(size, element_class.get());
+  }
+
+  /**
+   * Creates a new primitive array where the element type is derived from the
+   * JNI type of `T`.
+   *
+   * @tparam T A C++ primitive type (like `uint8_t`) that maps onto a Java
+   *     primitive type (like `byte`).
+   * @param size The fixed size of the array.
+   */
+  template 
+  EnableForPrimitive>> NewArray(size_t size) {
+    if (!ok()) return {};
+
+    auto env_method = CallTraits>::kNewArray;
+    auto result = INVOKE(env_, env_method, ToJni(size));
+    RecordException();
+    return MakeResult>(result);
+  }
+
+  /**
+   * Returns a reference to the element at the given index in the Java object
+   * array.
+   */
+  template 
+  EnableForReference> GetArrayElement(const Array& array,
+                                                  size_t index) {
+    if (!ok()) return {};
+
+    jobject result = env_->GetObjectArrayElement(ToJni(array), ToJni(index));
+    RecordException();
+    return MakeResult(result);
+  }
+
+  /**
+   * Sets the value at the given index in the Java object array.
+   */
+  template 
+  EnableForReference SetArrayElement(Array& array, size_t index,
+                                              const Object& value) {
+    if (!ok()) return;
+
+    env_->SetObjectArrayElement(ToJni(array), ToJni(index), ToJni(value));
+    RecordException();
+  }
+
+  /**
+   * Copies elements in the given range from the Java array to the C++ buffer.
+   * The caller must ensure that the buffer is large enough to copy `len`
+   * elements.
+   */
+  template 
+  EnableForPrimitive GetArrayRegion(const Array& array,
+                                             size_t start, size_t len,
+                                             T* buffer) {
+    if (!ok()) return;
+
+    auto env_method = CallTraits>::kGetArrayRegion;
+    INVOKE(env_, env_method, ToJni(array), ToJni(start), ToJni(len),
+           ToJni(buffer));
+    RecordException();
+  }
+
+  /**
+   * Copies elements in the given range from the Java array to a new C++ vector.
+   */
+  template 
+  EnableForPrimitive> GetArrayRegion(const Array& array,
+                                                       size_t start,
+                                                       size_t len) {
+    std::vector result(len);
+    GetArrayRegion(array, start, len, &result[0]);
+    return result;
+  }
+
+  /**
+   * Copies elements from the C++ buffer into the given range of the Java array.
+   * The caller must ensure that the array is large enough to copy `len`
+   * elements.
+   */
+  template 
+  EnableForPrimitive SetArrayRegion(Array& array, size_t start,
+                                             size_t len, const T* buffer) {
+    if (!ok()) return;
+
+    auto env_method = CallTraits>::kSetArrayRegion;
+    INVOKE(env_, env_method, ToJni(array), ToJni(start), ToJni(len),
+           ToJni(buffer));
+    RecordException();
   }
 
  private:
@@ -232,6 +491,8 @@ class Env {
   }
 
   void RecordException();
+  std::string ErrorDescription(const Object& object);
+  const char* ErrorName(jint error);
 
   template 
   EnableForPrimitive MakeResult(JniType value) {
@@ -248,11 +509,44 @@ class Env {
   }
 
   JNIEnv* env_ = nullptr;
-  jthrowable last_exception_ = nullptr;
+
+  UnhandledExceptionHandler exception_handler_ = nullptr;
+  void* context_ = nullptr;
+  int initial_pending_exceptions_ = 0;
 };
 
 #undef INVOKE
 
+/**
+ * Temporarily clears any pending exception state in the environment by calling
+ * `JNIEnv::ExceptionClear`. If there was an exception pending when
+ * `ExceptionClearGuard` is constructed, the guard restores that exception when
+ * it is destructed. This is useful for executing cleanup code that needs to run
+ * even if an exception is pending, similar to the way a `finally` block works
+ * in Java.
+ *
+ * Like a Java `finally` block, if an exception is thrown before the
+ * `ExceptionClearGuard` is destructed, that exception takes precedence and any
+ * original exception is lost. Exceptions thrown during the lifetime of an
+ * `ExceptionClearGuard` are not suppressed, so if a multi-step cleanup action
+ * can throw, multiple `ExceptionClearGuard`s may be required.
+ */
+class ExceptionClearGuard {
+ public:
+  explicit ExceptionClearGuard(Env& env)
+      : env_(env), exception_(env.ClearExceptionOccurred()) {}
+
+  ~ExceptionClearGuard() {
+    if (exception_) {
+      env_.Throw(exception_);
+    }
+  }
+
+ private:
+  Env& env_;
+  Local exception_;
+};
+
 }  // namespace jni
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/jni/hash_map.cc b/firestore/src/jni/hash_map.cc
new file mode 100644
index 0000000000..8fb06f1de7
--- /dev/null
+++ b/firestore/src/jni/hash_map.cc
@@ -0,0 +1,26 @@
+#include "firestore/src/jni/hash_map.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+constexpr char kClass[] = "java/util/HashMap";
+Constructor kConstructor("()V");
+
+}  // namespace
+
+void HashMap::Initialize(Loader& loader) {
+  loader.LoadFromExistingClass(kClass, util::hash_map::GetClass(),
+                               kConstructor);
+}
+
+Local HashMap::Create(Env& env) { return env.New(kConstructor); }
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/hash_map.h b/firestore/src/jni/hash_map.h
new file mode 100644
index 0000000000..407622e849
--- /dev/null
+++ b/firestore/src/jni/hash_map.h
@@ -0,0 +1,25 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_HASH_MAP_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_HASH_MAP_H_
+
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/map.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/** A C++ proxy for a Java `HashMap`. */
+class HashMap : public Map {
+ public:
+  using Map::Map;
+
+  static void Initialize(Loader& loader);
+
+  static Local Create(Env& env);
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_HASH_MAP_H_
diff --git a/firestore/src/jni/integer.cc b/firestore/src/jni/integer.cc
new file mode 100644
index 0000000000..34ad7cb810
--- /dev/null
+++ b/firestore/src/jni/integer.cc
@@ -0,0 +1,34 @@
+#include "firestore/src/jni/integer.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+constexpr char kClassName[] = "java/lang/Integer";
+Constructor kConstructor("(I)V");
+Method kIntValue("intValue", "()I");
+jclass g_clazz = nullptr;
+
+}  // namespace
+
+void Integer::Initialize(Loader& loader) {
+  g_clazz = util::integer_class::GetClass();
+  loader.LoadFromExistingClass(kClassName, g_clazz, kConstructor, kIntValue);
+}
+
+Class Integer::GetClass() { return Class(g_clazz); }
+
+Local Integer::Create(Env& env, int32_t value) {
+  return env.New(kConstructor, value);
+}
+
+int32_t Integer::IntValue(Env& env) const { return env.Call(*this, kIntValue); }
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/integer.h b/firestore/src/jni/integer.h
new file mode 100644
index 0000000000..8d391b7e33
--- /dev/null
+++ b/firestore/src/jni/integer.h
@@ -0,0 +1,29 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_INTEGER_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_INTEGER_H_
+
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/** A C++ proxy for a Java `Integer`. */
+class Integer : public Object {
+ public:
+  using Object::Object;
+
+  static void Initialize(Loader& loader);
+
+  static Class GetClass();
+
+  static Local Create(Env& env, int32_t value);
+
+  int32_t IntValue(Env& env) const;
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_INTEGER_H_
diff --git a/firestore/src/jni/iterator.cc b/firestore/src/jni/iterator.cc
new file mode 100644
index 0000000000..e0d63b0955
--- /dev/null
+++ b/firestore/src/jni/iterator.cc
@@ -0,0 +1,29 @@
+#include "firestore/src/jni/iterator.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+constexpr char kClass[] = "java/util/Iterator";
+Method kHasNext("hasNext", "()Z");
+Method kNext("next", "()Ljava/lang/Object;");
+
+}  // namespace
+
+void Iterator::Initialize(Loader& loader) {
+  loader.LoadFromExistingClass(kClass, util::iterator::GetClass(), kHasNext,
+                               kNext);
+}
+
+bool Iterator::HasNext(Env& env) const { return env.Call(*this, kHasNext); }
+
+Local Iterator::Next(Env& env) { return env.Call(*this, kNext); }
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/iterator.h b/firestore/src/jni/iterator.h
new file mode 100644
index 0000000000..d1f6c6a346
--- /dev/null
+++ b/firestore/src/jni/iterator.h
@@ -0,0 +1,26 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ITERATOR_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ITERATOR_H_
+
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/** A C++ proxy for a Java `Iterator`. */
+class Iterator : public Object {
+ public:
+  using Object::Object;
+
+  static void Initialize(Loader& loader);
+
+  bool HasNext(Env& env) const;
+  Local Next(Env& env);
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_ITERATOR_H_
diff --git a/firestore/src/jni/jni_fwd.h b/firestore/src/jni/jni_fwd.h
index ee105200f3..87987c48b8 100644
--- a/firestore/src/jni/jni_fwd.h
+++ b/firestore/src/jni/jni_fwd.h
@@ -1,6 +1,8 @@
 #ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_JNI_FWD_H_
 #define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_JNI_FWD_H_
 
+#include 
+
 namespace firebase {
 namespace firestore {
 namespace jni {
@@ -11,6 +13,7 @@ namespace jni {
 JNIEnv* GetEnv();
 
 class Env;
+class Loader;
 
 // Reference types
 template 
@@ -23,6 +26,39 @@ class NonOwning;
 class Class;
 class Object;
 class String;
+class Throwable;
+
+template 
+class Array;
+
+// Declaration types
+class ConstructorBase;
+class MethodBase;
+class StaticFieldBase;
+class StaticMethodBase;
+
+template 
+class Constructor;
+template 
+class Method;
+template 
+class StaticField;
+template 
+class StaticMethod;
+
+// Other elements of java.lang
+class Boolean;
+class Double;
+class Integer;
+class Long;
+
+// Collections from java.util
+class ArrayList;
+class Collection;
+class Iterator;
+class List;
+class HashMap;
+class Map;
 
 }  // namespace jni
 }  // namespace firestore
diff --git a/firestore/src/jni/list.cc b/firestore/src/jni/list.cc
new file mode 100644
index 0000000000..b4de125fb3
--- /dev/null
+++ b/firestore/src/jni/list.cc
@@ -0,0 +1,35 @@
+#include "firestore/src/jni/list.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+Method kGet("get", "(I)Ljava/lang/Object;");
+Method kSet("set", "(ILjava/lang/Object;)Ljava/lang/Object;");
+jclass g_clazz = nullptr;
+
+}  // namespace
+
+void List::Initialize(Loader& loader) {
+  g_clazz = util::list::GetClass();
+  loader.LoadFromExistingClass("java/util/List", g_clazz, kGet, kSet);
+}
+
+Class List::GetClass() { return Class(g_clazz); }
+
+Local List::Get(Env& env, size_t i) const {
+  return env.Call(*this, kGet, i);
+}
+
+Local List::Set(Env& env, size_t i, const Object& object) {
+  return env.Call(*this, kSet, i, object);
+}
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/list.h b/firestore/src/jni/list.h
new file mode 100644
index 0000000000..0c77c850c8
--- /dev/null
+++ b/firestore/src/jni/list.h
@@ -0,0 +1,28 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_LIST_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_LIST_H_
+
+#include "firestore/src/jni/collection.h"
+#include "firestore/src/jni/jni_fwd.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/** A C++ proxy for a Java `List`. */
+class List : public Collection {
+ public:
+  using Collection::Collection;
+
+  static void Initialize(Loader& loader);
+
+  static Class GetClass();
+
+  Local Get(Env& env, size_t i) const;
+  Local Set(Env& env, size_t i, const Object& object);
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_LIST_H_
diff --git a/firestore/src/jni/loader.cc b/firestore/src/jni/loader.cc
new file mode 100644
index 0000000000..e34952080e
--- /dev/null
+++ b/firestore/src/jni/loader.cc
@@ -0,0 +1,156 @@
+#include "firestore/src/jni/loader.h"
+
+#include "app/meta/move.h"
+#include "app/src/assert.h"
+#include "app/src/include/firebase/app.h"
+#include "app/src/util_android.h"
+#include "firestore/src/jni/class.h"
+#include "firestore/src/jni/declaration.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/jni.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+constexpr const char* StripProguardPrefix(const char* class_name) {
+  return class_name[0] == '%' ? &class_name[4] : class_name;
+}
+
+}  // namespace
+
+Loader::Loader(App* app) : app_(app), env_(app->GetJNIEnv()) {
+  Class::Initialize(*this);
+}
+
+Loader::~Loader() { Unload(); }
+
+void Loader::AddEmbeddedFile(const char* name, const unsigned char* data,
+                             size_t size) {
+#if defined(_STLPORT_VERSION)
+  embedded_files_.push_back(EmbeddedFile(name, data, size));
+#else
+  embedded_files_.emplace_back(name, data, size);
+#endif
+}
+
+void Loader::CacheEmbeddedFiles() {
+  if (!ok_) return;
+
+  util::CacheEmbeddedFiles(env_, app_->activity(), embedded_files_);
+}
+
+void Loader::UsingExistingClass(const char* class_name, jclass existing_ref) {
+  if (!ok_) return;
+
+  last_class_name_ = class_name;
+  last_class_ = existing_ref;
+}
+
+jclass Loader::LoadClass(const char* class_name) {
+  if (!ok_) return nullptr;
+
+  class_name = StripProguardPrefix(class_name);
+  last_class_name_ = class_name;
+  last_class_ = util::FindClassGlobal(env_, app_->activity(), &embedded_files_,
+                                      class_name, util::kClassRequired);
+  if (!last_class_) {
+    ok_ = false;
+    return nullptr;
+  }
+
+  loaded_classes_.push_back(last_class_);
+  return last_class_;
+}
+
+void Loader::Load(ConstructorBase& ctor) {
+  if (!ok_) return;
+
+  util::MethodNameSignature descriptor = {
+      "", ctor.sig_, util::kMethodTypeInstance, util::kMethodRequired};
+
+  jmethodID result = nullptr;
+  ok_ = util::LookupMethodIds(env_, last_class_, &descriptor,
+                              /*number_of_method_name_signatures=*/1u, &result,
+                              last_class_name_.c_str());
+  if (!ok_) return;
+
+  ctor.clazz_ = last_class_;
+  ctor.id_ = result;
+}
+
+void Loader::Load(MethodBase& method) {
+  if (!ok_) return;
+
+  util::MethodNameSignature descriptor = {method.name_, method.sig_,
+                                          util::kMethodTypeInstance,
+                                          util::kMethodRequired};
+
+  jmethodID result = nullptr;
+  ok_ = util::LookupMethodIds(env_, last_class_, &descriptor, 1u, &result,
+                              last_class_name_.c_str());
+  if (!ok_) return;
+
+  method.id_ = result;
+}
+
+void Loader::Load(StaticFieldBase& field) {
+  if (!ok_) return;
+
+  util::FieldDescriptor descriptor = {
+      field.name_, field.sig_, util::kFieldTypeStatic, util::kMethodRequired};
+
+  jfieldID result = nullptr;
+  ok_ = util::LookupFieldIds(env_, last_class_, &descriptor, 1u, &result,
+                             last_class_name_.c_str());
+  if (!ok_) return;
+
+  field.clazz_ = last_class_;
+  field.id_ = result;
+}
+
+void Loader::Load(StaticMethodBase& method) {
+  if (!ok_) return;
+
+  util::MethodNameSignature descriptor = {method.name_, method.sig_,
+                                          util::kMethodTypeStatic,
+                                          util::kMethodRequired};
+
+  jmethodID result = nullptr;
+  ok_ = util::LookupMethodIds(env_, last_class_, &descriptor, 1u, &result,
+                              last_class_name_.c_str());
+  if (!ok_) return;
+
+  method.clazz_ = last_class_;
+  method.id_ = result;
+}
+
+bool Loader::RegisterNatives(const JNINativeMethod methods[],
+                             size_t num_methods) {
+  if (!ok_) return false;
+
+  auto* true_native_methods = ConvertJNINativeMethod(methods, num_methods);
+  jint result =
+      env_->RegisterNatives(last_class_, true_native_methods, num_methods);
+  CleanUpConvertedJNINativeMethod(true_native_methods);
+
+  if (result != JNI_OK) {
+    ok_ = false;
+  }
+  return ok_;
+}
+
+void Loader::Unload() {
+  if (loaded_classes_.empty()) return;
+
+  JNIEnv* env = GetEnv();
+  for (jclass clazz : loaded_classes_) {
+    env->DeleteGlobalRef(clazz);
+  }
+  loaded_classes_.clear();
+}
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/loader.h b/firestore/src/jni/loader.h
new file mode 100644
index 0000000000..e202d99491
--- /dev/null
+++ b/firestore/src/jni/loader.h
@@ -0,0 +1,165 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_LOADER_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_LOADER_H_
+
+#include 
+
+#include 
+#include 
+
+#include "app/meta/move.h"
+#include "app/src/embedded_file.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/jni_fwd.h"
+
+// To ensure that Proguard doesn't strip the classes you're using, place this
+// string directly before the JNI class string in your Class global
+// declarations.
+#define PROGUARD_KEEP_CLASS "%PG%"
+
+namespace firebase {
+class App;
+
+namespace firestore {
+namespace jni {
+
+/**
+ * Loads cacheable JNI objects including classes, methods, and fields.
+ * Automatically unloads any loaded classes when destroyed.
+ */
+class Loader {
+ public:
+  using EmbeddedFile = firebase::internal::EmbeddedFile;
+
+  explicit Loader(App* app);
+  ~Loader();
+
+  // Loader is move-only
+  Loader(const Loader&) = delete;
+  Loader& operator=(const Loader&) = delete;
+
+  Loader(Loader&&) = default;
+  Loader& operator=(Loader&&) = default;
+
+  /**
+   * Returns true if the loader has succeeded. If not, any errors have already
+   * been logged.
+   */
+  bool ok() const { return ok_; }
+
+  /**
+   * Adds metadata about embedded class files in the binary distribution.
+   */
+  void AddEmbeddedFile(const char* name, const unsigned char* data,
+                       size_t size);
+  /**
+   * Unpacks any embedded files added above and writes them out to a temporary
+   * location. `Load(Class&)` will search these files for classes (in addition
+   * to the standard classpath).
+   */
+  void CacheEmbeddedFiles();
+
+  // TODO(mcg): remove once InitializeEmbeddedClasses instances are gone.
+  const std::vector* embedded_files() const {
+    return &embedded_files_;
+  }
+
+  /**
+   * Uses the given class reference as the basis for subsequent loads. The
+   * caller still owns the class reference and the Loader will not clean it up.
+   *
+   * @param class_name The class name the reference represents, in the form
+   *     "java/lang/String".
+   * param existing_ref An existing local or global reference to a Java class.
+   */
+  void UsingExistingClass(const char* class_name, jclass existing_ref);
+
+  /**
+   * Uses the given class reference for loading the given members. The caller
+   * still owns the class reference and the Loader will not clean it up.
+   *
+   * @param class_name The class name the reference represents, in the form
+   *     "java/lang/String".
+   * param existing_ref An existing local or global reference to a Java class.
+   */
+  template 
+  void LoadFromExistingClass(const char* class_name, jclass existing_ref,
+                             Members&&... members) {
+    UsingExistingClass(class_name, existing_ref);
+    LoadAll(Forward(members)...);
+  }
+
+  /**
+   * Loads a Java class described by the given class name. The class name as
+   * would be passed to `JNIEnv::FindClass`, e.g. `"java/util/String"`.
+   */
+  jclass LoadClass(const char* class_name);
+
+  /**
+   * Loads a Java class and all its members in a single invocation.
+   */
+  template 
+  jclass LoadClass(const char* name, Members&&... members) {
+    jclass result = LoadClass(name);
+    LoadAll(Forward(members)...);
+    return result;
+  }
+
+  /**
+   * Loads a Java constructor from the last loaded class.
+   */
+  void Load(ConstructorBase& method);
+
+  /**
+   * Loads a Java instance method from the last loaded class.
+   */
+  void Load(MethodBase& method);
+
+  /**
+   * Loads a Java static field from the last loaded class.
+   */
+  void Load(StaticFieldBase& field);
+
+  /**
+   * Loads a Java static method from the last loaded class.
+   */
+  void Load(StaticMethodBase& method);
+
+  /**
+   * Loads all the given members by calling the appropriate `Load` overload.
+   */
+  template 
+  void LoadAll(Member&& first, Members&&... rest) {
+    Load(Forward(first));
+    LoadAll(Forward(rest)...);
+  }
+  void LoadAll() {}
+
+  /**
+   * Registers the given native methods with the JVM.
+   */
+  bool RegisterNatives(const JNINativeMethod methods[], size_t num_methods);
+
+  void Unload();
+
+ private:
+  App* app_ = nullptr;
+  JNIEnv* env_ = nullptr;
+
+  std::string last_class_name_;
+  jclass last_class_ = nullptr;
+
+  bool ok_ = true;
+
+  // A list of classes that were successfully loaded. This is held as a
+  // UniquePtr to allow Loader to be move-only when built with STLPort.
+  std::vector loaded_classes_;
+
+  // A list of embedded files from which to load classes
+  std::vector embedded_files_;
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_LOADER_H_
diff --git a/firestore/src/jni/long.cc b/firestore/src/jni/long.cc
new file mode 100644
index 0000000000..dbe8865847
--- /dev/null
+++ b/firestore/src/jni/long.cc
@@ -0,0 +1,34 @@
+#include "firestore/src/jni/long.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+constexpr char kClassName[] = "java/lang/Long";
+Constructor kConstructor("(J)V");
+Method kLongValue("longValue", "()J");
+jclass g_clazz = nullptr;
+
+}  // namespace
+
+void Long::Initialize(Loader& loader) {
+  g_clazz = util::long_class::GetClass();
+  loader.LoadFromExistingClass(kClassName, g_clazz, kConstructor, kLongValue);
+}
+
+Class Long::GetClass() { return Class(g_clazz); }
+
+Local Long::Create(Env& env, int64_t value) {
+  return env.New(kConstructor, value);
+}
+
+int64_t Long::LongValue(Env& env) const { return env.Call(*this, kLongValue); }
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/long.h b/firestore/src/jni/long.h
new file mode 100644
index 0000000000..1aba3cc741
--- /dev/null
+++ b/firestore/src/jni/long.h
@@ -0,0 +1,29 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_LONG_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_LONG_H_
+
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/** A C++ proxy for a Java `Long`. */
+class Long : public Object {
+ public:
+  using Object::Object;
+
+  static void Initialize(Loader& loader);
+
+  static Class GetClass();
+
+  static Local Create(Env& env, int64_t value);
+
+  int64_t LongValue(Env& env) const;
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_LONG_H_
diff --git a/firestore/src/jni/map.cc b/firestore/src/jni/map.cc
new file mode 100644
index 0000000000..2d9ea209d0
--- /dev/null
+++ b/firestore/src/jni/map.cc
@@ -0,0 +1,44 @@
+#include "firestore/src/jni/map.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
+#include "firestore/src/jni/set.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+Method kSize("size", "()I");
+Method kGet("get", "(Ljava/lang/Object;)Ljava/lang/Object;");
+Method kPut("put",
+                    "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
+Method kKeySet("keySet", "()Ljava/util/Set;");
+jclass g_clazz = nullptr;
+
+}  // namespace
+
+void Map::Initialize(Loader& loader) {
+  g_clazz = util::map::GetClass();
+  loader.LoadFromExistingClass("java/util/Map", g_clazz, kSize, kGet, kPut,
+                               kKeySet);
+}
+
+Class Map::GetClass() { return Class(g_clazz); }
+
+size_t Map::Size(Env& env) const { return env.Call(*this, kSize); }
+
+Local Map::Get(Env& env, const Object& key) {
+  return env.Call(*this, kGet, key);
+}
+
+Local Map::Put(Env& env, const Object& key, const Object& value) {
+  return env.Call(*this, kPut, key, value);
+}
+
+Local Map::KeySet(Env& env) { return env.Call(*this, kKeySet); }
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/map.h b/firestore/src/jni/map.h
new file mode 100644
index 0000000000..1ccc4c4608
--- /dev/null
+++ b/firestore/src/jni/map.h
@@ -0,0 +1,33 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_MAP_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_MAP_H_
+
+#include "firestore/src/jni/jni_fwd.h"
+#include "firestore/src/jni/object.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+class Set;
+
+/** A C++ proxy for a Java `Map`. */
+class Map : public Object {
+ public:
+  using Object::Object;
+
+  static void Initialize(Loader& loader);
+
+  static Class GetClass();
+
+  size_t Size(Env& env) const;
+  Local Get(Env& env, const Object& key);
+  Local Put(Env& env, const Object& key, const Object& value);
+
+  Local KeySet(Env& env);
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_MAP_H_
diff --git a/firestore/src/jni/object.cc b/firestore/src/jni/object.cc
index 6cd649374b..4781c458c7 100644
--- a/firestore/src/jni/object.cc
+++ b/firestore/src/jni/object.cc
@@ -1,11 +1,26 @@
 #include "firestore/src/jni/object.h"
 
 #include "app/src/util_android.h"
+#include "firestore/src/jni/class.h"
 #include "firestore/src/jni/env.h"
+#include "firestore/src/jni/loader.h"
 
 namespace firebase {
 namespace firestore {
 namespace jni {
+namespace {
+
+Method kEquals("equals", "(Ljava/lang/Object;)Z");
+jclass g_clazz = nullptr;
+
+}  // namespace
+
+void Object::Initialize(Loader& loader) {
+  g_clazz = util::object::GetClass();
+  loader.LoadFromExistingClass("java/lang/Object", g_clazz, kEquals);
+}
+
+Class Object::GetClass() { return Class(util::object::GetClass()); }
 
 std::string Object::ToString(JNIEnv* env) const {
   return util::JniObjectToString(env, object_);
@@ -13,6 +28,20 @@ std::string Object::ToString(JNIEnv* env) const {
 
 std::string Object::ToString(Env& env) const { return ToString(env.get()); }
 
+bool Object::Equals(Env& env, const Object& other) const {
+  return env.Call(*this, kEquals, other);
+}
+
+bool Object::Equals(Env& env, const Object& lhs, const Object& rhs) {
+  // Most likely only happens when comparing one with itself or both are null.
+  if (lhs.get() == rhs.get()) return true;
+
+  // If only one of them is nullptr, then they cannot equal.
+  if (!lhs || !rhs) return false;
+
+  return lhs.Equals(env, rhs);
+}
+
 }  // namespace jni
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/jni/object.h b/firestore/src/jni/object.h
index a7b4f05fd6..7cf17b2f9d 100644
--- a/firestore/src/jni/object.h
+++ b/firestore/src/jni/object.h
@@ -9,7 +9,9 @@ namespace firebase {
 namespace firestore {
 namespace jni {
 
+class Class;
 class Env;
+class Loader;
 
 /**
  * A wrapper for a JNI `jobject` that adds additional behavior.
@@ -21,13 +23,17 @@ class Env;
 class Object {
  public:
   Object() = default;
-  explicit Object(jobject object) : object_(object) {}
+  constexpr explicit Object(jobject object) : object_(object) {}
   virtual ~Object() = default;
 
   explicit operator bool() const { return object_ != nullptr; }
 
   virtual jobject get() const { return object_; }
 
+  static void Initialize(Loader& loader);
+
+  static Class GetClass();
+
   /**
    * Converts this object to a C++ String by calling the Java `toString` method
    * on it.
@@ -35,6 +41,9 @@ class Object {
   std::string ToString(JNIEnv* env) const;
   std::string ToString(Env& env) const;
 
+  bool Equals(Env& env, const Object& other) const;
+  static bool Equals(Env& env, const Object& lhs, const Object& rhs);
+
  protected:
   jobject object_ = nullptr;
 };
diff --git a/firestore/src/jni/ownership.h b/firestore/src/jni/ownership.h
index aa6292c551..e6fb84be17 100644
--- a/firestore/src/jni/ownership.h
+++ b/firestore/src/jni/ownership.h
@@ -31,8 +31,27 @@ class Local : public T {
    */
   Local(JNIEnv* env, jni_type value) : T(value), env_(env) {}
 
-  Local(const Local& other) = delete;
-  Local& operator=(const Local&) = delete;
+  /**
+   * An explicit copy constructor, which prevents accidental copies at function
+   * call sites. Copies of a local reference should rarely be needed. For
+   * example, when keeping a reference as a member of a C++ object or lambda,
+   * you're almost exclusively better off promoting the local reference to a
+   * global one to avoid problems that arise from the thread-local restrictions
+   * of a local reference.
+   */
+  explicit Local(const Local& other) {  // NOLINT(google-explicit-constructor)
+    EnsureEnv(other.env());
+    T::object_ = env_->NewLocalRef(other.get());
+  }
+
+  Local& operator=(const Local& other) {
+    if (T::object_ != other.get()) {
+      EnsureEnv(other.env());
+      env_->DeleteLocalRef(T::object_);
+      T::object_ = env_->NewLocalRef(other.get());
+    }
+    return *this;
+  }
 
   Local(Local&& other) noexcept : T(other.release()), env_(other.env_) {}
 
@@ -85,6 +104,13 @@ class Local : public T {
     return static_cast(result);
   }
 
+  void clear() {
+    if (env_ && T::object_) {
+      env_->DeleteLocalRef(T::object_);
+      T::object_ = nullptr;
+    }
+  }
+
   JNIEnv* env() const { return env_; }
 
  private:
@@ -207,6 +233,14 @@ class Global : public T {
     return static_cast(result);
   }
 
+  void clear() {
+    if (T::object_) {
+      JNIEnv* env = GetEnv();
+      env->DeleteGlobalRef(T::object_);
+      T::object_ = nullptr;
+    }
+  }
+
  private:
   JNIEnv* EnsureEnv(JNIEnv* other = nullptr) {
     if (other != nullptr) {
diff --git a/firestore/src/jni/set.h b/firestore/src/jni/set.h
new file mode 100644
index 0000000000..bd8fc727d7
--- /dev/null
+++ b/firestore/src/jni/set.h
@@ -0,0 +1,26 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_SET_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_SET_H_
+
+#include "firestore/src/jni/collection.h"
+#include "firestore/src/jni/jni_fwd.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/**
+ * A C++ proxy for a Java `Set`.
+ *
+ * This class adds no methods on top of `Collection`, but exists to make the
+ * typings on `Map::KeySet` mirror the underlying Java types.
+ */
+class Set : public Collection {
+ public:
+  using Collection::Collection;
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_SET_H_
diff --git a/firestore/src/jni/string.cc b/firestore/src/jni/string.cc
index 088ea68a83..0c6765da7d 100644
--- a/firestore/src/jni/string.cc
+++ b/firestore/src/jni/string.cc
@@ -1,15 +1,18 @@
 #include "firestore/src/jni/string.h"
 
+#include "app/src/util_android.h"
+#include "firestore/src/jni/class.h"
 #include "firestore/src/jni/env.h"
 
 namespace firebase {
 namespace firestore {
 namespace jni {
 
+Class String::GetClass() { return Class(util::string::GetClass()); }
+
 std::string String::ToString(Env& env) const {
-  jstring str = get();
-  size_t len = env.GetStringUtfLength(str);
-  return env.GetStringUtfRegion(str, 0, len);
+  size_t len = env.GetStringUtfLength(*this);
+  return env.GetStringUtfRegion(*this, 0, len);
 }
 
 }  // namespace jni
diff --git a/firestore/src/jni/string.h b/firestore/src/jni/string.h
index 8ddc5fe8cc..3e63d47f87 100644
--- a/firestore/src/jni/string.h
+++ b/firestore/src/jni/string.h
@@ -7,6 +7,8 @@ namespace firebase {
 namespace firestore {
 namespace jni {
 
+class Class;
+
 /**
  * A wrapper for a JNI `jstring` that adds additional behavior. This is a proxy
  * for a Java String in the JVM.
@@ -22,6 +24,8 @@ class String : public Object {
 
   jstring get() const override { return static_cast(object_); }
 
+  static Class GetClass();
+
   /** Converts this Java String to a C++ string. */
   std::string ToString(Env& env) const;
 };
diff --git a/firestore/src/jni/throwable.cc b/firestore/src/jni/throwable.cc
new file mode 100644
index 0000000000..8dafdb07ce
--- /dev/null
+++ b/firestore/src/jni/throwable.cc
@@ -0,0 +1,17 @@
+#include "firestore/src/jni/throwable.h"
+
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+std::string Throwable::GetMessage(Env& env) const {
+  ExceptionClearGuard block(env);
+  return util::GetMessageFromException(env.get(), object_);
+}
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/jni/throwable.h b/firestore/src/jni/throwable.h
new file mode 100644
index 0000000000..98091f2fb5
--- /dev/null
+++ b/firestore/src/jni/throwable.h
@@ -0,0 +1,37 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_THROWABLE_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_THROWABLE_H_
+
+#include "firestore/src/jni/object.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+
+/**
+ * A wrapper for a JNI `jthrowable` that adds additional behavior. This is a
+ * proxy for a Java Throwable in the JVM.
+ *
+ * `Throwable` merely holds values with `jthrowable` type, see `Local` and
+ * `Global` template subclasses for reference-type-aware wrappers that
+ * automatically manage the lifetime of JNI objects.
+ */
+class Throwable : public Object {
+ public:
+  Throwable() = default;
+  explicit Throwable(jthrowable throwable) : Object(throwable) {}
+
+  jthrowable get() const override { return static_cast(object_); }
+
+  /**
+   * Gets the message associated with this throwable.
+   *
+   * This method can be run even when an exception is pending.
+   */
+  std::string GetMessage(Env& env) const;
+};
+
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_JNI_THROWABLE_H_
diff --git a/firestore/src/jni/traits.h b/firestore/src/jni/traits.h
index 135de40cf3..de05698d74 100644
--- a/firestore/src/jni/traits.h
+++ b/firestore/src/jni/traits.h
@@ -31,6 +31,7 @@ template <> struct IsReference : public true_type {};
 template <> struct IsReference : public true_type {};
 template <> struct IsReference : public true_type {};
 template <> struct IsReference : public true_type {};
+template <> struct IsReference : public true_type {};
 template <> struct IsReference : public true_type {};
 template <> struct IsReference : public true_type {};
 
@@ -55,6 +56,19 @@ template <> struct JniTypeMap { using type = void; };
 template <> struct JniTypeMap { using type = jclass; };
 template <> struct JniTypeMap { using type = jobject; };
 template <> struct JniTypeMap { using type = jstring; };
+template <> struct JniTypeMap { using type = jthrowable; };
+
+template  struct JniTypeMap> {
+  using type = jobjectArray;
+};
+template <> struct JniTypeMap> { using type = jbooleanArray; };
+template <> struct JniTypeMap> { using type = jbyteArray; };
+template <> struct JniTypeMap> { using type = jcharArray; };
+template <> struct JniTypeMap> { using type = jshortArray; };
+template <> struct JniTypeMap> { using type = jintArray; };
+template <> struct JniTypeMap> { using type = jlongArray; };
+template <> struct JniTypeMap> { using type = jfloatArray; };
+template <> struct JniTypeMap> { using type = jdoubleArray; };
 
 template  struct JniTypeMap> {
   using type = typename JniTypeMap::type;
@@ -89,6 +103,8 @@ namespace internal {
  * When finding a JNI converter, we try the following, in order:
  *   * pass through, for JNI primitive types;
  *   * static casts, for C++ primitive types;
+ *   * reinterpret casts, for C++ pointers to primitive types (only where
+ *     pointed-to sizes match);
  *   * pass through, for JNI reference types like jobject;
  *   * unwrapping, for JNI reference wrapper types like `Object` or
  *     `Local`.
@@ -104,7 +120,7 @@ template 
 struct ConverterChoice : public ConverterChoice {};
 
 template <>
-struct ConverterChoice<3> {};
+struct ConverterChoice<4> {};
 
 /**
  * Converts JNI primitive types to themselves.
@@ -125,12 +141,47 @@ JniType RankedToJni(T value, ConverterChoice<1>) {
   return static_cast>(value);
 }
 
+/**
+ * Returns true if a reinterpret cast from a pointer to a C++ primitive type to
+ * a pointer to the equivalent JNI type is well-defined. Reinterpreting certain
+ * C++ types does not work:
+ *
+ *   * `bool` doesn't have a fully specified representation so any
+ *     reinterpret_cast of such a value is invalid in portable code.
+ *   * `size_t` does not have a fixed size, so an array of such values cannot
+ *     be portably be reinterpreted as an array of jsize.
+ */
+template 
+constexpr bool IsConvertiblePointer() {
+  return IsPrimitive>::value && !is_same::value &&
+         !is_same::value && sizeof(T) == sizeof(JniType);
+}
+
+/**
+ * Converts pointers to C++ primitive types to their equivalent JNI pointers to
+ * primitive types by casting. This matches all potential primitive pointer
+ * types and will fail to compile if the type is ineligible for automatic
+ * conversion.
+ */
+template >::value>::type>
+const JniType* RankedToJni(const T* value, ConverterChoice<2>) {
+  static_assert(IsConvertiblePointer(), "conversion must be well defined");
+  return reinterpret_cast*>(value);
+}
+template >::value>::type>
+JniType* RankedToJni(T* value, ConverterChoice<2>) {
+  static_assert(IsConvertiblePointer(), "conversion must be well defined");
+  return reinterpret_cast*>(value);
+}
+
 /**
  * Converts direct use of a JNI reference types to themselves.
  */
 template ::value>::type>
-T RankedToJni(T value, ConverterChoice<2>) {
+T RankedToJni(T value, ConverterChoice<3>) {
   return value;
 }
 
@@ -146,7 +197,7 @@ inline jobject RankedToJni(nullptr_t, ConverterChoice<2>) { return nullptr; }
  * Converts wrapper types to JNI references by unwrapping them.
  */
 template >
-J RankedToJni(const T& value, ConverterChoice<3>) {
+J RankedToJni(const T& value, ConverterChoice<4>) {
   return value.get();
 }
 
diff --git a/firestore/src/stub/firestore_stub.h b/firestore/src/stub/firestore_stub.h
index b8a56b37b8..b868691cf5 100644
--- a/firestore/src/stub/firestore_stub.h
+++ b/firestore/src/stub/firestore_stub.h
@@ -112,6 +112,8 @@ class FirestoreInternal {
 
   void set_firestore_public(Firestore*) {}
 
+  static void SetClientLanguage(const std::string& language_token) {}
+
  private:
   CleanupNotifier cleanup_;
   App* app_;
diff --git a/firestore/src/tests/android/geo_point_android_test.cc b/firestore/src/tests/android/geo_point_android_test.cc
index 8d09aee483..883add3d32 100644
--- a/firestore/src/tests/android/geo_point_android_test.cc
+++ b/firestore/src/tests/android/geo_point_android_test.cc
@@ -1,24 +1,26 @@
 #include "firestore/src/android/geo_point_android.h"
 
-#include 
-
+#include "firestore/src/jni/env.h"
 #include "firestore/src/tests/firestore_integration_test.h"
-#include "testing/base/public/gmock.h"
 #include "gtest/gtest.h"
 #include "firebase/firestore/geo_point.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
+
+using jni::Env;
 
-TEST_F(FirestoreIntegrationTest, Converter) {
-  JNIEnv* env = app()->GetJNIEnv();
+using GeoPointTest = FirestoreIntegrationTest;
 
-  const GeoPoint point{12.0, 34.0};
-  jobject java_point = GeoPointInternal::GeoPointToJavaGeoPoint(env, point);
-  EXPECT_EQ(point, GeoPointInternal::JavaGeoPointToGeoPoint(env, java_point));
+TEST_F(GeoPointTest, Converts) {
+  Env env;
 
-  env->DeleteLocalRef(java_point);
+  GeoPoint point{12.0, 34.0};
+  auto java_point = GeoPointInternal::Create(env, point);
+  EXPECT_EQ(point, java_point.ToPublic(env));
 }
 
+}  // namespace
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/tests/android/settings_android_test.cc b/firestore/src/tests/android/settings_android_test.cc
new file mode 100644
index 0000000000..d06afdf93a
--- /dev/null
+++ b/firestore/src/tests/android/settings_android_test.cc
@@ -0,0 +1,49 @@
+#include "firestore/src/android/settings_android.h"
+
+#include "firestore/src/include/firebase/firestore/settings.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/tests/firestore_integration_test.h"
+#include "testing/base/public/gmock.h"
+#include "gtest/gtest.h"
+
+namespace firebase {
+namespace firestore {
+namespace {
+
+using jni::Env;
+
+using SettingsTest = FirestoreIntegrationTest;
+
+TEST_F(SettingsTest, ConverterBoolsAllTrue) {
+  Env env;
+
+  Settings settings;
+  settings.set_host("foo");
+  settings.set_ssl_enabled(true);
+  settings.set_persistence_enabled(true);
+
+  Settings result = SettingsInternal::Create(env, settings).ToPublic(env);
+
+  EXPECT_EQ("foo", result.host());
+  EXPECT_TRUE(result.is_ssl_enabled());
+  EXPECT_TRUE(result.is_persistence_enabled());
+}
+
+TEST_F(SettingsTest, ConverterBoolsAllFalse) {
+  Env env;
+
+  Settings settings;
+  settings.set_host("bar");
+  settings.set_ssl_enabled(false);
+  settings.set_persistence_enabled(false);
+
+  Settings result = SettingsInternal::Create(env, settings).ToPublic(env);
+
+  EXPECT_EQ("bar", result.host());
+  EXPECT_FALSE(result.is_ssl_enabled());
+  EXPECT_FALSE(result.is_persistence_enabled());
+}
+
+}  // namespace
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/tests/android/snapshot_metadata_android_test.cc b/firestore/src/tests/android/snapshot_metadata_android_test.cc
index 3781697f78..3e4a6b2f43 100644
--- a/firestore/src/tests/android/snapshot_metadata_android_test.cc
+++ b/firestore/src/tests/android/snapshot_metadata_android_test.cc
@@ -1,8 +1,7 @@
 #include "firestore/src/android/snapshot_metadata_android.h"
 
-#include 
-
 #include "firestore/src/include/firebase/firestore/snapshot_metadata.h"
+#include "firestore/src/jni/env.h"
 #include "firestore/src/tests/firestore_integration_test.h"
 #include "testing/base/public/gmock.h"
 #include "gtest/gtest.h"
@@ -10,30 +9,27 @@
 namespace firebase {
 namespace firestore {
 
-TEST_F(FirestoreIntegrationTest, ConvertHasPendingWrites) {
-  JNIEnv* env = app()->GetJNIEnv();
+using jni::Class;
+using jni::Env;
+using jni::Local;
+
+TEST_F(FirestoreIntegrationTest, Converts) {
+  Env env;
+
+  Local clazz =
+      env.FindClass("com/google/firebase/firestore/SnapshotMetadata");
+  jmethodID ctor = env.GetMethodId(clazz, "", "(ZZ)V");
 
-  const SnapshotMetadata has_pending_writes{/*has_pending_writes=*/true,
-                                            /*is_from_cache=*/false};
-  // The converter will delete local reference for us.
-  const SnapshotMetadata result =
-      SnapshotMetadataInternal::JavaSnapshotMetadataToSnapshotMetadata(
-          env, SnapshotMetadataInternal::SnapshotMetadataToJavaSnapshotMetadata(
-                   env, has_pending_writes));
+  auto java_metadata = env.New(
+      clazz, ctor, /*has_pending_writes=*/true, /*is_from_cache=*/false);
+  SnapshotMetadata result = java_metadata.ToPublic(env);
   EXPECT_TRUE(result.has_pending_writes());
   EXPECT_FALSE(result.is_from_cache());
-}
-
-TEST_F(FirestoreIntegrationTest, ConvertIsFromCache) {
-  JNIEnv* env = app()->GetJNIEnv();
 
-  const SnapshotMetadata is_from_cache{/*has_pending_writes=*/false,
-                                       /*is_from_cache=*/true};
-  // The converter will delete local reference for us.
-  const SnapshotMetadata result =
-      SnapshotMetadataInternal::JavaSnapshotMetadataToSnapshotMetadata(
-          env, SnapshotMetadataInternal::SnapshotMetadataToJavaSnapshotMetadata(
-                   env, is_from_cache));
+  java_metadata = env.New(
+      clazz, ctor, /*has_pending_writes=*/false,
+      /*is_from_cache=*/true);
+  result = java_metadata.ToPublic(env);
   EXPECT_FALSE(result.has_pending_writes());
   EXPECT_TRUE(result.is_from_cache());
 }
diff --git a/firestore/src/tests/android/timestamp_android_test.cc b/firestore/src/tests/android/timestamp_android_test.cc
index a30c5373c9..3256043af5 100644
--- a/firestore/src/tests/android/timestamp_android_test.cc
+++ b/firestore/src/tests/android/timestamp_android_test.cc
@@ -1,26 +1,26 @@
 #include "firestore/src/android/timestamp_android.h"
 
-#include 
-
+#include "firestore/src/jni/env.h"
 #include "firestore/src/tests/firestore_integration_test.h"
-#include "testing/base/public/gmock.h"
 #include "gtest/gtest.h"
 #include "firebase/firestore/timestamp.h"
 
 namespace firebase {
 namespace firestore {
+namespace {
+
+using jni::Env;
 
-TEST_F(FirestoreIntegrationTest, Converter) {
-  JNIEnv* env = app()->GetJNIEnv();
+using TimestampTest = FirestoreIntegrationTest;
 
-  const Timestamp timestamp{1234, 5678};
-  jobject java_timestamp =
-      TimestampInternal::TimestampToJavaTimestamp(env, timestamp);
-  EXPECT_EQ(timestamp,
-            TimestampInternal::JavaTimestampToTimestamp(env, java_timestamp));
+TEST_F(TimestampTest, Converts) {
+  Env env;
 
-  env->DeleteLocalRef(java_timestamp);
+  Timestamp timestamp{1234, 5678};
+  auto java_timestamp = TimestampInternal::Create(env, timestamp);
+  EXPECT_EQ(timestamp, java_timestamp.ToPublic(env));
 }
 
+}  // namespace
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/tests/cleanup_test.cc b/firestore/src/tests/cleanup_test.cc
index f23ee29be7..c625caa258 100644
--- a/firestore/src/tests/cleanup_test.cc
+++ b/firestore/src/tests/cleanup_test.cc
@@ -102,8 +102,8 @@ void ExpectAllMethodsAreNoOps(DocumentReference* ptr) {
   EXPECT_EQ(ptr->Delete(), FailedFuture());
 
 #if defined(FIREBASE_USE_STD_FUNCTION)
-  EXPECT_NO_THROW(
-      ptr->AddSnapshotListener([](const DocumentSnapshot&, Error) {}));
+  EXPECT_NO_THROW(ptr->AddSnapshotListener(
+      [](const DocumentSnapshot&, Error, const std::string&) {}));
 #else
   EXPECT_NO_THROW(ptr->AddSnapshotListener(nullptr));
 #endif
@@ -217,7 +217,8 @@ void ExpectAllMethodsAreNoOps(Query* ptr) {
   EXPECT_EQ(ptr->Get(), FailedFuture());
 
 #if defined(FIREBASE_USE_STD_FUNCTION)
-  EXPECT_NO_THROW(ptr->AddSnapshotListener([](const QuerySnapshot&, Error) {}));
+  EXPECT_NO_THROW(ptr->AddSnapshotListener(
+      [](const QuerySnapshot&, Error, const std::string&) {}));
 #else
   EXPECT_NO_THROW(ptr->AddSnapshotListener(nullptr));
 #endif
@@ -367,8 +368,8 @@ TEST_F(CleanupTest, ListenerRegistrationIsBlankAfterCleanup) {
   }
 
   DocumentReference doc = Document();
-  ListenerRegistration reg =
-      doc.AddSnapshotListener([](const DocumentSnapshot&, Error) {});
+  ListenerRegistration reg = doc.AddSnapshotListener(
+      [](const DocumentSnapshot&, Error, const std::string&) {});
   DeleteFirestore();
   SCOPED_TRACE("ListenerRegistration.AfterCleanup");
   ExpectAllMethodsAreNoOps(®);
diff --git a/firestore/src/tests/field_value_test.cc b/firestore/src/tests/field_value_test.cc
index 1349520e16..7e87c255d4 100644
--- a/firestore/src/tests/field_value_test.cc
+++ b/firestore/src/tests/field_value_test.cc
@@ -53,7 +53,7 @@ TEST_F(FieldValueTest, Assignment) {
 
 #endif  // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD)
 
-#if !defined(FIRESTORE_STUB_BUILD)  
+#if !defined(FIRESTORE_STUB_BUILD)
 
 TEST_F(FirestoreIntegrationTest, TestNullType) {
   FieldValue value = FieldValue::Null();
diff --git a/firestore/src/tests/firestore_integration_test.cc b/firestore/src/tests/firestore_integration_test.cc
index 7f43c9f13f..800218230b 100644
--- a/firestore/src/tests/firestore_integration_test.cc
+++ b/firestore/src/tests/firestore_integration_test.cc
@@ -15,16 +15,6 @@ namespace {
 // non-default app to avoid data ending up in the cache before tests run.
 static const char* kBootstrapAppName = "bootstrap";
 
-void Release(Firestore* firestore) {
-  if (firestore == nullptr) {
-    return;
-  }
-
-  App* app = firestore->app();
-  delete firestore;
-  delete app;
-}
-
 // Set Firestore up to use Firestore Emulator if it can be found.
 void LocateEmulator(Firestore* db) {
   // iOS and Android pass emulator address differently, iOS writes it to a
@@ -51,7 +41,62 @@ void LocateEmulator(Firestore* db) {
   }
 }
 
-}  // anonymous namespace
+}  // namespace
+
+std::string ToFirestoreErrorCodeName(int error_code) {
+  switch (error_code) {
+    case kErrorOk:
+      return "kErrorOk";
+    case kErrorCancelled:
+      return "kErrorCancelled";
+    case kErrorUnknown:
+      return "kErrorUnknown";
+    case kErrorInvalidArgument:
+      return "kErrorInvalidArgument";
+    case kErrorDeadlineExceeded:
+      return "kErrorDeadlineExceeded";
+    case kErrorNotFound:
+      return "kErrorNotFound";
+    case kErrorAlreadyExists:
+      return "kErrorAlreadyExists";
+    case kErrorPermissionDenied:
+      return "kErrorPermissionDenied";
+    case kErrorResourceExhausted:
+      return "kErrorResourceExhausted";
+    case kErrorFailedPrecondition:
+      return "kErrorFailedPrecondition";
+    case kErrorAborted:
+      return "kErrorAborted";
+    case kErrorOutOfRange:
+      return "kErrorOutOfRange";
+    case kErrorUnimplemented:
+      return "kErrorUnimplemented";
+    case kErrorInternal:
+      return "kErrorInternal";
+    case kErrorUnavailable:
+      return "kErrorUnavailable";
+    case kErrorDataLoss:
+      return "kErrorDataLoss";
+    case kErrorUnauthenticated:
+      return "kErrorUnauthenticated";
+    default:
+      return "[invalid error code]";
+  }
+}
+
+int WaitFor(const FutureBase& future) {
+  // Instead of getting a clock, we count the cycles instead.
+  int cycles = kTimeOutMillis / kCheckIntervalMillis;
+  while (future.status() == FutureStatus::kFutureStatusPending && cycles > 0) {
+    if (ProcessEvents(kCheckIntervalMillis)) {
+      std::cout << "WARNING: app receives an event requesting exit."
+                << std::endl;
+      break;
+    }
+    --cycles;
+  }
+  return cycles;
+}
 
 FirestoreIntegrationTest::FirestoreIntegrationTest() {
   // Allocate the default Firestore eagerly.
@@ -60,7 +105,7 @@ FirestoreIntegrationTest::FirestoreIntegrationTest() {
 }
 
 FirestoreIntegrationTest::~FirestoreIntegrationTest() {
-  for (auto named_firestore : firestores_) {
+  for (const auto& named_firestore : firestores_) {
     Release(named_firestore.second);
     firestores_[named_firestore.first] = nullptr;
   }
@@ -217,7 +262,7 @@ bool FirestoreIntegrationTest::FailIfUnsuccessful(const char* operation,
                   << std::endl;
     return true;
   } else if (future.error() != Error::kErrorOk) {
-    ADD_FAILURE() << operation << "failed: " << DescribeFailedFuture(future)
+    ADD_FAILURE() << operation << " failed: " << DescribeFailedFuture(future)
                   << std::endl;
     return true;
   } else {
@@ -228,8 +273,19 @@ bool FirestoreIntegrationTest::FailIfUnsuccessful(const char* operation,
 /* static */
 std::string FirestoreIntegrationTest::DescribeFailedFuture(
     const FutureBase& future) {
-  return "WARNING: Future failed. Error code " +
-         std::to_string(future.error()) + ", message " + future.error_message();
+  return "Future failed: " + ToFirestoreErrorCodeName(future.error()) + " (" +
+         std::to_string(future.error()) + "): " + future.error_message();
+}
+
+/* static */
+void FirestoreIntegrationTest::Release(Firestore* firestore) {
+  if (firestore == nullptr) {
+    return;
+  }
+
+  App* app = firestore->app();
+  delete firestore;
+  delete app;
 }
 
 }  // namespace firestore
diff --git a/firestore/src/tests/firestore_integration_test.h b/firestore/src/tests/firestore_integration_test.h
index 9cae76625c..0d5d364ba5 100644
--- a/firestore/src/tests/firestore_integration_test.h
+++ b/firestore/src/tests/firestore_integration_test.h
@@ -29,6 +29,17 @@ App* GetApp();
 App* GetApp(const char* name);
 bool ProcessEvents(int msec);
 
+// Converts a Firestore error code to a human-friendly name. The `error_code`
+// argument is expected to be an element from the firebase::firestore::Error
+// enum, but this function will gracefully handle the case where it is not.
+std::string ToFirestoreErrorCodeName(int error_code);
+
+// Waits for a Future to complete. If a timeout is reached then this method
+// returns as if successful; therefore, the caller should verify the status of
+// the given Future after this function returns. Returns the number of cycles
+// that were left before a timeout would have occurred.
+int WaitFor(const FutureBase& future);
+
 template 
 class EventAccumulator;
 
@@ -41,27 +52,30 @@ class TestEventListener : public EventListener {
 
   ~TestEventListener() override {}
 
-  void OnEvent(const T& value, Error error) override {
+  void OnEvent(const T& value, Error error_code,
+               const std::string& error_message) override {
     if (print_debug_info_) {
       std::cout << "TestEventListener got: ";
-      if (error == Error::kErrorOk) {
+      if (error_code == Error::kErrorOk) {
         std::cout << &value
                   << " from_cache=" << value.metadata().is_from_cache()
                   << " has_pending_write="
                   << value.metadata().has_pending_writes()
                   << " event_count=" << event_count() << std::endl;
       } else {
-        std::cout << "error=" << error << " event_count=" << event_count()
+        std::cout << "error_code=" << error_code << " error_message=\""
+                  << error_message << "\" event_count=" << event_count()
                   << std::endl;
       }
     }
 
     MutexLock lock(mutex_);
-    if (error != Error::kErrorOk) {
-      std::cerr << "ERROR: EventListener " << name_ << " got " << error
+    if (error_code != Error::kErrorOk) {
+      std::cerr << "ERROR: EventListener " << name_ << " got " << error_code
                 << std::endl;
-      if (first_error_ == Error::kErrorOk) {
-        first_error_ = error;
+      if (first_error_code_ == Error::kErrorOk) {
+        first_error_code_ = error_code;
+        first_error_message_ = error_message;
       }
     }
     last_results_.push_back(value);
@@ -85,16 +99,23 @@ class TestEventListener : public EventListener {
       U* ref, MetadataChanges metadata_changes = MetadataChanges::kExclude) {
 #if defined(FIREBASE_USE_STD_FUNCTION)
     return ref->AddSnapshotListener(
-        metadata_changes,
-        [this](const T& result, Error error) { OnEvent(result, error); });
+        metadata_changes, [this](const T& result, Error error_code,
+                                 const std::string& error_message) {
+          OnEvent(result, error_code, error_message);
+        });
 #else
     return ref->AddSnapshotListener(metadata_changes, this);
 #endif
   }
 
-  Error first_error() {
+  std::string first_error_message() {
     MutexLock lock(mutex_);
-    return first_error_;
+    return first_error_message_;
+  }
+
+  Error first_error_code() {
+    MutexLock lock(mutex_);
+    return first_error_code_;
   }
 
   // Set this to true to print more details for each arrived event for debug.
@@ -121,7 +142,8 @@ class TestEventListener : public EventListener {
 
   // We generally only check to see if there is any error. So we only store the
   // first non-OK error, if any.
-  Error first_error_ = Error::kErrorOk;
+  Error first_error_code_ = Error::kErrorOk;
+  std::string first_error_message_ = "";
 
   bool print_debug_info_ = false;
 };
@@ -211,22 +233,11 @@ class FirestoreIntegrationTest : public testing::Test {
   // A helper function to block until the future completes.
   template 
   static const T* Await(const Future& future) {
-    // Instead of getting a clock, we count the cycles instead.
-    int cycles = kTimeOutMillis / kCheckIntervalMillis;
-    while (future.status() == FutureStatus::kFutureStatusPending &&
-           cycles > 0) {
-      if (ProcessEvents(kCheckIntervalMillis)) {
-        std::cout << "WARNING: app receives an event requesting exit."
-                  << std::endl;
-        return nullptr;
-      }
-      --cycles;
-    }
+    int cycles = WaitFor(future);
     EXPECT_GT(cycles, 0) << "Waiting future timed out.";
     if (future.status() == FutureStatus::kFutureStatusComplete) {
       if (future.result() == nullptr) {
-        std::cout << "WARNING: Future failed. Error code " << future.error()
-                  << ", message " << future.error_message() << std::endl;
+        std::cout << "WARNING: " << DescribeFailedFuture(future) << std::endl;
       }
     } else {
       std::cout << "WARNING: Future is not completed." << std::endl;
@@ -261,15 +272,21 @@ class FirestoreIntegrationTest : public testing::Test {
 
   // Creates a new Firestore instance, without any caching, using a uniquely-
   // generated app_name.
+  // Use Release() to correctly delete the returned pointer.
   Firestore* CreateFirestore() const;
   // Creates a new Firestore instance, without any caching, using the given
   // app_name.
+  // Use Release() to correctly delete the returned pointer.
   Firestore* CreateFirestore(const std::string& app_name) const;
 
   void DisableNetwork() { Await(firestore()->DisableNetwork()); }
 
   void EnableNetwork() { Await(firestore()->EnableNetwork()); }
 
+  // Deletes the given Firestore instance and deletes the app by which it is
+  // owned.
+  static void Release(Firestore* firestore);
+
  private:
   template 
   friend class EventAccumulator;
diff --git a/firestore/src/tests/firestore_test.cc b/firestore/src/tests/firestore_test.cc
index 4547072f24..9f0f9ac0b1 100644
--- a/firestore/src/tests/firestore_test.cc
+++ b/firestore/src/tests/firestore_test.cc
@@ -8,6 +8,7 @@
 #include "firestore/src/include/firebase/firestore.h"
 #include "firestore/src/tests/firestore_integration_test.h"
 #include "firestore/src/tests/util/event_accumulator.h"
+#include "firestore/src/tests/util/future_test_util.h"
 #if defined(__ANDROID__)
 #include "firestore/src/android/util_android.h"
 #endif  // defined(__ANDROID__)
@@ -556,7 +557,7 @@ TEST_F(FirestoreIntegrationTest, TestCanRetrieveNonexistentDocument) {
   TestEventListener listener{"for document"};
   ListenerRegistration registration = listener.AttachTo(&document);
   Await(listener);
-  EXPECT_EQ(Error::kErrorOk, listener.first_error());
+  EXPECT_EQ(Error::kErrorOk, listener.first_error_code());
   EXPECT_FALSE(listener.last_result().exists());
   registration.Remove();
 }
@@ -583,8 +584,9 @@ TEST_F(FirestoreIntegrationTest,
                               std::vector* events)
         : TestEventListener(std::move(name)), events_(events) {}
 
-    void OnEvent(const DocumentSnapshot& value, Error error) override {
-      TestEventListener::OnEvent(value, error);
+    void OnEvent(const DocumentSnapshot& value, Error error_code,
+                 const std::string& error_message) override {
+      TestEventListener::OnEvent(value, error_code, error_message);
       events_->push_back("doc");
     }
 
@@ -709,20 +711,24 @@ TEST_F(FirestoreIntegrationTest, TestListenCanBeCalledMultipleTimes) {
   Semaphore completed{0};
 #endif
   DocumentSnapshot resulting_data;
-  document.AddSnapshotListener(
-      [&](const DocumentSnapshot& snapshot, Error error) {
-        EXPECT_EQ(Error::kErrorOk, error);
-        document.AddSnapshotListener(
-            [&](const DocumentSnapshot& snapshot, Error error) {
-              EXPECT_EQ(Error::kErrorOk, error);
-              resulting_data = snapshot;
+  document.AddSnapshotListener([&](const DocumentSnapshot& snapshot,
+                                   Error error_code,
+                                   const std::string& error_message) {
+    EXPECT_EQ(Error::kErrorOk, error_code);
+    EXPECT_EQ(std::string(), error_message);
+    document.AddSnapshotListener([&](const DocumentSnapshot& snapshot,
+                                     Error error_code,
+                                     const std::string& error_message) {
+      EXPECT_EQ(Error::kErrorOk, error_code);
+      EXPECT_EQ(std::string(), error_message);
+      resulting_data = snapshot;
 #if defined(__APPLE__)
-              promise.set_value();
+      promise.set_value();
 #else
-              completed.Post();
+      completed.Post();
 #endif
-            });
-      });
+    });
+  });
 #if defined(__APPLE__)
   promise.get_future().wait();
 #else
@@ -741,7 +747,7 @@ TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsNonExistent) {
       listener.AttachTo(&document, MetadataChanges::kInclude);
   Await(listener);
   EXPECT_EQ(1, listener.event_count());
-  EXPECT_EQ(Error::kErrorOk, listener.first_error());
+  EXPECT_EQ(Error::kErrorOk, listener.first_error_code());
   EXPECT_FALSE(listener.last_result().exists());
   registration.Remove();
 }
@@ -829,6 +835,20 @@ TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotEventsForDelete) {
   registration.Remove();
 }
 
+TEST_F(FirestoreIntegrationTest, TestDocumentSnapshotErrorReporting) {
+  DocumentReference document = Collection("col").Document("__badpath__");
+  TestEventListener listener("TestBadPath");
+  ListenerRegistration registration =
+      listener.AttachTo(&document, MetadataChanges::kInclude);
+  Await(listener);
+  EXPECT_EQ(1, listener.event_count());
+  EXPECT_EQ(Error::kErrorInvalidArgument, listener.first_error_code());
+  EXPECT_THAT(listener.first_error_message(),
+              testing::HasSubstr("__badpath__"));
+  EXPECT_FALSE(listener.last_result().exists());
+  registration.Remove();
+}
+
 TEST_F(FirestoreIntegrationTest, TestQuerySnapshotEventsForAdd) {
   CollectionReference collection = Collection();
   DocumentReference document = collection.Document();
@@ -914,6 +934,21 @@ TEST_F(FirestoreIntegrationTest, TestQuerySnapshotEventsForDelete) {
   registration.Remove();
 }
 
+TEST_F(FirestoreIntegrationTest, TestQuerySnapshotErrorReporting) {
+  CollectionReference collection =
+      Collection("a").Document("__badpath__").Collection("b");
+  TestEventListener listener("TestBadPath");
+  ListenerRegistration registration =
+      listener.AttachTo(&collection, MetadataChanges::kInclude);
+  Await(listener);
+  EXPECT_EQ(1, listener.event_count());
+  EXPECT_EQ(Error::kErrorInvalidArgument, listener.first_error_code());
+  EXPECT_THAT(listener.first_error_message(),
+              testing::HasSubstr("__badpath__"));
+  EXPECT_TRUE(listener.last_result().empty());
+  registration.Remove();
+}
+
 TEST_F(FirestoreIntegrationTest,
        TestMetadataOnlyChangesAreNotFiredWhenNoOptionsProvided) {
   DocumentReference document = Collection().Document();
@@ -1145,7 +1180,7 @@ TEST_F(FirestoreIntegrationTest, TestToString) {
 // exceptions.
 #if defined(__ANDROID__)
 TEST_F(FirestoreIntegrationTest, ClientCallsAfterTerminateFails) {
-  Await(firestore()->Terminate());
+  EXPECT_THAT(firestore()->Terminate(), FutureSucceeds());
   EXPECT_THROW(Await(firestore()->DisableNetwork()), FirestoreException);
 }
 
@@ -1154,7 +1189,7 @@ TEST_F(FirestoreIntegrationTest, NewOperationThrowsAfterFirestoreTerminate) {
   DocumentReference reference = firestore()->Document("abc/123");
   Await(reference.Set({{"Field", FieldValue::Integer(100)}}));
 
-  Await(instance->Terminate());
+  EXPECT_THAT(instance->Terminate(), FutureSucceeds());
 
   EXPECT_THROW(Await(reference.Get()), FirestoreException);
   EXPECT_THROW(Await(reference.Update({{"Field", FieldValue::Integer(1)}})),
@@ -1180,12 +1215,12 @@ TEST_F(FirestoreIntegrationTest, TerminateCanBeCalledMultipleTimes) {
   DocumentReference reference = instance->Document("abc/123");
   Await(reference.Set({{"Field", FieldValue::Integer(100)}}));
 
-  Await(instance->Terminate());
+  EXPECT_THAT(instance->Terminate(), FutureSucceeds());
 
   EXPECT_THROW(Await(reference.Get()), FirestoreException);
 
   // Calling a second time should go through and change nothing.
-  Await(instance->Terminate());
+  EXPECT_THAT(instance->Terminate(), FutureSucceeds());
 
   EXPECT_THROW(Await(reference.Update({{"Field", FieldValue::Integer(1)}})),
                FirestoreException);
@@ -1210,12 +1245,15 @@ TEST_F(FirestoreIntegrationTest, RestartFirestoreLeadsToNewInstance) {
 
   // Shutdown `db` and create a new instance, make sure they are different
   // instances.
-  Await(db->Terminate());
+  EXPECT_THAT(db->Terminate(), FutureSucceeds());
   auto db_2 = CreateFirestore(app->name());
   EXPECT_NE(db_2, db);
 
   // Make sure the new instance functions.
   Await(db_2->Document("abc/doc").Set({{"foo", FieldValue::String("bar")}}));
+
+  Release(db_2);
+  Release(db);
 }
 
 TEST_F(FirestoreIntegrationTest, CanStopListeningAfterTerminate) {
@@ -1226,7 +1264,7 @@ TEST_F(FirestoreIntegrationTest, CanStopListeningAfterTerminate) {
       accumulator.listener()->AttachTo(&reference);
 
   accumulator.Await();
-  Await(instance->Terminate());
+  EXPECT_THAT(instance->Terminate(), FutureSucceeds());
 
   // This should proceed without error.
   registration.Remove();
@@ -1273,27 +1311,47 @@ TEST_F(FirestoreIntegrationTest,
   EXPECT_EQ(await_pending_writes.status(), FutureStatus::kFutureStatusComplete);
 }
 
-TEST_F(FirestoreIntegrationTest, CanClearPersistenceAfterRestarting) {
-  Firestore* db = CreateFirestore();
-  App* app = db->app();
-  std::string app_name = app->name();
+TEST_F(FirestoreIntegrationTest, CanClearPersistenceTestHarnessVerification) {
+  // Verify that firestore() and DeleteFirestore() behave how we expect;
+  // otherwise, the tests for ClearPersistence() could yield false positives.
+  Firestore* db = firestore();
+  const std::string app_name = db->app()->name();
+  DocumentReference document = db->Collection("a").Document();
+  const std::string path = document.path();
+  WriteDocument(document, MapFieldValue{{"foo", FieldValue::Integer(42)}});
+  DeleteFirestore();
 
+  Firestore* db_2 = CachedFirestore(app_name);
+  DocumentReference document_2 = db_2->Document(path);
+  Future get_future = document_2.Get(Source::kCache);
+  DocumentSnapshot snapshot_2 = *Await(get_future);
+  EXPECT_THAT(
+      snapshot_2.GetData(),
+      testing::ContainerEq(MapFieldValue{{"foo", FieldValue::Integer(42)}}));
+}
+
+TEST_F(FirestoreIntegrationTest, CanClearPersistenceAfterRestarting) {
+  Firestore* db = firestore();
+  const std::string app_name = db->app()->name();
   DocumentReference document = db->Collection("a").Document("b");
-  std::string path = document.path();
+  const std::string path = document.path();
   WriteDocument(document, MapFieldValue{{"foo", FieldValue::Integer(42)}});
 
-  // ClearPersistence() requires Firestore to be terminated. Delete the app and
-  // the Firestore instance to emulate the way an end user would do this.
-  Await(db->Terminate());
-  Await(db->ClearPersistence());
-  delete db;
-  delete app;
+  // Call ClearPersistence(), but call Terminate() first because
+  // ClearPersistence() requires Firestore to be terminated.
+  EXPECT_THAT(db->Terminate(), FutureSucceeds());
+  EXPECT_THAT(db->ClearPersistence(), FutureSucceeds());
+  // Call DeleteFirestore() to ensure that both the App and Firestore instances
+  // are deleted, which emulates the way an end user would experience their
+  // application being killed and later re-launched by the user.
+  DeleteFirestore();
 
   // We restart the app with the same name and options to check that the
   // previous instance's persistent storage is actually cleared after the
-  // restart. Calling firestore() here would create a new instance of firestore,
-  // which defeats the purpose of this test.
-  Firestore* db_2 = CreateFirestore(app_name);
+  // restart. Although calling firestore() here would do the same thing, we
+  // use CachedFirestore() to be explicit about getting a new Firestore instance
+  // for the same Firebase app.
+  Firestore* db_2 = CachedFirestore(app_name);
   DocumentReference document_2 = db_2->Document(path);
   Future await_get = document_2.Get(Source::kCache);
   Await(await_get);
@@ -1302,24 +1360,31 @@ TEST_F(FirestoreIntegrationTest, CanClearPersistenceAfterRestarting) {
 }
 
 TEST_F(FirestoreIntegrationTest, CanClearPersistenceOnANewFirestoreInstance) {
-  Firestore* db = CreateFirestore();
-  App* app = db->app();
-  std::string app_name = app->name();
-
+  Firestore* db = firestore();
+  const std::string app_name = db->app()->name();
   DocumentReference document = db->Collection("a").Document("b");
-  std::string path = document.path();
+  const std::string path = document.path();
   WriteDocument(document, MapFieldValue{{"foo", FieldValue::Integer(42)}});
 
-  Await(db->Terminate());
-  delete db;
-  delete app;
+  #if defined(__ANDROID__)
+  // TODO(b/168628900) Remove this call to Terminate() once deleting the
+  // Firestore* instance removes the underlying Java object from the instance
+  // cache in Android.
+  EXPECT_THAT(db->Terminate(), FutureSucceeds());
+  #endif
+
+  // Call DeleteFirestore() to ensure that both the App and Firestore instances
+  // are deleted, which emulates the way an end user would experience their
+  // application being killed and later re-launched by the user.
+  DeleteFirestore();
 
   // We restart the app with the same name and options to check that the
   // previous instance's persistent storage is actually cleared after the
-  // restart. Calling firestore() here would create a new instance of firestore,
-  // which defeats the purpose of this test.
-  Firestore* db_2 = CreateFirestore(app_name);
-  Await(db_2->ClearPersistence());
+  // restart. Although calling firestore() here would do the same thing, we
+  // use CachedFirestore() to be explicit about getting a new Firestore instance
+  // for the same Firebase app.
+  Firestore* db_2 = CachedFirestore(app_name);
+  EXPECT_THAT(db_2->ClearPersistence(), FutureSucceeds());
   DocumentReference document_2 = db_2->Document(path);
   Future await_get = document_2.Get(Source::kCache);
   Await(await_get);
diff --git a/firestore/src/tests/includes_test.cc b/firestore/src/tests/includes_test.cc
index 01b64bde13..de52313812 100644
--- a/firestore/src/tests/includes_test.cc
+++ b/firestore/src/tests/includes_test.cc
@@ -19,7 +19,7 @@ class IncludesTest : public testing::Test {
 namespace {
 
 struct TestListener : EventListener {
-  void OnEvent(const int&, Error) override {}
+  void OnEvent(const int&, Error, const std::string&) override {}
 };
 
 struct TestTransactionFunction : TransactionFunction {
diff --git a/firestore/src/tests/jni/declaration_test.cc b/firestore/src/tests/jni/declaration_test.cc
new file mode 100644
index 0000000000..79f2af1dfd
--- /dev/null
+++ b/firestore/src/tests/jni/declaration_test.cc
@@ -0,0 +1,137 @@
+#include "firestore/src/jni/declaration.h"
+
+#include "app/memory/unique_ptr.h"
+#include "app/src/util_android.h"
+#include "firestore/src/jni/env.h"
+#include "firestore/src/jni/hash_map.h"
+#include "firestore/src/jni/iterator.h"
+#include "firestore/src/jni/loader.h"
+#include "firestore/src/jni/set.h"
+#include "firestore/src/tests/firestore_integration_test.h"
+#include "testing/base/public/gmock.h"
+#include "gtest/gtest.h"
+
+namespace firebase {
+namespace firestore {
+namespace jni {
+namespace {
+
+using testing::UnorderedElementsAre;
+
+constexpr char kString[] = "java/lang/String";
+Method kToLowerCase("toLowerCase", "()Ljava/lang/String;");
+StaticField kCaseInsensitiveOrder("CASE_INSENSITIVE_ORDER",
+                                          "Ljava/util/Comparator;");
+StaticMethod kValueOfInt("valueOf", "(I)Ljava/lang/String;");
+
+constexpr char kInteger[] = "java/lang/Integer";
+Constructor kNewInteger("(I)V");
+
+class DeclarationTest : public FirestoreIntegrationTest {
+ public:
+  DeclarationTest() : loader_(app()) { loader_.LoadClass(kString); }
+
+ protected:
+  Loader loader_;
+};
+
+TEST_F(DeclarationTest, TypesAreTriviallyDestructible) {
+  static_assert(std::is_trivially_destructible>::value,
+                "Constructor is trivially destructible");
+  static_assert(std::is_trivially_destructible>::value,
+                "Method is trivially destructible");
+  static_assert(std::is_trivially_destructible>::value,
+                "StaticField is trivially destructible");
+  static_assert(std::is_trivially_destructible>::value,
+                "StaticMethod is trivially destructible");
+}
+
+TEST_F(DeclarationTest, ConstructsObjects) {
+  loader_.LoadClass(kInteger);
+  loader_.Load(kNewInteger);
+  ASSERT_TRUE(loader_.ok());
+
+  Env env;
+  Local result = env.New(kNewInteger, 42);
+  EXPECT_EQ("42", result.ToString(env));
+}
+
+TEST_F(DeclarationTest, CallsObjectMethods) {
+  loader_.Load(kToLowerCase);
+  ASSERT_TRUE(loader_.ok());
+
+  Env env;
+  Local str = env.NewStringUtf("Foo");
+
+  Local result = env.Call(str, kToLowerCase);
+  EXPECT_EQ("foo", result.ToString(env));
+}
+
+TEST_F(DeclarationTest, GetsStaticFields) {
+  loader_.Load(kCaseInsensitiveOrder);
+
+  const char* kComparator = "java/util/Comparator";
+  Method kCompare("compare",
+                           "(Ljava/lang/Object;Ljava/lang/Object;)I");
+  loader_.LoadClass(kComparator);
+  loader_.Load(kCompare);
+  ASSERT_TRUE(loader_.ok());
+
+  Env env;
+  Local ordering = env.Get(kCaseInsensitiveOrder);
+  EXPECT_NE(ordering.get(), nullptr);
+
+  Local uppercase = env.NewStringUtf("GOO");
+  Local lowercase = env.NewStringUtf("foo");
+  EXPECT_EQ(0, env.Call(ordering, kCompare, uppercase, uppercase));
+  EXPECT_EQ(1, env.Call(ordering, kCompare, uppercase, lowercase));
+  EXPECT_EQ(-1, env.Call(ordering, kCompare, lowercase, uppercase));
+}
+
+TEST_F(DeclarationTest, CallsStaticObjectMethods) {
+  loader_.Load(kValueOfInt);
+
+  Env env;
+  Local result = env.Call(kValueOfInt, 42);
+  EXPECT_EQ("42", result.ToString(env));
+}
+
+TEST_F(DeclarationTest, CanUseUnownedClasses) {
+  Constructor ctor("()V");
+  Method add_method("add", "(Ljava/lang/Object;)Z");
+  Method size_method("size", "()I");
+
+  loader_.LoadFromExistingClass("java/util/ArrayList",
+                                util::array_list::GetClass(), ctor, add_method,
+                                size_method);
+
+  Env env;
+  Local str = env.NewStringUtf("foo");
+  Local list = env.New(ctor);
+  EXPECT_TRUE(env.Call(list, add_method, str));
+  EXPECT_EQ(1u, env.Call(list, size_method));
+}
+
+TEST_F(DeclarationTest, CanUseJavaCollections) {
+  Env env;
+  Local key1 = env.NewStringUtf("key1");
+  Local key2 = env.NewStringUtf("key2");
+
+  Local map = HashMap::Create(env);
+  map.Put(env, key1, key1);
+  map.Put(env, key2, key2);
+
+  std::vector actual_keys;
+  Local iter = map.KeySet(env).Iterator(env);
+  while (iter.HasNext(env)) {
+    Local key = iter.Next(env);
+    actual_keys.push_back(key.ToString(env));
+  }
+
+  EXPECT_THAT(actual_keys, UnorderedElementsAre("key1", "key2"));
+}
+
+}  // namespace
+}  // namespace jni
+}  // namespace firestore
+}  // namespace firebase
diff --git a/firestore/src/tests/jni/env_test.cc b/firestore/src/tests/jni/env_test.cc
index 7659049bdd..faf43ba479 100644
--- a/firestore/src/tests/jni/env_test.cc
+++ b/firestore/src/tests/jni/env_test.cc
@@ -1,5 +1,9 @@
 #include "firestore/src/jni/env.h"
 
+#include "app/memory/unique_ptr.h"
+#include "app/meta/move.h"
+#include "firestore/src/android/util_android.h"
+#include "firestore/src/jni/array.h"
 #include "firestore/src/tests/firestore_integration_test.h"
 #include "gtest/gtest.h"
 
@@ -9,10 +13,22 @@ namespace jni {
 
 class EnvTest : public FirestoreIntegrationTest {
  public:
-  EnvTest() : env_(GetEnv()) {}
+  EnvTest() : env_(MakeUnique(GetEnv())) {}
+
+  ~EnvTest() override {
+    // Ensure that after the test is done that any pending exception is cleared
+    // to prevent spurious errors in the teardown of FirestoreIntegrationTest.
+    env_->ExceptionClear();
+  }
+
+  Env& env() const { return *env_; }
 
  protected:
-  Env env_;
+  // Env is declared as having a `noexcept(false)` destructor, which causes the
+  // compiler to propagate this into `EnvTest`'s destructor, but this conflicts
+  // with the declaration of the parent class. Holding `Env` with a unique
+  // pointer sidesteps this restriction.
+  UniquePtr env_;
 };
 
 #if __cpp_exceptions
@@ -32,110 +48,307 @@ TEST_F(EnvTest, ToolchainSupportsThrowingFromDestructors) {
 #endif  // __cpp_exceptions
 
 TEST_F(EnvTest, ConstructsObjects) {
-  Local clazz = env_.FindClass("java/lang/Integer");
-  jmethodID new_integer = env_.GetMethodId(clazz, "", "(I)V");
+  Local clazz = env().FindClass("java/lang/Integer");
+  jmethodID new_integer = env().GetMethodId(clazz, "", "(I)V");
 
-  Local result = env_.New(clazz, new_integer, 42);
-  EXPECT_EQ("42", result.ToString(env_));
+  Local result = env().New(clazz, new_integer, 42);
+  EXPECT_EQ("42", result.ToString(env()));
 }
 
 TEST_F(EnvTest, CallsBooleanMethods) {
-  Local haystack = env_.NewStringUtf("Food");
-  Local needle = env_.NewStringUtf("Foo");
+  Local haystack = env().NewStringUtf("Food");
+  Local needle = env().NewStringUtf("Foo");
 
-  Local clazz = env_.FindClass("java/lang/String");
+  Local clazz = env().FindClass("java/lang/String");
   jmethodID starts_with =
-      env_.GetMethodId(clazz, "startsWith", "(Ljava/lang/String;)Z");
+      env().GetMethodId(clazz, "startsWith", "(Ljava/lang/String;)Z");
 
-  bool result = env_.Call(haystack, starts_with, needle);
+  bool result = env().Call(haystack, starts_with, needle);
   EXPECT_TRUE(result);
 
-  needle = env_.NewStringUtf("Bar");
-  result = env_.Call(haystack, starts_with, needle);
+  needle = env().NewStringUtf("Bar");
+  result = env().Call(haystack, starts_with, needle);
   EXPECT_FALSE(result);
 }
 
 TEST_F(EnvTest, CallsIntMethods) {
-  Local str = env_.NewStringUtf("Foo");
+  Local str = env().NewStringUtf("Foo");
 
-  Local clazz = env_.FindClass("java/lang/String");
-  jmethodID index_of = env_.GetMethodId(clazz, "indexOf", "(I)I");
+  Local clazz = env().FindClass("java/lang/String");
+  jmethodID index_of = env().GetMethodId(clazz, "indexOf", "(I)I");
 
-  int32_t result = env_.Call(str, index_of, jint('o'));
+  int32_t result = env().Call(str, index_of, jint('o'));
   EXPECT_EQ(1, result);
 
-  result = env_.Call(str, index_of, jint('z'));
+  result = env().Call(str, index_of, jint('z'));
   EXPECT_EQ(-1, result);
 }
 
 TEST_F(EnvTest, CallsObjectMethods) {
-  Local str = env_.NewStringUtf("Foo");
+  Local str = env().NewStringUtf("Foo");
 
-  Local clazz = env_.FindClass("java/lang/String");
+  Local clazz = env().FindClass("java/lang/String");
   jmethodID to_lower_case =
-      env_.GetMethodId(clazz, "toLowerCase", "()Ljava/lang/String;");
+      env().GetMethodId(clazz, "toLowerCase", "()Ljava/lang/String;");
 
-  Local result = env_.Call(str, to_lower_case);
-  EXPECT_EQ("foo", result.ToString(env_));
+  Local result = env().Call(str, to_lower_case);
+  EXPECT_EQ("foo", result.ToString(env()));
 }
 
 TEST_F(EnvTest, CallsVoidMethods) {
-  Local clazz = env_.FindClass("java/lang/StringBuilder");
-  jmethodID ctor = env_.GetMethodId(clazz, "", "()V");
-  jmethodID get_length = env_.GetMethodId(clazz, "length", "()I");
-  jmethodID set_length = env_.GetMethodId(clazz, "setLength", "(I)V");
+  Local clazz = env().FindClass("java/lang/StringBuilder");
+  jmethodID ctor = env().GetMethodId(clazz, "", "()V");
+  jmethodID get_length = env().GetMethodId(clazz, "length", "()I");
+  jmethodID set_length = env().GetMethodId(clazz, "setLength", "(I)V");
 
-  Local builder = env_.New(clazz, ctor);
-  env_.Call(builder, set_length, 42);
+  Local builder = env().New(clazz, ctor);
+  env().Call(builder, set_length, 42);
 
-  int32_t length = env_.Call(builder, get_length);
+  int32_t length = env().Call(builder, get_length);
   EXPECT_EQ(length, 42);
 }
 
 TEST_F(EnvTest, GetsStaticFields) {
-  Local clazz = env_.FindClass("java/lang/String");
-  jfieldID comparator = env_.GetStaticFieldId(clazz, "CASE_INSENSITIVE_ORDER",
-                                              "Ljava/util/Comparator;");
+  Local clazz = env().FindClass("java/lang/String");
+  jfieldID comparator = env().GetStaticFieldId(clazz, "CASE_INSENSITIVE_ORDER",
+                                               "Ljava/util/Comparator;");
 
-  Local result = env_.GetStaticField(clazz, comparator);
+  Local result = env().GetStaticField(clazz, comparator);
   EXPECT_NE(result.get(), nullptr);
 }
 
 TEST_F(EnvTest, CallsStaticObjectMethods) {
-  Local clazz = env_.FindClass("java/lang/String");
+  Local clazz = env().FindClass("java/lang/String");
   jmethodID value_of_int =
-      env_.GetStaticMethodId(clazz, "valueOf", "(I)Ljava/lang/String;");
+      env().GetStaticMethodId(clazz, "valueOf", "(I)Ljava/lang/String;");
 
-  Local result = env_.CallStatic(clazz, value_of_int, 42);
-  EXPECT_EQ("42", result.ToString(env_));
+  Local result = env().CallStatic(clazz, value_of_int, 42);
+  EXPECT_EQ("42", result.ToString(env()));
 }
 
 TEST_F(EnvTest, CallsStaticVoidMethods) {
-  Local clazz = env_.FindClass("java/lang/System");
-  jmethodID gc = env_.GetStaticMethodId(clazz, "gc", "()V");
+  Local clazz = env().FindClass("java/lang/System");
+  jmethodID gc = env().GetStaticMethodId(clazz, "gc", "()V");
 
-  env_.CallStatic(clazz, gc);
-  EXPECT_TRUE(env_.ok());
+  env().CallStatic(clazz, gc);
+  EXPECT_TRUE(env().ok());
 }
 
 TEST_F(EnvTest, GetStringUtfLength) {
-  Local str = env_.NewStringUtf("Foo");
-  size_t len = env_.GetStringUtfLength(str);
+  Local str = env().NewStringUtf("Foo");
+  size_t len = env().GetStringUtfLength(str);
   EXPECT_EQ(3, len);
 }
 
 TEST_F(EnvTest, GetStringUtfRegion) {
-  Local str = env_.NewStringUtf("Foo");
-  std::string result = env_.GetStringUtfRegion(str, 1, 2);
+  Local str = env().NewStringUtf("Foo");
+  std::string result = env().GetStringUtfRegion(str, 1, 2);
   EXPECT_EQ("oo", result);
 }
 
 TEST_F(EnvTest, ToString) {
-  Local str = env_.NewStringUtf("Foo");
-  std::string result = str.ToString(env_);
+  Local str = env().NewStringUtf("Foo");
+  std::string result = str.ToString(env());
   EXPECT_EQ("Foo", result);
 }
 
+TEST_F(EnvTest, Throw) {
+  Local clazz = env().FindClass("java/lang/Exception");
+  jmethodID ctor = env().GetMethodId(clazz, "", "(Ljava/lang/String;)V");
+
+  Local message = env().NewStringUtf("Testing throw");
+  Local exception = env().New(clazz, ctor, message);
+
+  // After throwing, use EXPECT rather than ASSERT to ensure that the exception
+  // is cleared.
+  env().Throw(exception);
+  EXPECT_FALSE(env().ok());
+
+  Local thrown = env().ClearExceptionOccurred();
+  EXPECT_EQ(thrown.GetMessage(env()), "Testing throw");
+}
+
+TEST_F(EnvTest, ThrowShortCircuitsExecution) {
+  // Set up the test by obtaining some classes and methods before throwing.
+  Local integer_class = env().FindClass("java/lang/Integer");
+  jmethodID integer_ctor = env().GetMethodId(integer_class, "", "(I)V");
+  jmethodID int_value = env().GetMethodId(integer_class, "intValue", "()I");
+  Local integer = env().New(integer_class, integer_ctor, 42);
+
+  // Verify things work under normal conditions
+  ASSERT_EQ(env().Call(integer, int_value), 42);
+
+  // After throwing, everything should short circuit.
+  Local exception_class = env().FindClass("java/lang/Exception");
+  env().ThrowNew(exception_class, "Testing throw");
+  Local thrown = env().ExceptionOccurred();
+
+  EXPECT_EQ(env().FindClass("java/lang/Double").get(), nullptr);
+  EXPECT_EQ(env().GetMethodId(integer_class, "doubleValue", "()D"), nullptr);
+  EXPECT_EQ(env().Call(integer, int_value), 0);
+
+  EXPECT_EQ(env().New(integer_class, integer_ctor, 95).get(), nullptr);
+  EXPECT_EQ(env().GetObjectClass(integer).get(), nullptr);
+
+  // Methods like IsSameObject also return zero values to short circuit
+  EXPECT_FALSE(env().IsInstanceOf(integer, integer_class));
+  EXPECT_FALSE(env().IsSameObject(exception_class, exception_class));
+
+  env().ExceptionClear();
+
+  // Verify things are back to normal.
+  EXPECT_EQ(env().Call(integer, int_value), 42);
+  EXPECT_TRUE(env().IsInstanceOf(integer, integer_class));
+  EXPECT_TRUE(env().IsSameObject(exception_class, exception_class));
+}
+
+TEST_F(EnvTest, ThrowShortCircuitsThrow) {
+  Local exception_class = env().FindClass("java/lang/Exception");
+  env().ThrowNew(exception_class, "Testing throw");
+  Local thrown = env().ExceptionOccurred();
+
+  env().ThrowNew(exception_class, "Testing throw 2");
+  Local thrown_while_throwing = env().ExceptionOccurred();
+
+  env().ExceptionClear();
+  EXPECT_TRUE(env().IsSameObject(thrown, thrown_while_throwing));
+  EXPECT_EQ(thrown_while_throwing.GetMessage(env()), "Testing throw");
+}
+
+TEST_F(EnvTest, ExceptionClearGuardRunsWhilePending) {
+  Local exception_class = env().FindClass("java/lang/Exception");
+  env().ThrowNew(exception_class, "Testing throw");
+  Local thrown = env().ExceptionOccurred();
+
+  EXPECT_EQ(thrown.GetMessage(env()), "Testing throw");
+  EXPECT_FALSE(env().ok());
+
+  {
+    ExceptionClearGuard block(env());
+    EXPECT_TRUE(env().ok());
+  }
+
+  EXPECT_EQ(thrown.GetMessage(env()), "Testing throw");
+  EXPECT_FALSE(env().ok());
+
+  {
+    ExceptionClearGuard block(env());
+    EXPECT_TRUE(env().ok());
+    EXPECT_TRUE(env().IsInstanceOf(thrown, exception_class));
+
+    // A new exception thrown while in the block will cause the prior exception
+    // to be lost. This mirrors the behavior of a Java finally block.
+    env().ThrowNew(exception_class, "Testing throw 2");
+    EXPECT_EQ(env().ExceptionOccurred().GetMessage(env()), "Testing throw 2");
+  }
+  EXPECT_EQ(env().ExceptionOccurred().GetMessage(env()), "Testing throw 2");
+
+  env().ExceptionClear();
+  {
+    ExceptionClearGuard block(env());
+    env().ThrowNew(exception_class, "Testing throw 3");
+  }
+
+  // Outside the block, the exception persists, again mirroring the behavior of
+  // a Java finally block.
+  EXPECT_EQ(env().ExceptionOccurred().GetMessage(env()), "Testing throw 3");
+}
+
+TEST_F(EnvTest, DestructorCallsExceptionHandler) {
+  struct Result {
+    Local exception;
+    int calls = 0;
+  };
+
+  auto handler = [](Env& env, Local&& exception, void* context) {
+    env.ExceptionClear();
+
+    auto result = static_cast(context);
+    result->exception = Move(exception);
+    result->calls++;
+  };
+
+  Result result;
+  {
+    Env env;
+    env.SetUnhandledExceptionHandler(handler, &result);
+  }
+  EXPECT_EQ(result.exception.get(), nullptr);
+  EXPECT_EQ(result.calls, 0);
+
+  {
+    Env env;
+    env.SetUnhandledExceptionHandler(handler, &result);
+    env.ThrowNew(env.FindClass("java/lang/Exception"), "testing");
+    EXPECT_EQ(result.calls, 0);
+  }
+  EXPECT_NE(result.exception.get(), nullptr);
+  EXPECT_EQ(result.exception.GetMessage(env()), "testing");
+  EXPECT_EQ(result.calls, 1);
+}
+
+#if __cpp_exceptions
+TEST_F(EnvTest, DestructorCanThrow) {
+  bool caught_exception = false;
+  try {
+    Env env;
+    env.SetUnhandledExceptionHandler(GlobalUnhandledExceptionHandler, nullptr);
+
+    env.ThrowNew(env.FindClass("java/lang/Exception"), "testing");
+
+    // When `env` is destroyed with a pending exception, it will throw.
+  } catch (const FirestoreException& e) {
+    caught_exception = true;
+    EXPECT_STREQ(e.what(), "testing");
+  }
+  EXPECT_TRUE(caught_exception);
+}
+#endif  // __cpp_exceptions
+
+TEST_F(EnvTest, ObjectArrayOperations) {
+  Env env;
+  Local> array = env.NewArray(2, String::GetClass());
+
+  array.Set(env, 0, env.NewStringUtf("str"));
+  Local value = array.Get(env, 0);
+  ASSERT_EQ(value.ToString(env), "str");
+
+  value = array.Get(env, 1);
+  ASSERT_EQ(value.get(), nullptr);
+}
+
+TEST_F(EnvTest, PrimitiveArrayOperations) {
+  Env env;
+
+  Class string_class = String::GetClass();
+  jmethodID ctor =
+      env.GetMethodId(string_class, "", "([BLjava/lang/String;)V");
+  jmethodID get_bytes =
+      env.GetMethodId(string_class, "getBytes", "(Ljava/lang/String;)[B");
+
+  Local encoding = env.NewStringUtf("UTF-8");
+
+  std::vector blob = {'f', 'o', 'o'};
+  Local> array = env.NewArray(blob.size());
+  env.SetArrayRegion(array, 0, blob.size(), &blob[0]);
+
+  Local str = env.New(string_class, ctor, array, encoding);
+  ASSERT_EQ(str.ToString(env), "foo");
+
+  Local> str_bytes =
+      env.Call>(str, get_bytes, encoding);
+
+  std::vector result(2);
+  env.GetArrayRegion(str_bytes, 1, 2, &result[0]);
+
+  std::vector expected = {'o', 'o'};
+  ASSERT_EQ(expected, result);
+
+  result = env.GetArrayRegion(str_bytes, 2, 1);
+  expected = {'o'};
+  ASSERT_EQ(expected, result);
+}
+
 }  // namespace jni
 }  // namespace firestore
 }  // namespace firebase
diff --git a/firestore/src/tests/jni/traits_test.cc b/firestore/src/tests/jni/traits_test.cc
index 4e0094998a..6c3dc0108d 100644
--- a/firestore/src/tests/jni/traits_test.cc
+++ b/firestore/src/tests/jni/traits_test.cc
@@ -129,6 +129,39 @@ TEST_F(TraitsTest, DecaysBeforeMappingTypes) {
   StaticAssertTypeEq, jobject>();
 }
 
+TEST_F(TraitsTest, ToJniHandlesPointers) {
+  // Baseline sanity checks that undergird our reasoning for being able to
+  // reinterpret_cast pointers to these types. Note that in C++ we prefer
+  // uint8_t for bytes despite the fact that Java defines these as signed.
+  // Even though signedness differs, these types are still similar and therefore
+  // valid to reinterpret.
+  static_assert(is_same::value, "JNI type is sane");
+  static_assert(is_same::value, "JNI type is sane");
+  static_assert(is_same::value, "JNI type is sane");
+  static_assert(is_same::value, "JNI type is sane");
+  static_assert(is_same::value, "JNI type is sane");
+  static_assert(is_same::value, "JNI type is sane");
+
+  // These assertions reflect our C++ preferred type.
+  static_assert(!internal::IsConvertiblePointer(), "bool not OK");
+  static_assert(internal::IsConvertiblePointer(), "uint8_t OK");
+  static_assert(internal::IsConvertiblePointer(), "uint16_t OK");
+  static_assert(internal::IsConvertiblePointer(), "int16_t OK");
+  static_assert(internal::IsConvertiblePointer(), "int32_t OK");
+  static_assert(internal::IsConvertiblePointer(), "int64_t OK");
+  static_assert(!internal::IsConvertiblePointer(), "size_t not OK");
+
+  uint8_t bytes[2] = {1, 2};
+  const jbyte* bytes_result = ToJni(bytes);
+  EXPECT_EQ(*bytes_result, 1);
+  EXPECT_EQ(bytes_result[1], 2);
+
+  int64_t longs[2] = {1, 2};
+  jlong* longs_result = ToJni(static_cast(longs));
+  EXPECT_EQ(*longs_result, 1);
+  EXPECT_EQ(longs_result[1], 2);
+}
+
 }  // namespace
 }  // namespace jni
 }  // namespace firestore
diff --git a/firestore/src/tests/query_test.cc b/firestore/src/tests/query_test.cc
index 0a48e4f2df..44b67bdcee 100644
--- a/firestore/src/tests/query_test.cc
+++ b/firestore/src/tests/query_test.cc
@@ -685,19 +685,11 @@ TEST_F(FirestoreIntegrationTest,
 
 #if defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD)
 TEST_F(QueryTest, Construction) {
-<<<<<<< HEAD
   testutil::AssertWrapperConstructionContract();
 }
 
 TEST_F(QueryTest, Assignment) {
   testutil::AssertWrapperAssignmentContract();
-=======
-  testutil::AssertWrapperConstructionContract();
-}
-
-TEST_F(QueryTest, Assignment) {
-  testutil::AssertWrapperAssignmentContract();
->>>>>>> dev
 }
 #endif  // defined(__ANDROID__) || defined(FIRESTORE_STUB_BUILD)
 
diff --git a/firestore/src/tests/util/event_accumulator.h b/firestore/src/tests/util/event_accumulator.h
index 70742c1a71..89ddbff856 100644
--- a/firestore/src/tests/util/event_accumulator.h
+++ b/firestore/src/tests/util/event_accumulator.h
@@ -19,12 +19,13 @@ class EventAccumulator {
     int desired_events = num_events_consumed_ + num_events;
     FirestoreIntegrationTest::Await(listener_, desired_events);
 
-    if (listener_.first_error() != Error::kErrorOk ||
+    if (listener_.first_error_code() != Error::kErrorOk ||
         listener_.event_count() < desired_events) {
       int received = listener_.event_count() - num_events_consumed_;
       ADD_FAILURE() << "Failed to await " << num_events
-                    << " events: error=" << listener_.first_error()
-                    << ", received " << received << " events";
+                    << " events: error_code=" << listener_.first_error_code()
+                    << " error_message=\"" << listener_.first_error_message()
+                    << "\", received " << received << " events";
 
       // If there are fewer events than requested, discard them.
       num_events_consumed_ += received;
diff --git a/firestore/src/tests/util/future_test_util.cc b/firestore/src/tests/util/future_test_util.cc
new file mode 100644
index 0000000000..4c204f2ca7
--- /dev/null
+++ b/firestore/src/tests/util/future_test_util.cc
@@ -0,0 +1,61 @@
+#include "firestore/src/tests/util/future_test_util.h"
+
+#include 
+
+#include "firestore/src/tests/firestore_integration_test.h"
+#include "firebase/firestore/firestore_errors.h"
+
+namespace firebase {
+
+namespace {
+
+void PrintTo(std::ostream* os, FutureStatus future_status, int error,
+             const char* error_message = nullptr) {
+  *os << "Future{status=" << ToEnumeratorName(future_status)
+      << " error=" << firestore::ToFirestoreErrorCodeName(error);
+  if (error_message != nullptr) {
+    *os << " error_message=" << error_message;
+  }
+  *os << "}";
+}
+
+class FutureSucceedsImpl
+    : public testing::MatcherInterface&> {
+ public:
+  void DescribeTo(std::ostream* os) const override {
+    PrintTo(os, FutureStatus::kFutureStatusComplete,
+            firestore::Error::kErrorOk);
+  }
+
+  bool MatchAndExplain(const Future& future,
+                       testing::MatchResultListener*) const override {
+    firestore::WaitFor(future);
+    return future.status() == FutureStatus::kFutureStatusComplete &&
+           future.error() == firestore::Error::kErrorOk;
+  }
+};
+
+}  // namespace
+
+std::string ToEnumeratorName(FutureStatus status) {
+  switch (status) {
+    case kFutureStatusComplete:
+      return "kFutureStatusComplete";
+    case kFutureStatusPending:
+      return "kFutureStatusPending";
+    case kFutureStatusInvalid:
+      return "kFutureStatusInvalid";
+    default:
+      return "[invalid FutureStatus]";
+  }
+}
+
+void PrintTo(const Future& future, std::ostream* os) {
+  PrintTo(os, future.status(), future.error(), future.error_message());
+}
+
+testing::Matcher&> FutureSucceeds() {
+  return testing::Matcher&>(new FutureSucceedsImpl());
+}
+
+}  // namespace firebase
diff --git a/firestore/src/tests/util/future_test_util.h b/firestore/src/tests/util/future_test_util.h
new file mode 100644
index 0000000000..f4111798cd
--- /dev/null
+++ b/firestore/src/tests/util/future_test_util.h
@@ -0,0 +1,35 @@
+#ifndef FIREBASE_FIRESTORE_CLIENT_CPP_SRC_TESTS_UTIL_FUTURE_TEST_UTIL_H_
+#define FIREBASE_FIRESTORE_CLIENT_CPP_SRC_TESTS_UTIL_FUTURE_TEST_UTIL_H_
+
+#include 
+#include 
+
+#include "app/src/include/firebase/future.h"
+#include "third_party/googletest/googletest/include/gtest/gtest-matchers.h"
+
+namespace firebase {
+
+// Prints a human-friendly representation of a Future to an ostream.
+// This function is found dynamically by googletest to print a Future object
+// in a test failure message.
+void PrintTo(const Future& future, std::ostream* os);
+
+// Creates and returns a "matcher" for a Future's success. The matcher will wait
+// for the Future to complete with a timeout. If the timeout is reached or the
+// Future completes unsuccessfully then the matcher will fail; otherwise, it
+// will succeed.
+//
+// Here is an example of how this function could be used:
+// EXPECT_THAT(firestore()->Terminate(), FutureSucceeds());
+testing::Matcher&> FutureSucceeds();
+
+// Converts a `FutureStatus` value to its enumerator name, and returns it. For
+// example, if `kFutureStatusComplete` is specified then "kFutureStatusComplete"
+// will be returned. If the argument is invalid (i.e. not equal to any element
+// from `FutureStatus`) then this function will gracefully return a "name" to
+// indicate this.
+std::string ToEnumeratorName(FutureStatus status);
+
+}  // namespace firebase
+
+#endif  // FIREBASE_FIRESTORE_CLIENT_CPP_SRC_TESTS_UTIL_FUTURE_TEST_UTIL_H_
diff --git a/firestore/src/tests/validation_test.cc b/firestore/src/tests/validation_test.cc
index 61be692f37..94df70e626 100644
--- a/firestore/src/tests/validation_test.cc
+++ b/firestore/src/tests/validation_test.cc
@@ -571,8 +571,8 @@ TEST_F(ValidationTest, QueriesWithNullOrNaNFiltersOtherThanEqualityFail) {
     FAIL() << "should throw exception";
   } catch (const FirestoreException& exception) {
     EXPECT_STREQ(
-        "Invalid Query. Null supports only equality comparisons (via "
-        "whereEqualTo()).",
+        "Invalid Query. Null only supports comparisons via whereEqualTo() and "
+        "whereNotEqualTo().",
         exception.what());
   }
   try {
@@ -580,8 +580,8 @@ TEST_F(ValidationTest, QueriesWithNullOrNaNFiltersOtherThanEqualityFail) {
     FAIL() << "should throw exception";
   } catch (const FirestoreException& exception) {
     EXPECT_STREQ(
-        "Invalid Query. Null supports only equality comparisons (via "
-        "whereEqualTo()).",
+        "Invalid Query. Null only supports comparisons via whereEqualTo() and "
+        "whereNotEqualTo().",
         exception.what());
   }
   try {
@@ -589,8 +589,8 @@ TEST_F(ValidationTest, QueriesWithNullOrNaNFiltersOtherThanEqualityFail) {
     FAIL() << "should throw exception";
   } catch (const FirestoreException& exception) {
     EXPECT_STREQ(
-        "Invalid Query. NaN supports only equality comparisons (via "
-        "whereEqualTo()).",
+        "Invalid Query. NaN only supports comparisons via whereEqualTo() and "
+        "whereNotEqualTo().",
         exception.what());
   }
   try {
@@ -598,8 +598,8 @@ TEST_F(ValidationTest, QueriesWithNullOrNaNFiltersOtherThanEqualityFail) {
     FAIL() << "should throw exception";
   } catch (const FirestoreException& exception) {
     EXPECT_STREQ(
-        "Invalid Query. NaN supports only equality comparisons (via "
-        "whereEqualTo()).",
+        "Invalid Query. NaN only supports comparisons via whereEqualTo() and "
+        "whereNotEqualTo().",
         exception.what());
   }
 }
@@ -664,7 +664,8 @@ TEST_F(ValidationTest,
 
   EXPECT_THROW(collection.OrderBy(FieldPath({"timestamp"}))
                    .EndAt(snapshot.documents().at(0))
-                   .AddSnapshotListener([](const QuerySnapshot&, Error) {}),
+                   .AddSnapshotListener(
+                       [](const QuerySnapshot&, Error, const std::string&) {}),
                FirestoreException);
 
   Await(firestore()->EnableNetwork());
@@ -673,8 +674,9 @@ TEST_F(ValidationTest,
   snapshot = accumulator.AwaitRemoteEvent();
   EXPECT_FALSE(snapshot.metadata().has_pending_writes());
   EXPECT_NO_THROW(collection.OrderBy(FieldPath({"timestamp"}))
-                   .EndAt(snapshot.documents().at(0))
-                   .AddSnapshotListener([](const QuerySnapshot&, Error) {}));
+                      .EndAt(snapshot.documents().at(0))
+                      .AddSnapshotListener([](const QuerySnapshot&, Error,
+                                              const std::string&) {}));
 }
 
 
@@ -733,8 +735,9 @@ TEST_F(ValidationTest, QueriesWithDifferentInequalityFieldsFail) {
     FAIL() << "should throw exception";
   } catch (const FirestoreException& exception) {
     EXPECT_STREQ(
-        "All where filters other than whereEqualTo() must be on the same "
-        "field. But you have filters on 'x' and 'y'",
+        "All where filters with an inequality (notEqualTo, notIn, lessThan, "
+        "lessThanOrEqualTo, greaterThan, or greaterThanOrEqualTo) must be on "
+        "the same field. But you have filters on 'x' and 'y'",
         exception.what());
   }
 }
diff --git a/ios_pod/Podfile b/ios_pod/Podfile
index e41e99243c..0c25b291a6 100644
--- a/ios_pod/Podfile
+++ b/ios_pod/Podfile
@@ -2,21 +2,19 @@ source 'https://github.com/CocoaPods/Specs.git'
 platform :ios, '8.0'
 
 target 'GetPods' do
-  pod 'Firebase/Core', '6.24.0'
+  pod 'Firebase/Core', '6.32.2'
 
-  pod 'Firebase/AdMob', '6.24.0'
-  pod 'Firebase/Analytics', '6.24.0'
-  pod 'Firebase/Auth', '6.24.0'
-  pod 'Firebase/Database', '6.24.0'
-  pod 'Firebase/DynamicLinks', '6.24.0'
-  # Firestore is pinned to Firebase/Firebase 6.26.0
-  # Return back to Firebase/Firebase when updating the rest
-  pod 'FirebaseFirestore', '1.15.0'
-  pod 'Firebase/Functions', '6.24.0'
-  pod 'FirebaseInstanceID', '4.3.4'
-  pod 'Firebase/Messaging', '6.24.0'
-  pod 'Firebase/RemoteConfig', '6.24.0'
-  pod 'Firebase/Storage', '6.24.0'
+  pod 'Firebase/AdMob', '6.32.2'
+  pod 'Firebase/Analytics', '6.32.2'
+  pod 'Firebase/Auth', '6.32.2'
+  pod 'Firebase/Database', '6.32.2'
+  pod 'Firebase/DynamicLinks', '6.32.2'
+  pod 'Firebase/Firestore', '6.32.2'
+  pod 'Firebase/Functions', '6.32.2'
+  pod 'FirebaseInstanceID', '4.6.0'
+  pod 'Firebase/Messaging', '6.32.2'
+  pod 'Firebase/RemoteConfig', '6.32.2'
+  pod 'Firebase/Storage', '6.32.2'
 
   pod 'Crashlytics', '3.14.0'
 end
diff --git a/merge_libraries_test_file.cc b/merge_libraries_test_file.cc
index 43e913050f..8c14f2cbe3 100644
--- a/merge_libraries_test_file.cc
+++ b/merge_libraries_test_file.cc
@@ -17,6 +17,11 @@
 /* This is a test file for merge_libraries.py tests. It contains some C and C++
  * symbols that merge_libraries can rename. */
 
+#include 
+#include 
+#include 
+#include 
+
 extern "C" {
 int test_symbol(void) { return 1; }  // NOLINT
 
@@ -34,11 +39,15 @@ namespace test_namespace {
 class TestClass {
  public:
   TestClass();
+  TestClass(const TestClass&);   // not in this file
+  TestClass(const TestClass&&);  // not in this file
+  ~TestClass();                  // not in this file
   int TestMethod();
   int TestMethodNotInThisfile();
-  int TestStaticMethod();
-  int TestStaticMethodNotInThisFile();
-  static int test_static_field;  // NOLINT
+  static int TestStaticMethod();
+  static int TestStaticMethodNotInThisFile();
+  static int test_static_field;                   // NOLINT
+  static int test_static_field_not_in_this_file;  // NOLINT
 };
 
 int global_cpp_symbol = 12345;  // NOLINT
@@ -47,8 +56,34 @@ int TestClass::test_static_field;
 
 TestClass::TestClass() {}
 
-int TestClass::TestMethod() { return not_in_this_file(); }
+int TestClass::TestMethod() {
+  return TestMethodNotInThisfile() + not_in_this_file();
+}
 
 int TestClass::TestStaticMethod() { return TestStaticMethodNotInThisFile(); }
 
 }  // namespace test_namespace
+
+void GlobalFunctionWithParameter(test_namespace::TestClass const&, int) {}
+
+void GlobalFunctionWithMultipleParameters(
+    test_namespace::TestClass* p1, std::vector p2,
+    std::unique_ptr p3,
+    std::vector> p4, std::string) {
+  p2.push_back(*p1);
+  p2.pop_back();
+  p4.push_back(std::move(p3));
+  p4.pop_back();
+}
+
+extern void ExternFunctionWithParameter(test_namespace::TestClass&&, int);
+
+extern void ExternFunctionWithMultipleParameters(
+    const test_namespace::TestClass&,
+    std::unique_ptr, std::string);
+
+namespace another_namespace {
+
+extern void ExternFunctionNotUsingNamespace(std::string);
+
+}  // namespace another_namespace
diff --git a/messaging/messaging_java/build.gradle b/messaging/messaging_java/build.gradle
index 0f95391ecb..60c27c2553 100644
--- a/messaging/messaging_java/build.gradle
+++ b/messaging/messaging_java/build.gradle
@@ -51,8 +51,8 @@ android {
 }
 
 dependencies {
-  implementation 'com.google.firebase:firebase-analytics:17.4.4'
-  implementation 'com.google.firebase:firebase-messaging:20.2.3'
+  implementation 'com.google.firebase:firebase-analytics:17.5.0'
+  implementation 'com.google.firebase:firebase-messaging:20.2.4'
   implementation 'com.google.flatbuffers:flatbuffers-java:1.9.0'
 }
 
diff --git a/messaging/src/android/java/LibraryManifest.xml b/messaging/src/android/java/LibraryManifest.xml
index 14fa6bd261..61c057a919 100644
--- a/messaging/src/android/java/LibraryManifest.xml
+++ b/messaging/src/android/java/LibraryManifest.xml
@@ -1,6 +1,11 @@
 
 
-    
-    
+    
+     
+        
+    
 
diff --git a/messaging/src/android/java/com/google/firebase/messaging/MessageForwardingService.java b/messaging/src/android/java/com/google/firebase/messaging/MessageForwardingService.java
index a52903ed19..2171ee6e13 100644
--- a/messaging/src/android/java/com/google/firebase/messaging/MessageForwardingService.java
+++ b/messaging/src/android/java/com/google/firebase/messaging/MessageForwardingService.java
@@ -14,9 +14,9 @@
 
 package com.google.firebase.messaging;
 
-import android.app.IntentService;
 import android.content.Context;
 import android.content.Intent;
+import androidx.core.app.JobIntentService;
 import com.google.firebase.messaging.cpp.DebugLogging;
 import com.google.firebase.messaging.cpp.MessageWriter;
 
@@ -24,19 +24,13 @@
  * Listens for Message intents from the application and sends them to the C++ app via the
  * ListenerService.
  */
-public class MessageForwardingService extends IntentService {
+public class MessageForwardingService extends JobIntentService {
   private static final String TAG = "FIREBASE_MSG_FWDR";
   public static final String ACTION_REMOTE_INTENT = "com.google.android.c2dm.intent.RECEIVE";
 
-  public MessageForwardingService() {
-    // The tag here is used only to name the worker thread; it's important only for debugging.
-    // http://developer.android.com/reference/android/app/IntentService.html#IntentService(java.lang.String)
-    super(TAG);
-  }
-
   // Handle message intents sent from the ListenerService.
   @Override
-  protected void onHandleIntent(Intent intent) {
+  protected void onHandleWork(Intent intent) {
     handleIntent(this, intent, MessageWriter.defaultInstance());
   }
 
diff --git a/messaging/src/include/firebase/messaging.h b/messaging/src/include/firebase/messaging.h
index dfa4a6705c..13ed783944 100644
--- a/messaging/src/include/firebase/messaging.h
+++ b/messaging/src/include/firebase/messaging.h
@@ -562,7 +562,9 @@ Future RequestPermissionLastResult();
 /// [FCM Developers Guide]: https://firebase.google.com/docs/cloud-messaging/
 ///
 /// @param[in] message The message to send upstream.
-void Send(const Message& message);
+///
+/// @deprecated Send() is deprecated and will be removed in a future release.
+FIREBASE_DEPRECATED void Send(const Message& message);
 
 /// @brief Subscribe to receive all messages to the specified topic.
 ///
diff --git a/release_build_files/Android/firebase_dependencies.gradle b/release_build_files/Android/firebase_dependencies.gradle
index 50717b914f..e5269c2e26 100644
--- a/release_build_files/Android/firebase_dependencies.gradle
+++ b/release_build_files/Android/firebase_dependencies.gradle
@@ -16,21 +16,25 @@ import org.gradle.util.ConfigureUtil;
 
 // A map of library to the dependencies that need to be added for it.
 def firebaseDependenciesMap = [
-  'app' : ['com.google.firebase:firebase-analytics:17.4.4'],
-  'admob' : ['com.google.firebase:firebase-ads:19.2.0',
-             'com.google.android.gms:play-services-measurement-sdk-api:17.4.4'],
-  'analytics' : ['com.google.firebase:firebase-analytics:17.4.4'],
+  'app' : ['com.google.firebase:firebase-analytics:17.5.0'],
+  'admob' : ['com.google.firebase:firebase-ads:19.3.0',
+             'com.google.android.gms:play-services-measurement-sdk-api:17.5.0',
+             'com.google.android.gms:play-services-base:17.4.0'],
+  'analytics' : ['com.google.firebase:firebase-analytics:17.5.0',
+                 'com.google.android.gms:play-services-base:17.4.0'],
   'auth' : ['com.google.firebase:firebase-auth:19.3.2'],
-  'database' : ['com.google.firebase:firebase-database:19.3.1'],
+  'database' : ['com.google.firebase:firebase-database:19.4.0'],
   'dynamic_links' : ['com.google.firebase:firebase-dynamic-links:19.1.0'],
-  'firestore' : ['com.google.firebase:firebase-firestore:21.5.0'],
-  'functions' : ['com.google.firebase:firebase-functions:19.0.2'],
-  'instance_id' : ['com.google.firebase:firebase-iid:20.2.3'],
+  'firestore' : ['com.google.firebase:firebase-firestore:21.6.0'],
+  'functions' : ['com.google.firebase:firebase-functions:19.1.0'],
+  'instance_id' : ['com.google.firebase:firebase-iid:20.2.4'],
   'messaging' : ['com.google.firebase.messaging.cpp:firebase_messaging_cpp@aar',
-                 'com.google.firebase:firebase-messaging:20.2.3'],
-  'performance' : ['com.google.firebase:firebase-perf:19.0.7'],
-  'remote_config' : ['com.google.firebase:firebase-config:19.2.0'],
-  'storage' : ['com.google.firebase:firebase-storage:19.1.1'],
+                 'com.google.firebase:firebase-messaging:20.2.4',
+                 'androidx.core:core:1.0.1'],
+  'performance' : ['com.google.firebase:firebase-perf:19.0.8'],
+  'remote_config' : ['com.google.firebase:firebase-config:19.2.0',
+                     'com.google.android.gms:play-services-base:17.4.0'],
+  'storage' : ['com.google.firebase:firebase-storage:19.2.0'],
   'testlab' : []
 ]
 
diff --git a/storage/storage_resources/build.gradle b/storage/storage_resources/build.gradle
index bb2d8f4fa9..868de4ea34 100644
--- a/storage/storage_resources/build.gradle
+++ b/storage/storage_resources/build.gradle
@@ -45,8 +45,8 @@ android {
 }
 
 dependencies {
-  implementation 'com.google.firebase:firebase-analytics:17.4.4'
-  implementation 'com.google.firebase:firebase-storage:19.1.1'
+  implementation 'com.google.firebase:firebase-analytics:17.5.0'
+  implementation 'com.google.firebase:firebase-storage:19.2.0'
 }
 
 afterEvaluate {
diff --git a/version.sh b/version.sh
index f4ad7d42e1..7bcd951a0a 100755
--- a/version.sh
+++ b/version.sh
@@ -18,9 +18,9 @@
 
 # Used to generate Pod dependencies in the Firebase Unity plugin.
 declare -A POD_SDK_VERSIONS=(
-  ["released"]="6.24.0"  # gen_build.sh: [FIR] Managed by a script. DO NOT EDIT.
-  ["stable"]="6.24.0"  # gen_build.sh: [FIR] Managed by a script. DO NOT EDIT.
-  ["head"]="6.24.0"  # gen_build.sh: [FIR] Managed by a script. DO NOT EDIT.
+  ["released"]="6.32.2"  # gen_build.sh: [FIR] Managed by a script. DO NOT EDIT.
+  ["stable"]="6.32.2"  # gen_build.sh: [FIR] Managed by a script. DO NOT EDIT.
+  ["head"]="6.32.2"  # gen_build.sh: [FIR] Managed by a script. DO NOT EDIT.
 )
 
 help() {