feat: enhance onboarding experience by adding human verification slide; implement OnboardingHumanFragment with interactive checkboxes, integrate PalestineRainView for animations, and update onboarding layouts and resources

This commit is contained in:
Faded
2025-05-28 00:42:30 +05:00
parent a86dca0d2c
commit 51a7eb7f1a
13 changed files with 639 additions and 51 deletions

View File

@@ -6,6 +6,7 @@ public abstract class Constants {
public static final String PREFS_NAME = "app_prefs";
public static final String LANGUAGE_KEY = "language";
public static final String COMPLETED_ONBOARDING_KEY = "pref_completed_onboarding";
public static final String PREF_VIDEO_RESOLUTION_WIDTH = "video_resolution_width";
public static final String PREF_VIDEO_RESOLUTION_HEIGHT = "video_resolution_height";

View File

@@ -358,24 +358,19 @@ public class SharedPreferencesManager {
}
// ----- Fix Ended for this class (SharedPreferencesManager_audio_input_source) -----
// ----- Fix Start: Add onboarding preference methods -----
private static final String PREF_SHOW_ONBOARDING = "PREF_SHOW_ONBOARDING";
private static final boolean DEFAULT_SHOW_ONBOARDING = true;
/**
* Returns true if onboarding should be shown (default: true).
*/
// ----- Fix Start for method(onboarding) -----
// Using the proper constant from Constants class
public boolean isShowOnboarding() {
return sharedPreferences.getBoolean(PREF_SHOW_ONBOARDING, DEFAULT_SHOW_ONBOARDING);
// Return true if onboarding hasn't been completed
return !sharedPreferences.getBoolean(Constants.COMPLETED_ONBOARDING_KEY, false);
}
/**
* Sets whether onboarding should be shown.
* @param show true to show onboarding, false to skip
*/
public void setShowOnboarding(boolean show) {
sharedPreferences.edit().putBoolean(PREF_SHOW_ONBOARDING, show).apply();
// Store the opposite value in COMPLETED_ONBOARDING_KEY
// If show is true, it means onboarding is not completed
// If show is false, it means onboarding is completed
sharedPreferences.edit().putBoolean(Constants.COMPLETED_ONBOARDING_KEY, !show).apply();
}
// ----- Fix End: Add onboarding preference methods -----
// ----- Fix End for method(onboarding) -----
}

View File

