Skip to content

Commit eff44b3

Browse files
Implement SignIn MVP (#2783)
* Implement SignIn MVP * Address PR feedback: add/fix comments, refactor code * Update scheme and url, add cancellation logic * Fix formatting
1 parent 460fd3f commit eff44b3

File tree

9 files changed

+501
-16
lines changed

9 files changed

+501
-16
lines changed

firebase-app-distribution/firebase-app-distribution.gradle

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ android {
2020
compileSdkVersion project.targetSdkVersion
2121

2222
defaultConfig {
23-
minSdkVersion project.minSdkVersion
23+
minSdkVersion 16
2424
targetSdkVersion project.targetSdkVersion
2525
multiDexEnabled true
2626
versionName version
@@ -40,6 +40,21 @@ android {
4040
dependencies {
4141
implementation 'org.jetbrains:annotations:15.0'
4242
implementation project(path: ':firebase-components')
43-
implementation 'com.google.android.gms:play-services-tasks:17.0.0'
43+
implementation project(path: ':firebase-installations-interop')
4444
implementation project(path: ':firebase-common')
45-
}
45+
implementation 'com.android.support:appcompat-v7:28.0.0'
46+
implementation project(path: ':firebase-installations')
47+
48+
testImplementation 'junit:junit:4.12'
49+
testImplementation 'junit:junit:4.12'
50+
testImplementation "org.robolectric:robolectric:$robolectricVersion"
51+
testImplementation "com.google.truth:truth:$googleTruthVersion"
52+
testImplementation 'org.mockito:mockito-core:2.25.0'
53+
androidTestImplementation "org.mockito:mockito-android:2.25.0"
54+
testImplementation 'androidx.test:core:1.2.0'
55+
56+
implementation 'com.google.android.gms:play-services-tasks:17.0.0'
57+
58+
implementation 'com.android.support:customtabs:28.0.0'
59+
implementation "androidx.browser:browser:1.3.0"
60+
}

firebase-app-distribution/gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313
# limitations under the License.
1414

1515
version=0.0.1
16+
android.useAndroidX=true
17+
android.enableJetifier=true

firebase-app-distribution/src/main/AndroidManifest.xml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,16 @@
2424
android:name="com.google.firebase.components:com.google.firebase.appdistribution.FirebaseAppDistributionRegistrar"
2525
android:value="com.google.firebase.components.ComponentRegistrar" />
2626
</service>
27+
28+
<activity android:name=".SignInResultActivity" android:exported="true">
29+
<intent-filter>
30+
<action android:name="android.intent.action.VIEW"/>
31+
<category android:name="android.intent.category.DEFAULT"/>
32+
<category android:name="android.intent.category.BROWSABLE"/>
33+
34+
<data android:scheme="appdistribution-${applicationId}" android:host="authredirect" />
35+
36+
</intent-filter>
37+
</activity>
2738
</application>
28-
</manifest>
39+
</manifest>

firebase-app-distribution/src/main/java/com/google/firebase/appdistribution/FirebaseAppDistribution.java

Lines changed: 239 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,69 @@
1414

1515
package com.google.firebase.appdistribution;
1616

17+
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_CANCELED;
18+
import static com.google.firebase.appdistribution.FirebaseAppDistributionException.Status.AUTHENTICATION_FAILURE;
19+
20+
import android.app.Activity;
21+
import android.app.AlertDialog;
22+
import android.app.Application;
23+
import android.content.Context;
24+
import android.content.DialogInterface;
25+
import android.content.Intent;
26+
import android.content.pm.ResolveInfo;
27+
import android.net.Uri;
28+
import android.os.Bundle;
29+
import android.util.Log;
1730
import androidx.annotation.NonNull;
31+
import androidx.annotation.VisibleForTesting;
32+
import androidx.browser.customtabs.CustomTabsIntent;
33+
import com.google.android.gms.common.internal.Preconditions;
34+
import com.google.android.gms.tasks.CancellationTokenSource;
35+
import com.google.android.gms.tasks.OnFailureListener;
36+
import com.google.android.gms.tasks.OnSuccessListener;
1837
import com.google.android.gms.tasks.Task;
38+
import com.google.android.gms.tasks.TaskCompletionSource;
1939
import com.google.android.gms.tasks.Tasks;
2040
import com.google.firebase.FirebaseApp;
41+
import com.google.firebase.installations.FirebaseInstallationsApi;
42+
import java.util.List;
43+
import org.jetbrains.annotations.NotNull;
44+
import org.jetbrains.annotations.Nullable;
2145

22-
public class FirebaseAppDistribution {
46+
public class FirebaseAppDistribution implements Application.ActivityLifecycleCallbacks {
2347

2448
private final FirebaseApp firebaseApp;
49+
private final FirebaseInstallationsApi firebaseInstallationsApi;
50+
private static final String TAG = "FirebaseAppDistribution";
51+
private Activity currentActivity;
52+
@VisibleForTesting private boolean currentlySigningIn = false;
53+
private TaskCompletionSource<Void> signInTaskCompletionSource = null;
54+
private CancellationTokenSource signInCancellationSource;
2555

2656
/** Constructor for FirebaseAppDistribution */
27-
FirebaseAppDistribution(@NonNull FirebaseApp firebaseApp) {
57+
public FirebaseAppDistribution(
58+
@NonNull FirebaseApp firebaseApp,
59+
@NonNull FirebaseInstallationsApi firebaseInstallationsApi) {
2860
this.firebaseApp = firebaseApp;
61+
this.firebaseInstallationsApi = firebaseInstallationsApi;
2962
}
3063

31-
/** @return a FirebaseInstallationsApi instance */
64+
/** @return a FirebaseAppDistribution instance */
3265
@NonNull
3366
public static FirebaseAppDistribution getInstance() {
34-
return new FirebaseAppDistribution(FirebaseApp.getInstance());
67+
return getInstance(FirebaseApp.getInstance());
68+
}
69+
70+
/**
71+
* Returns the {@link FirebaseAppDistribution} initialized with a custom {@link FirebaseApp}.
72+
*
73+
* @param app a custom {@link FirebaseApp}
74+
* @return a {@link FirebaseAppDistribution} instance
75+
*/
76+
@NonNull
77+
public static FirebaseAppDistribution getInstance(@NonNull FirebaseApp app) {
78+
Preconditions.checkArgument(app != null, "Null is not a valid value of FirebaseApp.");
79+
return (FirebaseAppDistribution) app.get(FirebaseAppDistribution.class);
3580
}
3681

3782
/**
@@ -44,7 +89,27 @@ public static FirebaseAppDistribution getInstance() {
4489
*/
4590
@NonNull
4691
public Task<AppDistributionRelease> updateToLatestRelease() {
47-
return Tasks.forResult(null);
92+
93+
TaskCompletionSource<AppDistributionRelease> taskCompletionSource =
94+
new TaskCompletionSource<>();
95+
96+
signInTester()
97+
.addOnSuccessListener(
98+
new OnSuccessListener<Void>() {
99+
@Override
100+
public void onSuccess(Void unused) {
101+
taskCompletionSource.setResult(null);
102+
}
103+
})
104+
.addOnFailureListener(
105+
new OnFailureListener() {
106+
@Override
107+
public void onFailure(@NonNull @NotNull Exception e) {
108+
taskCompletionSource.setException(e);
109+
}
110+
});
111+
112+
return taskCompletionSource.getTask();
48113
}
49114

50115
/**
@@ -62,18 +127,126 @@ public Task<AppDistributionRelease> checkForUpdate() {
62127
* starts an installation If the latest release is an AAB, directs the tester to the Play app to
63128
* complete the download and installation.
64129
*
65-
* @throws an {@link Status.UPDATE_NOT_AVAILABLE_ERROR} exception if no new release is cached from
66-
* checkForUpdate
130+
* @throws FirebaseAppDistributionException with UPDATE_NOT_AVAIALBLE exception if no new release
131+
* is cached from checkForUpdate
67132
*/
68133
@NonNull
69134
public UpdateTask updateApp() {
70135
return (UpdateTask) Tasks.forResult(new UpdateState(0, 0, UpdateStatus.PENDING));
71136
}
72137

138+
private boolean supportsCustomTabs(Context context) {
139+
Intent customTabIntent = new Intent("android.support.customtabs.action.CustomTabsService");
140+
customTabIntent.setPackage("com.android.chrome");
141+
List<ResolveInfo> resolveInfos =
142+
context.getPackageManager().queryIntentServices(customTabIntent, 0);
143+
return resolveInfos != null && !resolveInfos.isEmpty();
144+
}
145+
146+
private static String getApplicationName(Context context) {
147+
try {
148+
return context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
149+
} catch (Exception e) {
150+
Log.e(TAG, "Unable to retrieve App name");
151+
return "";
152+
}
153+
}
154+
155+
private void openSignInFlowInBrowser(Uri uri) {
156+
currentlySigningIn = true;
157+
if (supportsCustomTabs(firebaseApp.getApplicationContext())) {
158+
// If we can launch a chrome view, try that.
159+
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
160+
Intent intent = customTabsIntent.intent;
161+
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
162+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
163+
customTabsIntent.launchUrl(currentActivity, uri);
164+
165+
} else {
166+
// If we can't launch a chrome view try to launch anything that can handle a URL.
167+
Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
168+
ResolveInfo info = currentActivity.getPackageManager().resolveActivity(browserIntent, 0);
169+
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
170+
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
171+
currentActivity.startActivity(browserIntent);
172+
}
173+
}
174+
175+
private OnSuccessListener<String> getFidGenerationOnSuccessListener(Context context) {
176+
return new OnSuccessListener<String>() {
177+
@Override
178+
public void onSuccess(String fid) {
179+
Uri uri =
180+
Uri.parse(
181+
String.format(
182+
"https://appdistribution.firebase.google.com/pub/apps/%s/installations/%s/buildalerts?appName=%s&packageName=%s",
183+
firebaseApp.getOptions().getApplicationId(),
184+
fid,
185+
getApplicationName(context),
186+
context.getPackageName()));
187+
openSignInFlowInBrowser(uri);
188+
}
189+
};
190+
}
191+
192+
private AlertDialog getSignInAlertDialog(Context context) {
193+
AlertDialog alertDialog = new AlertDialog.Builder(currentActivity).create();
194+
alertDialog.setTitle(context.getString(R.string.signin_dialog_title));
195+
alertDialog.setMessage(context.getString(R.string.singin_dialog_message));
196+
alertDialog.setButton(
197+
AlertDialog.BUTTON_POSITIVE,
198+
context.getString(R.string.singin_yes_button),
199+
new DialogInterface.OnClickListener() {
200+
@Override
201+
public void onClick(DialogInterface dialogInterface, int i) {
202+
firebaseInstallationsApi
203+
.getId()
204+
.addOnSuccessListener(getFidGenerationOnSuccessListener(context))
205+
.addOnFailureListener(
206+
new OnFailureListener() {
207+
@Override
208+
public void onFailure(@NonNull @NotNull Exception e) {
209+
setSignInTaskCompletionError(
210+
new FirebaseAppDistributionException(AUTHENTICATION_FAILURE));
211+
}
212+
});
213+
}
214+
});
215+
alertDialog.setButton(
216+
AlertDialog.BUTTON_NEGATIVE,
217+
context.getString(R.string.singin_no_button),
218+
new DialogInterface.OnClickListener() {
219+
@Override
220+
public void onClick(DialogInterface dialogInterface, int i) {
221+
setSignInTaskCompletionError(
222+
new FirebaseAppDistributionException(AUTHENTICATION_CANCELED));
223+
dialogInterface.dismiss();
224+
}
225+
});
226+
return alertDialog;
227+
}
228+
73229
/** Signs in the App Distribution tester. Presents the tester with a Google sign in UI */
74230
@NonNull
75231
public Task<Void> signInTester() {
76-
return Tasks.forResult(null);
232+
if (signInTaskCompletionSource != null && !signInTaskCompletionSource.getTask().isComplete()) {
233+
signInCancellationSource.cancel();
234+
}
235+
236+
signInCancellationSource = new CancellationTokenSource();
237+
signInTaskCompletionSource = new TaskCompletionSource<>(signInCancellationSource.getToken());
238+
239+
Context context = firebaseApp.getApplicationContext();
240+
AlertDialog alertDialog = getSignInAlertDialog(context);
241+
alertDialog.show();
242+
243+
return signInTaskCompletionSource.getTask();
244+
}
245+
246+
private void setSignInTaskCompletionError(FirebaseAppDistributionException e) {
247+
if (signInTaskCompletionSource != null && !signInTaskCompletionSource.getTask().isComplete()) {
248+
signInTaskCompletionSource.setException(e);
249+
}
77250
}
78251

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

85258
/** Signs out the App Distribution tester */
86259
public void signOutTester() {}
260+
261+
@Override
262+
public void onActivityCreated(
263+
@NonNull @NotNull Activity activity, @androidx.annotation.Nullable @Nullable Bundle bundle) {
264+
Log.d(TAG, "Created activity: " + activity.getClass().getName());
265+
// if signinactivity is created, sign-in was succesful
266+
if (currentlySigningIn && activity instanceof SignInResultActivity) {
267+
currentlySigningIn = false;
268+
signInTaskCompletionSource.setResult(null);
269+
}
270+
}
271+
272+
@Override
273+
public void onActivityStarted(@NonNull @NotNull Activity activity) {
274+
Log.d(TAG, "Started activity: " + activity.getClass().getName());
275+
}
276+
277+
@Override
278+
public void onActivityResumed(@NonNull @NotNull Activity activity) {
279+
Log.d(TAG, "Resumed activity: " + activity.getClass().getName());
280+
281+
// signInActivity is only opened after successful redirection from signIn flow,
282+
// should not be treated as reentering the app
283+
if (activity instanceof SignInResultActivity) {
284+
return;
285+
}
286+
287+
// throw error if app reentered during signin
288+
if (currentlySigningIn) {
289+
currentlySigningIn = false;
290+
setSignInTaskCompletionError(new FirebaseAppDistributionException(AUTHENTICATION_FAILURE));
291+
}
292+
this.currentActivity = activity;
293+
}
294+
295+
@Override
296+
public void onActivityPaused(@NonNull @NotNull Activity activity) {
297+
Log.d(TAG, "Paused activity: " + activity.getClass().getName());
298+
}
299+
300+
@Override
301+
public void onActivityStopped(@NonNull @NotNull Activity activity) {
302+
Log.d(TAG, "Stopped activity: " + activity.getClass().getName());
303+
}
304+
305+
@Override
306+
public void onActivitySaveInstanceState(
307+
@NonNull @NotNull Activity activity, @NonNull @NotNull Bundle bundle) {
308+
Log.d(TAG, "Saved activity: " + activity.getClass().getName());
309+
}
310+
311+
@Override
312+
public void onActivityDestroyed(@NonNull @NotNull Activity activity) {
313+
Log.d(TAG, "Destroyed activity: " + activity.getClass().getName());
314+
if (this.currentActivity == activity) {
315+
this.currentActivity = null;
316+
}
317+
}
87318
}

firebase-app-distribution/src/main/java/com/google/firebase/appdistribution/FirebaseAppDistributionException.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.appdistribution;
1616

1717
import androidx.annotation.NonNull;
18+
import androidx.annotation.Nullable;
1819
import com.google.firebase.FirebaseException;
1920

2021
/** Possible exceptions thrown in FirebaseAppDistribution */
@@ -55,14 +56,19 @@ public enum Status {
5556
}
5657

5758
@NonNull private final Status status;
58-
@NonNull private final AppDistributionRelease release;
59+
@Nullable private final AppDistributionRelease release;
5960

6061
FirebaseAppDistributionException(
61-
@NonNull Status status, @NonNull AppDistributionRelease release) {
62+
@NonNull Status status, @Nullable AppDistributionRelease release) {
6263
this.status = status;
6364
this.release = release;
6465
}
6566

67+
FirebaseAppDistributionException(@NonNull Status status) {
68+
this.status = status;
69+
this.release = null;
70+
}
71+
6672
/** Get cached release when error was thrown */
6773
@NonNull
6874
public AppDistributionRelease getRelease() {

0 commit comments

Comments
 (0)