diff --git a/android/build.gradle b/android/build.gradle index 47fcc4e03..aa44779c6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -40,10 +40,18 @@ if (localPropFile.exists()) { } android { - compileSdkVersion 34 namespace "com.thebluealliance.androidclient" signingConfigs { + if (localProps.containsKey("debug.key")) { + debug { + storeFile file(localProps.getProperty("debug.key")) + storePassword localProps.getProperty("debug.key.password") + keyAlias localProps.getProperty("debug.key.alias") + keyPassword localProps.getProperty("debug.key.aliasPass") + } + } + release { storeFile file(localProps.getProperty("release.key", "somefile.jks")) storePassword localProps.getProperty("release.key.password", "notRealPassword") @@ -94,8 +102,9 @@ android { defaultConfig { applicationId "com.thebluealliance.androidclient" + compileSdk 34 minSdkVersion 19 - targetSdkVersion 31 + targetSdkVersion 34 versionCode versionNum versionName version.toString() multiDexEnabled true @@ -240,7 +249,7 @@ dependencies { // See http://developer.android.com/google/play-services/setup.html implementation 'com.google.guava:guava:24.1-jre' // implementation "com.google.firebase:firebase-bom:31.2.3" - implementation "com.google.android.gms:play-services-base:18.2.0" + implementation "com.google.android.gms:play-services-base:18.3.0" implementation "com.google.android.gms:play-services-analytics:18.0.4" implementation "com.google.firebase:firebase-messaging:23.4.0" implementation "com.google.android.gms:play-services-auth:20.7.0" diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 22ed2e90c..c2b19a377 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -14,6 +14,8 @@ + + diff --git a/android/src/main/java/com/thebluealliance/androidclient/activities/LaunchActivity.java b/android/src/main/java/com/thebluealliance/androidclient/activities/LaunchActivity.java index f4c4d8a1f..75814f039 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/activities/LaunchActivity.java +++ b/android/src/main/java/com/thebluealliance/androidclient/activities/LaunchActivity.java @@ -1,16 +1,22 @@ package com.thebluealliance.androidclient.activities; +import android.Manifest; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; import com.thebluealliance.androidclient.BuildConfig; import com.thebluealliance.androidclient.Constants; import com.thebluealliance.androidclient.TbaLogger; import com.thebluealliance.androidclient.Utilities; +import com.thebluealliance.androidclient.accounts.AccountController; import com.thebluealliance.androidclient.background.RecreateSearchIndexes; import com.thebluealliance.androidclient.background.firstlaunch.LoadTBADataWorker; import com.thebluealliance.androidclient.datafeed.status.TBAStatusController; @@ -28,6 +34,9 @@ public class LaunchActivity extends AppCompatActivity { @Inject Cache mDatafeedCache; @Inject SharedPreferences mSharedPreferences; + @Inject + AccountController mAccountController; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -35,6 +44,10 @@ protected void onCreate(Bundle savedInstanceState) { // Create intent to launch data download activity Intent redownloadIntent = new Intent(this, RedownloadActivity.class); boolean redownload = checkDataRedownload(redownloadIntent); + boolean needsToRequestNotificationPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED + && mAccountController.isMyTbaEnabled(); + if (mSharedPreferences.getBoolean(Constants.ALL_DATA_LOADED_KEY, false) && !redownload) { if (Intent.ACTION_VIEW.equals(getIntent().getAction())) { Uri data = getIntent().getData(); @@ -47,19 +60,21 @@ protected void onCreate(Bundle savedInstanceState) { if (intent != null) { startActivity(intent); finish(); - return; } else { goToHome(); - return; } } else { goToHome(); - return; } } else { goToHome(); - return; } + } else if (needsToRequestNotificationPermission) { + // Starting with Android 33, we need to request notification permissions + Toast.makeText(this, "Notification permission not found!", Toast.LENGTH_LONG).show(); + Intent mytbaIntent = new Intent(this, MyTBAOnboardingActivity.class); + startActivity(mytbaIntent); + finish(); } else if (redownload) { // Start redownload activity startActivity(redownloadIntent); diff --git a/android/src/main/java/com/thebluealliance/androidclient/activities/MyTBAOnboardingActivity.java b/android/src/main/java/com/thebluealliance/androidclient/activities/MyTBAOnboardingActivity.java index ed0094d09..cfd4c1235 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/activities/MyTBAOnboardingActivity.java +++ b/android/src/main/java/com/thebluealliance/androidclient/activities/MyTBAOnboardingActivity.java @@ -1,39 +1,37 @@ package com.thebluealliance.androidclient.activities; -import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.view.View; -import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.viewpager.widget.ViewPager; import com.thebluealliance.androidclient.R; -import com.thebluealliance.androidclient.TbaLogger; -import com.thebluealliance.androidclient.accounts.AccountController; -import com.thebluealliance.androidclient.auth.AuthProvider; import com.thebluealliance.androidclient.databinding.ActivityMytbaOnboardingBinding; +import com.thebluealliance.androidclient.mytba.MyTbaOnboardingController; import com.thebluealliance.androidclient.views.MyTBAOnboardingViewPager; import javax.inject.Inject; -import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class MyTBAOnboardingActivity extends AppCompatActivity - implements MyTBAOnboardingViewPager.Callbacks{ + implements MyTBAOnboardingViewPager.Callbacks, + MyTbaOnboardingController.MyTbaOnboardingCallbacks { private static final String MYTBA_LOGIN_COMPLETE = "mytba_login_complete"; - private static final int SIGNIN_CODE = 254; private ActivityMytbaOnboardingBinding mBinding; private boolean isMyTBALoginComplete = false; - @Inject @Named("firebase_auth") AuthProvider mAuthProvider; - @Inject AccountController mAccountController; + @Inject + MyTbaOnboardingController mMyTbaOnboardingController; @Override protected void onCreate(Bundle savedInstanceState) { @@ -67,49 +65,57 @@ public void onPageSelected(int position) { mBinding.cancelButton.setOnClickListener((View view) -> finish()); mBinding.continueButton.setOnClickListener(this::onContinueClick); + + mMyTbaOnboardingController.registerActivityCallbacks(this, this); } private void updateContinueButtonText() { - if (mBinding.mytbaViewPager.isOnLoginPage()) { + if (mBinding.mytbaViewPager.isDone()) { mBinding.continueButtonLabel.setText(R.string.finish_caps); + } else if (mBinding.mytbaViewPager.isOnLoginPage()) { + mBinding.continueButtonLabel.setText(R.string.continue_caps); } else { mBinding.continueButtonLabel.setText(R.string.skip_intro_caps); } } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == SIGNIN_CODE) { - if (resultCode == RESULT_OK) { - mAuthProvider.userFromSignInResult(requestCode, resultCode, data) - .subscribe(user -> { - TbaLogger.d("User logged in: " + user.getEmail()); - mBinding.mytbaViewPager.setUpForLoginSuccess(); - isMyTBALoginComplete = true; - mAccountController.onAccountConnect(MyTBAOnboardingActivity.this, user); - }, throwable -> { - TbaLogger.e("Error logging in"); - throwable.printStackTrace(); - mAccountController.setMyTbaEnabled(false); - }); - } else if (resultCode == RESULT_CANCELED) { - Toast.makeText(this, "Sign in canceled", Toast.LENGTH_LONG).show(); - mBinding.mytbaViewPager.setUpForLoginPrompt(); - } + public void onLoginSuccess() { + mBinding.mytbaViewPager.setUpForLoginSuccess(); + isMyTBALoginComplete = true; + + if (!mBinding.mytbaViewPager.isDone()) { + mBinding.mytbaViewPager.advance(); } } @Override - protected void onSaveInstanceState(Bundle outState) { + public void onLoginFailed() { + mBinding.mytbaViewPager.setUpForLoginPrompt(); + } + + @Override + public void onPermissionResult(boolean isGranted) { + mBinding.mytbaViewPager.setUpForPermissionResult(isGranted); + + if (isGranted) { + finish(); + } + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(MYTBA_LOGIN_COMPLETE, isMyTBALoginComplete); } private void onContinueClick(View view) { - if (mBinding.mytbaViewPager.isOnLoginPage()) { + if (mBinding.mytbaViewPager.isDone()) { // On the last page, the "continue" button turns into a "finish" button finish(); + } else if (mBinding.mytbaViewPager.isOnLoginPage()) { + // If there is an additional page after login, go there + mBinding.mytbaViewPager.advance(); } else { // On other pages, the "continue" button becomes a "skip intro" button mBinding.mytbaViewPager.scrollToLoginPage(); @@ -118,12 +124,12 @@ private void onContinueClick(View view) { @Override public void onSignInButtonClicked() { - Intent signInIntent = mAuthProvider.buildSignInIntent(); - if (signInIntent != null) { - startActivityForResult(signInIntent, SIGNIN_CODE); - } else { - Toast.makeText(this, R.string.mytba_no_signin_intent, Toast.LENGTH_SHORT).show(); - TbaLogger.e("Unable to get login Intent"); - } + mMyTbaOnboardingController.launchSignIn(this); + } + + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + @Override + public void onEnableNotificationsButtonClicked() { + mMyTbaOnboardingController.launchNotificationPermissionRequest(this); } } diff --git a/android/src/main/java/com/thebluealliance/androidclient/activities/OnboardingActivity.java b/android/src/main/java/com/thebluealliance/androidclient/activities/OnboardingActivity.java index c5223b188..92240bc51 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/activities/OnboardingActivity.java +++ b/android/src/main/java/com/thebluealliance/androidclient/activities/OnboardingActivity.java @@ -7,11 +7,12 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.view.View; import android.view.WindowManager; -import android.widget.Toast; +import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; @@ -19,12 +20,11 @@ import com.thebluealliance.androidclient.Constants; import com.thebluealliance.androidclient.R; import com.thebluealliance.androidclient.TbaLogger; -import com.thebluealliance.androidclient.accounts.AccountController; import com.thebluealliance.androidclient.adapters.FirstLaunchPagerAdapter; -import com.thebluealliance.androidclient.auth.AuthProvider; import com.thebluealliance.androidclient.background.firstlaunch.LoadTBADataWorker; import com.thebluealliance.androidclient.databinding.ActivityOnboardingBinding; import com.thebluealliance.androidclient.helpers.ConnectionDetector; +import com.thebluealliance.androidclient.mytba.MyTbaOnboardingController; import com.thebluealliance.androidclient.views.MyTBAOnboardingViewPager; import org.jetbrains.annotations.NotNull; @@ -33,14 +33,14 @@ import java.util.UUID; import javax.inject.Inject; -import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; @AndroidEntryPoint public class OnboardingActivity extends AppCompatActivity implements LoadTBADataWorker.LoadTBADataCallbacks, - MyTBAOnboardingViewPager.Callbacks { + MyTBAOnboardingViewPager.Callbacks, + MyTbaOnboardingController.MyTbaOnboardingCallbacks { private static final String CURRENT_LOADING_MESSAGE_KEY = "current_loading_message"; private static final String LOADING_COMPLETE = "loading_complete"; @@ -48,19 +48,20 @@ public class OnboardingActivity extends AppCompatActivity private static final String WELCOME_PAGER_STATE = "welcome_pager_state"; private static final String MYTBA_PAGER_STATE = "mytba_pager_state"; private static final String LOAD_TASK_UUID = "load_task_uuid"; - private static final int SIGNIN_CODE = 254; private ActivityOnboardingBinding mBinding; private String currentLoadingMessage = ""; private boolean isDataFinishedLoading = false; private boolean isMyTBALoginComplete = false; - private @Nullable UUID dataLoadTask = null; - @Inject @Named("firebase_auth") AuthProvider mAuthProvider; - @Inject AccountController mAccountController; + private boolean didGrantNotificationPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU; + private @Nullable UUID dataLoadTask = null; @Inject SharedPreferences mPreferences; + @Inject + MyTbaOnboardingController mMyTbaOnboardingController; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -121,12 +122,14 @@ protected void onCreate(Bundle savedInstanceState) { mBinding.mytbaViewPager.setUpForLoginPrompt(); } - mBinding.continueToEnd.setOnClickListener(this::onContinueToEndClient); + mBinding.continueToEnd.setOnClickListener(this::onContinueToEndClick); mBinding.welcomeNextPage.setOnClickListener((View view) -> beginLoadingIfConnected()); mBinding.finish.setOnClickListener((View view) -> { startActivity(new Intent(this, HomeActivity.class)); finish(); }); + + mMyTbaOnboardingController.registerActivityCallbacks(this, this); } @Override @@ -148,33 +151,35 @@ protected void onSaveInstanceState(@NotNull Bundle outState) { } @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (requestCode == SIGNIN_CODE) { - if (resultCode == RESULT_OK) { - mAuthProvider.userFromSignInResult(requestCode, resultCode, data) - .subscribe(user -> { - TbaLogger.d("User logged in: " + user.getEmail()); - mBinding.mytbaViewPager.setUpForLoginSuccess(); - isMyTBALoginComplete = true; - mAccountController.onAccountConnect(OnboardingActivity.this, user); - }, throwable -> { - TbaLogger.e("Error logging in"); - throwable.printStackTrace(); - mAccountController.setMyTbaEnabled(false); - }); - } else if (resultCode == RESULT_CANCELED) { - Toast.makeText(this, "Sign in canceled", Toast.LENGTH_LONG).show(); - mBinding.mytbaViewPager.setUpForLoginPrompt(); - } + public void onLoginSuccess() { + mBinding.mytbaViewPager.setUpForLoginSuccess(); + isMyTBALoginComplete = true; + + if (!mBinding.mytbaViewPager.isDone()) { + mBinding.mytbaViewPager.advance(); + } + } + + @Override + public void onLoginFailed() { + mBinding.mytbaViewPager.setUpForLoginPrompt(); + } + + @Override + public void onPermissionResult(boolean isGranted) { + didGrantNotificationPermission = isGranted; + mBinding.mytbaViewPager.setUpForPermissionResult(isGranted); + + if (isGranted) { + mBinding.viewPager.setCurrentItem(2); } } - private void onContinueToEndClient(View view) { + private void onContinueToEndClick(View view) { // If myTBA hasn't been activated yet, prompt the user one last time to sign in - if (!mBinding.mytbaViewPager.isOnLoginPage()) { + if (mBinding.mytbaViewPager.isBeforeLoginPage()) { mBinding.mytbaViewPager.scrollToLoginPage(); - } else if (!isMyTBALoginComplete) { + } else if (mBinding.mytbaViewPager.isOnLoginPage() && !isMyTBALoginComplete) { // Only show this dialog if play services are actually available new AlertDialog.Builder(this) .setTitle(getString(R.string.mytba_prompt_title)) @@ -189,6 +194,22 @@ private void onContinueToEndClient(View view) { mBinding.viewPager.setCurrentItem(2); dialog.dismiss(); }).create().show(); + } else if (mBinding.mytbaViewPager.isOnNotificationPermissionPage() && !didGrantNotificationPermission) { + new AlertDialog.Builder(this) + .setTitle(getString(R.string.mytba_prompt_notif_permission)) + .setMessage(getString(R.string.mytba_prompt_notif_permission_message)) + .setCancelable(false) + .setPositiveButton(R.string.mytba_prompt_yes, (dialog, dialogId) -> { + // Do nothing; allow user to enable myTBA + dialog.dismiss(); + }) + .setNegativeButton(R.string.mytba_prompt_cancel, (dialog, dialogId) -> { + // Scroll to the last page + mBinding.viewPager.setCurrentItem(2); + dialog.dismiss(); + }).create().show(); + } else if (!mBinding.mytbaViewPager.isDone()) { + mBinding.mytbaViewPager.advance(); } else { mBinding.viewPager.setCurrentItem(2); } @@ -360,12 +381,12 @@ public void onProgressUpdate(LoadTBADataWorker.LoadProgressInfo info) { @Override public void onSignInButtonClicked() { - Intent signInIntent = mAuthProvider.buildSignInIntent(); - if (signInIntent != null) { - startActivityForResult(signInIntent, SIGNIN_CODE); - } else { - Toast.makeText(this, R.string.mytba_no_signin_intent, Toast.LENGTH_SHORT).show(); - TbaLogger.e("Unable to get login Intent"); - } + mMyTbaOnboardingController.launchSignIn(this); + } + + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + @Override + public void onEnableNotificationsButtonClicked() { + mMyTbaOnboardingController.launchNotificationPermissionRequest(this); } } diff --git a/android/src/main/java/com/thebluealliance/androidclient/activities/settings/NotificationSettingsActivity.java b/android/src/main/java/com/thebluealliance/androidclient/activities/settings/NotificationSettingsActivity.java index ff1b324d6..84cfbcbce 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/activities/settings/NotificationSettingsActivity.java +++ b/android/src/main/java/com/thebluealliance/androidclient/activities/settings/NotificationSettingsActivity.java @@ -1,12 +1,19 @@ package com.thebluealliance.androidclient.activities.settings; -import android.app.Fragment; +import android.Manifest; +import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceFragment; import android.view.View; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SwitchPreference; import com.thebluealliance.androidclient.R; @@ -18,24 +25,45 @@ protected void onCreate(Bundle savedInstanceState) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); - Fragment existingFragment = getFragmentManager().findFragmentById(android.R.id.content); + Fragment existingFragment = getSupportFragmentManager().findFragmentById(android.R.id.content); if (existingFragment == null || !existingFragment.getClass().equals(NotificationSettingsFragment.class)) { // Display the fragment as the main content. - getFragmentManager().beginTransaction() + getSupportFragmentManager().beginTransaction() .replace(android.R.id.content, new NotificationSettingsFragment()) .commit(); } } - public static class NotificationSettingsFragment extends PreferenceFragment { + public static class NotificationSettingsFragment extends PreferenceFragmentCompat { + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { + setPreferencesFromResource(R.xml.notification_preferences, rootKey); + } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.notification_preferences); if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) { addPreferencesFromResource(R.xml.notification_preferences_lollipop); } + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { + addPreferencesFromResource(R.xml.notification_preferences_tiramisu); + + SwitchPreference notifPermissionPref = findPreference("notification_permission_enabled"); + ActivityResultLauncher notificationPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), result -> notifPermissionPref.setChecked(result)); + boolean hasPermission = ContextCompat.checkSelfPermission(getContext(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED; + notifPermissionPref.setChecked(hasPermission); + notifPermissionPref.setOnPreferenceChangeListener((preference, newValue) -> { + if (!((boolean) newValue)) { + getContext().revokeSelfPermissionOnKill(Manifest.permission.POST_NOTIFICATIONS); + } else { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } + return true; + }); + } } @Override diff --git a/android/src/main/java/com/thebluealliance/androidclient/activities/settings/SettingsActivity.java b/android/src/main/java/com/thebluealliance/androidclient/activities/settings/SettingsActivity.java index 6de2e8342..3db3b85cf 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/activities/settings/SettingsActivity.java +++ b/android/src/main/java/com/thebluealliance/androidclient/activities/settings/SettingsActivity.java @@ -121,18 +121,19 @@ public void onCreate(Bundle savedInstanceState) { Preference tbaLink = findPreference("tba_link"); tbaLink.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.thebluealliance.com"))); - final SwitchPreference mytbaEnabled = (SwitchPreference) findPreference("mytba_enabled"); + final SwitchPreference mytbaEnabled = findPreference("mytba_enabled"); final Activity activity = getActivity(); if (mytbaEnabled != null) { mytbaEnabled.setChecked(mAccountController.isMyTbaEnabled()); - mytbaEnabled.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean enabled = mAccountController.isMyTbaEnabled(); - TbaLogger.d("myTBA is: " + enabled); + mytbaEnabled.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = mAccountController.isMyTbaEnabled(); + TbaLogger.d("myTBA is: " + enabled); + if (!enabled) { activity.startActivity(new Intent(getActivity(), MyTBAOnboardingActivity.class)); - return true; + } else { + mAccountController.setMyTbaEnabled(false); } + return true; }); } diff --git a/android/src/main/java/com/thebluealliance/androidclient/adapters/MyTBAOnboardingPagerAdapter.java b/android/src/main/java/com/thebluealliance/androidclient/adapters/MyTBAOnboardingPagerAdapter.java index 692ae3d6b..b8a9168d5 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/adapters/MyTBAOnboardingPagerAdapter.java +++ b/android/src/main/java/com/thebluealliance/androidclient/adapters/MyTBAOnboardingPagerAdapter.java @@ -1,19 +1,27 @@ package com.thebluealliance.androidclient.adapters; +import android.os.Build; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.RequiresApi; import androidx.viewpager.widget.PagerAdapter; import com.thebluealliance.androidclient.R; public class MyTBAOnboardingPagerAdapter extends PagerAdapter { - private int mCount = 5; - private ViewGroup mView; + private final int mCount; + private final ViewGroup mView; public MyTBAOnboardingPagerAdapter(ViewGroup view) { mView = view; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // Starting with API 33, there is an additional page to request notification permissions + mCount = 6; + } else { + mCount = 5; + } } @Override @@ -21,9 +29,21 @@ public int getCount() { return mCount; } + public int getLoginPageId() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return mCount - 2; + } else { + return mCount - 1; + } + } + + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + public int getNotificationPermissionPageId() { + return mCount - 1; + } + @Override public Object instantiateItem(ViewGroup collection, int position) { - int resId = 0; switch (position) { case 0: @@ -41,6 +61,9 @@ public Object instantiateItem(ViewGroup collection, int position) { case 4: resId = R.id.page_five; break; + case 5: + resId = R.id.page_six; + break; } return mView.findViewById(resId); } diff --git a/android/src/main/java/com/thebluealliance/androidclient/auth/AuthModule.java b/android/src/main/java/com/thebluealliance/androidclient/auth/AuthModule.java index 7ea29660d..ec56bc16e 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/auth/AuthModule.java +++ b/android/src/main/java/com/thebluealliance/androidclient/auth/AuthModule.java @@ -8,6 +8,7 @@ import com.thebluealliance.androidclient.accounts.AccountModule; import com.thebluealliance.androidclient.auth.firebase.FirebaseAuthProvider; import com.thebluealliance.androidclient.auth.google.GoogleAuthProvider; +import com.thebluealliance.androidclient.mytba.MyTbaOnboardingController; import javax.annotation.Nullable; import javax.inject.Named; @@ -46,4 +47,10 @@ public AuthProvider provideFirebaseAuthProvider(@Nullable FirebaseAuth firebaseA GoogleAuthProvider googleAuthProvider) { return new FirebaseAuthProvider(firebaseAuth, googleAuthProvider); } + + @Provides + public MyTbaOnboardingController provideMyTbaOnbordingController(@Named("firebase_auth") AuthProvider authProvider, + AccountController accountController) { + return new MyTbaOnboardingController(authProvider, accountController); + } } diff --git a/android/src/main/java/com/thebluealliance/androidclient/auth/AuthProvider.java b/android/src/main/java/com/thebluealliance/androidclient/auth/AuthProvider.java index b09feb5ea..44a3a4d37 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/auth/AuthProvider.java +++ b/android/src/main/java/com/thebluealliance/androidclient/auth/AuthProvider.java @@ -8,10 +8,6 @@ public interface AuthProvider { - void onStart(); - - void onStop(); - /** * Check if a user is currently signed in */ @@ -32,7 +28,7 @@ public interface AuthProvider { @Nullable Intent buildSignInIntent(); - Observable userFromSignInResult(int requestCode, int resultCode, Intent data); + Observable userFromSignInResult(int resultCode, Intent data); Observable signInLegacyUser(); } diff --git a/android/src/main/java/com/thebluealliance/androidclient/auth/firebase/FirebaseAuthProvider.java b/android/src/main/java/com/thebluealliance/androidclient/auth/firebase/FirebaseAuthProvider.java index 81ae46b65..e115514c0 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/auth/firebase/FirebaseAuthProvider.java +++ b/android/src/main/java/com/thebluealliance/androidclient/auth/firebase/FirebaseAuthProvider.java @@ -33,16 +33,6 @@ public FirebaseAuthProvider( mGoogleAuthProvider = googleProvider; } - @Override - public void onStart() { - mGoogleAuthProvider.onStart(); - } - - @Override - public void onStop() { - mGoogleAuthProvider.onStop(); - } - @Override public boolean isUserSignedIn() { return mFirebaseAuth != null && mFirebaseAuth.getCurrentUser() != null; @@ -80,8 +70,8 @@ public Intent buildSignInIntent() { } @Override - public Observable userFromSignInResult(int requestCode, int resultCode, Intent data) { - Observable googleUser = mGoogleAuthProvider.userFromSignInResult(requestCode, resultCode, data); + public Observable userFromSignInResult(int resultCode, Intent data) { + Observable googleUser = mGoogleAuthProvider.userFromSignInResult(resultCode, data); return googleUser.switchMap(user -> { if (mFirebaseAuth == null || !(user instanceof GoogleSignInUser)) { return Observable.empty(); @@ -90,20 +80,15 @@ public Observable userFromSignInResult(int requestCode, int GoogleSignInUser googleSignInUser = (GoogleSignInUser) user; AuthCredential credential = mGoogleAuthProvider .getAuthCredential(googleSignInUser.getIdToken()); - return Observable.create(new Observable.OnSubscribe() { - @Override - public void call(Subscriber subscriber) { - mFirebaseAuth.signInWithCredential(credential) - .addOnCompleteListener(task -> { - if (task.isSuccessful()) { - AuthResult result = task.getResult(); - subscriber.onNext(new FirebaseSignInUser(result.getUser())); - } - subscriber.onCompleted(); - }) - .addOnFailureListener(subscriber::onError); - } - }); + return Observable.create(subscriber -> mFirebaseAuth.signInWithCredential(credential) + .addOnCompleteListener(task -> { + if (task.isSuccessful()) { + AuthResult result = task.getResult(); + subscriber.onNext(new FirebaseSignInUser(result.getUser())); + } + subscriber.onCompleted(); + }) + .addOnFailureListener(subscriber::onError)); }); } diff --git a/android/src/main/java/com/thebluealliance/androidclient/auth/google/GoogleAuthProvider.java b/android/src/main/java/com/thebluealliance/androidclient/auth/google/GoogleAuthProvider.java index e8ff60a19..6d71644ab 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/auth/google/GoogleAuthProvider.java +++ b/android/src/main/java/com/thebluealliance/androidclient/auth/google/GoogleAuthProvider.java @@ -2,18 +2,15 @@ import android.content.Context; import android.content.Intent; -import android.os.Bundle; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import com.google.android.gms.auth.api.Auth; +import com.google.android.gms.auth.api.signin.GoogleSignIn; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.auth.api.signin.GoogleSignInResult; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.OptionalPendingResult; import com.google.firebase.auth.AuthCredential; import com.thebluealliance.androidclient.TbaLogger; import com.thebluealliance.androidclient.accounts.AccountController; @@ -25,14 +22,13 @@ import rx.Observable; @Singleton -public class GoogleAuthProvider implements AuthProvider, - GoogleApiClient.OnConnectionFailedListener, - GoogleApiClient.ConnectionCallbacks -{ +public class GoogleAuthProvider implements AuthProvider { private final Context mContext; private final AccountController mAccountController; - private @Nullable GoogleApiClient mGoogleApiClient; + + private GoogleSignInClient mSignInClient; + private @Nullable GoogleSignInUser mCurrentUser; @Inject @@ -40,51 +36,29 @@ public GoogleAuthProvider(Context context, AccountController accountController) mCurrentUser = null; mAccountController = accountController; mContext = context; - } private void loadGoogleApiClient() { String clientId = mAccountController.getWebClientId(); + TbaLogger.d("Google client id: " + clientId); if (clientId.isEmpty()) { // No client id set in tba.properties, can't continue TbaLogger.w("Oauth client ID not set, can't enable myTBA. See https://goo.gl/Swp5PC " + "for config details"); - mGoogleApiClient = null; + mSignInClient = null; return; } GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestEmail() .requestIdToken(clientId) .build(); - mGoogleApiClient = new GoogleApiClient.Builder(mContext) - .addConnectionCallbacks(this) - .addOnConnectionFailedListener(this) - .addApi(Auth.GOOGLE_SIGN_IN_API, gso) - .build(); + mSignInClient = GoogleSignIn.getClient(mContext, gso); } public AuthCredential getAuthCredential(String idToken) { return com.google.firebase.auth.GoogleAuthProvider.getCredential(idToken, null); } - @Override - public void onStart() { - if (mGoogleApiClient != null - && !mGoogleApiClient.isConnecting() - && !mGoogleApiClient.isConnected()) { - mGoogleApiClient.connect(); - } - } - - @Override - public void onStop() { - if (mGoogleApiClient != null - && !mGoogleApiClient.isConnecting() - && mGoogleApiClient.isConnected()) { - mGoogleApiClient.disconnect(); - } - } - @Override public boolean isUserSignedIn() { return mCurrentUser != null; @@ -97,13 +71,13 @@ public GoogleSignInUser getCurrentUser() { @Nullable @Override public Intent buildSignInIntent() { - if (mGoogleApiClient == null) { + if (mSignInClient == null) { // Lazy load the API client, if needed loadGoogleApiClient(); } - if (mGoogleApiClient != null) { - return Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient); + if (mSignInClient != null) { + return mSignInClient.getSignInIntent(); } // If we still can't get the API client, just give up @@ -111,9 +85,9 @@ public Intent buildSignInIntent() { } @Override - public Observable userFromSignInResult(int requestCode, int resultCode, Intent data) { + public Observable userFromSignInResult(int resultCode, Intent data) { GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data); - boolean success = result.isSuccess(); + boolean success = result != null && result.isSuccess(); TbaLogger.d("Google Sign In Result: " + success); if (success) { mCurrentUser = new GoogleSignInUser(result.getSignInAccount()); @@ -123,40 +97,7 @@ public Observable userFromSignInResult(int requestCode, int re @WorkerThread public Observable signInLegacyUser() { - if (mGoogleApiClient == null) { - TbaLogger.i("Lazy loading Google API Client for legacy sign in"); - loadGoogleApiClient(); - } - if (mGoogleApiClient == null) { - TbaLogger.i("Unable to get API Client for legacy sign in"); - return Observable.empty(); - } - onStart(); - OptionalPendingResult optionalResult = Auth.GoogleSignInApi - .silentSignIn(mGoogleApiClient); - GoogleSignInResult result = optionalResult.await(); - onStop(); - if (result.isSuccess()) { - return Observable.just(new GoogleSignInUser(result.getSignInAccount())); - } else { - TbaLogger.w("Unable to complete legacy sign in: " + result.getStatus().getStatusMessage()); - } + TbaLogger.w("Legacy sign in migration no longer supported"); return Observable.empty(); } - - @Override - public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { - TbaLogger.w("Google API client connection failed"); - TbaLogger.w(connectionResult.getErrorMessage()); - } - - @Override - public void onConnected(@Nullable Bundle bundle) { - TbaLogger.d("Google API client connected"); - } - - @Override - public void onConnectionSuspended(int i) { - - } } diff --git a/android/src/main/java/com/thebluealliance/androidclient/gcm/GCMMessageHandler.java b/android/src/main/java/com/thebluealliance/androidclient/gcm/GCMMessageHandler.java index 5922689ca..af1a67c7c 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/gcm/GCMMessageHandler.java +++ b/android/src/main/java/com/thebluealliance/androidclient/gcm/GCMMessageHandler.java @@ -1,8 +1,10 @@ package com.thebluealliance.androidclient.gcm; +import android.Manifest; import android.app.Notification; import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; @@ -215,6 +217,11 @@ protected void notify(Context c, BaseNotification notification, Notification bui NotificationManagerCompat notificationManager = NotificationManagerCompat.from(c); int id = notification.getNotificationId(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(c, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + TbaLogger.w("Notification permission not granted! Skipping posting notifications..."); + return; + } + setNotificationParams(built, c, notification.getNotificationType(), mPrefs); TbaLogger.i("Notifying: " + id); notificationManager.notify(id, built); diff --git a/android/src/main/java/com/thebluealliance/androidclient/mytba/MyTbaOnboardingController.java b/android/src/main/java/com/thebluealliance/androidclient/mytba/MyTbaOnboardingController.java new file mode 100644 index 000000000..1e0649665 --- /dev/null +++ b/android/src/main/java/com/thebluealliance/androidclient/mytba/MyTbaOnboardingController.java @@ -0,0 +1,97 @@ +package com.thebluealliance.androidclient.mytba; + +import static android.app.Activity.RESULT_CANCELED; +import static android.app.Activity.RESULT_OK; + +import android.Manifest; +import android.content.Intent; +import android.os.Build; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.gms.auth.api.signin.GoogleSignInStatusCodes; +import com.google.android.gms.common.api.Status; +import com.thebluealliance.androidclient.R; +import com.thebluealliance.androidclient.TbaLogger; +import com.thebluealliance.androidclient.accounts.AccountController; +import com.thebluealliance.androidclient.auth.AuthProvider; +import com.thebluealliance.androidclient.auth.User; + +import javax.inject.Inject; +import javax.inject.Named; + +import rx.Observable; + +public class MyTbaOnboardingController { + + final AuthProvider mAuthProvider; + final AccountController mAccountController; + + ActivityResultLauncher mSignInLauncher; + ActivityResultLauncher mNotificationPermissionLauncher; + + @Inject + public MyTbaOnboardingController( + @Named("firebase_auth") AuthProvider authProvider, + AccountController accountController + ) { + mAuthProvider = authProvider; + mAccountController = accountController; + } + + public void registerActivityCallbacks(AppCompatActivity activity, MyTbaOnboardingCallbacks callbacks) { + mSignInLauncher = activity.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + onSignInResult(activity, callbacks, result.getResultCode(), result.getData()); + }); + mNotificationPermissionLauncher = + activity.registerForActivityResult(new ActivityResultContracts.RequestPermission(), callbacks::onPermissionResult); + } + + private void onSignInResult(AppCompatActivity activity, MyTbaOnboardingCallbacks callbacks, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + Observable observable = mAuthProvider.userFromSignInResult(resultCode, data); + observable.subscribe(user -> { + TbaLogger.d("User logged in: " + user.getEmail()); + mAccountController.onAccountConnect(activity, user); + callbacks.onLoginSuccess(); + }, throwable -> { + TbaLogger.e("Error logging in", throwable); + mAccountController.setMyTbaEnabled(false); + callbacks.onLoginFailed(); + }); + } else if (resultCode == RESULT_CANCELED) { + Status signInStatus = (Status)data.getExtras().get("googleSignInStatus"); + String errorReason = GoogleSignInStatusCodes.getStatusCodeString(signInStatus.getStatusCode()); + Toast.makeText(activity, "Google sign in error: " + errorReason, Toast.LENGTH_LONG).show(); + TbaLogger.w("Google sign in error: " + errorReason); + callbacks.onLoginFailed(); + } + } + + public void launchSignIn(AppCompatActivity activity) { + Intent signInIntent = mAuthProvider.buildSignInIntent(); + if (signInIntent == null) { + Toast.makeText(activity, R.string.mytba_no_signin_intent, Toast.LENGTH_SHORT).show(); + TbaLogger.e("Unable to get login Intent"); + return; + } + + mSignInLauncher.launch(signInIntent); + } + + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + public void launchNotificationPermissionRequest(AppCompatActivity activity) { + TbaLogger.i("Requesting notification permission"); + mNotificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } + + public interface MyTbaOnboardingCallbacks { + void onLoginSuccess(); + void onLoginFailed(); + void onPermissionResult(boolean isGranted); + } +} diff --git a/android/src/main/java/com/thebluealliance/androidclient/views/MyTBAOnboardingViewPager.java b/android/src/main/java/com/thebluealliance/androidclient/views/MyTBAOnboardingViewPager.java index 3042b9af2..8a2887d3c 100644 --- a/android/src/main/java/com/thebluealliance/androidclient/views/MyTBAOnboardingViewPager.java +++ b/android/src/main/java/com/thebluealliance/androidclient/views/MyTBAOnboardingViewPager.java @@ -1,48 +1,37 @@ package com.thebluealliance.androidclient.views; import android.content.Context; +import android.os.Build; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.RelativeLayout; -import android.widget.TextView; +import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.viewpager.widget.ViewPager; import com.google.android.gms.common.SignInButton; import com.thebluealliance.androidclient.R; import com.thebluealliance.androidclient.adapters.MyTBAOnboardingPagerAdapter; - -import me.relex.circleindicator.CircleIndicator; +import com.thebluealliance.androidclient.databinding.MytbaOnboardingViewPagerBinding; public class MyTBAOnboardingViewPager extends RelativeLayout implements View.OnClickListener { - - private final ViewPager mViewPager; - private final SignInButton mSignInButton; - private final TextView myTBATitle; - private final TextView myTBASubtitle; + final private MytbaOnboardingViewPagerBinding mBinding; + final private MyTBAOnboardingPagerAdapter mAdapter; private Callbacks mCallbacks; public MyTBAOnboardingViewPager(Context context, AttributeSet attrs) { super(context, attrs); - - LayoutInflater.from(context).inflate(R.layout.mytba_onboarding_view_pager, this, true); - - myTBATitle = (TextView) findViewById(R.id.mytba_title); - myTBASubtitle = (TextView) findViewById(R.id.mytba_subtitle); - - mViewPager = (ViewPager) findViewById(R.id.view_pager); - mViewPager.setAdapter(new MyTBAOnboardingPagerAdapter(mViewPager)); - mViewPager.setOffscreenPageLimit(10); - - CircleIndicator indicator = (CircleIndicator) findViewById(R.id.mytba_pager_indicator); - indicator.setViewPager(mViewPager); - - mSignInButton = findViewById(R.id.google_sign_in_button); - mSignInButton.setSize(SignInButton.SIZE_WIDE); - mSignInButton.setOnClickListener(this); + mBinding = MytbaOnboardingViewPagerBinding.inflate(LayoutInflater.from(context), this); + mAdapter = new MyTBAOnboardingPagerAdapter(mBinding.viewPager); + mBinding.viewPager.setAdapter(mAdapter); + mBinding.viewPager.setOffscreenPageLimit(10); + mBinding.mytbaPagerIndicator.setViewPager(mBinding.viewPager); + mBinding.googleSignInButton.setSize(SignInButton.SIZE_WIDE); + mBinding.googleSignInButton.setOnClickListener(this); + mBinding.enableNotificationsButton.setOnClickListener(this); } public void setCallbacks(Callbacks callbacks) { @@ -52,60 +41,76 @@ public void setCallbacks(Callbacks callbacks) { @Override public void onClick(View v) { int id = v.getId(); - switch (id) { - case R.id.google_sign_in_button: - if (mCallbacks != null) { - mCallbacks.onSignInButtonClicked(); - } - break; + if (id == R.id.google_sign_in_button && mCallbacks != null) { + mCallbacks.onSignInButtonClicked(); + } else if (id == R.id.enable_notifications_button && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + mCallbacks.onEnableNotificationsButtonClicked(); } - } public void scrollToLoginPage() { // Login page should always be the last page - mViewPager.setCurrentItem(mViewPager.getAdapter().getCount() - 1); + mBinding.viewPager.setCurrentItem(mAdapter.getLoginPageId()); + } + + public boolean isBeforeLoginPage() { + return mBinding.viewPager.getCurrentItem() < mAdapter.getLoginPageId(); } public boolean isOnLoginPage() { - return mViewPager.getCurrentItem() == (mViewPager.getAdapter().getCount() - 1); + return mBinding.viewPager.getCurrentItem() == mAdapter.getLoginPageId(); } - public ViewPager getViewPager() { - return mViewPager; + public boolean isOnNotificationPermissionPage() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && mBinding.viewPager.getCurrentItem() == mAdapter.getNotificationPermissionPageId(); } - public void setTitleText(@StringRes int resId) { - myTBATitle.setText(resId); + public boolean isDone() { + return mBinding.viewPager.getCurrentItem() == mAdapter.getCount() - 1; } - public void setUpForNoPlayServices() { - myTBATitle.setVisibility(View.VISIBLE); - myTBATitle.setText(R.string.mytba_no_play_services); + public void advance() { + mBinding.viewPager.setCurrentItem(mBinding.viewPager.getCurrentItem() + 1); + } + + public ViewPager getViewPager() { + return mBinding.viewPager; + } - myTBASubtitle.setVisibility(View.VISIBLE); - myTBASubtitle.setText(R.string.mytba_no_play_services_subtitle); + public void setTitleText(@StringRes int resId) { + mBinding.mytbaTitle.setText(resId); } public void setUpForLoginPrompt() { - myTBATitle.setVisibility(View.VISIBLE); - myTBATitle.setText(R.string.mytba_get_started_title); + mBinding.mytbaTitle.setVisibility(View.VISIBLE); + mBinding.mytbaTitle.setText(R.string.mytba_get_started_title); - myTBASubtitle.setVisibility(View.VISIBLE); - myTBASubtitle.setText(R.string.mytba_login_prompt); + mBinding.mytbaSubtitle.setVisibility(View.VISIBLE); + mBinding.mytbaSubtitle.setText(R.string.mytba_login_prompt); } public void setUpForLoginSuccess() { - myTBATitle.setVisibility(View.VISIBLE); - myTBATitle.setText(R.string.mytba_login_success); + mBinding.mytbaTitle.setVisibility(View.VISIBLE); + mBinding.mytbaTitle.setText(R.string.mytba_login_success); - myTBASubtitle.setVisibility(View.VISIBLE); - myTBASubtitle.setText(R.string.mytba_login_success_subtitle); + mBinding.mytbaSubtitle.setVisibility(View.VISIBLE); + mBinding.mytbaSubtitle.setText(R.string.mytba_login_success_subtitle); - mSignInButton.setVisibility(View.GONE); + mBinding.googleSignInButton.setVisibility(View.GONE); + } + + public void setUpForPermissionResult(boolean permissionGranted) { + if (permissionGranted) { + mBinding.enableNotificationsButton.setVisibility(GONE); + } else { + mBinding.enableNotificationsClarification.setVisibility(VISIBLE); + } } public interface Callbacks { void onSignInButtonClicked(); + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + void onEnableNotificationsButtonClicked(); } } diff --git a/android/src/main/res/layout/mytba_onboarding_view_pager.xml b/android/src/main/res/layout/mytba_onboarding_view_pager.xml index 2a5900925..7d89e9206 100644 --- a/android/src/main/res/layout/mytba_onboarding_view_pager.xml +++ b/android/src/main/res/layout/mytba_onboarding_view_pager.xml @@ -168,6 +168,71 @@ android:layout_margin="16dp" /> + + + + + + + + + + + + + +