diff options
author | Yiming Jing <yimingjing@google.com> | 2021-01-20 13:15:15 -0800 |
---|---|---|
committer | Yiming Jing <yimingjing@google.com> | 2021-01-20 13:56:46 -0800 |
commit | be1bc0daaacd04f7bfe31da0539bd7496ad03939 (patch) | |
tree | 63502da2cda28003cfdbb327599d9c698dbb8ede | |
parent | 0a7842c31c3900ceef1c5897f5abde730cf40cb2 (diff) | |
download | DebuggingRestrictionController-be1bc0daaacd04f7bfe31da0539bd7496ad03939.tar.gz |
Setup Firebase Functions for Token Issuer
This CL enables the app to request debugging access tokens from the
remote web service using Firebase Functions. Only signed-in users can
request tokens.
Bug: 173732645
Test: Place google-services.json in app/
Test: Get credentials from valentine/#/show/1611079519238994
Test: Set envvars DRC_TEST_EMAIL and DRC_TEST_PASSWORD
Test: ./gradlew connectedAndroidTest
Test: ./gradlew createDebugCoverageReport
Change-Id: I162f95ac2a74b01995f92e63a11247a27217dddc
-rw-r--r-- | app/build.gradle | 1 | ||||
-rw-r--r-- | app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/LoginTest.java (renamed from app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/UiTest.java) | 3 | ||||
-rw-r--r-- | app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/TokenTest.java | 87 | ||||
-rw-r--r-- | app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/ViewModelFactory.java | 27 | ||||
-rw-r--r-- | app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/login/LoginActivity.java | 25 | ||||
-rw-r--r-- | app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenActivity.java | 57 | ||||
-rw-r--r-- | app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenViewModel.java | 30 | ||||
-rw-r--r-- | app/src/main/res/layout/activity_token.xml | 2 |
8 files changed, 173 insertions, 59 deletions
diff --git a/app/build.gradle b/app/build.gradle index df2c4b7..f5c4ee1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,5 +52,6 @@ dependencies { testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0' } diff --git a/app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/UiTest.java b/app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/LoginTest.java index 2be1c7c..b90bc67 100644 --- a/app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/UiTest.java +++ b/app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/LoginTest.java @@ -37,7 +37,7 @@ import org.junit.Test; import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) -public class UiTest { +public class LoginTest { private static final String TEST_EMAIL = BuildConfig.DRC_TEST_EMAIL; private static final String TEST_PASSWORD = BuildConfig.DRC_TEST_PASSWORD; @@ -45,6 +45,7 @@ public class UiTest { private static final String SHORT_PASSWORD = "word"; private static final String WRONG_PASSWORD = "wrong_password"; private final FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(); + @Rule public ActivityScenarioRule<LoginActivity> activityScenarioRule = new ActivityScenarioRule<LoginActivity>(LoginActivity.class); diff --git a/app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/TokenTest.java b/app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/TokenTest.java new file mode 100644 index 0000000..604c347 --- /dev/null +++ b/app/src/androidTest/java/com/android/car/debuggingrestrictioncontroller/TokenTest.java @@ -0,0 +1,87 @@ +package com.android.car.debuggingrestrictioncontroller; + +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.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static org.junit.Assert.assertThat; + +import android.app.Activity; +import androidx.test.espresso.IdlingRegistry; +import androidx.test.espresso.contrib.ActivityResultMatchers; +import androidx.test.espresso.idling.CountingIdlingResource; +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.android.car.debuggingrestrictioncontroller.ui.token.TokenActivity; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.auth.FirebaseAuth; +import java.util.concurrent.ExecutionException; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class TokenTest { + + private static final String TEST_EMAIL = BuildConfig.DRC_TEST_EMAIL; + private static final String TEST_PASSWORD = BuildConfig.DRC_TEST_PASSWORD; + private final FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(); + @Rule + public ActivityScenarioRule<TokenActivity> activityScenarioRule = + new ActivityScenarioRule<TokenActivity>(TokenActivity.class); + private CountingIdlingResource idlingResource; + + public TokenTest() throws ExecutionException, InterruptedException { + Tasks.await(firebaseAuth.signInWithEmailAndPassword(TEST_EMAIL, TEST_PASSWORD)); + } + + @Before + public void setUp() { + activityScenarioRule.getScenario().onActivity(activity -> { + idlingResource = activity.getIdlingResource(); + }); + IdlingRegistry.getInstance().register(idlingResource); + } + + @After + public void tearDown() { + IdlingRegistry.getInstance().unregister(idlingResource); + firebaseAuth.signOut(); + } + + @Test + public void initialButtonStates() { + onView(withId(R.id.agreement)).check(matches(isDisplayed())); + onView(withId(R.id.agree)).check(matches(isDisplayed())); + onView(withId(R.id.agree)).check(matches(isEnabled())); + onView(withId(R.id.disagree)).check(matches(isDisplayed())); + } + + @Test + public void agree() { + onView(withId(R.id.agree)).check(matches(isEnabled())); + onView(withId(R.id.agree)).perform(click()); + assertThat(activityScenarioRule.getScenario().getResult(), + ActivityResultMatchers.hasResultCode(Activity.RESULT_OK)); + } + + @Test + public void disagree() { + onView(withId(R.id.disagree)).check(matches(isEnabled())); + onView(withId(R.id.disagree)).perform(click()); + assertThat(activityScenarioRule.getScenario().getResult(), + ActivityResultMatchers.hasResultCode(Activity.RESULT_CANCELED)); + } + + @Test + public void userNotSignedIn() { + firebaseAuth.signOut(); + onView(withId(R.id.agree)).perform(click()); + assertThat(activityScenarioRule.getScenario().getResult(), + ActivityResultMatchers.hasResultCode(Activity.RESULT_CANCELED)); + } +} diff --git a/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/ViewModelFactory.java b/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/ViewModelFactory.java deleted file mode 100644 index 0c3379a..0000000 --- a/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/ViewModelFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.android.car.debuggingrestrictioncontroller.ui; - -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; -import com.android.car.debuggingrestrictioncontroller.ui.login.LoginViewModel; -import com.android.car.debuggingrestrictioncontroller.ui.token.TokenViewModel; - -/** - * ViewModel provider factory to instantiate LoginViewModel. Required given LoginViewModel has a - * non-empty constructor - */ -public class ViewModelFactory implements ViewModelProvider.Factory { - - @NonNull - @Override - @SuppressWarnings("unchecked") - public <T extends ViewModel> T create(@NonNull Class<T> modelClass) { - if (modelClass.isAssignableFrom(LoginViewModel.class)) { - return (T) new LoginViewModel(); - } else if (modelClass.isAssignableFrom(TokenViewModel.class)) { - return (T) new TokenViewModel(); - } else { - throw new IllegalArgumentException("Unknown ViewModel class"); - } - } -} diff --git a/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/login/LoginActivity.java b/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/login/LoginActivity.java index e498ce6..1dff976 100644 --- a/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/login/LoginActivity.java +++ b/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/login/LoginActivity.java @@ -12,15 +12,14 @@ import android.widget.Button; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; import androidx.test.espresso.idling.CountingIdlingResource; import com.android.car.debuggingrestrictioncontroller.R; -import com.android.car.debuggingrestrictioncontroller.ui.ViewModelFactory; import com.android.car.debuggingrestrictioncontroller.ui.token.TokenActivity; import com.google.android.material.snackbar.Snackbar; import com.google.firebase.auth.FirebaseAuth; @@ -33,7 +32,7 @@ public class LoginActivity extends AppCompatActivity { private final FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(); @VisibleForTesting private final CountingIdlingResource idlingResource = new CountingIdlingResource(TAG); - private LoginViewModel loginViewModel; + private final LoginViewModel loginViewModel = new LoginViewModel(); private Button loginButton; private Button nextButton; @@ -46,8 +45,6 @@ public class LoginActivity extends AppCompatActivity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); - loginViewModel = new ViewModelProvider(this, new ViewModelFactory()) - .get(LoginViewModel.class); final EditText usernameEditText = findViewById(R.id.username); final EditText passwordEditText = findViewById(R.id.password); @@ -55,14 +52,9 @@ public class LoginActivity extends AppCompatActivity { nextButton = findViewById(R.id.next); final ProgressBar loadingProgressBar = findViewById(R.id.loading); - updateButtonState(); - loginViewModel.getLoginFormState().observe(this, new Observer<LoginFormState>() { @Override - public void onChanged(@Nullable LoginFormState loginFormState) { - if (loginFormState == null) { - return; - } + public void onChanged(@NonNull LoginFormState loginFormState) { loginButton.setEnabled(loginFormState.isDataValid()); if (loginFormState.getUsernameError() != null) { usernameEditText.setError(getString(loginFormState.getUsernameError())); @@ -75,10 +67,7 @@ public class LoginActivity extends AppCompatActivity { loginViewModel.getLoginResult().observe(this, new Observer<LoginResult>() { @Override - public void onChanged(@Nullable LoginResult loginResult) { - if (loginResult == null) { - return; - } + public void onChanged(@NonNull LoginResult loginResult) { loadingProgressBar.setVisibility(View.GONE); if (loginResult.getError() != null) { showSnackBar(R.string.not_signed_in); @@ -91,7 +80,9 @@ public class LoginActivity extends AppCompatActivity { loginButton.setText(R.string.button_sign_out); nextButton.setEnabled(true); } - idlingResource.decrement(); + if (!idlingResource.isIdleNow()) { + idlingResource.decrement(); + } } }); @@ -115,10 +106,10 @@ public class LoginActivity extends AppCompatActivity { usernameEditText.addTextChangedListener(afterTextChangedListener); passwordEditText.addTextChangedListener(afterTextChangedListener); passwordEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { - @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_DONE) { + idlingResource.increment(); loginViewModel.login(usernameEditText.getText().toString(), passwordEditText.getText().toString()); } diff --git a/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenActivity.java b/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenActivity.java index 28e402d..37283ad 100644 --- a/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenActivity.java +++ b/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenActivity.java @@ -4,34 +4,47 @@ import android.app.Activity; import android.os.Bundle; import android.text.Html; import android.text.Spanned; +import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModelProvider; +import androidx.test.espresso.idling.CountingIdlingResource; import com.android.car.debuggingrestrictioncontroller.R; -import com.android.car.debuggingrestrictioncontroller.ui.ViewModelFactory; +import com.google.firebase.auth.FirebaseAuth; import java.util.HashMap; import java.util.Map; public class TokenActivity extends AppCompatActivity { private static final String TAG = TokenActivity.class.getSimpleName(); - private TokenViewModel tokenViewModel; + private static final String API_NAME = "requestAccessToken"; + + private final FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(); + @VisibleForTesting + private final CountingIdlingResource idlingResource = new CountingIdlingResource(TAG); + private final TokenViewModel tokenViewModel = new TokenViewModel(); + private Button agreeButton; + private Button disagreeButton; + + @VisibleForTesting + public CountingIdlingResource getIdlingResource() { + return idlingResource; + } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_token); - tokenViewModel = new ViewModelProvider(this, new ViewModelFactory()).get( - TokenViewModel.class); final TextView agreementTextView = findViewById(R.id.agreement); - final Button agreeButton = findViewById(R.id.agree); - final Button disagreeButton = findViewById(R.id.disagree); final ProgressBar loadingProgressBar = findViewById(R.id.token_loading); + agreeButton = findViewById(R.id.agree); + disagreeButton = findViewById(R.id.disagree); Spanned agreementString = Html .fromHtml(getString(R.string.agreement_text), Html.FROM_HTML_MODE_LEGACY); @@ -39,17 +52,20 @@ public class TokenActivity extends AppCompatActivity { tokenViewModel.getTokenResult().observe(this, new Observer<TokenResult>() { @Override - public void onChanged(TokenResult result) { - if (result == null) { - return; - } + public void onChanged(@NonNull TokenResult result) { loadingProgressBar.setVisibility(View.GONE); + if (!idlingResource.isIdleNow()) { + idlingResource.decrement(); + } if (result.getError() != null) { setResult(Activity.RESULT_CANCELED); finish(); } if (result.getSuccess() != null) { setResult(Activity.RESULT_OK); + Log.d(TAG, "Message: " + result.getSuccess().getMessage()); + HashMap<String, Boolean> approvedRestrictions = result.getSuccess() + .getApprovedRestrictions(); finish(); } } @@ -58,9 +74,10 @@ public class TokenActivity extends AppCompatActivity { agreeButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { + idlingResource.increment(); Map<String, Object> query = new HashMap<>(); loadingProgressBar.setVisibility(View.VISIBLE); - tokenViewModel.requestAccessToken("", "", query); + tokenViewModel.requestAccessToken("", API_NAME, query); } }); @@ -72,4 +89,20 @@ public class TokenActivity extends AppCompatActivity { } }); } + + @Override + protected void onResume() { + updateButtonState(); + super.onResume(); + } + + private void updateButtonState() { + if (firebaseAuth.getCurrentUser() == null) { + agreeButton.setEnabled(false); + setResult(Activity.RESULT_CANCELED); + finish(); + } else { + agreeButton.setEnabled(true); + } + } } diff --git a/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenViewModel.java b/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenViewModel.java index 3af4194..4ef716a 100644 --- a/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenViewModel.java +++ b/app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenViewModel.java @@ -1,15 +1,24 @@ package com.android.car.debuggingrestrictioncontroller.ui.token; +import android.util.Base64; +import android.util.Log; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.ViewModel; +import com.google.firebase.functions.FirebaseFunctions; +import java.security.SecureRandom; import java.util.HashMap; import java.util.Map; public class TokenViewModel extends ViewModel { private static final String TAG = TokenViewModel.class.getSimpleName(); + private static final String FIELD_NONCE = "nonce"; + private static final String FIELD_TOKEN = "token"; + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private final FirebaseFunctions firebaseFunctions = FirebaseFunctions.getInstance(); private final MutableLiveData<TokenResult> tokenResult = new MutableLiveData<>(); LiveData<TokenResult> getTokenResult() { @@ -18,6 +27,25 @@ public class TokenViewModel extends ViewModel { public void requestAccessToken(@NonNull String hostName, @NonNull String apiName, @NonNull Map<String, Object> query) { - tokenResult.postValue(new TokenResult(new TokenView("OK", new HashMap<>()))); + byte[] nonceBytes = new byte[16]; + SECURE_RANDOM.nextBytes(nonceBytes); + final String nonce = Base64.encodeToString(nonceBytes, Base64.DEFAULT); + query.put(FIELD_NONCE, nonce); + + firebaseFunctions + .getHttpsCallable(apiName) + .call(query) + .continueWith(task -> { + Map<String, Object> result = (Map<String, Object>) task.getResult().getData(); + return (String) result.get(FIELD_TOKEN); + }) + .addOnCompleteListener(task -> { + if (task.isSuccessful()) { + Log.d(TAG, "Token: " + task.getResult()); + tokenResult.postValue(new TokenResult(new TokenView("OK", new HashMap<>()))); + } else { + tokenResult.postValue(new TokenResult(task.getException().getMessage())); + } + }); } } diff --git a/app/src/main/res/layout/activity_token.xml b/app/src/main/res/layout/activity_token.xml index f3c6676..95b14d9 100644 --- a/app/src/main/res/layout/activity_token.xml +++ b/app/src/main/res/layout/activity_token.xml @@ -24,7 +24,7 @@ android:layout_marginStart="48dp" android:layout_marginEnd="48dp" android:layout_gravity="start" - android:enabled="true" + android:enabled="false" android:text="@string/button_agree" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/disagree" |