diff --git a/app/build.gradle b/app/build.gradle index 05282b53..764d8b6d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,6 +42,7 @@ android { buildFeatures { viewBinding true + dataBinding true buildConfig true } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ads/views/NativeAdBannerView.java b/app/src/main/java/com/d4rk/androidtutorials/java/ads/views/NativeAdBannerView.java index eec659dc..b1fb9a0d 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ads/views/NativeAdBannerView.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ads/views/NativeAdBannerView.java @@ -39,7 +39,7 @@ public NativeAdBannerView(@NonNull Context context, @Nullable AttributeSet attrs private void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { if (attrs != null) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NativeAdBannerView, defStyleAttr, 0); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.NativeAdBannerView, defStyleAttr, 0); // FIXME: 'TypedArray' used without 'try'-with-resources statement layoutRes = a.getResourceId(R.styleable.NativeAdBannerView_nativeAdLayout, R.layout.ad_home_banner_large); a.recycle(); } @@ -49,7 +49,7 @@ public void loadAd(AdRequest adRequest) { loadAd(adRequest, null); } - public void loadAd(AdRequest adRequest, @Nullable AdListener listener) { + public void loadAd(AdRequest adRequest, @Nullable AdListener listener) { // FIXME: Parameter 'adRequest' is never used NativeAdLoader.load(getContext(), this, layoutRes, listener); } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/data/repository/DefaultHomeRepository.java b/app/src/main/java/com/d4rk/androidtutorials/java/data/repository/DefaultHomeRepository.java index f2714fe5..c1ebd017 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/data/repository/DefaultHomeRepository.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/data/repository/DefaultHomeRepository.java @@ -28,7 +28,7 @@ public String getAppPlayStoreUrl(String packageName) { } @Override - public String getDailyTip() { + public String dailyTip() { return localDataSource.getDailyTip(); } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/data/repository/HomeRepository.java b/app/src/main/java/com/d4rk/androidtutorials/java/data/repository/HomeRepository.java index 26254989..63a30037 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/data/repository/HomeRepository.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/data/repository/HomeRepository.java @@ -13,7 +13,7 @@ public interface HomeRepository { String getAppPlayStoreUrl(String packageName); - String getDailyTip(); + String dailyTip(); void fetchPromotedApps(PromotedAppsCallback callback); diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/domain/home/GetDailyTipUseCase.java b/app/src/main/java/com/d4rk/androidtutorials/java/domain/home/GetDailyTipUseCase.java index 7d9e6141..3f05c74a 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/domain/home/GetDailyTipUseCase.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/domain/home/GetDailyTipUseCase.java @@ -16,6 +16,6 @@ public GetDailyTipUseCase(HomeRepository repository) { * Returns today's tip string. */ public String invoke() { - return repository.getDailyTip(); + return repository.dailyTip(); } } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/about/AboutFragment.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/about/AboutFragment.java index 8d905467..a9f45463 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/about/AboutFragment.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/about/AboutFragment.java @@ -67,7 +67,7 @@ public android.view.View onCreateView(@NonNull android.view.LayoutInflater infla }); binding.imageViewAppIcon.setOnClickListener( - v -> openUrl("https://d4rk7355608.github.io/home/")); + v -> openUrl("https://mihaicristiancondrea.github.io/profile")); binding.chipGoogleDev.setOnClickListener( v -> openUrl("https://g.dev/D4rK7355608")); binding.chipYoutube.setOnClickListener( diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/data/room/RoomActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/data/room/RoomActivity.java index 3c962495..78b39541 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/data/room/RoomActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/data/room/RoomActivity.java @@ -89,7 +89,7 @@ protected void onDestroy() { private static class NotesAdapter extends ListAdapter { private static final DiffUtil.ItemCallback DIFF_CALLBACK = - new DiffUtil.ItemCallback() { + new DiffUtil.ItemCallback<>() { @Override public boolean areItemsTheSame(@NonNull Note oldItem, @NonNull Note newItem) { return oldItem.id == newItem.id; diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/networking/retrofit/RetrofitActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/networking/retrofit/RetrofitActivity.java index 3ba23381..3951c1f9 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/networking/retrofit/RetrofitActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/networking/retrofit/RetrofitActivity.java @@ -44,9 +44,9 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { binding.buttonFetch.setOnClickListener(v -> { binding.buttonFetch.setEnabled(false); - api.getTodo().enqueue(new Callback() { + api.getTodo().enqueue(new Callback<>() { @Override - public void onResponse(Call call, Response response) { + public void onResponse(Call call, Response response) { // FIXME: Not annotated parameter overrides @EverythingIsNonNull parameter if (response.isSuccessful() && response.body() != null) { binding.textViewResult.setText(response.body().title); } else { @@ -56,7 +56,7 @@ public void onResponse(Call call, Response response) { } @Override - public void onFailure(Call call, Throwable t) { + public void onFailure(Call call, Throwable t) { // FIXME: Not annotated parameter overrides @EverythingIsNonNull parameter binding.textViewResult.setText(R.string.snack_general_error); binding.buttonFetch.setEnabled(true); } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/start/AndroidStartProjectActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/start/AndroidStartProjectActivity.java index 08cd83ab..4a6f974f 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/start/AndroidStartProjectActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/start/AndroidStartProjectActivity.java @@ -25,7 +25,7 @@ protected void onCreate(Bundle savedInstanceState) { edgeToEdgeDelegate.applyEdgeToEdge(binding.constraintLayout); setSupportActionBar(binding.topAppBar); - binding.topAppBar.setNavigationOnClickListener(v -> onBackPressed()); + binding.topAppBar.setNavigationOnClickListener(v -> onBackPressed()); // FIXME: 'onBackPressed()' is deprecated binding.topAppBar.setOnMenuItemClickListener(item -> { if (item.getItemId() == R.id.action_share) { Intent sharingIntent = new Intent(Intent.ACTION_SEND); diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/textboxes/passwordbox/PasswordBoxActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/textboxes/passwordbox/PasswordBoxActivity.java index 75844a8f..75908d6e 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/textboxes/passwordbox/PasswordBoxActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/textboxes/passwordbox/PasswordBoxActivity.java @@ -62,7 +62,7 @@ private void hidePassword() { private void addKeyListener() { binding.buttonShowPassword.setOnClickListener(v -> - Snackbar.make(binding.getRoot(), binding.editText.getText(), Snackbar.LENGTH_LONG).show()); + Snackbar.make(binding.getRoot(), binding.editText.getText(), Snackbar.LENGTH_LONG).show()); // FIXME: Argument 'binding.editText.getText()' might be null } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/views/web/WebViewActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/views/web/WebViewActivity.java index bce77bbb..75edff7a 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/views/web/WebViewActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/android/lessons/views/web/WebViewActivity.java @@ -36,7 +36,7 @@ protected void onCreate(Bundle savedInstanceState) { @SuppressLint("SetJavaScriptEnabled") private void setupWebView() { WebView webView = binding.webView; - webView.loadUrl("https://d4rk7355608.github.io/profile/#home"); + webView.loadUrl("https://mihaicristiancondrea.github.io/profile/"); WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setDomStorageEnabled(true); diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/help/HelpActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/help/HelpActivity.java index 920d98a3..327e14a1 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/help/HelpActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/help/HelpActivity.java @@ -76,10 +76,10 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { openLink("https://play.google.com/apps/testing/" + getPackageName()); return true; } else if (itemId == R.id.terms_of_service) { - openLink("https://d4rk7355608.github.io/profile/#terms-of-service-apps"); + openLink("https://mihaicristiancondrea.github.io/profile/#terms-of-service-end-user-software"); return true; } else if (itemId == R.id.privacy_policy) { - openLink("https://d4rk7355608.github.io/profile/#privacy-policy-apps"); + openLink("https://mihaicristiancondrea.github.io/profile/#privacy-policy-end-user-software"); return true; } else if (itemId == R.id.oss) { OpenSourceLicensesUtils.loadHtmlData(this, (changelogHtml, eulaHtml) -> openLicensesScreen(this, eulaHtml, changelogHtml)); diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeFragment.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeFragment.java index af22fb7f..228abb19 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeFragment.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeFragment.java @@ -107,7 +107,7 @@ private void shareTip(String tip) { private void shareApp(com.d4rk.androidtutorials.java.data.model.PromotedApp app) { android.content.Intent sharingIntent = new android.content.Intent(android.content.Intent.ACTION_SEND); sharingIntent.setType("text/plain"); - String shareLink = homeViewModel.getPromotedAppIntent(app.packageName()).getData().toString(); + String shareLink = homeViewModel.getPromotedAppIntent(app.packageName()).getData().toString(); // FIXME: Method invocation 'toString' may produce 'NullPointerException' String shareMessage = getString(com.d4rk.androidtutorials.java.R.string.share_message, shareLink); sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareMessage); sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, getString(com.d4rk.androidtutorials.java.R.string.share_subject)); diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java index 704f77fd..f29ccf56 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainActivity.java @@ -76,13 +76,13 @@ public void onResume(@NonNull LifecycleOwner owner) { if (ConsentUtils.canShowAds(MainActivity.this)) { if (mBinding.adView.getVisibility() != View.VISIBLE) { MobileAds.initialize(MainActivity.this); - mBinding.adPlaceholder.setVisibility(View.GONE); + mBinding.adPlaceholder.setVisibility(View.GONE); // FIXME: Method invocation 'setVisibility' may produce 'NullPointerException' mBinding.adView.setVisibility(View.VISIBLE); mBinding.adView.loadAd(new AdRequest.Builder().build()); } } else { mBinding.adView.setVisibility(View.GONE); - mBinding.adPlaceholder.setVisibility(View.VISIBLE); + mBinding.adPlaceholder.setVisibility(View.VISIBLE); // FIXME: Method invocation 'setVisibility' may produce 'NullPointerException' } } } @@ -90,7 +90,7 @@ public void onResume(@NonNull LifecycleOwner owner) { private MainViewModel mainViewModel; private NavController navController; private int currentNavIndex; - private AppUpdateNotificationsManager appUpdateNotificationsManager; + private AppUpdateNotificationsManager appUpdateNotificationsManager; // FIXME: Private field 'appUpdateNotificationsManager' is assigned but never accessed private AppUpdateManager appUpdateManager; private InstallStateUpdatedListener installStateUpdatedListener; private long backPressedTime; @@ -173,24 +173,24 @@ private void observeViewModel() { EdgeToEdgeDelegate edgeToEdgeDelegate = new EdgeToEdgeDelegate(this); NavigationBarView navBarView = (NavigationBarView) mBinding.navView; if (useRail) { - mBinding.navRail.setVisibility(View.VISIBLE); + mBinding.navRail.setVisibility(View.VISIBLE); // FIXME: Method invocation 'setVisibility' may produce 'NullPointerException' navBarView.setVisibility(View.GONE); edgeToEdgeDelegate.applyEdgeToEdge(mBinding.container); } else { - mBinding.navRail.setVisibility(View.GONE); + mBinding.navRail.setVisibility(View.GONE); // FIXME: Method invocation 'setVisibility' may produce 'NullPointerException' navBarView.setVisibility(View.VISIBLE); edgeToEdgeDelegate.applyEdgeToEdgeBottomBar(mBinding.container, navBarView); - navBarView.setLabelVisibilityMode(uiState.getBottomNavVisibility()); + navBarView.setLabelVisibilityMode(uiState.bottomNavVisibility()); if (mBinding.adView != null) { if (ConsentUtils.canShowAds(this)) { MobileAds.initialize(this); - mBinding.adPlaceholder.setVisibility(View.GONE); + mBinding.adPlaceholder.setVisibility(View.GONE); // FIXME: Method invocation 'setVisibility' may produce 'NullPointerException' mBinding.adView.setVisibility(View.VISIBLE); mBinding.adView.loadAd(new AdRequest.Builder().build()); } else { mBinding.adView.setVisibility(View.GONE); - mBinding.adPlaceholder.setVisibility(View.VISIBLE); + mBinding.adPlaceholder.setVisibility(View.VISIBLE); // FIXME: Method invocation 'setVisibility' may produce 'NullPointerException' } } } @@ -200,13 +200,13 @@ private void observeViewModel() { if (navHostFragment != null) { navController = navHostFragment.getNavController(); NavGraph navGraph = navController.getNavInflater().inflate(R.navigation.mobile_navigation); - navGraph.setStartDestination(uiState.getDefaultNavDestination()); + navGraph.setStartDestination(uiState.defaultNavDestination()); navController.setGraph(navGraph); navOrder.put(R.id.navigation_home, 0); navOrder.put(R.id.navigation_android_studio, 1); navOrder.put(R.id.navigation_about, 2); - currentNavIndex = navOrder.get(navController.getCurrentDestination().getId()); + currentNavIndex = navOrder.get(navController.getCurrentDestination().getId()); // FIXME: Method invocation 'getId' may produce 'NullPointerException' NavOptions forwardOptions = new NavOptions.Builder() .setEnterAnim(R.anim.fragment_spring_enter) @@ -256,13 +256,13 @@ private void observeViewModel() { }); } - if (uiState.isThemeChanged()) { + if (uiState.themeChanged()) { recreate(); } }); mainViewModel.getLoadingState().observe(this, isLoading -> - mBinding.progressBar.setVisibility(Boolean.TRUE.equals(isLoading) ? View.VISIBLE : View.GONE)); + mBinding.progressBar.setVisibility(Boolean.TRUE.equals(isLoading) ? View.VISIBLE : View.GONE)); // FIXME: Method invocation 'setVisibility' may produce 'NullPointerException' } private void setupUpdateNotifications() { @@ -285,7 +285,8 @@ public boolean onOptionsItemSelected(android.view.MenuItem item) { return super.onOptionsItemSelected(item); } - private void checkForImmediateUpdate() { + // TODO: Call on onResume + private void checkForImmediateUpdate() { // FIXME: Private method 'checkForImmediateUpdate()' is never used appUpdateManager .getAppUpdateInfo() .addOnSuccessListener( diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainUiState.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainUiState.java index cf9d9074..0b4a6b2b 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainUiState.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/main/MainUiState.java @@ -7,28 +7,6 @@ * bottom navigation visibility, the default navigation destination, and whether the theme * has changed requiring a recreation of the activity. */ -public class MainUiState { - @NavigationBarView.LabelVisibility - private final int bottomNavVisibility; - private final int defaultNavDestination; - private final boolean themeChanged; - - public MainUiState(@NavigationBarView.LabelVisibility int bottomNavVisibility, int defaultNavDestination, boolean themeChanged) { - this.bottomNavVisibility = bottomNavVisibility; - this.defaultNavDestination = defaultNavDestination; - this.themeChanged = themeChanged; - } - - @NavigationBarView.LabelVisibility - public int getBottomNavVisibility() { - return bottomNavVisibility; - } - - public int getDefaultNavDestination() { - return defaultNavDestination; - } - - public boolean isThemeChanged() { - return themeChanged; - } +public record MainUiState(@NavigationBarView.LabelVisibility int bottomNavVisibility, + int defaultNavDestination, boolean themeChanged) { } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/DataFragment.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/DataFragment.java index 0a1e8645..31693cb8 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/DataFragment.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/DataFragment.java @@ -1,5 +1,7 @@ package com.d4rk.androidtutorials.java.ui.screens.onboarding; +import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -11,6 +13,10 @@ import androidx.lifecycle.ViewModelProvider; import com.d4rk.androidtutorials.java.databinding.FragmentOnboardingDataBinding; +import com.d4rk.androidtutorials.java.utils.ConsentUtils; +import androidx.preference.PreferenceManager; +import android.content.SharedPreferences; +import com.d4rk.androidtutorials.java.R; public class DataFragment extends Fragment { @@ -28,6 +34,44 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); viewModel = new ViewModelProvider(requireActivity()).get(OnboardingViewModel.class); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + String keyAnalytics = getString(R.string.key_consent_analytics); + String keyAdPersonalization = getString(R.string.key_consent_ad_personalization); + + boolean analytics = prefs.getBoolean(keyAnalytics, true); + boolean ads = prefs.getBoolean(keyAdPersonalization, true); + binding.switchCrashlytics.setChecked(analytics); + binding.switchAds.setChecked(ads); + + binding.switchCrashlytics.setOnCheckedChangeListener((buttonView, isChecked) -> { + viewModel.setCrashlyticsEnabled(isChecked); + viewModel.setConsentAnalytics(isChecked); + ConsentUtils.updateFirebaseConsent(requireContext(), isChecked, binding.switchAds.isChecked(), binding.switchAds.isChecked(), binding.switchAds.isChecked()); + }); + + binding.switchAds.setOnCheckedChangeListener((buttonView, isChecked) -> { + viewModel.setConsentAdStorage(isChecked); + viewModel.setConsentAdUserData(isChecked); + viewModel.setConsentAdPersonalization(isChecked); + ConsentUtils.updateFirebaseConsent(requireContext(), binding.switchCrashlytics.isChecked(), isChecked, isChecked, isChecked); + }); + + binding.linkPrivacy.setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_VIEW, + Uri.parse("https://mihaicristiancondrea.github.io/profile/#privacy-policy-end-user-software")); + startActivity(intent); + }); + } + + public void saveSelection() { + boolean analytics = binding.switchCrashlytics.isChecked(); + boolean ads = binding.switchAds.isChecked(); + viewModel.setCrashlyticsEnabled(analytics); + viewModel.setConsentAnalytics(analytics); + viewModel.setConsentAdStorage(ads); + viewModel.setConsentAdUserData(ads); + viewModel.setConsentAdPersonalization(ads); + ConsentUtils.updateFirebaseConsent(requireContext(), analytics, ads, ads, ads); } @Override diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/DoneFragment.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/DoneFragment.java new file mode 100644 index 00000000..cf0baf6b --- /dev/null +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/DoneFragment.java @@ -0,0 +1,45 @@ +package com.d4rk.androidtutorials.java.ui.screens.onboarding; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; + +import com.d4rk.androidtutorials.java.databinding.FragmentOnboardingDoneBinding; + +public class DoneFragment extends Fragment { + + private FragmentOnboardingDoneBinding binding; + private OnboardingViewModel viewModel; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + binding = FragmentOnboardingDoneBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + viewModel = new ViewModelProvider(requireActivity()).get(OnboardingViewModel.class); + binding.buttonGetStarted.setOnClickListener(v -> { + viewModel.markOnboardingComplete(); + if (getActivity() instanceof OnboardingActivity) { + ((OnboardingActivity) getActivity()).finishOnboarding(); + } + }); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingActivity.java index 70154219..b6dfaba6 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingActivity.java @@ -4,6 +4,7 @@ import android.os.Bundle; import android.view.View; import android.widget.ImageView; +import android.widget.LinearLayout; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; @@ -37,6 +38,9 @@ protected void onCreate(Bundle savedInstanceState) { adapter = new OnboardingPagerAdapter(this); binding.viewPager.setAdapter(adapter); + int startPage = viewModel.getCurrentPage(); + binding.viewPager.setCurrentItem(startPage, false); + currentPosition = startPage; binding.viewPager.registerOnPageChangeCallback(new androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback() { @Override @@ -48,25 +52,29 @@ public void onPageSelected(int position) { ((ThemeFragment) fragment).saveSelection(); } else if (fragment instanceof StartPageFragment) { ((StartPageFragment) fragment).saveSelection(); - } else if (fragment instanceof BottomLabelsFragment) { - ((BottomLabelsFragment) fragment).saveSelection(); - } else if (fragment instanceof FontFragment) { - ((FontFragment) fragment).saveSelection(); + } else if (fragment instanceof DataFragment) { + ((DataFragment) fragment).saveSelection(); } } currentPosition = position; + viewModel.setCurrentPage(position); } }); new TabLayoutMediator(binding.tabIndicator, binding.viewPager, (tab, position) -> { ImageView dot = new ImageView(this); dot.setImageResource(R.drawable.onboarding_dot_unselected); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + params.setMargins(8,0,8,0); + dot.setLayoutParams(params); tab.setCustomView(dot); }).attach(); - TabLayout.Tab firstTab = binding.tabIndicator.getTabAt(0); - if (firstTab != null && firstTab.getCustomView() instanceof ImageView) { - ((ImageView) firstTab.getCustomView()).setImageResource(R.drawable.onboarding_dot_selected); + TabLayout.Tab startTab = binding.tabIndicator.getTabAt(startPage); + if (startTab != null && startTab.getCustomView() instanceof ImageView) { + ((ImageView) startTab.getCustomView()).setImageResource(R.drawable.onboarding_dot_selected); } binding.tabIndicator.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @@ -97,6 +105,11 @@ public void onTabReselected(TabLayout.Tab tab) { } }); + binding.buttonSkip.setOnClickListener(v -> { + viewModel.markOnboardingComplete(); + finishOnboarding(); + }); + binding.buttonNext.setOnClickListener(v -> { int current = binding.viewPager.getCurrentItem(); Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + current); @@ -104,10 +117,8 @@ public void onTabReselected(TabLayout.Tab tab) { ((ThemeFragment) fragment).saveSelection(); } else if (fragment instanceof StartPageFragment) { ((StartPageFragment) fragment).saveSelection(); - } else if (fragment instanceof BottomLabelsFragment) { - ((BottomLabelsFragment) fragment).saveSelection(); - } else if (fragment instanceof FontFragment) { - ((FontFragment) fragment).saveSelection(); + } else if (fragment instanceof DataFragment) { + ((DataFragment) fragment).saveSelection(); } if (current < adapter.getItemCount() - 1) { @@ -118,7 +129,7 @@ public void onTabReselected(TabLayout.Tab tab) { } }); - updateButtons(0); + updateButtons(startPage); } void finishOnboarding() { @@ -127,11 +138,15 @@ void finishOnboarding() { } private void updateButtons(int position) { - binding.buttonBack.setVisibility(position == 0 ? View.INVISIBLE : View.VISIBLE); if (position == adapter.getItemCount() - 1) { - binding.buttonNext.setText(R.string.finish); + binding.bottomBar.setVisibility(View.GONE); + binding.buttonSkip.setVisibility(View.GONE); } else { + binding.bottomBar.setVisibility(View.VISIBLE); + binding.buttonSkip.setVisibility(View.VISIBLE); + binding.buttonBack.setVisibility(position == 0 ? View.INVISIBLE : View.VISIBLE); binding.buttonNext.setText(R.string.next); + binding.buttonNext.setIconResource(R.drawable.ic_arrow_forward); } } @@ -144,23 +159,17 @@ private static class OnboardingPagerAdapter extends FragmentStateAdapter { @NonNull @Override public Fragment createFragment(int position) { - switch (position) { - case 0: - return new ThemeFragment(); - case 1: - return new StartPageFragment(); - case 2: - return new BottomLabelsFragment(); - case 3: - return new FontFragment(); - default: - return new DataFragment(); - } + return switch (position) { + case 0 -> new ThemeFragment(); + case 1 -> new StartPageFragment(); + case 2 -> new DataFragment(); + default -> new DoneFragment(); + }; } @Override public int getItemCount() { - return 5; + return 4; } } } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingViewModel.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingViewModel.java index dd02e95f..fd17d219 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingViewModel.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/OnboardingViewModel.java @@ -18,6 +18,7 @@ public class OnboardingViewModel extends ViewModel { private final Context context; private final SharedPreferences prefs; + private int currentPage = 0; @Inject public OnboardingViewModel(@ApplicationContext Context context) { @@ -29,6 +30,19 @@ public void setTheme(String value) { prefs.edit().putString(context.getString(R.string.key_theme), value).apply(); } + public String getTheme() { + String[] values = context.getResources().getStringArray(R.array.preference_theme_values); + return prefs.getString(context.getString(R.string.key_theme), values[0]); + } + + public void setCurrentPage(int page) { + currentPage = page; + } + + public int getCurrentPage() { + return currentPage; + } + public void setDefaultTab(String value) { prefs.edit().putString(context.getString(R.string.key_default_tab), value).apply(); } @@ -41,6 +55,26 @@ public void setMonospaceFont(String value) { prefs.edit().putString(context.getString(R.string.key_monospace_font), value).apply(); } + public void setCrashlyticsEnabled(boolean enabled) { + prefs.edit().putBoolean(context.getString(R.string.key_firebase_crashlytics), enabled).apply(); + } + + public void setConsentAnalytics(boolean enabled) { + prefs.edit().putBoolean(context.getString(R.string.key_consent_analytics), enabled).apply(); + } + + public void setConsentAdStorage(boolean enabled) { + prefs.edit().putBoolean(context.getString(R.string.key_consent_ad_storage), enabled).apply(); + } + + public void setConsentAdUserData(boolean enabled) { + prefs.edit().putBoolean(context.getString(R.string.key_consent_ad_user_data), enabled).apply(); + } + + public void setConsentAdPersonalization(boolean enabled) { + prefs.edit().putBoolean(context.getString(R.string.key_consent_ad_personalization), enabled).apply(); + } + public void markOnboardingComplete() { prefs.edit().putBoolean(context.getString(R.string.key_onboarding_complete), true).apply(); } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/StartPageFragment.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/StartPageFragment.java index 63e60ae2..2c392d0e 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/StartPageFragment.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/StartPageFragment.java @@ -7,21 +7,35 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.d4rk.androidtutorials.java.R; -import com.d4rk.androidtutorials.java.databinding.FragmentOnboardingStartPageBinding; +import com.d4rk.androidtutorials.java.databinding.FragmentOnboardingSelectionBinding; public class StartPageFragment extends Fragment { - private FragmentOnboardingStartPageBinding binding; + private FragmentOnboardingSelectionBinding binding; private OnboardingViewModel viewModel; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - binding = FragmentOnboardingStartPageBinding.inflate(inflater, container, false); + binding = FragmentOnboardingSelectionBinding.inflate(inflater, container, false); + + binding.setTitle(getString(R.string.default_tab)); + binding.setDescription(getString(R.string.default_tab_description)); + binding.setFirstIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_home)); + binding.setFirstTitle(getString(R.string.home)); + binding.setFirstDescription(getString(R.string.home_description)); + binding.setSecondIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_android_sdk)); + binding.setSecondTitle(getString(R.string.android_studio)); + binding.setSecondDescription(getString(R.string.android_studio_description)); + binding.setThirdIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_about)); + binding.setThirdTitle(getString(R.string.about)); + binding.setThirdDescription(getString(R.string.about_description)); + return binding.getRoot(); } @@ -29,15 +43,34 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); viewModel = new ViewModelProvider(requireActivity()).get(OnboardingViewModel.class); + + binding.optionFirst.radioButton.setId(View.generateViewId()); + binding.optionSecond.radioButton.setId(View.generateViewId()); + binding.optionThird.radioButton.setId(View.generateViewId()); + + selectOption(0); + + binding.cardFirst.setOnClickListener(v -> selectOption(0)); + binding.cardSecond.setOnClickListener(v -> selectOption(1)); + binding.cardThird.setOnClickListener(v -> selectOption(2)); + + binding.optionFirst.radioButton.setOnClickListener(v -> selectOption(0)); + binding.optionSecond.radioButton.setOnClickListener(v -> selectOption(1)); + binding.optionThird.radioButton.setOnClickListener(v -> selectOption(2)); + } + + private void selectOption(int index) { + binding.optionFirst.radioButton.setChecked(index == 0); + binding.optionSecond.radioButton.setChecked(index == 1); + binding.optionThird.radioButton.setChecked(index == 2); } public void saveSelection() { - int checkedId = binding.startPageGroup.getCheckedRadioButtonId(); String[] values = getResources().getStringArray(R.array.preference_default_tab_values); String value = values[0]; - if (checkedId == R.id.radio_android_studio) { + if (binding.optionSecond.radioButton.isChecked()) { value = values[1]; - } else if (checkedId == R.id.radio_about) { + } else if (binding.optionThird.radioButton.isChecked()) { value = values[2]; } viewModel.setDefaultTab(value); diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/ThemeFragment.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/ThemeFragment.java index 93c46b94..ebe32541 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/ThemeFragment.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/onboarding/ThemeFragment.java @@ -7,21 +7,36 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; import com.d4rk.androidtutorials.java.R; -import com.d4rk.androidtutorials.java.databinding.FragmentOnboardingThemeBinding; +import com.d4rk.androidtutorials.java.databinding.FragmentOnboardingSelectionBinding; public class ThemeFragment extends Fragment { - private FragmentOnboardingThemeBinding binding; + private FragmentOnboardingSelectionBinding binding; private OnboardingViewModel viewModel; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - binding = FragmentOnboardingThemeBinding.inflate(inflater, container, false); + binding = FragmentOnboardingSelectionBinding.inflate(inflater, container, false); + + binding.setTitle(getString(R.string.choose_your_style)); + binding.setDescription(getString(R.string.select_how_you_d_like_the_app_to_look)); + binding.setFirstIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_light_mode)); + binding.setFirstTitle(getString(R.string.light_mode)); + binding.setFirstDescription(getString(R.string.light_mode_description)); + binding.setSecondIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_dark_mode)); + binding.setSecondTitle(getString(R.string.dark_mode)); + binding.setSecondDescription(getString(R.string.dark_mode_description)); + binding.setThirdIcon(ContextCompat.getDrawable(requireContext(), R.drawable.ic_system_mode)); + binding.setThirdTitle(getString(R.string.follow_system)); + binding.setThirdDescription(getString(R.string.follow_system_mode_description)); + return binding.getRoot(); } @@ -29,20 +44,54 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); viewModel = new ViewModelProvider(requireActivity()).get(OnboardingViewModel.class); + + binding.optionFirst.radioButton.setId(View.generateViewId()); + binding.optionSecond.radioButton.setId(View.generateViewId()); + binding.optionThird.radioButton.setId(View.generateViewId()); + + String themeValue = viewModel.getTheme(); + String[] values = getResources().getStringArray(R.array.preference_theme_values); + int index = 2; // default follow system + if (themeValue.equals(values[1])) index = 0; + else if (themeValue.equals(values[2])) index = 1; + setRadioButtons(index); + + binding.cardFirst.setOnClickListener(v -> selectOption(0)); + binding.cardSecond.setOnClickListener(v -> selectOption(1)); + binding.cardThird.setOnClickListener(v -> selectOption(2)); + + binding.optionFirst.radioButton.setOnClickListener(v -> selectOption(0)); + binding.optionSecond.radioButton.setOnClickListener(v -> selectOption(1)); + binding.optionThird.radioButton.setOnClickListener(v -> selectOption(2)); } - public void saveSelection() { - int checkedId = binding.themeGroup.getCheckedRadioButtonId(); + private void setRadioButtons(int index) { + binding.optionFirst.radioButton.setChecked(index == 0); + binding.optionSecond.radioButton.setChecked(index == 1); + binding.optionThird.radioButton.setChecked(index == 2); + } + + private void selectOption(int index) { + setRadioButtons(index); + int mode; String[] values = getResources().getStringArray(R.array.preference_theme_values); - String value = values[0]; - if (checkedId == R.id.radio_light) { + String value; + if (index == 0) { + mode = AppCompatDelegate.MODE_NIGHT_NO; value = values[1]; - } else if (checkedId == R.id.radio_dark) { + } else if (index == 1) { + mode = AppCompatDelegate.MODE_NIGHT_YES; value = values[2]; - } else if (checkedId == R.id.radio_auto_battery) { - value = values[3]; + } else { + mode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; + value = values[0]; } viewModel.setTheme(value); + AppCompatDelegate.setDefaultNightMode(mode); + } + + public void saveSelection() { + // theme stored on selection } @Override diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/quiz/QuizActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/quiz/QuizActivity.java index 86319e98..1bc070a2 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/quiz/QuizActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/quiz/QuizActivity.java @@ -69,7 +69,7 @@ private void onNextClicked() { if (selectedIndex != -1) { viewModel.answer(selectedIndex); } - if (viewModel.getCurrentIndex().getValue() >= viewModel.getTotalQuestions()) { + if (viewModel.getCurrentIndex().getValue() >= viewModel.getTotalQuestions()) { // FIXME: Unboxing of 'viewModel.getCurrentIndex().getValue()' may produce 'NullPointerException' showResult(); } else { showQuestion(viewModel.getCurrentQuestion()); @@ -89,7 +89,7 @@ private void showQuestion(QuizQuestion question) { } private void showResult() { - int score = viewModel.getScore().getValue(); + int score = viewModel.getScore().getValue(); // FIXME: Unboxing of 'viewModel.getScore().getValue()' may produce 'NullPointerException' int total = viewModel.getTotalQuestions(); View view = LayoutInflater.from(this).inflate(R.layout.dialog_quiz_result, null, false); TextView textResult = view.findViewById(R.id.text_result); diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/quiz/QuizViewModel.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/quiz/QuizViewModel.java index 6365b501..99ab4a34 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/quiz/QuizViewModel.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/quiz/QuizViewModel.java @@ -23,18 +23,18 @@ public class QuizViewModel extends ViewModel { private final MutableLiveData> questions = new MutableLiveData<>(Collections.emptyList()); private final MutableLiveData currentIndex = new MutableLiveData<>(0); private final MutableLiveData score = new MutableLiveData<>(0); - private final LoadQuizQuestionsUseCase loadQuizQuestionsUseCase; + private final LoadQuizQuestionsUseCase loadQuizQuestionsUseCase; // FIXME: Field can be converted to a local variable && Private field 'loadQuizQuestionsUseCase' is assigned but never accessed @Inject public QuizViewModel(LoadQuizQuestionsUseCase loadQuizQuestionsUseCase) { this.loadQuizQuestionsUseCase = loadQuizQuestionsUseCase; - loadQuizQuestionsUseCase.invoke(result -> questions.postValue(result)); + loadQuizQuestionsUseCase.invoke(questions::postValue); } public QuizQuestion getCurrentQuestion() { List list = questions.getValue(); if (list == null || list.isEmpty()) return null; - int index = currentIndex.getValue(); + int index = currentIndex.getValue(); // FIXME: Unboxing of 'currentIndex.getValue()' may produce 'NullPointerException' return list.get(Math.min(index, list.size() - 1)); } @@ -46,16 +46,16 @@ public LiveData getScore() { return score; } - public LiveData> getQuestions() { + public LiveData> getQuestions() { // FIXME: Method 'getQuestions()' is never used return questions; } public void answer(int optionIndex) { QuizQuestion question = getCurrentQuestion(); if (question != null && optionIndex == question.answerIndex()) { - score.setValue(score.getValue() + 1); + score.setValue(score.getValue() + 1); // FIXME: Unboxing of 'score.getValue()' may produce 'NullPointerException' } - currentIndex.setValue(currentIndex.getValue() + 1); + currentIndex.setValue(currentIndex.getValue() + 1); // FIXME: Unboxing of 'currentIndex.getValue()' may produce 'NullPointerException' } public int getTotalQuestions() { diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/settings/repository/SettingsRepository.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/settings/repository/SettingsRepository.java index 975cb375..00e059dc 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/settings/repository/SettingsRepository.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/settings/repository/SettingsRepository.java @@ -51,7 +51,7 @@ public boolean applyTheme() { String preference = sharedPreferences.getString(preferenceKey, defaultThemeValue); int currentNightMode = AppCompatDelegate.getDefaultNightMode(); - int newNightMode = getNewNightMode(currentNightMode, preference, darkModeValues); + int newNightMode = getNewNightMode(currentNightMode, preference, darkModeValues); // FIXME: Argument 'preference' might be null if (newNightMode != currentNightMode) { AppCompatDelegate.setDefaultNightMode(newNightMode); return true; diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/StartupActivity.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/StartupActivity.java index 7b75b49b..0bc2ccd8 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/StartupActivity.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/StartupActivity.java @@ -36,7 +36,7 @@ protected void onCreate(Bundle savedInstanceState) { binding.buttonBrowsePrivacyPolicyAndTermsOfService.setOnClickListener(v -> startActivity(new Intent(Intent.ACTION_VIEW, - Uri.parse("https://d4rk7355608.github.io/profile/#privacy-policy-apps"))) + Uri.parse("https://mihaicristiancondrea.github.io/profile/#privacy-policy-end-user-software"))) ); binding.floatingButtonAgree.setOnClickListener(v -> { diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/dialogs/ConsentDialogFragment.java b/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/dialogs/ConsentDialogFragment.java deleted file mode 100644 index 00ab6da6..00000000 --- a/app/src/main/java/com/d4rk/androidtutorials/java/ui/screens/startup/dialogs/ConsentDialogFragment.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.d4rk.androidtutorials.java.ui.screens.startup.dialogs; - -import android.app.Dialog; -import android.content.SharedPreferences; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.DialogFragment; -import androidx.preference.PreferenceManager; - -import com.d4rk.androidtutorials.java.BuildConfig; -import com.d4rk.androidtutorials.java.R; -import com.d4rk.androidtutorials.java.databinding.DialogConsentBinding; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -public class ConsentDialogFragment extends DialogFragment { - - private ConsentListener listener; - - public void setConsentListener(ConsentListener listener) { - this.listener = listener; - } - - @NonNull - @Override - public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - DialogConsentBinding binding = DialogConsentBinding.inflate(getLayoutInflater()); - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - boolean defaultChecked = !BuildConfig.DEBUG; - - // Cache preference keys early so the dialog can still operate safely if the - // fragment gets detached before the positive button callback runs. - String keyAnalytics = getString(R.string.key_consent_analytics); - String keyAdStorage = getString(R.string.key_consent_ad_storage); - String keyAdUserData = getString(R.string.key_consent_ad_user_data); - String keyAdPersonalization = getString(R.string.key_consent_ad_personalization); - - boolean analytics = prefs.getBoolean(keyAnalytics, defaultChecked); - boolean adStorage = prefs.getBoolean(keyAdStorage, defaultChecked); - boolean adUserData = prefs.getBoolean(keyAdUserData, defaultChecked); - boolean adPersonalization = prefs.getBoolean(keyAdPersonalization, defaultChecked); - - binding.checkAnalyticsStorage.setChecked(analytics); - binding.checkAdStorage.setChecked(adStorage); - binding.checkAdUserData.setChecked(adUserData); - binding.checkAdPersonalization.setChecked(adPersonalization); - - setCancelable(false); - - return new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.consent_dialog_title) - .setView(binding.getRoot()) - .setCancelable(false) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - boolean a = binding.checkAnalyticsStorage.isChecked(); - boolean b = binding.checkAdStorage.isChecked(); - boolean c = binding.checkAdUserData.isChecked(); - boolean d = binding.checkAdPersonalization.isChecked(); - - prefs.edit() - .putBoolean(keyAnalytics, a) - .putBoolean(keyAdStorage, b) - .putBoolean(keyAdUserData, c) - .putBoolean(keyAdPersonalization, d) - .apply(); - - if (listener != null) { - listener.onConsentSet(a, b, c, d); - } - }) - .create(); - } - - public interface ConsentListener { - void onConsentSet(boolean analytics, boolean adStorage, boolean adUserData, boolean adPersonalization); - } -} diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/utils/FontManager.java b/app/src/main/java/com/d4rk/androidtutorials/java/utils/FontManager.java index 4038e0db..91f395f5 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/utils/FontManager.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/utils/FontManager.java @@ -19,14 +19,14 @@ public static Typeface getMonospaceFont(Context context, SharedPreferences prefs prefs.edit().remove(key).apply(); font = "6"; } - return switch (font) { + return switch (font) { // FIXME: Dereference of 'font' may produce 'NullPointerException' case "0" -> ResourcesCompat.getFont(context, R.font.font_audiowide); case "1" -> ResourcesCompat.getFont(context, R.font.font_fira_code); case "2" -> ResourcesCompat.getFont(context, R.font.font_jetbrains_mono); case "3" -> ResourcesCompat.getFont(context, R.font.font_noto_sans_mono); case "4" -> ResourcesCompat.getFont(context, R.font.font_poppins); case "5" -> ResourcesCompat.getFont(context, R.font.font_roboto_mono); - case "6" -> ResourcesCompat.getFont(context, R.font.font_google_sans_code); + case "6" -> ResourcesCompat.getFont(context, R.font.font_google_sans_code); // FIXME: Branch in 'switch' is a duplicate of the default branch default -> ResourcesCompat.getFont(context, R.font.font_google_sans_code); }; } diff --git a/app/src/main/java/com/d4rk/androidtutorials/java/utils/OpenSourceLicensesUtils.java b/app/src/main/java/com/d4rk/androidtutorials/java/utils/OpenSourceLicensesUtils.java index c4e23ebb..3dc9d3c1 100644 --- a/app/src/main/java/com/d4rk/androidtutorials/java/utils/OpenSourceLicensesUtils.java +++ b/app/src/main/java/com/d4rk/androidtutorials/java/utils/OpenSourceLicensesUtils.java @@ -30,8 +30,8 @@ public static void loadHtmlData(final Context context, final HtmlDataCallback ca executor.execute(() -> { String packageName = context.getPackageName(); String currentVersion = getAppVersion(context); - String changelogUrl = "https://raw.githubusercontent.com/D4rK7355608/" + packageName + "/refs/heads/main/CHANGELOG.md"; - String eulaUrl = "https://raw.githubusercontent.com/D4rK7355608/" + packageName + "/refs/heads/main/EULA.md"; + String changelogUrl = "https://raw.githubusercontent.com/MihaiCristianCondrea/" + packageName + "/refs/heads/main/CHANGELOG.md"; + String eulaUrl = "https://raw.githubusercontent.com/MihaiCristianCondrea/" + packageName + "/refs/heads/main/EULA.md"; String changelogMarkdown = loadMarkdown(context, changelogUrl, R.string.error_loading_changelog); String extractedChangelog = extractLatestVersionChangelog(changelogMarkdown, currentVersion); diff --git a/app/src/main/res/drawable-anydpi/ic_dark_mode.xml b/app/src/main/res/drawable-anydpi/ic_dark_mode.xml new file mode 100644 index 00000000..7063d45d --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_dark_mode.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_light_mode.xml b/app/src/main/res/drawable-anydpi/ic_light_mode.xml new file mode 100644 index 00000000..281ec137 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_light_mode.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_open_new.xml b/app/src/main/res/drawable-anydpi/ic_open_new.xml new file mode 100644 index 00000000..63a3c643 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_open_new.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-anydpi/ic_system_mode.xml b/app/src/main/res/drawable-anydpi/ic_system_mode.xml new file mode 100644 index 00000000..eff884fc --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_system_mode.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_back.xml b/app/src/main/res/drawable/ic_arrow_back.xml new file mode 100644 index 00000000..5498475b --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_forward.xml b/app/src/main/res/drawable/ic_arrow_forward.xml new file mode 100644 index 00000000..0a68f5fe --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_forward.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 00000000..3099c0a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_security.xml b/app/src/main/res/drawable/ic_security.xml new file mode 100644 index 00000000..7cb8b532 --- /dev/null +++ b/app/src/main/res/drawable/ic_security.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_onboarding.xml b/app/src/main/res/layout/activity_onboarding.xml index b7720b68..0a396bb6 100644 --- a/app/src/main/res/layout/activity_onboarding.xml +++ b/app/src/main/res/layout/activity_onboarding.xml @@ -5,6 +5,18 @@ android:layout_height="match_parent" android:fitsSystemWindows="true"> + + + app:layout_constraintTop_toBottomOf="@id/buttonSkip" /> + android:text="@string/back" + app:icon="@drawable/ic_arrow_back" + app:iconGravity="textStart" /> + android:text="@string/next" + app:icon="@drawable/ic_arrow_forward" + app:iconGravity="textEnd" /> diff --git a/app/src/main/res/layout/dialog_consent.xml b/app/src/main/res/layout/dialog_consent.xml deleted file mode 100644 index 61a15a82..00000000 --- a/app/src/main/res/layout/dialog_consent.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_onboarding_data.xml b/app/src/main/res/layout/fragment_onboarding_data.xml index 798968a1..d2a96c39 100644 --- a/app/src/main/res/layout/fragment_onboarding_data.xml +++ b/app/src/main/res/layout/fragment_onboarding_data.xml @@ -13,13 +13,88 @@ - + android:orientation="vertical" + android:padding="24dp"> + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_onboarding_done.xml b/app/src/main/res/layout/fragment_onboarding_done.xml new file mode 100644 index 00000000..c6b83733 --- /dev/null +++ b/app/src/main/res/layout/fragment_onboarding_done.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_onboarding_selection.xml b/app/src/main/res/layout/fragment_onboarding_selection.xml new file mode 100644 index 00000000..ff0f2530 --- /dev/null +++ b/app/src/main/res/layout/fragment_onboarding_selection.xml @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_onboarding_start_page.xml b/app/src/main/res/layout/fragment_onboarding_start_page.xml deleted file mode 100644 index d274ce73..00000000 --- a/app/src/main/res/layout/fragment_onboarding_start_page.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_onboarding_theme.xml b/app/src/main/res/layout/fragment_onboarding_theme.xml deleted file mode 100644 index f57137e2..00000000 --- a/app/src/main/res/layout/fragment_onboarding_theme.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_onboarding_option.xml b/app/src/main/res/layout/item_onboarding_option.xml new file mode 100644 index 00000000..e7e779ea --- /dev/null +++ b/app/src/main/res/layout/item_onboarding_option.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/raw/text_webview_java.txt b/app/src/main/res/raw/text_webview_java.txt index 5faf61d2..837b041f 100644 --- a/app/src/main/res/raw/text_webview_java.txt +++ b/app/src/main/res/raw/text_webview_java.txt @@ -24,6 +24,6 @@ public class MainActivity extends AppCompatActivity { webSettings.setJavaScriptEnabled(true); webSettings.setDomStorageEnabled(true); webSettings.setJavaScriptCanOpenWindowsAutomatically(true); - binding.webView.loadUrl("https://d4rk7355608.github.io/profile/#home"); + binding.webView.loadUrl("https://mihaicristiancondrea.github.io/profile/"); } } \ No newline at end of file diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml index 748376a6..eec5ed78 100644 --- a/app/src/main/res/values-bg-rBG/strings.xml +++ b/app/src/main/res/values-bg-rBG/strings.xml @@ -1,5 +1,5 @@ - + Преглед на изображението Научете как да създавате прости Java приложения в Android Studio. 📱 Налична е нова актуализация. diff --git a/app/src/main/res/values-fil-rPH/strings.xml b/app/src/main/res/values-fil-rPH/strings.xml index 29d093d5..1741f20e 100644 --- a/app/src/main/res/values-fil-rPH/strings.xml +++ b/app/src/main/res/values-fil-rPH/strings.xml @@ -1,5 +1,5 @@ - + Paunang tingin sa image view Alamin kung paano gumawa ng mga simpleng Java app sa Android Studio. 📱 May bagong update na available. diff --git a/app/src/main/res/values-hu-rHU/strings.xml b/app/src/main/res/values-hu-rHU/strings.xml index 1dc23138..42f914ac 100644 --- a/app/src/main/res/values-hu-rHU/strings.xml +++ b/app/src/main/res/values-hu-rHU/strings.xml @@ -1,5 +1,5 @@ - + Képnézet előnézete Tanuld meg, hogyan készíts egyszerű Java alkalmazásokat az Android Studioban. 📱 Új frissítés érhető el. diff --git a/app/src/main/res/values-in-rID/strings.xml b/app/src/main/res/values-in-rID/strings.xml index 1ba113fc..a02f9ea7 100644 --- a/app/src/main/res/values-in-rID/strings.xml +++ b/app/src/main/res/values-in-rID/strings.xml @@ -1,5 +1,5 @@ - + Pratinjau tampilan gambar Pelajari cara membuat aplikasi sederhana di Android Studio. 📱 Pembaruan baru tersedia. diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index c173f126..3e01a683 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -1,5 +1,5 @@ - + 이미지 뷰 미리보기 Android Studio에서 간단한 Java 앱을 만드는 방법을 배워보세요. 📱 새로운 업데이트가 있습니다. diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 5f0a6deb..1ec3740c 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -1,5 +1,5 @@ - + Podgląd widoku obrazu Naucz się tworzyć proste aplikacje Java w Android Studio. 📱 Dostępna jest nowa aktualizacja. diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 84d1b907..5501e0fa 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -1,5 +1,5 @@ - + Предпросмотр изображения Узнайте, как создавать простые приложения в Android Studio. 📱 Доступно новое обновление. diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 67da5496..89b7dba5 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -1,5 +1,5 @@ - + Förhandsgranskning av bildvy Lär dig hur man skapar enkla Java-appar i Android Studio. 📱 En ny uppdatering finns tillgänglig. diff --git a/app/src/main/res/values-th-rTH/strings.xml b/app/src/main/res/values-th-rTH/strings.xml index f1c60142..cbc7f638 100644 --- a/app/src/main/res/values-th-rTH/strings.xml +++ b/app/src/main/res/values-th-rTH/strings.xml @@ -1,5 +1,5 @@ - + ดูตัวอย่างมุมมองภาพ เรียนรู้วิธีสร้างแอป Java ง่ายๆ ใน Android Studio 📱 มีการอัปเดตใหม่. diff --git a/app/src/main/res/values-tr-rTR/strings.xml b/app/src/main/res/values-tr-rTR/strings.xml index 97d2b1da..7eb23b1b 100644 --- a/app/src/main/res/values-tr-rTR/strings.xml +++ b/app/src/main/res/values-tr-rTR/strings.xml @@ -1,5 +1,5 @@ - + Görüntü görünümü önizlemesi Android Studio\'da basit Java uygulamaları yapmayı öğrenin. 📱 Yeni bir güncelleme mevcut. diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index 781f0bf8..691a6d9f 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -1,5 +1,5 @@ - + Попередній перегляд зображення Дізнайтеся, як створювати прості програми на Java в Android Studio. 📱 Доступне нове оновлення. diff --git a/app/src/main/res/values-vi-rVN/strings.xml b/app/src/main/res/values-vi-rVN/strings.xml index 46634052..e3b29446 100644 --- a/app/src/main/res/values-vi-rVN/strings.xml +++ b/app/src/main/res/values-vi-rVN/strings.xml @@ -1,5 +1,5 @@ - + Xem trước khung nhìn hình ảnh Học cách tạo các ứng dụng Java đơn giản trong Android Studio. 📱 Có bản cập nhật mới. diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index b897a104..09e9aa9b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,5 +1,5 @@ - + 圖片檢視預覽 學習如何在 Android Studio 中製作簡單的 Java 應用程式。 📱 有可用的新更新! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8a6ea224..8f6fe066 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,8 +25,6 @@ Learn more Play Store Ad - Search tutorials - Search tutorials Search lessons Search lessons @@ -254,6 +252,10 @@ Dark mode Auto battery dark mode Default tab + Choose which tab appears when the app opens + Top stories and recent updates + Android Studio tools, tips, and the full tutorial catalog + App details, version, and credits Bottom navigation bar labels Labeled Selected %1$s @@ -312,7 +314,6 @@ Version %1$s (%2$d) Music Made with ❤ in Romania. - Close? Restart required. What is Android Studio Tutorials: Java Edition? How can I download Android Studio Tutorials: Java Edition? @@ -339,10 +340,26 @@ Allows the app to use the Google Play Billing Library to handle in-app purchases and donations. Allows the app to verify its compliance with the license agreement and enforce licensing terms to protect intellectual property. Next - Finish Back Bottom navigation labels - We collect data to improve your experience. + Skip + Choose your style + Choose how the app looks + Bright, clean appearance + Comfortable in low light + Follows your device theme + Help Us Improve Your Experience + Help us make the app better for you by sharing anonymous crash reports and usage statistics. + Enable Crash Reporting + By enabling this, you help us identify and fix bugs faster. We do not collect any personal information. + Allow Personalized Ads + Permit storing ad data, user info, and personalization to keep ads relevant. + Read our Privacy Policy + Privacy Icon + You\'re All Set! + Your setup is complete. You are now ready to explore all the features. + Get Started + Success Icon Allows the app to access and modify the device\'s notification policy, controlling how and when notifications are displayed to the user and providing custom notification management features. Allows the app to create and use services that run in the foreground, giving them priority over other background processes and improving performance and reliability. Set application language. @@ -386,7 +403,6 @@ Learn how to use inbox style notifications in your Android app with this lesson. Discover how to create a notification channel and builder, and how to set the style of your notifications to an InboxStyle with multiple lines of text and a summary text. Explore the different options available for customizing your inbox style notifications. A bottom navigation bar lets you quickly switch between top-level views in your app. A navigation drawer slides in from the side and displays the app\'s main navigation options. - Are you sure you want to exit? This will be the message you will see on screen. To take effect, please restart the app. The Android Software Development Kit (SDK) is a collection of tools that allow developers to create Android apps. It includes a set of libraries, a debugger, a handset emulator, and documentation. The SDK also includes an API library and a set of API documentation. The packages you download have libraries, which helps you in creating your app.\n\nThis is an overview of all Android versions and their corresponding identifiers for Android developers. Anyone is welcome to open an issue or pull request. Happy developing. @@ -411,7 +427,6 @@ Thanks for your %1$.1f-star rating. ❤️ Image button clicked. This is a toast. - Show code syntax Show Java code snippet Open me 🌐 Type here @@ -430,7 +445,6 @@ Error loading layout Error loading code An error occurred while checking for updates - Data and ads consent Analytics storage Ad storage Ad user data diff --git a/app/src/main/res/xml/preferences_settings.xml b/app/src/main/res/xml/preferences_settings.xml index 0a02bec5..a39bf3a0 100644 --- a/app/src/main/res/xml/preferences_settings.xml +++ b/app/src/main/res/xml/preferences_settings.xml @@ -75,21 +75,21 @@ app:title="@string/privacy_policy"> + android:data="https://mihaicristiancondrea.github.io/profile/#privacy-policy-end-user-software" /> + android:data="https://mihaicristiancondrea.github.io/profile/#terms-of-service-end-user-software" /> + android:data="https://mihaicristiancondrea.github.io/profile/#code-of-conduct" /> > result = new AtomicReference<>(); repository.fetchPromotedApps(result::set); diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/DefaultQuizRepositoryTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/DefaultQuizRepositoryTest.java index f8923820..8d258ad8 100644 --- a/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/DefaultQuizRepositoryTest.java +++ b/app/src/test/java/com/d4rk/androidtutorials/java/data/repository/DefaultQuizRepositoryTest.java @@ -29,16 +29,12 @@ public void loadQuestionsReturnsLocalData() throws InterruptedException { assertTrue(latch.await(1, TimeUnit.SECONDS)); } - private static class FakeQuizLocalDataSource implements QuizLocalDataSource { - private final List questions; - - FakeQuizLocalDataSource(List questions) { - this.questions = questions; - } + private record FakeQuizLocalDataSource( + List questions) implements QuizLocalDataSource { @Override - public void loadQuestions(QuestionsCallback callback) { - callback.onResult(questions); + public void loadQuestions(QuestionsCallback callback) { + callback.onResult(questions); + } } - } } diff --git a/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeViewModelTest.java b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeViewModelTest.java index 50f3026b..b59b860c 100644 --- a/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeViewModelTest.java +++ b/app/src/test/java/com/d4rk/androidtutorials/java/ui/screens/home/HomeViewModelTest.java @@ -60,33 +60,21 @@ public void uiStateHandlesEmptyPromotedApps() { assertTrue(state.promotedApps().isEmpty()); } - static class FakeHomeRepository implements HomeRepository { - final String dailyTip; - final List apps; - - FakeHomeRepository(String dailyTip, List apps) { - this.dailyTip = dailyTip; - this.apps = apps; - } - - @Override - public String getPlayStoreUrl() { - return ""; - } - - @Override - public String getAppPlayStoreUrl(String packageName) { - return ""; - } - - @Override - public String getDailyTip() { - return dailyTip; - } + record FakeHomeRepository(String dailyTip, List apps) implements HomeRepository { @Override - public void fetchPromotedApps(PromotedAppsCallback callback) { - callback.onResult(apps); + public String getPlayStoreUrl() { + return ""; + } + + @Override + public String getAppPlayStoreUrl(String packageName) { + return ""; + } + + @Override + public void fetchPromotedApps(PromotedAppsCallback callback) { + callback.onResult(apps); + } } - } }