summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYiming Jing <yimingjing@google.com>2021-01-20 13:15:15 -0800
committerYiming Jing <yimingjing@google.com>2021-01-20 13:56:46 -0800
commitbe1bc0daaacd04f7bfe31da0539bd7496ad03939 (patch)
tree63502da2cda28003cfdbb327599d9c698dbb8ede
parent0a7842c31c3900ceef1c5897f5abde730cf40cb2 (diff)
downloadDebuggingRestrictionController-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.gradle1
-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.java87
-rw-r--r--app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/ViewModelFactory.java27
-rw-r--r--app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/login/LoginActivity.java25
-rw-r--r--app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenActivity.java57
-rw-r--r--app/src/main/java/com/android/car/debuggingrestrictioncontroller/ui/token/TokenViewModel.java30
-rw-r--r--app/src/main/res/layout/activity_token.xml2
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"