Skip to content

Implement SignIn MVP #2783

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 2, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions firebase-app-distribution/firebase-app-distribution.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ android {
compileSdkVersion project.targetSdkVersion

defaultConfig {
minSdkVersion project.minSdkVersion
minSdkVersion 16
targetSdkVersion project.targetSdkVersion
multiDexEnabled true
versionName version
Expand All @@ -40,6 +40,21 @@ android {
dependencies {
implementation 'org.jetbrains:annotations:15.0'
implementation project(path: ':firebase-components')
implementation 'com.google.android.gms:play-services-tasks:17.0.0'
implementation project(path: ':firebase-installations-interop')
implementation project(path: ':firebase-common')
}
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation project(path: ':firebase-installations')

testImplementation 'junit:junit:4.12'
testImplementation 'junit:junit:4.12'
testImplementation "org.robolectric:robolectric:$robolectricVersion"
testImplementation "com.google.truth:truth:$googleTruthVersion"
testImplementation 'org.mockito:mockito-core:2.25.0'
androidTestImplementation "org.mockito:mockito-android:2.25.0"
testImplementation 'androidx.test:core:1.2.0'

implementation 'com.google.android.gms:play-services-tasks:17.0.0'

implementation 'com.android.support:customtabs:28.0.0'
implementation "androidx.browser:browser:1.3.0"
}
2 changes: 2 additions & 0 deletions firebase-app-distribution/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
# limitations under the License.

version=0.0.1
android.useAndroidX=true
android.enableJetifier=true
13 changes: 12 additions & 1 deletion firebase-app-distribution/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,16 @@
android:name="com.google.firebase.components:com.google.firebase.appdistribution.FirebaseAppDistributionRegistrar"
android:value="com.google.firebase.components.ComponentRegistrar" />
</service>

<activity android:name=".SignInResultActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>

<data android:scheme="${applicationId}" android:host="authredirect" />

</intent-filter>
</activity>
</application>
</manifest>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,67 @@

package com.google.firebase.appdistribution;

import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_CANCELED;
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Application;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsIntent;
import com.google.android.gms.common.internal.Preconditions;
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.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.installations.FirebaseInstallationsApi;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class FirebaseAppDistribution {
public class FirebaseAppDistribution implements Application.ActivityLifecycleCallbacks {

private final FirebaseApp firebaseApp;
private final FirebaseInstallationsApi firebaseInstallationsApi;
private static final String TAG = "FirebaseAppDistribution";
private Activity currentActivity;
@VisibleForTesting private boolean currentlySigningIn = false;
private TaskCompletionSource<Void> signInTaskCompletionSource = null;

/** Constructor for FirebaseAppDistribution */
FirebaseAppDistribution(@NonNull FirebaseApp firebaseApp) {
public FirebaseAppDistribution(
@NonNull FirebaseApp firebaseApp,
@NonNull FirebaseInstallationsApi firebaseInstallationsApi) {
this.firebaseApp = firebaseApp;
this.firebaseInstallationsApi = firebaseInstallationsApi;
}

/** @return a FirebaseInstallationsApi instance */
/** @return a FirebaseAppDistribution instance */
@NonNull
public static FirebaseAppDistribution getInstance() {
return new FirebaseAppDistribution(FirebaseApp.getInstance());
return getInstance(FirebaseApp.getInstance());
}

/**
* Returns the {@link FirebaseAppDistribution} initialized with a custom {@link FirebaseApp}.
*
* @param app a custom {@link FirebaseApp}
* @return a {@link FirebaseAppDistribution} instance
*/
@NonNull
public static FirebaseAppDistribution getInstance(@NonNull FirebaseApp app) {
Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp.");
return (FirebaseAppDistribution) app.get(FirebaseAppDistribution.class);
}

/**
Expand All @@ -44,7 +87,27 @@ public static FirebaseAppDistribution getInstance() {
*/
@NonNull
public Task<AppDistributionRelease> updateToLatestRelease() {
return Tasks.forResult(null);

TaskCompletionSource<AppDistributionRelease> taskCompletionSource =
new TaskCompletionSource<>();

signInTester()
.addOnSuccessListener(
new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void unused) {
taskCompletionSource.setResult(null);
}
})
.addOnFailureListener(
new OnFailureListener() {
@Override
public void onFailure(@NonNull @NotNull Exception e) {
taskCompletionSource.setException(e);
}
});

return taskCompletionSource.getTask();
}

/**
Expand All @@ -62,18 +125,119 @@ public Task<AppDistributionRelease> checkForUpdate() {
* starts an installation If the latest release is an AAB, directs the tester to the Play app to
* complete the download and installation.
*
* @throws an {@link Status.UPDATE_NOT_AVAILABLE_ERROR} exception if no new release is cached from
* checkForUpdate
* @throws FirebaseAppDistributionException with UPDATE_NOT_AVAIALBLE exception if no new release
* is cached from checkForUpdate
*/
@NonNull
public UpdateTask updateApp() {
return (UpdateTask) Tasks.forResult(new UpdateState(0, 0, UpdateStatus.PENDING));
}

private boolean supportsCustomTabs(Context context) {
Intent customTabIntent = new Intent("android.support.customtabs.action.CustomTabsService");
customTabIntent.setPackage("com.android.chrome");
List<ResolveInfo> resolveInfos =
context.getPackageManager().queryIntentServices(customTabIntent, 0);
return resolveInfos != null && !resolveInfos.isEmpty();
}

private static String getApplicationName(Context context) {
try {
return context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
} catch (Exception e) {
Log.e(TAG, "Unable to retrieve App name");
return "";
}
}

private void openSignInFlowInBrowser(Uri uri) {
currentlySigningIn = true;
if (supportsCustomTabs(firebaseApp.getApplicationContext())) {
// If we can launch a chrome view, try that.
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
Intent intent = customTabsIntent.intent;
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
customTabsIntent.launchUrl(currentActivity, uri);

} else {
// If we can't launch a chrome view try to launch anything that can handle a URL.
Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
ResolveInfo info = currentActivity.getPackageManager().resolveActivity(browserIntent, 0);
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
currentActivity.startActivity(browserIntent);
}
}

private OnSuccessListener<String> getFidGenerationOnSuccessListener(Context context) {
return new OnSuccessListener<String>() {
@Override
public void onSuccess(String fid) {
Uri uri =
Uri.parse(
String.format(
Copy link
Contributor

@pranavrajgopal pranavrajgopal Jul 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should change to https://appdistribution.firebase.google.com/pub/apps/" + "%s/installations/%s/buildalerts?appName=%s&packageName=%s

"https://appdistribution.firebase.dev/nba/pub/apps/"
+ "%s/installations/%s/buildalerts?appName=%s",
firebaseApp.getOptions().getApplicationId(), fid, getApplicationName(context)));
openSignInFlowInBrowser(uri);
}
};
}

private AlertDialog getSignInAlertDialog(Context context) {
AlertDialog alertDialog = new AlertDialog.Builder(currentActivity).create();
alertDialog.setTitle(context.getString(R.string.signin_dialog_title));
alertDialog.setMessage(context.getString(R.string.singin_dialog_message));
alertDialog.setButton(
AlertDialog.BUTTON_POSITIVE,
context.getString(R.string.singin_yes_button),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
firebaseInstallationsApi
.getId()
.addOnSuccessListener(getFidGenerationOnSuccessListener(context))
.addOnFailureListener(
new OnFailureListener() {
@Override
public void onFailure(@NonNull @NotNull Exception e) {
setSignInTaskCompletionError(
new FirebaseAppDistributionException(AUTHENTICATION_FAILURE));
}
});
}
});
alertDialog.setButton(
AlertDialog.BUTTON_NEGATIVE,
context.getString(R.string.singin_no_button),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
setSignInTaskCompletionError(
new FirebaseAppDistributionException(AUTHENTICATION_CANCELED));
dialogInterface.dismiss();
}
});
return alertDialog;
}