@@ -1490,11 +1490,11 @@ public class HomeFragment extends Fragment {
private void setupButtonListeners() {
buttonStartStop.setOnClickListener(v -> {
if (recordingState.equals(RecordingState.NONE)) {
startRecording();
} else {
stopRecording();
updateStats();
if (recordingState.equals(RecordingState.NONE)) {
startRecording();
} else {
stopRecording();
updateStats();
}
});

View File

@@ -9,6 +9,7 @@ import android.widget.TextView;
import android.widget.Spinner;
import android.widget.ArrayAdapter;
import com.fadcam.MainActivity;
import com.fadcam.SharedPreferencesManager;
import com.github.appintro.AppIntro;
import com.github.appintro.AppIntroFragment;
@@ -26,6 +27,8 @@ import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.viewpager.widget.ViewPager;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
@@ -34,12 +37,85 @@ import java.util.List;
* OnboardingActivity shows the app intro slides using AppIntro.
*/
public class OnboardingActivity extends AppIntro {
private boolean isLastSlide = false;
private boolean isFirstSlide = true; // Track if we're on the first slide
// Use View instead of Button to avoid class cast exceptions
private View backButton;
private View nextButton;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addSlide(AppIntroCustomLayoutFragment.newInstance(R.layout.onboarding_intro_slide));
addSlide(AppIntroCustomLayoutFragment.newInstance(R.layout.onboarding_language_slide));
addSlide(new OnboardingPermissionsFragment());
addSlide(new OnboardingHumanFragment());
// Hide only the Done button while keeping navigation buttons
setSkipButtonEnabled(false); // No skip button needed
setNextPageSwipeLock(false);
setIndicatorEnabled(true);
// Use wizard mode to replace Skip with Back button
setWizardMode(true);
// Fix for indicator dots - use custom colors
setIndicatorColor(
ContextCompat.getColor(this, R.color.white), // Selected dot
ContextCompat.getColor(this, R.color.gray500) // Unselected dot
);
// Disable color transitions as our slides don't implement SlideBackgroundColorHolder
setColorTransitionsEnabled(false);
// Set fade transition effect between slides
ViewPager viewPager = findViewById(com.github.appintro.R.id.view_pager);
if (viewPager != null) {
viewPager.setPageTransformer(true, new FadePageTransformer());
// Listen for page changes to update navigation buttons
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {
updateNavigationButtons(position);
}
@Override
public void onPageScrollStateChanged(int state) {}
});
}
// Initialize navigation button references
backButton = findViewById(com.github.appintro.R.id.back);
nextButton = findViewById(com.github.appintro.R.id.next);
// Completely remove the Done button
View doneButton = findViewById(com.github.appintro.R.id.done);
if (doneButton != null) {
// First make it invisible
doneButton.setVisibility(View.GONE);
// Zero out its dimensions
ViewGroup.LayoutParams params = doneButton.getLayoutParams();
if (params != null) {
params.width = 0;
params.height = 0;
doneButton.setLayoutParams(params);
}
// Also try to remove it from its parent
ViewGroup parent = (ViewGroup) doneButton.getParent();
if (parent != null) {
parent.removeView(doneButton);
}
}
// Update navigation buttons initially
updateNavigationButtons(0);
getSupportFragmentManager().registerFragmentLifecycleCallbacks(new androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks() {
@Override
public void onFragmentViewCreated(@NonNull androidx.fragment.app.FragmentManager fm, @NonNull Fragment f, @Nullable View v, @Nullable Bundle savedInstanceState) {
@@ -168,6 +244,48 @@ public class OnboardingActivity extends AppIntro {
}, true);
}
// Method to update navigation buttons based on current position
private void updateNavigationButtons(int position) {
ViewPager viewPager = findViewById(com.github.appintro.R.id.view_pager);
if (viewPager == null || viewPager.getAdapter() == null) return;
int slideCount = viewPager.getAdapter().getCount();
isFirstSlide = position == 0;
isLastSlide = position == slideCount - 1;
if (backButton != null) {
backButton.setVisibility(isFirstSlide ? View.INVISIBLE : View.VISIBLE);
}
if (nextButton != null) {
nextButton.setVisibility(isLastSlide ? View.INVISIBLE : View.VISIBLE);
}
}
// Implement a custom page transformer for fade transitions
private static class FadePageTransformer implements ViewPager.PageTransformer {
private static final float MIN_ALPHA = 0.0f;
private static final float MAX_ALPHA = 1.0f;
@Override
public void transformPage(@NonNull View page, float position) {
if (position < -1) { // Page is way off-screen to the left
page.setAlpha(MIN_ALPHA);
} else if (position <= 1) { // Page is visible or entering
// Calculate alpha based on position
float alphaFactor = Math.max(MIN_ALPHA, 1 - Math.abs(position));
page.setAlpha(alphaFactor);
// Apply a slight scale effect
float scaleFactor = Math.max(0.85f, 1 - Math.abs(position * 0.15f));
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
} else { // Page is way off-screen to the right
page.setAlpha(MIN_ALPHA);
}
}
}
/**
* Sets up the language choose button for onboarding, using a Material dialog for selection.
* @param chooseButton The MaterialButton to setup.
@@ -243,18 +361,24 @@ public class OnboardingActivity extends AppIntro {
}
}
/**
* This method is still overridden to handle any case where the done action might be triggered,
* even though we've hidden the done button and are using our custom Start button.
*/
@Override
public void onDonePressed(@Nullable androidx.fragment.app.Fragment currentFragment) {
// Mark onboarding as completed
SharedPreferencesManager.getInstance(this)
.sharedPreferences.edit().putBoolean("PREF_SHOW_ONBOARDING", false).apply();
// Launch MainActivity
Intent intent = new Intent(this, com.fadcam.MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
protected void onDonePressed(Fragment currentFragment) {
super.onDonePressed(currentFragment);
finishOnboarding();
}
public void finishOnboarding() {
// Mark onboarding as completed
SharedPreferencesManager sharedPreferencesManager = SharedPreferencesManager.getInstance(this);
sharedPreferencesManager.sharedPreferences.edit().putBoolean(com.fadcam.Constants.COMPLETED_ONBOARDING_KEY, true).apply();
// Return to MainActivity or just finish
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
}

View File

@@ -0,0 +1,109 @@
package com.fadcam.ui;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.graphics.PorterDuff;
import com.google.android.material.button.MaterialButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import com.airbnb.lottie.LottieAnimationView;
import com.fadcam.R;
public class OnboardingHumanFragment extends Fragment {
private boolean checked1 = false;
private boolean checked2 = false;
private MaterialButton continueButton;
private ImageView icon1, icon2;
private TextView label1, label2;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.onboarding_human_slide, container, false);
LinearLayout row1 = v.findViewById(R.id.checkbox_row_1);
LinearLayout row2 = v.findViewById(R.id.checkbox_row_2);
icon1 = v.findViewById(R.id.checkbox_icon_1);
icon2 = v.findViewById(R.id.checkbox_icon_2);
label1 = v.findViewById(R.id.checkbox_label_1);
label2 = v.findViewById(R.id.checkbox_label_2);
continueButton = v.findViewById(R.id.btn_human_continue);
// Find the PalestineRainView and prevent it from intercepting touch events
View palestineRainView = v.findViewById(R.id.palestineRainView);
if (palestineRainView != null) {
palestineRainView.setClickable(false);
palestineRainView.setFocusable(false);
}
// Configure the LottieAnimationView
LottieAnimationView lottieHuman = v.findViewById(R.id.lottieHuman);
if (lottieHuman != null) {
lottieHuman.setSpeed(0.8f);
}
// Set initial button state
continueButton.setEnabled(false);
continueButton.setAlpha(0.6f);
// Get the redPastel color
int redPastelColor = ContextCompat.getColor(requireContext(), R.color.redPastel);
View.OnClickListener update = view -> {
continueButton.setEnabled(checked1 && checked2);
continueButton.setAlpha((checked1 && checked2) ? 1f : 0.6f);
};
View.OnClickListener toggle1 = view -> {
checked1 = !checked1;
// Set the appropriate checkbox image and apply redPastel tint when checked
icon1.setImageResource(checked1 ? R.drawable.placeholder_checkbox_checked : R.drawable.placeholder_checkbox_outline);
if (checked1) {
// Apply redPastel tint to the checked checkbox
icon1.setColorFilter(redPastelColor, PorterDuff.Mode.SRC_IN);
} else {
// Clear any color filter for unchecked state
icon1.clearColorFilter();
}
update.onClick(view);
};
View.OnClickListener toggle2 = view -> {
checked2 = !checked2;
// Set the appropriate checkbox image and apply redPastel tint when checked
icon2.setImageResource(checked2 ? R.drawable.placeholder_checkbox_checked : R.drawable.placeholder_checkbox_outline);
if (checked2) {
// Apply redPastel tint to the checked checkbox
icon2.setColorFilter(redPastelColor, PorterDuff.Mode.SRC_IN);
} else {
// Clear any color filter for unchecked state
icon2.clearColorFilter();
}
update.onClick(view);
};
row1.setOnClickListener(toggle1);
icon1.setOnClickListener(toggle1);
label1.setOnClickListener(toggle1);
row2.setOnClickListener(toggle2);
icon2.setOnClickListener(toggle2);
label2.setOnClickListener(toggle2);
continueButton.setOnClickListener(view -> {
if (getActivity() instanceof OnboardingActivity) {
((OnboardingActivity) getActivity()).finishOnboarding();
} else {
requireActivity().finish();
}
});
return v;
}
}

View File

@@ -11,6 +11,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -34,41 +35,42 @@ public class OnboardingPermissionsFragment extends Fragment implements SlidePoli
private MaterialButton grantButton;
private int permissionRequestCount = 0; // Track how many times requested
private boolean permanentlyDenied = false;
private TextView permissionStatusText; // Text view to show status instead of toasts
private boolean statusToastShown = false; // Flag to prevent multiple toasts
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.onboarding_permissions_slide, container, false);
grantButton = view.findViewById(R.id.permissions_grant_button);
permissionStatusText = view.findViewById(R.id.permission_status_text);
if (permissionStatusText != null) {
permissionStatusText.setVisibility(View.GONE); // Hide initially
}
grantButton.setEnabled(true);
grantButton.setAlpha(1f);
grantButton.setOnClickListener(v -> {
if (permissionsGranted) {
// Do nothing, or optionally show a toast
Toast.makeText(requireContext(), R.string.permissions_granted, Toast.LENGTH_SHORT).show();
showPermissionStatus(R.string.permissions_granted, true);
} else {
// Always show the runtime permission dialog, even if permanently denied
requestAllPermissionsAlways();
}
});
checkPermissionsAndUpdateUI();
// ----- Fix Start: Always show runtime dialog if needed on first load -----
if (!permissionsGranted && !permanentlyDenied) {
requestAllPermissionsAlways();
}
// ----- Fix End: Always show runtime dialog if needed on first load -----
// ----- Fix Start: Add Open Settings link logic -----
View openSettingsLink = view.findViewById(R.id.open_settings_link);
if (openSettingsLink != null) {
openSettingsLink.setOnClickListener(v2 -> {
showManualPermissionDialog();
Toast.makeText(requireContext(), R.string.open_settings, Toast.LENGTH_SHORT).show();
showPermissionStatus(R.string.open_settings, false);
});
}
// ----- Fix End: Add Open Settings link logic -----
// ----- Fix Start: Add Disable Battery Optimization button logic -----
MaterialButton batteryOptButton = view.findViewById(R.id.disable_battery_optimization_button);
if (batteryOptButton != null) {
android.os.PowerManager pm = (android.os.PowerManager) requireContext().getSystemService(android.content.Context.POWER_SERVICE);
@@ -84,10 +86,21 @@ public class OnboardingPermissionsFragment extends Fragment implements SlidePoli
startActivity(intent);
});
}
// ----- Fix End: Add Disable Battery Optimization button logic -----
return view;
}
private void showPermissionStatus(int stringResId, boolean success) {
if (permissionStatusText != null) {
permissionStatusText.setText(stringResId);
permissionStatusText.setTextColor(ContextCompat.getColor(requireContext(),
success ? R.color.green : R.color.redPastel));
permissionStatusText.setVisibility(View.VISIBLE);
} else if (!statusToastShown) {
Toast.makeText(requireContext(), stringResId, Toast.LENGTH_SHORT).show();
statusToastShown = true;
}
}
private void requestAllPermissionsAlways() {
// Always request, system will ignore already granted
List<String> permissions = new ArrayList<>();
@@ -155,20 +168,17 @@ public class OnboardingPermissionsFragment extends Fragment implements SlidePoli
grantButton.setEnabled(false);
grantButton.setAlpha(0.5f);
grantButton.setText(R.string.permissions_granted);
showPermissionStatus(R.string.permissions_granted, true);
} else {
grantButton.setEnabled(true);
grantButton.setAlpha(1f);
grantButton.setText(R.string.grant_permissions);
}
if (permissionsGranted) {
Toast.makeText(requireContext(), R.string.permissions_granted, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onResume() {
super.onResume();
// Always re-check permission state when returning to this fragment
checkPermissionsAndUpdateUI();
}
@@ -188,9 +198,7 @@ public class OnboardingPermissionsFragment extends Fragment implements SlidePoli
@Override
public void onUserIllegallyRequestedNextPage() {
// Show toast and block navigation if permissions not granted
Toast.makeText(requireContext(), R.string.permissions_note, Toast.LENGTH_SHORT).show();
// Optionally, trigger permission dialog again
showPermissionStatus(R.string.permissions_note, false);
if (grantButton != null) {
grantButton.performClick();
}

View File

@@ -0,0 +1,175 @@
package com.fadcam.ui;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Shader;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.fadcam.R;
import java.util.Random;
/**
* PalestineRainView: Animated falling icons for human verification slide.
* Uses palestine.png as the icon.
*/
public class PalestineRainView extends View {
private static class IconDrop {
float y, speed, offset;
float tilt;
}
private IconDrop[] drops;
private int numColumns;
private int iconSizePx;
private int gapPx;
private int viewWidth, viewHeight;
private Bitmap iconBitmap;
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Handler handler = new Handler();
private static final int FRAME_DELAY = 24; // ms - slightly slower animation
private static final float SPEED_MIN = 2.0f; // slower minimum speed
private static final float SPEED_MAX = 4.5f; // slower maximum speed
private final Random random = new Random();
public PalestineRainView(Context context) {
super(context);
init(context);
}
public PalestineRainView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public PalestineRainView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
// Set this view to not intercept touch events
setClickable(false);
setFocusable(false);
iconSizePx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 28, context.getResources().getDisplayMetrics());
gapPx = 0; // denser, no gap
iconBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.palestine_main);
paint.setAlpha(255); // Full opacity
startAnimation();
}
private void startAnimation() {
handler.post(frameRunnable);
}
private final Runnable frameRunnable = new Runnable() {
@Override
public void run() {
updateDrops();
invalidate();
handler.postDelayed(this, FRAME_DELAY);
}
};
private void updateDrops() {
if (drops == null) return;
for (int i = 0; i < numColumns; i++) {
IconDrop drop = drops[i];
drop.y += drop.speed;
if (drop.y > viewHeight) {
drop.y = -iconSizePx - drop.offset;
drop.speed = SPEED_MIN + random.nextFloat() * 0.7f;
drop.offset = random.nextInt(iconSizePx * 2);
if (random.nextBoolean()) {
drop.tilt = -32f + random.nextFloat() * 20f;
} else {
drop.tilt = 12f + random.nextFloat() * 20f;
}
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (iconBitmap == null || drops == null) return;
Paint iconPaint = new Paint(paint);
android.graphics.LinearGradient iconGradient = new android.graphics.LinearGradient(
0, 0, 0, iconSizePx,
0xFFFFFFFF, 0x00FFFFFF, android.graphics.Shader.TileMode.CLAMP
);
iconPaint.setShader(iconGradient);
if (numColumns == 1) {
float x = (viewWidth - iconSizePx) / 2f;
IconDrop drop = drops[0];
Rect dest = new Rect((int)x, (int)drop.y, (int)x + iconSizePx, (int)drop.y + iconSizePx);
canvas.save();
canvas.rotate(drop.tilt, x + iconSizePx / 2f, drop.y + iconSizePx / 2f);
canvas.drawBitmap(iconBitmap, null, dest, iconPaint);
canvas.restore();
} else {
for (int i = 0; i < numColumns; i++) {
float x = i * (iconSizePx + gapPx);
IconDrop drop = drops[i];
float y = drop.y;
Rect dest = new Rect((int)x, (int)y, (int)x + iconSizePx, (int)y + iconSizePx);
canvas.save();
canvas.rotate(drop.tilt, x + iconSizePx / 2f, y + iconSizePx / 2f);
canvas.drawBitmap(iconBitmap, null, dest, iconPaint);
canvas.restore();
}
}
int gradientHeight = (int) (viewHeight * 0.4f);
Paint gradPaint = new Paint();
LinearGradient grad = new LinearGradient(
0, viewHeight,
0, viewHeight - gradientHeight,
0xCC000000, 0x00000000,
Shader.TileMode.CLAMP
);
gradPaint.setShader(grad);
canvas.drawRect(0, viewHeight - gradientHeight, viewWidth, viewHeight, gradPaint);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = w;
viewHeight = h;
numColumns = Math.max(2, w / (iconSizePx + gapPx) * 2);
drops = new IconDrop[numColumns];
for (int i = 0; i < numColumns; i++) {
drops[i] = new IconDrop();
drops[i].y = -random.nextInt(h + iconSizePx);
drops[i].speed = SPEED_MIN + random.nextFloat() * 0.7f;
drops[i].offset = random.nextInt(iconSizePx * 2);
if (random.nextBoolean()) {
drops[i].tilt = -32f + random.nextFloat() * 20f;
} else {
drops[i].tilt = 12f + random.nextFloat() * 20f;
}
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
handler.removeCallbacksAndMessages(null);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Don't intercept touch events, pass them to parent
return false;
}
}

View File

@@ -1643,12 +1643,12 @@ public class SettingsFragment extends Fragment {
String newLangCode = getLanguageCode(which);
if (!newLangCode.equals(sharedPreferencesManager.getLanguage())) {
saveLanguagePreference(newLangCode);
if(getActivity() instanceof MainActivity){
if(getActivity() instanceof MainActivity){
((MainActivity) requireActivity()).applyLanguage(newLangCode);
} else {
Toast.makeText(getContext(), "Language changed. Restart app to apply.", Toast.LENGTH_LONG).show();
}
} else {
Toast.makeText(getContext(), "Language changed. Restart app to apply.", Toast.LENGTH_LONG).show();
}
}
chooseButton.setText(languages[which]);
dialog.dismiss();
})

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/amoled_surface"
android:fillViewport="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.fadcam.ui.PalestineRainView
android:id="@+id/palestineRainView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.35" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="120dp"
android:paddingTop="@dimen/onboarding_nav_margin"
android:paddingBottom="@dimen/onboarding_nav_margin">
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/lottieHuman"
android:layout_width="180dp"
android:layout_height="120dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
app:lottie_fileName="document_rules_green_checkmark.json"
app:lottie_autoPlay="true"
app:lottie_loop="true" />
<TextView
android:id="@+id/tvHumanTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/onboarding_human_title"
android:textSize="@dimen/onboarding_title_size"
android:textStyle="bold"
android:fontFamily="monospace"
android:textColor="@color/redPastel"
android:gravity="center"
android:layout_marginLeft="@dimen/onboarding_margin"
android:layout_marginRight="@dimen/onboarding_margin"/>
<TextView
android:id="@+id/tvHumanDesc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/onboarding_human_desc"
android:textSize="@dimen/onboarding_desc_size"
android:fontFamily="monospace"
android:textColor="?android:attr/textColorSecondary"
android:gravity="center"
android:layout_marginTop="@dimen/onboarding_margin"
android:layout_marginLeft="@dimen/onboarding_margin"
android:layout_marginRight="@dimen/onboarding_margin"/>
<!-- Custom Checkbox Row 1 -->
<LinearLayout
android:id="@+id/checkbox_row_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="@dimen/onboarding_margin"
android:layout_marginLeft="@dimen/onboarding_margin"
android:layout_marginRight="@dimen/onboarding_margin"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/checkbox_icon_1"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/placeholder_checkbox_outline"
android:contentDescription="@string/onboarding_human_checkbox1"
android:layout_marginRight="12dp"
android:padding="2dp"/>
<TextView
android:id="@+id/checkbox_label_1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/onboarding_human_checkbox1"
android:textColor="@color/white"
android:fontFamily="monospace"
android:textSize="@dimen/onboarding_desc_size"/>
</LinearLayout>
<!-- Custom Checkbox Row 2 -->
<LinearLayout
android:id="@+id/checkbox_row_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginTop="12dp"
android:layout_marginLeft="@dimen/onboarding_margin"
android:layout_marginRight="@dimen/onboarding_margin"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/checkbox_icon_2"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/placeholder_checkbox_outline"
android:contentDescription="@string/onboarding_human_checkbox2"
android:layout_marginRight="12dp"
android:padding="2dp"/>
<TextView
android:id="@+id/checkbox_label_2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/onboarding_human_checkbox2"
android:textColor="@color/white"
android:fontFamily="monospace"
android:textSize="@dimen/onboarding_desc_size"/>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_human_continue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/onboarding_human_button"
android:fontFamily="monospace"
android:textSize="@dimen/onboarding_button_text_size"
android:textColor="@color/white"
android:backgroundTint="@color/redPastel"
app:cornerRadius="12dp"
android:layout_marginTop="@dimen/onboarding_margin"
android:layout_marginLeft="@dimen/onboarding_nav_margin"
android:layout_marginRight="@dimen/onboarding_nav_margin"/>
</LinearLayout>
</RelativeLayout>
</ScrollView>

View File

@@ -57,13 +57,30 @@
android:layout_marginTop="@dimen/onboarding_margin"
android:layout_marginBottom="@dimen/onboarding_margin" />
<TextView
android:id="@+id/permission_status_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/permissions_granted"
android:textSize="@dimen/onboarding_desc_size"
android:fontFamily="monospace"
android:textColor="@color/green"
android:gravity="center"
android:visibility="gone"
android:paddingLeft="@dimen/onboarding_margin"
android:paddingRight="@dimen/onboarding_margin"
app:layout_constraintTop_toBottomOf="@id/permissionsDescription"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="8dp" />
<LinearLayout
android:id="@+id/permissionsListContainer"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:orientation="horizontal"
android:gravity="center"
app:layout_constraintTop_toBottomOf="@id/permissionsDescription"
app:layout_constraintTop_toBottomOf="@id/permission_status_text"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="@dimen/onboarding_margin"

View File

@@ -3,6 +3,7 @@
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="gray">#1f1e23</color>
<color name="gray500">#9E9E9E</color>
<color name="redPastel">#E43C3C</color>
<color name="greenPastel">#77DD77</color>
<color name="colorPrimary">#cfbafd</color>
@@ -17,6 +18,7 @@
<color name="danger_red">#D32F2F</color>
<color name="danger_red_light">#F44336</color>
<color name="colorError">#D32F2F</color>
<color name="green">#4CAF50</color>
<!-- Added for Trash Item Remaining Time Display -->
<color name="colorWarning">#FFA726</color> <!-- Orange A400 -->

View File

@@ -453,4 +453,10 @@ Recommended: 192000 bps, 48000 Hz for most users.</string>
<string name="disable_battery_optimization">Disable Battery Optimization</string>
</resources>
<string name="onboarding_human_title">Verify if you are a human</string>
<string name="onboarding_human_desc">Hmmmm... alright. These rules are non-negotiable. If you can\'t agree, please uninstall FadCam right now.</string>
<string name="onboarding_human_checkbox1">I won\'t use FadCam to break any laws, invade anyone\'s privacy, or be a creep. The dev\'s out of the picture if I mess up. (That\'s on me.)</string>
<string name="onboarding_human_checkbox2">I already love FadCam before even using it. (Not optional)</string>
<string name="onboarding_human_button">Let me in!</string>
</resources>

View File

@@ -18,4 +18,5 @@ android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true
org.gradle.configuration-cache=true