14
14
15
15
package com .google .firebase .appdistribution ;
16
16
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 ;
17
30
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 ;
18
37
import com .google .android .gms .tasks .Task ;
38
+ import com .google .android .gms .tasks .TaskCompletionSource ;
19
39
import com .google .android .gms .tasks .Tasks ;
20
40
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 ;
21
45
22
- public class FirebaseAppDistribution {
46
+ public class FirebaseAppDistribution implements Application . ActivityLifecycleCallbacks {
23
47
24
48
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 ;
25
55
26
56
/** Constructor for FirebaseAppDistribution */
27
- FirebaseAppDistribution (@ NonNull FirebaseApp firebaseApp ) {
57
+ public FirebaseAppDistribution (
58
+ @ NonNull FirebaseApp firebaseApp ,
59
+ @ NonNull FirebaseInstallationsApi firebaseInstallationsApi ) {
28
60
this .firebaseApp = firebaseApp ;
61
+ this .firebaseInstallationsApi = firebaseInstallationsApi ;
29
62
}
30
63
31
- /** @return a FirebaseInstallationsApi instance */
64
+ /** @return a FirebaseAppDistribution instance */
32
65
@ NonNull
33
66
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 );
35
80
}
36
81
37
82
/**
@@ -44,7 +89,27 @@ public static FirebaseAppDistribution getInstance() {
44
89
*/
45
90
@ NonNull
46
91
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 ();
48
113
}
49
114
50
115
/**
@@ -62,18 +127,126 @@ public Task<AppDistributionRelease> checkForUpdate() {
62
127
* starts an installation If the latest release is an AAB, directs the tester to the Play app to
63
128
* complete the download and installation.
64
129
*
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
67
132
*/
68
133
@ NonNull
69
134
public UpdateTask updateApp () {
70
135
return (UpdateTask ) Tasks .forResult (new UpdateState (0 , 0 , UpdateStatus .PENDING ));
71
136
}
72
137
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
+
73
229
/** Signs in the App Distribution tester. Presents the tester with a Google sign in UI */
74
230
@ NonNull
75
231
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
+ }
77
250
}
78
251
79
252
/** Returns true if the App Distribution tester is signed in */
@@ -84,4 +257,62 @@ public boolean isTesterSignedIn() {
84
257
85
258
/** Signs out the App Distribution tester */
86
259
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
+ }
87
318
}
0 commit comments