/** Signs in the App Distribution tester. Presents the tester with a Google sign in UI */
@NonNull
public Task<Void> signInTester() {
return Tasks.forResult(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the comment here.
Also should we consider adding some logic to handle the case where a previous signInTaskCompletionSource is not yet complete. Maybe if the previoussignInTaskCompleteionSource is not complete, we just cancel it with an error and then proceed forward?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to add cancellation logic, will do that in the next push.


this.signInTaskCompletionSource = new TaskCompletionSource<>();
Context context = firebaseApp.getApplicationContext();
AlertDialog alertDialog = getSignInAlertDialog(context);
alertDialog.show();

return signInTaskCompletionSource.getTask();
}

private void setSignInTaskCompletionError(FirebaseAppDistributionException e) {
if (signInTaskCompletionSource != null && !signInTaskCompletionSource.getTask().isComplete()) {
this.signInTaskCompletionSource.setException(e);
}
}

/** Returns true if the App Distribution tester is signed in */
Expand All @@ -84,4 +248,62 @@ public boolean isTesterSignedIn() {

/** Signs out the App Distribution tester */
public void signOutTester() {}

@Override
public void onActivityCreated(
@NonNull @NotNull Activity activity, @androidx.annotation.Nullable @Nullable Bundle bundle) {
Log.d(TAG, "Created activity: " + activity.getClass().getName());
// if signinactivity is created, sign-in was succesful
if (currentlySigningIn && activity instanceof SignInResultActivity) {
currentlySigningIn = false;
signInTaskCompletionSource.setResult(null);
}
}

@Override
public void onActivityStarted(@NonNull @NotNull Activity activity) {
Log.d(TAG, "Started activity: " + activity.getClass().getName());
}

@Override
public void onActivityResumed(@NonNull @NotNull Activity activity) {
Log.d(TAG, "Resumed activity: " + activity.getClass().getName());

// signInActivity is only opened after successful redirection from signIn flow,
// should not be treated as reentering the app
if (activity instanceof SignInResultActivity) {
return;
}

// throw error if app reentered during signin
if (currentlySigningIn) {
currentlySigningIn = false;
setSignInTaskCompletionError(new FirebaseAppDistributionException(AUTHENTICATION_FAILURE));
}
this.currentActivity = activity;
}

@Override
public void onActivityPaused(@NonNull @NotNull Activity activity) {
Log.d(TAG, "Paused activity: " + activity.getClass().getName());
}

@Override
public void onActivityStopped(@NonNull @NotNull Activity activity) {
Log.d(TAG, "Stopped activity: " + activity.getClass().getName());
}

@Override
public void onActivitySaveInstanceState(
@NonNull @NotNull Activity activity, @NonNull @NotNull Bundle bundle) {
Log.d(TAG, "Saved activity: " + activity.getClass().getName());
}

@Override
public void onActivityDestroyed(@NonNull @NotNull Activity activity) {
Log.d(TAG, "Destroyed activity: " + activity.getClass().getName());
if (this.currentActivity == activity) {
this.currentActivity = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package com.google.firebase.appdistribution;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.firebase.FirebaseException;

/** Possible exceptions thrown in FirebaseAppDistribution */
Expand Down Expand Up @@ -55,14 +56,19 @@ public enum Status {
}

@NonNull private final Status status;
@NonNull private final AppDistributionRelease release;
@Nullable private final AppDistributionRelease release;

FirebaseAppDistributionException(
@NonNull Status status, @NonNull AppDistributionRelease release) {
@NonNull Status status, @Nullable AppDistributionRelease release) {
this.status = status;
this.release = release;
}

FirebaseAppDistributionException(@NonNull Status status) {
this.status = status;
this.release = null;
}

/** Get cached release when error was thrown */
@NonNull
public AppDistributionRelease getRelease() {
Expand Down
Loading