task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithCustomToken:success");
FirebaseUser user = mAuth.getCurrentUser();
updateUI(user);
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCustomToken:failure", task.getException());
Toast.makeText(getContext(), "Authentication failed.",
Toast.LENGTH_SHORT).show();
updateUI(null);
}
}
});
}
private void updateUI(FirebaseUser user) {
if (user != null) {
mBinding.textSignInStatus.setText("User ID: " + user.getUid());
} else {
mBinding.textSignInStatus.setText("Error: sign in failed.");
}
}
private void setCustomToken(String token) {
mCustomToken = token;
String status;
if (mCustomToken != null) {
status = "Token:" + mCustomToken;
} else {
status = "Token: null";
}
// Enable/disable sign-in button and show the token
mBinding.buttonSignIn.setEnabled((mCustomToken != null));
mBinding.textTokenStatus.setText(status);
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/EmailPasswordFragment.java
================================================
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.quickstart.auth.java;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthMultiFactorException;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.MultiFactorResolver;
import com.google.firebase.quickstart.auth.R;
import com.google.firebase.quickstart.auth.databinding.FragmentEmailpasswordBinding;
public class EmailPasswordFragment extends BaseFragment {
private static final String TAG = "EmailPassword";
private FragmentEmailpasswordBinding mBinding;
private FirebaseAuth mAuth;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentEmailpasswordBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setProgressBar(mBinding.progressBar);
// Buttons
mBinding.emailSignInButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String email = mBinding.fieldEmail.getText().toString();
String password = mBinding.fieldPassword.getText().toString();
signIn(email, password);
}
});
mBinding.emailCreateAccountButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String email = mBinding.fieldEmail.getText().toString();
String password = mBinding.fieldPassword.getText().toString();
createAccount(email, password);
}
});
mBinding.signOutButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signOut();
}
});
mBinding.verifyEmailButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendEmailVerification();
}
});
mBinding.reloadButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
reload();
}
});
// Initialize Firebase Auth
mAuth = FirebaseAuth.getInstance();
}
@Override
public void onStart() {
super.onStart();
// Check if user is signed in (non-null) and update UI accordingly.
FirebaseUser currentUser = mAuth.getCurrentUser();
if(currentUser != null){
reload();
}
}
private void createAccount(String email, String password) {
Log.d(TAG, "createAccount:" + email);
if (!validateForm()) {
return;
}
showProgressBar();
mAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(requireActivity(), new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "createUserWithEmail:success");
FirebaseUser user = mAuth.getCurrentUser();
updateUI(user);
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "createUserWithEmail:failure", task.getException());
Toast.makeText(getContext(), "Authentication failed.",
Toast.LENGTH_SHORT).show();
updateUI(null);
}
hideProgressBar();
}
});
}
private void signIn(String email, String password) {
Log.d(TAG, "signIn:" + email);
if (!validateForm()) {
return;
}
showProgressBar();
mAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(requireActivity(), new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithEmail:success");
FirebaseUser user = mAuth.getCurrentUser();
updateUI(user);
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithEmail:failure", task.getException());
Toast.makeText(getContext(), "Authentication failed.",
Toast.LENGTH_SHORT).show();
updateUI(null);
checkForMultiFactorFailure(task.getException());
}
if (!task.isSuccessful()) {
mBinding.status.setText(R.string.auth_failed);
}
hideProgressBar();
}
});
}
private void signOut() {
mAuth.signOut();
updateUI(null);
}
private void sendEmailVerification() {
// Disable button
mBinding.verifyEmailButton.setEnabled(false);
// Send verification email
final FirebaseUser user = mAuth.getCurrentUser();
user.sendEmailVerification()
.addOnCompleteListener(requireActivity(), new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
// Re-enable button
mBinding.verifyEmailButton.setEnabled(true);
if (task.isSuccessful()) {
Toast.makeText(getContext(),
"Verification email sent to " + user.getEmail(),
Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "sendEmailVerification", task.getException());
Toast.makeText(getContext(),
"Failed to send verification email.",
Toast.LENGTH_SHORT).show();
}
}
});
}
private void reload() {
mAuth.getCurrentUser().reload().addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
updateUI(mAuth.getCurrentUser());
Toast.makeText(getContext(),
"Reload successful!",
Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "reload", task.getException());
Toast.makeText(getContext(),
"Failed to reload user.",
Toast.LENGTH_SHORT).show();
}
}
});
}
private boolean validateForm() {
boolean valid = true;
String email = mBinding.fieldEmail.getText().toString();
if (TextUtils.isEmpty(email)) {
mBinding.fieldEmail.setError("Required.");
valid = false;
} else {
mBinding.fieldEmail.setError(null);
}
String password = mBinding.fieldPassword.getText().toString();
if (TextUtils.isEmpty(password)) {
mBinding.fieldPassword.setError("Required.");
valid = false;
} else {
mBinding.fieldPassword.setError(null);
}
return valid;
}
private void updateUI(FirebaseUser user) {
hideProgressBar();
if (user != null) {
mBinding.status.setText(getString(R.string.emailpassword_status_fmt,
user.getEmail(), user.isEmailVerified()));
mBinding.detail.setText(getString(R.string.firebase_status_fmt, user.getUid()));
mBinding.emailPasswordButtons.setVisibility(View.GONE);
mBinding.emailPasswordFields.setVisibility(View.GONE);
mBinding.signedInButtons.setVisibility(View.VISIBLE);
if (user.isEmailVerified()) {
mBinding.verifyEmailButton.setVisibility(View.GONE);
} else {
mBinding.verifyEmailButton.setVisibility(View.VISIBLE);
}
} else {
mBinding.status.setText(R.string.signed_out);
mBinding.detail.setText(null);
mBinding.emailPasswordButtons.setVisibility(View.VISIBLE);
mBinding.emailPasswordFields.setVisibility(View.VISIBLE);
mBinding.signedInButtons.setVisibility(View.GONE);
}
}
private void checkForMultiFactorFailure(Exception e) {
// Multi-factor authentication with SMS is currently only available for
// Google Cloud Identity Platform projects. For more information:
// https://cloud.google.com/identity-platform/docs/android/mfa
if (e instanceof FirebaseAuthMultiFactorException) {
Log.w(TAG, "multiFactorFailure", e);
MultiFactorResolver resolver = ((FirebaseAuthMultiFactorException) e).getResolver();
Bundle args = new Bundle();
args.putParcelable(MultiFactorSignInFragment.EXTRA_MFA_RESOLVER, resolver);
NavHostFragment.findNavController(this)
.navigate(R.id.action_emailpassword_to_mfasignin, args);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/FacebookLoginFragment.java
================================================
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.quickstart.auth.java;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.AccessToken;
import com.facebook.CallbackManager;
import com.facebook.FacebookCallback;
import com.facebook.FacebookException;
import com.facebook.login.LoginManager;
import com.facebook.login.LoginResult;
import com.facebook.login.widget.LoginButton;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthCredential;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FacebookAuthProvider;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.quickstart.auth.R;
import com.google.firebase.quickstart.auth.databinding.FragmentFacebookBinding;
/**
* Demonstrate Firebase Authentication using a Facebook access token.
*/
public class FacebookLoginFragment extends BaseFragment {
private static final String TAG = "FacebookLogin";
private FragmentFacebookBinding mBinding;
private FirebaseAuth mAuth;
private CallbackManager mCallbackManager;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentFacebookBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setProgressBar(mBinding.progressBar);
// Views
mBinding.buttonFacebookSignout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signOut();
}
});
// Initialize Firebase Auth
mAuth = FirebaseAuth.getInstance();
// Initialize Facebook Login button
mCallbackManager = CallbackManager.Factory.create();
LoginButton loginButton = mBinding.buttonFacebookLogin;
loginButton.setPermissions("email", "public_profile");
loginButton.registerCallback(mCallbackManager, new FacebookCallback() {
@Override
public void onSuccess(LoginResult loginResult) {
Log.d(TAG, "facebook:onSuccess:" + loginResult);
handleFacebookAccessToken(loginResult.getAccessToken());
}
@Override
public void onCancel() {
Log.d(TAG, "facebook:onCancel");
updateUI(null);
}
@Override
public void onError(FacebookException error) {
Log.d(TAG, "facebook:onError", error);
updateUI(null);
}
});
}
@Override
public void onStart() {
super.onStart();
// Check if user is signed in (non-null) and update UI accordingly.
FirebaseUser currentUser = mAuth.getCurrentUser();
updateUI(currentUser);
}
private void handleFacebookAccessToken(AccessToken token) {
Log.d(TAG, "handleFacebookAccessToken:" + token);
showProgressBar();
AuthCredential credential = FacebookAuthProvider.getCredential(token.getToken());
mAuth.signInWithCredential(credential)
.addOnCompleteListener(requireActivity(), new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithCredential:success");
FirebaseUser user = mAuth.getCurrentUser();
updateUI(user);
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCredential:failure", task.getException());
Toast.makeText(getContext(), "Authentication failed.",
Toast.LENGTH_SHORT).show();
updateUI(null);
}
hideProgressBar();
}
});
}
public void signOut() {
mAuth.signOut();
LoginManager.getInstance().logOut();
updateUI(null);
}
private void updateUI(FirebaseUser user) {
hideProgressBar();
if (user != null) {
mBinding.status.setText(getString(R.string.facebook_status_fmt, user.getDisplayName()));
mBinding.detail.setText(getString(R.string.firebase_status_fmt, user.getUid()));
mBinding.buttonFacebookLogin.setVisibility(View.GONE);
mBinding.buttonFacebookSignout.setVisibility(View.VISIBLE);
} else {
mBinding.status.setText(R.string.signed_out);
mBinding.detail.setText(null);
mBinding.buttonFacebookLogin.setVisibility(View.VISIBLE);
mBinding.buttonFacebookSignout.setVisibility(View.GONE);
}
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/FirebaseUIFragment.java
================================================
package com.google.firebase.quickstart.auth.java;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.firebase.ui.auth.AuthUI;
import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract;
import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.quickstart.auth.BuildConfig;
import com.google.firebase.quickstart.auth.R;
import com.google.firebase.quickstart.auth.databinding.FragmentFirebaseUiBinding;
import java.util.Collections;
/**
* Demonstrate authentication using the FirebaseUI-Android library. This fragment demonstrates
* using FirebaseUI for basic email/password sign in.
*
* For more information, visit https://github.com/firebase/firebaseui-android
*/
public class FirebaseUIFragment extends Fragment {
private FirebaseAuth mAuth;
private FragmentFirebaseUiBinding mBinding;
// Build FirebaseUI sign in intent. For documentation on this operation and all
// possible customization see: https://github.com/firebase/firebaseui-android
private final ActivityResultLauncher signInLauncher = registerForActivityResult(
new FirebaseAuthUIActivityResultContract(),
this::onSignInResult
);
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentFirebaseUiBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Initialize Firebase Auth
mAuth = FirebaseAuth.getInstance();
mBinding.signInButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startSignIn();
}
});
mBinding.signOutButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signOut();
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
@Override
public void onStart() {
super.onStart();
updateUI(mAuth.getCurrentUser());
}
private void onSignInResult(FirebaseAuthUIAuthenticationResult result) {
if (result.getResultCode() == Activity.RESULT_OK) {
// Sign in succeeded
updateUI(mAuth.getCurrentUser());
} else {
// Sign in failed
Toast.makeText(getContext(), "Sign In Failed", Toast.LENGTH_SHORT).show();
updateUI(null);
}
}
private void startSignIn() {
Intent intent = AuthUI.getInstance().createSignInIntentBuilder()
.setCredentialManagerEnabled(!BuildConfig.DEBUG)
.setAvailableProviders(Collections.singletonList(
new AuthUI.IdpConfig.EmailBuilder().build()))
.setLogo(R.mipmap.ic_launcher)
.build();
signInLauncher.launch(intent);
}
private void updateUI(FirebaseUser user) {
if (user != null) {
// Signed in
mBinding.status.setText(getString(R.string.firebaseui_status_fmt, user.getEmail()));
mBinding.detail.setText(getString(R.string.id_fmt, user.getUid()));
mBinding.signInButton.setVisibility(View.GONE);
mBinding.signOutButton.setVisibility(View.VISIBLE);
} else {
// Signed out
mBinding.status.setText(R.string.signed_out);
mBinding.detail.setText(null);
mBinding.signInButton.setVisibility(View.VISIBLE);
mBinding.signOutButton.setVisibility(View.GONE);
}
}
private void signOut() {
AuthUI.getInstance().signOut(getContext());
updateUI(null);
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/GenericIdpFragment.java
================================================
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.quickstart.auth.java;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.OAuthProvider;
import com.google.firebase.quickstart.auth.R;
import com.google.firebase.quickstart.auth.databinding.FragmentGenericIdpBinding;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Demonstrate Firebase Authentication using a Generic Identity Provider (IDP).
*/
@SuppressWarnings("Convert2Lambda")
public class GenericIdpFragment extends BaseFragment {
private static final String TAG = "GenericIdp";
private static final Map PROVIDER_MAP = new HashMap() {
{
put("Apple", "apple.com");
put("Microsoft", "microsoft.com");
put("Yahoo", "yahoo.com");
put("Twitter", "twitter.com");
}
};
private FragmentGenericIdpBinding mBinding;
private ArrayAdapter mSpinnerAdapter;
private FirebaseAuth mAuth;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentGenericIdpBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Initialize Firebase Auth
mAuth = FirebaseAuth.getInstance();
// Set up button click listeners
mBinding.genericSignInButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signIn();
}
});
mBinding.signOutButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mAuth.signOut();
updateUI(null);
}
});
// Spinner
List providers = new ArrayList<>(PROVIDER_MAP.keySet());
mSpinnerAdapter = new ArrayAdapter<>(requireContext(), R.layout.item_spinner_list, providers);
mBinding.providerSpinner.setAdapter(mSpinnerAdapter);
mBinding.providerSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView> parent, View view, int position, long id) {
mBinding.genericSignInButton.setText(getString(R.string.generic_signin_fmt, mSpinnerAdapter.getItem(position)));
}
@Override
public void onNothingSelected(AdapterView> parent) {}
});
mBinding.providerSpinner.setSelection(0);
}
@Override
public void onStart() {
super.onStart();
// Check if user is signed in (non-null) and update UI accordingly.
FirebaseUser currentUser = mAuth.getCurrentUser();
updateUI(currentUser);
// Look for a pending auth result
Task pending = mAuth.getPendingAuthResult();
if (pending != null) {
pending.addOnSuccessListener(new OnSuccessListener() {
@Override
public void onSuccess(AuthResult authResult) {
Log.d(TAG, "checkPending:onSuccess:" + authResult);
updateUI(authResult.getUser());
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.w(TAG, "checkPending:onFailure", e);
}
});
} else {
Log.d(TAG, "checkPending: null");
}
}
private void signIn() {
// Could add custom scopes here
ArrayList scopes = new ArrayList<>();
// Examples of provider ID: apple.com (Apple), microsoft.com (Microsoft), yahoo.com (Yahoo)
String providerId = getProviderId();
mAuth.startActivityForSignInWithProvider(requireActivity(),
OAuthProvider.newBuilder(providerId, mAuth)
.setScopes(scopes)
.build())
.addOnSuccessListener(
new OnSuccessListener() {
@Override
public void onSuccess(AuthResult authResult) {
Log.d(TAG, "activitySignIn:onSuccess:" + authResult.getUser());
updateUI(authResult.getUser());
}
})
.addOnFailureListener(
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.w(TAG, "activitySignIn:onFailure", e);
showToast(getString(R.string.error_sign_in_failed));
}
});
}
private String getProviderId() {
String providerName = mSpinnerAdapter.getItem(mBinding.providerSpinner.getSelectedItemPosition());
return PROVIDER_MAP.get(providerName);
}
private void updateUI(FirebaseUser user) {
hideProgressBar();
if (user != null) {
mBinding.status.setText(getString(R.string.generic_status_fmt, user.getDisplayName(), user.getEmail()));
mBinding.detail.setText(getString(R.string.firebase_status_fmt, user.getUid()));
mBinding.spinnerLayout.setVisibility(View.GONE);
mBinding.genericSignInButton.setVisibility(View.GONE);
mBinding.signOutButton.setVisibility(View.VISIBLE);
} else {
mBinding.status.setText(R.string.signed_out);
mBinding.detail.setText(null);
mBinding.spinnerLayout.setVisibility(View.VISIBLE);
mBinding.genericSignInButton.setVisibility(View.VISIBLE);
mBinding.signOutButton.setVisibility(View.GONE);
}
}
private void showToast(String message) {
Toast.makeText(getContext(), message, Toast.LENGTH_SHORT).show();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/GoogleSignInFragment.java
================================================
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.quickstart.auth.java;
import static com.google.android.libraries.identity.googleid.GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.credentials.ClearCredentialStateRequest;
import androidx.credentials.Credential;
import androidx.credentials.CredentialManager;
import androidx.credentials.CredentialManagerCallback;
import androidx.credentials.CustomCredential;
import androidx.credentials.GetCredentialRequest;
import androidx.credentials.GetCredentialResponse;
import androidx.credentials.exceptions.ClearCredentialException;
import androidx.credentials.exceptions.GetCredentialException;
import com.google.android.libraries.identity.googleid.GetGoogleIdOption;
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption;
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential;
import com.google.android.material.snackbar.Snackbar;
import com.google.firebase.auth.AuthCredential;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.GoogleAuthProvider;
import com.google.firebase.quickstart.auth.R;
import com.google.firebase.quickstart.auth.databinding.FragmentGoogleBinding;
import java.util.concurrent.Executors;
/**
* Demonstrate Firebase Authentication using a Google ID Token.
*/
public class GoogleSignInFragment extends BaseFragment {
private static final String TAG = "GoogleFragment";
private FirebaseAuth mAuth;
private CredentialManager credentialManager;
private FragmentGoogleBinding mBinding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentGoogleBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setProgressBar(mBinding.progressBar);
// Initialize Credential Manager
credentialManager = CredentialManager.create(requireContext());
// Initialize Firebase Auth
mAuth = FirebaseAuth.getInstance();
// Button listeners
mBinding.signInButton.setOnClickListener(v -> signIn());
mBinding.signOutButton.setOnClickListener(v -> signOut());
// Display Credential Manager Bottom Sheet if user isn't logged in
if (mAuth.getCurrentUser() == null) {
showBottomSheet();
}
}
@Override
public void onStart() {
super.onStart();
// Check if user is signed in (non-null) and update UI accordingly.
FirebaseUser currentUser = mAuth.getCurrentUser();
updateUI(currentUser);
}
private void signIn() {
// Create the dialog configuration for the Credential Manager request
GetSignInWithGoogleOption signInWithGoogleOption = new GetSignInWithGoogleOption
.Builder(requireContext().getString(R.string.default_web_client_id))
.build();
// Create the Credential Manager request using the configuration created above
GetCredentialRequest request = new GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build();
launchCredentialManager(request);
}
private void showBottomSheet() {
// Create the bottom sheet configuration for the Credential Manager request
GetGoogleIdOption googleIdOption = new GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(requireContext().getString(R.string.default_web_client_id))
.build();
// Create the Credential Manager request using the configuration created above
GetCredentialRequest request = new GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build();
launchCredentialManager(request);
}
private void launchCredentialManager(GetCredentialRequest request) {
credentialManager.getCredentialAsync(
requireContext(),
request,
new CancellationSignal(),
Executors.newSingleThreadExecutor(),
new CredentialManagerCallback<>() {
@Override
public void onResult(GetCredentialResponse result) {
// Extract credential from the result returned by Credential Manager
createGoogleIdToken(result.getCredential());
}
@Override
public void onError(GetCredentialException e) {
Log.e(TAG, "Couldn't retrieve user's credentials: " + e.getLocalizedMessage());
}
}
);
}
private void createGoogleIdToken(Credential credential) {
// Update UI to show progress bar while response is being processed
requireActivity().runOnUiThread(this::showProgressBar);
// Check if credential is of type Google ID
if (credential instanceof CustomCredential customCredential
&& credential.getType().equals(TYPE_GOOGLE_ID_TOKEN_CREDENTIAL)) {
// Create Google ID Token
Bundle credentialData = customCredential.getData();
GoogleIdTokenCredential googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credentialData);
// Sign in to Firebase with using the token
firebaseAuthWithGoogle(googleIdTokenCredential.getIdToken());
} else {
Log.w(TAG, "Credential is not of type Google ID!");
}
}
private void firebaseAuthWithGoogle(String idToken) {
AuthCredential credential = GoogleAuthProvider.getCredential(idToken, null);
mAuth.signInWithCredential(credential)
.addOnCompleteListener(requireActivity(), task -> {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithCredential:success");
FirebaseUser user = mAuth.getCurrentUser();
updateUI(user);
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCredential:failure", task.getException());
Snackbar.make(mBinding.mainLayout, "Authentication Failed.", Snackbar.LENGTH_SHORT).show();
updateUI(null);
}
hideProgressBar();
});
}
private void signOut() {
// Firebase sign out
mAuth.signOut();
// When a user signs out, clear the current user credential state from all credential providers.
// This will notify all providers that any stored credential session for the given app should be cleared.
ClearCredentialStateRequest clearRequest = new ClearCredentialStateRequest();
credentialManager.clearCredentialStateAsync(
clearRequest,
new CancellationSignal(),
Executors.newSingleThreadExecutor(),
new CredentialManagerCallback<>() {
@Override
public void onResult(@NonNull Void result) {
updateUI(null);
}
@Override
public void onError(@NonNull ClearCredentialException e) {
Log.e(TAG, "Couldn't clear user credentials: " + e.getLocalizedMessage());
}
});
}
private void updateUI(FirebaseUser user) {
requireActivity().runOnUiThread(() -> {
hideProgressBar();
if (user != null) {
mBinding.status.setText(getString(R.string.google_status_fmt, user.getEmail()));
mBinding.detail.setText(getString(R.string.firebase_status_fmt, user.getUid()));
mBinding.signInButton.setVisibility(View.GONE);
mBinding.signOutButton.setVisibility(View.VISIBLE);
} else {
mBinding.status.setText(R.string.signed_out);
mBinding.detail.setText(null);
mBinding.signInButton.setVisibility(View.VISIBLE);
mBinding.signOutButton.setVisibility(View.GONE);
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/MainActivity.java
================================================
package com.google.firebase.quickstart.auth.java;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.Navigation;
import com.google.firebase.quickstart.auth.R;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Navigation.findNavController(this, R.id.nav_host_fragment)
.setGraph(R.navigation.nav_graph_java);
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/MultiFactorEnrollFragment.java
================================================
package com.google.firebase.quickstart.auth.java;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.FirebaseException;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.MultiFactorSession;
import com.google.firebase.auth.PhoneAuthCredential;
import com.google.firebase.auth.PhoneAuthOptions;
import com.google.firebase.auth.PhoneAuthProvider;
import com.google.firebase.auth.PhoneAuthProvider.OnVerificationStateChangedCallbacks;
import com.google.firebase.auth.PhoneMultiFactorGenerator;
import com.google.firebase.quickstart.auth.databinding.FragmentPhoneAuthBinding;
import java.util.concurrent.TimeUnit;
/**
* Fragment that allows the user to enroll second factors.
*/
public class MultiFactorEnrollFragment extends BaseFragment {
private static final String TAG = "MfaEnrollFragment";
private FragmentPhoneAuthBinding mBinding;
private String mCodeVerificationId;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentPhoneAuthBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mBinding.titleText.setText("SMS as a Second Factor");
mBinding.status.setVisibility(View.GONE);
mBinding.detail.setVisibility(View.GONE);
mBinding.buttonStartVerification.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onClickVerifyPhoneNumber();
}
});
mBinding.buttonVerifyPhone.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onClickSignInWithPhoneNumber();
}
});
}
private void onClickVerifyPhoneNumber() {
String phoneNumber = mBinding.fieldPhoneNumber.getText().toString();
OnVerificationStateChangedCallbacks callbacks =
new OnVerificationStateChangedCallbacks() {
@Override
public void onVerificationCompleted(PhoneAuthCredential credential) {
// Instant-validation has been disabled (see requireSmsValidation below).
// Auto-retrieval has also been disabled (timeout is set to 0).
// This should never be triggered.
throw new RuntimeException(
"onVerificationCompleted() triggered with instant-validation and auto-retrieval disabled.");
}
@Override
public void onCodeSent(
final String verificationId, PhoneAuthProvider.ForceResendingToken token) {
Log.d(TAG, "onCodeSent:" + verificationId);
Toast.makeText( getContext(), "SMS code has been sent", Toast.LENGTH_SHORT)
.show();
mCodeVerificationId = verificationId;
}
@Override
public void onVerificationFailed(FirebaseException e) {
Log.w(TAG, "onVerificationFailed ", e);
Toast.makeText(getContext(), "Verification failed: " + e.getMessage(), Toast.LENGTH_SHORT)
.show();
}
};
FirebaseAuth.getInstance()
.getCurrentUser()
.getMultiFactor()
.getSession()
.addOnCompleteListener(
new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
PhoneAuthOptions phoneAuthOptions =
PhoneAuthOptions.newBuilder()
.setActivity(requireActivity())
.setPhoneNumber(phoneNumber)
// A timeout of 0 disables SMS-auto-retrieval.
.setTimeout(0L, TimeUnit.SECONDS)
.setMultiFactorSession(task.getResult())
.setCallbacks(callbacks)
// Disable instant-validation.
.requireSmsValidation(true)
.build();
PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
} else {
Toast.makeText(getContext(),
"Failed to get session: " + task.getException(), Toast.LENGTH_SHORT)
.show();
}
}
});
}
private void onClickSignInWithPhoneNumber() {
String smsCode = mBinding.fieldVerificationCode.getText().toString();
if (TextUtils.isEmpty(smsCode)) {
return;
}
PhoneAuthCredential credential = PhoneAuthProvider.getCredential(mCodeVerificationId, smsCode);
enrollWithPhoneAuthCredential(credential);
}
private void enrollWithPhoneAuthCredential(PhoneAuthCredential credential) {
FirebaseAuth.getInstance()
.getCurrentUser()
.getMultiFactor()
.enroll(PhoneMultiFactorGenerator.getAssertion(credential), /* displayName= */ null)
.addOnSuccessListener(new OnSuccessListener() {
@Override
public void onSuccess(Void aVoid) {
Toast.makeText(getContext(), "MFA enrollment was successful",
Toast.LENGTH_LONG)
.show();
NavHostFragment.findNavController(MultiFactorEnrollFragment.this)
.popBackStack();
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.d(TAG, "MFA failure", e);
Toast.makeText(getContext(),
"MFA enrollment was unsuccessful. " + e,
Toast.LENGTH_LONG)
.show();
}
});
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/MultiFactorFragment.java
================================================
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.quickstart.auth.java;
import android.app.AlertDialog;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.MultiFactorInfo;
import com.google.firebase.auth.PhoneMultiFactorInfo;
import com.google.firebase.quickstart.auth.R;
import com.google.firebase.quickstart.auth.databinding.FragmentMultiFactorBinding;
import java.util.List;
public class MultiFactorFragment extends BaseFragment {
private static final String TAG = "MultiFactor";
private FragmentMultiFactorBinding mBinding;
private FirebaseAuth mAuth;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentMultiFactorBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setProgressBar(mBinding.progressBar);
// Buttons
mBinding.emailSignInButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NavHostFragment.findNavController(MultiFactorFragment.this)
.navigate(R.id.action_mfa_to_emailpassword);
}
});
mBinding.signOutButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signOut();
}
});
mBinding.verifyEmailButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendEmailVerification();
}
});
mBinding.enrollMfa.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NavHostFragment.findNavController(MultiFactorFragment.this)
.navigate(R.id.action_mfa_to_enroll);
}
});
mBinding.unenrollMfa.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NavHostFragment.findNavController(MultiFactorFragment.this)
.navigate(R.id.action_mfa_to_unenroll);
}
});
mBinding.reloadButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
reload();
}
});
// Initialize Firebase Auth
mAuth = FirebaseAuth.getInstance();
showDisclaimer();
}
@Override
public void onStart() {
super.onStart();
// Check if user is signed in (non-null) and update UI accordingly.
FirebaseUser currentUser = mAuth.getCurrentUser();
updateUI(currentUser);
}
private void signOut() {
mAuth.signOut();
updateUI(null);
}
private void sendEmailVerification() {
// Disable button
mBinding.verifyEmailButton.setEnabled(false);
// Send verification email
final FirebaseUser user = mAuth.getCurrentUser();
user.sendEmailVerification()
.addOnCompleteListener(requireActivity(), new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
// Re-enable button
mBinding.verifyEmailButton.setEnabled(true);
if (task.isSuccessful()) {
Toast.makeText(getContext(),
"Verification email sent to " + user.getEmail(),
Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "sendEmailVerification", task.getException());
Toast.makeText(getContext(),
"Failed to send verification email.",
Toast.LENGTH_SHORT).show();
}
}
});
}
private void reload() {
mAuth.getCurrentUser().reload().addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
updateUI(mAuth.getCurrentUser());
Toast.makeText(getContext(),
"Reload successful!",
Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "reload", task.getException());
Toast.makeText(getContext(),
"Failed to reload user.",
Toast.LENGTH_SHORT).show();
}
}
});
}
private void updateUI(FirebaseUser user) {
hideProgressBar();
if (user != null) {
mBinding.status.setText(getString(R.string.emailpassword_status_fmt,
user.getEmail(), user.isEmailVerified()));
mBinding.detail.setText(getString(R.string.firebase_status_fmt, user.getUid()));
List secondFactors = user.getMultiFactor().getEnrolledFactors();
if (secondFactors.isEmpty()) {
mBinding.unenrollMfa.setVisibility(View.GONE);
} else {
mBinding.unenrollMfa.setVisibility(View.VISIBLE);
StringBuilder sb = new StringBuilder("Second Factors: ");
String delimiter = ", ";
for (MultiFactorInfo x : secondFactors) {
sb.append(((PhoneMultiFactorInfo) x).getPhoneNumber() + delimiter);
}
sb.setLength(sb.length() - delimiter.length());
mBinding.mfaInfo.setText(sb.toString());
}
mBinding.emailSignInButton.setVisibility(View.GONE);
mBinding.signedInButtons.setVisibility(View.VISIBLE);
int reloadVisibility = secondFactors.isEmpty() ? View.VISIBLE : View.GONE;
mBinding.reloadButton.setVisibility(reloadVisibility);
if (user.isEmailVerified()) {
mBinding.verifyEmailButton.setVisibility(View.GONE);
mBinding.enrollMfa.setVisibility(View.VISIBLE);
} else {
mBinding.verifyEmailButton.setVisibility(View.VISIBLE);
mBinding.enrollMfa.setVisibility(View.GONE);
}
} else {
mBinding.status.setText(R.string.multi_factor_signed_out);
mBinding.detail.setText(null);
mBinding.mfaInfo.setText(null);
mBinding.emailSignInButton.setVisibility(View.VISIBLE);
mBinding.signedInButtons.setVisibility(View.GONE);
}
}
private void showDisclaimer() {
new AlertDialog.Builder(getContext())
.setTitle("Warning")
.setMessage("Multi-factor authentication with SMS is currently only available for " +
"Google Cloud Identity Platform projects. For more information see: " +
"https://cloud.google.com/identity-platform/docs/android/mfa")
.setPositiveButton("OK", null)
.show();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/MultiFactorSignInFragment.java
================================================
package com.google.firebase.quickstart.auth.java;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.firebase.FirebaseException;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.MultiFactorInfo;
import com.google.firebase.auth.MultiFactorResolver;
import com.google.firebase.auth.PhoneAuthCredential;
import com.google.firebase.auth.PhoneAuthOptions;
import com.google.firebase.auth.PhoneAuthProvider;
import com.google.firebase.auth.PhoneMultiFactorGenerator;
import com.google.firebase.auth.PhoneMultiFactorInfo;
import com.google.firebase.quickstart.auth.R;
import com.google.firebase.quickstart.auth.databinding.FragmentMultiFactorSignInBinding;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static android.text.TextUtils.isEmpty;
/**
* Fragment that handles MFA sign-in
*/
public class MultiFactorSignInFragment extends BaseFragment {
private static final String KEY_VERIFICATION_ID = "key_verification_id";
public static final String EXTRA_MFA_RESOLVER = "EXTRA_MFA_RESOLVER";
private FragmentMultiFactorSignInBinding mBinding;
private MultiFactorResolver mMultiFactorResolver;
private PhoneAuthCredential mPhoneAuthCredential;
private String mVerificationId;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentMultiFactorSignInBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null) {
onViewStateRestored(savedInstanceState);
}
List phoneFactorButtonList = new ArrayList<>();
phoneFactorButtonList.add(mBinding.phoneFactor1);
phoneFactorButtonList.add(mBinding.phoneFactor2);
phoneFactorButtonList.add(mBinding.phoneFactor3);
phoneFactorButtonList.add(mBinding.phoneFactor4);
phoneFactorButtonList.add(mBinding.phoneFactor5);
for (Button button : phoneFactorButtonList) {
button.setVisibility(View.GONE);
}
mBinding.finishMfaSignIn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onClickFinishSignIn();
}
});
mMultiFactorResolver = getResolverFromArguments(requireArguments());
List multiFactorInfoList = mMultiFactorResolver.getHints();
for (int i = 0; i < multiFactorInfoList.size(); ++i) {
PhoneMultiFactorInfo phoneMultiFactorInfo = (PhoneMultiFactorInfo) multiFactorInfoList.get(i);
Button button = phoneFactorButtonList.get(i);
button.setVisibility(View.VISIBLE);
button.setText(phoneMultiFactorInfo.getPhoneNumber());
button.setClickable(true);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PhoneAuthProvider.verifyPhoneNumber(
PhoneAuthOptions.newBuilder()
.setActivity(requireActivity())
.setMultiFactorSession(mMultiFactorResolver.getSession())
.setMultiFactorHint(phoneMultiFactorInfo)
.setCallbacks(generateCallbacks())
// A timeout of 0 disables SMS-auto-retrieval.
.setTimeout(0L, TimeUnit.SECONDS)
.build());
}
});
}
}
@Override
public void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
bundle.putString(KEY_VERIFICATION_ID, mVerificationId);
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
mVerificationId = savedInstanceState.getString(KEY_VERIFICATION_ID);
}
}
private PhoneAuthProvider.OnVerificationStateChangedCallbacks generateCallbacks() {
return new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
@Override
public void onVerificationCompleted(@NonNull PhoneAuthCredential phoneAuthCredential) {
MultiFactorSignInFragment.this.mPhoneAuthCredential = phoneAuthCredential;
mBinding.finishMfaSignIn.performClick();
Toast.makeText(getContext(), "Verification complete!", Toast.LENGTH_SHORT)
.show();
}
@Override
public void onCodeSent(@NonNull String verificationId, @NonNull PhoneAuthProvider.ForceResendingToken token) {
MultiFactorSignInFragment.this.mVerificationId = verificationId;
mBinding.finishMfaSignIn.setClickable(true);
}
@Override
public void onVerificationFailed(@NonNull FirebaseException e) {
Toast.makeText(getContext(), "Error: " + e.getMessage(), Toast.LENGTH_SHORT)
.show();
}
};
}
private MultiFactorResolver getResolverFromArguments(Bundle arguments) {
return arguments.getParcelable(EXTRA_MFA_RESOLVER);
}
private void onClickFinishSignIn() {
if (mPhoneAuthCredential == null) {
if (isEmpty(mBinding.smsCode.getText().toString())) {
Toast.makeText(getContext(), "You need to enter an SMS code.", Toast.LENGTH_SHORT)
.show();
return;
}
mPhoneAuthCredential =
PhoneAuthProvider.getCredential(
mVerificationId, mBinding.smsCode.getText().toString());
}
mMultiFactorResolver
.resolveSignIn(PhoneMultiFactorGenerator.getAssertion(mPhoneAuthCredential))
.addOnSuccessListener(
new OnSuccessListener() {
@Override
public void onSuccess(AuthResult authResult) {
NavHostFragment.findNavController(MultiFactorSignInFragment.this)
.navigate(R.id.action_mfasignin_to_mfa);
}
})
.addOnFailureListener(
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Toast.makeText(getContext(), "Error: " + e.getMessage(), Toast.LENGTH_SHORT)
.show();
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/MultiFactorUnenrollFragment.java
================================================
package com.google.firebase.quickstart.auth.java;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.MultiFactorInfo;
import com.google.firebase.auth.PhoneMultiFactorInfo;
import com.google.firebase.quickstart.auth.databinding.FragmentMultiFactorSignInBinding;
import java.util.ArrayList;
import java.util.List;
public class MultiFactorUnenrollFragment extends BaseFragment {
private FragmentMultiFactorSignInBinding mBinding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentMultiFactorSignInBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mBinding.smsCode.setVisibility(View.GONE);
mBinding.finishMfaSignIn.setVisibility(View.GONE);
List phoneFactorButtonList = new ArrayList<>();
phoneFactorButtonList.add(mBinding.phoneFactor1);
phoneFactorButtonList.add(mBinding.phoneFactor2);
phoneFactorButtonList.add(mBinding.phoneFactor3);
phoneFactorButtonList.add(mBinding.phoneFactor4);
phoneFactorButtonList.add(mBinding.phoneFactor5);
for (Button button : phoneFactorButtonList) {
button.setVisibility(View.GONE);
}
List multiFactorInfoList =
FirebaseAuth.getInstance().getCurrentUser().getMultiFactor().getEnrolledFactors();
for (int i = 0; i < multiFactorInfoList.size(); ++i) {
PhoneMultiFactorInfo phoneMultiFactorInfo = (PhoneMultiFactorInfo) multiFactorInfoList.get(i);
Button button = phoneFactorButtonList.get(i);
button.setVisibility(View.VISIBLE);
button.setText(phoneMultiFactorInfo.getPhoneNumber());
button.setClickable(true);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FirebaseAuth.getInstance()
.getCurrentUser()
.getMultiFactor()
.unenroll(phoneMultiFactorInfo)
.addOnCompleteListener(
new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
Toast.makeText(getContext(),
"Successfully unenrolled!", Toast.LENGTH_SHORT).show();
NavHostFragment.findNavController(MultiFactorUnenrollFragment.this)
.popBackStack();
} else {
Toast.makeText(getContext(),
"Unable to unenroll second factor. " + task.getException(), Toast.LENGTH_SHORT).show();
}
}
});
}
});
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/PasswordlessActivity.java
================================================
package com.google.firebase.quickstart.auth.java;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.material.snackbar.Snackbar;
import com.google.firebase.auth.ActionCodeSettings;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthActionCodeException;
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.quickstart.auth.R;
import com.google.firebase.quickstart.auth.databinding.ActivityPasswordlessBinding;
/**
* Demonstrate Firebase Authentication without a password, using a link sent to an
* email address.
*/
public class PasswordlessActivity extends BaseActivity implements View.OnClickListener {
private static final String TAG = "PasswordlessSignIn";
private static final String KEY_PENDING_EMAIL = "key_pending_email";
private FirebaseAuth mAuth;
private ActivityPasswordlessBinding mBinding;
private String mPendingEmail;
private String mEmailLink;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = ActivityPasswordlessBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
setProgressBar(mBinding.progressBar);
// Initialize Firebase Auth
mAuth = FirebaseAuth.getInstance();
mBinding.passwordlessSendEmailButton.setOnClickListener(this);
mBinding.passwordlessSignInButton.setOnClickListener(this);
mBinding.signOutButton.setOnClickListener(this);
// Restore the "pending" email address
if (savedInstanceState != null) {
mPendingEmail = savedInstanceState.getString(KEY_PENDING_EMAIL, null);
mBinding.fieldEmail.setText(mPendingEmail);
}
// Check if the Intent that started the Activity contains an email sign-in link.
checkIntent(getIntent());
}
@Override
protected void onStart() {
super.onStart();
updateUI(mAuth.getCurrentUser());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
checkIntent(intent);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(KEY_PENDING_EMAIL, mPendingEmail);
}
/**
* Check to see if the Intent has an email link, and if so set up the UI accordingly.
* This can be called from either onCreate or onNewIntent, depending on how the Activity
* was launched.
*/
private void checkIntent(@Nullable Intent intent) {
if (intentHasEmailLink(intent)) {
mEmailLink = intent.getData().toString();
mBinding.status.setText(R.string.status_link_found);
mBinding.passwordlessSendEmailButton.setEnabled(false);
mBinding.passwordlessSignInButton.setEnabled(true);
} else {
mBinding.status.setText(R.string.status_email_not_sent);
mBinding.passwordlessSendEmailButton.setEnabled(true);
mBinding.passwordlessSignInButton.setEnabled(false);
}
}
/**
* Determine if the given Intent contains an email sign-in link.
*/
private boolean intentHasEmailLink(@Nullable Intent intent) {
if (intent != null && intent.getData() != null) {
String intentData = intent.getData().toString();
if (mAuth.isSignInWithEmailLink(intentData)) {
return true;
}
}
return false;
}
/**
* Send an email sign-in link to the specified email.
*/
private void sendSignInLink(String email) {
ActionCodeSettings settings = ActionCodeSettings.newBuilder()
.setAndroidPackageName(
getPackageName(),
false, /* install if not available? */
null /* minimum app version */)
.setHandleCodeInApp(true)
.setUrl("https://auth.example.com/emailSignInLink")
.build();
hideKeyboard(mBinding.fieldEmail);
showProgressBar();
mAuth.sendSignInLinkToEmail(email, settings)
.addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
hideProgressBar();
if (task.isSuccessful()) {
Log.d(TAG, "Link sent");
showSnackbar("Sign-in link sent!");
mPendingEmail = email;
mBinding.status.setText(R.string.status_email_sent);
} else {
Exception e = task.getException();
Log.w(TAG, "Could not send link", e);
showSnackbar("Failed to send link.");
if (e instanceof FirebaseAuthInvalidCredentialsException) {
mBinding.fieldEmail.setError("Invalid email address.");
}
}
}
});
}
/**
* Sign in using an email address and a link, the link is passed to the Activity
* from the dynamic link contained in the email.
*/
private void signInWithEmailLink(String email, String link) {
Log.d(TAG, "signInWithLink:" + link);
hideKeyboard(mBinding.fieldEmail);
showProgressBar();
mAuth.signInWithEmailLink(email, link)
.addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
hideProgressBar();
mPendingEmail = null;
if (task.isSuccessful()) {
Log.d(TAG, "signInWithEmailLink:success");
mBinding.fieldEmail.setText(null);
updateUI(task.getResult().getUser());
} else {
Log.w(TAG, "signInWithEmailLink:failure", task.getException());
updateUI(null);
if (task.getException() instanceof FirebaseAuthActionCodeException) {
showSnackbar("Invalid or expired sign-in link.");
}
}
}
});
}
private void onSendLinkClicked() {
String email = mBinding.fieldEmail.getText().toString();
if (TextUtils.isEmpty(email)) {
mBinding.fieldEmail.setError("Email must not be empty.");
return;
}
sendSignInLink(email);
}
private void onSignInClicked() {
String email = mBinding.fieldEmail.getText().toString();
if (TextUtils.isEmpty(email)) {
mBinding.fieldEmail.setError("Email must not be empty.");
return;
}
signInWithEmailLink(email, mEmailLink);
}
private void onSignOutClicked() {
mAuth.signOut();
updateUI(null);
mBinding.status.setText(R.string.status_email_not_sent);
}
private void updateUI(@Nullable FirebaseUser user) {
if (user != null) {
mBinding.status.setText(getString(R.string.passwordless_status_fmt,
user.getEmail(), user.isEmailVerified()));
mBinding.fieldEmail.setVisibility(View.GONE);
mBinding.passwordlessButtons.setVisibility(View.GONE);
mBinding.signOutButton.setVisibility(View.VISIBLE);
} else {
mBinding.fieldEmail.setVisibility(View.VISIBLE);
mBinding.passwordlessButtons.setVisibility(View.VISIBLE);
mBinding.signOutButton.setVisibility(View.GONE);
}
}
private void showSnackbar(String message) {
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_SHORT).show();
}
@Override
public void onClick(View view) {
//Due to bump in Java version, we can not use view ids in switch
//(see: http://tools.android.com/tips/non-constant-fields), so we
//need to use if/else:
int viewId = view.getId();
if (viewId == R.id.passwordlessSendEmailButton) {
onSendLinkClicked();
} else if (viewId == R.id.passwordlessSignInButton) {
onSignInClicked();
} else if (viewId == R.id.signOutButton) {
onSignOutClicked();
}
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/PhoneAuthFragment.java
================================================
package com.google.firebase.quickstart.auth.java;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.material.snackbar.Snackbar;
import com.google.firebase.FirebaseException;
import com.google.firebase.FirebaseTooManyRequestsException;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.auth.PhoneAuthCredential;
import com.google.firebase.auth.PhoneAuthOptions;
import com.google.firebase.auth.PhoneAuthProvider;
import com.google.firebase.quickstart.auth.R;
import com.google.firebase.quickstart.auth.databinding.FragmentPhoneAuthBinding;
import java.util.concurrent.TimeUnit;
public class PhoneAuthFragment extends Fragment {
private static final String TAG = "PhoneAuthFragment";
private static final String KEY_VERIFY_IN_PROGRESS = "key_verify_in_progress";
private static final int STATE_INITIALIZED = 1;
private static final int STATE_CODE_SENT = 2;
private static final int STATE_VERIFY_FAILED = 3;
private static final int STATE_VERIFY_SUCCESS = 4;
private static final int STATE_SIGNIN_FAILED = 5;
private static final int STATE_SIGNIN_SUCCESS = 6;
private FirebaseAuth mAuth;
private boolean mVerificationInProgress = false;
private String mVerificationId;
private PhoneAuthProvider.ForceResendingToken mResendToken;
private PhoneAuthProvider.OnVerificationStateChangedCallbacks mCallbacks;
private FragmentPhoneAuthBinding mBinding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentPhoneAuthBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Restore instance state
if (savedInstanceState != null) {
onViewStateRestored(savedInstanceState);
}
// Assign click listeners
mBinding.buttonStartVerification.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!validatePhoneNumber()) {
return;
}
startPhoneNumberVerification(mBinding.fieldPhoneNumber.getText().toString());
}
});
mBinding.buttonVerifyPhone.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String code = mBinding.fieldVerificationCode.getText().toString();
if (TextUtils.isEmpty(code)) {
mBinding.fieldVerificationCode.setError("Cannot be empty.");
return;
}
verifyPhoneNumberWithCode(mVerificationId, code);
}
});
mBinding.buttonResend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!validatePhoneNumber()) {
return;
}
resendVerificationCode(mBinding.fieldPhoneNumber.getText().toString(), mResendToken);
}
});
mBinding.signOutButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
signOut();
}
});
// Initialize Firebase Auth
mAuth = FirebaseAuth.getInstance();
// Initialize phone auth callbacks
mCallbacks = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
@Override
public void onVerificationCompleted(PhoneAuthCredential credential) {
// This callback will be invoked in two situations:
// 1 - Instant verification. In some cases the phone number can be instantly
// verified without needing to send or enter a verification code.
// 2 - Auto-retrieval. On some devices Google Play services can automatically
// detect the incoming verification SMS and perform verification without
// user action.
Log.d(TAG, "onVerificationCompleted:" + credential);
mVerificationInProgress = false;
// Update the UI and attempt sign in with the phone credential
updateUI(STATE_VERIFY_SUCCESS, credential);
signInWithPhoneAuthCredential(credential);
}
@Override
public void onVerificationFailed(FirebaseException e) {
// This callback is invoked in an invalid request for verification is made,
// for instance if the the phone number format is not valid.
Log.w(TAG, "onVerificationFailed", e);
mVerificationInProgress = false;
if (e instanceof FirebaseAuthInvalidCredentialsException) {
// Invalid request
mBinding.fieldPhoneNumber.setError("Invalid phone number.");
} else if (e instanceof FirebaseTooManyRequestsException) {
// The SMS quota for the project has been exceeded
Snackbar.make(view, "Quota exceeded.",
Snackbar.LENGTH_SHORT).show();
}
// Show a message and update the UI
updateUI(STATE_VERIFY_FAILED);
}
@Override
public void onCodeSent(@NonNull String verificationId,
@NonNull PhoneAuthProvider.ForceResendingToken token) {
// The SMS verification code has been sent to the provided phone number, we
// now need to ask the user to enter the code and then construct a credential
// by combining the code with a verification ID.
Log.d(TAG, "onCodeSent:" + verificationId);
// Save verification ID and resending token so we can use them later
mVerificationId = verificationId;
mResendToken = token;
// Update UI
updateUI(STATE_CODE_SENT);
}
};
}
@Override
public void onStart() {
super.onStart();
// Check if user is signed in (non-null) and update UI accordingly.
FirebaseUser currentUser = mAuth.getCurrentUser();
updateUI(currentUser);
if (mVerificationInProgress && validatePhoneNumber()) {
startPhoneNumberVerification(mBinding.fieldPhoneNumber.getText().toString());
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_VERIFY_IN_PROGRESS, mVerificationInProgress);
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (savedInstanceState != null) {
mVerificationInProgress = savedInstanceState.getBoolean(KEY_VERIFY_IN_PROGRESS);
}
}
private void startPhoneNumberVerification(String phoneNumber) {
PhoneAuthOptions options =
PhoneAuthOptions.newBuilder(mAuth)
.setPhoneNumber(phoneNumber) // Phone number to verify
.setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit
.setActivity(requireActivity()) // Activity (for callback binding)
.setCallbacks(mCallbacks) // OnVerificationStateChangedCallbacks
.build();
PhoneAuthProvider.verifyPhoneNumber(options);
mVerificationInProgress = true;
}
private void verifyPhoneNumberWithCode(String verificationId, String code) {
PhoneAuthCredential credential = PhoneAuthProvider.getCredential(verificationId, code);
signInWithPhoneAuthCredential(credential);
}
private void resendVerificationCode(String phoneNumber,
PhoneAuthProvider.ForceResendingToken token) {
PhoneAuthOptions options =
PhoneAuthOptions.newBuilder(mAuth)
.setPhoneNumber(phoneNumber) // Phone number to verify
.setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit
.setActivity(requireActivity()) // Activity (for callback binding)
.setCallbacks(mCallbacks) // OnVerificationStateChangedCallbacks
.setForceResendingToken(token) // ForceResendingToken from callbacks
.build();
PhoneAuthProvider.verifyPhoneNumber(options);
}
private void signInWithPhoneAuthCredential(PhoneAuthCredential credential) {
mAuth.signInWithCredential(credential)
.addOnCompleteListener(requireActivity(), new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithCredential:success");
FirebaseUser user = task.getResult().getUser();
updateUI(STATE_SIGNIN_SUCCESS, user);
} else {
// Sign in failed, display a message and update the UI
Log.w(TAG, "signInWithCredential:failure", task.getException());
if (task.getException() instanceof FirebaseAuthInvalidCredentialsException) {
// The verification code entered was invalid
mBinding.fieldVerificationCode.setError("Invalid code.");
}
// Update UI
updateUI(STATE_SIGNIN_FAILED);
}
}
});
}
private void signOut() {
mAuth.signOut();
updateUI(STATE_INITIALIZED);
}
private void updateUI(int uiState) {
updateUI(uiState, mAuth.getCurrentUser(), null);
}
private void updateUI(FirebaseUser user) {
if (user != null) {
updateUI(STATE_SIGNIN_SUCCESS, user);
} else {
updateUI(STATE_INITIALIZED);
}
}
private void updateUI(int uiState, FirebaseUser user) {
updateUI(uiState, user, null);
}
private void updateUI(int uiState, PhoneAuthCredential cred) {
updateUI(uiState, null, cred);
}
private void updateUI(int uiState, FirebaseUser user, PhoneAuthCredential cred) {
switch (uiState) {
case STATE_INITIALIZED:
// Initialized state, show only the phone number field and start button
enableViews(mBinding.buttonStartVerification, mBinding.fieldPhoneNumber);
disableViews(mBinding.buttonVerifyPhone, mBinding.buttonResend, mBinding.fieldVerificationCode);
mBinding.detail.setText(null);
break;
case STATE_CODE_SENT:
// Code sent state, show the verification field, the
enableViews(mBinding.buttonVerifyPhone, mBinding.buttonResend, mBinding.fieldPhoneNumber, mBinding.fieldVerificationCode);
disableViews(mBinding.buttonStartVerification);
mBinding.detail.setText(R.string.status_code_sent);
break;
case STATE_VERIFY_FAILED:
// Verification has failed, show all options
enableViews(mBinding.buttonStartVerification, mBinding.buttonVerifyPhone, mBinding.buttonResend, mBinding.fieldPhoneNumber,
mBinding.fieldVerificationCode);
mBinding.detail.setText(R.string.status_verification_failed);
break;
case STATE_VERIFY_SUCCESS:
// Verification has succeeded, proceed to firebase sign in
disableViews(mBinding.buttonStartVerification, mBinding.buttonVerifyPhone, mBinding.buttonResend, mBinding.fieldPhoneNumber,
mBinding.fieldVerificationCode);
mBinding.detail.setText(R.string.status_verification_succeeded);
// Set the verification text based on the credential
if (cred != null) {
if (cred.getSmsCode() != null) {
mBinding.fieldVerificationCode.setText(cred.getSmsCode());
} else {
mBinding.fieldVerificationCode.setText(R.string.instant_validation);
}
}
break;
case STATE_SIGNIN_FAILED:
// No-op, handled by sign-in check
mBinding.detail.setText(R.string.status_sign_in_failed);
break;
case STATE_SIGNIN_SUCCESS:
// Np-op, handled by sign-in check
break;
}
if (user == null) {
// Signed out
mBinding.phoneAuthFields.setVisibility(View.VISIBLE);
mBinding.signOutButton.setVisibility(View.GONE);
mBinding.status.setText(R.string.signed_out);
} else {
// Signed in
mBinding.phoneAuthFields.setVisibility(View.GONE);
mBinding.signOutButton.setVisibility(View.VISIBLE);
enableViews(mBinding.fieldPhoneNumber, mBinding.fieldVerificationCode);
mBinding.fieldPhoneNumber.setText(null);
mBinding.fieldVerificationCode.setText(null);
mBinding.status.setText(R.string.signed_in);
mBinding.detail.setText(getString(R.string.firebase_status_fmt, user.getUid()));
}
}
private boolean validatePhoneNumber() {
String phoneNumber = mBinding.fieldPhoneNumber.getText().toString();
if (TextUtils.isEmpty(phoneNumber)) {
mBinding.fieldPhoneNumber.setError("Invalid phone number.");
return false;
}
return true;
}
private void enableViews(View... views) {
for (View v : views) {
v.setEnabled(true);
}
}
private void disableViews(View... views) {
for (View v : views) {
v.setEnabled(false);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/java/TokenBroadcastReceiver.java
================================================
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.quickstart.auth.java;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.util.Log;
/**
* Receiver to capture tokens broadcast via ADB and insert them into the
* running application to facilitate easy testing of custom authentication.
*/
public abstract class TokenBroadcastReceiver extends BroadcastReceiver {
private static final String TAG = "TokenBroadcastReceiver";
public static final String ACTION_TOKEN = "com.google.example.ACTION_TOKEN";
public static final String EXTRA_KEY_TOKEN = "key_token";
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive:" + intent);
if (ACTION_TOKEN.equals(intent.getAction())) {
String token = intent.getExtras().getString(EXTRA_KEY_TOKEN);
onNewToken(token);
}
}
public static IntentFilter getFilter() {
IntentFilter filter = new IntentFilter(ACTION_TOKEN);
return filter;
}
public abstract void onNewToken(String token);
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/AnonymousAuthFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.google.firebase.auth.EmailAuthProvider
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentAnonymousAuthBinding
class AnonymousAuthFragment : BaseFragment() {
private var _binding: FragmentAnonymousAuthBinding? = null
private val binding: FragmentAnonymousAuthBinding
get() = _binding!!
private lateinit var auth: FirebaseAuth
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentAnonymousAuthBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setProgressBar(binding.progressBar)
// Initialize Firebase Auth
auth = Firebase.auth
// Click listeners
binding.buttonAnonymousSignIn.setOnClickListener {
signInAnonymously()
}
binding.buttonAnonymousSignOut.setOnClickListener {
signOut()
}
binding.buttonLinkAccount.setOnClickListener {
linkAccount()
}
}
override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
updateUI(currentUser)
}
private fun signInAnonymously() {
showProgressBar()
auth.signInAnonymously()
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInAnonymously:success")
val user = auth.currentUser
updateUI(user)
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInAnonymously:failure", task.exception)
Toast.makeText(
context,
"Authentication failed.",
Toast.LENGTH_SHORT,
).show()
updateUI(null)
}
hideProgressBar()
}
}
private fun signOut() {
auth.signOut()
updateUI(null)
}
private fun linkAccount() {
// Make sure form is valid
if (!validateLinkForm()) {
return
}
// Get email and password from the form
val email = binding.fieldEmail.text.toString()
val password = binding.fieldPassword.text.toString()
// Create EmailAuthCredential with email and password
val credential = EmailAuthProvider.getCredential(email, password)
// Link the anonymous user to the email credential
showProgressBar()
auth.currentUser!!.linkWithCredential(credential)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
Log.d(TAG, "linkWithCredential:success")
val user = task.result?.user
updateUI(user)
} else {
Log.w(TAG, "linkWithCredential:failure", task.exception)
Toast.makeText(
context,
"Authentication failed.",
Toast.LENGTH_SHORT,
).show()
updateUI(null)
}
hideProgressBar()
}
}
private fun validateLinkForm(): Boolean {
var valid = true
val email = binding.fieldEmail.text.toString()
if (TextUtils.isEmpty(email)) {
binding.fieldEmail.error = "Required."
valid = false
} else {
binding.fieldEmail.error = null
}
val password = binding.fieldPassword.text.toString()
if (TextUtils.isEmpty(password)) {
binding.fieldPassword.error = "Required."
valid = false
} else {
binding.fieldPassword.error = null
}
return valid
}
private fun updateUI(user: FirebaseUser?) {
hideProgressBar()
val isSignedIn = user != null
// Status text
if (isSignedIn) {
binding.anonymousStatusId.text = getString(R.string.id_fmt, user!!.uid)
binding.anonymousStatusEmail.text = getString(R.string.email_fmt, user.email)
} else {
binding.anonymousStatusId.setText(R.string.signed_out)
binding.anonymousStatusEmail.text = null
}
// Button visibility
binding.buttonAnonymousSignIn.isEnabled = !isSignedIn
binding.buttonAnonymousSignOut.isEnabled = isSignedIn
binding.buttonLinkAccount.isEnabled = isSignedIn
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "AnonymousAuth"
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/BaseActivity.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
open class BaseActivity : AppCompatActivity() {
private var progressBar: ProgressBar? = null
fun setProgressBar(bar: ProgressBar) {
progressBar = bar
}
fun showProgressBar() {
progressBar?.visibility = View.VISIBLE
}
fun hideProgressBar() {
progressBar?.visibility = View.INVISIBLE
}
fun hideKeyboard(view: View) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
public override fun onStop() {
super.onStop()
hideProgressBar()
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/BaseFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.ProgressBar
import androidx.fragment.app.Fragment
open class BaseFragment : Fragment() {
private var progressBar: ProgressBar? = null
fun setProgressBar(bar: ProgressBar) {
progressBar = bar
}
fun showProgressBar() {
progressBar?.visibility = View.VISIBLE
}
fun hideProgressBar() {
progressBar?.visibility = View.INVISIBLE
}
fun hideKeyboard(view: View) {
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
override fun onStop() {
super.onStop()
hideProgressBar()
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/ChooserFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentChooserBinding
/**
* Simple list-based Fragment to redirect to one of the other Fragments. This Fragment does not
* contain any useful code related to Firebase Authentication. You may want to start with
* one of the following Files:
* {@link GoogleSignInFragment}
* {@link FacebookLoginFragment}
* {@link EmailPasswordFragment}
* {@link PasswordlessActivity}
* {@link PhoneAuthFragment}
* {@link AnonymousAuthFragment}
* {@link CustomAuthFragment}
* {@link GenericIdpFragment}
* {@link MultiFactorFragment}
*/
class ChooserFragment : Fragment() {
private var _binding: FragmentChooserBinding? = null
private val binding: FragmentChooserBinding
get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentChooserBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Set up Adapter
val adapter = MyArrayAdapter(requireContext(), android.R.layout.simple_list_item_2)
adapter.setDescriptionIds(DESCRIPTION_IDS)
binding.listView.adapter = adapter
binding.listView.setOnItemClickListener { _, _, position, _ ->
val actionId = NAV_ACTIONS[position]
findNavController().navigate(actionId)
}
}
class MyArrayAdapter(
private val ctx: Context,
resource: Int,
) : ArrayAdapter(ctx, resource, CLASS_NAMES) {
private var descriptionIds: IntArray? = null
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var view = convertView
if (convertView == null) {
val inflater = ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
view = inflater.inflate(android.R.layout.simple_list_item_2, null)
}
// Android internal resource hence can't use synthetic binding
view?.findViewById(android.R.id.text1)?.text = CLASS_NAMES[position]
view?.findViewById(android.R.id.text2)?.setText(descriptionIds!![position])
return view!!
}
fun setDescriptionIds(descriptionIds: IntArray) {
this.descriptionIds = descriptionIds
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private val NAV_ACTIONS = arrayOf(
R.id.action_google,
R.id.action_facebook,
R.id.action_emailpassword,
R.id.action_passwordless,
R.id.action_phoneauth,
R.id.action_anonymousauth,
R.id.action_firebaseui,
R.id.action_customauth,
R.id.action_genericidp,
R.id.action_mfa,
)
private val CLASS_NAMES = arrayOf(
"GoogleSignInFragment",
"FacebookLoginFragment",
"EmailPasswordFragment",
"PasswordlessActivity",
"PhoneAuthFragment",
"AnonymousAuthFragment",
"FirebaseUIFragment",
"CustomAuthFragment",
"GenericIdpFragment",
"MultiFactorFragment",
)
private val DESCRIPTION_IDS = intArrayOf(
R.string.desc_google_sign_in,
R.string.desc_facebook_login,
R.string.desc_emailpassword,
R.string.desc_passwordless,
R.string.desc_phone_auth,
R.string.desc_anonymous_auth,
R.string.desc_firebase_ui,
R.string.desc_custom_auth,
R.string.desc_generic_idp,
R.string.desc_multi_factor,
)
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/CustomAuthFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentCustomBinding
/**
* Demonstrate Firebase Authentication using a custom minted token. For more information, see:
* https://firebase.google.com/docs/auth/android/custom-auth
*/
class CustomAuthFragment : Fragment() {
private lateinit var auth: FirebaseAuth
private var _binding: FragmentCustomBinding? = null
private val binding: FragmentCustomBinding
get() = _binding!!
private var customToken: String? = null
private lateinit var tokenReceiver: TokenBroadcastReceiver
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentCustomBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Button click listeners
binding.buttonSignIn.setOnClickListener { startSignIn() }
// Create token receiver (for demo purposes only)
tokenReceiver = object : TokenBroadcastReceiver() {
override fun onNewToken(token: String?) {
Log.d(TAG, "onNewToken:$token")
setCustomToken(token.toString())
}
}
// Initialize Firebase Auth
auth = Firebase.auth
}
override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
updateUI(currentUser)
}
override fun onResume() {
super.onResume()
requireActivity().registerReceiver(tokenReceiver, TokenBroadcastReceiver.filter)
}
override fun onPause() {
super.onPause()
requireActivity().unregisterReceiver(tokenReceiver)
}
private fun startSignIn() {
// Initiate sign in with custom token
customToken?.let {
auth.signInWithCustomToken(it)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithCustomToken:success")
val user = auth.currentUser
updateUI(user)
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCustomToken:failure", task.exception)
Toast.makeText(
context,
"Authentication failed.",
Toast.LENGTH_SHORT,
).show()
updateUI(null)
}
}
}
}
private fun updateUI(user: FirebaseUser?) {
if (user != null) {
binding.textSignInStatus.text = getString(R.string.custom_auth_signin_status_user, user.uid)
} else {
binding.textSignInStatus.text = getString(R.string.custom_auth_signin_status_failed)
}
}
private fun setCustomToken(token: String) {
customToken = token
val status = "Token:$customToken"
// Enable/disable sign-in button and show the token
binding.buttonSignIn.isEnabled = true
binding.textTokenStatus.text = status
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "CustomAuthFragment"
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/EmailPasswordFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.navigation.fragment.findNavController
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseAuthMultiFactorException
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentEmailpasswordBinding
class EmailPasswordFragment : BaseFragment() {
private lateinit var auth: FirebaseAuth
private var _binding: FragmentEmailpasswordBinding? = null
private val binding: FragmentEmailpasswordBinding
get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentEmailpasswordBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setProgressBar(binding.progressBar)
// Buttons
with(binding) {
emailSignInButton.setOnClickListener {
val email = binding.fieldEmail.text.toString()
val password = binding.fieldPassword.text.toString()
signIn(email, password)
}
emailCreateAccountButton.setOnClickListener {
val email = binding.fieldEmail.text.toString()
val password = binding.fieldPassword.text.toString()
createAccount(email, password)
}
signOutButton.setOnClickListener { signOut() }
verifyEmailButton.setOnClickListener { sendEmailVerification() }
reloadButton.setOnClickListener { reload() }
}
// Initialize Firebase Auth
auth = Firebase.auth
}
public override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
if (currentUser != null) {
reload()
}
}
private fun createAccount(email: String, password: String) {
Log.d(TAG, "createAccount:$email")
if (!validateForm()) {
return
}
showProgressBar()
auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "createUserWithEmail:success")
val user = auth.currentUser
updateUI(user)
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "createUserWithEmail:failure", task.exception)
Toast.makeText(
context,
"Authentication failed.",
Toast.LENGTH_SHORT,
).show()
updateUI(null)
}
hideProgressBar()
}
}
private fun signIn(email: String, password: String) {
Log.d(TAG, "signIn:$email")
if (!validateForm()) {
return
}
showProgressBar()
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithEmail:success")
val user = auth.currentUser
updateUI(user)
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithEmail:failure", task.exception)
Toast.makeText(
context,
"Authentication failed.",
Toast.LENGTH_SHORT,
).show()
updateUI(null)
checkForMultiFactorFailure(task.exception!!)
}
if (!task.isSuccessful) {
binding.status.setText(R.string.auth_failed)
}
hideProgressBar()
}
}
private fun signOut() {
auth.signOut()
updateUI(null)
}
private fun sendEmailVerification() {
// Disable button
binding.verifyEmailButton.isEnabled = false
// Send verification email
val user = auth.currentUser!!
user.sendEmailVerification()
.addOnCompleteListener(requireActivity()) { task ->
// Re-enable button
binding.verifyEmailButton.isEnabled = true
if (task.isSuccessful) {
Toast.makeText(
context,
"Verification email sent to ${user.email} ",
Toast.LENGTH_SHORT,
).show()
} else {
Log.e(TAG, "sendEmailVerification", task.exception)
Toast.makeText(
context,
"Failed to send verification email.",
Toast.LENGTH_SHORT,
).show()
}
}
}
private fun reload() {
auth.currentUser!!.reload().addOnCompleteListener { task ->
if (task.isSuccessful) {
updateUI(auth.currentUser)
Toast.makeText(context, "Reload successful!", Toast.LENGTH_SHORT).show()
} else {
Log.e(TAG, "reload", task.exception)
Toast.makeText(context, "Failed to reload user.", Toast.LENGTH_SHORT).show()
}
}
}
private fun validateForm(): Boolean {
var valid = true
val email = binding.fieldEmail.text.toString()
if (TextUtils.isEmpty(email)) {
binding.fieldEmail.error = "Required."
valid = false
} else {
binding.fieldEmail.error = null
}
val password = binding.fieldPassword.text.toString()
if (TextUtils.isEmpty(password)) {
binding.fieldPassword.error = "Required."
valid = false
} else {
binding.fieldPassword.error = null
}
return valid
}
private fun updateUI(user: FirebaseUser?) {
hideProgressBar()
if (user != null) {
binding.status.text = getString(
R.string.emailpassword_status_fmt,
user.email,
user.isEmailVerified,
)
binding.detail.text = getString(R.string.firebase_status_fmt, user.uid)
binding.emailPasswordButtons.visibility = View.GONE
binding.emailPasswordFields.visibility = View.GONE
binding.signedInButtons.visibility = View.VISIBLE
if (user.isEmailVerified) {
binding.verifyEmailButton.visibility = View.GONE
} else {
binding.verifyEmailButton.visibility = View.VISIBLE
}
} else {
binding.status.setText(R.string.signed_out)
binding.detail.text = null
binding.emailPasswordButtons.visibility = View.VISIBLE
binding.emailPasswordFields.visibility = View.VISIBLE
binding.signedInButtons.visibility = View.GONE
}
}
private fun checkForMultiFactorFailure(e: Exception) {
// Multi-factor authentication with SMS is currently only available for
// Google Cloud Identity Platform projects. For more information:
// https://cloud.google.com/identity-platform/docs/android/mfa
if (e is FirebaseAuthMultiFactorException) {
Log.w(TAG, "multiFactorFailure", e)
val resolver = e.resolver
val args = bundleOf(
MultiFactorSignInFragment.EXTRA_MFA_RESOLVER to resolver
)
findNavController().navigate(R.id.action_emailpassword_to_mfasignin, args)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "EmailPassword"
private const val RC_MULTI_FACTOR = 9005
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FacebookLoginFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.facebook.AccessToken
import com.facebook.CallbackManager
import com.facebook.FacebookCallback
import com.facebook.FacebookException
import com.facebook.login.LoginManager
import com.facebook.login.LoginResult
import com.google.firebase.auth.FacebookAuthProvider
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentFacebookBinding
/**
* Demonstrate Firebase Authentication using a Facebook access token.
*/
class FacebookLoginFragment : BaseFragment() {
private lateinit var auth: FirebaseAuth
private var _binding: FragmentFacebookBinding? = null
private val binding: FragmentFacebookBinding
get() = _binding!!
private lateinit var callbackManager: CallbackManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentFacebookBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setProgressBar(binding.progressBar)
binding.buttonFacebookSignout.setOnClickListener { signOut() }
// Initialize Firebase Auth
auth = Firebase.auth
// Initialize Facebook Login button
callbackManager = CallbackManager.Factory.create()
binding.buttonFacebookLogin.setPermissions("email", "public_profile")
binding.buttonFacebookLogin.registerCallback(
callbackManager,
object : FacebookCallback {
override fun onSuccess(loginResult: LoginResult) {
Log.d(TAG, "facebook:onSuccess:$loginResult")
handleFacebookAccessToken(loginResult.accessToken)
}
override fun onCancel() {
Log.d(TAG, "facebook:onCancel")
updateUI(null)
}
override fun onError(error: FacebookException) {
Log.d(TAG, "facebook:onError", error)
updateUI(null)
}
},
)
}
override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
updateUI(currentUser)
}
private fun handleFacebookAccessToken(token: AccessToken) {
Log.d(TAG, "handleFacebookAccessToken:$token")
showProgressBar()
val credential = FacebookAuthProvider.getCredential(token.token)
auth.signInWithCredential(credential)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithCredential:success")
val user = auth.currentUser
updateUI(user)
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCredential:failure", task.exception)
Toast.makeText(
context,
"Authentication failed.",
Toast.LENGTH_SHORT,
).show()
updateUI(null)
}
hideProgressBar()
}
}
fun signOut() {
auth.signOut()
LoginManager.getInstance().logOut()
updateUI(null)
}
private fun updateUI(user: FirebaseUser?) {
hideProgressBar()
if (user != null) {
binding.status.text = getString(R.string.facebook_status_fmt, user.displayName)
binding.detail.text = getString(R.string.firebase_status_fmt, user.uid)
binding.buttonFacebookLogin.visibility = View.GONE
binding.buttonFacebookSignout.visibility = View.VISIBLE
} else {
binding.status.setText(R.string.signed_out)
binding.detail.text = null
binding.buttonFacebookLogin.visibility = View.VISIBLE
binding.buttonFacebookSignout.visibility = View.GONE
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "FacebookLogin"
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/FirebaseUIFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import com.firebase.ui.auth.AuthUI
import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract
import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.BuildConfig
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentFirebaseUiBinding
/**
* Demonstrate authentication using the FirebaseUI-Android library. This fragment demonstrates
* using FirebaseUI for basic email/password sign in.
*
* For more information, visit https://github.com/firebase/firebaseui-android
*/
class FirebaseUIFragment : Fragment() {
private lateinit var auth: FirebaseAuth
private var _binding: FragmentFirebaseUiBinding? = null
private val binding: FragmentFirebaseUiBinding
get() = _binding!!
// Build FirebaseUI sign in intent. For documentation on this operation and all
// possible customization see: https://github.com/firebase/firebaseui-android
private val signInLauncher = registerForActivityResult(
FirebaseAuthUIActivityResultContract(),
) { result -> this.onSignInResult(result) }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentFirebaseUiBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Initialize Firebase Auth
auth = Firebase.auth
binding.signInButton.setOnClickListener { startSignIn() }
binding.signOutButton.setOnClickListener { signOut() }
}
override fun onStart() {
super.onStart()
updateUI(auth.currentUser)
}
private fun onSignInResult(result: FirebaseAuthUIAuthenticationResult) {
if (result.resultCode == Activity.RESULT_OK) {
// Sign in succeeded
updateUI(auth.currentUser)
} else {
// Sign in failed
Toast.makeText(context, "Sign In Failed", Toast.LENGTH_SHORT).show()
updateUI(null)
}
}
private fun startSignIn() {
val intent = AuthUI.getInstance().createSignInIntentBuilder()
.setCredentialManagerEnabled(!BuildConfig.DEBUG)
.setAvailableProviders(listOf(AuthUI.IdpConfig.EmailBuilder().build()))
.setLogo(R.mipmap.ic_launcher)
.build()
signInLauncher.launch(intent)
}
private fun updateUI(user: FirebaseUser?) {
if (user != null) {
// Signed in
binding.status.text = getString(R.string.firebaseui_status_fmt, user.email)
binding.detail.text = getString(R.string.id_fmt, user.uid)
binding.signInButton.visibility = View.GONE
binding.signOutButton.visibility = View.VISIBLE
} else {
// Signed out
binding.status.setText(R.string.signed_out)
binding.detail.text = null
binding.signInButton.visibility = View.VISIBLE
binding.signOutButton.visibility = View.GONE
}
}
private fun signOut() {
AuthUI.getInstance().signOut(requireContext())
updateUI(null)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GenericIdpFragment.kt
================================================
/**
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Toast
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.auth
import com.google.firebase.auth.oAuthProvider
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentGenericIdpBinding
import java.util.ArrayList
/**
* Demonstrate Firebase Authentication using a Generic Identity Provider (IDP).
*/
class GenericIdpFragment : BaseFragment() {
private lateinit var auth: FirebaseAuth
private var _binding: FragmentGenericIdpBinding? = null
private val binding: FragmentGenericIdpBinding
get() = _binding!!
private lateinit var spinnerAdapter: ArrayAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentGenericIdpBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Initialize Firebase Auth
auth = Firebase.auth
// Set up button click listeners
binding.genericSignInButton.setOnClickListener { signIn() }
binding.signOutButton.setOnClickListener {
auth.signOut()
updateUI(null)
}
// Spinner
val providers = ArrayList(PROVIDER_MAP.keys)
spinnerAdapter = ArrayAdapter(requireContext(), R.layout.item_spinner_list, providers)
binding.providerSpinner.adapter = spinnerAdapter
binding.providerSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
binding.genericSignInButton.text =
getString(R.string.generic_signin_fmt, spinnerAdapter.getItem(position))
}
override fun onNothingSelected(parent: AdapterView<*>) {}
}
binding.providerSpinner.setSelection(0)
}
override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
updateUI(currentUser)
// Look for a pending auth result
val pending = auth.pendingAuthResult
if (pending != null) {
pending.addOnSuccessListener { authResult ->
Log.d(TAG, "checkPending:onSuccess:$authResult")
updateUI(authResult.user)
}.addOnFailureListener { e ->
Log.w(TAG, "checkPending:onFailure", e)
}
} else {
Log.d(TAG, "checkPending: null")
}
}
private fun signIn() {
// Could add custom scopes here
val customScopes = ArrayList()
// Examples of provider ID: apple.com (Apple), microsoft.com (Microsoft), yahoo.com (Yahoo)
val providerId = getProviderId()
auth.startActivityForSignInWithProvider(
requireActivity(),
oAuthProvider(providerId, auth) {
scopes = customScopes
},
)
.addOnSuccessListener { authResult ->
Log.d(TAG, "activitySignIn:onSuccess:${authResult.user}")
updateUI(authResult.user)
}
.addOnFailureListener { e ->
Log.w(TAG, "activitySignIn:onFailure", e)
showToast(getString(R.string.error_sign_in_failed))
}
}
private fun getProviderId(): String {
val providerName = spinnerAdapter.getItem(binding.providerSpinner.selectedItemPosition)
return PROVIDER_MAP[providerName!!] ?: error("No provider selected")
}
private fun updateUI(user: FirebaseUser?) {
hideProgressBar()
if (user != null) {
binding.status.text = getString(R.string.generic_status_fmt, user.displayName, user.email)
binding.detail.text = getString(R.string.firebase_status_fmt, user.uid)
binding.spinnerLayout.visibility = View.GONE
binding.genericSignInButton.visibility = View.GONE
binding.signOutButton.visibility = View.VISIBLE
} else {
binding.status.setText(R.string.signed_out)
binding.detail.text = null
binding.spinnerLayout.visibility = View.VISIBLE
binding.genericSignInButton.visibility = View.VISIBLE
binding.signOutButton.visibility = View.GONE
}
}
private fun showToast(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "GenericIdp"
private val PROVIDER_MAP = mapOf(
"Apple" to "apple.com",
"Microsoft" to "microsoft.com",
"Yahoo" to "yahoo.com",
"Twitter" to "twitter.com",
)
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/GoogleSignInFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.credentials.ClearCredentialStateRequest
import androidx.credentials.Credential
import androidx.credentials.CredentialManager
import androidx.credentials.CustomCredential
import androidx.credentials.GetCredentialRequest
import androidx.credentials.exceptions.ClearCredentialException
import androidx.credentials.exceptions.GetCredentialException
import androidx.lifecycle.lifecycleScope
import com.google.android.libraries.identity.googleid.GetGoogleIdOption
import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential.Companion.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.GoogleAuthProvider
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentGoogleBinding
import kotlinx.coroutines.launch
/**
* Demonstrate Firebase Authentication using a Google ID Token.
*/
class GoogleSignInFragment : BaseFragment() {
private lateinit var auth: FirebaseAuth
private var _binding: FragmentGoogleBinding? = null
private val binding: FragmentGoogleBinding
get() = _binding!!
private lateinit var credentialManager: CredentialManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentGoogleBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setProgressBar(binding.progressBar)
// Initialize Credential Manager
credentialManager = CredentialManager.create(requireContext())
// Initialize Firebase Auth
auth = Firebase.auth
// Button listeners
binding.signInButton.setOnClickListener { signIn() }
binding.signOutButton.setOnClickListener { signOut() }
// Display Credential Manager Bottom Sheet if user isn't logged in
if (auth.currentUser == null) { showBottomSheet() }
}
override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
updateUI(currentUser)
}
private fun signIn() {
// Create the dialog configuration for the Credential Manager request
val signInWithGoogleOption = GetSignInWithGoogleOption
.Builder(serverClientId = requireContext().getString(R.string.default_web_client_id))
.build()
// Create the Credential Manager request using the configuration created above
val request = GetCredentialRequest.Builder()
.addCredentialOption(signInWithGoogleOption)
.build()
launchCredentialManager(request)
}
private fun showBottomSheet() {
// Create the bottom sheet configuration for the Credential Manager request
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(true)
.setServerClientId(requireContext().getString(R.string.default_web_client_id))
.build()
// Create the Credential Manager request using the configuration created above
val request = GetCredentialRequest.Builder()
.addCredentialOption(googleIdOption)
.build()
launchCredentialManager(request)
}
private fun launchCredentialManager(request: GetCredentialRequest) {
viewLifecycleOwner.lifecycleScope.launch {
try {
// Launch Credential Manager UI
val result = credentialManager.getCredential(
context = requireContext(),
request = request
)
// Extract credential from the result returned by Credential Manager
createGoogleIdToken(result.credential)
} catch (e: GetCredentialException) {
Log.e(TAG, "Couldn't retrieve user's credentials: ${e.localizedMessage}")
}
}
}
private fun createGoogleIdToken(credential: Credential) {
// Check if credential is of type Google ID
if (credential is CustomCredential && credential.type == TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
// Create Google ID Token
val googleIdTokenCredential = GoogleIdTokenCredential.createFrom(credential.data)
// Sign in to Firebase with using the token
firebaseAuthWithGoogle(googleIdTokenCredential.idToken)
} else {
Log.w(TAG, "Credential is not of type Google ID!")
}
}
private fun firebaseAuthWithGoogle(idToken: String) {
showProgressBar()
val credential = GoogleAuthProvider.getCredential(idToken, null)
auth.signInWithCredential(credential)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithCredential:success")
val user = auth.currentUser
updateUI(user)
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCredential:failure", task.exception)
val view = binding.mainLayout
Snackbar.make(view, "Authentication Failed.", Snackbar.LENGTH_SHORT).show()
updateUI(null)
}
hideProgressBar()
}
}
private fun signOut() {
// Firebase sign out
auth.signOut()
// When a user signs out, clear the current user credential state from all credential providers.
// This will notify all providers that any stored credential session for the given app should be cleared.
viewLifecycleOwner.lifecycleScope.launch {
try {
val clearRequest = ClearCredentialStateRequest()
credentialManager.clearCredentialState(clearRequest)
updateUI(null)
} catch (e: ClearCredentialException) {
Log.e(TAG, "Couldn't clear user credentials: ${e.localizedMessage}")
}
}
}
private fun updateUI(user: FirebaseUser?) {
hideProgressBar()
if (user != null) {
binding.status.text = getString(R.string.google_status_fmt, user.email)
binding.detail.text = getString(R.string.firebase_status_fmt, user.uid)
binding.signInButton.visibility = View.GONE
binding.signOutButton.visibility = View.VISIBLE
} else {
binding.status.setText(R.string.signed_out)
binding.detail.text = null
binding.signInButton.visibility = View.VISIBLE
binding.signOutButton.visibility = View.GONE
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "GoogleFragmentKt"
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/MainActivity.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import com.google.firebase.quickstart.auth.R
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findNavController(R.id.nav_host_fragment).setGraph(R.navigation.nav_graph_kotlin)
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/MultiFactorEnrollFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.navigation.fragment.findNavController
import com.google.firebase.FirebaseException
import com.google.firebase.auth.PhoneAuthCredential
import com.google.firebase.auth.PhoneAuthOptions
import com.google.firebase.auth.PhoneAuthProvider
import com.google.firebase.auth.PhoneMultiFactorGenerator
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.databinding.FragmentPhoneAuthBinding
import java.util.concurrent.TimeUnit
/**
* Activity that allows the user to enroll second factors.
*/
class MultiFactorEnrollFragment : BaseFragment() {
private var _binding: FragmentPhoneAuthBinding? = null
private val binding: FragmentPhoneAuthBinding
get() = _binding!!
private var lastCodeVerificationId: String? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentPhoneAuthBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.titleText.text = "SMS as a Second Factor"
binding.status.visibility = View.GONE
binding.detail.visibility = View.GONE
binding.buttonStartVerification.setOnClickListener { onClickVerifyPhoneNumber() }
binding.buttonVerifyPhone.setOnClickListener { onClickSignInWithPhoneNumber() }
}
private fun onClickVerifyPhoneNumber() {
val phoneNumber = binding.fieldPhoneNumber.text.toString()
val callbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
override fun onVerificationCompleted(credential: PhoneAuthCredential) {
// Instant-validation has been disabled (see requireSmsValidation below).
// Auto-retrieval has also been disabled (timeout is set to 0).
// This should never be triggered.
throw RuntimeException(
"onVerificationCompleted() triggered with instant-validation and auto-retrieval disabled.",
)
}
override fun onCodeSent(
verificationId: String,
token: PhoneAuthProvider.ForceResendingToken,
) {
Log.d(TAG, "onCodeSent:$verificationId")
Toast.makeText(context, "SMS code has been sent", Toast.LENGTH_SHORT)
.show()
lastCodeVerificationId = verificationId
}
override fun onVerificationFailed(e: FirebaseException) {
Log.w(TAG, "onVerificationFailed ", e)
Toast.makeText(context, "Verification failed: ${e.message}", Toast.LENGTH_SHORT)
.show()
}
}
Firebase.auth
.currentUser!!
.multiFactor
.session
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val phoneAuthOptions = PhoneAuthOptions.newBuilder()
.setActivity(requireActivity())
.setPhoneNumber(phoneNumber) // A timeout of 0 disables SMS-auto-retrieval.
.setTimeout(0L, TimeUnit.SECONDS)
.setMultiFactorSession(task.result!!)
.setCallbacks(callbacks) // Disable instant-validation.
.requireSmsValidation(true)
.build()
PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions)
} else {
Toast.makeText(
context,
"Failed to get session: ${task.exception}",
Toast.LENGTH_SHORT,
)
.show()
}
}
}
private fun onClickSignInWithPhoneNumber() {
val smsCode = binding.fieldVerificationCode.text.toString()
if (TextUtils.isEmpty(smsCode)) {
return
}
val credential = PhoneAuthProvider.getCredential(lastCodeVerificationId!!, smsCode)
enrollWithPhoneAuthCredential(credential)
}
private fun enrollWithPhoneAuthCredential(credential: PhoneAuthCredential) {
Firebase.auth
.currentUser!!
.multiFactor
.enroll(PhoneMultiFactorGenerator.getAssertion(credential), null)
.addOnSuccessListener {
Toast.makeText(context, "MFA enrollment was successful", Toast.LENGTH_LONG)
.show()
findNavController().popBackStack()
}
.addOnFailureListener { e ->
Log.d(TAG, "MFA failure", e)
Toast.makeText(
context,
"MFA enrollment was unsuccessful. $e",
Toast.LENGTH_LONG,
)
.show()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "MfaEnrollFragment"
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/MultiFactorFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.app.AlertDialog
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isGone
import androidx.navigation.fragment.findNavController
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.PhoneMultiFactorInfo
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentMultiFactorBinding
class MultiFactorFragment : BaseFragment() {
private lateinit var auth: FirebaseAuth
private var _binding: FragmentMultiFactorBinding? = null
private val binding: FragmentMultiFactorBinding
get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentMultiFactorBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setProgressBar(binding.progressBar)
// Buttons
binding.emailSignInButton.setOnClickListener {
findNavController().navigate(R.id.action_mfa_to_emailpassword)
}
binding.signOutButton.setOnClickListener { signOut() }
binding.verifyEmailButton.setOnClickListener { sendEmailVerification() }
binding.enrollMfa.setOnClickListener {
findNavController().navigate(R.id.action_mfa_to_enroll)
}
binding.unenrollMfa.setOnClickListener {
findNavController().navigate(R.id.action_mfa_to_unenroll)
}
binding.reloadButton.setOnClickListener { reload() }
// Initialize Firebase Auth
auth = Firebase.auth
showDisclaimer()
}
public override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
updateUI(currentUser)
}
private fun signOut() {
auth.signOut()
updateUI(null)
}
private fun sendEmailVerification() { // Disable button
binding.verifyEmailButton.isEnabled = false
// Send verification email
val user = auth.currentUser!!
user.sendEmailVerification()
.addOnCompleteListener(requireActivity()) { task ->
// Re-enable button
binding.verifyEmailButton.isEnabled = true
if (task.isSuccessful) {
Toast.makeText(
context,
"Verification email sent to " + user.email,
Toast.LENGTH_SHORT,
).show()
} else {
Log.e(TAG, "sendEmailVerification", task.exception)
Toast.makeText(
context,
"Failed to send verification email.",
Toast.LENGTH_SHORT,
).show()
}
}
}
private fun reload() {
auth.currentUser!!.reload().addOnCompleteListener { task ->
if (task.isSuccessful) {
updateUI(auth.currentUser)
Toast.makeText(
context,
"Reload successful!",
Toast.LENGTH_SHORT,
).show()
} else {
Log.e(TAG, "reload", task.exception)
Toast.makeText(
context,
"Failed to reload user.",
Toast.LENGTH_SHORT,
).show()
}
}
}
private fun updateUI(user: FirebaseUser?) {
hideProgressBar()
if (user != null) {
binding.status.text = getString(
R.string.emailpassword_status_fmt,
user.email,
user.isEmailVerified,
)
binding.detail.text = getString(R.string.firebase_status_fmt, user.uid)
val secondFactors = user.multiFactor.enrolledFactors
if (secondFactors.isEmpty()) {
binding.unenrollMfa.visibility = View.GONE
} else {
binding.unenrollMfa.visibility = View.VISIBLE
val sb = StringBuilder("Second Factors: ")
val delimiter = ", "
for (x in secondFactors) {
sb.append((x as PhoneMultiFactorInfo).phoneNumber + delimiter)
}
sb.setLength(sb.length - delimiter.length)
binding.mfaInfo.text = sb.toString()
}
binding.emailSignInButton.visibility = View.GONE
binding.signedInButtons.visibility = View.VISIBLE
val reloadVisibility = if (secondFactors.isEmpty()) View.VISIBLE else View.GONE
binding.reloadButton.visibility = reloadVisibility
binding.verifyEmailButton.isGone = user.isEmailVerified
binding.enrollMfa.isGone = !user.isEmailVerified
} else {
binding.status.setText(R.string.multi_factor_signed_out)
binding.detail.text = null
binding.mfaInfo.text = null
binding.emailSignInButton.visibility = View.VISIBLE
binding.signedInButtons.visibility = View.GONE
}
}
private fun showDisclaimer() {
AlertDialog.Builder(requireContext())
.setTitle("Warning")
.setMessage(
"Multi-factor authentication with SMS is currently only available for " +
"Google Cloud Identity Platform projects. For more information see: " +
"https://cloud.google.com/identity-platform/docs/android/mfa",
)
.setPositiveButton("OK", null)
.show()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "MultiFactor"
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/MultiFactorSignInFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.navigation.fragment.findNavController
import com.google.firebase.FirebaseException
import com.google.firebase.auth.MultiFactorResolver
import com.google.firebase.auth.PhoneAuthCredential
import com.google.firebase.auth.PhoneAuthOptions
import com.google.firebase.auth.PhoneAuthProvider
import com.google.firebase.auth.PhoneMultiFactorGenerator
import com.google.firebase.auth.PhoneMultiFactorInfo
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentMultiFactorSignInBinding
import java.util.concurrent.TimeUnit
/**
* Fragment that handles MFA sign-in
*/
class MultiFactorSignInFragment : BaseFragment() {
private var _binding: FragmentMultiFactorSignInBinding? = null
private val binding: FragmentMultiFactorSignInBinding
get() = _binding!!
private lateinit var multiFactorResolver: MultiFactorResolver
private var lastPhoneAuthCredential: PhoneAuthCredential? = null
private var lastVerificationId: String? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentMultiFactorSignInBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
savedInstanceState?.let { onViewStateRestored(it) }
// Users are currently limited to having 5 second factors
val phoneFactorButtonList = listOf(
binding.phoneFactor1,
binding.phoneFactor2,
binding.phoneFactor3,
binding.phoneFactor4,
binding.phoneFactor5,
)
for (button in phoneFactorButtonList) {
button.visibility = View.GONE
}
binding.finishMfaSignIn.setOnClickListener { onClickFinishSignIn() }
multiFactorResolver = getResolverFromArguments(requireArguments())
val multiFactorInfoList = multiFactorResolver.hints
for (i in multiFactorInfoList.indices) {
val phoneMultiFactorInfo = multiFactorInfoList[i] as PhoneMultiFactorInfo
val button = phoneFactorButtonList[i]
button.visibility = View.VISIBLE
button.text = phoneMultiFactorInfo.phoneNumber
button.isClickable = true
button.setOnClickListener(generateFactorOnClickListener(phoneMultiFactorInfo))
}
}
override fun onSaveInstanceState(bundle: Bundle) {
super.onSaveInstanceState(bundle)
bundle.putString(KEY_VERIFICATION_ID, lastVerificationId)
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
savedInstanceState?.let { savedState ->
lastVerificationId = savedState.getString(KEY_VERIFICATION_ID)
}
}
private fun generateFactorOnClickListener(phoneMultiFactorInfo: PhoneMultiFactorInfo): View.OnClickListener {
return View.OnClickListener {
PhoneAuthProvider.verifyPhoneNumber(
PhoneAuthOptions.newBuilder()
.setActivity(requireActivity())
.setMultiFactorSession(multiFactorResolver.session)
.setMultiFactorHint(phoneMultiFactorInfo)
.setCallbacks(generateCallbacks()) // A timeout of 0 disables SMS-auto-retrieval.
.setTimeout(0L, TimeUnit.SECONDS)
.build(),
)
}
}
private fun generateCallbacks(): PhoneAuthProvider.OnVerificationStateChangedCallbacks {
return object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
override fun onVerificationCompleted(phoneAuthCredential: PhoneAuthCredential) {
lastPhoneAuthCredential = phoneAuthCredential
binding.finishMfaSignIn.performClick()
Toast.makeText(context, "Verification complete!", Toast.LENGTH_SHORT)
.show()
}
override fun onCodeSent(verificationId: String, token: PhoneAuthProvider.ForceResendingToken) {
lastVerificationId = verificationId
binding.finishMfaSignIn.isClickable = true
}
override fun onVerificationFailed(e: FirebaseException) {
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT)
.show()
}
}
}
private fun getResolverFromArguments(arguments: Bundle): MultiFactorResolver {
return arguments.getParcelable(EXTRA_MFA_RESOLVER)!!
}
private fun onClickFinishSignIn() {
if (lastPhoneAuthCredential == null) {
if (TextUtils.isEmpty(binding.smsCode.text.toString())) {
Toast.makeText(context, "You need to enter an SMS code.", Toast.LENGTH_SHORT)
.show()
return
}
lastPhoneAuthCredential = PhoneAuthProvider.getCredential(
lastVerificationId!!,
binding.smsCode.text.toString(),
)
}
multiFactorResolver
.resolveSignIn(PhoneMultiFactorGenerator.getAssertion(lastPhoneAuthCredential!!))
.addOnSuccessListener {
findNavController().navigate(R.id.action_mfasignin_to_mfa)
}
.addOnFailureListener { e ->
Toast.makeText(context, "Error: " + e.message, Toast.LENGTH_SHORT)
.show()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val KEY_VERIFICATION_ID = "key_verification_id"
const val EXTRA_MFA_RESOLVER = "EXTRA_MFA_RESOLVER"
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/MultiFactorUnenrollFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.navigation.fragment.findNavController
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.PhoneMultiFactorInfo
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.databinding.FragmentMultiFactorSignInBinding
class MultiFactorUnenrollFragment : BaseFragment() {
private var _binding: FragmentMultiFactorSignInBinding? = null
private val binding: FragmentMultiFactorSignInBinding
get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentMultiFactorSignInBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.smsCode.visibility = View.GONE
binding.finishMfaSignIn.visibility = View.GONE
// Users are currently limited to having 5 second factors
val phoneFactorButtonList = listOf(
binding.phoneFactor1,
binding.phoneFactor2,
binding.phoneFactor3,
binding.phoneFactor4,
binding.phoneFactor5,
)
for (button in phoneFactorButtonList) {
button.visibility = View.GONE
}
val multiFactorInfoList = FirebaseAuth.getInstance().currentUser!!.multiFactor.enrolledFactors
for (i in multiFactorInfoList.indices) {
val phoneMultiFactorInfo = multiFactorInfoList[i] as PhoneMultiFactorInfo
val button = phoneFactorButtonList[i]
button.visibility = View.VISIBLE
button.text = phoneMultiFactorInfo.phoneNumber
button.isClickable = true
button.setOnClickListener {
Firebase.auth
.currentUser!!
.multiFactor
.unenroll(phoneMultiFactorInfo)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
Toast.makeText(
context,
"Successfully unenrolled!",
Toast.LENGTH_SHORT,
).show()
findNavController().popBackStack()
} else {
Toast.makeText(
context,
"Unable to unenroll second factor. ${task.exception}",
Toast.LENGTH_SHORT,
)
.show()
}
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/PasswordlessActivity.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.View
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseAuthActionCodeException
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.actionCodeSettings
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.ActivityPasswordlessBinding
/**
* Demonstrate Firebase Authentication without a password, using a link sent to an
* email address.
*/
class PasswordlessActivity : BaseActivity(), View.OnClickListener {
private var pendingEmail: String = ""
private var emailLink: String = ""
private lateinit var auth: FirebaseAuth
private lateinit var binding: ActivityPasswordlessBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPasswordlessBinding.inflate(layoutInflater)
setContentView(binding.root)
setProgressBar(binding.progressBar)
// Initialize Firebase Auth
auth = Firebase.auth
binding.passwordlessSendEmailButton.setOnClickListener(this)
binding.passwordlessSignInButton.setOnClickListener(this)
binding.signOutButton.setOnClickListener(this)
// Restore the "pending" email address
if (savedInstanceState != null) {
pendingEmail = savedInstanceState.getString(KEY_PENDING_EMAIL, null)
binding.fieldEmail.setText(pendingEmail)
}
// Check if the Intent that started the Activity contains an email sign-in link.
checkIntent(intent)
}
override fun onStart() {
super.onStart()
updateUI(auth.currentUser)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
checkIntent(intent)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_PENDING_EMAIL, pendingEmail)
}
/**
* Check to see if the Intent has an email link, and if so set up the UI accordingly.
* This can be called from either onCreate or onNewIntent, depending on how the Activity
* was launched.
*/
private fun checkIntent(intent: Intent?) {
if (intentHasEmailLink(intent)) {
emailLink = intent!!.data!!.toString()
binding.status.setText(R.string.status_link_found)
binding.passwordlessSendEmailButton.isEnabled = false
binding.passwordlessSignInButton.isEnabled = true
} else {
binding.status.setText(R.string.status_email_not_sent)
binding.passwordlessSendEmailButton.isEnabled = true
binding.passwordlessSignInButton.isEnabled = false
}
}
/**
* Determine if the given Intent contains an email sign-in link.
*/
private fun intentHasEmailLink(intent: Intent?): Boolean {
if (intent != null && intent.data != null) {
val intentData = intent.data.toString()
if (auth.isSignInWithEmailLink(intentData)) {
return true
}
}
return false
}
/**
* Send an email sign-in link to the specified email.
*/
private fun sendSignInLink(email: String) {
val settings = actionCodeSettings {
setAndroidPackageName(
packageName,
false,
null, // minimum app version
) // install if not available?
handleCodeInApp = true
url = "https://kotlin.auth.example.com/emailSignInLink"
}
hideKeyboard(binding.fieldEmail)
showProgressBar()
auth.sendSignInLinkToEmail(email, settings)
.addOnCompleteListener { task ->
hideProgressBar()
if (task.isSuccessful) {
Log.d(TAG, "Link sent")
showSnackbar("Sign-in link sent!")
pendingEmail = email
binding.status.setText(R.string.status_email_sent)
} else {
val e = task.exception
Log.w(TAG, "Could not send link", e)
showSnackbar("Failed to send link.")
if (e is FirebaseAuthInvalidCredentialsException) {
binding.fieldEmail.error = "Invalid email address."
}
}
}
}
/**
* Sign in using an email address and a link, the link is passed to the Activity
* from the dynamic link contained in the email.
*/
private fun signInWithEmailLink(email: String, link: String?) {
Log.d(TAG, "signInWithLink:" + link!!)
hideKeyboard(binding.fieldEmail)
showProgressBar()
auth.signInWithEmailLink(email, link)
.addOnCompleteListener { task ->
hideProgressBar()
if (task.isSuccessful) {
Log.d(TAG, "signInWithEmailLink:success")
binding.fieldEmail.text = null
updateUI(task.result?.user)
} else {
Log.w(TAG, "signInWithEmailLink:failure", task.exception)
updateUI(null)
if (task.exception is FirebaseAuthActionCodeException) {
showSnackbar("Invalid or expired sign-in link.")
}
}
}
}
private fun onSendLinkClicked() {
val email = binding.fieldEmail.text.toString()
if (TextUtils.isEmpty(email)) {
binding.fieldEmail.error = "Email must not be empty."
return
}
sendSignInLink(email)
}
private fun onSignInClicked() {
val email = binding.fieldEmail.text.toString()
if (TextUtils.isEmpty(email)) {
binding.fieldEmail.error = "Email must not be empty."
return
}
signInWithEmailLink(email, emailLink)
}
private fun onSignOutClicked() {
auth.signOut()
updateUI(null)
binding.status.setText(R.string.status_email_not_sent)
}
private fun updateUI(user: FirebaseUser?) {
if (user != null) {
binding.status.text = getString(
R.string.passwordless_status_fmt,
user.email,
user.isEmailVerified,
)
binding.passwordlessButtons.visibility = View.GONE
binding.signOutButton.visibility = View.VISIBLE
} else {
binding.passwordlessButtons.visibility = View.VISIBLE
binding.signOutButton.visibility = View.GONE
}
}
private fun showSnackbar(message: String) {
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_SHORT).show()
}
override fun onClick(view: View) {
when (view.id) {
R.id.passwordlessSendEmailButton -> onSendLinkClicked()
R.id.passwordlessSignInButton -> onSignInClicked()
R.id.signOutButton -> onSignOutClicked()
}
}
companion object {
private const val TAG = "PasswordlessSignIn"
private const val KEY_PENDING_EMAIL = "key_pending_email"
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/PhoneAuthFragment.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.FirebaseException
import com.google.firebase.FirebaseTooManyRequestsException
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseAuthInvalidCredentialsException
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.PhoneAuthCredential
import com.google.firebase.auth.PhoneAuthOptions
import com.google.firebase.auth.PhoneAuthProvider
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.auth.R
import com.google.firebase.quickstart.auth.databinding.FragmentPhoneAuthBinding
import java.util.concurrent.TimeUnit
class PhoneAuthFragment : Fragment() {
private lateinit var auth: FirebaseAuth
private var _binding: FragmentPhoneAuthBinding? = null
private val binding: FragmentPhoneAuthBinding
get() = _binding!!
private var verificationInProgress = false
private var storedVerificationId: String? = ""
private lateinit var resendToken: PhoneAuthProvider.ForceResendingToken
private lateinit var callbacks: PhoneAuthProvider.OnVerificationStateChangedCallbacks
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentPhoneAuthBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Restore instance state
savedInstanceState?.let { onViewStateRestored(it) }
// Assign click listeners
binding.buttonStartVerification.setOnClickListener {
if (!validatePhoneNumber()) {
return@setOnClickListener
}
startPhoneNumberVerification(binding.fieldPhoneNumber.text.toString())
}
binding.buttonVerifyPhone.setOnClickListener {
val code = binding.fieldVerificationCode.text.toString()
if (TextUtils.isEmpty(code)) {
binding.fieldVerificationCode.error = "Cannot be empty."
return@setOnClickListener
}
verifyPhoneNumberWithCode(storedVerificationId, code)
}
binding.buttonResend.setOnClickListener {
if (!validatePhoneNumber()) {
return@setOnClickListener
}
resendVerificationCode(binding.fieldPhoneNumber.text.toString(), resendToken)
}
binding.signOutButton.setOnClickListener { signOut() }
// Initialize Firebase Auth
auth = Firebase.auth
// Initialize phone auth callbacks
callbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
override fun onVerificationCompleted(credential: PhoneAuthCredential) {
// This callback will be invoked in two situations:
// 1 - Instant verification. In some cases the phone number can be instantly
// verified without needing to send or enter a verification code.
// 2 - Auto-retrieval. On some devices Google Play services can automatically
// detect the incoming verification SMS and perform verification without
// user action.
Log.d(TAG, "onVerificationCompleted:$credential")
verificationInProgress = false
// Update the UI and attempt sign in with the phone credential
updateUI(STATE_VERIFY_SUCCESS, credential)
signInWithPhoneAuthCredential(credential)
}
override fun onVerificationFailed(e: FirebaseException) {
// This callback is invoked in an invalid request for verification is made,
// for instance if the the phone number format is not valid.
Log.w(TAG, "onVerificationFailed", e)
verificationInProgress = false
if (e is FirebaseAuthInvalidCredentialsException) {
// Invalid request
binding.fieldPhoneNumber.error = "Invalid phone number."
} else if (e is FirebaseTooManyRequestsException) {
// The SMS quota for the project has been exceeded
Snackbar.make(
view,
"Quota exceeded.",
Snackbar.LENGTH_SHORT,
).show()
}
// Show a message and update the UI
updateUI(STATE_VERIFY_FAILED)
}
override fun onCodeSent(
verificationId: String,
token: PhoneAuthProvider.ForceResendingToken,
) {
// The SMS verification code has been sent to the provided phone number, we
// now need to ask the user to enter the code and then construct a credential
// by combining the code with a verification ID.
Log.d(TAG, "onCodeSent:$verificationId")
// Save verification ID and resending token so we can use them later
storedVerificationId = verificationId
resendToken = token
// Update UI
updateUI(STATE_CODE_SENT)
}
}
}
override fun onStart() {
super.onStart()
// Check if user is signed in (non-null) and update UI accordingly.
val currentUser = auth.currentUser
updateUI(currentUser)
if (verificationInProgress && validatePhoneNumber()) {
startPhoneNumberVerification(binding.fieldPhoneNumber.text.toString())
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_VERIFY_IN_PROGRESS, verificationInProgress)
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
savedInstanceState?.let { savedState ->
verificationInProgress = savedState.getBoolean(KEY_VERIFY_IN_PROGRESS)
}
}
private fun startPhoneNumberVerification(phoneNumber: String) {
val options = PhoneAuthOptions.newBuilder(auth)
.setPhoneNumber(phoneNumber) // Phone number to verify
.setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit
.setActivity(requireActivity()) // Activity (for callback binding)
.setCallbacks(callbacks) // OnVerificationStateChangedCallbacks
.build()
PhoneAuthProvider.verifyPhoneNumber(options)
verificationInProgress = true
}
private fun verifyPhoneNumberWithCode(verificationId: String?, code: String) {
val credential = PhoneAuthProvider.getCredential(verificationId!!, code)
signInWithPhoneAuthCredential(credential)
}
private fun resendVerificationCode(
phoneNumber: String,
token: PhoneAuthProvider.ForceResendingToken?,
) {
val optionsBuilder = PhoneAuthOptions.newBuilder(auth)
.setPhoneNumber(phoneNumber) // Phone number to verify
.setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit
.setActivity(requireActivity()) // Activity (for callback binding)
.setCallbacks(callbacks) // OnVerificationStateChangedCallbacks
if (token != null) {
optionsBuilder.setForceResendingToken(token) // callback's ForceResendingToken
}
PhoneAuthProvider.verifyPhoneNumber(optionsBuilder.build())
}
private fun signInWithPhoneAuthCredential(credential: PhoneAuthCredential) {
auth.signInWithCredential(credential)
.addOnCompleteListener(requireActivity()) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.d(TAG, "signInWithCredential:success")
val user = task.result?.user
updateUI(STATE_SIGNIN_SUCCESS, user)
} else {
// Sign in failed, display a message and update the UI
Log.w(TAG, "signInWithCredential:failure", task.exception)
if (task.exception is FirebaseAuthInvalidCredentialsException) {
// The verification code entered was invalid
binding.fieldVerificationCode.error = "Invalid code."
}
// Update UI
updateUI(STATE_SIGNIN_FAILED)
}
}
}
private fun signOut() {
auth.signOut()
updateUI(STATE_INITIALIZED)
}
private fun updateUI(user: FirebaseUser?) {
if (user != null) {
updateUI(STATE_SIGNIN_SUCCESS, user)
} else {
updateUI(STATE_INITIALIZED)
}
}
private fun updateUI(uiState: Int, cred: PhoneAuthCredential) {
updateUI(uiState, null, cred)
}
private fun updateUI(
uiState: Int,
user: FirebaseUser? = auth.currentUser,
cred: PhoneAuthCredential? = null,
) {
when (uiState) {
STATE_INITIALIZED -> {
// Initialized state, show only the phone number field and start button
enableViews(binding.buttonStartVerification, binding.fieldPhoneNumber)
disableViews(binding.buttonVerifyPhone, binding.buttonResend, binding.fieldVerificationCode)
binding.detail.text = null
}
STATE_CODE_SENT -> {
// Code sent state, show the verification field, the
enableViews(
binding.buttonVerifyPhone,
binding.buttonResend,
binding.fieldPhoneNumber,
binding.fieldVerificationCode,
)
disableViews(binding.buttonStartVerification)
binding.detail.setText(R.string.status_code_sent)
}
STATE_VERIFY_FAILED -> {
// Verification has failed, show all options
enableViews(
binding.buttonStartVerification,
binding.buttonVerifyPhone,
binding.buttonResend,
binding.fieldPhoneNumber,
binding.fieldVerificationCode,
)
binding.detail.setText(R.string.status_verification_failed)
}
STATE_VERIFY_SUCCESS -> {
// Verification has succeeded, proceed to firebase sign in
disableViews(
binding.buttonStartVerification,
binding.buttonVerifyPhone,
binding.buttonResend,
binding.fieldPhoneNumber,
binding.fieldVerificationCode,
)
binding.detail.setText(R.string.status_verification_succeeded)
// Set the verification text based on the credential
if (cred != null) {
if (cred.smsCode != null) {
binding.fieldVerificationCode.setText(cred.smsCode)
} else {
binding.fieldVerificationCode.setText(R.string.instant_validation)
}
}
}
STATE_SIGNIN_FAILED ->
// No-op, handled by sign-in check
binding.detail.setText(R.string.status_sign_in_failed)
STATE_SIGNIN_SUCCESS -> {
}
} // Np-op, handled by sign-in check
if (user == null) {
// Signed out
binding.phoneAuthFields.visibility = View.VISIBLE
binding.signOutButton.visibility = View.GONE
binding.status.setText(R.string.signed_out)
} else {
// Signed in
binding.phoneAuthFields.visibility = View.GONE
binding.signOutButton.visibility = View.VISIBLE
enableViews(binding.fieldPhoneNumber, binding.fieldVerificationCode)
binding.fieldPhoneNumber.text = null
binding.fieldVerificationCode.text = null
binding.status.setText(R.string.signed_in)
binding.detail.text = getString(R.string.firebase_status_fmt, user.uid)
}
}
private fun validatePhoneNumber(): Boolean {
val phoneNumber = binding.fieldPhoneNumber.text.toString()
if (TextUtils.isEmpty(phoneNumber)) {
binding.fieldPhoneNumber.error = "Invalid phone number."
return false
}
return true
}
private fun enableViews(vararg views: View) {
for (v in views) {
v.isEnabled = true
}
}
private fun disableViews(vararg views: View) {
for (v in views) {
v.isEnabled = false
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val TAG = "PhoneAuthFragment"
private const val KEY_VERIFY_IN_PROGRESS = "key_verify_in_progress"
private const val STATE_INITIALIZED = 1
private const val STATE_VERIFY_FAILED = 3
private const val STATE_VERIFY_SUCCESS = 4
private const val STATE_CODE_SENT = 2
private const val STATE_SIGNIN_FAILED = 5
private const val STATE_SIGNIN_SUCCESS = 6
}
}
================================================
FILE: auth/app/src/main/java/com/google/firebase/quickstart/auth/kotlin/TokenBroadcastReceiver.kt
================================================
package com.google.firebase.quickstart.auth.kotlin
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
/**
* Receiver to capture tokens broadcast via ADB and insert them into the
* running application to facilitate easy testing of custom authentication.
*/
abstract class TokenBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "onReceive:$intent")
if (ACTION_TOKEN == intent.action) {
val token = intent.extras?.getString(EXTRA_KEY_TOKEN)
onNewToken(token)
}
}
abstract fun onNewToken(token: String?)
companion object {
const val ACTION_TOKEN = "com.google.example.ACTION_TOKEN"
val filter: IntentFilter
get() = IntentFilter(ACTION_TOKEN)
private const val TAG = "TokenBroadcastReceiver"
const val EXTRA_KEY_TOKEN = "key_token"
}
}
================================================
FILE: auth/app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/activity_passwordless.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_anonymous_auth.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_chooser.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_custom.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_emailpassword.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_facebook.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_firebase_ui.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_generic_idp.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_google.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_multi_factor.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_multi_factor_sign_in.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_passwordless.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/fragment_phone_auth.xml
================================================
================================================
FILE: auth/app/src/main/res/layout/item_spinner_list.xml
================================================
================================================
FILE: auth/app/src/main/res/navigation/nav_graph_java.xml
================================================
================================================
FILE: auth/app/src/main/res/navigation/nav_graph_kotlin.xml
================================================
================================================
FILE: auth/app/src/main/res/values/colors.xml
================================================
#039BE5
#0288D1
#FFA000
#F5F5F5
#E0E0E0
#9E9E9E
================================================
FILE: auth/app/src/main/res/values/dimens.xml
================================================
16dp
16dp
24dp
24dp
16dp
160dp
8dp
================================================
FILE: auth/app/src/main/res/values/ids.xml
================================================
REPLACE_ME
REPLACE_ME
================================================
FILE: auth/app/src/main/res/values/strings.xml
================================================
Firebase Authentication
Firebase + Google Sign In
Use a Google Sign In credential to authenticate with Firebase.
Firebase + Facebook Login
Use a Facebook Login credential to authenticate with Firebase.
Email/Password Authentication
Use an email and password to authenticate with Firebase.
Passwordless Authentication
Use only an email to authenticate with Firebase.
Phone Number Authentication
Use a phone number to authenticate with Firebase.
Anonymous Authentication
Sign in anonymously and then later upgrade to a full Firebase Auth user.
Anonymous Sign In
Account Linking
Link Account
Custom Authentication
Use a custom token signed by your own server to authenticate with Firebase.
FirebaseUI Auth
Use the FirebaseUI-Android library to authenticate with Firebase.
Generic OAuth
Use a generic OAuth identity provider, like Apple, Microsoft, Yahoo, or Twitter to authenticate with Firebase.
Multi-Factor Authentication
Use SMS for two-factor authentication. Note: this feature is only available for Cloud Identity Platform.
User ID
Get Token
Sign In
Create Account
Send Link
Sign Out
Verify
Loading…
Custom Token
Signed In
Signed Out
Token: null
Email
Password
Firebase logo and name
Authentication failed
Start
Verify
Resend
Phone Number
Verification Code
Code Sent
Verification failed
Verification succeeded
Sign-in failed
(instant validation)
Firebase UID: %s
Firebase User Management
Google User: %s
Google Sign In
Facebook User: %s
Facebook Login
Email User: %1$s (verified: %2$b)
Email and Password
Email User: %1$s (verified: %2$b)
Passwordless Sign In
Send link to your email to get started.
Link sent, check your email to continue.
Link received, enter email to sign in.
Phone Number
Twitter User: %s
Twitter Login
Firebase User: %s
FirebaseUI Auth
OAuth Sign In
Sign In with %s
User: %s (%s)
Provider:
User ID: %s
Email: %s
Enroll MFA
Unenroll MFA
Reload
User ID: %s
Error: sign in failed
Sign in failed, see logs for details.
You are not signed in. To use this sample, first navigate to the Email/Password example to sign in and then return here to enroll in multi-factor auth.
================================================
FILE: auth/app/src/main/res/values/styles.xml
================================================
================================================
FILE: auth/app/src/main/res/values-land/dimens.xml
================================================
0dp
0dp
================================================
FILE: auth/app/src/main/res/values-v21/styles.xml
================================================
================================================
FILE: auth/app/src/main/res/values-w820dp/dimens.xml
================================================
64dp
================================================
FILE: auth/build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.google.services) apply false
}
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
}
}
tasks {
register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}
}
================================================
FILE: auth/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: auth/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
================================================
FILE: auth/gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: auth/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: auth/settings.gradle.kts
================================================
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
include(":app")
// Required so that gradle can resolve these dependencies even when
// building only a single project.
include(":internal:lintchecks")
project(":internal:lintchecks").projectDir = file("../internal/lintchecks")
include(":internal:lint")
project(":internal:lint").projectDir = file("../internal/lint")
include(":internal:chooserx")
project(":internal:chooserx").projectDir = file("../internal/chooserx")
================================================
FILE: auth/web/auth.html
================================================
Custom Token Generator Example
================================================
FILE: build.gradle.kts
================================================
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.firebase.perf) apply false
alias(libs.plugins.navigation.safeargs) apply false
alias(libs.plugins.gradle.versions) apply true
alias(libs.plugins.compose.compiler) apply false
}
allprojects {
repositories {
google()
mavenLocal()
mavenCentral()
}
}
val ktlint by configurations.creating
dependencies {
ktlint("com.pinterest:ktlint:0.49.1") {
attributes {
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL))
}
}
}
tasks.register("ktlintCheck") {
val outputDir = "${project.layout.buildDirectory}/reports/ktlint/"
val inputFiles = project.fileTree("src").include("**/*.kt")
val outputFile = "${outputDir}ktlint-checkstyle-report.xml"
// See: https://medium.com/@vanniktech/making-your-gradle-tasks-incremental-7f26e4ef09c3
inputs.files(inputFiles)
outputs.file(outputFile)
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Check Kotlin code style"
classpath = ktlint
mainClass.set("com.pinterest.ktlint.Main")
args(
"--format",
"--code-style=android_studio",
"--reporter=plain",
"--reporter=checkstyle,output=${outputFile}",
"**/*.kt",
"!**/build/**"
)
jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED")
}
fun notFromFirebase(candidate: ModuleComponentIdentifier): Boolean {
return candidate.group != "com.google.firebase"
}
fun isNonStable(candidate: ModuleComponentIdentifier): Boolean {
return listOf("alpha", "beta", "rc", "snapshot", "-m", "final").any { keyword ->
keyword in candidate.version.lowercase()
}
}
fun isBlockListed(candidate: ModuleComponentIdentifier): Boolean {
return listOf(
"androidx.browser:browser",
"androidx.webkit:webkit",
"com.facebook.android",
"com.google.guava",
"com.github.bumptech.glide"
).any { keyword ->
keyword in candidate.toString().lowercase()
}
}
tasks.withType {
rejectVersionIf {
(isNonStable(candidate) && notFromFirebase(candidate)) || isBlockListed(candidate)
}
}
tasks {
register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}
}
================================================
FILE: build_pull_request.sh
================================================
#!/bin/bash
# Exit on error
set -e
# unshallow since GitHub actions does a shallow clone
git fetch --unshallow
git fetch origin
# Get all the modules that were changed
while read line; do
module_name=${line%%/*}
if [[ ${MODULES} != *"${module_name}" ]]; then
MODULES="${MODULES} ${module_name}"
fi
done < <(git diff --name-only origin/$GITHUB_BASE_REF)
changed_modules=$MODULES
# Get a list of all available gradle tasks
AVAILABLE_TASKS=$(./gradlew tasks --all)
# Check if these modules have gradle tasks
build_commands=""
for module in $changed_modules
do
if [[ $AVAILABLE_TASKS =~ $module":app:" ]]; then
build_commands=${build_commands}" :"${module}":app:assembleDebug :"${module}":app:check"
fi
done
# Build
echo "Building Pull Request with"
echo $build_commands
eval "./gradlew clean ktlint ${build_commands}"
================================================
FILE: config/README.md
================================================
Firebase Remote Config Quickstart
==============================
The Firebase Remote Config Android quickstart app demonstrates using Remote
Config to define user-facing text in an Android app.
Introduction
------------
This is a simple example of using Remote Config to override in-app default
values by defining service-side parameter values in the Firebase console. This
example demonstrates a small subset of the capabilities of Firebase Remote
Config. To learn more about how you can use Firebase Remote Config in your app,
see
[Firebase Remote Config Introduction](https://firebase.google.com/docs/remote-config/).
Getting started
---------------
1. [Add Firebase to your Android Project](https://firebase.google.com/docs/android/setup).
2. [Create a Remote Config project for the quickstart sample](https://firebase.google.com/docs/remote-config/android#create_a_product_name_project_for_the_quickstart_sample),
defining the parameter values and parameter keys used by the sample.
3. Run the sample on an Android device or emulator.
4. Change one or more parameter values in the Firebase Console (the value of
`welcome_message`, `welcome_message_caps`, or both).
5. Tap **Fetch Remote Config** in the app to fetch new parameter values and see
the resulting change in the app.
Best practices
--------------
This section provides some additional information about how the quickstart
example sets in-app default parameter values and fetches values from the Remote
Config service
### In-app default parameter values ###
In-app default values are set using an XML file in this example, but you can
also set in-app default values inline using other `setDefault` methods of the
[`FirebaseRemoteConfig` class](https://firebase.google.com/docs/reference/android/com/google/firebase/remoteconfig/FirebaseRemoteConfig#public-method-summary).
Then, you can override only those values that you need to change from the
Firebase console. This lets you use Remote Config for any default value that you
might want to override in the future, without the need to set all of those
values in the Firebase console.
### Fetch values from the Remote Config service ###
When an app calls `fetch`, locally stored parameter values are used unless the
minimum fetch interval is reached. The minimal fetch interval is determined by:
1. The parameter passed to `fetch(long minFetchInterval)`.
2. The minimum fetch interval set in Remote Config settings.
3. The default minimum fetch interval, 12 hours.
Fetched values are immediately activated when retrieved using `fetchAndActivate`.
`fetchAndActivate` returns true if the final set of key/value pairs now available
to the application is different to the set before calling `fetchAndActivate`, false
is returned otherwise. In the quickstart sample app, you call `fetchAndActivate`
from the UI by tapping **Fetch Remote Config**.
To control when fetched values are activated and available to your app use `fetch`, the
values are locally stored, but not immediately activated. To activate
fetched values so that they take effect, call the `activate` method.
You can also create a Remote Config Setting to enable developer mode, but you
must remove this setting before distributing your app. Fetching Remote Config
data from the service is normally limited to a few requests per hour. By
enabling developer mode, you can make many more requests per hour, so you can
test your app with different Remote Config parameter values during development.
- To learn more about fetching data from remote config, see the Remote Config
Frequently Asked Question (FAQ) on
[fetching and activating parameter values](https://firebase.google.com/support/faq#remote-config-values).
- To learn about parameters and conditions that you can use to change the
behavior and appearance of your app for segments of your userbase, see
[Remote Config Parameters and Conditions](https://firebase.google.com/docs/remote-config/parameters).
- To learn more about the Remote Config API, see
[Remote Config API Overview](https://firebase.google.com/docs/remote-config/api-overview).
Result
-----------
Support
-------
- [Stack Overflow](https://stackoverflow.com/questions/tagged/firebase-remote-config)
- [Firebase Support](https://firebase.google.com/support/)
License
-------
Copyright 2016 Google, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
================================================
FILE: config/app/build.gradle.kts
================================================
import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.google.services)
}
tasks {
check.dependsOn("assembleDebugAndroidTest")
}
android {
namespace = "com.google.samples.quickstart.config"
compileSdk = 36
defaultConfig {
applicationId = "com.google.samples.quickstart.config"
minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
multiDexEnabled = true
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
implementation("com.google.android.material:material:1.13.0")
// Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom)
implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Firebase Remote Config
implementation("com.google.firebase:firebase-config")
// For an optimal experience using Remote Config, add the Firebase SDK
// for Google Analytics. This is recommended, but not required.
implementation("com.google.firebase:firebase-analytics")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
}
================================================
FILE: config/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keepattributes EnclosingMethod
-keepattributes InnerClasses
================================================
FILE: config/app/src/androidTest/java/com/google/samples/quickstart/config/MainActivityTest.java
================================================
package com.google.samples.quickstart.config;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import androidx.test.filters.LargeTest;
import com.google.samples.quickstart.config.java.MainActivity;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.Matchers.is;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class);
@Test
public void testFetchConfig() {
// Click fetch config button
onView(withText(R.string.fetch_remote_welcome_message))
.check(matches(isDisplayed()))
.perform(click());
// Watch for success Toast
onView(withText(startsWith("Fetch Succeeded")))
.inRoot(withDecorView(not(is(rule.getActivity().getWindow().getDecorView()))))
.check(matches(isDisplayed()));
}
}
================================================
FILE: config/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: config/app/src/main/java/com/google/samples/quickstart/config/EntryChoiceActivity.kt
================================================
package com.google.samples.quickstart.config
import android.content.Intent
import com.firebase.example.internal.BaseEntryChoiceActivity
import com.firebase.example.internal.Choice
class EntryChoiceActivity : BaseEntryChoiceActivity() {
override fun getChoices(): List {
return listOf(
Choice(
"Java",
"Run the Firebase Remote Config quickstart written in Java.",
Intent(
this,
com.google.samples.quickstart.config.java.MainActivity::class.java,
),
),
Choice(
"Kotlin",
"Run the Firebase Remote Config quickstart written in Kotlin.",
Intent(
this,
com.google.samples.quickstart.config.kotlin.MainActivity::class.java,
),
),
)
}
}
================================================
FILE: config/app/src/main/java/com/google/samples/quickstart/config/java/MainActivity.java
================================================
/*
* Copyright Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* For more information on setting up and running this sample code, see
* https://firebase.google.com/docs/remote-config/android
*/
package com.google.samples.quickstart.config.java;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.remoteconfig.FirebaseRemoteConfig;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException;
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings;
import com.google.samples.quickstart.config.R;
import com.google.samples.quickstart.config.databinding.ActivityMainBinding;
import com.google.firebase.remoteconfig.ConfigUpdateListener;
import com.google.firebase.remoteconfig.ConfigUpdate;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// Remote Config keys
private static final String LOADING_PHRASE_CONFIG_KEY = "loading_phrase";
private static final String WELCOME_MESSAGE_KEY = "welcome_message";
private static final String WELCOME_MESSAGE_CAPS_KEY = "welcome_message_caps";
private FirebaseRemoteConfig mFirebaseRemoteConfig;
private ActivityMainBinding mBinding;
private TextView mWelcomeTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(mBinding.getRoot());
mWelcomeTextView = mBinding.welcomeTextView;
mBinding.fetchButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
fetchWelcome();
}
});
// Get Remote Config instance.
// [START get_remote_config_instance]
mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();
// [END get_remote_config_instance]
// Create a Remote Config Setting to enable developer mode, which you can use to increase
// the number of fetches available per hour during development. Also use Remote Config
// Setting to set the minimum fetch interval.
// [START enable_dev_mode]
FirebaseRemoteConfigSettings configSettings = new FirebaseRemoteConfigSettings.Builder()
.setMinimumFetchIntervalInSeconds(3600)
.build();
mFirebaseRemoteConfig.setConfigSettingsAsync(configSettings);
// [END enable_dev_mode]
// Set default Remote Config parameter values. An app uses the in-app default values, and
// when you need to adjust those defaults, you set an updated value for only the values you
// want to change in the Firebase console. See Best Practices in the README for more
// information.
// [START set_default_values]
mFirebaseRemoteConfig.setDefaultsAsync(R.xml.remote_config_defaults);
// [END set_default_values]
// [START add_config_update_listener]
mFirebaseRemoteConfig.addOnConfigUpdateListener(new ConfigUpdateListener() {
@Override
public void onUpdate(ConfigUpdate configUpdate) {
Log.d(TAG, "Updated keys: " + configUpdate.getUpdatedKeys());
if (configUpdate.getUpdatedKeys().contains(WELCOME_MESSAGE_KEY)) {
mFirebaseRemoteConfig.activate().addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
displayWelcomeMessage();
}
});
}
}
@Override
public void onError(FirebaseRemoteConfigException error) {
Log.w(TAG, "Config update error with code: " + error.getCode(), error);
}
});
// [END add_config_update_listener]
fetchWelcome();
}
/**
* Fetch a welcome message from the Remote Config service, and then activate it.
*/
private void fetchWelcome() {
mWelcomeTextView.setText(mFirebaseRemoteConfig.getString(LOADING_PHRASE_CONFIG_KEY));
// [START fetch_config_with_callback]
mFirebaseRemoteConfig.fetchAndActivate()
.addOnCompleteListener(this, new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
boolean updated = task.getResult();
Log.d(TAG, "Config params updated: " + updated);
Toast.makeText(MainActivity.this, "Fetch and activate succeeded",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "Fetch failed",
Toast.LENGTH_SHORT).show();
}
displayWelcomeMessage();
}
});
// [END fetch_config_with_callback]
}
/**
* Display a welcome message in all caps if welcome_message_caps is set to true. Otherwise,
* display a welcome message as fetched from welcome_message.
*/
// [START display_welcome_message]
private void displayWelcomeMessage() {
// [START get_config_values]
String welcomeMessage = mFirebaseRemoteConfig.getString(WELCOME_MESSAGE_KEY);
// [END get_config_values]
if (mFirebaseRemoteConfig.getBoolean(WELCOME_MESSAGE_CAPS_KEY)) {
mWelcomeTextView.setAllCaps(true);
} else {
mWelcomeTextView.setAllCaps(false);
}
mWelcomeTextView.setText(welcomeMessage);
}
// [END display_welcome_message]
}
================================================
FILE: config/app/src/main/java/com/google/samples/quickstart/config/kotlin/MainActivity.kt
================================================
package com.google.samples.quickstart.config.kotlin
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.Firebase
import com.google.firebase.remoteconfig.ConfigUpdate
import com.google.firebase.remoteconfig.ConfigUpdateListener
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
import com.google.firebase.remoteconfig.get
import com.google.firebase.remoteconfig.remoteConfig
import com.google.firebase.remoteconfig.remoteConfigSettings
import com.google.samples.quickstart.config.R
import com.google.samples.quickstart.config.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var remoteConfig: FirebaseRemoteConfig
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.fetchButton.setOnClickListener { fetchWelcome() }
// Get Remote Config instance.
// [START get_remote_config_instance]
remoteConfig = Firebase.remoteConfig
// [END get_remote_config_instance]
// Create a Remote Config Setting to enable developer mode, which you can use to increase
// the number of fetches available per hour during development. Also use Remote Config
// Setting to set the minimum fetch interval.
// [START enable_dev_mode]
val configSettings = remoteConfigSettings {
minimumFetchIntervalInSeconds = 3600
}
remoteConfig.setConfigSettingsAsync(configSettings)
// [END enable_dev_mode]
// Set default Remote Config parameter values. An app uses the in-app default values, and
// when you need to adjust those defaults, you set an updated value for only the values you
// want to change in the Firebase console. See Best Practices in the README for more
// information.
// [START set_default_values]
remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
// [END set_default_values]
// [START add_config_update_listener]
remoteConfig.addOnConfigUpdateListener(object : ConfigUpdateListener {
override fun onUpdate(configUpdate: ConfigUpdate) {
Log.d(TAG, "Updated keys: " + configUpdate.updatedKeys)
if (configUpdate.updatedKeys.contains(WELCOME_MESSAGE_KEY)) {
remoteConfig.activate().addOnCompleteListener {
displayWelcomeMessage()
}
}
}
override fun onError(error: FirebaseRemoteConfigException) {
Log.w(TAG, "Config update error with code: " + error.code, error)
}
})
// [END add_config_update_listener]
fetchWelcome()
}
/**
* Fetch a welcome message from the Remote Config service, and then activate it.
*/
private fun fetchWelcome() {
binding.welcomeTextView.text = remoteConfig[LOADING_PHRASE_CONFIG_KEY].asString()
// [START fetch_config_with_callback]
remoteConfig.fetchAndActivate()
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
val updated = task.result
Log.d(TAG, "Config params updated: $updated")
Toast.makeText(
this,
"Fetch and activate succeeded",
Toast.LENGTH_SHORT,
).show()
} else {
Toast.makeText(
this,
"Fetch failed",
Toast.LENGTH_SHORT,
).show()
}
displayWelcomeMessage()
}
// [END fetch_config_with_callback]
}
/**
* Display a welcome message in all caps if welcome_message_caps is set to true. Otherwise,
* display a welcome message as fetched from welcome_message.
*/
// [START display_welcome_message]
private fun displayWelcomeMessage() {
// [START get_config_values]
val welcomeMessage = remoteConfig[WELCOME_MESSAGE_KEY].asString()
// [END get_config_values]
binding.welcomeTextView.isAllCaps = remoteConfig[WELCOME_MESSAGE_CAPS_KEY].asBoolean()
binding.welcomeTextView.text = welcomeMessage
}
companion object {
private const val TAG = "MainActivity"
// Remote Config keys
private const val LOADING_PHRASE_CONFIG_KEY = "loading_phrase"
private const val WELCOME_MESSAGE_KEY = "welcome_message"
private const val WELCOME_MESSAGE_CAPS_KEY = "welcome_message_caps"
}
// [END display_welcome_message]
}
================================================
FILE: config/app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: config/app/src/main/res/values/colors.xml
================================================
#039BE5
#0288D1
#FFA000
================================================
FILE: config/app/src/main/res/values/dimens.xml
================================================
16dp
16dp
================================================
FILE: config/app/src/main/res/values/strings.xml
================================================
Firebase Remote Config
fetch remote welcome
================================================
FILE: config/app/src/main/res/values/styles.xml
================================================
================================================
FILE: config/app/src/main/res/values-w820dp/dimens.xml
================================================
64dp
================================================
FILE: config/app/src/main/res/xml/remote_config_defaults.xml
================================================
loading_phrase
Fetching config…
welcome_message_caps
false
welcome_message
Welcome to my awesome app!
================================================
FILE: config/build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.google.services) apply false
}
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
}
}
================================================
FILE: config/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: config/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
================================================
FILE: config/gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: config/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: config/settings.gradle.kts
================================================
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
include(":app")
// Required so that gradle can resolve these dependencies even when
// building only a single project.
include(":internal:lintchecks")
project(":internal:lintchecks").projectDir = file("../internal/lintchecks")
include(":internal:lint")
project(":internal:lint").projectDir = file("../internal/lint")
include(":internal:chooserx")
project(":internal:chooserx").projectDir = file("../internal/chooserx")
================================================
FILE: copy_mock_google_services_json.sh
================================================
#!/bin/bash
# Exit on error
set -e
# Copy mock google-services file
echo "Using mock google-services.json"
cp mock-google-services.json admob/app/google-services.json
cp mock-google-services.json analytics/app/google-services.json
cp mock-google-services.json appdistribution/app/google-services.json
cp mock-google-services.json auth/app/google-services.json
cp mock-google-services.json config/app/google-services.json
cp mock-google-services.json crash/app/google-services.json
cp mock-google-services.json database/app/google-services.json
cp mock-google-services.json dataconnect/app/google-services.json
cp mock-google-services.json firebase-ai/app/google-services.json
cp mock-google-services.json firestore/app/google-services.json
cp mock-google-services.json functions/app/google-services.json
cp mock-google-services.json inappmessaging/app/google-services.json
cp mock-google-services.json perf/app/google-services.json
cp mock-google-services.json messaging/app/google-services.json
cp mock-google-services.json storage/app/google-services.json
================================================
FILE: crash/.gitignore
================================================
*.iml
google-services.json
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
================================================
FILE: crash/README.md
================================================
Firebase Crashlytics Quickstart
===============================
Introduction
------------
- [Read more about Firebase Crashlytics](https://firebase.google.com/docs/crashlytics)
Getting Started
---------------
- [Add Firebase to your Android Project](https://firebase.google.com/docs/android/setup).
- Run the sample on Android device or emulator.
Screenshots
-----------
Support
-------
- [Stack Overflow](https://stackoverflow.com/questions/tagged/crashlytics)
- [Firebase Support](https://firebase.google.com/support/)
License
-------
Copyright 2016 Google, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
================================================
FILE: crash/app/.gitignore
================================================
/build
================================================
FILE: crash/app/build.gradle.kts
================================================
import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.google.services)
alias(libs.plugins.firebase.crashlytics)
}
tasks {
check.dependsOn("assembleDebugAndroidTest")
}
android {
namespace = "com.google.samples.quickstart.crash"
compileSdk = 36
defaultConfig {
applicationId = "com.google.samples.quickstart.crash"
minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled = true
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
getByName("debug") {
isMinifyEnabled = false
testProguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "test-proguard-rules.pro")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.activity:activity-ktx:1.12.1")
// Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom)
implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Firebase Crashlytics
implementation("com.google.firebase:firebase-crashlytics")
// For an optimal experience using Crashlytics, add the Firebase SDK
// for Google Analytics. This is recommended, but not required.
implementation("com.google.firebase:firebase-analytics")
// For use in the CustomKeySamples -- for testing Google Api Availability.
implementation("com.google.android.gms:play-services-base:18.5.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
}
================================================
FILE: crash/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keepattributes EnclosingMethod
-keepattributes InnerClasses
-dontwarn org.xmlpull.v1.**
-dontnote org.xmlpull.v1.**
-keep class org.xmlpull.** { *; }
-keepclassmembers class org.xmlpull.** { *; }
================================================
FILE: crash/app/src/androidTest/java/com/google/samples/quickstart/crash/MainActivityTest.java
================================================
package com.google.samples.quickstart.crash;
import androidx.test.espresso.ViewInteraction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import androidx.test.filters.LargeTest;
import android.widget.CheckBox;
import com.google.samples.quickstart.crash.java.MainActivity;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void caughtExceptionTest() {
// Make sure the checkbox is on screen
ViewInteraction al = onView(
allOf(withId(R.id.catchCrashCheckBox),
withText(R.string.catch_crash_checkbox_label),
isDisplayed()));
// Click the checkbox if it's not already checked
CheckBox checkBox = (CheckBox) mActivityTestRule.getActivity()
.findViewById(R.id.catchCrashCheckBox);
if (!checkBox.isChecked()) {
al.perform(click());
}
// Cause a crash
ViewInteraction ak = onView(
allOf(withId(R.id.crashButton),
withText(R.string.crash_button_label),
isDisplayed()));
ak.perform(click());
}
}
================================================
FILE: crash/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: crash/app/src/main/java/com/google/samples/quickstart/crash/EntryChoiceActivity.kt
================================================
package com.google.samples.quickstart.crash
import android.content.Intent
import com.firebase.example.internal.BaseEntryChoiceActivity
import com.firebase.example.internal.Choice
class EntryChoiceActivity : BaseEntryChoiceActivity() {
override fun getChoices(): List {
return listOf(
Choice(
"Java",
"Run the Firebase Crash quickstart written in Java.",
Intent(this, com.google.samples.quickstart.crash.java.MainActivity::class.java),
),
Choice(
"Kotlin",
"Run the Firebase Crash quickstart written in Kotlin.",
Intent(this, com.google.samples.quickstart.crash.kotlin.MainActivity::class.java),
),
)
}
}
================================================
FILE: crash/app/src/main/java/com/google/samples/quickstart/crash/java/CustomKeySamples.java
================================================
package com.google.samples.quickstart.crash.java;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.InstallSourceInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.telephony.TelephonyManager;
import android.view.View;
import androidx.core.content.ContextCompat;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.firebase.crashlytics.CustomKeysAndValues;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
/**
* The following are samples of custom keys that may be useful to record via Crashlytics prior to a Crash.
*
* These utility methods are not meant to be comprehensive, but they are illustrative of the types of things you
* could track using custom keys in Crashlytics.
*/
public class CustomKeySamples {
private final Context context;
// Lazily instantiated when a listener is set up.
private ConnectivityManager.NetworkCallback callback = null;
public CustomKeySamples(Context context) {
this.context = context;
}
/**
* Set a subset of custom keys simultaneously.
*/
public void setSampleCustomKeys() {
FirebaseCrashlytics.getInstance().setCustomKeys(new CustomKeysAndValues.Builder()
.putString("Locale", getLocale())
.putFloat("Screen Density", getDensity())
.putString("Google Play Services Availability", getGooglePlayServicesAvailability())
.putString("Os Version", getOsVersion())
.putString("Install Source", getInstallSource())
.putString("Preferred ABI", getPreferredAbi()).build());
}
/**
* Update network state and add a hook to update network state going forward.
*
* Note: This code is executed above API level N.
*/
public void updateAndTrackNetworkState() {
ConnectivityManager connectivityManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
NetworkCapabilities networkCapabilities = connectivityManager
.getNetworkCapabilities(connectivityManager.getActiveNetwork());
if (networkCapabilities != null) {
updateNetworkCapabilityCustomKeys(networkCapabilities);
}
synchronized(this) {
if (callback == null) {
// Set up a callback to match our best-practices around custom keys being up-to-date
callback = new ConnectivityManager.NetworkCallback() {
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
updateNetworkCapabilityCustomKeys(networkCapabilities);
}
};
connectivityManager.registerDefaultNetworkCallback(callback);
}
}
}
}
/**
* Remove the handler for the network state.
*
* Note: This code is executed above API level N.
*/
public void stopTrackingNetworkState() {
ConnectivityManager connectivityManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
synchronized(this) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && callback != null) {
connectivityManager.unregisterNetworkCallback(callback);
callback = null;
}
}
}
private void updateNetworkCapabilityCustomKeys(NetworkCapabilities networkCapabilities) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
FirebaseCrashlytics.getInstance().setCustomKeys(new CustomKeysAndValues.Builder()
.putInt("Network Bandwidth", networkCapabilities.getLinkDownstreamBandwidthKbps())
.putInt("Network Upstream", networkCapabilities.getLinkUpstreamBandwidthKbps())
.putBoolean("Network Metered", networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED))
// This key is long and not as easy to filter by.
.putString("Network Capabilities", networkCapabilities.toString()).build());
}
}
/**
* @see {@link com.google.samples.quickstart.crash.java.CustomKeySamples#updateAndTrackNetworkState},
* which does not require READ_PHONE_STATE
* and returns more useful information about bandwidth, metering, and capabilities.
*
* Supressed deprecation warning because that code path is only used below API Level N.
*/
@Deprecated
@SuppressLint("MissingPermission")
public void addPhoneStateRequiredNetworkKeys() {
TelephonyManager telephonyManager = ((TelephonyManager) context
.getSystemService(Context.TELEPHONY_SERVICE));
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) !=
PackageManager.PERMISSION_GRANTED) {
return;
}
int networkType;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
networkType = telephonyManager.getDataNetworkType();
} else {
networkType = telephonyManager.getNetworkType();
}
String simOperatorId = telephonyManager.getSimOperator();
FirebaseCrashlytics.getInstance().setCustomKey("Network Type", networkType);
FirebaseCrashlytics.getInstance().setCustomKey("Sim Operator", simOperatorId);
}
/**
* Retrieve the locale information for the app.
*
* Supressed deprecation warning because that code path is only used below API Level N.
*/
@SuppressWarnings("deprecation")
public String getLocale() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return context
.getResources()
.getConfiguration()
.getLocales().get(0).toString();
}
return context
.getResources()
.getConfiguration().locale.toString();
}
/**
* Retrieve the screen density information for the app.
*/
public float getDensity() {
return context
.getResources()
.getDisplayMetrics()
.density;
}
/**
* Retrieve the locale information for the app.
*/
public String getGooglePlayServicesAvailability() {
return GoogleApiAvailability
.getInstance()
.isGooglePlayServicesAvailable(context) == 0 ? "Unavailable" : "Available";
}
/**
* Return the underlying kernel version of the Android device.
*/
public String getOsVersion() {
String osVersion = System.getProperty("os.version");
return osVersion != null ? osVersion : "Unknown";
}
/**
* Retrieve the preferred ABI of the device. Some devices can support
* multiple ABIs and the first one returned in the preferred one.
*
* Supressed deprecation warning because that code path is only used below Lollipop.
*/
@SuppressWarnings("deprecation")
public String getPreferredAbi() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return Build.SUPPORTED_ABIS[0];
}
return Build.CPU_ABI;
}
/**
* Retrieve the install source and return it as a string.
*
* Supressed deprecation warning because that code path is only used below API level R.
*/
public String getInstallSource() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
InstallSourceInfo info = context
.getPackageManager()
.getInstallSourceInfo(context.getPackageName());
String originating = info.getOriginatingPackageName() == null ? "None" :
info.getOriginatingPackageName();
String installing = info.getInstallingPackageName() == null ? "None" : info.getInstallingPackageName();
String initiating = info.getInitiatingPackageName() == null ? "None" : info.getInitiatingPackageName();
// This returns all three of the install source, originating source, and initiating
// source.
return "Originating: " + originating +
", Installing: " + installing +
", Initiating: " + initiating;
} catch (PackageManager.NameNotFoundException e) {
return "Unknown";
}
}
String installerPackageName = context
.getPackageManager()
.getInstallerPackageName(context.getPackageName());
return installerPackageName == null ? "None" : installerPackageName;
}
/**
* Add a focus listener that updates a custom key when this view gains the focus.
**/
public void focusListener(View view, boolean hasFocus) {
if (hasFocus) {
FirebaseCrashlytics.getInstance().setCustomKey("view_focus", view.getId());
}
}
}
================================================
FILE: crash/app/src/main/java/com/google/samples/quickstart/crash/java/MainActivity.java
================================================
/*
* Copyright Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.quickstart.crash.java;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import com.google.samples.quickstart.crash.databinding.ActivityMainBinding;
/**
* This Activity shows the different ways of reporting application crashes.
* - Report non-fatal exceptions that are caught by your app.
* - Automatically Report uncaught crashes.
*
* It also shows how to add log messages to crash reports using log().
*
* Check https://console.firebase.google.com to view and analyze your crash reports.
*
* Check https://firebase.google.com/docs/crashlytics for more information on Firebase Crashlytics.
*/
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private FirebaseCrashlytics mCrashlytics;
private CustomKeySamples samples;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
this.samples = new CustomKeySamples(this.getApplicationContext());
samples.setSampleCustomKeys();
samples.updateAndTrackNetworkState();
mCrashlytics = FirebaseCrashlytics.getInstance();
// Log the onCreate event, this will also be printed in logcat
mCrashlytics.log("onCreate");
// Add some custom values and identifiers to be included in crash reports
mCrashlytics.setCustomKey("MeaningOfLife", 42);
mCrashlytics.setCustomKey("LastUIAction", "Test value");
mCrashlytics.setUserId("123456789");
// Report a non-fatal exception, for demonstration purposes
mCrashlytics.recordException(new Exception("Non-fatal exception: something went wrong!"));
// Button that causes NullPointerException to be thrown.
binding.crashButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Log that crash button was clicked.
mCrashlytics.log("Crash button clicked.");
// If catchCrashCheckBox is checked catch the exception and report it using
// logException(), Otherwise throw the exception and let Crashlytics automatically
// report the crash.
if (binding.catchCrashCheckBox.isChecked()) {
try {
throw new NullPointerException();
} catch (NullPointerException ex) {
// [START crashlytics_log_and_report]
mCrashlytics.log("NPE caught!");
mCrashlytics.recordException(ex);
// [END crashlytics_log_and_report]
}
} else {
throw new NullPointerException();
}
}
});
// Log that the Activity was created.
// [START crashlytics_log_event]
mCrashlytics.log("Activity created");
// [END crashlytics_log_event]
}
@Override
public void onDestroy() {
super.onDestroy();
samples.stopTrackingNetworkState();
}
}
================================================
FILE: crash/app/src/main/java/com/google/samples/quickstart/crash/kotlin/CustomKeySamples.kt
================================================
package com.google.samples.quickstart.crash.kotlin
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
import android.telephony.TelephonyManager
import android.view.View
import androidx.core.content.ContextCompat
import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.Firebase
import kotlinx.coroutines.internal.synchronized
/**
* The following are samples of custom keys that may be useful to record via Crashlytics prior to a Crash.
*
* These utility methods are not meant to be comprehensive, but they are illustrative of the types of things you
* could track using custom keys in Crashlytics.
*/
class CustomKeySamples(private val context: Context, private var callback: NetworkCallback? = null) {
/**
* Set a subset of custom keys simultaneously.
*/
fun setSampleCustomKeys() {
Firebase.crashlytics.setCustomKeys {
key("Locale", locale)
key("Screen Density", density)
key("Google Play Services Availability", googlePlayServicesAvailability)
key("Os Version", osVersion)
key("Install Source", installSource)
key("Preferred ABI", preferredAbi)
}
}
/**
* Update network state and add a hook to update network state going forward.
*
* Note: This code is executed above API level N.
*/
fun updateAndTrackNetworkState() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager
.getNetworkCapabilities(connectivityManager.activeNetwork)
?.let { updateNetworkCapabilityCustomKeys(it) }
kotlin.synchronized(this) {
if (callback == null) {
// Set up a callback to match our best-practices around custom keys being up-to-date
val newCallback: NetworkCallback = object : NetworkCallback() {
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
updateNetworkCapabilityCustomKeys(networkCapabilities)
}
}
callback = newCallback
connectivityManager.registerDefaultNetworkCallback(newCallback)
}
}
}
}
/**
* Remove the handler for the network state.
*
* Note: This code is executed above API level N.
*/
fun stopTrackingNetworkState() {
val connectivityManager = context
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val oldCallback = callback
kotlin.synchronized(this) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && oldCallback != null) {
connectivityManager.unregisterNetworkCallback(oldCallback)
callback = null
}
}
}
private fun updateNetworkCapabilityCustomKeys(networkCapabilities: NetworkCapabilities) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Firebase.crashlytics.setCustomKeys {
key("Network Bandwidth", networkCapabilities.linkDownstreamBandwidthKbps)
key("Network Upstream", networkCapabilities.linkUpstreamBandwidthKbps)
key(
"Network Metered",
networkCapabilities
.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED),
)
// This key is long and not as easy to filter by.
key("Network Capabilities", networkCapabilities.toString())
}
}
}
/**
* @see {@link com.google.samples.quickstart.crash.java.CustomKeySamples.updateAndTrackNetworkState}, which does not require READ_PHONE_STATE
* and returns more useful information about bandwidth, metering, and capabilities.
*
* Supressed deprecation warning because that code path is only used below API Level N.
*
* Supressed Lint warning because READ_PHONE_STATE is a high priority permission and
* we don't want to enforce needing it for this code example.
*/
@Suppress("DEPRECATION")
@Deprecated("Prefer updateAndTrackNetworkState, which does not require READ_PHONE_STATE")
@SuppressLint("MissingPermission")
fun addPhoneStateRequiredNetworkKeys() {
val telephonyManager = context
.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE)
!= PackageManager.PERMISSION_GRANTED
) {
return
}
val networkType: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
telephonyManager.dataNetworkType
} else {
telephonyManager.networkType
}
Firebase.crashlytics.setCustomKeys {
key("Network Type", networkType)
key("Sim Operator", telephonyManager.simOperator)
}
}
/**
* Retrieve the locale information for the app.
*
* Supressed deprecation warning because that code path is only used below API Level N.
*/
@Suppress("DEPRECATION")
val locale: String
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context
.resources
.configuration
.locales[0].toString()
} else {
context
.resources
.configuration.locale.toString()
}
/**
* Retrieve the screen density information for the app.
*/
val density: Float
get() = context
.resources
.displayMetrics.density
/**
* Retrieve the locale information for the app.
*/
val googlePlayServicesAvailability: String
get() = if (GoogleApiAvailability
.getInstance()
.isGooglePlayServicesAvailable(context) == 0
) {
"Unavailable"
} else {
"Available"
}
/**
* Return the underlying kernel version of the Android device.
*/
val osVersion: String
get() = System.getProperty("os.version") ?: "Unknown"
/**
* Retrieve the preferred ABI of the device. Some devices can support
* multiple ABIs and the first one returned in the preferred one.
*
* Supressed deprecation warning because that code path is only used below Lollipop.
*/
@Suppress("DEPRECATION")
val preferredAbi: String
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Build.SUPPORTED_ABIS[0]
} else Build.CPU_ABI
/**
* Retrieve the install source and return it as a string.
*
* Supressed deprecation warning because that code path is only used below API level R.
*/
@Suppress("DEPRECATION")
val installSource: String
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
val info = context
.packageManager
.getInstallSourceInfo(context.packageName)
// This returns all three of the install source, originating source, and initiating
// source.
"Originating: ${info.originatingPackageName ?: "None"}, " +
"Installing: ${info.installingPackageName ?: "None"}, " +
"Initiating: ${info.initiatingPackageName ?: "None"}"
} catch (e: PackageManager.NameNotFoundException) {
"Unknown"
}
} else {
context.packageManager.getInstallerPackageName(context.packageName) ?: "None"
}
/**
* Add a focus listener that updates a custom key when this view gains the focus.
*/
fun focusListener(view: View, hasFocus: Boolean) {
if (hasFocus) {
Firebase.crashlytics.setCustomKey("view_focus", view.id)
}
}
}
================================================
FILE: crash/app/src/main/java/com/google/samples/quickstart/crash/kotlin/MainActivity.kt
================================================
package com.google.samples.quickstart.crash.kotlin
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.crashlytics
import com.google.firebase.crashlytics.setCustomKeys
import com.google.firebase.Firebase
import com.google.samples.quickstart.crash.databinding.ActivityMainBinding
/**
* This Activity shows the different ways of reporting application crashes.
* - Report non-fatal exceptions that are caught by your app.
* - Automatically Report uncaught crashes.
*
* It also shows how to add log messages to crash reports using log().
*
* Check https://console.firebase.google.com to view and analyze your crash reports.
*
* Check https://firebase.google.com/docs/crashlytics for more information on Firebase Crashlytics.
*/
class MainActivity : AppCompatActivity() {
private lateinit var crashlytics: FirebaseCrashlytics
private lateinit var customKeySamples: CustomKeySamples
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
customKeySamples = CustomKeySamples(applicationContext)
customKeySamples.setSampleCustomKeys()
customKeySamples.updateAndTrackNetworkState()
crashlytics = Firebase.crashlytics
// Log the onCreate event, this will also be printed in logcat
crashlytics.log("onCreate")
// Add some custom values and identifiers to be included in crash reports
crashlytics.setCustomKeys {
key("MeaningOfLife", 42)
key("LastUIAction", "Test value")
}
crashlytics.setUserId("123456789")
// Report a non-fatal exception, for demonstration purposes
crashlytics.recordException(Exception("Non-fatal exception: something went wrong!"))
// Button that causes NullPointerException to be thrown.
binding.crashButton.setOnClickListener {
// Log that crash button was clicked.
crashlytics.log("Crash button clicked.")
// If catchCrashCheckBox is checked catch the exception and report it using
// logException(), Otherwise throw the exception and let Crashlytics automatically
// report the crash.
if (binding.catchCrashCheckBox.isChecked) {
try {
throw NullPointerException()
} catch (ex: NullPointerException) {
// [START crashlytics_log_and_report]
crashlytics.log("NPE caught!")
crashlytics.recordException(ex)
// [END crashlytics_log_and_report]
}
} else {
throw NullPointerException()
}
}
// Log that the Activity was created.
// [START crashlytics_log_event]
crashlytics.log("Activity created")
// [END crashlytics_log_event]
}
override fun onDestroy() {
super.onDestroy()
customKeySamples.stopTrackingNetworkState()
}
companion object {
private const val TAG = "MainActivity"
}
}
================================================
FILE: crash/app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: crash/app/src/main/res/values/colors.xml
================================================
#039BE5
#0288D1
#FFA000
================================================
FILE: crash/app/src/main/res/values/dimens.xml
================================================
16dp
16dp
================================================
FILE: crash/app/src/main/res/values/strings.xml
================================================
Firebase Crashlytics
Cause Crash
Catch Crash
================================================
FILE: crash/app/src/main/res/values/styles.xml
================================================
================================================
FILE: crash/app/src/main/res/values-w820dp/dimens.xml
================================================
64dp
================================================
FILE: crash/app/test-proguard-rules.pro
================================================
-dontobfuscate
-dontwarn
-dontwarn org.xmlpull.v1.**
-dontnote org.xmlpull.v1.**
-keep class org.xmlpull.** { *; }
-keepclassmembers class org.xmlpull.** { *; }
================================================
FILE: crash/build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.firebase.crashlytics) apply false
}
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
}
}
tasks {
register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}
}
================================================
FILE: crash/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: crash/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
================================================
FILE: crash/gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: crash/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: crash/settings.gradle.kts
================================================
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
include(":app")
// Required so that gradle can resolve these dependencies even when
// building only a single project.
include(":internal:lintchecks")
project(":internal:lintchecks").projectDir = file("../internal/lintchecks")
include(":internal:lint")
project(":internal:lint").projectDir = file("../internal/lint")
include(":internal:chooserx")
project(":internal:chooserx").projectDir = file("../internal/chooserx")
================================================
FILE: database/.gitignore
================================================
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
================================================
FILE: database/README.md
================================================
Firebase Database Quickstart
=============================
Introduction
------------
- [Read more about Firebase Database](https://firebase.google.com/docs/database)
Getting Started
---------------
- [Add Firebase to your Android Project](https://firebase.google.com/docs/android/setup).
- Log in to the [Firebase Console](https://console.firebase.google.com).
- Go to **Auth** tab and enable **Email/Password** authentication.
- Run the sample on Android device or emulator.
Result
-----------
Data Model
-----------
This quickstart demonstrates a simple data model for a social application.
While this data model uses some of the Firebase best practices, it has some
known tradeoffs made for simplicity that would not scale to very large numbers
of users.
The database has four "root" nodes:
* `users` - a list of `User` objects, keyed by user ID. So
`/users//email` is the email address of the user with id=``.
* `posts` - a list of `Post` objects, keyed by randomly generated push ID.
Each `Post` contains the `uid` and `author` properties to determine the
identity of the author without a JOIN-style query.
* Posts contain a `stars` property which is a `Map` of user IDs to boolean
values. If `/posts//stars/` is `true`, this means
the user with ID `` has starred the post with ID ``.
This data nesting makes it easy to tell if a specific user has already
starred a specific post, but would not scale to large numbers of stars
per post as it would make loading the Post data more expensive.
* `user-posts` - a list of posts by the user. `/user-posts/` is a list
of all posts made by a specific user, keyed by the same push ID used in
the `posts` tree. This makes it easy to query "all posts by a specific
user" without filtering through all Post objects.
* `post-comments` - comments on a particular posts, where
`/post-comments/` is a list of all comments on post with id
``. Each comment has a randomly generated push key. By keeping
this data in its own tree rather than nesting it under `posts`, we make it
possible to load a post without loading all comments while still
having a known path to access all comments for a particular post.
Database Rules
---------------
Below are some samples rules that limit access and validate data:
```javascript
{
"rules": {
// User profiles are only readable/writable by the user who owns it
"users": {
"$UID": {
".read": "auth.uid == $UID",
".write": "auth.uid == $UID"
}
},
// Posts can be read by anyone but only written by logged-in users.
"posts": {
".read": true,
".write": "auth.uid != null",
"$POSTID": {
// UID must match logged in user and is fixed once set
"uid": {
".validate": "(data.exists() && data.val() == newData.val()) || newData.val() == auth.uid"
},
// User can only update own stars
"stars": {
"$UID": {
".validate": "auth.uid == $UID"
}
}
}
},
// User posts can be read by anyone but only written by the user that owns it,
// and with a matching UID
"user-posts": {
".read": true,
"$UID": {
"$POSTID": {
".write": "auth.uid == $UID",
".validate": "data.exists() || newData.child('uid').val() == auth.uid"
}
}
},
// Comments can be read by anyone but only written by a logged in user
"post-comments": {
".read": true,
".write": "auth.uid != null",
"$POSTID": {
"$COMMENTID": {
// UID must match logged in user and is fixed once set
"uid": {
".validate": "(data.exists() && data.val() == newData.val()) || newData.val() == auth.uid"
}
}
}
}
}
}
```
Support
-------
- [Stack Overflow](https://stackoverflow.com/questions/tagged/firebase-database)
- [Firebase Support](https://firebase.google.com/support/)
License
-------
Copyright 2016 Google, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
================================================
FILE: database/app/.gitignore
================================================
/build
================================================
FILE: database/app/build.gradle.kts
================================================
import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.google.services)
}
tasks {
check.dependsOn("assembleDebugAndroidTest")
}
android {
namespace = "com.google.firebase.quickstart.database"
compileSdk = 36
defaultConfig {
applicationId = "com.google.firebase.quickstart.database"
minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
multiDexEnabled = true
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.9.6")
implementation("androidx.navigation:navigation-ui-ktx:2.9.6")
// Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom)
implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Firebase Realtime Database
implementation("com.google.firebase:firebase-database")
// Firebase Authentication
implementation("com.google.firebase:firebase-auth")
implementation("com.firebaseui:firebase-ui-database:9.1.1")
// Needed to fix a dependency conflict with FirebaseUI'
implementation("androidx.arch.core:core-runtime:2.2.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test:runner:1.7.0")
}
================================================
FILE: database/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keepattributes Signature
-keepattributes *Annotation*
-keepattributes EnclosingMethod
-keepattributes InnerClasses
-keep class com.google.firebase.quickstart.database.java.viewholder.** {
*;
}
-keepclassmembers class com.google.firebase.quickstart.database.java.models.** {
*;
}
-keepclassmembers class com.google.firebase.quickstart.database.kotlin.models.** {
*;
}
================================================
FILE: database/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/EntryChoiceActivity.kt
================================================
package com.google.firebase.quickstart.database
import android.content.Intent
import com.firebase.example.internal.BaseEntryChoiceActivity
import com.firebase.example.internal.Choice
class EntryChoiceActivity : BaseEntryChoiceActivity() {
override fun getChoices(): List {
return listOf(
Choice(
"Java",
"Run the Firebase Realtime Database quickstart written in Java.",
Intent(this, com.google.firebase.quickstart.database.java.MainActivity::class.java),
),
Choice(
"Kotlin",
"Run the Firebase Realtime Database quickstart written in Kotlin.",
Intent(this, com.google.firebase.quickstart.database.kotlin.MainActivity::class.java),
),
)
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/BaseFragment.java
================================================
package com.google.firebase.quickstart.database.java;
import android.view.View;
import android.widget.ProgressBar;
import androidx.fragment.app.Fragment;
import com.google.firebase.auth.FirebaseAuth;
public class BaseFragment extends Fragment {
private ProgressBar mProgressBar;
public void setProgressBar(int resId) {
mProgressBar = getView().findViewById(resId);
}
public void showProgressBar() {
if (mProgressBar != null) {
mProgressBar.setVisibility(View.VISIBLE);
}
}
public void hideProgressBar() {
if (mProgressBar != null) {
mProgressBar.setVisibility(View.INVISIBLE);
}
}
public String getUid() {
return FirebaseAuth.getInstance().getCurrentUser().getUid();
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/MainActivity.java
================================================
/*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.quickstart.database.java;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.NavDestination;
import androidx.navigation.Navigation;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.firebase.quickstart.database.R;
import com.google.firebase.quickstart.database.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
private FloatingActionButton fab;
private NavController navController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
fab = binding.fab;
navController = Navigation.findNavController(this, R.id.nav_host_fragment);
navController.setGraph(R.navigation.nav_graph_java);
navController.addOnDestinationChangedListener(new NavController.OnDestinationChangedListener() {
@Override
public void onDestinationChanged(@NonNull NavController controller, @NonNull NavDestination destination, @Nullable Bundle arguments) {
if (destination.getId() == R.id.MainFragment) {
fab.setVisibility(View.VISIBLE);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
navController.navigate(R.id.action_MainFragment_to_NewPostFragment);
}
});
} else {
fab.setVisibility(View.GONE);
}
}
});
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/MainFragment.java
================================================
package com.google.firebase.quickstart.database.java;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.MenuHost;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.tabs.TabLayoutMediator;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.quickstart.database.R;
import com.google.firebase.quickstart.database.databinding.FragmentMainBinding;
import com.google.firebase.quickstart.database.java.listfragments.MyPostsFragment;
import com.google.firebase.quickstart.database.java.listfragments.MyTopPostsFragment;
import com.google.firebase.quickstart.database.java.listfragments.RecentPostsFragment;
public class MainFragment extends Fragment implements MenuProvider {
private FragmentMainBinding binding;
private MenuHost menuHost;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentMainBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// MenuProvider
menuHost = requireActivity();
menuHost.addMenuProvider(this);
// Create the adapter that will return a fragment for each section
FragmentStateAdapter mPagerAdapter = new FragmentStateAdapter(getParentFragmentManager(),
getViewLifecycleOwner().getLifecycle()) {
private final Fragment[] mFragments = new Fragment[]{
new RecentPostsFragment(),
new MyPostsFragment(),
new MyTopPostsFragment(),
};
@NonNull
@Override
public Fragment createFragment(int position) {
return mFragments[position];
}
@Override
public int getItemCount() {
return mFragments.length;
}
};
// Set up the ViewPager with the sections adapter.
binding.container.setAdapter(mPagerAdapter);
String[] mFragmentNames = new String[]{
getString(R.string.heading_recent),
getString(R.string.heading_my_posts),
getString(R.string.heading_my_top_posts)
};
new TabLayoutMediator(binding.tabs, binding.container,
(tab, position) -> tab.setText(mFragmentNames[position])
).attach();
}
@Override
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
menuInflater.inflate(R.menu.menu_main, menu);
}
@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
int i = menuItem.getItemId();
if (i == R.id.action_logout) {
FirebaseAuth.getInstance().signOut();
NavHostFragment.findNavController(this)
.navigate(R.id.action_MainFragment_to_SignInFragment);
return true;
} else {
return false;
}
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/NewPostFragment.java
================================================
package com.google.firebase.quickstart.database.java;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import com.google.firebase.quickstart.database.R;
import com.google.firebase.quickstart.database.databinding.FragmentNewPostBinding;
import com.google.firebase.quickstart.database.java.models.Post;
import com.google.firebase.quickstart.database.java.models.User;
import java.util.HashMap;
import java.util.Map;
public class NewPostFragment extends BaseFragment {
private static final String TAG = "NewPostFragment";
private static final String REQUIRED = "Required";
private DatabaseReference mDatabase;
private FragmentNewPostBinding binding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentNewPostBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mDatabase = FirebaseDatabase.getInstance().getReference();
binding.fabSubmitPost.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
submitPost();
}
});
}
private void submitPost() {
final String title = binding.fieldTitle.getText().toString();
final String body = binding.fieldBody.getText().toString();
// Title is required
if (TextUtils.isEmpty(title)) {
binding.fieldTitle.setError(REQUIRED);
return;
}
// Body is required
if (TextUtils.isEmpty(body)) {
binding.fieldBody.setError(REQUIRED);
return;
}
// Disable button so there are no multi-posts
setEditingEnabled(false);
Toast.makeText(getContext(), "Posting...", Toast.LENGTH_SHORT).show();
final String userId = getUid();
mDatabase.child("users").child(userId).addListenerForSingleValueEvent(
new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
// Get user value
User user = dataSnapshot.getValue(User.class);
if (user == null) {
// User is null, error out
Log.e(TAG, "User " + userId + " is unexpectedly null");
Toast.makeText(getContext(),
"Error: could not fetch user.",
Toast.LENGTH_SHORT).show();
} else {
// Write new post
writeNewPost(userId, user.username, title, body);
}
setEditingEnabled(true);
NavHostFragment.findNavController(NewPostFragment.this)
.navigate(R.id.action_NewPostFragment_to_MainFragment);
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
Log.w(TAG, "getUser:onCancelled", databaseError.toException());
setEditingEnabled(true);
}
});
}
private void setEditingEnabled(boolean enabled) {
binding.fieldTitle.setEnabled(enabled);
binding.fieldBody.setEnabled(enabled);
if (enabled) {
binding.fabSubmitPost.show();
} else {
binding.fabSubmitPost.hide();
}
}
private void writeNewPost(String userId, String username, String title, String body) {
// Create new post at /user-posts/$userid/$postid and at
// /posts/$postid simultaneously
String key = mDatabase.child("posts").push().getKey();
Post post = new Post(userId, username, title, body);
Map postValues = post.toMap();
Map childUpdates = new HashMap<>();
childUpdates.put("/posts/" + key, postValues);
childUpdates.put("/user-posts/" + userId + "/" + key, postValues);
mDatabase.updateChildren(childUpdates);
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/PostDetailFragment.java
================================================
package com.google.firebase.quickstart.database.java;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import com.google.firebase.quickstart.database.R;
import com.google.firebase.quickstart.database.databinding.FragmentPostDetailBinding;
import com.google.firebase.quickstart.database.java.models.Comment;
import com.google.firebase.quickstart.database.java.models.Post;
import com.google.firebase.quickstart.database.java.models.User;
import com.google.firebase.quickstart.database.java.viewholder.CommentViewHolder;
import java.util.ArrayList;
import java.util.List;
public class PostDetailFragment extends BaseFragment {
private static final String TAG = "PostDetailFragment";
public static final String EXTRA_POST_KEY = "post_key";
private DatabaseReference mPostReference;
private DatabaseReference mCommentsReference;
private ValueEventListener mPostListener;
private String mPostKey;
private CommentAdapter mAdapter;
private FragmentPostDetailBinding binding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentPostDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Get post key from arguments
mPostKey = requireArguments().getString(EXTRA_POST_KEY);
if (mPostKey == null) {
throw new IllegalArgumentException("Must pass EXTRA_POST_KEY");
}
// Initialize Database
mPostReference = FirebaseDatabase.getInstance().getReference()
.child("posts").child(mPostKey);
mCommentsReference = FirebaseDatabase.getInstance().getReference()
.child("post-comments").child(mPostKey);
binding.buttonPostComment.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
postComment();
}
});
binding.recyclerPostComments.setLayoutManager(new LinearLayoutManager(getContext()));
}
@Override
public void onStart() {
super.onStart();
// Add value event listener to the post
ValueEventListener postListener = new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get Post object and use the values to update the UI
Post post = dataSnapshot.getValue(Post.class);
binding.postAuthorLayout.postAuthor.setText(post.author);
binding.postTextLayout.postTitle.setText(post.title);
binding.postTextLayout.postBody.setText(post.body);
}
@Override
public void onCancelled(DatabaseError databaseError) {
// Getting Post failed, log a message
Log.w(TAG, "loadPost:onCancelled", databaseError.toException());
Toast.makeText(getContext(), "Failed to load post.",
Toast.LENGTH_SHORT).show();
}
};
mPostReference.addValueEventListener(postListener);
// Keep copy of post listener so we can remove it when app stops
mPostListener = postListener;
// Listen for comments
mAdapter = new CommentAdapter(getContext(), mCommentsReference);
binding.recyclerPostComments.setAdapter(mAdapter);
}
@Override
public void onStop() {
super.onStop();
// Remove post value event listener
if (mPostListener != null) {
mPostReference.removeEventListener(mPostListener);
}
// Clean up comments listener
mAdapter.cleanupListener();
}
private void postComment() {
final String uid = getUid();
FirebaseDatabase.getInstance().getReference().child("users").child(uid)
.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get user information
User user = dataSnapshot.getValue(User.class);
String authorName = user.username;
// Create new comment object
String commentText = binding.fieldCommentText.getText().toString();
Comment comment = new Comment(uid, authorName, commentText);
// Push the comment, it will appear in the list
mCommentsReference.push().setValue(comment);
// Clear the field
binding.fieldCommentText.setText(null);
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
private static class CommentAdapter extends RecyclerView.Adapter {
private Context mContext;
private DatabaseReference mDatabaseReference;
private ChildEventListener mChildEventListener;
private List mCommentIds = new ArrayList<>();
private List mComments = new ArrayList<>();
public CommentAdapter(final Context context, DatabaseReference ref) {
mContext = context;
mDatabaseReference = ref;
// Create child event listener
ChildEventListener childEventListener = new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
Log.d(TAG, "onChildAdded:" + dataSnapshot.getKey());
// A new comment has been added, add it to the displayed list
Comment comment = dataSnapshot.getValue(Comment.class);
// Update RecyclerView
mCommentIds.add(dataSnapshot.getKey());
mComments.add(comment);
notifyItemInserted(mComments.size() - 1);
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
Log.d(TAG, "onChildChanged:" + dataSnapshot.getKey());
// A comment has changed, use the key to determine if we are displaying this
// comment and if so displayed the changed comment.
Comment newComment = dataSnapshot.getValue(Comment.class);
String commentKey = dataSnapshot.getKey();
int commentIndex = mCommentIds.indexOf(commentKey);
if (commentIndex > -1) {
// Replace with the new data
mComments.set(commentIndex, newComment);
// Update the RecyclerView
notifyItemChanged(commentIndex);
} else {
Log.w(TAG, "onChildChanged:unknown_child:" + commentKey);
}
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
Log.d(TAG, "onChildRemoved:" + dataSnapshot.getKey());
// A comment has changed, use the key to determine if we are displaying this
// comment and if so remove it.
String commentKey = dataSnapshot.getKey();
int commentIndex = mCommentIds.indexOf(commentKey);
if (commentIndex > -1) {
// Remove data from the list
mCommentIds.remove(commentIndex);
mComments.remove(commentIndex);
// Update the RecyclerView
notifyItemRemoved(commentIndex);
} else {
Log.w(TAG, "onChildRemoved:unknown_child:" + commentKey);
}
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
Log.d(TAG, "onChildMoved:" + dataSnapshot.getKey());
// A comment has changed position, use the key to determine if we are
// displaying this comment and if so move it.
Comment movedComment = dataSnapshot.getValue(Comment.class);
String commentKey = dataSnapshot.getKey();
// ...
}
@Override
public void onCancelled(DatabaseError databaseError) {
Log.w(TAG, "postComments:onCancelled", databaseError.toException());
Toast.makeText(mContext, "Failed to load comments.",
Toast.LENGTH_SHORT).show();
}
};
ref.addChildEventListener(childEventListener);
// Store reference to listener so it can be removed on app stop
mChildEventListener = childEventListener;
}
@Override
public CommentViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.item_comment, parent, false);
return new CommentViewHolder(view);
}
@Override
public void onBindViewHolder(CommentViewHolder holder, int position) {
Comment comment = mComments.get(position);
holder.authorView.setText(comment.author);
holder.bodyView.setText(comment.text);
}
@Override
public int getItemCount() {
return mComments.size();
}
public void cleanupListener() {
if (mChildEventListener != null) {
mDatabaseReference.removeEventListener(mChildEventListener);
}
}
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/SignInFragment.java
================================================
package com.google.firebase.quickstart.database.java;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.quickstart.database.R;
import com.google.firebase.quickstart.database.databinding.FragmentSignInBinding;
import com.google.firebase.quickstart.database.java.models.User;
public class SignInFragment extends BaseFragment implements View.OnClickListener {
private static final String TAG = "SignInFragment";
private DatabaseReference mDatabase;
private FirebaseAuth mAuth;
private FragmentSignInBinding binding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
binding = FragmentSignInBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mDatabase = FirebaseDatabase.getInstance().getReference();
mAuth = FirebaseAuth.getInstance();
// Views
setProgressBar(R.id.progressBar);
// Click listeners
binding.buttonSignIn.setOnClickListener(this);
binding.buttonSignUp.setOnClickListener(this);
}
@Override
public void onStart() {
super.onStart();
// Check auth on Activity start
if (mAuth.getCurrentUser() != null) {
onAuthSuccess(mAuth.getCurrentUser());
}
}
private void signIn() {
Log.d(TAG, "signIn");
if (!validateForm()) {
return;
}
showProgressBar();
String email = binding.fieldEmail.getText().toString();
String password = binding.fieldPassword.getText().toString();
mAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(getActivity(), new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
Log.d(TAG, "signIn:onComplete:" + task.isSuccessful());
hideProgressBar();
if (task.isSuccessful()) {
onAuthSuccess(task.getResult().getUser());
} else {
Toast.makeText(getContext(), "Sign In Failed",
Toast.LENGTH_SHORT).show();
}
}
});
}
private void signUp() {
Log.d(TAG, "signUp");
if (!validateForm()) {
return;
}
showProgressBar();
String email = binding.fieldEmail.getText().toString();
String password = binding.fieldPassword.getText().toString();
mAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(getActivity(), new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
Log.d(TAG, "createUser:onComplete:" + task.isSuccessful());
hideProgressBar();
if (task.isSuccessful()) {
onAuthSuccess(task.getResult().getUser());
} else {
Toast.makeText(getContext(), "Sign Up Failed",
Toast.LENGTH_SHORT).show();
}
}
});
}
private void onAuthSuccess(FirebaseUser user) {
String username = usernameFromEmail(user.getEmail());
// Write new user
writeNewUser(user.getUid(), username, user.getEmail());
// Go to MainFragment
NavHostFragment.findNavController(this).navigate(R.id.action_SignInFragment_to_MainFragment);
}
private String usernameFromEmail(String email) {
if (email.contains("@")) {
return email.split("@")[0];
} else {
return email;
}
}
private boolean validateForm() {
boolean result = true;
if (TextUtils.isEmpty(binding.fieldEmail.getText().toString())) {
binding.fieldEmail.setError("Required");
result = false;
} else {
binding.fieldEmail.setError(null);
}
if (TextUtils.isEmpty(binding.fieldPassword.getText().toString())) {
binding.fieldPassword.setError("Required");
result = false;
} else {
binding.fieldPassword.setError(null);
}
return result;
}
private void writeNewUser(String userId, String name, String email) {
User user = new User(name, email);
mDatabase.child("users").child(userId).setValue(user);
}
public void onClick(View v) {
int i = v.getId();
if (i == R.id.buttonSignIn) {
signIn();
} else if (i == R.id.buttonSignUp) {
signUp();
}
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/listfragments/MyPostsFragment.java
================================================
package com.google.firebase.quickstart.database.java.listfragments;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.Query;
public class MyPostsFragment extends PostListFragment {
public MyPostsFragment() {}
@Override
public Query getQuery(DatabaseReference databaseReference) {
// All my posts
return databaseReference.child("user-posts")
.child(getUid());
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/listfragments/MyTopPostsFragment.java
================================================
package com.google.firebase.quickstart.database.java.listfragments;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.Query;
public class MyTopPostsFragment extends PostListFragment {
public MyTopPostsFragment() {}
@Override
public Query getQuery(DatabaseReference databaseReference) {
// My top posts by number of stars
String myUserId = getUid();
Query myTopPostsQuery = databaseReference.child("user-posts").child(myUserId)
.orderByChild("starCount");
return myTopPostsQuery;
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/listfragments/PostListFragment.java
================================================
package com.google.firebase.quickstart.database.java.listfragments;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.fragment.app.Fragment;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.firebase.ui.database.FirebaseRecyclerAdapter;
import com.firebase.ui.database.FirebaseRecyclerOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.MutableData;
import com.google.firebase.database.Query;
import com.google.firebase.database.Transaction;
import com.google.firebase.quickstart.database.R;
import com.google.firebase.quickstart.database.java.PostDetailFragment;
import com.google.firebase.quickstart.database.java.models.Post;
import com.google.firebase.quickstart.database.java.viewholder.PostViewHolder;
public abstract class PostListFragment extends Fragment {
private static final String TAG = "PostListFragment";
// [START define_database_reference]
private DatabaseReference mDatabase;
// [END define_database_reference]
private FirebaseRecyclerAdapter mAdapter;
private RecyclerView mRecycler;
private LinearLayoutManager mManager;
public PostListFragment() {}
@Override
public View onCreateView (LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View rootView = inflater.inflate(R.layout.fragment_all_posts, container, false);
// [START create_database_reference]
mDatabase = FirebaseDatabase.getInstance().getReference();
// [END create_database_reference]
mRecycler = rootView.findViewById(R.id.messagesList);
mRecycler.setHasFixedSize(true);
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Set up Layout Manager, reverse layout
mManager = new LinearLayoutManager(getActivity());
mManager.setReverseLayout(true);
mManager.setStackFromEnd(true);
mRecycler.setLayoutManager(mManager);
// Set up FirebaseRecyclerAdapter with the Query
Query postsQuery = getQuery(mDatabase);
FirebaseRecyclerOptions options = new FirebaseRecyclerOptions.Builder()
.setQuery(postsQuery, Post.class)
.build();
mAdapter = new FirebaseRecyclerAdapter(options) {
@Override
public PostViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
return new PostViewHolder(inflater.inflate(R.layout.item_post, viewGroup, false));
}
@Override
protected void onBindViewHolder(PostViewHolder viewHolder, int position, final Post model) {
final DatabaseReference postRef = getRef(position);
// Set click listener for the whole post view
final String postKey = postRef.getKey();
viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Launch PostDetailFragment
NavController navController = Navigation.findNavController(requireActivity(),
R.id.nav_host_fragment);
Bundle args = new Bundle();
args.putString(PostDetailFragment.EXTRA_POST_KEY, postKey);
navController.navigate(R.id.action_MainFragment_to_PostDetailFragment, args);
}
});
// Determine if the current user has liked this post and set UI accordingly
if (model.stars.containsKey(getUid())) {
viewHolder.starView.setImageResource(R.drawable.ic_toggle_star_24);
} else {
viewHolder.starView.setImageResource(R.drawable.ic_toggle_star_outline_24);
}
// Bind Post to ViewHolder, setting OnClickListener for the star button
viewHolder.bindToPost(model, new View.OnClickListener() {
@Override
public void onClick(View starView) {
// Need to write to both places the post is stored
DatabaseReference globalPostRef = mDatabase.child("posts").child(postRef.getKey());
DatabaseReference userPostRef = mDatabase.child("user-posts").child(model.uid).child(postRef.getKey());
// Run two transactions
onStarClicked(globalPostRef);
onStarClicked(userPostRef);
}
});
}
};
mRecycler.setAdapter(mAdapter);
}
// [START post_stars_transaction]
private void onStarClicked(DatabaseReference postRef) {
postRef.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData mutableData) {
Post p = mutableData.getValue(Post.class);
if (p == null) {
return Transaction.success(mutableData);
}
if (p.stars.containsKey(getUid())) {
// Unstar the post and remove self from stars
p.starCount = p.starCount - 1;
p.stars.remove(getUid());
} else {
// Star the post and add self to stars
p.starCount = p.starCount + 1;
p.stars.put(getUid(), true);
}
// Set value and report transaction success
mutableData.setValue(p);
return Transaction.success(mutableData);
}
@Override
public void onComplete(DatabaseError databaseError, boolean committed,
DataSnapshot currentData) {
// Transaction completed
Log.d(TAG, "postTransaction:onComplete:" + databaseError);
}
});
}
// [END post_stars_transaction]
@Override
public void onStart() {
super.onStart();
if (mAdapter != null) {
mAdapter.startListening();
}
}
@Override
public void onStop() {
super.onStop();
if (mAdapter != null) {
mAdapter.stopListening();
}
}
public String getUid() {
return FirebaseAuth.getInstance().getCurrentUser().getUid();
}
public abstract Query getQuery(DatabaseReference databaseReference);
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/listfragments/RecentPostsFragment.java
================================================
package com.google.firebase.quickstart.database.java.listfragments;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.Query;
public class RecentPostsFragment extends PostListFragment {
public RecentPostsFragment() {}
@Override
public Query getQuery(DatabaseReference databaseReference) {
// [START recent_posts_query]
// Last 100 posts, these are automatically the 100 most recent
// due to sorting by push() keys
Query recentPostsQuery = databaseReference.child("posts")
.limitToFirst(100);
// [END recent_posts_query]
return recentPostsQuery;
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/models/Comment.java
================================================
package com.google.firebase.quickstart.database.java.models;
import com.google.firebase.database.IgnoreExtraProperties;
@IgnoreExtraProperties
public class Comment {
public String uid;
public String author;
public String text;
public Comment() {
// Default constructor required for calls to DataSnapshot.getValue(Comment.class)
}
public Comment(String uid, String author, String text) {
this.uid = uid;
this.author = author;
this.text = text;
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/models/Post.java
================================================
package com.google.firebase.quickstart.database.java.models;
import com.google.firebase.database.Exclude;
import com.google.firebase.database.IgnoreExtraProperties;
import java.util.HashMap;
import java.util.Map;
@IgnoreExtraProperties
public class Post {
public String uid;
public String author;
public String title;
public String body;
public int starCount = 0;
public Map stars = new HashMap<>();
public Post() {
// Default constructor required for calls to DataSnapshot.getValue(Post.class)
}
public Post(String uid, String author, String title, String body) {
this.uid = uid;
this.author = author;
this.title = title;
this.body = body;
}
@Exclude
public Map toMap() {
HashMap result = new HashMap<>();
result.put("uid", uid);
result.put("author", author);
result.put("title", title);
result.put("body", body);
result.put("starCount", starCount);
result.put("stars", stars);
return result;
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/models/User.java
================================================
package com.google.firebase.quickstart.database.java.models;
import com.google.firebase.database.IgnoreExtraProperties;
@IgnoreExtraProperties
public class User {
public String username;
public String email;
public User() {
// Default constructor required for calls to DataSnapshot.getValue(User.class)
}
public User(String username, String email) {
this.username = username;
this.email = email;
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/viewholder/CommentViewHolder.java
================================================
package com.google.firebase.quickstart.database.java.viewholder;
import android.view.View;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.google.firebase.quickstart.database.R;
public class CommentViewHolder extends RecyclerView.ViewHolder {
public TextView authorView;
public TextView bodyView;
public CommentViewHolder(View itemView) {
super(itemView);
authorView = itemView.findViewById(R.id.commentAuthor);
bodyView = itemView.findViewById(R.id.commentBody);
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/java/viewholder/PostViewHolder.java
================================================
package com.google.firebase.quickstart.database.java.viewholder;
import androidx.recyclerview.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.firebase.quickstart.database.R;
import com.google.firebase.quickstart.database.java.models.Post;
public class PostViewHolder extends RecyclerView.ViewHolder {
public TextView titleView;
public TextView authorView;
public ImageView starView;
public TextView numStarsView;
public TextView bodyView;
public PostViewHolder(View itemView) {
super(itemView);
titleView = itemView.findViewById(R.id.postTitle);
authorView = itemView.findViewById(R.id.postAuthor);
starView = itemView.findViewById(R.id.star);
numStarsView = itemView.findViewById(R.id.postNumStars);
bodyView = itemView.findViewById(R.id.postBody);
}
public void bindToPost(Post post, View.OnClickListener starClickListener) {
titleView.setText(post.title);
authorView.setText(post.author);
numStarsView.setText(String.valueOf(post.starCount));
bodyView.setText(post.body);
starView.setOnClickListener(starClickListener);
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/BaseFragment.kt
================================================
package com.google.firebase.quickstart.database.kotlin
import android.view.View
import android.widget.ProgressBar
import androidx.fragment.app.Fragment
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
open class BaseFragment : Fragment() {
private var progressBar: ProgressBar? = null
val uid: String
get() = Firebase.auth.currentUser!!.uid
fun setProgressBar(resId: Int) {
progressBar = view?.findViewById(resId)
}
fun showProgressBar() {
progressBar?.visibility = View.VISIBLE
}
fun hideProgressBar() {
progressBar?.visibility = View.INVISIBLE
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/MainActivity.kt
================================================
package com.google.firebase.quickstart.database.kotlin
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.navigation.findNavController
import com.google.firebase.quickstart.database.R
import com.google.firebase.quickstart.database.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val toolbar = binding.toolbar
setSupportActionBar(toolbar)
val fab = binding.fab
val navController = findNavController(R.id.nav_host_fragment)
navController.setGraph(R.navigation.nav_graph_kotlin)
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.id == R.id.MainFragment) {
fab.isVisible = true
fab.setOnClickListener {
navController.navigate(R.id.action_MainFragment_to_NewPostFragment)
}
} else {
fab.isGone = true
}
}
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/MainFragment.kt
================================================
package com.google.firebase.quickstart.database.kotlin
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import com.google.firebase.auth.auth
import com.google.firebase.Firebase
import com.google.firebase.quickstart.database.R
import com.google.firebase.quickstart.database.databinding.FragmentMainBinding
import com.google.firebase.quickstart.database.kotlin.listfragments.MyPostsFragment
import com.google.firebase.quickstart.database.kotlin.listfragments.MyTopPostsFragment
import com.google.firebase.quickstart.database.kotlin.listfragments.RecentPostsFragment
class MainFragment : Fragment(), MenuProvider {
private var _binding: FragmentMainBinding? = null
private val binding get() = _binding!!
private lateinit var pagerAdapter: FragmentStateAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// MenuProvider
val menuHost: MenuHost = requireActivity() as MenuHost
menuHost.addMenuProvider(this)
// Create the adapter that will return a fragment for each section
pagerAdapter = object : FragmentStateAdapter(parentFragmentManager, viewLifecycleOwner.lifecycle) {
private val fragments = arrayOf(
RecentPostsFragment(),
MyPostsFragment(),
MyTopPostsFragment(),
)
override fun createFragment(position: Int) = fragments[position]
override fun getItemCount() = fragments.size
}
// Set up the ViewPager with the sections adapter.
with(binding) {
container.adapter = pagerAdapter
TabLayoutMediator(tabs, container) { tab, position ->
tab.text = when (position) {
0 -> getString(R.string.heading_recent)
1 -> getString(R.string.heading_my_posts)
else -> getString(R.string.heading_my_top_posts)
}
}.attach()
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_main, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return if (menuItem.itemId == R.id.action_logout) {
Firebase.auth.signOut()
findNavController().navigate(R.id.action_MainFragment_to_SignInFragment)
true
} else {
false
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/NewPostFragment.kt
================================================
package com.google.firebase.quickstart.database.kotlin
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.navigation.fragment.findNavController
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.database
import com.google.firebase.database.getValue
import com.google.firebase.Firebase
import com.google.firebase.quickstart.database.R
import com.google.firebase.quickstart.database.databinding.FragmentNewPostBinding
import com.google.firebase.quickstart.database.kotlin.models.Post
import com.google.firebase.quickstart.database.kotlin.models.User
class NewPostFragment : BaseFragment() {
private var _binding: FragmentNewPostBinding? = null
private val binding get() = _binding!!
private lateinit var database: DatabaseReference
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentNewPostBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
database = Firebase.database.reference
binding.fabSubmitPost.setOnClickListener { submitPost() }
}
private fun submitPost() {
val title = binding.fieldTitle.text.toString()
val body = binding.fieldBody.text.toString()
// Title is required
if (TextUtils.isEmpty(title)) {
binding.fieldTitle.error = REQUIRED
return
}
// Body is required
if (TextUtils.isEmpty(body)) {
binding.fieldBody.error = REQUIRED
return
}
// Disable button so there are no multi-posts
setEditingEnabled(false)
Toast.makeText(context, "Posting...", Toast.LENGTH_SHORT).show()
val userId = uid
database.child("users").child(userId).addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
// Get user value
val user = dataSnapshot.getValue()
if (user == null) {
// User is null, error out
Log.e(TAG, "User $userId is unexpectedly null")
Toast.makeText(
context,
"Error: could not fetch user.",
Toast.LENGTH_SHORT,
).show()
} else {
// Write new post
writeNewPost(userId, user.username.toString(), title, body)
}
setEditingEnabled(true)
findNavController().navigate(R.id.action_NewPostFragment_to_MainFragment)
}
override fun onCancelled(databaseError: DatabaseError) {
Log.w(TAG, "getUser:onCancelled", databaseError.toException())
setEditingEnabled(true)
}
},
)
}
private fun setEditingEnabled(enabled: Boolean) {
with(binding) {
fieldTitle.isEnabled = enabled
fieldBody.isEnabled = enabled
if (enabled) {
fabSubmitPost.show()
} else {
fabSubmitPost.hide()
}
}
}
private fun writeNewPost(userId: String, username: String, title: String, body: String) {
// Create new post at /user-posts/$userid/$postid and at
// /posts/$postid simultaneously
val key = database.child("posts").push().key
if (key == null) {
Log.w(TAG, "Couldn't get push key for posts")
return
}
val post = Post(userId, username, title, body)
val postValues = post.toMap()
val childUpdates = hashMapOf(
"/posts/$key" to postValues,
"/user-posts/$userId/$key" to postValues,
)
database.updateChildren(childUpdates)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
companion object {
private const val TAG = "NewPostFragment"
private const val REQUIRED = "Required"
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/PostDetailFragment.kt
================================================
package com.google.firebase.quickstart.database.kotlin
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.database.ChildEventListener
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.database
import com.google.firebase.database.getValue
import com.google.firebase.Firebase
import com.google.firebase.quickstart.database.R
import com.google.firebase.quickstart.database.databinding.FragmentPostDetailBinding
import com.google.firebase.quickstart.database.kotlin.models.Comment
import com.google.firebase.quickstart.database.kotlin.models.Post
import com.google.firebase.quickstart.database.kotlin.models.User
import com.google.firebase.quickstart.database.kotlin.viewholder.CommentViewHolder
import java.lang.IllegalArgumentException
import java.util.ArrayList
class PostDetailFragment : BaseFragment() {
private lateinit var postKey: String
private lateinit var postReference: DatabaseReference
private lateinit var commentsReference: DatabaseReference
private var postListener: ValueEventListener? = null
private var adapter: CommentAdapter? = null
private var _binding: FragmentPostDetailBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentPostDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Get post key from arguments
postKey = requireArguments().getString(EXTRA_POST_KEY)
?: throw IllegalArgumentException("Must pass EXTRA_POST_KEY")
// Initialize Database
postReference = Firebase.database.reference
.child("posts").child(postKey)
commentsReference = Firebase.database.reference
.child("post-comments").child(postKey)
// Initialize Views
with(binding) {
buttonPostComment.setOnClickListener { postComment() }
recyclerPostComments.layoutManager = LinearLayoutManager(context)
}
}
override fun onStart() {
super.onStart()
// Add value event listener to the post
val postListener = object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
// Get Post object and use the values to update the UI
val post = dataSnapshot.getValue()
post?.let {
binding.postAuthorLayout.postAuthor.text = it.author
with(binding.postTextLayout) {
postTitle.text = it.title
postBody.text = it.body
}
}
}
override fun onCancelled(databaseError: DatabaseError) {
// Getting Post failed, log a message
Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
Toast.makeText(
context,
"Failed to load post.",
Toast.LENGTH_SHORT,
).show()
}
}
postReference.addValueEventListener(postListener)
// Keep copy of post listener so we can remove it when app stops
this.postListener = postListener
// Listen for comments
adapter = CommentAdapter(requireContext(), commentsReference)
binding.recyclerPostComments.adapter = adapter
}
override fun onStop() {
super.onStop()
// Remove post value event listener
postListener?.let {
postReference.removeEventListener(it)
}
// Clean up comments listener
adapter?.cleanupListener()
}
private fun postComment() {
val uid = uid
Firebase.database.reference.child("users").child(uid)
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
// Get user information
val user = dataSnapshot.getValue() ?: return
val authorName = user.username
// Create new comment object
val commentText = binding.fieldCommentText.text.toString()
val comment = Comment(uid, authorName, commentText)
// Push the comment, it will appear in the list
commentsReference.push().setValue(comment)
// Clear the field
binding.fieldCommentText.text = null
}
override fun onCancelled(databaseError: DatabaseError) {
}
})
}
private class CommentAdapter(
private val context: Context,
private val databaseReference: DatabaseReference,
) : RecyclerView.Adapter() {
private val childEventListener: ChildEventListener?
private val commentIds = ArrayList()
private val comments = ArrayList()
init {
// Create child event listener
val childEventListener = object : ChildEventListener {
override fun onChildAdded(dataSnapshot: DataSnapshot, previousChildName: String?) {
Log.d(TAG, "onChildAdded:" + dataSnapshot.key!!)
// A new comment has been added, add it to the displayed list
val comment = dataSnapshot.getValue()
// Update RecyclerView
commentIds.add(dataSnapshot.key!!)
comments.add(comment!!)
notifyItemInserted(comments.size - 1)
}
override fun onChildChanged(dataSnapshot: DataSnapshot, previousChildName: String?) {
Log.d(TAG, "onChildChanged: ${dataSnapshot.key}")
// A comment has changed, use the key to determine if we are displaying this
// comment and if so displayed the changed comment.
val newComment = dataSnapshot.getValue()
val commentKey = dataSnapshot.key
val commentIndex = commentIds.indexOf(commentKey)
if (commentIndex > -1 && newComment != null) {
// Replace with the new data
comments[commentIndex] = newComment
// Update the RecyclerView
notifyItemChanged(commentIndex)
} else {
Log.w(TAG, "onChildChanged:unknown_child: $commentKey")
}
}
override fun onChildRemoved(dataSnapshot: DataSnapshot) {
Log.d(TAG, "onChildRemoved:" + dataSnapshot.key!!)
// A comment has changed, use the key to determine if we are displaying this
// comment and if so remove it.
val commentKey = dataSnapshot.key
val commentIndex = commentIds.indexOf(commentKey)
if (commentIndex > -1) {
// Remove data from the list
commentIds.removeAt(commentIndex)
comments.removeAt(commentIndex)
// Update the RecyclerView
notifyItemRemoved(commentIndex)
} else {
Log.w(TAG, "onChildRemoved:unknown_child:" + commentKey!!)
}
}
override fun onChildMoved(dataSnapshot: DataSnapshot, previousChildName: String?) {
Log.d(TAG, "onChildMoved:" + dataSnapshot.key!!)
// A comment has changed position, use the key to determine if we are
// displaying this comment and if so move it.
val movedComment = dataSnapshot.getValue()
val commentKey = dataSnapshot.key
// ...
}
override fun onCancelled(databaseError: DatabaseError) {
Log.w(TAG, "postComments:onCancelled", databaseError.toException())
Toast.makeText(
context,
"Failed to load comments.",
Toast.LENGTH_SHORT,
).show()
}
}
databaseReference.addChildEventListener(childEventListener)
// Store reference to listener so it can be removed on app stop
this.childEventListener = childEventListener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.item_comment, parent, false)
return CommentViewHolder(view)
}
override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
holder.bind(comments[position])
}
override fun getItemCount(): Int = comments.size
fun cleanupListener() {
childEventListener?.let {
databaseReference.removeEventListener(it)
}
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
companion object {
private const val TAG = "PostDetailFragment"
const val EXTRA_POST_KEY = "post_key"
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/SignInFragment.kt
================================================
package com.google.firebase.quickstart.database.kotlin
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.navigation.fragment.findNavController
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.auth
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.database
import com.google.firebase.Firebase
import com.google.firebase.quickstart.database.R
import com.google.firebase.quickstart.database.databinding.FragmentSignInBinding
import com.google.firebase.quickstart.database.kotlin.models.User
class SignInFragment : BaseFragment() {
private var _binding: FragmentSignInBinding? = null
private val binding get() = _binding!!
private lateinit var database: DatabaseReference
private lateinit var auth: FirebaseAuth
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentSignInBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
database = Firebase.database.reference
auth = Firebase.auth
setProgressBar(R.id.progressBar)
// Click listeners
with(binding) {
buttonSignIn.setOnClickListener { signIn() }
buttonSignUp.setOnClickListener { signUp() }
}
}
override fun onStart() {
super.onStart()
// Check auth on Fragment start
auth.currentUser?.let {
onAuthSuccess(it)
}
}
private fun signIn() {
Log.d(TAG, "signIn")
if (!validateForm()) {
return
}
showProgressBar()
val email = binding.fieldEmail.text.toString()
val password = binding.fieldPassword.text.toString()
auth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(requireActivity()) { task ->
Log.d(TAG, "signIn:onComplete:" + task.isSuccessful)
hideProgressBar()
if (task.isSuccessful) {
onAuthSuccess(task.result?.user!!)
} else {
Toast.makeText(
context,
"Sign In Failed",
Toast.LENGTH_SHORT,
).show()
}
}
}
private fun signUp() {
Log.d(TAG, "signUp")
if (!validateForm()) {
return
}
showProgressBar()
val email = binding.fieldEmail.text.toString()
val password = binding.fieldPassword.text.toString()
auth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(requireActivity()) { task ->
Log.d(TAG, "createUser:onComplete:" + task.isSuccessful)
hideProgressBar()
if (task.isSuccessful) {
onAuthSuccess(task.result?.user!!)
} else {
Toast.makeText(
context,
"Sign Up Failed",
Toast.LENGTH_SHORT,
).show()
}
}
}
private fun onAuthSuccess(user: FirebaseUser) {
val username = usernameFromEmail(user.email!!)
// Write new user
writeNewUser(user.uid, username, user.email)
// Go to MainFragment
findNavController().navigate(R.id.action_SignInFragment_to_MainFragment)
}
private fun usernameFromEmail(email: String): String {
return if (email.contains("@")) {
email.split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0]
} else {
email
}
}
private fun validateForm(): Boolean {
var result = true
if (TextUtils.isEmpty(binding.fieldEmail.text.toString())) {
binding.fieldEmail.error = "Required"
result = false
} else {
binding.fieldEmail.error = null
}
if (TextUtils.isEmpty(binding.fieldPassword.text.toString())) {
binding.fieldPassword.error = "Required"
result = false
} else {
binding.fieldPassword.error = null
}
return result
}
private fun writeNewUser(userId: String, name: String, email: String?) {
val user = User(name, email)
database.child("users").child(userId).setValue(user)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
companion object {
private const val TAG = "SignInFragment"
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/listfragments/MyPostsFragment.kt
================================================
package com.google.firebase.quickstart.database.kotlin.listfragments
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.Query
class MyPostsFragment : PostListFragment() {
override fun getQuery(databaseReference: DatabaseReference): Query {
// All my posts
return databaseReference.child("user-posts")
.child(uid)
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/listfragments/MyTopPostsFragment.kt
================================================
package com.google.firebase.quickstart.database.kotlin.listfragments
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.Query
class MyTopPostsFragment : PostListFragment() {
override fun getQuery(databaseReference: DatabaseReference): Query {
// My top posts by number of stars
val myUserId = uid
return databaseReference.child("user-posts").child(myUserId)
.orderByChild("starCount")
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/listfragments/PostListFragment.kt
================================================
package com.google.firebase.quickstart.database.kotlin.listfragments
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.firebase.ui.database.FirebaseRecyclerAdapter
import com.firebase.ui.database.FirebaseRecyclerOptions
import com.google.firebase.auth.auth
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.MutableData
import com.google.firebase.database.Query
import com.google.firebase.database.Transaction
import com.google.firebase.database.database
import com.google.firebase.Firebase
import com.google.firebase.quickstart.database.R
import com.google.firebase.quickstart.database.kotlin.PostDetailFragment
import com.google.firebase.quickstart.database.kotlin.models.Post
import com.google.firebase.quickstart.database.kotlin.viewholder.PostViewHolder
abstract class PostListFragment : Fragment() {
// [START define_database_reference]
private lateinit var database: DatabaseReference
// [END define_database_reference]
private lateinit var recycler: RecyclerView
private lateinit var manager: LinearLayoutManager
private var adapter: FirebaseRecyclerAdapter? = null
val uid: String
get() = Firebase.auth.currentUser!!.uid
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
super.onCreateView(inflater, container, savedInstanceState)
val rootView = inflater.inflate(R.layout.fragment_all_posts, container, false)
// [START create_database_reference]
database = Firebase.database.reference
// [END create_database_reference]
recycler = rootView.findViewById(R.id.messagesList)
recycler.setHasFixedSize(true)
return rootView
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Set up Layout Manager, reverse layout
manager = LinearLayoutManager(activity)
manager.reverseLayout = true
manager.stackFromEnd = true
recycler.layoutManager = manager
// Set up FirebaseRecyclerAdapter with the Query
val postsQuery = getQuery(database)
val options = FirebaseRecyclerOptions.Builder()
.setQuery(postsQuery, Post::class.java)
.build()
adapter = object : FirebaseRecyclerAdapter(options) {
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): PostViewHolder {
val inflater = LayoutInflater.from(viewGroup.context)
return PostViewHolder(inflater.inflate(R.layout.item_post, viewGroup, false))
}
override fun onBindViewHolder(viewHolder: PostViewHolder, position: Int, model: Post) {
val postRef = getRef(position)
// Set click listener for the whole post view
val postKey = postRef.key
viewHolder.itemView.setOnClickListener {
// Launch PostDetailFragment
val navController = requireActivity().findNavController(R.id.nav_host_fragment)
val args = bundleOf(PostDetailFragment.EXTRA_POST_KEY to postKey)
navController.navigate(R.id.action_MainFragment_to_PostDetailFragment, args)
}
// Determine if the current user has liked this post and set UI accordingly
viewHolder.setLikedState(model.stars.containsKey(uid))
// Bind Post to ViewHolder, setting OnClickListener for the star button
viewHolder.bindToPost(model) {
// Need to write to both places the post is stored
val globalPostRef = database.child("posts").child(postRef.key!!)
val userPostRef = database.child("user-posts").child(model.uid!!).child(postRef.key!!)
// Run two transactions
onStarClicked(globalPostRef)
onStarClicked(userPostRef)
}
}
}
recycler.adapter = adapter
}
// [START post_stars_transaction]
private fun onStarClicked(postRef: DatabaseReference) {
postRef.runTransaction(object : Transaction.Handler {
override fun doTransaction(mutableData: MutableData): Transaction.Result {
val p = mutableData.getValue(Post::class.java)
?: return Transaction.success(mutableData)
if (p.stars.containsKey(uid)) {
// Unstar the post and remove self from stars
p.starCount = p.starCount - 1
p.stars.remove(uid)
} else {
// Star the post and add self to stars
p.starCount = p.starCount + 1
p.stars[uid] = true
}
// Set value and report transaction success
mutableData.value = p
return Transaction.success(mutableData)
}
override fun onComplete(
databaseError: DatabaseError?,
committed: Boolean,
currentData: DataSnapshot?,
) {
// Transaction completed
Log.d(TAG, "postTransaction:onComplete:" + databaseError!!)
}
})
}
// [END post_stars_transaction]
override fun onStart() {
super.onStart()
adapter?.startListening()
}
override fun onStop() {
super.onStop()
adapter?.stopListening()
}
abstract fun getQuery(databaseReference: DatabaseReference): Query
companion object {
private const val TAG = "PostListFragment"
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/listfragments/RecentPostsFragment.kt
================================================
package com.google.firebase.quickstart.database.kotlin.listfragments
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.Query
class RecentPostsFragment : PostListFragment() {
override fun getQuery(databaseReference: DatabaseReference): Query {
// [START recent_posts_query]
// Last 100 posts, these are automatically the 100 most recent
// due to sorting by push() keys.
return databaseReference.child("posts")
.limitToFirst(100)
// [END recent_posts_query]
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/models/Comment.kt
================================================
package com.google.firebase.quickstart.database.kotlin.models
import com.google.firebase.database.IgnoreExtraProperties
@IgnoreExtraProperties
data class Comment(
var uid: String? = "",
var author: String? = "",
var text: String? = "",
)
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/models/Post.kt
================================================
package com.google.firebase.quickstart.database.kotlin.models
import com.google.firebase.database.Exclude
import com.google.firebase.database.IgnoreExtraProperties
import java.util.HashMap
@IgnoreExtraProperties
data class Post(
var uid: String? = "",
var author: String? = "",
var title: String? = "",
var body: String? = "",
var starCount: Int = 0,
var stars: MutableMap = HashMap(),
) {
@Exclude
fun toMap(): Map {
return mapOf(
"uid" to uid,
"author" to author,
"title" to title,
"body" to body,
"starCount" to starCount,
"stars" to stars,
)
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/models/User.kt
================================================
package com.google.firebase.quickstart.database.kotlin.models
import com.google.firebase.database.IgnoreExtraProperties
@IgnoreExtraProperties
data class User(
var username: String? = "",
var email: String? = "",
)
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/viewholder/CommentViewHolder.kt
================================================
package com.google.firebase.quickstart.database.kotlin.viewholder
import android.view.View
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.quickstart.database.R
import com.google.firebase.quickstart.database.kotlin.models.Comment
class CommentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun bind(comment: Comment) {
itemView.findViewById(R.id.commentAuthor).text = comment.author
itemView.findViewById(R.id.commentBody).text = comment.text
}
}
================================================
FILE: database/app/src/main/java/com/google/firebase/quickstart/database/kotlin/viewholder/PostViewHolder.kt
================================================
package com.google.firebase.quickstart.database.kotlin.viewholder
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.quickstart.database.R
import com.google.firebase.quickstart.database.kotlin.models.Post
class PostViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val postTitle: TextView = itemView.findViewById(R.id.postTitle)
private val postAuthor: TextView = itemView.findViewById(R.id.postAuthor)
private val postNumStars: TextView = itemView.findViewById(R.id.postNumStars)
private val postBody: TextView = itemView.findViewById(R.id.postBody)
private val star: ImageView = itemView.findViewById(R.id.star)
fun bindToPost(post: Post, starClickListener: View.OnClickListener) {
postTitle.text = post.title
postAuthor.text = post.author
postNumStars.text = post.starCount.toString()
postBody.text = post.body
star.setOnClickListener(starClickListener)
}
fun setLikedState(liked: Boolean) {
if (liked) {
star.setImageResource(R.drawable.ic_toggle_star_24)
} else {
star.setImageResource(R.drawable.ic_toggle_star_outline_24)
}
}
}
================================================
FILE: database/app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: database/app/src/main/res/layout/fragment_all_posts.xml
================================================
================================================
FILE: database/app/src/main/res/layout/fragment_main.xml
================================================
================================================
FILE: database/app/src/main/res/layout/fragment_new_post.xml
================================================
================================================
FILE: database/app/src/main/res/layout/fragment_post_detail.xml
================================================
================================================
FILE: database/app/src/main/res/layout/fragment_sign_in.xml
================================================
================================================
FILE: database/app/src/main/res/layout/include_post_author.xml
================================================
================================================
FILE: database/app/src/main/res/layout/include_post_text.xml
================================================
================================================
FILE: database/app/src/main/res/layout/item_comment.xml
================================================
================================================
FILE: database/app/src/main/res/layout/item_post.xml
================================================
================================================
FILE: database/app/src/main/res/menu/menu_main.xml
================================================
================================================
FILE: database/app/src/main/res/navigation/nav_graph_java.xml
================================================
================================================
FILE: database/app/src/main/res/navigation/nav_graph_kotlin.xml
================================================
================================================
FILE: database/app/src/main/res/values/colors.xml
================================================
#039BE5
#0288D1
#FFA000
#212121
#727272
#212121
#B6B6B6
================================================
FILE: database/app/src/main/res/values/dimens.xml
================================================
16dp
16dp
16dp
================================================
FILE: database/app/src/main/res/values/strings.xml
================================================
Firebase Database
Settings
Log in
Log out
Log In
Email
Password
Send
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nec condimentum ante, quis mattis mi. Proin ut rhoncus libero, ac euismod neque. Ut suscipit, enim a facilisis consequat, orci lacus venenatis orci, at ultricies orci nulla sed ex. In condimentum viverra velit, eget feugiat libero imperdiet vel. Aenean pretium, lectus vitae bibendum bibendum, diam turpis convallis eros, quis tristique leo sapien at ipsum. Praesent eu odio id nulla lobortis auctor. Donec eget dolor eget nisl euismod commodo vel a lacus. Quisque ut justo vitae urna pharetra accumsan. Etiam dignissim neque iaculis mauris viverra, blandit rhoncus odio porttitor. Phasellus nibh arcu.
Welcome
Sign Up
password
email
Sign In
Recent
My Posts
My Top Posts
================================================
FILE: database/app/src/main/res/values/styles.xml
================================================
================================================
FILE: database/app/src/main/res/values-v21/styles.xml
================================================
================================================
FILE: database/app/src/main/res/values-w820dp/dimens.xml
================================================
64dp
================================================
FILE: database/build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.google.services) apply false
}
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
}
}
tasks {
register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}
}
================================================
FILE: database/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: database/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
================================================
FILE: database/gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: database/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: database/settings.gradle.kts
================================================
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
include(":app")
// Required so that gradle can resolve these dependencies even when
// building only a single project.
include(":internal:lintchecks")
project(":internal:lintchecks").projectDir = file("../internal/lintchecks")
include(":internal:lint")
project(":internal:lint").projectDir = file("../internal/lint")
include(":internal:chooserx")
project(":internal:chooserx").projectDir = file("../internal/chooserx")
================================================
FILE: dataconnect/.gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
.dataconnect/
.firebaserc
.firebase/
*.log
================================================
FILE: dataconnect/README.md
================================================
# Firebase Data Connect Quickstart
## Introduction
This quickstart is a movie review app to demonstrate the use of Firebase Data Connect
with a Cloud SQL database.
For more information about Firebase Data Connect visit [the docs](https://firebase.google.com/docs/data-connect/).
## Getting Started
Follow these steps to get up and running with Firebase Data Connect. For more detailed instructions,
check out the [official documentation](https://firebase.google.com/docs/data-connect/quickstart).
### 0. Prerequisites
- Latest version of [Android Studio](https://developer.android.com/studio)
- Latest version of [Visual Studio Code](https://code.visualstudio.com/)
- The [Firebase Data Connect VS Code Extension](https://marketplace.visualstudio.com/items?itemName=GoogleCloudTools.firebase-dataconnect-vscode)
### 1. Connect to your Firebase project
1. If you haven't already, create a Firebase project.
1. In the [Firebase console](https://console.firebase.google.com), click
**Add project**, then follow the on-screen instructions.
2. Upgrade your project to the Blaze plan. This lets you create a Cloud SQL
for PostgreSQL instance.
> Note: Though you set up billing in your Blaze upgrade, you won't be
charged for usage of Firebase Data Connect or the
[default Cloud SQL for PostgreSQL configuration](https://firebase.google.com/docs/data-connect/#pricing)
during the preview.
3. Navigate to the [Data Connect section](https://console.firebase.google.com/u/0/project/_/dataconnect)
of the Firebase console, click on the "Get Started" button and follow the setup workflow:
- Select a location for your Cloud SQL for PostgreSQL database (this sample uses `us-central1`). If you choose a different location, you'll also need to change the `quickstart-android/dataconnect/dataconnect/dataconnect.yaml` file.
- Select the option to create a new Cloud SQL instance and fill in the following fields:
- Service ID: `dataconnect`
- Cloud SQL Instance ID: `fdc-sql`
- Database name: `fdcdb`
4. Allow some time for the Cloud SQL instance to be provisioned. After it's provisioned, the instance
can be managed in the [Cloud Console](https://console.cloud.google.com/sql).
5. If you haven’t already, add an Android app to your Firebase project, with the android package name `com.google.firebase.example.dataconnect`.
Click **Download google-services.json** to obtain your Firebase Android config file.
### 2. Cloning the repository
1. Clone this repository to your local machine:
```sh
git clone https://github.com/firebase/quickstart-android.git
```
2. Move the `google-services.json` config file (downloaded in the previous step) into the
`quickstart-android/dataconnect/app/` directory.
### 3. Open in Visual Studio Code (VS Code)
1. Open the `quickstart-android/dataconnect` directory in VS Code.
2. Click on the Firebase Data Connect icon on the VS Code sidebar to load the Extension.
a. Sign in with your Google Account if you haven't already.
3. Click on "Connect a Firebase project" and choose the project where you have set up Data Connect.
4. Click on "Start Emulators" - this should generate the Kotlin SDK for you and start the emulators.
### 4. Populate the database
In VS Code, open the `quickstart-android/dataconnect/dataconnect/moviedata_insert.gql` file and click the
`Run (local)` button at the top of the file.
If you’d like to confirm that the data was correctly inserted,
open `quickstart-android/dataconnect/movie-connector/queries.gql` and run the `ListMovies` query.
### 5. Running the app
Press the Run button in Android Studio to run the sample app on your device.
================================================
FILE: dataconnect/app/.gitignore
================================================
/build
================================================
FILE: dataconnect/app/build.gradle.kts
================================================
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.google.services)
alias(libs.plugins.compose.compiler)
}
android {
namespace = "com.google.firebase.example.dataconnect"
compileSdk = 36
defaultConfig {
applicationId = "com.google.firebase.example.dataconnect"
minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.13"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
sourceSets.getByName("main") {
kotlin.directories.add("build/generated/sources")
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.compose.material.icons)
implementation(libs.compose.navigation)
implementation(libs.androidx.lifecycle.runtime.compose.android)
implementation(libs.coil.compose)
// Data Connect dependencies
implementation(libs.kotlinx.serialization.core)
// Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom)
implementation(platform(libs.firebase.bom))
// Data Connect
implementation("com.google.firebase:firebase-dataconnect")
// Firebase Authentication
implementation("com.google.firebase:firebase-auth")
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
================================================
FILE: dataconnect/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: dataconnect/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/MainActivity.kt
================================================
package com.google.firebase.example.dataconnect
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.google.firebase.dataconnect.movies.MoviesConnector
import com.google.firebase.dataconnect.movies.instance
import com.google.firebase.example.dataconnect.feature.actordetail.ActorDetailRoute
import com.google.firebase.example.dataconnect.feature.actordetail.ActorDetailScreen
import com.google.firebase.example.dataconnect.feature.moviedetail.MovieDetailRoute
import com.google.firebase.example.dataconnect.feature.moviedetail.MovieDetailScreen
import com.google.firebase.example.dataconnect.feature.movies.MoviesRoute
import com.google.firebase.example.dataconnect.feature.movies.MoviesScreen
import com.google.firebase.example.dataconnect.feature.profile.ProfileRoute
import com.google.firebase.example.dataconnect.feature.profile.ProfileScreen
import com.google.firebase.example.dataconnect.feature.search.searchScreen
import com.google.firebase.example.dataconnect.ui.theme.FirebaseDataConnectTheme
data class TopLevelRoute(val labelResId: Int, val route: T, val icon: ImageVector)
val TOP_LEVEL_ROUTES = listOf(
TopLevelRoute(R.string.label_movies, MoviesRoute, Icons.Filled.Home),
TopLevelRoute(R.string.label_profile, ProfileRoute, Icons.Filled.Person)
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// Comment the line below to use a production environment instead
MoviesConnector.instance.dataConnect.useEmulator("10.0.2.2", 9399)
setContent {
FirebaseDataConnectTheme {
val navController = rememberNavController()
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
TOP_LEVEL_ROUTES.forEach { topLevelRoute ->
val label = stringResource(topLevelRoute.labelResId)
NavigationBarItem(
icon = { Icon(topLevelRoute.icon, contentDescription = label) },
label = { Text(label) },
selected = currentDestination?.hierarchy?.any {
it.hasRoute(topLevelRoute.route::class)
} == true,
onClick = {
navController.navigate(
topLevelRoute.route,
{ launchSingleTop = true }
)
}
)
}
}
}
) { innerPadding ->
NavHost(
navController,
startDestination = MoviesRoute,
Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding),
) {
composable() {
MoviesScreen(
onMovieClicked = { movieId ->
navController.navigate(
route = MovieDetailRoute(movieId),
builder = {
launchSingleTop = true
}
)
}
)
}
composable {
MovieDetailScreen(
onActorClicked = { actorId ->
navController.navigate(
ActorDetailRoute(actorId),
{ launchSingleTop = true }
)
}
)
}
composable() {
ActorDetailScreen(
onMovieClicked = { movieId ->
navController.navigate(
MovieDetailRoute(movieId),
{ launchSingleTop = true }
)
}
)
}
searchScreen()
composable { ProfileScreen(
onMovieClicked = { movieId ->
navController.navigate(
MovieDetailRoute(movieId),
{ launchSingleTop = true }
)
}
) }
}
}
}
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailScreen.kt
================================================
package com.google.firebase.example.dataconnect.feature.actordetail
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import com.google.firebase.dataconnect.movies.GetActorByIdQuery
import com.google.firebase.example.dataconnect.R
import com.google.firebase.example.dataconnect.ui.components.ErrorCard
import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
import com.google.firebase.example.dataconnect.ui.components.Movie
import com.google.firebase.example.dataconnect.ui.components.MoviesList
import kotlinx.serialization.Serializable
@Serializable
data class ActorDetailRoute(val actorId: String)
@Composable
fun ActorDetailScreen(
actorDetailViewModel: ActorDetailViewModel = viewModel(),
onMovieClicked: (actorId: String) -> Unit
) {
val uiState by actorDetailViewModel.uiState.collectAsState()
ActorDetailScreen(
uiState = uiState,
onMovieClicked = onMovieClicked
)
}
@Composable
fun ActorDetailScreen(
uiState: ActorDetailUIState,
onMovieClicked: (actorId: String) -> Unit
) {
when (uiState) {
is ActorDetailUIState.Error -> ErrorCard(uiState.errorMessage)
is ActorDetailUIState.Loading -> LoadingScreen()
is ActorDetailUIState.Success -> {
Scaffold { innerPadding ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.padding(innerPadding)
.verticalScroll(scrollState)
) {
ActorInformation(
actor = uiState.actor,
)
MoviesList(
listTitle = stringResource(R.string.title_main_roles),
movies = uiState.actor?.mainActors?.mapNotNull {
Movie(it.id.toString(), it.imageUrl, it.title)
},
onMovieClicked = onMovieClicked
)
Spacer(modifier = Modifier.height(8.dp))
MoviesList(
listTitle = stringResource(R.string.title_supporting_actors),
movies = uiState.actor?.supportingActors?.mapNotNull {
Movie(it.id.toString(), it.imageUrl, it.title)
},
onMovieClicked = onMovieClicked
)
}
}
}
}
}
@Composable
fun ActorInformation(
actor: GetActorByIdQuery.Data.Actor?
) {
if (actor == null) {
ErrorCard(stringResource(R.string.error_actor_not_found))
} else {
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = actor.imageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.width(120.dp)
.padding(vertical = 8.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = actor.name,
style = MaterialTheme.typography.headlineLarge
)
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailUIState.kt
================================================
package com.google.firebase.example.dataconnect.feature.actordetail
import com.google.firebase.dataconnect.movies.GetActorByIdQuery
import com.google.firebase.dataconnect.movies.GetMovieByIdQuery
sealed class ActorDetailUIState {
data object Loading: ActorDetailUIState()
data class Error(val errorMessage: String?): ActorDetailUIState()
data class Success(
// Actor is null if it can't be found on the DB
val actor: GetActorByIdQuery.Data.Actor?,
val isUserSignedIn: Boolean = false,
) : ActorDetailUIState()
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/actordetail/ActorDetailViewModel.kt
================================================
package com.google.firebase.example.dataconnect.feature.actordetail
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.auth
import com.google.firebase.dataconnect.movies.MoviesConnector
import com.google.firebase.dataconnect.movies.execute
import com.google.firebase.dataconnect.movies.instance
import java.util.UUID
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class ActorDetailViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val actorDetailRoute = savedStateHandle.toRoute()
private val actorId: String = actorDetailRoute.actorId
private val firebaseAuth: FirebaseAuth = Firebase.auth
private val moviesConnector: MoviesConnector = MoviesConnector.instance
private val _uiState = MutableStateFlow(ActorDetailUIState.Loading)
val uiState: StateFlow
get() = _uiState
init {
fetchActor()
}
private fun fetchActor() {
viewModelScope.launch {
try {
val user = firebaseAuth.currentUser
val actor = moviesConnector.getActorById.execute(
id = UUID.fromString(actorId)
).data.actor
_uiState.value = ActorDetailUIState.Success(
actor = actor,
isUserSignedIn = user != null
)
} catch (e: Exception) {
_uiState.value = ActorDetailUIState.Error(e.message)
}
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailScreen.kt
================================================
package com.google.firebase.example.dataconnect.feature.moviedetail
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.FavoriteBorder
import androidx.compose.material.icons.outlined.Star
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import com.google.firebase.dataconnect.movies.GetMovieByIdQuery
import com.google.firebase.example.dataconnect.R
import com.google.firebase.example.dataconnect.ui.components.Actor
import com.google.firebase.example.dataconnect.ui.components.ActorsList
import com.google.firebase.example.dataconnect.ui.components.ErrorCard
import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
import com.google.firebase.example.dataconnect.ui.components.ReviewCard
import com.google.firebase.example.dataconnect.ui.components.ToggleButton
import kotlinx.serialization.Serializable
@Serializable
data class MovieDetailRoute(val movieId: String)
@Composable
fun MovieDetailScreen(
onActorClicked: (actorId: String) -> Unit,
movieDetailViewModel: MovieDetailViewModel = viewModel()
) {
val uiState by movieDetailViewModel.uiState.collectAsState()
Scaffold { padding ->
when (uiState) {
is MovieDetailUIState.Error -> {
ErrorCard((uiState as MovieDetailUIState.Error).errorMessage)
}
MovieDetailUIState.Loading -> LoadingScreen()
is MovieDetailUIState.Success -> {
val ui = uiState as MovieDetailUIState.Success
val movie = ui.movie
val scrollState = rememberScrollState()
Column(
modifier = Modifier.verticalScroll(scrollState)
) {
MovieInformation(
modifier = Modifier.padding(padding),
movie = movie,
isMovieFavorite = ui.isFavorite,
onFavoriteToggled = { newValue ->
movieDetailViewModel.toggleFavorite(newValue)
},
)
// Main Actors list
ActorsList(
listTitle = stringResource(R.string.title_main_actors),
actors = movie?.mainActors?.mapNotNull {
Actor(it.id.toString(), it.name, it.imageUrl)
},
onActorClicked = { onActorClicked(it) }
)
// Supporting Actors list
ActorsList(
listTitle = stringResource(R.string.title_supporting_actors),
actors = movie?.supportingActors?.mapNotNull {
Actor(it.id.toString(), it.name, it.imageUrl)
},
onActorClicked = { onActorClicked(it) }
)
UserReviews(
onReviewSubmitted = { rating, text ->
movieDetailViewModel.addRating(rating, text)
},
movie?.reviews
)
}
}
}
}
}
@Composable
fun MovieInformation(
modifier: Modifier = Modifier,
movie: GetMovieByIdQuery.Data.Movie?,
isMovieFavorite: Boolean,
onFavoriteToggled: (newValue: Boolean) -> Unit
) {
if (movie == null) {
ErrorCard(stringResource(R.string.error_movie_not_found))
} else {
Column(
modifier = modifier
.padding(16.dp)
) {
Text(
text = movie.title,
style = MaterialTheme.typography.headlineLarge
)
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = movie.releaseYear.toString(),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(end = 4.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Icon(Icons.Outlined.Star, "Favorite")
Text(
text = movie.rating?.toString() ?: "0.0",
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 2.dp)
)
}
Row {
AsyncImage(
model = movie.imageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.width(150.dp)
.aspectRatio(9f / 16f)
.padding(vertical = 8.dp)
)
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
Row {
movie.tags?.let { movieTags ->
movieTags.filterNotNull().forEach { tag ->
SuggestionChip(
onClick = { },
label = { Text(tag) },
modifier = Modifier
.padding(horizontal = 4.dp)
)
}
}
}
Text(
text = movie.description ?: stringResource(R.string.description_not_available),
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(8.dp))
ToggleButton(
iconEnabled = Icons.Filled.Favorite,
iconDisabled = Icons.Outlined.FavoriteBorder,
textEnabled = stringResource(R.string.button_remove_favorite),
textDisabled = stringResource(R.string.button_favorite),
isEnabled = isMovieFavorite,
onToggle = onFavoriteToggled
)
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailUIState.kt
================================================
package com.google.firebase.example.dataconnect.feature.moviedetail
import com.google.firebase.dataconnect.movies.GetMovieByIdQuery
sealed class MovieDetailUIState {
data object Loading: MovieDetailUIState()
data class Error(val errorMessage: String?): MovieDetailUIState()
data class Success(
// Movie is null if it can't be found on the DB
val movie: GetMovieByIdQuery.Data.Movie?,
val isUserSignedIn: Boolean = false,
var isFavorite: Boolean = false
) : MovieDetailUIState()
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/MovieDetailViewModel.kt
================================================
package com.google.firebase.example.dataconnect.feature.moviedetail
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.auth
import com.google.firebase.dataconnect.movies.MoviesConnector
import com.google.firebase.dataconnect.movies.execute
import com.google.firebase.dataconnect.movies.instance
import java.util.UUID
import kotlin.math.roundToInt
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
class MovieDetailViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val movieDetailRoute = savedStateHandle.toRoute()
private val movieId: String = movieDetailRoute.movieId
private val firebaseAuth: FirebaseAuth = Firebase.auth
private val moviesConnector: MoviesConnector = MoviesConnector.instance
private val _uiState = MutableStateFlow(MovieDetailUIState.Loading)
val uiState: StateFlow
get() = _uiState
init {
fetchMovie()
}
private fun fetchMovie() {
viewModelScope.launch {
try {
val user = firebaseAuth.currentUser
val movie = moviesConnector.getMovieById.execute(
id = UUID.fromString(movieId)
).data.movie
_uiState.value = if (user == null) {
MovieDetailUIState.Success(movie, isUserSignedIn = false)
} else {
val isFavorite = moviesConnector.getIfFavoritedMovie.execute(
movieId = UUID.fromString(movieId)
).data.favorite_movie != null
MovieDetailUIState.Success(
movie = movie,
isUserSignedIn = true,
isFavorite = isFavorite
)
}
} catch (e: Exception) {
_uiState.value = MovieDetailUIState.Error(e.message)
}
}
}
fun toggleFavorite(newValue: Boolean) {
// TODO(thatfiredev): hide the button if there's no user logged in
val uid = firebaseAuth.currentUser?.uid ?: return
viewModelScope.launch {
try {
if (newValue) {
moviesConnector.addFavoritedMovie.execute(UUID.fromString(movieId))
} else {
moviesConnector.deleteFavoritedMovie.execute(
movieId = UUID.fromString(movieId)
)
}
// Re-run the query to fetch movie
fetchMovie()
} catch (e: Exception) {
_uiState.value = MovieDetailUIState.Error(e.message)
}
}
}
fun addRating(rating: Float, text: String) {
// TODO(thatfiredev): hide the button if there's no user logged in
if (firebaseAuth.currentUser?.uid == null) return
viewModelScope.launch {
try {
moviesConnector.addReview.execute(
movieId = UUID.fromString(movieId),
rating = rating.roundToInt(),
reviewText = text
)
// Re-run the query to fetch movie
fetchMovie()
} catch (e: Exception) {
_uiState.value = MovieDetailUIState.Error(e.message)
}
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/moviedetail/UserReviews.kt
================================================
package com.google.firebase.example.dataconnect.feature.moviedetail
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.google.firebase.dataconnect.movies.GetMovieByIdQuery
import com.google.firebase.example.dataconnect.R
import com.google.firebase.example.dataconnect.ui.components.ReviewCard
@Composable
fun UserReviews(
onReviewSubmitted: (rating: Float, text: String) -> Unit,
reviews: List? = emptyList()
) {
var reviewText by remember { mutableStateOf("") }
Text(
text = "User Reviews",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
var rating by remember { mutableFloatStateOf(5f) }
Text("Rating: ${rating}")
Slider(
value = rating,
onValueChange = { rating = Math.round(it).toFloat() },
steps = 10,
valueRange = 1f..10f
)
TextField(
value = reviewText,
onValueChange = { if (it.length <= 280) reviewText = it },
label = { Text(stringResource(R.string.hint_write_review)) },
supportingText = {
Text(
"${reviewText.length} / 280",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.End
)
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
if (!reviewText.isNullOrEmpty()) {
onReviewSubmitted(rating, reviewText)
reviewText = ""
}
}
) {
Text(stringResource(R.string.button_submit_review))
}
}
Column {
// TODO(thatfiredev): Handle cases where the list is too long to display
reviews.orEmpty().forEach {
ReviewCard(
userName = it.user.username,
date = it.reviewDate,
rating = it.rating?.toDouble() ?: 0.0,
text = it.reviewText ?: "",
)
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesScreen.kt
================================================
package com.google.firebase.example.dataconnect.feature.movies
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.example.dataconnect.R
import com.google.firebase.example.dataconnect.ui.components.ErrorCard
import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
import com.google.firebase.example.dataconnect.ui.components.Movie
import com.google.firebase.example.dataconnect.ui.components.MoviesList
import kotlinx.serialization.Serializable
@Serializable
object MoviesRoute
@Composable
fun MoviesScreen(
onMovieClicked: (movie: String) -> Unit,
moviesViewModel: MoviesViewModel = viewModel()
) {
val movies by moviesViewModel.uiState.collectAsState()
MoviesScreen(movies, onMovieClicked)
}
@Composable
fun MoviesScreen(
uiState: MoviesUIState,
onMovieClicked: (movie: String) -> Unit
) {
when (uiState) {
MoviesUIState.Loading -> LoadingScreen()
is MoviesUIState.Error -> ErrorCard(uiState.errorMessage)
is MoviesUIState.Success -> {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.verticalScroll(scrollState)
) {
MoviesList(
listTitle = stringResource(R.string.title_top_10_movies),
movies = uiState.top10movies.mapNotNull {
Movie(it.id.toString(), it.imageUrl, it.title, it.rating?.toFloat())
},
onMovieClicked = onMovieClicked
)
Spacer(modifier = Modifier.height(16.dp))
MoviesList(
listTitle = stringResource(R.string.title_latest_movies),
movies = uiState.latestMovies.mapNotNull {
Movie(it.id.toString(), it.imageUrl, it.title, it.rating?.toFloat())
},
onMovieClicked = onMovieClicked
)
}
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesUIState.kt
================================================
package com.google.firebase.example.dataconnect.feature.movies
import com.google.firebase.dataconnect.movies.ListMoviesQuery
sealed class MoviesUIState {
data object Loading: MoviesUIState()
data class Error(val errorMessage: String?): MoviesUIState()
data class Success(
val top10movies: List,
val latestMovies: List
) : MoviesUIState()
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/movies/MoviesViewModel.kt
================================================
package com.google.firebase.example.dataconnect.feature.movies
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.dataconnect.movies.MoviesConnector
import com.google.firebase.dataconnect.movies.OrderDirection
import com.google.firebase.dataconnect.movies.execute
import com.google.firebase.dataconnect.movies.instance
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class MoviesViewModel(
private val moviesConnector: MoviesConnector = MoviesConnector.instance
) : ViewModel() {
private val _uiState = MutableStateFlow(MoviesUIState.Loading)
val uiState: StateFlow
get() = _uiState
init {
viewModelScope.launch {
try {
val top10Movies = moviesConnector.listMovies.execute {
orderByRating = OrderDirection.DESC
limit = 10
}.data.movies
val latestMovies = moviesConnector.listMovies.execute {
orderByReleaseYear = OrderDirection.DESC
}.data.movies
_uiState.value = MoviesUIState.Success(top10Movies, latestMovies)
} catch (e: Exception) {
_uiState.value = MoviesUIState.Error(e.localizedMessage)
}
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/AuthScreen.kt
================================================
package com.google.firebase.example.dataconnect.feature.profile
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
@Composable
fun AuthScreen(
onSignUp: (email: String, password: String, displayName: String) -> Unit,
onSignIn: (email: String, password: String) -> Unit,
) {
var isSignUp by remember { mutableStateOf(false) }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var displayName by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") }
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
visualTransformation = PasswordVisualTransformation()
)
Spacer(modifier = Modifier.height(8.dp))
if (isSignUp) {
OutlinedTextField(
value = displayName,
onValueChange = { displayName = it },
label = { Text("Name") }
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = {
if (isSignUp) {
onSignUp(email, password, displayName)
} else {
onSignIn(email, password)
}
}) {
Text(
text = if (isSignUp) {
"Sign up"
} else {
"Sign in"
}
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = if (isSignUp) {
"Already have an account?"
} else {
"Don't have an account?"
}
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = {
isSignUp = !isSignUp
}) {
Text(
text = if (isSignUp) {
"Sign in"
} else {
"Sign up"
}
)
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileScreen.kt
================================================
package com.google.firebase.example.dataconnect.feature.profile
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.dataconnect.movies.GetCurrentUserQuery
import com.google.firebase.example.dataconnect.R
import com.google.firebase.example.dataconnect.ui.components.Actor
import com.google.firebase.example.dataconnect.ui.components.ActorsList
import com.google.firebase.example.dataconnect.ui.components.ErrorCard
import com.google.firebase.example.dataconnect.ui.components.LoadingScreen
import com.google.firebase.example.dataconnect.ui.components.Movie
import com.google.firebase.example.dataconnect.ui.components.MoviesList
import com.google.firebase.example.dataconnect.ui.components.ReviewCard
import kotlinx.serialization.Serializable
@Serializable
object ProfileRoute
@Composable
fun ProfileScreen(
profileViewModel: ProfileViewModel = viewModel(),
onMovieClicked: (String) -> Unit,
) {
val uiState by profileViewModel.uiState.collectAsState()
when (uiState) {
is ProfileUIState.Error -> {
ErrorCard((uiState as ProfileUIState.Error).errorMessage)
}
is ProfileUIState.AuthState -> {
AuthScreen(
onSignUp = { email, password, displayName ->
profileViewModel.signUp(email, password, displayName)
},
onSignIn = { email, password ->
profileViewModel.signIn(email, password)
}
)
}
is ProfileUIState.ProfileState -> {
val ui = uiState as ProfileUIState.ProfileState
ProfileScreen(
ui.username ?: "User",
ui.reviews.orEmpty(),
ui.favoriteMovies.orEmpty(),
onMovieClicked = onMovieClicked,
onSignOut = {
profileViewModel.signOut()
}
)
}
ProfileUIState.Loading -> LoadingScreen()
}
}
@Composable
fun ProfileScreen(
name: String,
reviews: List,
favoriteMovies: List,
onMovieClicked: (String) -> Unit,
onSignOut: () -> Unit
) {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.padding(vertical = 16.dp)
.verticalScroll(scrollState)
) {
Text(
text = "Welcome back, $name!",
style = MaterialTheme.typography.displaySmall,
modifier = Modifier.padding(horizontal = 16.dp)
)
TextButton(
onClick = {
onSignOut()
},
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text("Sign out")
}
Spacer(modifier = Modifier.height(16.dp))
MoviesList(
listTitle = stringResource(R.string.title_favorite_movies),
movies = favoriteMovies.mapNotNull {
Movie(it.movie.id.toString(), it.movie.imageUrl, it.movie.title, it.movie.rating?.toFloat())
},
onMovieClicked = onMovieClicked
)
Spacer(modifier = Modifier.height(16.dp))
ProfileSection(title = "Reviews", content = { ReviewsList(name, reviews) })
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
fun ProfileSection(title: String, content: @Composable () -> Unit) {
Column {
Text(
text = title,
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(horizontal = 16.dp)
)
Spacer(modifier = Modifier.height(8.dp))
content()
}
}
@Composable
fun ReviewsList(
userName: String,
reviews: List
) {
Column {
// TODO(thatfiredev): Handle cases where the list is too long to display
reviews.forEach { review ->
ReviewCard(
userName = userName,
date = review.reviewDate,
rating = review.rating?.toDouble() ?: 0.0,
text = review.reviewText ?: "",
movieName = review.movie.title
)
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileUIState.kt
================================================
package com.google.firebase.example.dataconnect.feature.profile
import com.google.firebase.dataconnect.movies.GetCurrentUserQuery
sealed class ProfileUIState {
data object Loading: ProfileUIState()
data class Error(val errorMessage: String?): ProfileUIState()
data object AuthState: ProfileUIState()
data class ProfileState(
val username: String?,
val reviews: List? = emptyList(),
val favoriteMovies: List? = emptyList(),
) : ProfileUIState()
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/profile/ProfileViewModel.kt
================================================
package com.google.firebase.example.dataconnect.feature.profile
import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.Firebase
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseAuth.AuthStateListener
import com.google.firebase.auth.UserProfileChangeRequest
import com.google.firebase.auth.auth
import com.google.firebase.dataconnect.movies.MoviesConnector
import com.google.firebase.dataconnect.movies.execute
import com.google.firebase.dataconnect.movies.instance
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
class ProfileViewModel(
private val auth: FirebaseAuth = Firebase.auth,
private val moviesConnector: MoviesConnector = MoviesConnector.instance
) : ViewModel() {
private val _uiState = MutableStateFlow(ProfileUIState.Loading)
val uiState: StateFlow
get() = _uiState
private val authStateListener: AuthStateListener
init {
authStateListener = AuthStateListener {
val currentUser = auth.currentUser
if (currentUser != null) {
displayUser(currentUser.uid)
} else {
_uiState.value = ProfileUIState.AuthState
}
}
auth.addAuthStateListener(authStateListener)
}
fun signUp(
email: String,
password: String,
displayName: String
) {
viewModelScope.launch {
try {
val signInResult = auth.createUserWithEmailAndPassword(email, password).await()
signInResult.user?.updateProfile(
UserProfileChangeRequest.Builder()
.setDisplayName(displayName)
.build()
)?.await()
moviesConnector.upsertUser.execute(username = displayName)
} catch (e: Exception) {
_uiState.value = ProfileUIState.Error(e.message)
e.printStackTrace()
}
}
}
fun signIn(email: String, password: String) {
viewModelScope.launch {
try {
auth.signInWithEmailAndPassword(email, password).await()
} catch (e: Exception) {
_uiState.value = ProfileUIState.Error(e.message)
}
}
}
fun signOut() {
auth.signOut()
}
private fun displayUser(
userId: String
) {
viewModelScope.launch {
try {
val user = moviesConnector.getCurrentUser.execute().data.user
_uiState.value = ProfileUIState.ProfileState(
user?.username,
favoriteMovies = user?.favoriteMovies,
reviews = user?.reviews
)
Log.d("DisplayUser", "$user")
} catch (e: Exception) {
_uiState.value = ProfileUIState.Error(e.message)
}
}
}
override fun onCleared() {
super.onCleared()
auth.removeAuthStateListener(authStateListener)
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/feature/search/Navigation.kt
================================================
package com.google.firebase.example.dataconnect.feature.search
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.composable
const val SEARCH_ROUTE = "search_route"
fun NavController.navigateToSearch(navOptions: NavOptionsBuilder.() -> Unit) =
navigate(SEARCH_ROUTE, navOptions)
fun NavGraphBuilder.searchScreen(
) {
composable(route = SEARCH_ROUTE) {
// TODO: Call composable
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ActorsList.kt
================================================
package com.google.firebase.example.dataconnect.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
val ACTOR_CARD_SIZE = 64.dp
/**
* Used to represent an actor in a list UI
*/
data class Actor(
val id: String,
val name: String,
val imageUrl: String
)
/**
* Displays a scrollable horizontal list of actors.
*/
@Composable
fun ActorsList(
modifier: Modifier = Modifier,
listTitle: String,
actors: List? = emptyList(),
onActorClicked: (actorId: String) -> Unit
) {
Column(
modifier = modifier.padding(horizontal = 16.dp)
.fillMaxWidth()
) {
Text(
text = listTitle,
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(8.dp))
LazyRow {
items(actors.orEmpty()) { actor ->
ActorTile(actor, onActorClicked)
}
}
}
}
/**
* Used to display each actor item in the list.
*/
@Composable
fun ActorTile(
actor: Actor,
onActorClicked: (actorId: String) -> Unit
) {
Card(
modifier = Modifier
.padding(end = 8.dp)
.clickable {
onActorClicked(actor.id)
}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.sizeIn(
maxWidth = 160.dp,
maxHeight = ACTOR_CARD_SIZE + 16.dp
)
.padding(8.dp)
.fillMaxWidth()
) {
AsyncImage(
model = actor.imageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.padding(end = 8.dp)
.size(ACTOR_CARD_SIZE)
.clip(CircleShape)
)
Text(
text = actor.name,
style = MaterialTheme.typography.bodyLarge,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ErrorCard.kt
================================================
package com.google.firebase.example.dataconnect.ui.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.firebase.example.dataconnect.R
@Composable
fun ErrorCard(
errorMessage: String?
) {
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
modifier = Modifier.padding(16.dp)
.fillMaxWidth()
) {
Text(
text = errorMessage ?: stringResource(R.string.unknown_error),
modifier = Modifier.padding(16.dp)
.fillMaxWidth()
)
}
}
@Composable
@Preview
fun ErrorCardPreview() {
ErrorCard("Something went terribly wrong :(")
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/LoadingScreen.kt
================================================
package com.google.firebase.example.dataconnect.ui.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
/**
* A screen that displays a loading spinner in the center.
*/
@Composable
fun LoadingScreen() {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator()
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/MoviesList.kt
================================================
package com.google.firebase.example.dataconnect.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
/**
* Used to represent a movie in a list UI
*/
data class Movie(
val id: String,
val imageUrl: String,
val title: String,
val rating: Float? = null
)
/**
* Displays a scrollable horizontal list of movies.
*/
@Composable
fun MoviesList(
modifier: Modifier = Modifier,
listTitle: String,
movies: List? = emptyList(),
onMovieClicked: (movieId: String) -> Unit
) {
Column(
modifier = modifier.padding(horizontal = 16.dp)
.fillMaxWidth()
) {
Text(
text = listTitle,
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 8.dp)
)
LazyRow {
items(movies.orEmpty()) { movie ->
MovieTile(
movie = movie,
onMovieClicked = {
onMovieClicked(movie.id.toString())
}
)
}
}
}
}
/**
* Used to display each movie item in the list.
*/
@Composable
fun MovieTile(
modifier: Modifier = Modifier,
tileWidth: Dp = 150.dp,
movie: Movie,
onMovieClicked: (movieId: String) -> Unit
) {
Card(
modifier = modifier
.padding(4.dp)
.sizeIn(maxWidth = tileWidth)
.clickable {
onMovieClicked(movie.id)
},
) {
AsyncImage(
model = movie.imageUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.aspectRatio(9f / 16f)
)
Text(
text = movie.title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(8.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
movie.rating?.let {
Text(
text = "Rating: $it",
modifier = Modifier.padding(bottom = 8.dp, start = 8.dp, end = 8.dp),
style = MaterialTheme.typography.bodySmall
)
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ReviewCard.kt
================================================
package com.google.firebase.example.dataconnect.ui.components
import android.os.Build
import android.widget.Space
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.text
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.firebase.dataconnect.LocalDate
import com.google.firebase.dataconnect.toJavaLocalDate
import java.text.SimpleDateFormat
import java.time.format.DateTimeFormatter
import java.util.Locale
@Composable
fun ReviewCard(
userName: String,
date: LocalDate,
rating: Double,
text: String,
movieName: String? = null
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colorScheme.secondaryContainer)
.padding(16.dp)
) {
Text(
text = if (movieName != null) {
userName + " on " + movieName
} else {
userName
},
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge
)
Row(
modifier = Modifier.padding(bottom = 8.dp)
) {
Text(
text =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val dateFormatter = DateTimeFormatter.ofPattern("dd MMM, yyyy", Locale.getDefault())
date.toJavaLocalDate().format(dateFormatter)
} else {
val parseableDateString = date.run {
val year = "$year".padStart(4, '0')
val month = "$month".padStart(2, '0')
val day = "$day".padStart(2, '0')
"$year-$month-$day"
}
val dateParser = SimpleDateFormat("y-M-d", Locale.US)
val parsedDate = dateParser.parse(parseableDateString) ?:
throw Exception("INTERNAL ERROR: unparseable date string: $parseableDateString")
val dateFormatter = SimpleDateFormat("dd MMM, yyyy", Locale.getDefault())
dateFormatter.format(parsedDate)
},
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Rating: ",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "$rating",
style = MaterialTheme.typography.titleMedium
)
}
Text(
text = text,
modifier = Modifier.fillMaxWidth()
)
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/components/ToggleButton.kt
================================================
package com.google.firebase.example.dataconnect.ui.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@Composable
fun ToggleButton(
iconEnabled: ImageVector,
iconDisabled: ImageVector,
textEnabled: String,
textDisabled: String,
isEnabled: Boolean,
onToggle: (newValue: Boolean) -> Unit
) {
val onClick = {
onToggle(!isEnabled)
}
if (isEnabled) {
FilledTonalButton(onClick) {
Icon(iconEnabled, textEnabled)
Text(textEnabled, modifier = Modifier.padding(horizontal = 4.dp))
}
} else {
OutlinedButton(onClick) {
Icon(iconDisabled, textDisabled)
Text(textDisabled, modifier = Modifier.padding(horizontal = 4.dp))
}
}
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Color.kt
================================================
package com.google.firebase.example.dataconnect.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Theme.kt
================================================
package com.google.firebase.example.dataconnect.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun FirebaseDataConnectTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
================================================
FILE: dataconnect/app/src/main/java/com/google/firebase/example/dataconnect/ui/theme/Type.kt
================================================
package com.google.firebase.example.dataconnect.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
================================================
FILE: dataconnect/app/src/main/res/drawable/firebase_data_connect.xml
================================================
================================================
FILE: dataconnect/app/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: dataconnect/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: dataconnect/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: dataconnect/app/src/main/res/values/colors.xml
================================================
#FFBB86FC
#FF6200EE
#FF3700B3
#FF03DAC5
#FF018786
#FF000000
#FFFFFFFF
================================================
FILE: dataconnect/app/src/main/res/values/strings.xml
================================================
Firebase Data Connect
An unknown error occurred
Movies
Genres
Search
Profile
Top 10 Movies
Latest Movies
%s Movies
Most Popular
Most Recent
Couldn\'t find movie in the database
Description not available
Mark as watched
Watched
Add to favorites
Favorite
Main Actors
Supporting Actors
User Reviews
Write your review
Submit Review
Couldn\'t find actor in the database
Main Roles
Supporting Roles
Favorite Movies
Reviews
================================================
FILE: dataconnect/app/src/main/res/values/themes.xml
================================================
================================================
FILE: dataconnect/app/src/main/res/xml/backup_rules.xml
================================================
================================================
FILE: dataconnect/app/src/main/res/xml/data_extraction_rules.xml
================================================
================================================
FILE: dataconnect/build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.compose.compiler) apply false
}
tasks {
register("dataconnectCompile") {
workingDir = project.file("./dataconnect")
if (org.apache.tools.ant.taskdefs.condition.Os.isFamily(org.apache.tools.ant.taskdefs.condition.Os.FAMILY_WINDOWS)) {
commandLine("npx.cmd", "-y", "firebase-tools@latest", "dataconnect:compile")
} else {
commandLine("npx", "-y", "firebase-tools@latest", "dataconnect:compile")
}
isIgnoreExitValue = true
}
register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
finalizedBy("dataconnectCompile")
}
}
================================================
FILE: dataconnect/dataconnect/dataconnect.yaml
================================================
specVersion: "v1"
serviceId: "dataconnect"
location: "us-central1"
schema:
source: "./schema"
datasource:
postgresql:
database: "fdcdb"
cloudSql:
instanceId: "fdc-sql"
connectorDirs: ["./movie-connector"]
================================================
FILE: dataconnect/dataconnect/movie-connector/connector.yaml
================================================
connectorId: movies
# Required. Accepted values are either "PUBLIC" or "ADMIN" (only "PUBLIC" for gated private
# preview). If "ADMIN", the connector in this directory is an AdminConnector and its operations
# are gated by IAM.
authMode: PUBLIC
generate:
# (Web SDK generation omitted, but can be found in https://github.com/firebase/quickstart-js)
kotlinSdk:
# Create a custom package name for your generated SDK
package: com.google.firebase.dataconnect.movies
# Specify where to store the generated SDK
# We're using the build/ directory so that generated code doesn't get checked into git
outputDir: ../../app/build/generated/sources
================================================
FILE: dataconnect/dataconnect/movie-connector/mutations.gql
================================================
mutation UpsertUser($username: String!) @auth(level: USER) {
user_upsert(
data: {
id_expr: "auth.uid"
username: $username
}
)
}
# Add a movie to the user's favorites list
mutation AddFavoritedMovie($movieId: UUID!) @auth(level: USER) {
favorite_movie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId })
}
# Remove a movie from the user's favorites list
mutation DeleteFavoritedMovie($movieId: UUID!) @auth(level: USER) {
favorite_movie_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
}
# Add a review for a movie
mutation AddReview($movieId: UUID!, $rating: Int!, $reviewText: String!)
@auth(level: USER) {
review_insert(
data: {
userId_expr: "auth.uid"
movieId: $movieId
rating: $rating
reviewText: $reviewText
reviewDate_date: { today: true }
}
)
}
# Update a user's review for a movie
mutation UpdateReview($movieId: UUID!, $rating: Int!, $reviewText: String!)
@auth(level: USER) {
review_update(
key: { userId_expr: "auth.uid", movieId: $movieId }
data: {
userId_expr: "auth.uid"
movieId: $movieId
rating: $rating
reviewText: $reviewText
reviewDate_date: { today: true }
}
)
}
# Delete a user's review for a movie
mutation DeleteReview($movieId: UUID!) @auth(level: USER) {
review_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
}
# The mutations below are unused by the application, but are useful examples for more complex cases
# Create a movie based on user input
# mutation CreateMovie(
# $title: String!
# $releaseYear: Int!
# $genre: String!
# $rating: Float
# $description: String
# $imageUrl: String!
# $tags: [String!] = []
# ) @auth(expr: "auth.token.isAdmin == true") {
# }
# Update movie information based on the provided ID
# mutation UpdateMovie(
# $id: UUID!
# $title: String
# $releaseYear: Int
# $genre: String
# $rating: Float
# $description: String
# $imageUrl: String
# $tags: [String!] = []
# ) @auth(level: USER_EMAIL_VERIFIED) {
# movie_update(
# id: $id
# data: {
# title: $title
# releaseYear: $releaseYear
# genre: $genre
# rating: $rating
# description: $description
# imageUrl: $imageUrl
# tags: $tags
# }
# )
# }
# Delete a movie by its ID
# mutation DeleteMovie($id: UUID!) @auth(level: USER_EMAIL_VERIFIED) {
# movie_delete(id: $id)
# }
# Delete movies with a rating lower than the specified minimum rating
# mutation DeleteUnpopularMovies($minRating: Float!) @auth(level: USER_EMAIL_VERIFIED) {
# movie_deleteMany(where: { rating: { le: $minRating } })
# }
# End of example mutations
================================================
FILE: dataconnect/dataconnect/movie-connector/queries.gql
================================================
# List subset of fields for movies
query ListMovies($orderByRating: OrderDirection, $orderByReleaseYear: OrderDirection, $limit: Int) @auth(level: PUBLIC, insecureReason: "Test Mode") {
movies(
orderBy: [
{ rating: $orderByRating },
{ releaseYear: $orderByReleaseYear }
]
limit: $limit
) {
id
title
imageUrl
releaseYear
genre
rating
tags
description
}
}
# Get movie by id
query GetMovieById($id: UUID!) @auth(level: PUBLIC, insecureReason: "Test Mode") {
movie(id: $id) {
id
title
imageUrl
releaseYear
genre
rating
description
tags
metadata: movieMetadatas_on_movie {
director
}
mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) {
id
name
imageUrl
}
supportingActors: actors_via_MovieActor(
where: { role: { eq: "supporting" } }
) {
id
name
imageUrl
}
reviews: reviews_on_movie {
id
reviewText
reviewDate
rating
user {
id
username
}
}
}
}
# Get actor by id
query GetActorById($id: UUID!) @auth(level: PUBLIC, insecureReason: "Test Mode") {
actor(id: $id) {
id
name
imageUrl
mainActors: movies_via_MovieActor(where: { role: { eq: "main" } }) {
id
title
genre
tags
imageUrl
}
supportingActors: movies_via_MovieActor(
where: { role: { eq: "supporting" } }
) {
id
title
genre
tags
imageUrl
}
}
}
# Get user by ID
query GetCurrentUser @auth(level: USER) {
user(key: { id_expr: "auth.uid" }) {
id
username
reviews: reviews_on_user {
id
rating
reviewDate
reviewText
movie {
id
title
}
}
favoriteMovies: favorite_movies_on_user {
movie {
id
title
genre
imageUrl
releaseYear
rating
description
tags
metadata: movieMetadatas_on_movie {
director
}
}
}
}
}
query GetIfFavoritedMovie($movieId: UUID!) @auth(level: USER) {
favorite_movie(key: { userId_expr: "auth.uid", movieId: $movieId }) {
movieId
}
}
# Search for movies, actors, and reviews
query SearchAll(
$input: String
$minYear: Int!
$maxYear: Int!
$minRating: Float!
$maxRating: Float!
$genre: String!
) @auth(level: PUBLIC, insecureReason: "Test Mode") {
moviesMatchingTitle: movies(
where: {
_and: [
{ releaseYear: { ge: $minYear } }
{ releaseYear: { le: $maxYear } }
{ rating: { ge: $minRating } }
{ rating: { le: $maxRating } }
{ genre: { contains: $genre } }
{ title: { contains: $input } }
]
}
) {
id
title
genre
rating
imageUrl
}
moviesMatchingDescription: movies(
where: {
_and: [
{ releaseYear: { ge: $minYear } }
{ releaseYear: { le: $maxYear } }
{ rating: { ge: $minRating } }
{ rating: { le: $maxRating } }
{ genre: { contains: $genre } }
{ description: { contains: $input } }
]
}
) {
id
title
genre
rating
imageUrl
}
actorsMatchingName: actors(where: { name: { contains: $input } }) {
id
name
imageUrl
}
reviewsMatchingText: reviews(where: { reviewText: { contains: $input } }) {
id
rating
reviewText
reviewDate
movie {
id
title
}
user {
id
username
}
}
}
# Search movie descriptions using L2 similarity with Vertex AI
# query SearchMovieDescriptionUsingL2Similarity($query: String!)
# @auth(level: PUBLIC) {
# movies_descriptionEmbedding_similarity(
# compare_embed: { model: "textembedding-gecko@003", text: $query }
# method: L2
# within: 2
# limit: 5
# ) {
# id
# title
# description
# tags
# rating
# imageUrl
# }
# }
# # The queries below are unused by the application, but are useful examples for more complex cases
# # List movies by partial title match
# query ListMoviesByPartialTitle($input: String!) @auth(level: PUBLIC) {
# movies(where: { title: { contains: $input } }) {
# id
# title
# genre
# rating
# imageUrl
# }
# }
# # List movies by tag
# query ListMoviesByTag($tag: String!) @auth(level: PUBLIC) {
# movies(where: { tags: { includes: $tag } }) {
# id
# title
# imageUrl
# genre
# rating
# }
# }
# # List movies by release year range
# query MoviesByReleaseYear($min: Int, $max: Int) @auth(level: PUBLIC) {
# movies(
# where: { releaseYear: { le: $max, ge: $min } }
# orderBy: { releaseYear: ASC }
# ) {
# id
# rating
# title
# imageUrl
# }
# }
# # List movies by rating and genre with OR filters
# query SearchMovieOr(
# $minRating: Float
# $maxRating: Float
# $genre: String
# $tag: String
# $input: String
# ) @auth(level: PUBLIC) {
# movies(
# where: {
# _or: [
# { rating: { ge: $minRating } }
# { rating: { le: $maxRating } }
# { genre: { eq: $genre } }
# { tags: { includes: $tag } }
# { title: { contains: $input } }
# ]
# }
# ) {
# id
# rating
# title
# imageUrl
# }
# }
# # List movies by rating and genre with AND filters
# query SearchMovieAnd(
# $minRating: Float
# $maxRating: Float
# $genre: String
# $tag: String
# $input: String
# ) @auth(level: PUBLIC) {
# movies(
# where: {
# _and: [
# { rating: { ge: $minRating } }
# { rating: { le: $maxRating } }
# { genre: { eq: $genre } }
# { tags: { includes: $tag } }
# { title: { contains: $input } }
# ]
# }
# ) {
# id
# rating
# title
# imageUrl
# }
# }
# # Get favorite actors by user ID
# query GetFavoriteActors @auth(level: USER) {
# user(key: {id_expr: "auth.uid"}) {
# favorite_actors_on_user {
# actor {
# id
# name
# imageUrl
# }
# }
# }
# }
# # Get favorite movies by user ID
# query GetUserFavoriteMovies @auth(level: USER) {
# user(id_expr: "auth.uid") {
# favorite_movies_on_user {
# movie {
# id
# title
# genre
# imageUrl
# releaseYear
# rating
# description
# }
# }
# }
# }
# # end of example queries
================================================
FILE: dataconnect/dataconnect/moviedata_insert.gql
================================================
mutation {
movie_insertMany(
data: [
{
id: "550e8400-e29b-41d4-a716-446655440000"
title: "Quantum Paradox"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fquantum_paradox.jpeg?alt=media&token=4142e2a1-bf43-43b5-b7cf-6616be3fd4e3"
releaseYear: 2025
genre: "sci-fi"
rating: 7.9
description: "A group of scientists accidentally open a portal to a parallel universe, causing a rift in time. As the team races to close the portal, they encounter alternate versions of themselves, leading to shocking revelations."
tags: ["thriller", "adventure"]
}
{
id: "550e8400-e29b-41d4-a716-446655440001"
title: "The Lone Outlaw"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Flone_outlaw.jpeg?alt=media&token=15525ffc-208f-4b59-b506-ae8348e06e85"
releaseYear: 2023
genre: "western"
rating: 8.2
description: "In the lawless Wild West, a mysterious gunslinger with a hidden past takes on a corrupt sheriff and his band of outlaws to bring justice to a small town."
tags: ["action", "drama"]
}
{
id: "550e8400-e29b-41d4-a716-446655440002"
title: "Celestial Harmony"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fcelestial_harmony.jpeg?alt=media&token=3edf1cf9-c2f5-4c75-9819-36ff6a734c9a"
releaseYear: 2024
genre: "romance"
rating: 7.5
description: "Two astronauts, stationed on a remote space station, fall in love amidst the isolation of deep space. But when a mysterious signal disrupts their communication, they must find a way to reconnect and survive."
tags: ["romance", "sci-fi"]
}
{
id: "550e8400-e29b-41d4-a716-446655440003"
title: "Noir Mystique"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fnoir_mystique.jpeg?alt=media&token=3299adba-cb98-4302-8b23-aeb679a4f913"
releaseYear: 2022
genre: "mystery"
rating: 8.0
description: "A private detective gets caught up in a web of lies, deception, and betrayal while investigating the disappearance of a famous actress in 1940s Hollywood."
tags: ["crime", "thriller"]
}
{
id: "550e8400-e29b-41d4-a716-446655440004"
title: "The Forgotten Island"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fforgotten_island.jpeg?alt=media&token=bc2b16e1-caed-4649-952c-73b6113f205c"
releaseYear: 2025
genre: "adventure"
rating: 7.6
description: "An explorer leads an expedition to a remote island rumored to be home to mythical creatures. As the team ventures deeper into the island, they uncover secrets that change the course of history."
tags: ["adventure", "fantasy"]
}
{
id: "550e8400-e29b-41d4-a716-446655440005"
title: "Digital Nightmare"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fdigital_nightmare.jpeg?alt=media&token=335ec842-1ca4-4b09-abd1-e96d9f5c0c2f"
releaseYear: 2024
genre: "horror"
rating: 6.9
description: "A tech-savvy teenager discovers a cursed app that brings nightmares to life. As the horrors of the digital world cross into reality, she must find a way to break the curse before it's too late."
tags: ["horror", "thriller"]
}
{
id: "550e8400-e29b-41d4-a716-446655440006"
title: "Eclipse of Destiny"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Feclipse_destiny.jpeg?alt=media&token=346649b3-cb5c-4d7e-b0d4-6f02e3df5959"
releaseYear: 2026
genre: "fantasy"
rating: 8.1
description: "In a kingdom on the brink of war, a prophecy speaks of an eclipse that will grant power to the rightful ruler. As factions vie for control, a young warrior must decide where his true loyalty lies."
tags: ["fantasy", "adventure"]
}
{
id: "550e8400-e29b-41d4-a716-446655440007"
title: "Heart of Steel"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fheart_steel.jpeg?alt=media&token=17883d71-329b-415a-86f8-dd4d9e941d7f"
releaseYear: 2023
genre: "sci-fi"
rating: 7.7
description: "A brilliant scientist creates a robot with a human heart. As the robot struggles to understand emotions, it becomes entangled in a plot that could change the fate of humanity."
tags: ["sci-fi", "drama"]
}
{
id: "550e8400-e29b-41d4-a716-446655440008"
title: "Rise of the Crimson Empire"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Frise_crimson_empire.jpeg?alt=media&token=6faa73ad-7504-4146-8f3a-50b90f607f33"
releaseYear: 2025
genre: "action"
rating: 8.4
description: "A legendary warrior rises to challenge the tyrannical rule of a powerful empire. As rebellion brews, the warrior must unite different factions to lead an uprising."
tags: ["action", "adventure"]
}
{
id: "550e8400-e29b-41d4-a716-446655440009"
title: "Silent Waves"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fsilent_waves.jpeg?alt=media&token=bd626bf1-ec60-4e57-aa07-87ba14e35bb7"
releaseYear: 2024
genre: "drama"
rating: 8.2
description: "A talented pianist, who loses his hearing in a tragic accident, must rediscover his passion for music with the help of a young music teacher who believes in him."
tags: ["drama", "music"]
}
{
id: "550e8400-e29b-41d4-a716-446655440010"
title: "Echoes of the Past"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fecho_of_past.jpeg?alt=media&token=d866aa27-8534-4d72-8988-9da4a1b9e452"
releaseYear: 2023
genre: "historical"
rating: 7.8
description: "A historian stumbles upon an ancient artifact that reveals hidden truths about an empire long forgotten. As she deciphers the clues, a shadowy organization tries to stop her from unearthing the past."
tags: ["drama", "mystery"]
}
{
id: "550e8400-e29b-41d4-a716-446655440011"
title: "Beyond the Horizon"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fbeyond_horizon.jpeg?alt=media&token=31493973-0692-4e6e-8b88-afb1aaea17ee"
releaseYear: 2026
genre: "sci-fi"
rating: 8.5
description: "In the future, Earth's best pilots are sent on a mission to explore a mysterious planet beyond the solar system. What they find changes humanity's understanding of the universe forever."
tags: ["sci-fi", "adventure"]
}
{
id: "550e8400-e29b-41d4-a716-446655440012"
title: "Shadows and Lies"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fshadows_lies.jpeg?alt=media&token=01afb80d-caee-47f8-a00e-aea8b9e459a2"
releaseYear: 2022
genre: "crime"
rating: 7.9
description: "A young detective with a dark past investigates a series of mysterious murders in a city plagued by corruption. As she digs deeper, she realizes nothing is as it seems."
tags: ["crime", "thriller"]
}
{
id: "550e8400-e29b-41d4-a716-446655440013"
title: "The Last Symphony"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Flast_symphony.jpeg?alt=media&token=f9bf80cd-3d8e-4e24-8503-7feb11f4e397"
releaseYear: 2024
genre: "drama"
rating: 8.0
description: "An aging composer struggling with memory loss attempts to complete his final symphony. With the help of a young prodigy, he embarks on an emotional journey through his memories and legacy."
tags: ["drama", "music"]
}
{
id: "550e8400-e29b-41d4-a716-446655440014"
title: "Moonlit Crusade"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fmoonlit_crusade.jpeg?alt=media&token=b13241f5-d7d0-4370-b651-07847ad99dc2"
releaseYear: 2025
genre: "fantasy"
rating: 8.3
description: "A knight is chosen by an ancient order to embark on a quest under the light of the full moon. Facing mythical beasts and treacherous landscapes, he seeks a relic that could save his kingdom."
tags: ["fantasy", "adventure"]
}
{
id: "550e8400-e29b-41d4-a716-446655440015"
title: "Abyss of the Deep"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fabyss_deep.jpeg?alt=media&token=2417321d-2451-4ec0-9ed6-6297042170e6"
releaseYear: 2023
genre: "horror"
rating: 7.2
description: "When a group of marine biologists descends into the unexplored depths of the ocean, they encounter a terrifying and ancient force. Now, they must survive as the abyss comes alive."
tags: ["horror", "thriller"]
}
{
id: "550e8400-e29b-41d4-a716-446655440016"
title: "Phoenix Rising"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fpheonix_rising.jpeg?alt=media&token=7298b1fd-833c-471c-a55d-e8fc798b4ab2"
releaseYear: 2025
genre: "action"
rating: 8.6
description: "A special forces operative, presumed dead, returns to avenge his fallen comrades. With nothing to lose, he faces a powerful enemy in a relentless pursuit for justice."
tags: ["action", "thriller"]
}
{
id: "550e8400-e29b-41d4-a716-446655440017"
title: "The Infinite Knot"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Finfinite_knot.jpeg?alt=media&token=93d54d93-d933-4663-a6fe-26b707ef823e"
releaseYear: 2026
genre: "romance"
rating: 7.4
description: "Two souls destined to meet across multiple lifetimes struggle to find each other in a chaotic world. With each incarnation, they get closer, but time itself becomes their greatest obstacle."
tags: ["romance", "fantasy"]
}
{
id: "550e8400-e29b-41d4-a716-446655440018"
title: "Parallel Justice"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fparalel_justice.jpeg?alt=media&token=4544b5e2-7a1d-46ca-a97f-eb6a490d4288"
releaseYear: 2024
genre: "crime"
rating: 8.1
description: "A lawyer who can see the outcomes of different timelines must choose between justice and personal gain. When a high-stakes case arises, he faces a moral dilemma that could alter his life forever."
tags: ["crime", "thriller"]
}
{
id: "550e8400-e29b-41d4-a716-446655440019"
title: "Veil of Illusion"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/movie%2Fveil_illusion.jpeg?alt=media&token=7bf09a3c-c531-478a-9d02-5d99fca9393b"
releaseYear: 2022
genre: "mystery"
rating: 7.8
description: "A magician-turned-detective uses his skills in illusion to solve crimes. When a series of murders leaves the city in fear, he must reveal the truth hidden behind a veil of deceit."
tags: ["mystery", "crime"]
}
]
)
actor_insertMany(
data: [
{
id: "123e4567-e89b-12d3-a456-426614174020"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Foliver_blackwood.jpeg?alt=media&token=79cdbc29-c2c6-4dc3-b87f-f6dc4f1a8208"
name: "Oliver Blackwood"
}
{
id: "123e4567-e89b-12d3-a456-426614174021"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Femma_westfield.jpeg?alt=media&token=2991c3c9-cfa8-4067-8b26-c5239b6894c4"
name: "Emma Westfield"
}
{
id: "123e4567-e89b-12d3-a456-426614174022"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fjack_stone.jpeg?alt=media&token=74a564aa-d840-4bdd-a8a6-c6b17bbde608"
name: "Jack Stone"
}
{
id: "123e4567-e89b-12d3-a456-426614174023"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fclara_woods.jpeg?alt=media&token=b4ff2a15-ef6d-4f20-86c9-07d67015fb29"
name: "Clara Woods"
}
{
id: "123e4567-e89b-12d3-a456-426614174024"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fnoah_frost.jpeg?alt=media&token=0d08179a-7778-405e-9501-feac43b77a99"
name: "Noah Frost"
}
{
id: "123e4567-e89b-12d3-a456-426614174025"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fisabelle_hart.jpeg?alt=media&token=d4fdf896-0f5b-4c32-91a4-7a541a95e77d"
name: "Isabella Hart"
}
{
id: "123e4567-e89b-12d3-a456-426614174026"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fliam_hale.jpeg?alt=media&token=08e8eeee-b8ef-4e7b-8f97-a1e0b59321cc"
name: "Liam Hale"
}
{
id: "123e4567-e89b-12d3-a456-426614174027"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fsophia_knight.jpeg?alt=media&token=7a79ef21-93e0-46f9-934c-6bbef7b5d430"
name: "Sophia Knight"
}
{
id: "123e4567-e89b-12d3-a456-426614174028"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Falex_clay.jpeg?alt=media&token=2a798cdb-f44f-48d5-91bc-9d26a758944e"
name: "Alexander Clay"
}
{
id: "123e4567-e89b-12d3-a456-426614174029"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Famelia_stone.jpeg?alt=media&token=34f21ba9-9e28-4708-9e55-f123634ab506"
name: "Amelia Stone"
}
{
id: "123e4567-e89b-12d3-a456-426614174030"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fethan_blake.jpeg?alt=media&token=41352170-a5cd-4088-b8fd-1c4ee0d52cad"
name: "Ethan Blake"
}
{
id: "123e4567-e89b-12d3-a456-426614174031"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fmia_gray.jpeg?alt=media&token=1ba1831a-3ada-485a-b5c9-2d018bf1862b"
name: "Mia Gray"
}
{
id: "123e4567-e89b-12d3-a456-426614174032"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Flucas_reed.jpeg?alt=media&token=c74f44f3-ae98-4208-8e67-18c2db65a5c1"
name: "Lucas Reed"
}
{
id: "123e4567-e89b-12d3-a456-426614174033"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fevelyn_harper.jpeg?alt=media&token=b138b308-9589-4dfe-8c50-a6d70f06dfb1"
name: "Evelyn Harper"
}
{
id: "123e4567-e89b-12d3-a456-426614174034"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Foscar_smith.jpeg?alt=media&token=d493da85-644d-4d45-a09d-ecb5416645e4"
name: "Oscar Smith"
}
{
id: "123e4567-e89b-12d3-a456-426614174035"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fava_winter.jpeg?alt=media&token=757e4b11-0372-401e-8fa0-61797e90312a"
name: "Ava Winter"
}
{
id: "123e4567-e89b-12d3-a456-426614174036"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fleo_hunt.jpeg?alt=media&token=2cb14738-b39b-47b1-87f9-b45f38245179"
name: "Leo Hunt"
}
{
id: "123e4567-e89b-12d3-a456-426614174037"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Flucy_walsh.jpeg?alt=media&token=016a216c-f329-4c10-bbe8-b31425f73c69"
name: "Lucy Walsh"
}
{
id: "123e4567-e89b-12d3-a456-426614174038"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Fmason_ford.jpeg?alt=media&token=55388be9-fdc8-483f-8352-c29755ed3574"
name: "Mason Ford"
}
{
id: "123e4567-e89b-12d3-a456-426614174039"
imageUrl: "https://firebasestorage.googleapis.com/v0/b/fdc-web-quickstart.appspot.com/o/actor%2Flily_moore.jpeg?alt=media&token=19538aa6-1baf-4033-8fd7-d2a62aa79f51"
name: "Lily Moore"
}
]
)
movieMetadata_insertMany(
data: [
{
movieId: "550e8400-e29b-41d4-a716-446655440000"
director: "Henry Caldwell"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440001"
director: "Juliana Mason, Clark Avery"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440002"
director: "Diana Rivers"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440003"
director: "Liam Thatcher"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440004"
director: "Evelyn Hart"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440005"
director: "Grayson Brooks"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440006"
director: "Isabella Quinn"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440007"
director: "Vincent Hale"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440008"
director: "Amelia Sutton"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440009"
director: "Lucas Stone"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440010"
director: "Sophia Langford"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440011"
director: "Noah Bennett"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440012"
director: "Chloe Armstrong"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440013"
director: "Sebastian Crane"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440014"
director: "Isla Fitzgerald"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440015"
director: "Oliver Hayes"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440016"
director: "Mila Donovan"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440017"
director: "Carter Monroe, Elise Turner"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440018"
director: "Adrian Blake"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440019"
director: "Hazel Carter"
}
]
)
movieActor_insertMany(
data: [
{
movieId: "550e8400-e29b-41d4-a716-446655440000"
actorId: "123e4567-e89b-12d3-a456-426614174020"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440000"
actorId: "123e4567-e89b-12d3-a456-426614174021"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440001"
actorId: "123e4567-e89b-12d3-a456-426614174021"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440001"
actorId: "123e4567-e89b-12d3-a456-426614174022"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440002"
actorId: "123e4567-e89b-12d3-a456-426614174022"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440002"
actorId: "123e4567-e89b-12d3-a456-426614174023"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440003"
actorId: "123e4567-e89b-12d3-a456-426614174023"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440003"
actorId: "123e4567-e89b-12d3-a456-426614174024"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440004"
actorId: "123e4567-e89b-12d3-a456-426614174024"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440004"
actorId: "123e4567-e89b-12d3-a456-426614174025"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440005"
actorId: "123e4567-e89b-12d3-a456-426614174025"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440005"
actorId: "123e4567-e89b-12d3-a456-426614174026"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440006"
actorId: "123e4567-e89b-12d3-a456-426614174026"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440006"
actorId: "123e4567-e89b-12d3-a456-426614174027"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440007"
actorId: "123e4567-e89b-12d3-a456-426614174027"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440007"
actorId: "123e4567-e89b-12d3-a456-426614174028"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440008"
actorId: "123e4567-e89b-12d3-a456-426614174028"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440008"
actorId: "123e4567-e89b-12d3-a456-426614174029"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440009"
actorId: "123e4567-e89b-12d3-a456-426614174029"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440009"
actorId: "123e4567-e89b-12d3-a456-426614174030"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440010"
actorId: "123e4567-e89b-12d3-a456-426614174030"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440010"
actorId: "123e4567-e89b-12d3-a456-426614174031"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440011"
actorId: "123e4567-e89b-12d3-a456-426614174031"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440011"
actorId: "123e4567-e89b-12d3-a456-426614174032"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440012"
actorId: "123e4567-e89b-12d3-a456-426614174032"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440012"
actorId: "123e4567-e89b-12d3-a456-426614174033"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440013"
actorId: "123e4567-e89b-12d3-a456-426614174033"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440013"
actorId: "123e4567-e89b-12d3-a456-426614174034"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440014"
actorId: "123e4567-e89b-12d3-a456-426614174034"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440014"
actorId: "123e4567-e89b-12d3-a456-426614174035"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440015"
actorId: "123e4567-e89b-12d3-a456-426614174035"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440015"
actorId: "123e4567-e89b-12d3-a456-426614174036"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440016"
actorId: "123e4567-e89b-12d3-a456-426614174036"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440016"
actorId: "123e4567-e89b-12d3-a456-426614174037"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440017"
actorId: "123e4567-e89b-12d3-a456-426614174037"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440017"
actorId: "123e4567-e89b-12d3-a456-426614174038"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440018"
actorId: "123e4567-e89b-12d3-a456-426614174038"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440018"
actorId: "123e4567-e89b-12d3-a456-426614174039"
role: "supporting"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440019"
actorId: "123e4567-e89b-12d3-a456-426614174039"
role: "main"
}
{
movieId: "550e8400-e29b-41d4-a716-446655440019"
actorId: "123e4567-e89b-12d3-a456-426614174020"
role: "supporting"
}
]
)
user_insertMany(
data: [
{ id: "SnLgOC3lN4hcIl69s53cW0Q8R1T2", username: "sherlock_h" }
{ id: "fep4fXpGWsaRpuphq9CIrBIXQ0S2", username: "hercule_p" }
{ id: "TBedjwCX0Jf955Uuoxk6k74sY0l1", username: "jane_d" }
]
)
review_insertMany(
data: [
{
id: "345e4567-e89b-12d3-a456-426614174000"
userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2"
movieId: "550e8400-e29b-41d4-a716-446655440000"
rating: 5
reviewText: "An incredible movie with a mind-blowing plot!"
reviewDate_date: { today: true }
}
{
id: "345e4567-e89b-12d3-a456-426614174001"
userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2"
movieId: "550e8400-e29b-41d4-a716-446655440001"
rating: 5
reviewText: "A revolutionary film that changed cinema forever."
reviewDate_date: { today: true }
}
{
id: "345e4567-e89b-12d3-a456-426614174002"
userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1"
movieId: "550e8400-e29b-41d4-a716-446655440002"
rating: 5
reviewText: "A visually stunning and emotionally impactful movie."
reviewDate_date: { today: true }
}
{
id: "345e4567-e89b-12d3-a456-426614174003"
userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2"
movieId: "550e8400-e29b-41d4-a716-446655440003"
rating: 4
reviewText: "A fantastic superhero film with great performances."
reviewDate_date: { today: true }
}
{
id: "345e4567-e89b-12d3-a456-426614174004"
userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2"
movieId: "550e8400-e29b-41d4-a716-446655440004"
rating: 5
reviewText: "An amazing film that keeps you on the edge of your seat."
reviewDate_date: { today: true }
}
{
id: "345e4567-e89b-12d3-a456-426614174005"
userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1"
movieId: "550e8400-e29b-41d4-a716-446655440005"
rating: 5
reviewText: "An absolute classic with unforgettable dialogue."
reviewDate_date: { today: true }
}
]
)
favorite_movie_insertMany(
data: [
{
userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2"
movieId: "550e8400-e29b-41d4-a716-446655440000"
}
{
userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2"
movieId: "550e8400-e29b-41d4-a716-446655440001"
}
{
userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1"
movieId: "550e8400-e29b-41d4-a716-446655440002"
}
{
userId: "SnLgOC3lN4hcIl69s53cW0Q8R1T2"
movieId: "550e8400-e29b-41d4-a716-446655440003"
}
{
userId: "fep4fXpGWsaRpuphq9CIrBIXQ0S2"
movieId: "550e8400-e29b-41d4-a716-446655440004"
}
{
userId: "TBedjwCX0Jf955Uuoxk6k74sY0l1"
movieId: "550e8400-e29b-41d4-a716-446655440005"
}
]
)
}
================================================
FILE: dataconnect/dataconnect/schema/schema.gql
================================================
# Movies
# TODO: Fill out Movie table
type Movie
# The below parameter values are generated by default with @table, and can be edited manually.
@table {
# implicitly calls @col to generates a column name. ex: @col(name: "movie_id")
id: UUID! @default(expr: "uuidV4()")
title: String!
imageUrl: String!
releaseYear: Int
genre: String
rating: Float
description: String
tags: [String]
# descriptionEmbedding: Vector @col(size:768) # Enables vector search
}
# Movie Metadata
# Movie - MovieMetadata is a one-to-one relationship
# TODO: Fill out MovieMetadata table
type MovieMetadata
@table {
# @ref creates a field in the current table (MovieMetadata)
# It is a reference that holds the primary key of the referenced type
# In this case, @ref(fields: "movieId", references: "id") is implied
movie: Movie! @ref
# movieId: UUID <- this is created by the above @ref
director: String
}
# Actors
# Suppose an actor can participate in multiple movies and movies can have multiple actors
# Movie - Actors (or vice versa) is a many to many relationship
# TODO: Fill out Actor table
type Actor @table {
id: UUID!
imageUrl: String!
name: String! @col(name: "name", dataType: "varchar(30)")
}
# Users
# Suppose a user can leave reviews for movies
# user-reviews is a one to many relationship, movie-reviews is a one to many relationship, movie:user is a many to many relationship
# TODO: Fill out User table
type User
@table {
id: String! @col(name: "user_auth")
username: String! @col(name: "username", dataType: "varchar(50)")
# The following are generated from the @ref in the Review table
# reviews_on_user
# movies_via_Review
}
# Reviews
# TODO: Fill out Review table
type Review @table(name: "Reviews", key: ["movie", "user"]) {
id: UUID! @default(expr: "uuidV4()")
user: User!
movie: Movie!
rating: Int
reviewText: String
reviewDate: Date! @default(expr: "request.time")
}
# Join table for many-to-many relationship for movies and actors
# The 'key' param signifies the primary key(s) of this table
# In this case, the keys are [movieId, actorId], the generated fields of the reference types [movie, actor]
# TODO: Fill out MovieActor table
type MovieActor @table(key: ["movie", "actor"]) {
# @ref creates a field in the current table (MovieActor) that holds the primary key of the referenced type
# In this case, @ref(fields: "id") is implied
movie: Movie!
# movieId: UUID! <- this is created by the implied @ref, see: implicit.gql
actor: Actor!
# actorId: UUID! <- this is created by the implied @ref, see: implicit.gql
role: String! # "main" or "supporting"
}
# Join table for many-to-many relationship for users and favorite movies
# TODO: Fill out FavoriteMovie table
type FavoriteMovie
@table(name: "FavoriteMovies", singular: "favorite_movie", plural: "favorite_movies", key: ["user", "movie"]) {
# @ref is implicit
user: User!
movie: Movie!
}
================================================
FILE: dataconnect/firebase.json
================================================
{
"dataconnect": {
"source": "dataconnect"
}
}
================================================
FILE: dataconnect/gradle/wrapper/gradle-wrapper.properties
================================================
#Wed May 08 19:29:05 BST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: dataconnect/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
================================================
FILE: dataconnect/gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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
#
# https://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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: dataconnect/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: dataconnect/settings.gradle.kts
================================================
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
}
rootProject.name = "Firebase Data Connect"
include(":app")
================================================
FILE: dynamiclinks/README.md
================================================
Firebase Dynamic Links Quickstart
==============================
> [!IMPORTANT]
> Firebase Dynamic Links is **deprecated** and should not be used in new projects. The service will shut down on August 25, 2025.
>
> Please see our [Dynamic Links Deprecation FAQ documentation](https://firebase.google.com/support/dynamic-links-faq) for more guidance.
================================================
FILE: firebase-ai/.gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
================================================
FILE: firebase-ai/README.md
================================================
# Firebase AI Logic quickstart sample app
This Android sample app demonstrates how to use state-of-the-art
generative AI models (like Gemini) to build AI-powered features and applications.
For more information about Firebase AI Logic, visit the [documentation](http://firebase.google.com/docs/ai-logic).
## Setup & Configuration
### Prerequisites
* **Google AI (Gemini) API Key**: Most samples work out of the box with the Google AI SDK.
* **Vertex AI**: Samples marked with *(Vertex AI)* require you to enable the Vertex AI API in your Google Cloud project and have your files in Cloud Storage.
* **Server Prompt Templates**: These samples require you to set up templates in the [Firebase Console](https://console.firebase.google.com/project/_/ai-logic).
## Getting Started
To try out this sample app, you need to use latest stable version of Android Studio.
* [Set up your Android app for Firebase][setup-android]
* Use the package name `com.google.firebase.quickstart.ai`
* [Set up Firebase AI Logic][setup-ai-logic]
* Run the app on an Android device or emulator.
## Features
You can find the implementation for each feature by clicking on the links below:
### Text / Chat
- [Travel tips](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TravelTipsViewModel.kt): The user wants the model to help a new traveler with travel tips
- [Chatbot recommendations for courses](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/CourseRecommendationsViewModel.kt): A chatbot suggests courses for a performing arts program.
- [Weather Chat](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/WeatherChatViewModel.kt): Use function calling to get the weather conditions for a specific US city on a specific date.
- [Grounding with Google Search](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/GoogleSearchGroundingViewModel.kt): Use Grounding with Google Search to get responses based on up-to-date information from the web.
- [Thinking](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ThinkingChatViewModel.kt): Gemini 2.5 Flash with dynamic thinking
- [Server Prompt Templates - Gemini](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ServerPromptTemplateViewModel.kt): Generate an invoice using server prompt templates.
### Image analysis / generation
- [Imagen 4 - image generation](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenGenerationViewModel.kt): Generate images using Imagen 4
- [Imagen 3 - Inpainting (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenInpaintingViewModel.kt): Replace part of an image using Imagen 3
- [Imagen 3 - Outpainting (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenOutpaintingViewModel.kt): Expand an image by drawing in more background
- [Imagen 3 - Subject Reference (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenSubjectReferenceViewModel.kt): Generate an image using a referenced subject (must be an animal)
- [Imagen 3 - Style Transfer (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenStyleTransferViewModel.kt): Change the art style of a cat picture using a reference
- [Gemini 2.5 Flash Image (aka nanobanana)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageGenerationViewModel.kt): Generate and/or edit images using Gemini 2.5 Flash Image aka nanobanana
- [Server Prompt Template - Imagen](app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenTemplateViewModel.kt): Generate an image using a server prompt template.
- [SVG Generator](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/SvgViewModel.kt): Use Gemini 3 Flash preview to create SVG illustrations
- [Blog post creator (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageBlogCreatorViewModel.kt): Create a blog post from an image file stored in Cloud Storage.
### Audio analysis
- [Audio Summarization](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioSummarizationViewModel.kt): Summarize an audio file
- [Translation from audio (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioTranslationViewModel.kt): Translate an audio file stored in Cloud Storage
### Video analysis
- [Hashtags for a video (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoHashtagGeneratorViewModel.kt): Generate hashtags for a video ad stored in Cloud Storage
- [Summarize video](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoSummarizationViewModel.kt): Summarize a video and extract important dialogue.
### Live API (Real-time bidrectional streaming)
- [ForecastTalk](app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamAudioViewModel.kt): Use bidirectional streaming to get information about weather conditions
- [Gemini Live (Video input)](app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamVideoViewModel.kt): Use bidirectional streaming to chat with Gemini using your phone's camera
### Document (PDFs) analysis
- [Document comparison (Vertex AI)](app/src/main/java/com/google/firebase/quickstart/ai/feature/text/DocumentComparisonViewModel.kt): Compare the contents of 2 documents in Cloud Storage.
## All samples
The full list of available samples can be found in the
[FirebaseAISamples.kt file](app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt).
[setup-android]: https://firebase.google.com/docs/android/setup
[setup-ai-logic]: https://firebase.google.com/docs/ai-logic/get-started?api=dev#set-up-firebase
================================================
FILE: firebase-ai/app/.gitignore
================================================
/build
================================================
FILE: firebase-ai/app/build.gradle.kts
================================================
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.google.services)
}
android {
namespace = "com.google.firebase.quickstart.ai"
compileSdk = 36
defaultConfig {
applicationId = "com.google.firebase.quickstart.ai"
minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.material3.adaptive.navigation.suite)
implementation(libs.compose.navigation)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
// ViewModel utilities for Compose
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.kotlinx.serialization.json)
// Webkit
implementation(libs.androidx.webkit)
// CameraX (for video with the Gemini Live API)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extensions)
// Material for XML-based theme
implementation(libs.material)
// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.ai)
// Image loading
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
implementation(libs.coil.svg)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
================================================
FILE: firebase-ai/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: firebase-ai/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/MainActivity.kt
================================================
package com.google.firebase.quickstart.ai
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.firebase.quickstart.ai.feature.live.BidiViewModel
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenViewModel
import com.google.firebase.quickstart.ai.feature.text.ChatViewModel
import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateViewModel
import com.google.firebase.quickstart.ai.feature.text.SvgViewModel
import com.google.firebase.quickstart.ai.ui.ChatScreen
import com.google.firebase.quickstart.ai.ui.ImagenScreen
import com.google.firebase.quickstart.ai.ui.ServerPromptScreen
import com.google.firebase.quickstart.ai.ui.StreamRealtimeScreen
import com.google.firebase.quickstart.ai.ui.StreamRealtimeVideoScreen
import com.google.firebase.quickstart.ai.ui.SvgScreen
import com.google.firebase.quickstart.ai.ui.navigation.FIREBASE_AI_SAMPLES
import com.google.firebase.quickstart.ai.ui.navigation.MainMenuScreen
import com.google.firebase.quickstart.ai.ui.navigation.ScreenType
import com.google.firebase.quickstart.ai.ui.theme.FirebaseAILogicTheme
class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
catImage = BitmapFactory.decodeResource(applicationContext.resources, R.drawable.cat)
setContent {
val navController = rememberNavController()
var topBarTitle: String by rememberSaveable { mutableStateOf(getString(R.string.app_name)) }
FirebaseAILogicTheme {
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
),
title = {
Text(topBarTitle)
}
)
},
modifier = Modifier.fillMaxSize()
) { innerPadding ->
NavHost(
navController,
startDestination = "mainMenu",
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
composable("mainMenu") {
MainMenuScreen(
onSampleClicked = {
topBarTitle = it.title
navController.navigate(it.route)
}
)
}
// Add navigation for all of the samples
FIREBASE_AI_SAMPLES.forEach { sample ->
composable(
route = sample.route::class,
typeMap = emptyMap()
) {
val viewModelClass = sample.viewModelClass?.java
?: return@composable
val vm = viewModel(modelClass = viewModelClass)
when (sample.screenType) {
ScreenType.CHAT -> {
(vm as? ChatViewModel)?.let { ChatScreen(it) }
}
ScreenType.IMAGEN -> {
(vm as? ImagenViewModel)?.let { ImagenScreen(it) }
}
ScreenType.SVG -> {
(vm as? SvgViewModel)?.let { SvgScreen(it) }
}
ScreenType.SERVER_PROMPT -> {
(vm as? ServerPromptTemplateViewModel)?.let { ServerPromptScreen(it) }
}
ScreenType.BIDI -> {
(vm as? BidiViewModel)?.let {
@SuppressLint("MissingPermission")
StreamRealtimeScreen(it)
}
}
ScreenType.BIDI_VIDEO -> {
(vm as? BidiViewModel)?.let {
@SuppressLint("MissingPermission")
StreamRealtimeVideoScreen(it)
}
}
}
}
}
}
}
}
navController.addOnDestinationChangedListener { _, destination, _ ->
if (destination.route == "mainMenu") {
topBarTitle = getString(R.string.app_name)
}
}
}
}
companion object {
lateinit var catImage: Bitmap
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/BidiViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.live
import android.annotation.SuppressLint
import android.graphics.Bitmap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.ai.type.FunctionCallPart
import com.google.firebase.ai.type.FunctionResponsePart
import com.google.firebase.ai.type.InlineData
import com.google.firebase.ai.type.LiveSession
import com.google.firebase.ai.type.PublicPreviewAPI
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonObject
import java.io.ByteArrayOutputStream
@OptIn(PublicPreviewAPI::class)
abstract class BidiViewModel : ViewModel() {
protected lateinit var liveSession: LiveSession
open fun handler(functionCall: FunctionCallPart): FunctionResponsePart {
return FunctionResponsePart(functionCall.name, JsonObject(emptyMap()), functionCall.id)
}
// The permission check is handled by the view that calls this function.
@SuppressLint("MissingPermission")
suspend fun startConversation() {
liveSession.startAudioConversation(::handler)
}
fun endConversation() {
liveSession.stopAudioConversation()
}
fun sendVideoFrame(frame: Bitmap) {
viewModelScope.launch {
// Directly compress the Bitmap to a ByteArray
val byteArrayOutputStream = ByteArrayOutputStream()
frame.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream)
val jpegBytes = byteArrayOutputStream.toByteArray()
liveSession.sendVideoRealtime(InlineData(jpegBytes, "image/jpeg"))
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamAudioViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.live
import com.google.firebase.Firebase
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.FunctionCallPart
import com.google.firebase.ai.type.FunctionDeclaration
import com.google.firebase.ai.type.FunctionResponsePart
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.ai.type.ResponseModality
import com.google.firebase.ai.type.Schema
import com.google.firebase.ai.type.SpeechConfig
import com.google.firebase.ai.type.Tool
import com.google.firebase.ai.type.Voice
import com.google.firebase.ai.type.liveGenerationConfig
import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository.Companion.fetchWeather
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
@Serializable
object StreamRealtimeAudioRoute
@OptIn(PublicPreviewAPI::class)
class StreamAudioViewModel : BidiViewModel() {
init {
val liveGenerationConfig = liveGenerationConfig {
speechConfig = SpeechConfig(voice = Voice("CHARON"))
responseModality = ResponseModality.AUDIO
}
val liveModel =
Firebase.ai(backend = GenerativeBackend.googleAI())
.liveModel(
// Note that each backend supports a different set of models.
// See our documentation for a breakdown of models by backend:
// https://firebase.google.com/docs/ai-logic/live-api#supported-models
modelName = "gemini-2.5-flash-native-audio-preview-09-2025",
generationConfig = liveGenerationConfig,
tools = listOf(
Tool.functionDeclarations(
listOf(
FunctionDeclaration(
"fetchWeather",
"Get the weather conditions for a specific US city on a specific date.",
mapOf(
"city" to Schema.string("The US city of the location."),
"state" to Schema.string("The US state of the location."),
"date" to Schema.string(
"The date for which to get the weather." +
" Date must be in the format: YYYY-MM-DD."
),
),
)
)
)
),
)
runBlocking { liveSession = liveModel.connect() }
}
override fun handler(functionCall: FunctionCallPart): FunctionResponsePart {
val response: JsonObject
if (functionCall.name == "fetchWeather") {
val city = functionCall.args["city"]?.jsonPrimitive?.content
val state = functionCall.args["state"]?.jsonPrimitive?.content
val date = functionCall.args["date"]?.jsonPrimitive?.content
runBlocking {
response =
if (!city.isNullOrEmpty() and !state.isNullOrEmpty() and !date.isNullOrEmpty()) {
fetchWeather(city!!, state!!, date!!)
} else {
JsonObject(emptyMap())
}
}
} else {
response = JsonObject(emptyMap())
}
return FunctionResponsePart(functionCall.name, response, functionCall.id)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/live/StreamVideoViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.live
import com.google.firebase.Firebase
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.ai.type.ResponseModality
import com.google.firebase.ai.type.SpeechConfig
import com.google.firebase.ai.type.Voice
import com.google.firebase.ai.type.liveGenerationConfig
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
@Serializable
object StreamRealtimeVideoRoute
@OptIn(PublicPreviewAPI::class)
class StreamVideoViewModel : BidiViewModel() {
init {
val liveGenerationConfig = liveGenerationConfig {
speechConfig = SpeechConfig(voice = Voice("CHARON"))
responseModality = ResponseModality.AUDIO
}
// Note that each backend supports a different set of models.
// See our documentation for a breakdown of models by backend:
// https://firebase.google.com/docs/ai-logic/live-api#supported-models
val liveModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).liveModel(
modelName = "gemini-2.5-flash-native-audio-preview-09-2025",
generationConfig = liveGenerationConfig,
)
runBlocking { liveSession = liveModel.connect() }
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenGenerationViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.media.imagen
import android.graphics.Bitmap
import com.google.firebase.Firebase
import com.google.firebase.ai.ImagenModel
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.ImagenGenerationResponse
import com.google.firebase.ai.type.ImagenImageFormat
import com.google.firebase.ai.type.ImagenInlineImage
import com.google.firebase.ai.type.ImagenPersonFilterLevel
import com.google.firebase.ai.type.ImagenSafetyFilterLevel
import com.google.firebase.ai.type.ImagenSafetySettings
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.ai.type.imagenGenerationConfig
import com.google.firebase.quickstart.ai.ui.ImagenUiState
import kotlinx.serialization.Serializable
@Serializable
object ImagenGenerationRoute
@OptIn(PublicPreviewAPI::class)
class ImagenGenerationViewModel : ImagenViewModel() {
override val initialPrompt: String = ""
override val includeAttach: Boolean = false
override val selectionOptions: List = emptyList()
override val allowEmptyPrompt: Boolean = false
override val additionalImage: Bitmap? = null
override val imageLabels: List = emptyList()
private val imagenModel: ImagenModel
init {
imagenModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).imagenModel(
modelName = "imagen-4.0-generate-001",
generationConfig = imagenGenerationConfig {
numberOfImages = 4
imageFormat = ImagenImageFormat.png()
},
safetySettings = ImagenSafetySettings(
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
)
)
}
override suspend fun performGeneration(
inputText: String,
currentState: ImagenUiState.Success
): ImagenGenerationResponse {
return imagenModel.generateImages(inputText)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenInpaintingViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.media.imagen
import android.graphics.Bitmap
import com.google.firebase.Firebase
import com.google.firebase.ai.ImagenModel
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.ImagenBackgroundMask
import com.google.firebase.ai.type.ImagenEditMode
import com.google.firebase.ai.type.ImagenEditingConfig
import com.google.firebase.ai.type.ImagenForegroundMask
import com.google.firebase.ai.type.ImagenGenerationResponse
import com.google.firebase.ai.type.ImagenImageFormat
import com.google.firebase.ai.type.ImagenInlineImage
import com.google.firebase.ai.type.ImagenPersonFilterLevel
import com.google.firebase.ai.type.ImagenRawImage
import com.google.firebase.ai.type.ImagenSafetyFilterLevel
import com.google.firebase.ai.type.ImagenSafetySettings
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.ai.type.imagenGenerationConfig
import com.google.firebase.ai.type.toImagenInlineImage
import com.google.firebase.quickstart.ai.ui.ImagenUiState
import kotlinx.serialization.Serializable
@Serializable
object ImagenInpaintingRoute
@OptIn(PublicPreviewAPI::class)
class ImagenInpaintingViewModel : ImagenViewModel() {
override val initialPrompt: String = "A sunny beach"
override val includeAttach: Boolean = true
override val selectionOptions: List = listOf("Mask", "Background", "Foreground")
override val allowEmptyPrompt: Boolean = true
override val additionalImage: Bitmap? = null
override val imageLabels: List = emptyList()
private val imagenModel: ImagenModel
init {
val config = imagenGenerationConfig {
numberOfImages = 4
imageFormat = ImagenImageFormat.png()
}
val settings = ImagenSafetySettings(
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
)
imagenModel = Firebase.ai(
backend = GenerativeBackend.vertexAI()
).imagenModel(
modelName = "imagen-3.0-capability-001",
generationConfig = config,
safetySettings = settings
)
}
override suspend fun performGeneration(
inputText: String,
currentState: ImagenUiState.Success
): ImagenGenerationResponse {
val bitmap = currentState.attachedImage!!
val mask = when (currentState.selectedOption) {
"Foreground" -> ImagenForegroundMask()
else -> ImagenBackgroundMask()
}
return imagenModel.editImage(
listOfNotNull(ImagenRawImage(bitmap.toImagenInlineImage()), mask),
inputText,
ImagenEditingConfig(ImagenEditMode.INPAINT_INSERTION)
)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenOutpaintingViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.media.imagen
import android.graphics.Bitmap
import com.google.firebase.Firebase
import com.google.firebase.ai.ImagenModel
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Dimensions
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.ImagenEditMode
import com.google.firebase.ai.type.ImagenEditingConfig
import com.google.firebase.ai.type.ImagenGenerationResponse
import com.google.firebase.ai.type.ImagenImageFormat
import com.google.firebase.ai.type.ImagenImagePlacement
import com.google.firebase.ai.type.ImagenInlineImage
import com.google.firebase.ai.type.ImagenMaskReference
import com.google.firebase.ai.type.ImagenPersonFilterLevel
import com.google.firebase.ai.type.ImagenRawMask
import com.google.firebase.ai.type.ImagenSafetyFilterLevel
import com.google.firebase.ai.type.ImagenSafetySettings
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.ai.type.imagenGenerationConfig
import com.google.firebase.ai.type.toImagenInlineImage
import com.google.firebase.quickstart.ai.ui.ImagenUiState
import kotlinx.serialization.Serializable
@Serializable
object ImagenOutpaintingRoute
@OptIn(PublicPreviewAPI::class)
class ImagenOutpaintingViewModel : ImagenViewModel() {
override val initialPrompt: String = ""
override val includeAttach: Boolean = true
override val selectionOptions: List = listOf("Image Alignment", "Center", "Top", "Bottom", "Left", "Right")
override val allowEmptyPrompt: Boolean = true
override val additionalImage: Bitmap? = null
override val imageLabels: List = emptyList()
private val imagenModel: ImagenModel
init {
val config = imagenGenerationConfig {
numberOfImages = 4
imageFormat = ImagenImageFormat.png()
}
val settings = ImagenSafetySettings(
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
)
imagenModel = Firebase.ai(
backend = GenerativeBackend.vertexAI()
).imagenModel(
modelName = "imagen-3.0-capability-001",
generationConfig = config,
safetySettings = settings
)
}
override suspend fun performGeneration(
inputText: String,
currentState: ImagenUiState.Success
): ImagenGenerationResponse {
val bitmap = currentState.attachedImage!!
val position = when (currentState.selectedOption) {
"Top" -> ImagenImagePlacement.TOP_CENTER
"Bottom" -> ImagenImagePlacement.BOTTOM_CENTER
"Left" -> ImagenImagePlacement.LEFT_CENTER
"Right" -> ImagenImagePlacement.RIGHT_CENTER
else -> ImagenImagePlacement.CENTER
}
val dimensions = Dimensions(bitmap.width * 2, bitmap.height * 2)
val (sourceImage, mask) = ImagenMaskReference.generateMaskAndPadForOutpainting(
bitmap.toImagenInlineImage(),
dimensions,
position
)
return imagenModel.editImage(
listOf(sourceImage, ImagenRawMask(mask.image!!, 0.05)),
inputText,
ImagenEditingConfig(ImagenEditMode.OUTPAINT)
)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenStyleTransferViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.media.imagen
import android.graphics.Bitmap
import com.google.firebase.Firebase
import com.google.firebase.ai.ImagenModel
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.ImagenGenerationResponse
import com.google.firebase.ai.type.ImagenImageFormat
import com.google.firebase.ai.type.ImagenInlineImage
import com.google.firebase.ai.type.ImagenPersonFilterLevel
import com.google.firebase.ai.type.ImagenRawImage
import com.google.firebase.ai.type.ImagenSafetyFilterLevel
import com.google.firebase.ai.type.ImagenSafetySettings
import com.google.firebase.ai.type.ImagenStyleReference
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.ai.type.imagenGenerationConfig
import com.google.firebase.ai.type.toImagenInlineImage
import com.google.firebase.quickstart.ai.MainActivity
import com.google.firebase.quickstart.ai.ui.ImagenUiState
import kotlinx.serialization.Serializable
@Serializable
object ImagenStyleTransferRoute
@OptIn(PublicPreviewAPI::class)
class ImagenStyleTransferViewModel : ImagenViewModel() {
override val initialPrompt: String = "A picture of a cat"
override val includeAttach: Boolean = true
override val selectionOptions: List = emptyList()
override val allowEmptyPrompt: Boolean = true
override val additionalImage: Bitmap = MainActivity.catImage
override val imageLabels: List = listOf("Style Target", "Style Source")
private val imagenModel: ImagenModel
init {
val config = imagenGenerationConfig {
numberOfImages = 4
imageFormat = ImagenImageFormat.png()
}
val settings = ImagenSafetySettings(
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
)
imagenModel = Firebase.ai(
backend = GenerativeBackend.vertexAI()
).imagenModel(
modelName = "imagen-3.0-capability-001",
generationConfig = config,
safetySettings = settings
)
}
override suspend fun performGeneration(
inputText: String,
currentState: ImagenUiState.Success
): ImagenGenerationResponse {
val attachedImage = currentState.attachedImage!!
return imagenModel.editImage(
listOf(
ImagenRawImage(MainActivity.catImage.toImagenInlineImage()),
ImagenStyleReference(attachedImage.toImagenInlineImage(), 1, "an art style")
),
"Generate an image in an art style [1] based on the following caption: $inputText",
)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenSubjectReferenceViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.media.imagen
import android.graphics.Bitmap
import com.google.firebase.Firebase
import com.google.firebase.ai.ImagenModel
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.ImagenGenerationResponse
import com.google.firebase.ai.type.ImagenImageFormat
import com.google.firebase.ai.type.ImagenInlineImage
import com.google.firebase.ai.type.ImagenPersonFilterLevel
import com.google.firebase.ai.type.ImagenSafetyFilterLevel
import com.google.firebase.ai.type.ImagenSafetySettings
import com.google.firebase.ai.type.ImagenSubjectReference
import com.google.firebase.ai.type.ImagenSubjectReferenceType
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.ai.type.imagenGenerationConfig
import com.google.firebase.ai.type.toImagenInlineImage
import com.google.firebase.quickstart.ai.ui.ImagenUiState
import kotlinx.serialization.Serializable
@Serializable
object ImagenSubjectReferenceRoute
@OptIn(PublicPreviewAPI::class)
class ImagenSubjectReferenceViewModel : ImagenViewModel() {
override val initialPrompt: String = " flying through space"
override val includeAttach: Boolean = true
override val selectionOptions: List = emptyList()
override val allowEmptyPrompt: Boolean = false
override val additionalImage: Bitmap? = null
override val imageLabels: List = emptyList()
private val imagenModel: ImagenModel
init {
val config = imagenGenerationConfig {
numberOfImages = 4
imageFormat = ImagenImageFormat.png()
}
val settings = ImagenSafetySettings(
safetyFilterLevel = ImagenSafetyFilterLevel.BLOCK_LOW_AND_ABOVE,
personFilterLevel = ImagenPersonFilterLevel.BLOCK_ALL
)
imagenModel = Firebase.ai(
backend = GenerativeBackend.vertexAI()
).imagenModel(
modelName = "imagen-3.0-capability-001",
generationConfig = config,
safetySettings = settings
)
}
override suspend fun performGeneration(
inputText: String,
currentState: ImagenUiState.Success
): ImagenGenerationResponse {
val attachedImage = currentState.attachedImage!!
return imagenModel.editImage(
listOf(
ImagenSubjectReference(
referenceId = 1,
image = attachedImage.toImagenInlineImage(),
subjectType = ImagenSubjectReferenceType.ANIMAL,
description = "An animal"
)
),
"Create an image about An animal [1] to match the description: " +
inputText.replace("", "An animal [1]"),
)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenTemplateViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.media.imagen
import android.graphics.Bitmap
import com.google.firebase.Firebase
import com.google.firebase.ai.TemplateImagenModel
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.ImagenGenerationResponse
import com.google.firebase.ai.type.ImagenInlineImage
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.quickstart.ai.ui.ImagenUiState
import kotlinx.serialization.Serializable
@Serializable
object ImagenTemplateRoute
@OptIn(PublicPreviewAPI::class)
class ImagenTemplateViewModel : ImagenViewModel() {
override val initialPrompt: String = "List of things that should be in the image"
override val includeAttach: Boolean = false
override val selectionOptions: List = emptyList()
override val allowEmptyPrompt: Boolean = false
override val additionalImage: Bitmap? = null
override val imageLabels: List = emptyList()
private var templateImagenModel: TemplateImagenModel
init {
templateImagenModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).templateImagenModel()
}
override suspend fun performGeneration(
inputText: String,
currentState: ImagenUiState.Success
): ImagenGenerationResponse {
return try {
templateImagenModel.generateImages("imagen-basic", mapOf("prompt" to inputText))
} catch (e: Exception) {
if (e.localizedMessage?.contains("not found") == true) {
throw Exception(
"""
Template was not found, please verify that your project contains a template named "imagen-basic".
""".trimIndent()
)
} else {
throw e
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/media/imagen/ImagenViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.media.imagen
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.core.graphics.scale
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.ai.type.ImagenGenerationResponse
import com.google.firebase.ai.type.ImagenInlineImage
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.quickstart.ai.ui.ImagenUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@OptIn(PublicPreviewAPI::class)
abstract class ImagenViewModel : ViewModel() {
abstract val initialPrompt: String
abstract val includeAttach: Boolean
abstract val selectionOptions: List
abstract val allowEmptyPrompt: Boolean
abstract val additionalImage: Bitmap?
abstract val imageLabels: List
private val _uiState = MutableStateFlow(ImagenUiState.Success())
val uiState: StateFlow = _uiState.asStateFlow()
protected abstract suspend fun performGeneration(
inputText: String,
currentState: ImagenUiState.Success
): ImagenGenerationResponse
fun generateImages(inputText: String) {
val currentState = (_uiState.value as? ImagenUiState.Success) ?: ImagenUiState.Success()
viewModelScope.launch {
_uiState.value = ImagenUiState.Loading
try {
val imageResponse = performGeneration(inputText, currentState)
_uiState.value = currentState.copy(images = imageResponse.images.map { it.asBitmap() })
} catch (e: Exception) {
_uiState.value = ImagenUiState.Error(e.localizedMessage ?: "Unknown error")
}
}
}
suspend fun attachImage(
fileInBytes: ByteArray,
) {
val originalBitmap = BitmapFactory.decodeByteArray(fileInBytes, 0, fileInBytes.size)
val resizedBitmap = originalBitmap.scale(
512,
(originalBitmap.height * (512.0 / originalBitmap.width)).toInt()
)
val currentState = (_uiState.value as? ImagenUiState.Success) ?: ImagenUiState.Success()
_uiState.value = currentState.copy(attachedImage = resizedBitmap)
}
fun selectOption(selection: String) {
val currentState = (_uiState.value as? ImagenUiState.Success) ?: ImagenUiState.Success()
_uiState.value = currentState.copy(selectedOption = selection)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioSummarizationViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.content
import com.google.firebase.quickstart.ai.ui.ChatUiState
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object AudioSummarizationRoute
class AudioSummarizationViewModel : ChatViewModel() {
override val initialPrompt: String =
"""
I have attached the audio file. Please analyze it and summarize
the contents of the audio as bullet points.
""".trimIndent()
private val chat: Chat
init {
val generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-3.1-flash-lite-preview"
)
chat = generativeModel.startChat(
listOf(
content { text("Can you help me summarize an audio file?") },
content("model") {
text(
"Of course! Click on the attach button" +
" below and choose an audio file for me to summarize."
)
}
))
_messages.value = chat.history.map { UiChatMessage(it) }
_uiState.value = ChatUiState.Success
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/AudioTranslationViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object AudioTranslationRoute
class AudioTranslationViewModel : ChatViewModel() {
override val initialPrompt: String = "Please translate the audio to Mandarin."
private val chat: Chat
init {
val generativeModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel(
modelName = "gemini-2.5-flash"
)
chat = generativeModel.startChat()
// Handling the initial fileData in the prompt builder for the first message
contentBuilder.fileData(
"https://storage.googleapis.com/cloud-samples-data/generative-ai/audio/" +
"How_to_create_a_My_Map_in_Google_Maps.mp3",
"audio/mpeg"
)
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ChatViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import android.graphics.BitmapFactory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerateContentResponse
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.quickstart.ai.ui.Attachment
import com.google.firebase.quickstart.ai.ui.ChatUiState
import com.google.firebase.quickstart.ai.ui.UiChatMessage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@OptIn(PublicPreviewAPI::class)
abstract class ChatViewModel : ViewModel() {
protected val _uiState = MutableStateFlow(ChatUiState.Success)
val uiState: StateFlow = _uiState.asStateFlow()
protected val _messages = MutableStateFlow>(emptyList())
val messages: StateFlow> = _messages.asStateFlow()
protected val _attachments = MutableStateFlow>(emptyList())
val attachments: StateFlow> = _attachments.asStateFlow()
abstract val initialPrompt: String
// Builder for the next message
protected var contentBuilder = Content.Builder()
/**
* Entry point for sending a message.
* Handles adding the message to the UI and setting the loading state.
*/
fun sendMessage(userMessage: String) {
val prompt = contentBuilder
.text(userMessage)
.build()
_messages.value = _messages.value + UiChatMessage(prompt)
viewModelScope.launch {
_uiState.value = ChatUiState.Loading
try {
performSendMessage(prompt, _messages.value)
} catch (e: Exception) {
_uiState.value = ChatUiState.Error(e.localizedMessage ?: "Unknown error")
} finally {
contentBuilder = Content.Builder() // reset the builder
}
}
}
/**
* Subclasses implement this to handle the actual AI logic.
*/
protected abstract suspend fun performSendMessage(
prompt: Content,
currentMessages: List
)
/**
* Centralized method to validate the AI response (grounding check) and update the UI state.
*/
protected fun validateAndDisplayResponse(
response: GenerateContentResponse,
currentMessages: List
) {
val candidate = response.candidates.firstOrNull() ?: return
// Compliance check for grounding
if (candidate.groundingMetadata != null
&& candidate.groundingMetadata?.groundingChunks?.isNotEmpty() == true
&& candidate.groundingMetadata?.searchEntryPoint == null
) {
_uiState.value = ChatUiState.Error(
"Could not display the response because it was missing required attribution components."
)
} else {
_messages.value = currentMessages + UiChatMessage(candidate.content, candidate.groundingMetadata)
_attachments.value = emptyList()
_uiState.value = ChatUiState.Success
}
}
fun attachFile(
fileInBytes: ByteArray,
mimeType: String?,
fileName: String? = "Unnamed file"
) {
if (mimeType?.contains("image") == true) {
// images should be attached as ImageParts
contentBuilder.image(decodeBitmapFromImage(fileInBytes))
} else {
contentBuilder.inlineData(fileInBytes, mimeType ?: "text/plain")
}
_attachments.value = _attachments.value + Attachment(fileName ?: "Unnamed file")
}
protected fun decodeBitmapFromImage(input: ByteArray) =
BitmapFactory.decodeByteArray(input, 0, input.size)
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/CourseRecommendationsViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.content
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object CourseRecommendationsRoute
class CourseRecommendationsViewModel : ChatViewModel() {
override val initialPrompt: String = "I am interested in Performing Arts. I have taken Theater 1A."
private val chat: Chat
init {
val generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-2.5-flash",
systemInstruction = content {
text(
"You are a chatbot for the county's performing and fine arts" +
" program. You help students decide what course they will" +
" take during the summer."
)
}
)
chat = generativeModel.startChat()
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/DocumentComparisonViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object DocumentComparisonRoute
class DocumentComparisonViewModel : ChatViewModel() {
override val initialPrompt: String = "The first document is from 2013, and the second document is" +
" from 2023. How did the standard deduction evolve?"
private val chat: Chat
init {
val generativeModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel(
modelName = "gemini-2.5-flash"
)
chat = generativeModel.startChat()
// Pre-attach the documents
contentBuilder.fileData(
"https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/form_1040_2013.pdf",
"application/pdf"
)
contentBuilder.fileData(
"https://storage.googleapis.com/cloud-samples-data/generative-ai/pdf/form_1040_2023.pdf",
"application/pdf"
)
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/GoogleSearchGroundingViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.Tool
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object GoogleSearchGroundingRoute
class GoogleSearchGroundingViewModel : ChatViewModel() {
override val initialPrompt: String = "What's the weather in Chicago this weekend?"
private val chat: Chat
init {
val generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-2.5-flash",
tools = listOf(Tool.googleSearch())
)
chat = generativeModel.startChat()
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageBlogCreatorViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object ImageBlogCreatorRoute
class ImageBlogCreatorViewModel : ChatViewModel() {
override val initialPrompt: String = "Write a short, engaging blog post based on this picture." +
" It should include a description of the meal in the" +
" photo and talk about my journey meal prepping."
private val chat: Chat
init {
val generativeModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel(
modelName = "gemini-2.5-flash"
)
chat = generativeModel.startChat()
// Pre-attach the image from cloud storage
contentBuilder.fileData(
"https://storage.googleapis.com/cloud-samples-data/generative-ai/image/meal-prep.jpeg",
"image/jpeg"
)
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ImageGenerationViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.ResponseModality
import com.google.firebase.ai.type.generationConfig
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object ImageGenerationRoute
class ImageGenerationViewModel : ChatViewModel() {
override val initialPrompt: String = """
Hi, can you create a 3d rendered image of a pig
with wings and a top hat flying over a happy
futuristic scifi city with lots of greenery?
""".trimIndent()
private val chat: Chat
init {
val generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-2.5-flash-image",
generationConfig = generationConfig {
responseModalities = listOf(ResponseModality.TEXT, ResponseModality.IMAGE)
}
)
chat = generativeModel.startChat()
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ServerPromptTemplateViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.Firebase
import com.google.firebase.ai.TemplateGenerativeModel
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.PublicPreviewAPI
import com.google.firebase.quickstart.ai.ui.ServerPromptUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@Serializable
object ServerPromptTemplateRoute
@OptIn(PublicPreviewAPI::class)
class ServerPromptTemplateViewModel : ViewModel() {
val initialPrompt = "Jane Doe"
val allowEmptyPrompt = false
private val _uiState = MutableStateFlow(ServerPromptUiState.Success())
val uiState: StateFlow = _uiState.asStateFlow()
private var templateGenerativeModel: TemplateGenerativeModel
init {
templateGenerativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).templateGenerativeModel()
}
fun generate(inputText: String) {
viewModelScope.launch {
_uiState.value = ServerPromptUiState.Loading
try {
val response = templateGenerativeModel
.generateContent("input-system-instructions", mapOf("customerName" to inputText))
_uiState.value = ServerPromptUiState.Success(response.text)
} catch (e: Exception) {
_uiState.value = ServerPromptUiState.Error(
if (e.localizedMessage?.contains("not found") == true) {
"""
Template was not found, please verify that your project contains a template
named "input-system-instructions".
""".trimIndent()
} else {
e.localizedMessage ?: "Unknown error"
}
)
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/SvgViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.firebase.Firebase
import com.google.firebase.ai.GenerativeModel
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.content
import com.google.firebase.ai.type.generationConfig
import com.google.firebase.ai.type.thinkingConfig
import kotlinx.coroutines.Dispatchers
import com.google.firebase.quickstart.ai.ui.SvgUiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@Serializable
object SvgRoute
class SvgViewModel : ViewModel() {
private val _uiState = MutableStateFlow(SvgUiState.Success())
val uiState: StateFlow = _uiState.asStateFlow()
private val generativeModel: GenerativeModel
init {
generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-3-flash-preview",
systemInstruction = content {
text(
"""
You are an expert at turning image prompts into SVG code. When given a prompt,
use your creativity to code a 800x600 SVG rendering of it.
Always add viewBox="0 0 800 600" to the root svg tag. Do
not import external assets, they won't work. Return ONLY the SVG code, nothing else,
no commentary.
""".trimIndent()
)
},
generationConfig = generationConfig {
thinkingConfig {
thinkingBudget = -1
}
}
)
}
fun generateSVG(prompt: String) {
val currentSvgs = (_uiState.value as? SvgUiState.Success)?.svgs ?: emptyList()
_uiState.value = SvgUiState.Loading
viewModelScope.launch(Dispatchers.IO) {
try {
val response = generativeModel.generateContent(prompt)
val newSvg = response.text
if (newSvg != null) {
_uiState.value = SvgUiState.Success(listOf(newSvg) + currentSvgs)
} else {
_uiState.value = SvgUiState.Success(currentSvgs)
}
} catch (e: Exception) {
_uiState.value = SvgUiState.Error(e.localizedMessage ?: "Unknown error")
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/ThinkingChatViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.generationConfig
import com.google.firebase.ai.type.thinkingConfig
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object ThinkingChatRoute
class ThinkingChatViewModel : ChatViewModel() {
override val initialPrompt: String = "Analogize photosynthesis and growing up."
private val chat: Chat
init {
val generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-2.5-flash",
generationConfig = generationConfig {
thinkingConfig = thinkingConfig {
includeThoughts = true
thinkingBudget = -1 // Dynamic Thinking
}
}
)
chat = generativeModel.startChat()
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TranslationViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.content
import com.google.firebase.quickstart.ai.ui.UiChatMessage
import kotlinx.serialization.Serializable
@Serializable
object TranslationRoute
class TranslationViewModel : ChatViewModel() {
override val initialPrompt: String
get() = """
Translate the following text to Spanish:
Hey, are you down to grab some pizza later? I'm starving!
""".trimIndent()
private val chat: Chat
init {
val generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-3.1-flash-lite-preview",
systemInstruction = content {
text("Only output the translated text")
}
)
chat = generativeModel.startChat()
}
override suspend fun performSendMessage(
prompt: Content,
currentMessages: List
) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/TravelTipsViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.content
import com.google.firebase.quickstart.ai.ui.ChatUiState
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object TravelTipsRoute
class TravelTipsViewModel : ChatViewModel() {
override val initialPrompt: String = "What else is important when traveling?"
private val chat: Chat
init {
val generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-2.5-flash",
systemInstruction = content {
text(
"You are a Travel assistant. You will answer" +
" questions the user asks based on the information listed" +
" in Relevant Information. Do not hallucinate. Do not use" +
" the internet."
)
}
)
chat = generativeModel.startChat(
history = listOf(
content("role") {
text("I have never traveled before. When should I book a flight?")
},
content("model") {
text(
"You should book flights a couple of months ahead of time." +
" It will be cheaper and more flexible for you."
)
},
content("user") {
text("Do I need a passport?")
},
content("model") {
text(
"If you are traveling outside your own country, make sure" +
" your passport is up-to-date and valid for more" +
" than 6 months during your travel."
)
}
)
)
_messages.value = chat.history.map { UiChatMessage(it) }
_uiState.value = ChatUiState.Success
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoHashtagGeneratorViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object VideoHashtagGeneratorRoute
class VideoHashtagGeneratorViewModel : ChatViewModel() {
override val initialPrompt: String = "Generate 5-10 hashtags that relate to the video content." +
" Try to use more popular and engaging terms," +
" e.g. #Viral. Do not add content not related to" +
" the video.\n Start the output with 'Tags:'"
private val chat: Chat
init {
val generativeModel = Firebase.ai(backend = GenerativeBackend.vertexAI()).generativeModel(
modelName = "gemini-2.5-flash"
)
chat = generativeModel.startChat()
// Pre-attach the video
contentBuilder.fileData(
"https://storage.googleapis.com/cloud-samples-data/generative-ai/video/google_home_celebrity_ad.mp4",
"video/mpeg"
)
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/VideoSummarizationViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.content
import com.google.firebase.quickstart.ai.ui.ChatUiState
import com.google.firebase.quickstart.ai.ui.UiChatMessage
@Serializable
object VideoSummarizationRoute
class VideoSummarizationViewModel : ChatViewModel() {
override val initialPrompt: String = "I have attached the video file. Provide a description of" +
" the video. The description should also contain" +
" anything important which people say in the video."
private val chat: Chat
init {
val chatHistory = listOf(
content { text("Can you help me with the description of a video file?") },
content("model") {
text(
"Sure! Click on the attach button below and choose a" +
" video file for me to describe."
)
}
)
_messages.value = chatHistory.map { UiChatMessage(it) }
_uiState.value = ChatUiState.Success
val generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-2.5-flash"
)
chat = generativeModel.startChat(chatHistory)
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
validateAndDisplayResponse(response, currentMessages)
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/WeatherChatViewModel.kt
================================================
package com.google.firebase.quickstart.ai.feature.text
import kotlinx.serialization.Serializable
import android.util.Log
import com.google.firebase.Firebase
import com.google.firebase.ai.Chat
import com.google.firebase.ai.ai
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.FunctionDeclaration
import com.google.firebase.ai.type.FunctionResponsePart
import com.google.firebase.ai.type.GenerateContentResponse
import com.google.firebase.ai.type.GenerativeBackend
import com.google.firebase.ai.type.Schema
import com.google.firebase.ai.type.Tool
import com.google.firebase.ai.type.content
import com.google.firebase.quickstart.ai.feature.text.functioncalling.WeatherRepository
import com.google.firebase.quickstart.ai.ui.UiChatMessage
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
@Serializable
object WeatherChatRoute
class WeatherChatViewModel : ChatViewModel() {
override val initialPrompt: String = "What was the weather in Boston, MA on October 17, 2024?"
private val chat: Chat
init {
val generativeModel = Firebase.ai(
backend = GenerativeBackend.googleAI()
).generativeModel(
modelName = "gemini-2.5-flash",
tools = listOf(
Tool.functionDeclarations(
listOf(
FunctionDeclaration(
"fetchWeather",
"Get the weather conditions for a specific US city on a specific date.",
mapOf(
"city" to Schema.string("The US city of the location."),
"state" to Schema.string("The US state of the location."),
"date" to Schema.string(
"The date for which to get the weather." +
" Date must be in the format: YYYY-MM-DD."
),
),
)
)
)
)
)
chat = generativeModel.startChat()
}
override suspend fun performSendMessage(prompt: Content, currentMessages: List) {
val response = chat.sendMessage(prompt)
if (response.functionCalls.isEmpty()) {
validateAndDisplayResponse(response, currentMessages)
} else {
handleFunctionCalls(response, currentMessages)
}
}
private suspend fun handleFunctionCalls(
response: GenerateContentResponse,
currentMessages: List
) {
response.functionCalls.forEach { functionCall ->
Log.d(
"WeatherChatViewModel", "Model responded with function call:" +
functionCall.name
)
when (functionCall.name) {
"fetchWeather" -> {
val city = functionCall.args["city"]?.jsonPrimitive?.content
val state = functionCall.args["state"]?.jsonPrimitive?.content // Fixed state retrieval
val date = functionCall.args["date"]?.jsonPrimitive?.content
val finalResponse = if (city == null || state == null || date == null) {
chat.sendMessage(content("function") {
part(FunctionResponsePart("fetchWeather",
JsonObject(
mapOf(
"error" to JsonPrimitive("Unable to fetch weather - one of the parameters was null"),
)
)))
})
} else {
val functionResponse = WeatherRepository
.fetchWeather(city, state, date)
chat.sendMessage(content("function") {
part(FunctionResponsePart("fetchWeather", functionResponse))
})
}
validateAndDisplayResponse(finalResponse, currentMessages)
}
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/feature/text/functioncalling/WeatherRepository.kt
================================================
package com.google.firebase.quickstart.ai.feature.text.functioncalling
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
/**
* Hypothetical repository that calls an external weather API.
*/
class WeatherRepository {
companion object {
suspend fun fetchWeather(
city: String, state: String, date: String
): JsonObject = withContext(Dispatchers.IO) {
// For demo purposes, this hypothetical response is
// hardcoded here in the expected format.
return@withContext JsonObject(
mapOf(
"temperature" to JsonPrimitive(38),
"chancePrecipitation" to JsonPrimitive("56%"),
"cloudConditions" to JsonPrimitive("partlyCloudy")
)
)
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/CameraView.kt
================================================
package com.google.firebase.quickstart.ai.ui
import android.annotation.SuppressLint
import android.graphics.Bitmap
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlin.time.Duration.Companion.seconds
@Composable
fun CameraView(
modifier: Modifier = Modifier,
cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA,
onFrameCaptured: (Bitmap) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx)
val executor = ContextCompat.getMainExecutor(ctx)
cameraProviderFuture.addListener(
{
val cameraProvider = cameraProviderFuture.get()
bindPreview(
lifecycleOwner,
previewView,
cameraProvider,
cameraSelector,
onFrameCaptured,
)
},
executor,
)
previewView
},
modifier = modifier,
)
}
private fun bindPreview(
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
cameraProvider: ProcessCameraProvider,
cameraSelector: CameraSelector,
onFrameCaptured: (Bitmap) -> Unit,
) {
val preview =
Preview.Builder().build().also { it.surfaceProvider = previewView.surfaceProvider }
val imageAnalysis =
ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(
ContextCompat.getMainExecutor(previewView.context),
SnapshotFrameAnalyzer(onFrameCaptured),
)
}
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
}
// Calls the [onFrameCaptured] callback with the captured frame every second.
private class SnapshotFrameAnalyzer(private val onFrameCaptured: (Bitmap) -> Unit) :
ImageAnalysis.Analyzer {
private var lastFrameTimestamp = 0L
private val interval = 1.seconds // 1 second
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(image: ImageProxy) {
val currentTimestamp = System.currentTimeMillis()
if (lastFrameTimestamp == 0L) {
lastFrameTimestamp = currentTimestamp
}
if (currentTimestamp - lastFrameTimestamp >= interval.inWholeMilliseconds) {
onFrameCaptured(image.toBitmap())
lastFrameTimestamp = currentTimestamp
}
image.close()
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ChatScreen.kt
================================================
package com.google.firebase.quickstart.ai.ui
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.provider.OpenableColumns
import android.text.format.Formatter
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Attachment
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.firebase.ai.type.FileDataPart
import com.google.firebase.ai.type.ImagePart
import com.google.firebase.ai.type.InlineDataPart
import com.google.firebase.ai.type.TextPart
import com.google.firebase.ai.type.WebGroundingChunk
import com.google.firebase.quickstart.ai.feature.text.ChatViewModel
import kotlinx.coroutines.launch
@Composable
fun ChatScreen(
chatViewModel: ChatViewModel
) {
val uiState by chatViewModel.uiState.collectAsStateWithLifecycle()
val messages by chatViewModel.messages.collectAsStateWithLifecycle()
val attachments by chatViewModel.attachments.collectAsStateWithLifecycle()
val initialPrompt: String = chatViewModel.initialPrompt
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxSize()
) {
ChatList(
messages,
listState,
modifier = Modifier
.fillMaxSize()
.weight(0.5f)
)
Box(
contentAlignment = Alignment.BottomCenter
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(color = MaterialTheme.colorScheme.surfaceContainer)
) {
if (uiState is ChatUiState.Loading) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
(uiState as? ChatUiState.Error)?.let {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = it.message,
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
AttachmentsList(attachments)
val context = LocalContext.current
val contentResolver = context.contentResolver
MessageInput(
initialPrompt = initialPrompt,
onSendMessage = { inputText ->
chatViewModel.sendMessage(inputText)
},
resetScroll = {
coroutineScope.launch {
listState.scrollToItem(0)
}
},
onFileAttached = { uri ->
val mimeType = contentResolver.getType(uri).orEmpty()
var fileName: String? = null
// Fetch file name and size
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
val humanReadableSize = Formatter.formatShortFileSize(
context,
cursor.getLong(sizeIndex)
)
fileName = "${cursor.getString(nameIndex)} ($humanReadableSize)"
}
contentResolver.openInputStream(uri)?.use { stream ->
val bytes = stream.readBytes()
chatViewModel.attachFile(bytes, mimeType, fileName)
}
},
isLoading = uiState is ChatUiState.Loading
)
}
}
}
}
@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
fun ChatBubbleItem(
message: UiChatMessage
) {
val isModelMessage = message.content.role == "model"
val isDarkTheme = isSystemInDarkTheme()
val backgroundColor = when (message.content.role) {
"user" -> MaterialTheme.colorScheme.tertiaryContainer
else -> MaterialTheme.colorScheme.secondaryContainer
}
val textColor = if (isModelMessage) {
MaterialTheme.colorScheme.onBackground
} else {
MaterialTheme.colorScheme.onTertiaryContainer
}
val bubbleShape = if (isModelMessage) {
RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp)
} else {
RoundedCornerShape(20.dp, 4.dp, 20.dp, 20.dp)
}
val horizontalAlignment = if (isModelMessage) {
Alignment.Start
} else {
Alignment.End
}
Column(
horizontalAlignment = horizontalAlignment,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
.fillMaxWidth()
) {
Text(
text = message.content.role?.uppercase() ?: "USER",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(bottom = 4.dp)
)
Row {
BoxWithConstraints {
Card(
colors = CardDefaults.cardColors(containerColor = backgroundColor),
shape = bubbleShape,
modifier = Modifier.widthIn(0.dp, maxWidth * 0.9f)
) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
message.content.parts.forEach { part ->
when (part) {
is TextPart -> {
if (part.isThought) {
ThoughtBubble(part.text)
} else {
Text(
text = part.text.trimIndent(),
modifier = Modifier.fillMaxWidth(),
color = textColor
)
}
}
is ImagePart -> {
Image(
bitmap = part.image.asImageBitmap(),
contentDescription = "Attached image",
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 4.dp)
)
}
is InlineDataPart -> {
// TODO: show a human readable version of audio, PDFs and videos
val attachmentType = if (part.mimeType.contains("audio")) {
"audio attached"
} else if (part.mimeType.contains("application/pdf")) {
"PDF attached"
} else if (part.mimeType.contains("video")) {
"video"
} else {
"file attached"
}
Text(
text = "($attachmentType)",
modifier = Modifier
.padding(4.dp)
.fillMaxWidth(),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.End
)
}
is FileDataPart -> {
Text(
text = part.uri,
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.End,
modifier = Modifier
.background(
backgroundColor.copy(
red = backgroundColor.red * 0.7f,
green = backgroundColor.green * 0.7f,
blue = backgroundColor.blue * 0.7f
)
)
.padding(4.dp)
.fillMaxWidth()
)
}
}
}
message.groundingMetadata?.let { metadata ->
HorizontalDivider(modifier = Modifier.padding(vertical = 18.dp))
// Search Entry Point (WebView)
metadata.searchEntryPoint?.let { searchEntryPoint ->
val context = LocalContext.current
AndroidView(
factory = {
WebView(it).apply {
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
request?.url?.let { uri ->
val intent = Intent(Intent.ACTION_VIEW, uri)
context.startActivity(intent)
}
// Return true to indicate we handled the URL loading
return true
}
}
setBackgroundColor(Color.TRANSPARENT)
loadDataWithBaseURL(
null,
searchEntryPoint.renderedContent,
"text/html",
"UTF-8",
null
)
}
},
modifier = Modifier
.clip(RoundedCornerShape(22.dp))
.fillMaxHeight()
.fillMaxWidth()
)
}
if (metadata.groundingChunks.isNotEmpty()) {
Text(
text = "Sources",
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
)
metadata.groundingChunks.forEach { chunk ->
chunk.web?.let { SourceLinkView(it) }
}
}
}
}
}
}
}
}
}
@Composable
fun SourceLinkView(
webChunk: WebGroundingChunk
) {
val context = LocalContext.current
val annotatedString = AnnotatedString.Builder(webChunk.title ?: "Untitled Source").apply {
addStyle(
style = SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline
),
start = 0,
end = webChunk.title?.length ?: "Untitled Source".length
)
webChunk.uri?.let { addStringAnnotation("URL", it, 0, it.length) }
}.toAnnotatedString()
Row(modifier = Modifier.padding(bottom = 8.dp)) {
Icon(
Icons.Default.Attachment,
contentDescription = "Source link",
modifier = Modifier.padding(end = 8.dp)
)
ClickableText(text = annotatedString, onClick = { offset ->
annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset)
.firstOrNull()?.let { annotation ->
context.startActivity(Intent(Intent.ACTION_VIEW, annotation.item.toUri()))
}
})
}
}
@Composable
fun ChatList(
chatMessages: List,
listState: LazyListState,
modifier: Modifier = Modifier
) {
LazyColumn(
reverseLayout = true,
state = listState,
modifier = modifier
) {
items(chatMessages.reversed()) { message ->
ChatBubbleItem(message)
}
}
}
@Composable
fun MessageInput(
initialPrompt: String,
onSendMessage: (String) -> Unit,
resetScroll: () -> Unit = {},
onFileAttached: (Uri) -> Unit,
isLoading: Boolean = false
) {
var userMessage by rememberSaveable { mutableStateOf(initialPrompt) }
Row(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
OutlinedTextField(
value = userMessage,
label = { Text("Message") },
onValueChange = { userMessage = it },
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Sentences
),
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(end = 4.dp)
.fillMaxWidth()
.weight(1f)
)
AttachmentsMenu(
modifier = Modifier.align(Alignment.CenterVertically),
onFileAttached = onFileAttached
)
IconButton(
onClick = {
if (userMessage.isNotBlank()) {
onSendMessage(userMessage)
userMessage = ""
resetScroll()
}
},
enabled = !isLoading,
modifier = Modifier
.align(Alignment.CenterVertically)
.clip(CircleShape)
.background(
color = if (isLoading) {
IconButtonDefaults.iconButtonColors().disabledContainerColor
} else {
MaterialTheme.colorScheme.primary
}
)
) {
Icon(
Icons.AutoMirrored.Default.Send,
contentDescription = "Send",
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
)
}
}
}
@Composable
fun AttachmentsMenu(
modifier: Modifier = Modifier,
onFileAttached: (Uri) -> Unit
) {
var expanded by remember { mutableStateOf(false) }
val openDocument = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
uri?.let {
onFileAttached(it)
}
}
Box(
modifier = modifier
.padding(end = 4.dp)
) {
IconButton(
onClick = {
expanded = !expanded
}
) {
Icon(
Icons.Default.AttachFile,
contentDescription = "Attach",
modifier = Modifier
.fillMaxSize()
.padding(4.dp)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
Text(
text = "Attach",
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
DropdownMenuItem(
text = { Text("Image / Video") },
onClick = {
openDocument.launch(arrayOf("image/*", "video/*"))
expanded = !expanded
}
)
DropdownMenuItem(
text = { Text("Audio") },
onClick = {
openDocument.launch(arrayOf("audio/*"))
expanded = !expanded
}
)
DropdownMenuItem(
text = { Text("Document (PDF)") },
onClick = {
openDocument.launch(arrayOf("application/pdf"))
expanded = !expanded
}
)
}
}
}
@Composable
fun AttachmentsList(
attachments: List
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
attachments.forEach { attachment ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Icon(
Icons.Default.Attachment,
contentDescription = "Attachment icon",
modifier = Modifier
.padding(4.dp)
.align(Alignment.CenterVertically)
)
Column(modifier = Modifier.align (Alignment.CenterVertically)) {
attachment.image?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = attachment.fileName,
modifier = Modifier
)
}
Text(
text = attachment.fileName,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.padding(horizontal = 4.dp)
)
}
}
}
}
}
@Composable
fun ThoughtBubble(
text: String
) {
var expanded by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.tertiaryContainer)
.clickable { expanded = !expanded }
.padding(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = if (expanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
contentDescription = if (expanded) "Collapse thoughts" else "Expand thoughts",
modifier = Modifier.padding(end = 8.dp),
tint = MaterialTheme.colorScheme.onTertiaryContainer
)
Text(
text = "Thoughts",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
AnimatedVisibility(visible = expanded) {
Column {
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Text(
text = text.trimIndent(),
style = MaterialTheme.typography.bodySmall.copy(
fontStyle = FontStyle.Italic
),
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.onTertiaryContainer
)
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ChatUiState.kt
================================================
package com.google.firebase.quickstart.ai.ui
import android.graphics.Bitmap
import com.google.firebase.ai.type.Content
import com.google.firebase.ai.type.GroundingMetadata
/**
* Meant to present attachments in the UI
*/
data class Attachment(
val fileName: String,
val image: Bitmap? = null // only for image attachments
)
/**
* A wrapper for a model [Content] object that includes additional UI-specific metadata.
*/
data class UiChatMessage(
val content: Content,
val groundingMetadata: GroundingMetadata? = null,
)
sealed interface ChatUiState {
data object Loading : ChatUiState
data object Success : ChatUiState
data class Error(val message: String) : ChatUiState
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ImagenScreen.kt
================================================
package com.google.firebase.quickstart.ai.ui
import android.net.Uri
import android.provider.OpenableColumns
import android.text.format.Formatter
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.firebase.quickstart.ai.R
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenViewModel
import kotlinx.coroutines.launch
@Composable
fun ImagenScreen(
imagenViewModel: ImagenViewModel
) {
val uiState by imagenViewModel.uiState.collectAsStateWithLifecycle()
val successState = uiState as? ImagenUiState.Success
val attachedImage = successState?.attachedImage
val generatedImages = successState?.images ?: emptyList()
var imagenPrompt by rememberSaveable { mutableStateOf(imagenViewModel.initialPrompt) }
val context = LocalContext.current
val contentResolver = context.contentResolver
val scope = rememberCoroutineScope()
val openDocument = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { optionalUri: Uri? ->
optionalUri?.let { uri ->
var fileName: String? = null
// Fetch file name and size
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
val humanReadableSize = Formatter.formatShortFileSize(
context, cursor.getLong(sizeIndex)
)
fileName = "${cursor.getString(nameIndex)} ($humanReadableSize)"
}
contentResolver.openInputStream(uri)?.use { stream ->
val bytes = stream.readBytes()
scope.launch {
imagenViewModel.attachImage(bytes)
}
}
}
}
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
ElevatedCard(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large
) {
OutlinedTextField(
value = imagenPrompt,
label = { Text("Prompt") },
placeholder = { Text("Enter text to generate image") },
onValueChange = { imagenPrompt = it },
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
)
if (imagenViewModel.selectionOptions.isNotEmpty()) {
DropDownMenu(imagenViewModel.selectionOptions) { imagenViewModel.selectOption(it) }
}
val attachmentsList = buildList {
if (imagenViewModel.additionalImage != null) {
add(
Attachment(
imagenViewModel.imageLabels.getOrElse(0) { "" },
imagenViewModel.additionalImage
)
)
}
if (attachedImage != null) {
add(Attachment(imagenViewModel.imageLabels.getOrElse(1) { "" }, attachedImage))
}
}
if (imagenViewModel.includeAttach && attachmentsList.isNotEmpty()) {
AttachmentsList(attachmentsList)
}
Row() {
if (imagenViewModel.includeAttach) {
TextButton(
onClick = {
openDocument.launch(arrayOf("image/*"))
},
modifier = Modifier
.padding(end = 16.dp, bottom = 16.dp)
) { Text("Attach") }
}
TextButton(
onClick = {
if (imagenViewModel.allowEmptyPrompt || imagenPrompt.isNotBlank()) {
imagenViewModel.generateImages(imagenPrompt)
}
},
modifier = Modifier
.padding(end = 16.dp, bottom = 16.dp)
) {
Text("Generate")
}
}
}
if (uiState is ImagenUiState.Loading) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(all = 8.dp)
.align(Alignment.CenterHorizontally)
) {
CircularProgressIndicator()
}
}
(uiState as? ImagenUiState.Error)?.let {
Card(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Text(
text = it.message,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(all = 16.dp)
)
}
}
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
modifier = Modifier
.padding(16.dp)
.height(500.dp)
) {
items(generatedImages) { image ->
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.onSecondaryContainer
)
) {
Image(bitmap = image.asImageBitmap(), "Generated image")
}
}
}
}
}
@Composable
fun DropDownMenu(items: List, onClick: (String) -> Unit) {
val isDropDownExpanded = remember {
mutableStateOf(false)
}
val itemPosition = remember {
mutableIntStateOf(0)
}
Column(
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
modifier = Modifier.padding(horizontal = 10.dp)
) {
Box {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.Top,
modifier = Modifier.clickable {
isDropDownExpanded.value = true
}
) {
Text(text = items[itemPosition.intValue])
Image(
painter = painterResource(id = R.drawable.round_arrow_drop_down_24),
contentDescription = "Dropdown Icon"
)
}
DropdownMenu(
expanded = isDropDownExpanded.value,
onDismissRequest = {
isDropDownExpanded.value = false
}) {
items.forEachIndexed { index, item ->
DropdownMenuItem(
text = {
Text(text = item)
},
onClick = {
isDropDownExpanded.value = false
itemPosition.intValue = index
onClick(item)
})
}
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ImagenUiState.kt
================================================
package com.google.firebase.quickstart.ai.ui
import android.graphics.Bitmap
sealed interface ImagenUiState {
data object Idle : ImagenUiState
data object Loading : ImagenUiState
data class Success(
val images: List = emptyList(),
val attachedImage: Bitmap? = null,
val selectedOption: String? = null
) : ImagenUiState
data class Error(val message: String) : ImagenUiState
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ServerPromptScreen.kt
================================================
package com.google.firebase.quickstart.ai.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateViewModel
@Composable
fun ServerPromptScreen(
viewModel: ServerPromptTemplateViewModel
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
val isLoading = uiState is ServerPromptUiState.Loading
val errorMessage = (uiState as? ServerPromptUiState.Error)?.message
val generatedText = (uiState as? ServerPromptUiState.Success)?.generatedText
ServerPromptContent(
initialPrompt = viewModel.initialPrompt,
isLoading = isLoading,
errorMessage = errorMessage,
generatedText = generatedText,
allowEmptyPrompt = viewModel.allowEmptyPrompt,
onGenerate = { viewModel.generate(it) }
)
}
@Composable
private fun ServerPromptContent(
initialPrompt: String,
isLoading: Boolean,
errorMessage: String?,
generatedText: String?,
allowEmptyPrompt: Boolean,
onGenerate: (String) -> Unit
) {
var textPrompt by rememberSaveable { mutableStateOf(initialPrompt) }
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
ElevatedCard(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large
) {
OutlinedTextField(
value = textPrompt,
label = { Text("Prompt") },
placeholder = { Text("Enter text to generate") },
onValueChange = { textPrompt = it },
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
)
Row() {
TextButton(
onClick = {
if (allowEmptyPrompt || textPrompt.isNotBlank()) {
onGenerate(textPrompt)
}
},
modifier = Modifier.padding(end = 16.dp, bottom = 16.dp)
) {
Text("Generate")
}
}
}
if (isLoading) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(all = 8.dp)
.align(Alignment.CenterHorizontally)
) {
CircularProgressIndicator()
}
}
errorMessage?.let {
Card(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Text(
text = it,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(all = 16.dp)
)
}
}
generatedText?.let {
Card(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
) {
Text(
text = it,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(all = 16.dp)
)
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/ServerPromptUiState.kt
================================================
package com.google.firebase.quickstart.ai.ui
sealed interface ServerPromptUiState {
data object Idle : ServerPromptUiState
data object Loading : ServerPromptUiState
data class Success(val generatedText: String? = null) : ServerPromptUiState
data class Error(val message: String) : ServerPromptUiState
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StreamRealtimeScreen.kt
================================================
package com.google.firebase.quickstart.ai.ui
import android.Manifest
import androidx.annotation.RequiresPermission
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CallEnd
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.quickstart.ai.feature.live.BidiViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
@Composable
fun StreamRealtimeScreen(bidiView: BidiViewModel) {
val isConversationActive = remember { mutableStateOf(false) }
val backgroundColor =
MaterialTheme.colorScheme.background
Surface(
modifier = Modifier.fillMaxSize(),
color = backgroundColor
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// The content will animate its size when it changes
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.animateContentSize()
) {
if (isConversationActive.value) {
// Active state UI
Text(
text = "Conversation Active",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Tap the end button to stop",
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
// Idle state UI
Text(
text = "Start Conversation",
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Tap the microphone to begin",
fontSize = 18.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Spacer(modifier = Modifier.height(80.dp))
// The main button with pulsing animation
if (isConversationActive.value) {
// Button to end the conversation
IconButton(
onClick = {
bidiView.endConversation()
isConversationActive.value = false },
modifier = Modifier
.size(90.dp)
.clip(CircleShape),
colors = IconButtonDefaults.iconButtonColors(
containerColor = Color(0xFFE63946), // A nice red color
contentColor = Color.White
)
) {
Icon(
imageVector = Icons.Default.CallEnd,
contentDescription = "End Conversation",
modifier = Modifier.size(48.dp)
)
}
} else {
// Button to start the conversation
IconButton(
onClick = {
CoroutineScope(Dispatchers.IO).launch {
bidiView.startConversation()
}
isConversationActive.value = true },
modifier = Modifier
.size(90.dp)
.clip(CircleShape),
colors = IconButtonDefaults.iconButtonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = Color.White
)
) {
Icon(
imageVector = Icons.Default.Mic,
contentDescription = "Start Conversation",
modifier = Modifier.size(48.dp)
)
}
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/StreamRealtimeVideoScreen.kt
================================================
package com.google.firebase.quickstart.ai.ui
import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresPermission
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.quickstart.ai.feature.live.BidiViewModel
import kotlinx.coroutines.launch
@RequiresPermission(allOf = [Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA])
@Composable
fun StreamRealtimeVideoScreen(bidiView: BidiViewModel) {
val backgroundColor = MaterialTheme.colorScheme.background
val scope = rememberCoroutineScope()
val context = LocalContext.current
var hasPermissions by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
)
}
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
permissions ->
hasPermissions = permissions.values.all { it }
}
LaunchedEffect(Unit) {
if (!hasPermissions) {
launcher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
}
}
DisposableEffect(hasPermissions) {
if (hasPermissions) {
scope.launch { bidiView.startConversation() }
}
onDispose { bidiView.endConversation() }
}
Surface(modifier = Modifier.fillMaxSize(), color = backgroundColor) {
Column(modifier = Modifier.fillMaxSize()) {
if (hasPermissions) {
Box(modifier = Modifier.fillMaxSize()) {
CameraView(
modifier = Modifier.fillMaxHeight(0.5f),
onFrameCaptured = { bitmap -> bidiView.sendVideoFrame(bitmap) },
)
}
} else {
Text("Camera and audio permissions are required to use this feature.")
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SvgScreen.kt
================================================
package com.google.firebase.quickstart.ai.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.SubcomposeAsyncImage
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.svg.SvgDecoder
import com.google.firebase.quickstart.ai.feature.text.SvgViewModel
import kotlinx.coroutines.Dispatchers
import java.nio.ByteBuffer
@Composable
fun SvgScreen(
svgViewModel: SvgViewModel
) {
var prompt by rememberSaveable { mutableStateOf("A kitten") }
val uiState by svgViewModel.uiState.collectAsStateWithLifecycle()
val isLoading = uiState is SvgUiState.Loading
val errorMessage = (uiState as? SvgUiState.Error)?.message
val generatedSvgs = (uiState as? SvgUiState.Success)?.svgs ?: emptyList()
Column {
ElevatedCard(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large
) {
OutlinedTextField(
value = prompt,
label = { Text("Generate a SVG of") },
placeholder = { Text("Enter text to generate image") },
onValueChange = { prompt = it },
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
)
TextButton(
onClick = {
svgViewModel.generateSVG(prompt)
},
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 16.dp)
.align(Alignment.End)
) {
Text("Generate")
}
}
if (isLoading) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(all = 8.dp)
.align(Alignment.CenterHorizontally)
) {
CircularProgressIndicator()
}
}
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(generatedSvgs) { svg ->
Card(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.onSecondaryContainer
)
) {
SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(ByteBuffer.wrap(svg.toByteArray()))
.decoderFactory(SvgDecoder.Factory())
.decoderCoroutineContext(Dispatchers.Main)
.crossfade(true)
.build(),
contentDescription = "Generated SVG",
modifier = Modifier
.fillMaxWidth()
)
}
}
}
errorMessage?.let {
Card(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
) {
Text(
text = it,
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(all = 16.dp)
)
}
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/SvgUiState.kt
================================================
package com.google.firebase.quickstart.ai.ui
sealed interface SvgUiState {
data object Idle : SvgUiState
data object Loading : SvgUiState
data class Success(val svgs: List = emptyList()) : SvgUiState
data class Error(val message: String) : SvgUiState
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/FirebaseAISamples.kt
================================================
package com.google.firebase.quickstart.ai.ui.navigation
import com.google.firebase.quickstart.ai.feature.live.StreamAudioViewModel
import com.google.firebase.quickstart.ai.feature.live.StreamVideoViewModel
import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeAudioRoute
import com.google.firebase.quickstart.ai.feature.live.StreamRealtimeVideoRoute
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenGenerationRoute
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenGenerationViewModel
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenInpaintingRoute
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenInpaintingViewModel
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenOutpaintingRoute
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenOutpaintingViewModel
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenStyleTransferRoute
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenStyleTransferViewModel
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenSubjectReferenceRoute
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenSubjectReferenceViewModel
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenTemplateRoute
import com.google.firebase.quickstart.ai.feature.media.imagen.ImagenTemplateViewModel
import com.google.firebase.quickstart.ai.feature.text.AudioSummarizationRoute
import com.google.firebase.quickstart.ai.feature.text.AudioSummarizationViewModel
import com.google.firebase.quickstart.ai.feature.text.AudioTranslationRoute
import com.google.firebase.quickstart.ai.feature.text.AudioTranslationViewModel
import com.google.firebase.quickstart.ai.feature.text.CourseRecommendationsRoute
import com.google.firebase.quickstart.ai.feature.text.CourseRecommendationsViewModel
import com.google.firebase.quickstart.ai.feature.text.DocumentComparisonRoute
import com.google.firebase.quickstart.ai.feature.text.DocumentComparisonViewModel
import com.google.firebase.quickstart.ai.feature.text.GoogleSearchGroundingRoute
import com.google.firebase.quickstart.ai.feature.text.GoogleSearchGroundingViewModel
import com.google.firebase.quickstart.ai.feature.text.ImageBlogCreatorRoute
import com.google.firebase.quickstart.ai.feature.text.ImageBlogCreatorViewModel
import com.google.firebase.quickstart.ai.feature.text.ImageGenerationRoute
import com.google.firebase.quickstart.ai.feature.text.ImageGenerationViewModel
import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateRoute
import com.google.firebase.quickstart.ai.feature.text.ServerPromptTemplateViewModel
import com.google.firebase.quickstart.ai.feature.text.SvgRoute
import com.google.firebase.quickstart.ai.feature.text.SvgViewModel
import com.google.firebase.quickstart.ai.feature.text.ThinkingChatRoute
import com.google.firebase.quickstart.ai.feature.text.ThinkingChatViewModel
import com.google.firebase.quickstart.ai.feature.text.TranslationRoute
import com.google.firebase.quickstart.ai.feature.text.TranslationViewModel
import com.google.firebase.quickstart.ai.feature.text.TravelTipsRoute
import com.google.firebase.quickstart.ai.feature.text.TravelTipsViewModel
import com.google.firebase.quickstart.ai.feature.text.VideoHashtagGeneratorRoute
import com.google.firebase.quickstart.ai.feature.text.VideoHashtagGeneratorViewModel
import com.google.firebase.quickstart.ai.feature.text.VideoSummarizationRoute
import com.google.firebase.quickstart.ai.feature.text.VideoSummarizationViewModel
import com.google.firebase.quickstart.ai.feature.text.WeatherChatRoute
import com.google.firebase.quickstart.ai.feature.text.WeatherChatViewModel
val FIREBASE_AI_SAMPLES = listOf(
Sample(
title = "Translate text",
description = "Use Gemini 3.1 Flash-Lite to translate text",
route = TranslationRoute,
screenType = ScreenType.CHAT,
viewModelClass = TranslationViewModel::class,
categories = listOf(Category.TEXT)
),
Sample(
title = "Travel tips",
description = "The user wants the model to help a new traveler" +
" with travel tips",
route = TravelTipsRoute,
screenType = ScreenType.CHAT,
viewModelClass = TravelTipsViewModel::class,
categories = listOf(Category.TEXT),
),
Sample(
title = "Chatbot recommendations for courses",
description = "A chatbot suggests courses for a performing arts program.",
route = CourseRecommendationsRoute,
screenType = ScreenType.CHAT,
viewModelClass = CourseRecommendationsViewModel::class,
categories = listOf(Category.TEXT),
),
Sample(
title = "Audio Summarization",
description = "Use Gemini 3.1 Flash Lite to summarize an audio file",
route = AudioSummarizationRoute,
screenType = ScreenType.CHAT,
viewModelClass = AudioSummarizationViewModel::class,
categories = listOf(Category.AUDIO),
),
Sample(
title = "Translation from audio (Vertex AI)",
description = "Translate an audio file stored in Cloud Storage",
route = AudioTranslationRoute,
screenType = ScreenType.CHAT,
viewModelClass = AudioTranslationViewModel::class,
categories = listOf(Category.AUDIO)
),
Sample(
title = "Blog post creator (Vertex AI)",
description = "Create a blog post from an image file stored in Cloud Storage.",
route = ImageBlogCreatorRoute,
screenType = ScreenType.CHAT,
viewModelClass = ImageBlogCreatorViewModel::class,
categories = listOf(Category.IMAGE)
),
Sample(
title = "Imagen 4 - image generation",
description = "Generate images using Imagen 4",
route = ImagenGenerationRoute,
screenType = ScreenType.IMAGEN,
viewModelClass = ImagenGenerationViewModel::class,
categories = listOf(Category.IMAGE)
),
Sample(
title = "Imagen 3 - Inpainting (Vertex AI)",
description = "Replace part of an image using Imagen 3",
route = ImagenInpaintingRoute,
screenType = ScreenType.IMAGEN,
viewModelClass = ImagenInpaintingViewModel::class,
categories = listOf(Category.IMAGE)
),
Sample(
title = "Imagen 3 - Outpainting (Vertex AI)",
description = "Expand an image by drawing in more background",
route = ImagenOutpaintingRoute,
screenType = ScreenType.IMAGEN,
viewModelClass = ImagenOutpaintingViewModel::class,
categories = listOf(Category.IMAGE)
),
Sample(
title = "Imagen 3 - Subject Reference (Vertex AI)",
description = "Generate an image using a referenced subject (must be an animal)",
route = ImagenSubjectReferenceRoute,
screenType = ScreenType.IMAGEN,
viewModelClass = ImagenSubjectReferenceViewModel::class,
categories = listOf(Category.IMAGE)
),
Sample(
title = "Imagen 3 - Style Transfer (Vertex AI)",
description = "Change the art style of a cat picture using a reference",
route = ImagenStyleTransferRoute,
screenType = ScreenType.IMAGEN,
viewModelClass = ImagenStyleTransferViewModel::class,
categories = listOf(Category.IMAGE)
),
Sample(
title = "Gemini 2.5 Flash Image (aka nanobanana)",
description = "Generate and/or edit images using Gemini 2.5 Flash Image aka nanobanana",
route = ImageGenerationRoute,
screenType = ScreenType.CHAT,
viewModelClass = ImageGenerationViewModel::class,
categories = listOf(Category.IMAGE)
),
Sample(
title = "Document comparison (Vertex AI)",
description = "Compare the contents of 2 documents." +
" Only supported by the Vertex AI Gemini API because the documents are stored in Cloud Storage",
route = DocumentComparisonRoute,
screenType = ScreenType.CHAT,
viewModelClass = DocumentComparisonViewModel::class,
categories = listOf(Category.DOCUMENT)
),
Sample(
title = "Hashtags for a video (Vertex AI)",
description = "Generate hashtags for a video ad stored in Cloud Storage",
route = VideoHashtagGeneratorRoute,
screenType = ScreenType.CHAT,
viewModelClass = VideoHashtagGeneratorViewModel::class,
categories = listOf(Category.VIDEO)
),
Sample(
title = "Summarize video",
description = "Summarize a video and extract important dialogue.",
route = VideoSummarizationRoute,
screenType = ScreenType.CHAT,
viewModelClass = VideoSummarizationViewModel::class,
categories = listOf(Category.VIDEO)
),
Sample(
title = "ForecastTalk",
description = "Use bidirectional streaming to get information about" +
" weather conditions for a specific US city on a specific date",
route = StreamRealtimeAudioRoute,
screenType = ScreenType.BIDI,
viewModelClass = StreamAudioViewModel::class,
categories = listOf(Category.LIVE_API, Category.AUDIO, Category.FUNCTION_CALLING)
),
Sample(
title = "Gemini Live (Video input)",
description = "Use bidirectional streaming to chat with Gemini using your" +
" phone's camera",
route = StreamRealtimeVideoRoute,
screenType = ScreenType.BIDI_VIDEO,
viewModelClass = StreamVideoViewModel::class,
categories = listOf(Category.LIVE_API, Category.VIDEO, Category.FUNCTION_CALLING)
),
Sample(
title = "Weather Chat",
description = "Use function calling to get the weather conditions" +
" for a specific US city on a specific date.",
route = WeatherChatRoute,
screenType = ScreenType.CHAT,
viewModelClass = WeatherChatViewModel::class,
categories = listOf(Category.TEXT, Category.FUNCTION_CALLING)
),
Sample(
title = "Grounding with Google Search",
description = "Use Grounding with Google Search to get responses based on up-to-date information from the" +
" web.",
route = GoogleSearchGroundingRoute,
screenType = ScreenType.CHAT,
viewModelClass = GoogleSearchGroundingViewModel::class,
categories = listOf(Category.TEXT)
),
Sample(
title = "Server Prompt Template - Imagen",
description = "Generate an image using a server prompt template. Note that you need to setup the template in " +
"the Firebase console before running this demo.",
route = ImagenTemplateRoute,
screenType = ScreenType.IMAGEN,
viewModelClass = ImagenTemplateViewModel::class,
categories = listOf(Category.IMAGE)
),
Sample(
title = "Server Prompt Templates - Gemini",
description = "Generate an invoice using server prompt templates. Note that you need to setup the template" +
" in the Firebase console before running this demo.",
route = ServerPromptTemplateRoute,
screenType = ScreenType.SERVER_PROMPT,
viewModelClass = ServerPromptTemplateViewModel::class,
categories = listOf(Category.TEXT),
),
Sample(
title = "Thinking",
description = "Gemini 2.5 Flash with dynamic thinking",
route = ThinkingChatRoute,
screenType = ScreenType.CHAT,
viewModelClass = ThinkingChatViewModel::class,
categories = listOf(Category.TEXT)
),
Sample(
title = "SVG Generator",
description = "Use Gemini 3 Flash preview to create SVG illustrations",
route = SvgRoute,
screenType = ScreenType.SVG,
viewModelClass = SvgViewModel::class,
categories = listOf(Category.IMAGE, Category.TEXT)
)
)
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/MainMenuScreen.kt
================================================
package com.google.firebase.quickstart.ai.ui.navigation
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.Card
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
val MIN_CARD_SIZE = 180.dp
@Composable
fun MainMenuScreen(
onSampleClicked: (Sample) -> Unit
) {
MenuScreen(
filterTitle = "Filter by use case:",
filters = Category.entries.toList(),
samples = FIREBASE_AI_SAMPLES,
onSampleClicked = {
onSampleClicked(it)
}
)
}
@Composable
fun MenuScreen(
filterTitle: String,
filters: List,
samples: List,
onSampleClicked: (sample: Sample) -> Unit = {}
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
var selectedCategory by rememberSaveable { mutableStateOf(filters.first()) }
Text(text = filterTitle, style = MaterialTheme.typography.titleLarge)
LazyRow {
items(filters) { capability ->
FilterChip(
onClick = { selectedCategory = capability },
label = {
Text(capability.label)
},
selected = selectedCategory == capability,
leadingIcon = if (selectedCategory == capability) {
{
Icon(
imageVector = Icons.Filled.Done,
contentDescription = "Done icon",
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
} else {
null
},
modifier = Modifier.padding(end = 8.dp)
)
}
}
Text(
text = "Samples",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp)
)
val filteredSamples = samples.filter {
it.categories.contains(selectedCategory)
}
LazyVerticalGrid(
columns = GridCells.Adaptive(MIN_CARD_SIZE),
modifier = Modifier
) {
items(filteredSamples) { sample ->
SampleItem(sample.title, sample.description, onItemClicked = {
onSampleClicked(sample)
})
}
}
}
}
@Composable
fun SampleItem(
titleResId: String,
descriptionResId: String,
onItemClicked: () -> Unit = {}
) {
Card(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = MIN_CARD_SIZE)
.padding(4.dp)
.clickable {
onItemClicked()
}
) {
Column(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxSize()
) {
Text(
text = titleResId,
style = MaterialTheme.typography.labelLarge
)
Text(
text = descriptionResId,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 8.dp)
)
}
}
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/navigation/Sample.kt
================================================
package com.google.firebase.quickstart.ai.ui.navigation
import androidx.lifecycle.ViewModel
import kotlin.reflect.KClass
enum class Category(
val label: String
) {
TEXT("Text"),
IMAGE("Image"),
VIDEO("Video"),
AUDIO("Audio"),
DOCUMENT("Document"),
FUNCTION_CALLING("Function calling"),
LIVE_API("Live API Streaming")
}
enum class ScreenType {
CHAT,
IMAGEN,
SVG,
SERVER_PROMPT,
BIDI,
BIDI_VIDEO
}
data class Sample(
val title: String,
val description: String,
val route: Any,
val screenType: ScreenType,
val viewModelClass: KClass? = null,
val categories: List,
)
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/theme/Color.kt
================================================
package com.google.firebase.quickstart.ai.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/theme/Theme.kt
================================================
package com.google.firebase.quickstart.ai.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun FirebaseAILogicTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
================================================
FILE: firebase-ai/app/src/main/java/com/google/firebase/quickstart/ai/ui/theme/Type.kt
================================================
package com.google.firebase.quickstart.ai.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
================================================
FILE: firebase-ai/app/src/main/res/drawable/ic_launcher_background.xml
================================================
================================================
FILE: firebase-ai/app/src/main/res/drawable/round_arrow_drop_down_24.xml
================================================
================================================
FILE: firebase-ai/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
================================================
FILE: firebase-ai/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: firebase-ai/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: firebase-ai/app/src/main/res/values/colors.xml
================================================
================================================
FILE: firebase-ai/app/src/main/res/values/strings.xml
================================================
Firebase AI Logic
================================================
FILE: firebase-ai/app/src/main/res/values/themes.xml
================================================
================================================
FILE: firebase-ai/app/src/main/res/xml/backup_rules.xml
================================================
================================================
FILE: firebase-ai/app/src/main/res/xml/data_extraction_rules.xml
================================================
================================================
FILE: firebase-ai/build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.google.services) apply false
}
================================================
FILE: firebase-ai/gradle/wrapper/gradle-wrapper.properties
================================================
#Tue Aug 19 11:04:48 PDT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: firebase-ai/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
================================================
FILE: firebase-ai/gradlew
================================================
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# 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
#
# https://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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
================================================
FILE: firebase-ai/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: firebase-ai/settings.gradle.kts
================================================
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenLocal()
google()
mavenCentral()
}
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
}
rootProject.name = "Firebase AI Logic"
include(":app")
================================================
FILE: firestore/.gitignore
================================================
.gradle
local.properties
.idea
build/
.DS_Store
*.iml
*.apk
*.aar
*.zip
google-services.json
================================================
FILE: firestore/CONTRIBUTING.md
================================================
# Contributing
We'd love for you to contribute to our source code and to make the project even better than it is today! Here are the guidelines we'd like you to follow:
- [Code of Conduct](#coc)
- [Question or Problem?](#question)
- [Issues and Bugs](#issue)
- [Submission Guidelines](#submit)
- [Signing the CLA](#cla)
## Code of Conduct
As contributors and maintainers of the project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
Communication through any of Firebase's channels (GitHub, StackOverflow, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the project to do the same.
If any member of the community violates this code of conduct, the maintainers of the project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
If you are subject to or witness unacceptable behavior, or have any other concerns, please drop us a line at samstern@google.com
## Got a Question or Problem?
If you have questions about how to use the Firebase Android Quickstarts, please direct these to [StackOverflow][stackoverflow] and use the `firebase` tag. We are also available on GitHub issues.
If you feel that we're missing an important bit of documentation, feel free to
file an issue so we can help. Here's an example to get you started:
```
What are you trying to do or find out more about?
Where have you looked?
Where did you expect to find this information?
```
## Found an Issue?
If you find a bug in the source code or a mistake in the documentation, you can help us by
submitting an issue to our [GitHub Repository][github]. Even better you can submit a Pull Request
with a fix.
See [below](#submit) for some guidelines.
## Submission Guidelines
### Submitting an Issue
Before you submit your issue search the archive, maybe your question was already answered.
If your issue appears to be a bug, and hasn't been reported, open a new issue.
Help us to maximize the effort we can spend fixing issues and adding new
features, by not reporting duplicate issues. Providing the following information will increase the
chances of your issue being dealt with quickly:
* **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
* **Motivation for or Use Case** - explain why this is a bug for you
* **Browsers and Operating System** - is this a problem with all browsers or only IE9?
* **Reproduce the Error** - provide a live example (using JSBin) or a unambiguous set of steps.
* **Related Issues** - has a similar issue been reported before?
* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
causing the problem (line of code or commit)
**If you get help, help others. Good karma rulez!**
Here's a template to get you started:
```
Browser:
Browser version:
Operating system:
Operating system version:
What steps will reproduce the problem:
1.
2.
3.
What is the expected result?
What happens instead of that?
Please provide any other information below, and attach a screenshot if possible.
```
## Signing the CLA
Please sign our [Contributor License Agreement][google-cla] (CLA) before sending pull requests. For any code
changes to be accepted, the CLA must be signed. It's a quick process, we promise!
*This guide was inspired by the [AngularJS contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).*
[google-cla]: https://cla.developers.google.com
[stackoverflow]: http://stackoverflow.com/questions/tagged/firebase
[global-gitignore]: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore
================================================
FILE: firestore/LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2015 Google Inc
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.
All code in any directories or sub-directories that end with *.html or
*.css is licensed under the Creative Commons Attribution International
4.0 License, which full text can be found here:
https://creativecommons.org/licenses/by/4.0/legalcode.
As an exception to this license, all html or css that is generated by
the software at the direction of the user is copyright the user. The
user has full ownership and control over such content, including
whether and how they wish to license it.
================================================
FILE: firestore/README.md
================================================
# Firestore Quickstart
## Introduction
Friendly Eats is a restaurant recommendation app built on Firestore.
For more information about Firestore visit [the docs][firestore-docs].
## Getting Started
* [Set up your Android app for Firestore][setup-android]
* Use the package name `com.google.firebase.example.fireeats`
* In the Authentication tab of the Firebase console go to the
[Sign-in Method][auth-providers] page and enable 'Email/Password'.
* This app uses [FirebaseUI][firebaseui] for authentication.
* Run the app on an Android device or emulator.
### Security Rules
Add the following security rules to your project in the:
[rules tab](https://console.firebase.google.com/project/_/database/firestore/rules):
```
service cloud.firestore {
match /databases/{database}/documents {
// Anyone can read a restaurant, only authorized
// users can create or update. Deletes are not allowed.
match /restaurants/{restaurantId} {
allow read: if true;
allow create, update: if request.auth.uid != null;
}
// Anyone can read a rating. Only the user who made the rating
// can delete it. Ratings can never be updated.
match /restaurants/{restaurantId}/ratings/{ratingId} {
allow read: if true;
allow create: if request.auth.uid != null;
allow delete: if request.resource.data.userId == request.auth.uid;
allow update: if false;
}
}
}
```
### Run the App
* When you open the app you will be prompted to sign in, choose
any email and password.
* When you first open the app it will be empty, choose
**Add Random Items** from the overflow menu to add some
new entries.
### Result
### Indexes
As you use the app's filter functionality you may see warnings
in logcat that look like this:
```
com.google.firebase.example.fireeats W/Firestore Adapter: onEvent:error
com.google.firebase.firestore.FirebaseFirestoreException: FAILED_PRECONDITION: The query requires an index. You can create it here: https://console.firebase.google.com/project/...
```
This is because indexes are required for most compound queries in
Firestore. Clicking on the link from the error message will
automatically open the index creation UI in the Firebase console
with the correct paramters filled in:
This app also provides an index specification file in `indexes.json`
which specifies all indexes required to run the application. You can
add all of these indexes programatically using the [Firebase CLI][firebase-cli].
[firestore-docs]: https://firebase.google.com/docs/firestore/
[setup-android]: https://firebase.google.com/docs/firestore/client/setup-android
[auth-providers]: https://console.firebase.google.com/project/_/authentication/providers
[firebaseui]: https://github.com/firebase/FirebaseUI-Android
[firebase-cli]: https://firebase.google.com/docs/firestore/query-data/indexing#use_the_firebase_cli
================================================
FILE: firestore/accounts.json
================================================
{
"users": [{
"localId": "1",
"email": "test@mailinator.com",
"passwordHash": "XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=",
}]
}
================================================
FILE: firestore/app/.gitignore
================================================
/build
================================================
FILE: firestore/app/build.gradle.kts
================================================
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.google.services)
alias(libs.plugins.navigation.safeargs)
}
android {
namespace = "com.google.firebase.example.fireeats"
testBuildType = "release"
compileSdk = 36
defaultConfig {
applicationId = "com.google.firebase.example.fireeats"
minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
multiDexEnabled = true
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
testProguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "test-proguard-rules.pro")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
viewBinding = true
}
lint {
disable += "InvalidPackage"
// TODO(thatfiredev): Remove this once
// https://github.com/bumptech/glide/issues/4940 is fixed
disable += "NotificationPermission"
}
}
dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
// Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom)
implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Firestore
implementation("com.google.firebase:firebase-firestore")
// Firebase Authentication
implementation("com.google.firebase:firebase-auth")
// Google Play services
implementation("com.google.android.gms:play-services-auth:20.7.0")
// FirebaseUI (for authentication)
implementation("com.firebaseui:firebase-ui-auth:9.1.1")
// Support Libs
implementation("androidx.activity:activity-ktx:1.12.1")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.vectordrawable:vectordrawable-animated:1.2.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.browser:browser:1.5.0")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.media:media:1.7.1")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.9.6")
implementation("androidx.navigation:navigation-ui-ktx:2.9.6")
// Android architecture components
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
annotationProcessor("androidx.lifecycle:lifecycle-compiler:2.10.0")
// Third-party libraries
implementation("me.zhanghai.android.materialratingbar:library:1.4.0")
implementation("com.github.bumptech.glide:glide:4.12.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.7.0")
androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
androidTestImplementation("junit:junit:4.13.2")
androidTestImplementation("org.hamcrest:hamcrest-library:3.0")
androidTestImplementation("com.google.firebase:firebase-auth")
}
================================================
FILE: firestore/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/google/home/samstern/android-sdk-linux/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# Keep custom model classes
-keep class com.google.firebase.example.fireeats.java.model.** { *; }
-keep class com.google.firebase.example.fireeats.kotlin.model.** { *; }
# https://github.com/firebase/FirebaseUI-Android/issues/1175
-dontwarn okio.**
-dontwarn retrofit2.Call
-dontnote retrofit2.Platform$IOS$MainThreadExecutor
-keep class android.support.v7.widget.RecyclerView { *; }
================================================
FILE: firestore/app/src/androidTest/AndroidManifest.xml
================================================
================================================
FILE: firestore/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/EntryChoiceActivity.kt
================================================
package com.google.firebase.example.fireeats
import android.content.Intent
import com.firebase.example.internal.BaseEntryChoiceActivity
import com.firebase.example.internal.Choice
class EntryChoiceActivity : BaseEntryChoiceActivity() {
override fun getChoices(): List {
return listOf(
Choice(
"Java",
"Run the Firestore quickstart written in Java.",
Intent(this, com.google.firebase.example.fireeats.java.MainActivity::class.java),
),
Choice(
"Kotlin",
"Run the Firestore quickstart written in Kotlin.",
Intent(this, com.google.firebase.example.fireeats.kotlin.MainActivity::class.java),
),
)
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/FilterDialogFragment.java
================================================
package com.google.firebase.example.fireeats.java;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Spinner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.google.firebase.example.fireeats.R;
import com.google.firebase.example.fireeats.databinding.DialogFiltersBinding;
import com.google.firebase.example.fireeats.java.model.Restaurant;
import com.google.firebase.firestore.Query;
/**
* Dialog Fragment containing filter form.
*/
public class FilterDialogFragment extends DialogFragment implements View.OnClickListener {
public static final String TAG = "FilterDialog";
interface FilterListener {
void onFilter(Filters filters);
}
private DialogFiltersBinding mBinding;
private FilterListener mFilterListener;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
mBinding = DialogFiltersBinding.inflate(inflater, container, false);
mBinding.buttonSearch.setOnClickListener(this);
mBinding.buttonCancel.setOnClickListener(this);
return mBinding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (getParentFragment() instanceof FilterListener) {
mFilterListener = (FilterListener) getParentFragment();
}
}
@Override
public void onResume() {
super.onResume();
getDialog().getWindow().setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
public void onSearchClicked() {
if (mFilterListener != null) {
mFilterListener.onFilter(getFilters());
}
dismiss();
}
public void onCancelClicked() {
dismiss();
}
@Nullable
private String getSelectedCategory() {
String selected = (String) mBinding.spinnerCategory.getSelectedItem();
if (getString(R.string.value_any_category).equals(selected)) {
return null;
} else {
return selected;
}
}
@Nullable
private String getSelectedCity() {
String selected = (String) mBinding.spinnerCity.getSelectedItem();
if (getString(R.string.value_any_city).equals(selected)) {
return null;
} else {
return selected;
}
}
private int getSelectedPrice() {
String selected = (String) mBinding.spinnerPrice.getSelectedItem();
if (selected.equals(getString(R.string.price_1))) {
return 1;
} else if (selected.equals(getString(R.string.price_2))) {
return 2;
} else if (selected.equals(getString(R.string.price_3))) {
return 3;
} else {
return -1;
}
}
@Nullable
private String getSelectedSortBy() {
String selected = (String) mBinding.spinnerSort.getSelectedItem();
if (getString(R.string.sort_by_rating).equals(selected)) {
return Restaurant.FIELD_AVG_RATING;
} if (getString(R.string.sort_by_price).equals(selected)) {
return Restaurant.FIELD_PRICE;
} if (getString(R.string.sort_by_popularity).equals(selected)) {
return Restaurant.FIELD_POPULARITY;
}
return null;
}
@Nullable
private Query.Direction getSortDirection() {
String selected = (String) mBinding.spinnerSort.getSelectedItem();
if (getString(R.string.sort_by_rating).equals(selected)) {
return Query.Direction.DESCENDING;
} if (getString(R.string.sort_by_price).equals(selected)) {
return Query.Direction.ASCENDING;
} if (getString(R.string.sort_by_popularity).equals(selected)) {
return Query.Direction.DESCENDING;
}
return null;
}
public void resetFilters() {
if (mBinding != null) {
mBinding.spinnerCategory.setSelection(0);
mBinding.spinnerCity.setSelection(0);
mBinding.spinnerPrice.setSelection(0);
mBinding.spinnerSort.setSelection(0);
}
}
public Filters getFilters() {
Filters filters = new Filters();
if (mBinding != null) {
filters.setCategory(getSelectedCategory());
filters.setCity(getSelectedCity());
filters.setPrice(getSelectedPrice());
filters.setSortBy(getSelectedSortBy());
filters.setSortDirection(getSortDirection());
}
return filters;
}
@Override
public void onClick(View v) {
//Due to bump in Java version, we can not use view ids in switch
//(see: http://tools.android.com/tips/non-constant-fields), so we
//need to use if/else:
int viewId = v.getId();
if (viewId == R.id.buttonSearch) {
onSearchClicked();
} else if (viewId == R.id.buttonCancel) {
onCancelClicked();
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/Filters.java
================================================
package com.google.firebase.example.fireeats.java;
import android.content.Context;
import android.text.TextUtils;
import com.google.firebase.example.fireeats.R;
import com.google.firebase.example.fireeats.java.model.Restaurant;
import com.google.firebase.example.fireeats.java.util.RestaurantUtil;
import com.google.firebase.firestore.Query;
/**
* Object for passing filters around.
*/
public class Filters {
private String category = null;
private String city = null;
private int price = -1;
private String sortBy = null;
private Query.Direction sortDirection = null;
public Filters() {}
public static Filters getDefault() {
Filters filters = new Filters();
filters.setSortBy(Restaurant.FIELD_AVG_RATING);
filters.setSortDirection(Query.Direction.DESCENDING);
return filters;
}
public boolean hasCategory() {
return !(TextUtils.isEmpty(category));
}
public boolean hasCity() {
return !(TextUtils.isEmpty(city));
}
public boolean hasPrice() {
return (price > 0);
}
public boolean hasSortBy() {
return !(TextUtils.isEmpty(sortBy));
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public String getSortBy() {
return sortBy;
}
public void setSortBy(String sortBy) {
this.sortBy = sortBy;
}
public Query.Direction getSortDirection() {
return sortDirection;
}
public void setSortDirection(Query.Direction sortDirection) {
this.sortDirection = sortDirection;
}
public String getSearchDescription(Context context) {
StringBuilder desc = new StringBuilder();
if (category == null && city == null) {
desc.append("");
desc.append(context.getString(R.string.all_restaurants));
desc.append(" ");
}
if (category != null) {
desc.append("");
desc.append(category);
desc.append(" ");
}
if (category != null && city != null) {
desc.append(" in ");
}
if (city != null) {
desc.append("");
desc.append(city);
desc.append(" ");
}
if (price > 0) {
desc.append(" for ");
desc.append("");
desc.append(RestaurantUtil.getPriceString(price));
desc.append(" ");
}
return desc.toString();
}
public String getOrderDescription(Context context) {
if (Restaurant.FIELD_PRICE.equals(sortBy)) {
return context.getString(R.string.sorted_by_price);
} else if (Restaurant.FIELD_POPULARITY.equals(sortBy)) {
return context.getString(R.string.sorted_by_popularity);
} else {
return context.getString(R.string.sorted_by_rating);
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainActivity.java
================================================
package com.google.firebase.example.fireeats.java;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.navigation.Navigation;
import com.google.firebase.example.fireeats.R;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setSupportActionBar(this.findViewById(R.id.toolbar));
Navigation.findNavController(this, R.id.nav_host_fragment)
.setGraph(R.navigation.nav_graph_java);
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/MainFragment.java
================================================
package com.google.firebase.example.fireeats.java;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.text.HtmlCompat;
import androidx.core.view.MenuHost;
import androidx.core.view.MenuProvider;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.firebase.ui.auth.AuthUI;
import com.firebase.ui.auth.ErrorCodes;
import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract;
import com.firebase.ui.auth.IdpResponse;
import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.android.material.snackbar.Snackbar;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.example.fireeats.R;
import com.google.firebase.example.fireeats.databinding.FragmentMainBinding;
import com.google.firebase.example.fireeats.java.adapter.RestaurantAdapter;
import com.google.firebase.example.fireeats.java.model.Rating;
import com.google.firebase.example.fireeats.java.model.Restaurant;
import com.google.firebase.example.fireeats.java.util.RatingUtil;
import com.google.firebase.example.fireeats.java.util.RestaurantUtil;
import com.google.firebase.example.fireeats.java.viewmodel.MainActivityViewModel;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.WriteBatch;
import java.util.Collections;
import java.util.List;
public class MainFragment extends Fragment implements
FilterDialogFragment.FilterListener,
RestaurantAdapter.OnRestaurantSelectedListener, View.OnClickListener,
MenuProvider {
private static final String TAG = "MainActivity";
private static final int LIMIT = 50;
private FragmentMainBinding mBinding;
private FirebaseFirestore mFirestore;
private Query mQuery;
private FilterDialogFragment mFilterDialog;
private RestaurantAdapter mAdapter;
private MainActivityViewModel mViewModel;
private MenuHost menuHost;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentMainBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mBinding.filterBar.setOnClickListener(this);
mBinding.buttonClearFilter.setOnClickListener(this);
// MenuProvider
menuHost = requireActivity();
menuHost.addMenuProvider(this);
// View model
mViewModel = new ViewModelProvider(this).get(MainActivityViewModel.class);
// Enable Firestore logging
FirebaseFirestore.setLoggingEnabled(true);
// Firestore
mFirestore = FirebaseFirestore.getInstance();
// Get ${LIMIT} restaurants
mQuery = mFirestore.collection("restaurants")
.orderBy("avgRating", Query.Direction.DESCENDING)
.limit(LIMIT);
// RecyclerView
mAdapter = new RestaurantAdapter(mQuery, this) {
@Override
protected void onDataChanged() {
// Show/hide content if the query returns empty.
if (getItemCount() == 0) {
mBinding.recyclerRestaurants.setVisibility(View.GONE);
mBinding.viewEmpty.setVisibility(View.VISIBLE);
} else {
mBinding.recyclerRestaurants.setVisibility(View.VISIBLE);
mBinding.viewEmpty.setVisibility(View.GONE);
}
}
@Override
protected void onError(FirebaseFirestoreException e) {
// Show a snackbar on errors
Snackbar.make(mBinding.getRoot(),
"Error: check logs for info.", Snackbar.LENGTH_LONG).show();
}
};
mBinding.recyclerRestaurants.setLayoutManager(new LinearLayoutManager(requireContext()));
mBinding.recyclerRestaurants.setAdapter(mAdapter);
// Filter Dialog
mFilterDialog = new FilterDialogFragment();
}
@Override
public void onStart() {
super.onStart();
// Start sign in if necessary
if (shouldStartSignIn()) {
startSignIn();
return;
}
// Apply filters
onFilter(mViewModel.getFilters());
// Start listening for Firestore updates
if (mAdapter != null) {
mAdapter.startListening();
}
}
@Override
public void onStop() {
super.onStop();
if (mAdapter != null) {
mAdapter.stopListening();
}
}
@Override
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
menuInflater.inflate(R.menu.menu_main, menu);
}
@Override
public boolean onMenuItemSelected(@NonNull MenuItem item) {
//Due to bump in Java version, we can not use view ids in switch
//(see: http://tools.android.com/tips/non-constant-fields), so we
//need to use if/else:
int itemId = item.getItemId();
if (itemId == R.id.menu_add_items) {
onAddItemsClicked();
return true;
} else if (itemId == R.id.menu_sign_out) {
AuthUI.getInstance().signOut(requireContext());
startSignIn();
return true;
}
return false;
}
private void onSignInResult(FirebaseAuthUIAuthenticationResult result) {
IdpResponse response = result.getIdpResponse();
mViewModel.setIsSigningIn(false);
if (result.getResultCode() != Activity.RESULT_OK) {
if (response == null) {
// User pressed the back button.
requireActivity().finish();
} else if (response.getError() != null
&& response.getError().getErrorCode() == ErrorCodes.NO_NETWORK) {
showSignInErrorDialog(R.string.message_no_network);
} else {
showSignInErrorDialog(R.string.message_unknown);
}
}
}
public void onFilterClicked() {
// Show the dialog containing filter options
mFilterDialog.show(getChildFragmentManager(), FilterDialogFragment.TAG);
}
public void onClearFilterClicked() {
mFilterDialog.resetFilters();
onFilter(Filters.getDefault());
}
@Override
public void onRestaurantSelected(DocumentSnapshot restaurant) {
// Go to the details page for the selected restaurant
MainFragmentDirections.ActionMainFragmentToRestaurantDetailFragment action = MainFragmentDirections
.actionMainFragmentToRestaurantDetailFragment(restaurant.getId());
NavHostFragment.findNavController(this)
.navigate(action);
}
@Override
public void onFilter(Filters filters) {
// Construct query basic query
Query query = mFirestore.collection("restaurants");
// Category (equality filter)
if (filters.hasCategory()) {
query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.getCategory());
}
// City (equality filter)
if (filters.hasCity()) {
query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.getCity());
}
// Price (equality filter)
if (filters.hasPrice()) {
query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.getPrice());
}
// Sort by (orderBy with direction)
if (filters.hasSortBy()) {
query = query.orderBy(filters.getSortBy(), filters.getSortDirection());
}
// Limit items
query = query.limit(LIMIT);
// Update the query
mAdapter.setQuery(query);
// Set header
mBinding.textCurrentSearch.setText(HtmlCompat.fromHtml(filters.getSearchDescription(requireContext()),
HtmlCompat.FROM_HTML_MODE_LEGACY));
mBinding.textCurrentSortBy.setText(filters.getOrderDescription(requireContext()));
// Save filters
mViewModel.setFilters(filters);
}
private boolean shouldStartSignIn() {
return (!mViewModel.getIsSigningIn() && FirebaseAuth.getInstance().getCurrentUser() == null);
}
private void startSignIn() {
// Sign in with FirebaseUI
ActivityResultLauncher signinLauncher = requireActivity()
.registerForActivityResult(new FirebaseAuthUIActivityResultContract(),
this::onSignInResult
);
Intent intent = AuthUI.getInstance().createSignInIntentBuilder()
.setAvailableProviders(Collections.singletonList(
new AuthUI.IdpConfig.EmailBuilder().build()))
.setCredentialManagerEnabled(false)
.build();
signinLauncher.launch(intent);
mViewModel.setIsSigningIn(true);
}
private void onAddItemsClicked() {
// Add a bunch of random restaurants
WriteBatch batch = mFirestore.batch();
for (int i = 0; i < 10; i++) {
DocumentReference restRef = mFirestore.collection("restaurants").document();
// Create random restaurant / ratings
Restaurant randomRestaurant = RestaurantUtil.getRandom(requireContext());
List randomRatings = RatingUtil.getRandomList(randomRestaurant.getNumRatings());
randomRestaurant.setAvgRating(RatingUtil.getAverageRating(randomRatings));
// Add restaurant
batch.set(restRef, randomRestaurant);
// Add ratings to subcollection
for (Rating rating : randomRatings) {
batch.set(restRef.collection("ratings").document(), rating);
}
}
batch.commit().addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (task.isSuccessful()) {
Log.d(TAG, "Write batch succeeded.");
} else {
Log.w(TAG, "write batch failed.", task.getException());
}
}
});
}
private void showSignInErrorDialog(@StringRes int message) {
AlertDialog dialog = new AlertDialog.Builder(requireContext())
.setTitle(R.string.title_sign_in_error)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.option_retry, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
startSignIn();
}
})
.setNegativeButton(R.string.option_exit, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
requireActivity().finish();
}
}).create();
dialog.show();
}
@Override
public void onClick(View v) {
//Due to bump in Java version, we can not use view ids in switch
//(see: http://tools.android.com/tips/non-constant-fields), so we
//need to use if/else:
int viewId = v.getId();
if (viewId == R.id.filterBar) {
onFilterClicked();
} else if (viewId == R.id.buttonClearFilter) {
onClearFilterClicked();
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RatingDialogFragment.java
================================================
package com.google.firebase.example.fireeats.java;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.example.fireeats.R;
import com.google.firebase.example.fireeats.databinding.DialogRatingBinding;
import com.google.firebase.example.fireeats.java.model.Rating;
import me.zhanghai.android.materialratingbar.MaterialRatingBar;
/**
* Dialog Fragment containing rating form.
*/
public class RatingDialogFragment extends DialogFragment implements View.OnClickListener {
public static final String TAG = "RatingDialog";
private DialogRatingBinding mBinding;
interface RatingListener {
void onRating(Rating rating);
}
private RatingListener mRatingListener;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
mBinding = DialogRatingBinding.inflate(inflater, container, false);
mBinding.restaurantFormButton.setOnClickListener(this);
mBinding.restaurantFormCancel.setOnClickListener(this);
return mBinding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
mBinding = null;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (getParentFragment() instanceof RatingListener) {
mRatingListener = (RatingListener) getParentFragment();
}
}
@Override
public void onResume() {
super.onResume();
getDialog().getWindow().setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
private void onSubmitClicked(View view) {
Rating rating = new Rating(
FirebaseAuth.getInstance().getCurrentUser(),
mBinding.restaurantFormRating.getRating(),
mBinding.restaurantFormText.getText().toString());
if (mRatingListener != null) {
mRatingListener.onRating(rating);
}
dismiss();
}
private void onCancelClicked(View view) {
dismiss();
}
@Override
public void onClick(View v) {
//Due to bump in Java version, we can not use view ids in switch
//(see: http://tools.android.com/tips/non-constant-fields), so we
//need to use if/else:
int viewId = v.getId();
if (viewId == R.id.restaurantFormButton) {
onSubmitClicked(v);
} else if (viewId == R.id.restaurantFormCancel) {
onCancelClicked(v);
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/RestaurantDetailFragment.java
================================================
package com.google.firebase.example.fireeats.java;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.Glide;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.android.material.snackbar.Snackbar;
import com.google.firebase.example.fireeats.R;
import com.google.firebase.example.fireeats.databinding.FragmentRestaurantDetailBinding;
import com.google.firebase.example.fireeats.java.adapter.RatingAdapter;
import com.google.firebase.example.fireeats.java.model.Rating;
import com.google.firebase.example.fireeats.java.model.Restaurant;
import com.google.firebase.example.fireeats.java.util.RestaurantUtil;
import com.google.firebase.firestore.DocumentReference;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.EventListener;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.ListenerRegistration;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.Transaction;
public class RestaurantDetailFragment extends Fragment
implements EventListener, RatingDialogFragment.RatingListener, View.OnClickListener {
private static final String TAG = "RestaurantDetail";
private FragmentRestaurantDetailBinding mBinding;
private RatingDialogFragment mRatingDialog;
private FirebaseFirestore mFirestore;
private DocumentReference mRestaurantRef;
private ListenerRegistration mRestaurantRegistration;
private RatingAdapter mRatingAdapter;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding = FragmentRestaurantDetailBinding.inflate(inflater, container, false);
return mBinding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mBinding.restaurantButtonBack.setOnClickListener(this);
mBinding.fabShowRatingDialog.setOnClickListener(this);
String restaurantId = RestaurantDetailFragmentArgs.fromBundle(getArguments()).getKeyRestaurantId();
// Initialize Firestore
mFirestore = FirebaseFirestore.getInstance();
// Get reference to the restaurant
mRestaurantRef = mFirestore.collection("restaurants").document(restaurantId);
// Get ratings
Query ratingsQuery = mRestaurantRef
.collection("ratings")
.orderBy("timestamp", Query.Direction.DESCENDING)
.limit(50);
// RecyclerView
mRatingAdapter = new RatingAdapter(ratingsQuery) {
@Override
protected void onDataChanged() {
if (getItemCount() == 0) {
mBinding.recyclerRatings.setVisibility(View.GONE);
mBinding.viewEmptyRatings.setVisibility(View.VISIBLE);
} else {
mBinding.recyclerRatings.setVisibility(View.VISIBLE);
mBinding.viewEmptyRatings.setVisibility(View.GONE);
}
}
};
mBinding.recyclerRatings.setLayoutManager(new LinearLayoutManager(requireContext()));
mBinding.recyclerRatings.setAdapter(mRatingAdapter);
mRatingDialog = new RatingDialogFragment();
}
@Override
public void onStart() {
super.onStart();
mRatingAdapter.startListening();
mRestaurantRegistration = mRestaurantRef.addSnapshotListener(this);
}
@Override
public void onStop() {
super.onStop();
mRatingAdapter.stopListening();
if (mRestaurantRegistration != null) {
mRestaurantRegistration.remove();
mRestaurantRegistration = null;
}
}
/**
* Listener for the Restaurant document ({@link #mRestaurantRef}).
*/
@Override
public void onEvent(DocumentSnapshot snapshot, FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "restaurant:onEvent", e);
return;
}
onRestaurantLoaded(snapshot.toObject(Restaurant.class));
}
private void onRestaurantLoaded(Restaurant restaurant) {
mBinding.restaurantName.setText(restaurant.getName());
mBinding.restaurantRating.setRating((float) restaurant.getAvgRating());
mBinding.restaurantNumRatings.setText(getString(R.string.fmt_num_ratings, restaurant.getNumRatings()));
mBinding.restaurantCity.setText(restaurant.getCity());
mBinding.restaurantCategory.setText(restaurant.getCategory());
mBinding.restaurantPrice.setText(RestaurantUtil.getPriceString(restaurant));
// Background image
Glide.with(mBinding.restaurantImage.getContext())
.load(restaurant.getPhoto())
.into(mBinding.restaurantImage);
}
public void onBackArrowClicked(View view) {
NavHostFragment.findNavController(this).popBackStack();
}
public void onAddRatingClicked(View view) {
mRatingDialog.show(getChildFragmentManager(), RatingDialogFragment.TAG);
}
@Override
public void onRating(Rating rating) {
// In a transaction, add the new rating and update the aggregate totals
addRating(mRestaurantRef, rating)
.addOnSuccessListener(requireActivity(), new OnSuccessListener() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "Rating added");
// Hide keyboard and scroll to top
hideKeyboard();
mBinding.recyclerRatings.smoothScrollToPosition(0);
}
})
.addOnFailureListener(requireActivity(), new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.w(TAG, "Add rating failed", e);
// Show failure message and hide keyboard
hideKeyboard();
Snackbar.make(mBinding.getRoot(), "Failed to add rating",
Snackbar.LENGTH_SHORT).show();
}
});
}
private Task addRating(final DocumentReference restaurantRef, final Rating rating) {
// Create reference for new rating, for use inside the transaction
final DocumentReference ratingRef = restaurantRef.collection("ratings").document();
// In a transaction, add the new rating and update the aggregate totals
return mFirestore.runTransaction(new Transaction.Function() {
@Override
public Void apply(Transaction transaction) throws FirebaseFirestoreException {
Restaurant restaurant = transaction.get(restaurantRef).toObject(Restaurant.class);
// Compute new number of ratings
int newNumRatings = restaurant.getNumRatings() + 1;
// Compute new average rating
double oldRatingTotal = restaurant.getAvgRating() * restaurant.getNumRatings();
double newAvgRating = (oldRatingTotal + rating.getRating()) / newNumRatings;
// Set new restaurant info
restaurant.setNumRatings(newNumRatings);
restaurant.setAvgRating(newAvgRating);
// Commit to Firestore
transaction.set(restaurantRef, restaurant);
transaction.set(ratingRef, rating);
return null;
}
});
}
private void hideKeyboard() {
View view = requireActivity().getCurrentFocus();
if (view != null) {
((InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
}
@Override
public void onClick(View v) {
//Due to bump in Java version, we can not use view ids in switch
//(see: http://tools.android.com/tips/non-constant-fields), so we
//need to use if/else:
int viewId = v.getId();
if (viewId == R.id.restaurantButtonBack) {
onBackArrowClicked(v);
} else if (viewId == R.id.fabShowRatingDialog) {
onAddRatingClicked(v);
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/FirestoreAdapter.java
================================================
package com.google.firebase.example.fireeats.java.adapter;
import androidx.recyclerview.widget.RecyclerView;
import android.util.Log;
import com.google.firebase.firestore.DocumentChange;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.EventListener;
import com.google.firebase.firestore.FirebaseFirestoreException;
import com.google.firebase.firestore.ListenerRegistration;
import com.google.firebase.firestore.Query;
import com.google.firebase.firestore.QuerySnapshot;
import java.util.ArrayList;
/**
* RecyclerView adapter for displaying the results of a Firestore {@link Query}.
*
* Note that this class forgoes some efficiency to gain simplicity. For example, the result of
* {@link DocumentSnapshot#toObject(Class)} is not cached so the same object may be deserialized
* many times as the user scrolls.
*/
public abstract class FirestoreAdapter
extends RecyclerView.Adapter
implements EventListener {
private static final String TAG = "FirestoreAdapter";
private Query mQuery;
private ListenerRegistration mRegistration;
private ArrayList mSnapshots = new ArrayList<>();
public FirestoreAdapter(Query query) {
mQuery = query;
}
@Override
public void onEvent(QuerySnapshot documentSnapshots, FirebaseFirestoreException e) {
if (e != null) {
Log.w(TAG, "onEvent:error", e);
onError(e);
return;
}
// Dispatch the event
Log.d(TAG, "onEvent:numChanges:" + documentSnapshots.getDocumentChanges().size());
for (DocumentChange change : documentSnapshots.getDocumentChanges()) {
switch (change.getType()) {
case ADDED:
onDocumentAdded(change);
break;
case MODIFIED:
onDocumentModified(change);
break;
case REMOVED:
onDocumentRemoved(change);
break;
}
}
onDataChanged();
}
public void startListening() {
if (mQuery != null && mRegistration == null) {
mRegistration = mQuery.addSnapshotListener(this);
}
}
public void stopListening() {
if (mRegistration != null) {
mRegistration.remove();
mRegistration = null;
}
mSnapshots.clear();
notifyDataSetChanged();
}
public void setQuery(Query query) {
// Stop listening
stopListening();
// Clear existing data
mSnapshots.clear();
notifyDataSetChanged();
// Listen to new query
mQuery = query;
startListening();
}
@Override
public int getItemCount() {
return mSnapshots.size();
}
protected DocumentSnapshot getSnapshot(int index) {
return mSnapshots.get(index);
}
protected void onDocumentAdded(DocumentChange change) {
mSnapshots.add(change.getNewIndex(), change.getDocument());
notifyItemInserted(change.getNewIndex());
}
protected void onDocumentModified(DocumentChange change) {
if (change.getOldIndex() == change.getNewIndex()) {
// Item changed but remained in same position
mSnapshots.set(change.getOldIndex(), change.getDocument());
notifyItemChanged(change.getOldIndex());
} else {
// Item changed and changed position
mSnapshots.remove(change.getOldIndex());
mSnapshots.add(change.getNewIndex(), change.getDocument());
notifyItemMoved(change.getOldIndex(), change.getNewIndex());
}
}
protected void onDocumentRemoved(DocumentChange change) {
mSnapshots.remove(change.getOldIndex());
notifyItemRemoved(change.getOldIndex());
}
protected void onError(FirebaseFirestoreException e) {
Log.w(TAG, "onError", e);
};
protected void onDataChanged() {}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/RatingAdapter.java
================================================
package com.google.firebase.example.fireeats.java.adapter;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.google.firebase.example.fireeats.databinding.ItemRatingBinding;
import com.google.firebase.example.fireeats.java.model.Rating;
import com.google.firebase.firestore.Query;
import java.text.SimpleDateFormat;
import java.util.Locale;
/**
* RecyclerView adapter for a list of {@link Rating}.
*/
public class RatingAdapter extends FirestoreAdapter {
public RatingAdapter(Query query) {
super(query);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(ItemRatingBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(getSnapshot(position).toObject(Rating.class));
}
static class ViewHolder extends RecyclerView.ViewHolder {
private static final SimpleDateFormat FORMAT = new SimpleDateFormat(
"MM/dd/yyyy", Locale.US);
private ItemRatingBinding binding;
public ViewHolder(ItemRatingBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(Rating rating) {
binding.ratingItemName.setText(rating.getUserName());
binding.ratingItemRating.setRating((float) rating.getRating());
binding.ratingItemText.setText(rating.getText());
if (rating.getTimestamp() != null) {
binding.ratingItemDate.setText(FORMAT.format(rating.getTimestamp()));
}
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/adapter/RestaurantAdapter.java
================================================
package com.google.firebase.example.fireeats.java.adapter;
import android.content.res.Resources;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.google.firebase.example.fireeats.R;
import com.google.firebase.example.fireeats.databinding.ItemRestaurantBinding;
import com.google.firebase.example.fireeats.java.model.Restaurant;
import com.google.firebase.example.fireeats.java.util.RestaurantUtil;
import com.google.firebase.firestore.DocumentSnapshot;
import com.google.firebase.firestore.Query;
/**
* RecyclerView adapter for a list of Restaurants.
*/
public class RestaurantAdapter extends FirestoreAdapter {
public interface OnRestaurantSelectedListener {
void onRestaurantSelected(DocumentSnapshot restaurant);
}
private OnRestaurantSelectedListener mListener;
public RestaurantAdapter(Query query, OnRestaurantSelectedListener listener) {
super(query);
mListener = listener;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ViewHolder(ItemRestaurantBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.bind(getSnapshot(position), mListener);
}
static class ViewHolder extends RecyclerView.ViewHolder {
private ItemRestaurantBinding binding;
public ViewHolder(ItemRestaurantBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public ViewHolder(View itemView) {
super(itemView);
}
public void bind(final DocumentSnapshot snapshot,
final OnRestaurantSelectedListener listener) {
Restaurant restaurant = snapshot.toObject(Restaurant.class);
Resources resources = itemView.getResources();
// Load image
Glide.with(binding.restaurantItemImage.getContext())
.load(restaurant.getPhoto())
.into(binding.restaurantItemImage);
binding.restaurantItemName.setText(restaurant.getName());
binding.restaurantItemRating.setRating((float) restaurant.getAvgRating());
binding.restaurantItemCity.setText(restaurant.getCity());
binding.restaurantItemCategory.setText(restaurant.getCategory());
binding.restaurantItemNumRatings.setText(resources.getString(R.string.fmt_num_ratings,
restaurant.getNumRatings()));
binding.restaurantItemPrice.setText(RestaurantUtil.getPriceString(restaurant));
// Click listener
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null) {
listener.onRestaurantSelected(snapshot);
}
}
});
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/model/Rating.java
================================================
package com.google.firebase.example.fireeats.java.model;
import android.text.TextUtils;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.firestore.ServerTimestamp;
import java.util.Date;
/**
* Model POJO for a rating.
*/
public class Rating {
private String userId;
private String userName;
private double rating;
private String text;
private @ServerTimestamp Date timestamp;
public Rating() {}
public Rating(FirebaseUser user, double rating, String text) {
this.userId = user.getUid();
this.userName = user.getDisplayName();
if (TextUtils.isEmpty(this.userName)) {
this.userName = user.getEmail();
}
this.rating = rating;
this.text = text;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public double getRating() {
return rating;
}
public void setRating(double rating) {
this.rating = rating;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/model/Restaurant.java
================================================
package com.google.firebase.example.fireeats.java.model;
import com.google.firebase.firestore.IgnoreExtraProperties;
/**
* Restaurant POJO.
*/
@IgnoreExtraProperties
public class Restaurant {
public static final String FIELD_CITY = "city";
public static final String FIELD_CATEGORY = "category";
public static final String FIELD_PRICE = "price";
public static final String FIELD_POPULARITY = "numRatings";
public static final String FIELD_AVG_RATING = "avgRating";
private String name;
private String city;
private String category;
private String photo;
private int price;
private int numRatings;
private double avgRating;
public Restaurant() {}
public Restaurant(String name, String city, String category, String photo,
int price, int numRatings, double avgRating) {
this.name = name;
this.city = city;
this.category = category;
this.photo = photo;
this.price = price;
this.numRatings = numRatings;
this.avgRating = avgRating;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public String getPhoto() {
return photo;
}
public void setPhoto(String photo) {
this.photo = photo;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getNumRatings() {
return numRatings;
}
public void setNumRatings(int numRatings) {
this.numRatings = numRatings;
}
public double getAvgRating() {
return avgRating;
}
public void setAvgRating(double avgRating) {
this.avgRating = avgRating;
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/util/RatingUtil.java
================================================
package com.google.firebase.example.fireeats.java.util;
import com.google.firebase.example.fireeats.java.model.Rating;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
/**
* Utilities for Ratings.
*/
public class RatingUtil {
public static final String[] REVIEW_CONTENTS = {
// 0 - 1 stars
"This was awful! Totally inedible.",
// 1 - 2 stars
"This was pretty bad, would not go back.",
// 2 - 3 stars
"I was fed, so that's something.",
// 3 - 4 stars
"This was a nice meal, I'd go back.",
// 4 - 5 stars
"This was fantastic! Best ever!"
};
/**
* Get a list of random Rating POJOs.
*/
public static List getRandomList(int length) {
List result = new ArrayList<>();
for (int i = 0; i < length; i++) {
result.add(getRandom());
}
return result;
}
/**
* Get the average rating of a List.
*/
public static double getAverageRating(List ratings) {
double sum = 0.0;
for (Rating rating : ratings) {
sum += rating.getRating();
}
return sum / ratings.size();
}
/**
* Create a random Rating POJO.
*/
public static Rating getRandom() {
Rating rating = new Rating();
Random random = new Random();
double score = random.nextDouble() * 5.0;
String text = REVIEW_CONTENTS[(int) Math.floor(score)];
rating.setUserId(UUID.randomUUID().toString());
rating.setUserName("Random User");
rating.setRating(score);
rating.setText(text);
return rating;
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/util/RestaurantUtil.java
================================================
package com.google.firebase.example.fireeats.java.util;
import android.content.Context;
import com.google.firebase.example.fireeats.R;
import com.google.firebase.example.fireeats.java.model.Restaurant;
import java.util.Arrays;
import java.util.Locale;
import java.util.Random;
/**
* Utilities for Restaurants.
*/
public class RestaurantUtil {
private static final String TAG = "RestaurantUtil";
private static final String RESTAURANT_URL_FMT = "https://storage.googleapis.com/firestorequickstarts.appspot.com/food_%d.png";
private static final int MAX_IMAGE_NUM = 22;
private static final String[] NAME_FIRST_WORDS = {
"Foo",
"Bar",
"Baz",
"Qux",
"Fire",
"Sam's",
"World Famous",
"Google",
"The Best",
};
private static final String[] NAME_SECOND_WORDS = {
"Restaurant",
"Cafe",
"Spot",
"Eatin' Place",
"Eatery",
"Drive Thru",
"Diner",
};
/**
* Create a random Restaurant POJO.
*/
public static Restaurant getRandom(Context context) {
Restaurant restaurant = new Restaurant();
Random random = new Random();
// Cities (first elemnt is 'Any')
String[] cities = context.getResources().getStringArray(R.array.cities);
cities = Arrays.copyOfRange(cities, 1, cities.length);
// Categories (first element is 'Any')
String[] categories = context.getResources().getStringArray(R.array.categories);
categories = Arrays.copyOfRange(categories, 1, categories.length);
int[] prices = new int[]{1, 2, 3};
restaurant.setName(getRandomName(random));
restaurant.setCity(getRandomString(cities, random));
restaurant.setCategory(getRandomString(categories, random));
restaurant.setPhoto(getRandomImageUrl(random));
restaurant.setPrice(getRandomInt(prices, random));
restaurant.setNumRatings(random.nextInt(20));
// Note: average rating intentionally not set
return restaurant;
}
/**
* Get a random image.
*/
private static String getRandomImageUrl(Random random) {
// Integer between 1 and MAX_IMAGE_NUM (inclusive)
int id = random.nextInt(MAX_IMAGE_NUM) + 1;
return String.format(Locale.getDefault(), RESTAURANT_URL_FMT, id);
}
/**
* Get price represented as dollar signs.
*/
public static String getPriceString(Restaurant restaurant) {
return getPriceString(restaurant.getPrice());
}
/**
* Get price represented as dollar signs.
*/
public static String getPriceString(int priceInt) {
switch (priceInt) {
case 1:
return "$";
case 2:
return "$$";
case 3:
default:
return "$$$";
}
}
private static String getRandomName(Random random) {
return getRandomString(NAME_FIRST_WORDS, random) + " "
+ getRandomString(NAME_SECOND_WORDS, random);
}
private static String getRandomString(String[] array, Random random) {
int ind = random.nextInt(array.length);
return array[ind];
}
private static int getRandomInt(int[] array, Random random) {
int ind = random.nextInt(array.length);
return array[ind];
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/java/viewmodel/MainActivityViewModel.java
================================================
package com.google.firebase.example.fireeats.java.viewmodel;
import androidx.lifecycle.ViewModel;
import com.google.firebase.example.fireeats.java.Filters;
/**
* ViewModel for {@link com.google.firebase.example.fireeats.MainActivity}.
*/
public class MainActivityViewModel extends ViewModel {
private boolean mIsSigningIn;
private Filters mFilters;
public MainActivityViewModel() {
mIsSigningIn = false;
mFilters = Filters.getDefault();
}
public boolean getIsSigningIn() {
return mIsSigningIn;
}
public void setIsSigningIn(boolean mIsSigningIn) {
this.mIsSigningIn = mIsSigningIn;
}
public Filters getFilters() {
return mFilters;
}
public void setFilters(Filters mFilters) {
this.mFilters = mFilters;
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/FilterDialogFragment.kt
================================================
package com.google.firebase.example.fireeats.kotlin
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.databinding.DialogFiltersBinding
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.firestore.Query
/**
* Dialog Fragment containing filter form.
*/
class FilterDialogFragment : DialogFragment() {
private var _binding: DialogFiltersBinding? = null
private val binding get() = _binding!!
private var filterListener: FilterListener? = null
private val selectedCategory: String?
get() {
val selected = binding.spinnerCategory.selectedItem as String
return if (getString(R.string.value_any_category) == selected) {
null
} else {
selected
}
}
private val selectedCity: String?
get() {
val selected = binding.spinnerCity.selectedItem as String
return if (getString(R.string.value_any_city) == selected) {
null
} else {
selected
}
}
private val selectedPrice: Int
get() {
val selected = binding.spinnerPrice.selectedItem as String
return when (selected) {
getString(R.string.price_1) -> 1
getString(R.string.price_2) -> 2
getString(R.string.price_3) -> 3
else -> -1
}
}
private val selectedSortBy: String?
get() {
val selected = binding.spinnerSort.selectedItem as String
if (getString(R.string.sort_by_rating) == selected) {
return Restaurant.FIELD_AVG_RATING
}
if (getString(R.string.sort_by_price) == selected) {
return Restaurant.FIELD_PRICE
}
return if (getString(R.string.sort_by_popularity) == selected) {
Restaurant.FIELD_POPULARITY
} else {
null
}
}
private val sortDirection: Query.Direction
get() {
val selected = binding.spinnerSort.selectedItem as String
if (getString(R.string.sort_by_rating) == selected) {
return Query.Direction.DESCENDING
}
if (getString(R.string.sort_by_price) == selected) {
return Query.Direction.ASCENDING
}
return if (getString(R.string.sort_by_popularity) == selected) {
Query.Direction.DESCENDING
} else {
Query.Direction.DESCENDING
}
}
val filters: Filters
get() {
val filters = Filters()
filters.category = selectedCategory
filters.city = selectedCity
filters.price = selectedPrice
filters.sortBy = selectedSortBy
filters.sortDirection = sortDirection
return filters
}
interface FilterListener {
fun onFilter(filters: Filters)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
_binding = DialogFiltersBinding.inflate(inflater, container, false)
binding.buttonSearch.setOnClickListener { onSearchClicked() }
binding.buttonCancel.setOnClickListener { onCancelClicked() }
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (parentFragment is FilterListener) {
filterListener = parentFragment as FilterListener
}
}
override fun onResume() {
super.onResume()
dialog?.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
}
private fun onSearchClicked() {
filterListener?.onFilter(filters)
dismiss()
}
private fun onCancelClicked() {
dismiss()
}
fun resetFilters() {
_binding?.let {
it.spinnerCategory.setSelection(0)
it.spinnerCity.setSelection(0)
it.spinnerPrice.setSelection(0)
it.spinnerSort.setSelection(0)
}
}
companion object {
const val TAG = "FilterDialog"
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/Filters.kt
================================================
package com.google.firebase.example.fireeats.kotlin
import android.content.Context
import android.text.TextUtils
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil
import com.google.firebase.firestore.Query
/**
* Object for passing filters around.
*/
class Filters {
var category: String? = null
var city: String? = null
var price = -1
var sortBy: String? = null
var sortDirection: Query.Direction = Query.Direction.DESCENDING
fun hasCategory(): Boolean {
return !TextUtils.isEmpty(category)
}
fun hasCity(): Boolean {
return !TextUtils.isEmpty(city)
}
fun hasPrice(): Boolean {
return price > 0
}
fun hasSortBy(): Boolean {
return !TextUtils.isEmpty(sortBy)
}
fun getSearchDescription(context: Context): String {
val desc = StringBuilder()
if (category == null && city == null) {
desc.append("")
desc.append(context.getString(R.string.all_restaurants))
desc.append(" ")
}
if (category != null) {
desc.append("")
desc.append(category)
desc.append(" ")
}
if (category != null && city != null) {
desc.append(" in ")
}
if (city != null) {
desc.append("")
desc.append(city)
desc.append(" ")
}
if (price > 0) {
desc.append(" for ")
desc.append("")
desc.append(RestaurantUtil.getPriceString(price))
desc.append(" ")
}
return desc.toString()
}
fun getOrderDescription(context: Context): String {
return when (sortBy) {
Restaurant.FIELD_PRICE -> context.getString(R.string.sorted_by_price)
Restaurant.FIELD_POPULARITY -> context.getString(R.string.sorted_by_popularity)
else -> context.getString(R.string.sorted_by_rating)
}
}
companion object {
val default: Filters
get() {
val filters = Filters()
filters.sortBy = Restaurant.FIELD_AVG_RATING
filters.sortDirection = Query.Direction.DESCENDING
return filters
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainActivity.kt
================================================
package com.google.firebase.example.fireeats.kotlin
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.Navigation
import com.google.firebase.example.fireeats.R
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
Navigation.findNavController(this, R.id.nav_host_fragment)
.setGraph(R.navigation.nav_graph_kotlin)
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/MainFragment.kt
================================================
package com.google.firebase.example.fireeats.kotlin
import android.app.Activity
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.get
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.firebase.ui.auth.AuthUI
import com.firebase.ui.auth.ErrorCodes
import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract
import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.auth.auth
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.databinding.FragmentMainBinding
import com.google.firebase.example.fireeats.kotlin.adapter.RestaurantAdapter
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.example.fireeats.kotlin.util.RatingUtil
import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil
import com.google.firebase.example.fireeats.kotlin.viewmodel.MainActivityViewModel
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.FirebaseFirestoreException
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.firestore
import com.google.firebase.Firebase
class MainFragment :
Fragment(),
FilterDialogFragment.FilterListener,
RestaurantAdapter.OnRestaurantSelectedListener,
MenuProvider {
lateinit var firestore: FirebaseFirestore
lateinit var query: Query
private lateinit var binding: FragmentMainBinding
private lateinit var filterDialog: FilterDialogFragment
lateinit var adapter: RestaurantAdapter
private lateinit var viewModel: MainActivityViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// View model
viewModel = ViewModelProvider(this).get()
// Enable Firestore logging
FirebaseFirestore.setLoggingEnabled(true)
// Firestore
firestore = Firebase.firestore
// Get ${LIMIT} restaurants
query = firestore.collection("restaurants")
.orderBy("avgRating", Query.Direction.DESCENDING)
.limit(LIMIT.toLong())
// RecyclerView
adapter = object : RestaurantAdapter(query, this@MainFragment) {
override fun onDataChanged() {
// Show/hide content if the query returns empty.
if (itemCount == 0) {
binding.recyclerRestaurants.visibility = View.GONE
binding.viewEmpty.visibility = View.VISIBLE
} else {
binding.recyclerRestaurants.visibility = View.VISIBLE
binding.viewEmpty.visibility = View.GONE
}
}
override fun onError(e: FirebaseFirestoreException) {
// Show a snackbar on errors
Snackbar.make(
binding.root,
"Error: check logs for info.",
Snackbar.LENGTH_LONG,
).show()
}
}
// MenuProvider
val menuHost: MenuHost = requireActivity() as MenuHost
menuHost.addMenuProvider(this)
binding.recyclerRestaurants.layoutManager = LinearLayoutManager(context)
binding.recyclerRestaurants.adapter = adapter
// Filter Dialog
filterDialog = FilterDialogFragment()
binding.filterBar.setOnClickListener { onFilterClicked() }
binding.buttonClearFilter.setOnClickListener { onClearFilterClicked() }
}
public override fun onStart() {
super.onStart()
// Start sign in if necessary
if (shouldStartSignIn()) {
startSignIn()
return
}
// Apply filters
onFilter(viewModel.filters)
// Start listening for Firestore updates
adapter.startListening()
}
public override fun onStop() {
super.onStop()
adapter.stopListening()
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_main, menu)
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_add_items -> {
onAddItemsClicked()
true
}
R.id.menu_sign_out -> {
AuthUI.getInstance().signOut(requireContext())
startSignIn()
true
}
else -> false
}
}
private fun onSignInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
viewModel.isSigningIn = false
if (result.resultCode != Activity.RESULT_OK) {
if (response == null) {
// User pressed the back button.
requireActivity().finish()
} else if (response.error != null && response.error!!.errorCode == ErrorCodes.NO_NETWORK) {
showSignInErrorDialog(R.string.message_no_network)
} else {
showSignInErrorDialog(R.string.message_unknown)
}
}
}
private fun onFilterClicked() {
// Show the dialog containing filter options
filterDialog.show(childFragmentManager, FilterDialogFragment.TAG)
}
private fun onClearFilterClicked() {
filterDialog.resetFilters()
onFilter(Filters.default)
}
override fun onRestaurantSelected(restaurant: DocumentSnapshot) {
// Go to the details page for the selected restaurant
val action = MainFragmentDirections
.actionMainFragmentToRestaurantDetailFragment(restaurant.id)
findNavController().navigate(action)
}
override fun onFilter(filters: Filters) {
// Construct query basic query
var query: Query = firestore.collection("restaurants")
// Category (equality filter)
if (filters.hasCategory()) {
query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category)
}
// City (equality filter)
if (filters.hasCity()) {
query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city)
}
// Price (equality filter)
if (filters.hasPrice()) {
query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price)
}
// Sort by (orderBy with direction)
if (filters.hasSortBy()) {
query = query.orderBy(filters.sortBy.toString(), filters.sortDirection)
}
// Limit items
query = query.limit(LIMIT.toLong())
// Update the query
adapter.setQuery(query)
// Set header
binding.textCurrentSearch.text = HtmlCompat.fromHtml(
filters.getSearchDescription(requireContext()),
HtmlCompat.FROM_HTML_MODE_LEGACY,
)
binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())
// Save filters
viewModel.filters = filters
}
private fun shouldStartSignIn(): Boolean {
return !viewModel.isSigningIn && Firebase.auth.currentUser == null
}
private fun startSignIn() {
// Sign in with FirebaseUI
val signInLauncher = requireActivity().registerForActivityResult(
FirebaseAuthUIActivityResultContract(),
) { result -> this.onSignInResult(result) }
val intent = AuthUI.getInstance().createSignInIntentBuilder()
.setAvailableProviders(listOf(AuthUI.IdpConfig.EmailBuilder().build()))
.setCredentialManagerEnabled(false)
.build()
signInLauncher.launch(intent)
viewModel.isSigningIn = true
}
private fun onAddItemsClicked() {
// Add a bunch of random restaurants
val batch = firestore.batch()
for (i in 0..9) {
val restRef = firestore.collection("restaurants").document()
// Create random restaurant / ratings
val randomRestaurant = RestaurantUtil.getRandom(requireContext())
val randomRatings = RatingUtil.getRandomList(randomRestaurant.numRatings)
randomRestaurant.avgRating = RatingUtil.getAverageRating(randomRatings)
// Add restaurant
batch.set(restRef, randomRestaurant)
// Add ratings to subcollection
for (rating in randomRatings) {
batch.set(restRef.collection("ratings").document(), rating)
}
}
batch.commit().addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d(TAG, "Write batch succeeded.")
} else {
Log.w(TAG, "write batch failed.", task.exception)
}
}
}
private fun showSignInErrorDialog(@StringRes message: Int) {
val dialog = AlertDialog.Builder(requireContext())
.setTitle(R.string.title_sign_in_error)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.option_retry) { _, _ -> startSignIn() }
.setNegativeButton(R.string.option_exit) { _, _ -> requireActivity().finish() }.create()
dialog.show()
}
companion object {
private const val TAG = "MainActivity"
private const val LIMIT = 50
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RatingDialogFragment.kt
================================================
package com.google.firebase.example.fireeats.kotlin
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.google.firebase.auth.auth
import com.google.firebase.example.fireeats.databinding.DialogRatingBinding
import com.google.firebase.example.fireeats.kotlin.model.Rating
import com.google.firebase.Firebase
/**
* Dialog Fragment containing rating form.
*/
class RatingDialogFragment : DialogFragment() {
private var _binding: DialogRatingBinding? = null
private val binding get() = _binding!!
private var ratingListener: RatingListener? = null
internal interface RatingListener {
fun onRating(rating: Rating)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
_binding = DialogRatingBinding.inflate(inflater, container, false)
binding.restaurantFormButton.setOnClickListener { onSubmitClicked() }
binding.restaurantFormCancel.setOnClickListener { onCancelClicked() }
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (parentFragment is RatingListener) {
ratingListener = parentFragment as RatingListener
}
}
override fun onResume() {
super.onResume()
dialog?.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
}
private fun onSubmitClicked() {
val user = Firebase.auth.currentUser
user?.let {
val rating = Rating(
it,
binding.restaurantFormRating.rating.toDouble(),
binding.restaurantFormText.text.toString(),
)
ratingListener?.onRating(rating)
}
dismiss()
}
private fun onCancelClicked() {
dismiss()
}
companion object {
const val TAG = "RatingDialog"
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/RestaurantDetailFragment.kt
================================================
package com.google.firebase.example.fireeats.kotlin
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.google.android.gms.tasks.Task
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.databinding.FragmentRestaurantDetailBinding
import com.google.firebase.example.fireeats.kotlin.adapter.RatingAdapter
import com.google.firebase.example.fireeats.kotlin.model.Rating
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil
import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.FirebaseFirestoreException
import com.google.firebase.firestore.ListenerRegistration
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.firestore
import com.google.firebase.firestore.toObject
import com.google.firebase.Firebase
class RestaurantDetailFragment :
Fragment(),
EventListener,
RatingDialogFragment.RatingListener {
private var ratingDialog: RatingDialogFragment? = null
private lateinit var binding: FragmentRestaurantDetailBinding
private lateinit var firestore: FirebaseFirestore
private lateinit var restaurantRef: DocumentReference
private lateinit var ratingAdapter: RatingAdapter
private var restaurantRegistration: ListenerRegistration? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentRestaurantDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Get restaurant ID from extras
val restaurantId = RestaurantDetailFragmentArgs.fromBundle(requireArguments()).keyRestaurantId
// Initialize Firestore
firestore = Firebase.firestore
// Get reference to the restaurant
restaurantRef = firestore.collection("restaurants").document(restaurantId)
// Get ratings
val ratingsQuery = restaurantRef
.collection("ratings")
.orderBy("timestamp", Query.Direction.DESCENDING)
.limit(50)
// RecyclerView
ratingAdapter = object : RatingAdapter(ratingsQuery) {
override fun onDataChanged() {
if (itemCount == 0) {
binding.recyclerRatings.visibility = View.GONE
binding.viewEmptyRatings.visibility = View.VISIBLE
} else {
binding.recyclerRatings.visibility = View.VISIBLE
binding.viewEmptyRatings.visibility = View.GONE
}
}
}
binding.recyclerRatings.layoutManager = LinearLayoutManager(context)
binding.recyclerRatings.adapter = ratingAdapter
ratingDialog = RatingDialogFragment()
binding.restaurantButtonBack.setOnClickListener { onBackArrowClicked() }
binding.fabShowRatingDialog.setOnClickListener { onAddRatingClicked() }
}
public override fun onStart() {
super.onStart()
ratingAdapter.startListening()
restaurantRegistration = restaurantRef.addSnapshotListener(this)
}
public override fun onStop() {
super.onStop()
ratingAdapter.stopListening()
restaurantRegistration?.remove()
restaurantRegistration = null
}
/**
* Listener for the Restaurant document ([.restaurantRef]).
*/
override fun onEvent(snapshot: DocumentSnapshot?, e: FirebaseFirestoreException?) {
if (e != null) {
Log.w(TAG, "restaurant:onEvent", e)
return
}
snapshot?.let {
val restaurant = snapshot.toObject()
if (restaurant != null) {
onRestaurantLoaded(restaurant)
}
}
}
private fun onRestaurantLoaded(restaurant: Restaurant) {
binding.restaurantName.text = restaurant.name
binding.restaurantRating.rating = restaurant.avgRating.toFloat()
binding.restaurantNumRatings.text = getString(R.string.fmt_num_ratings, restaurant.numRatings)
binding.restaurantCity.text = restaurant.city
binding.restaurantCategory.text = restaurant.category
binding.restaurantPrice.text = RestaurantUtil.getPriceString(restaurant)
// Background image
Glide.with(binding.restaurantImage.context)
.load(restaurant.photo)
.into(binding.restaurantImage)
}
private fun onBackArrowClicked() {
findNavController().popBackStack()
}
private fun onAddRatingClicked() {
ratingDialog?.show(childFragmentManager, RatingDialogFragment.TAG)
}
override fun onRating(rating: Rating) {
// In a transaction, add the new rating and update the aggregate totals
addRating(restaurantRef, rating)
.addOnSuccessListener(requireActivity()) {
Log.d(TAG, "Rating added")
// Hide keyboard and scroll to top
hideKeyboard()
binding.recyclerRatings.smoothScrollToPosition(0)
}
.addOnFailureListener(requireActivity()) { e ->
Log.w(TAG, "Add rating failed", e)
// Show failure message and hide keyboard
hideKeyboard()
Snackbar.make(
requireView().findViewById(android.R.id.content),
"Failed to add rating",
Snackbar.LENGTH_SHORT,
).show()
}
}
private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task {
// Create reference for new rating, for use inside the transaction
val ratingRef = restaurantRef.collection("ratings").document()
// In a transaction, add the new rating and update the aggregate totals
return firestore.runTransaction { transaction ->
val restaurant = transaction.get(restaurantRef).toObject()
?: throw Exception("Restaurant not found at ${restaurantRef.path}")
// Compute new number of ratings
val newNumRatings = restaurant.numRatings + 1
// Compute new average rating
val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings
// Set new restaurant info
restaurant.numRatings = newNumRatings
restaurant.avgRating = newAvgRating
// Commit to Firestore
transaction.set(restaurantRef, restaurant)
transaction.set(ratingRef, rating)
null
}
}
private fun hideKeyboard() {
val view = requireActivity().currentFocus
if (view != null) {
(requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.hideSoftInputFromWindow(view.windowToken, 0)
}
}
companion object {
private const val TAG = "RestaurantDetail"
const val KEY_RESTAURANT_ID = "key_restaurant_id"
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/FirestoreAdapter.kt
================================================
package com.google.firebase.example.fireeats.kotlin.adapter
import android.util.Log
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.firestore.DocumentChange
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.FirebaseFirestoreException
import com.google.firebase.firestore.ListenerRegistration
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.QuerySnapshot
import java.util.ArrayList
/**
* RecyclerView adapter for displaying the results of a Firestore [Query].
*
* Note that this class forgoes some efficiency to gain simplicity. For example, the result of
* [DocumentSnapshot.toObject] is not cached so the same object may be deserialized
* many times as the user scrolls.
*/
abstract class FirestoreAdapter(private var query: Query?) :
RecyclerView.Adapter(),
EventListener {
private var registration: ListenerRegistration? = null
private val snapshots = ArrayList()
override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
if (e != null) {
Log.w(TAG, "onEvent:error", e)
onError(e)
return
}
if (documentSnapshots == null) {
return
}
// Dispatch the event
Log.d(TAG, "onEvent:numChanges:" + documentSnapshots.documentChanges.size)
for (change in documentSnapshots.documentChanges) {
when (change.type) {
DocumentChange.Type.ADDED -> onDocumentAdded(change)
DocumentChange.Type.MODIFIED -> onDocumentModified(change)
DocumentChange.Type.REMOVED -> onDocumentRemoved(change)
}
}
onDataChanged()
}
fun startListening() {
if (query != null && registration == null) {
registration = query!!.addSnapshotListener(this)
}
}
fun stopListening() {
registration?.remove()
registration = null
snapshots.clear()
notifyDataSetChanged()
}
fun setQuery(query: Query) {
// Stop listening
stopListening()
// Clear existing data
snapshots.clear()
notifyDataSetChanged()
// Listen to new query
this.query = query
startListening()
}
open fun onError(e: FirebaseFirestoreException) {
Log.w(TAG, "onError", e)
}
open fun onDataChanged() {}
override fun getItemCount(): Int {
return snapshots.size
}
protected fun getSnapshot(index: Int): DocumentSnapshot {
return snapshots[index]
}
private fun onDocumentAdded(change: DocumentChange) {
snapshots.add(change.newIndex, change.document)
notifyItemInserted(change.newIndex)
}
private fun onDocumentModified(change: DocumentChange) {
if (change.oldIndex == change.newIndex) {
// Item changed but remained in same position
snapshots[change.oldIndex] = change.document
notifyItemChanged(change.oldIndex)
} else {
// Item changed and changed position
snapshots.removeAt(change.oldIndex)
snapshots.add(change.newIndex, change.document)
notifyItemMoved(change.oldIndex, change.newIndex)
}
}
private fun onDocumentRemoved(change: DocumentChange) {
snapshots.removeAt(change.oldIndex)
notifyItemRemoved(change.oldIndex)
}
companion object {
private const val TAG = "FirestoreAdapter"
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RatingAdapter.kt
================================================
package com.google.firebase.example.fireeats.kotlin.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.example.fireeats.databinding.ItemRatingBinding
import com.google.firebase.example.fireeats.kotlin.model.Rating
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.toObject
import java.text.SimpleDateFormat
import java.util.Locale
/**
* RecyclerView adapter for a list of [Rating].
*/
open class RatingAdapter(query: Query) : FirestoreAdapter(query) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemRatingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getSnapshot(position).toObject())
}
class ViewHolder(val binding: ItemRatingBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(rating: Rating?) {
if (rating == null) {
return
}
binding.ratingItemName.text = rating.userName
binding.ratingItemRating.rating = rating.rating.toFloat()
binding.ratingItemText.text = rating.text
if (rating.timestamp != null) {
binding.ratingItemDate.text = FORMAT.format(rating.timestamp)
}
}
companion object {
private val FORMAT = SimpleDateFormat(
"MM/dd/yyyy",
Locale.US,
)
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/adapter/RestaurantAdapter.kt
================================================
package com.google.firebase.example.fireeats.kotlin.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.databinding.ItemRestaurantBinding
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.toObject
/**
* RecyclerView adapter for a list of Restaurants.
*/
open class RestaurantAdapter(query: Query, private val listener: OnRestaurantSelectedListener) :
FirestoreAdapter(query) {
interface OnRestaurantSelectedListener {
fun onRestaurantSelected(restaurant: DocumentSnapshot)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
ItemRestaurantBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
),
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getSnapshot(position), listener)
}
class ViewHolder(val binding: ItemRestaurantBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(
snapshot: DocumentSnapshot,
listener: OnRestaurantSelectedListener?,
) {
val restaurant = snapshot.toObject() ?: return
val resources = binding.root.resources
// Load image
Glide.with(binding.restaurantItemImage.context)
.load(restaurant.photo)
.into(binding.restaurantItemImage)
val numRatings: Int = restaurant.numRatings
binding.restaurantItemName.text = restaurant.name
binding.restaurantItemRating.rating = restaurant.avgRating.toFloat()
binding.restaurantItemCity.text = restaurant.city
binding.restaurantItemCategory.text = restaurant.category
binding.restaurantItemNumRatings.text = resources.getString(
R.string.fmt_num_ratings,
numRatings,
)
binding.restaurantItemPrice.text = RestaurantUtil.getPriceString(restaurant)
// Click listener
binding.root.setOnClickListener {
listener?.onRestaurantSelected(snapshot)
}
}
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Rating.kt
================================================
package com.google.firebase.example.fireeats.kotlin.model
import android.text.TextUtils
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.firestore.ServerTimestamp
import java.util.Date
/**
* Model POJO for a rating.
*/
data class Rating(
var userId: String? = null,
var userName: String? = null,
var rating: Double = 0.toDouble(),
var text: String? = null,
@ServerTimestamp var timestamp: Date? = null,
) {
constructor(user: FirebaseUser, rating: Double, text: String) : this() {
this.userId = user.uid
this.userName = user.displayName
if (TextUtils.isEmpty(this.userName)) {
this.userName = user.email
}
this.rating = rating
this.text = text
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/model/Restaurant.kt
================================================
package com.google.firebase.example.fireeats.kotlin.model
import com.google.firebase.firestore.IgnoreExtraProperties
/**
* Restaurant POJO.
*/
@IgnoreExtraProperties
data class Restaurant(
var name: String? = null,
var city: String? = null,
var category: String? = null,
var photo: String? = null,
var price: Int = 0,
var numRatings: Int = 0,
var avgRating: Double = 0.toDouble(),
) {
companion object {
const val FIELD_CITY = "city"
const val FIELD_CATEGORY = "category"
const val FIELD_PRICE = "price"
const val FIELD_POPULARITY = "numRatings"
const val FIELD_AVG_RATING = "avgRating"
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RatingUtil.kt
================================================
package com.google.firebase.example.fireeats.kotlin.util
import com.google.firebase.example.fireeats.kotlin.model.Rating
import java.util.ArrayList
import java.util.Random
import java.util.UUID
import kotlin.math.floor
/**
* Utilities for Ratings.
*/
object RatingUtil {
private val REVIEW_CONTENTS = arrayOf(
// 0 - 1 stars
"This was awful! Totally inedible.",
// 1 - 2 stars
"This was pretty bad, would not go back.",
// 2 - 3 stars
"I was fed, so that's something.",
// 3 - 4 stars
"This was a nice meal, I'd go back.",
// 4 - 5 stars
"This was fantastic! Best ever!",
)
/**
* Create a random Rating POJO.
*/
private val random: Rating
get() {
val rating = Rating()
val random = Random()
val score = random.nextDouble() * 5.0
val text = REVIEW_CONTENTS[floor(score).toInt()]
rating.userId = UUID.randomUUID().toString()
rating.userName = "Random User"
rating.rating = score
rating.text = text
return rating
}
/**
* Get a list of random Rating POJOs.
*/
fun getRandomList(length: Int): List {
val result = ArrayList()
for (i in 0 until length) {
result.add(random)
}
return result
}
/**
* Get the average rating of a List.
*/
fun getAverageRating(ratings: List): Double {
var sum = 0.0
for (rating in ratings) {
sum += rating.rating
}
return sum / ratings.size
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/util/RestaurantUtil.kt
================================================
package com.google.firebase.example.fireeats.kotlin.util
import android.content.Context
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import java.util.Arrays
import java.util.Locale
import java.util.Random
/**
* Utilities for Restaurants.
*/
object RestaurantUtil {
private const val RESTAURANT_URL_FMT = "https://storage.googleapis.com/firestorequickstarts.appspot.com/food_%d.png"
private const val MAX_IMAGE_NUM = 22
private val NAME_FIRST_WORDS = arrayOf(
"Foo", "Bar", "Baz", "Qux", "Fire", "Sam's", "World Famous", "Google", "The Best",
)
private val NAME_SECOND_WORDS = arrayOf(
"Restaurant",
"Cafe",
"Spot",
"Eatin' Place",
"Eatery",
"Drive Thru",
"Diner",
)
/**
* Create a random Restaurant POJO.
*/
fun getRandom(context: Context): Restaurant {
val restaurant = Restaurant()
val random = Random()
// Cities (first elemnt is 'Any')
var cities = context.resources.getStringArray(R.array.cities)
cities = cities.copyOfRange(1, cities.size)
// Categories (first element is 'Any')
var categories = context.resources.getStringArray(R.array.categories)
categories = categories.copyOfRange(1, categories.size)
val prices = intArrayOf(1, 2, 3)
restaurant.name = getRandomName(random)
restaurant.city = getRandomString(cities, random)
restaurant.category = getRandomString(categories, random)
restaurant.photo = getRandomImageUrl(random)
restaurant.price = getRandomInt(prices, random)
restaurant.numRatings = random.nextInt(20)
// Note: average rating intentionally not set
return restaurant
}
/**
* Get a random image.
*/
private fun getRandomImageUrl(random: Random): String {
// Integer between 1 and MAX_IMAGE_NUM (inclusive)
val id = random.nextInt(MAX_IMAGE_NUM) + 1
return String.format(Locale.getDefault(), RESTAURANT_URL_FMT, id)
}
/**
* Get price represented as dollar signs.
*/
fun getPriceString(restaurant: Restaurant): String {
return getPriceString(restaurant.price)
}
/**
* Get price represented as dollar signs.
*/
fun getPriceString(priceInt: Int): String {
return when (priceInt) {
1 -> "$"
2 -> "$$"
3 -> "$$$"
else -> "$$$"
}
}
private fun getRandomName(random: Random): String {
return (
getRandomString(NAME_FIRST_WORDS, random) + " " +
getRandomString(NAME_SECOND_WORDS, random)
)
}
private fun getRandomString(array: Array, random: Random): String {
val ind = random.nextInt(array.size)
return array[ind]
}
private fun getRandomInt(array: IntArray, random: Random): Int {
val ind = random.nextInt(array.size)
return array[ind]
}
}
================================================
FILE: firestore/app/src/main/java/com/google/firebase/example/fireeats/kotlin/viewmodel/MainActivityViewModel.kt
================================================
package com.google.firebase.example.fireeats.kotlin.viewmodel
import androidx.lifecycle.ViewModel
import com.google.firebase.example.fireeats.kotlin.Filters
/**
* ViewModel for [com.google.firebase.example.fireeats.MainActivity].
*/
class MainActivityViewModel : ViewModel() {
var isSigningIn: Boolean = false
var filters: Filters = Filters.default
}
================================================
FILE: firestore/app/src/main/res/anim/slide_in_from_left.xml
================================================
================================================
FILE: firestore/app/src/main/res/anim/slide_in_from_right.xml
================================================
================================================
FILE: firestore/app/src/main/res/anim/slide_out_to_left.xml
================================================
================================================
FILE: firestore/app/src/main/res/anim/slide_out_to_right.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/bg_shadow.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/gradient_up.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_add_white_24px.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_arrow_back_white_24px.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_close_white_24px.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_fastfood_white_24dp.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_filter_list_white_24px.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_local_dining_white_24px.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_monetization_on_white_24px.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_place_white_24px.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_restaurant_white_24px.xml
================================================
================================================
FILE: firestore/app/src/main/res/drawable/ic_sort_white_24px.xml
================================================
================================================
FILE: firestore/app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: firestore/app/src/main/res/layout/dialog_filters.xml
================================================
================================================
FILE: firestore/app/src/main/res/layout/dialog_rating.xml
================================================
================================================
FILE: firestore/app/src/main/res/layout/fragment_main.xml
================================================
================================================
FILE: firestore/app/src/main/res/layout/fragment_restaurant_detail.xml
================================================
================================================
FILE: firestore/app/src/main/res/layout/item_rating.xml
================================================
================================================
FILE: firestore/app/src/main/res/layout/item_restaurant.xml
================================================
================================================
FILE: firestore/app/src/main/res/menu/menu_main.xml
================================================
================================================
FILE: firestore/app/src/main/res/navigation/nav_graph_java.xml
================================================
================================================
FILE: firestore/app/src/main/res/navigation/nav_graph_kotlin.xml
================================================
================================================
FILE: firestore/app/src/main/res/values/colors.xml
================================================
#4285F4
#3367D6
#F4B400
#DE000000
#8B000000
#61000000
================================================
FILE: firestore/app/src/main/res/values/dimens.xml
================================================
================================================
FILE: firestore/app/src/main/res/values/strings.xml
================================================
Friendly Eats
All food
Anywhere
Any price
$
$$
$$$
Category
City
Price
Sort By
(%d)
All Restaurants
Filter
Search
Sort by Rating
Sort by Popularity
Sort by Price
sorted by rating
sorted by price
sorted by popularity
Add Random Items
Sign Out
•
Apply
Cancel
Oops, couldn\'t find any results\nthat matched your filter
Be the first to leave a review!
How was your experience?
Submit
Add review
Sign In Error
No network connection.
Unknown error.
Retry
Exit
- @string/value_any_category
- Brunch
- Burgers
- Coffee
- Deli
- Dim Sum
- Indian
- Italian
- Mediterranean
- Mexican
- Pizza
- Ramen
- Sushi
- @string/value_any_city
- Albuquerque
- Arlington
- Atlanta
- Austin
- Baltimore
- Boston
- Charlotte
- Chicago
- Cleveland
- Colorado Springs
- Columbus
- Dallas
- Denver
- Detroit
- El Paso
- Fort Worth
- Fresno
- Houston
- Indianapolis
- Jacksonville
- Kansas City
- Las Vegas
- Long Beach
- Los Angeles
- Louisville
- Memphis
- Mesa
- Miami
- Milwaukee
- Nashville
- New York
- Oakland
- Oklahoma
- Omaha
- Philadelphia
- Phoenix
- Portland
- Raleigh
- Sacramento
- San Antonio
- San Diego
- San Francisco
- San Jose
- Tucson
- Tulsa
- Virginia Beach
- Washington
- @string/value_any_price
- @string/price_1
- @string/price_2
- @string/price_3
- @string/sort_by_rating
- @string/sort_by_popularity
- @string/sort_by_price
================================================
FILE: firestore/app/src/main/res/values/styles.xml
================================================
================================================
FILE: firestore/app/src/main/res/values-v21/styles.xml
================================================
================================================
FILE: firestore/app/test-proguard-rules.pro
================================================
-include proguard-rules.pro
-keepattributes SourceFile,LineNumberTable
-dontwarn org.xmlpull.v1.**
-dontnote org.xmlpull.v1.**
-keep class org.xmlpull.** { *; }
-keepclassmembers class org.xmlpull.** { *; }
-keep class com.google.firebase.auth.** {*;}
================================================
FILE: firestore/build.gradle.kts
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.navigation.safeargs) apply false
}
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
}
}
tasks {
register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}
}
================================================
FILE: firestore/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: firestore/gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# Don't use the gradle build cache since this sample uses experimental SDKs
android.enableBuildCache=false
android.useAndroidX=true
================================================
FILE: firestore/gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# 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
#
# https://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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: firestore/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: firestore/indexes.json
================================================
{
"indexes": [
{
"collectionId": "restaurants",
"fields": [
{ "fieldPath": "city", "mode": "ASCENDING" },
{ "fieldPath": "avgRating", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"fields": [
{ "fieldPath": "category", "mode": "ASCENDING" },
{ "fieldPath": "avgRating", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"fields": [
{ "fieldPath": "price", "mode": "ASCENDING" },
{ "fieldPath": "avgRating", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"fields": [
{ "fieldPath": "city", "mode": "ASCENDING" },
{ "fieldPath": "numRatings", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"fields": [
{ "fieldPath": "category", "mode": "ASCENDING" },
{ "fieldPath": "numRatings", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"fields": [
{ "fieldPath": "price", "mode": "ASCENDING" },
{ "fieldPath": "numRatings", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"fields": [
{ "fieldPath": "city", "mode": "ASCENDING" },
{ "fieldPath": "price", "mode": "ASCENDING" }
]
},
{
"collectionId": "restaurants",
"fields": [
{ "fieldPath": "category", "mode": "ASCENDING" },
{ "fieldPath": "price", "mode": "ASCENDING" }
]
}
]
}
================================================
FILE: firestore/settings.gradle.kts
================================================
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
include(":app")
// Required so that gradle can resolve these dependencies even when
// building only a single project.
include(":internal:lintchecks")
project(":internal:lintchecks").projectDir = file("../internal/lintchecks")
include(":internal:lint")
project(":internal:lint").projectDir = file("../internal/lint")
include(":internal:chooserx")
project(":internal:chooserx").projectDir = file("../internal/chooserx")
================================================
FILE: firestore/test_setup.sh
================================================
#!/bin/bash
set -o nounset
set -e
#delete the restaurants collection
echo "Deleting the restaurants collection under project"
firebase firestore:delete "restaurants" -r -y --project="$PROJECT_ID"
#create a test account test@mailinator.com
echo "Creating test accounts"
firebase auth:import accounts.json --hash-algo=SHA256 --rounds=1 --project="$PROJECT_ID"
================================================
FILE: functions/.gitignore
================================================
/build
.firebaserc
*-debug.log
================================================
FILE: functions/README.md
================================================
Firebase Functions Quickstart
=============================
Introduction
------------
This quickstart demontstrates **Callable Functions** which are HTTPS Cloud Functions
that can be invoked directly from your mobile application.
- [Read more about callable functions](https://firebase.google.com/docs/functions/callable)
Getting Started
---------------
- [Add Firebase to your Android Project](https://firebase.google.com/docs/android/setup).
- Deploy the provided cloud functions:
```bash
# Move to the `functions` subdirectory of quickstart-android
cd functions
# Install all of the dependencies of the cloud functions
cd functions
npm install
cd ../
# Deploy functions to your Firebase project
firebase --project=YOUR_PROJECT_ID deploy --only functions
```
- Run the sample on Android device or emulator.
Screenshots
-----------
Support
-------
- [Stack Overflow](https://stackoverflow.com/questions/tagged/google-cloud-functions)
- [Firebase Support](https://firebase.google.com/support/)
License
-------
Copyright 2018 Google, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.
================================================
FILE: functions/app/.gitignore
================================================
/build
================================================
FILE: functions/app/build.gradle.kts
================================================
plugins {
id("com.android.application")
id("com.google.gms.google-services")
}
android {
namespace = "com.google.samples.quickstart.functions"
// Changes the test build type for instrumented tests to "stage".
testBuildType = "release"
compileSdk = 36
defaultConfig {
applicationId = "com.google.samples.quickstart.functions"
minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
multiDexEnabled = true
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
testProguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "test-proguard-rules.pro")
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation(project(":internal:lintchecks"))
implementation(project(":internal:chooserx"))
implementation("androidx.activity:activity-ktx:1.12.1")
implementation("androidx.fragment:fragment-ktx:1.8.9")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("com.google.android.material:material:1.13.0")
// Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom)
implementation(platform("com.google.firebase:firebase-bom:34.7.0"))
// Cloud Functions for Firebase
implementation("com.google.firebase:firebase-functions")
// Firebase Authentication
implementation("com.google.firebase:firebase-auth")
// Firebase Cloud Messaging
implementation("com.google.firebase:firebase-messaging")
// Firebase UI
implementation("com.firebaseui:firebase-ui-auth:9.1.1")
// Google Play services
implementation("com.google.android.gms:play-services-auth:21.2.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
androidTestImplementation("androidx.test:rules:1.7.0")
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0")
}
================================================
FILE: functions/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-keepattributes EnclosingMethod
-keepattributes InnerClasses
# https://github.com/firebase/FirebaseUI-Android/issues/1227
-dontwarn com.firebase.ui.auth.data.remote.**
================================================
FILE: functions/app/src/androidTest/AndroidManifest.xml
================================================
================================================
FILE: functions/app/src/androidTest/java/com/google/samples/quickstart/functions/MainActivityTest.java
================================================
package com.google.samples.quickstart.functions;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import com.google.samples.quickstart.functions.java.MainActivity;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertTrue;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(MainActivity.class);
@Test
public void mainActivityTest() {
assertTrue(1 + 1 == 2);
}
}
================================================
FILE: functions/app/src/androidTest/java/com/google/samples/quickstart/functions/TestAddNumber.java
================================================
package com.google.samples.quickstart.functions;
import androidx.test.espresso.ViewInteraction;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiSelector;
import androidx.test.filters.LargeTest;
import com.google.samples.quickstart.functions.java.MainActivity;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
@LargeTest @RunWith(AndroidJUnit4.class)
public class TestAddNumber {
@Rule public ActivityTestRule mActivityTestRule =
new ActivityTestRule<>(MainActivity.class);
@Before public void setUp() {
UiDevice.getInstance(getInstrumentation());
}
@Test
public void testAddNumber() {
ViewInteraction firstNumber = onView(withId(R.id.fieldFirstNumber));
ViewInteraction secondNumber = onView(withId(R.id.fieldSecondNumber));
ViewInteraction addButton = onView(withId(R.id.buttonCalculate));
firstNumber.perform(replaceText("32"));
secondNumber.perform(replaceText("16"));
addButton.perform(scrollTo(), click());
Assert.assertTrue(
new UiObject(new UiSelector()
.resourceId("com.google.samples.quickstart.functions:id/fieldAddResult").text("48"))
.waitForExists(60000));
}
}
================================================
FILE: functions/app/src/main/AndroidManifest.xml
================================================
================================================
FILE: functions/app/src/main/java/com/google/samples/quickstart/functions/EntryChoiceActivity.kt
================================================
package com.google.samples.quickstart.functions
import android.content.Intent
import com.firebase.example.internal.BaseEntryChoiceActivity
import com.firebase.example.internal.Choice
class EntryChoiceActivity : BaseEntryChoiceActivity() {
override fun getChoices(): List {
return listOf(
Choice(
"Java",
"Run the Firebase Functions quickstart written in Java.",
Intent(this, com.google.samples.quickstart.functions.java.MainActivity::class.java),
),
Choice(
"Kotlin",
"Run the Firebase Functions quickstart written in Kotlin.",
Intent(
this,
com.google.samples.quickstart.functions.kotlin.MainActivity::class.java,
),
),
)
}
}
================================================
FILE: functions/app/src/main/java/com/google/samples/quickstart/functions/java/FunctionsMessagingService.java
================================================
package com.google.samples.quickstart.functions.java;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import android.util.Log;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.google.samples.quickstart.functions.R;
public class FunctionsMessagingService extends FirebaseMessagingService {
private static final String TAG = "MessagingService";
public FunctionsMessagingService() {
}
private void createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel("Messages", "Messages", importance);
channel.setDescription("All messages.");
// Register the channel with the system; you can't change the importance
// or other notification behaviors after this
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
createNotificationChannel();
// Check if message contains a data payload.
if (remoteMessage.getData().size() > 0) {
Log.d(TAG, "Message data payload: " + remoteMessage.getData());
// Check if permission to post notifications has been granted
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
NotificationManagerCompat manager = NotificationManagerCompat.from(this);
Notification notification = new NotificationCompat.Builder(this, "Messages")
.setContentText(remoteMessage.getData().get("text"))
.setContentTitle("New message")
.setSmallIcon(R.drawable.ic_stat_notification)
.build();
manager.notify(0, notification);
}
}
}
}
================================================
FILE: functions/app/src/main/java/com/google/samples/quickstart/functions/java/MainActivity.java
================================================
/*
* Copyright Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.quickstart.functions.java;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract;
import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult;
import com.google.android.material.snackbar.Snackbar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import com.firebase.ui.auth.AuthUI;
import com.firebase.ui.auth.IdpResponse;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.functions.FirebaseFunctions;
import com.google.firebase.functions.FirebaseFunctionsException;
import com.google.firebase.functions.HttpsCallableResult;
import com.google.samples.quickstart.functions.R;
import com.google.samples.quickstart.functions.databinding.ActivityMainBinding;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* This activity demonstrates the Android SDK for Callable Functions.
*
* For more information, see the documentation for Cloud Functions for Firebase:
* https://firebase.google.com/docs/functions/
*/
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "MainActivity";
private ActivityMainBinding binding;
// [START define_functions_instance]
private FirebaseFunctions mFunctions;
// [END define_functions_instance]
private final ActivityResultLauncher requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
Toast.makeText(this, "Notifications permission granted", Toast.LENGTH_SHORT)
.show();
} else {
Toast.makeText(this,
"FCM can't post notifications without POST_NOTIFICATIONS permission",
Toast.LENGTH_LONG).show();
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.buttonCalculate.setOnClickListener(this);
binding.buttonAddMessage.setOnClickListener(this);
binding.buttonSignIn.setOnClickListener(this);
// [START initialize_functions_instance]
mFunctions = FirebaseFunctions.getInstance();
// [END initialize_functions_instance]
askNotificationPermission();
}
// [START function_add_numbers]
private Task addNumbers(int a, int b) {
// Create the arguments to the callable function, which are two integers
Map data = new HashMap<>();
data.put("firstNumber", a);
data.put("secondNumber", b);
// Call the function and extract the operation from the result
return mFunctions
.getHttpsCallable("addNumbers")
.call(data)
.continueWith(new Continuation