From b388109f0bb2e34bf3800ab10eef991064ab054e Mon Sep 17 00:00:00 2001 From: Faded Date: Fri, 10 Jan 2025 15:41:48 +0500 Subject: [PATCH 01/10] feature: added debug toggle button --- app/src/main/java/com/fadcam/Constants.java | 1 + app/src/main/java/com/fadcam/Log.java | 25 ++- .../com/fadcam/SharedPreferencesManager.java | 4 + .../java/com/fadcam/ui/SettingsFragment.java | 14 ++ app/src/main/res/drawable/ic_debug.xml | 10 + app/src/main/res/layout/fragment_settings.xml | 189 ++++++++++-------- app/src/main/res/values/strings.xml | 17 +- 7 files changed, 166 insertions(+), 94 deletions(-) create mode 100644 app/src/main/res/drawable/ic_debug.xml diff --git a/app/src/main/java/com/fadcam/Constants.java b/app/src/main/java/com/fadcam/Constants.java index 9f040e96..ead897ce 100644 --- a/app/src/main/java/com/fadcam/Constants.java +++ b/app/src/main/java/com/fadcam/Constants.java @@ -15,6 +15,7 @@ public abstract class Constants { public static final String PREF_BOTH_TORCHES_ENABLED = "both_torches_enabled"; public static final String PREF_SELECTED_TORCH_SOURCE = "selected_torch_source"; public static final String PREF_LOCATION_DATA = "location_data"; + public static final String PREF_DEBUG_DATA = "debug_data"; public static final String PREF_WATERMARK_OPTION = "watermark_option"; public static final String PREF_VIDEO_CODEC = "video_codec"; diff --git a/app/src/main/java/com/fadcam/Log.java b/app/src/main/java/com/fadcam/Log.java index 5c8db0c3..d245a706 100644 --- a/app/src/main/java/com/fadcam/Log.java +++ b/app/src/main/java/com/fadcam/Log.java @@ -29,11 +29,26 @@ public class Log { private static Uri fileUri; + private static boolean isDebugEnabled = false; + public static void init(Context context) { Log.context = context; + + // Check SharedPreferences for debug setting + SharedPreferencesManager sharedPreferencesManager = SharedPreferencesManager.getInstance(context); + isDebugEnabled = sharedPreferencesManager.isDebugLoggingEnabled(); - createHtmlFile(context, "debug.html"); + if (isDebugEnabled) { + createHtmlFile(context, "debug.html"); + } + } + + public static void setDebugEnabled(boolean enabled) { + isDebugEnabled = enabled; + if (enabled) { + createHtmlFile(context, "debug.html"); + } } private static String getCurrentTimeStamp() { @@ -42,16 +57,22 @@ private static String getCurrentTimeStamp() { } public static void d(String tag, String message) { + if (!isDebugEnabled) return; + String logMessage = "" + getCurrentTimeStamp() + " INFO: [" + tag + "]" + message + ""; appendHtmlToFile(logMessage); } public static void w(String tag, String message) { + if (!isDebugEnabled) return; + String logMessage = "" + getCurrentTimeStamp() + " WARNING: [" + tag + "]" + message + ""; appendHtmlToFile(logMessage); } public static void e(String tag, Object... objects) { + if (!isDebugEnabled) return; + StringBuilder message = new StringBuilder(); for(Object object: objects) { @@ -99,6 +120,8 @@ public static Uri createHtmlFile(Context context, String fileName) { } public static void appendHtmlToFile(String htmlContent) { + if (!isDebugEnabled) return; + OutputStream outputStream = null; try { diff --git a/app/src/main/java/com/fadcam/SharedPreferencesManager.java b/app/src/main/java/com/fadcam/SharedPreferencesManager.java index 8bbdd7ec..c2fa4a31 100644 --- a/app/src/main/java/com/fadcam/SharedPreferencesManager.java +++ b/app/src/main/java/com/fadcam/SharedPreferencesManager.java @@ -68,6 +68,10 @@ public boolean isLocalisationEnabled() { return sharedPreferences.getBoolean(Constants.PREF_LOCATION_DATA, false); } + public boolean isDebugLoggingEnabled() { + return sharedPreferences.getBoolean(Constants.PREF_DEBUG_DATA, false); + } + public String getWatermarkOption() { return sharedPreferences.getString(Constants.PREF_WATERMARK_OPTION, Constants.DEFAULT_WATERMARK_OPTION); } diff --git a/app/src/main/java/com/fadcam/ui/SettingsFragment.java b/app/src/main/java/com/fadcam/ui/SettingsFragment.java index 458136cc..768cb73b 100644 --- a/app/src/main/java/com/fadcam/ui/SettingsFragment.java +++ b/app/src/main/java/com/fadcam/ui/SettingsFragment.java @@ -57,6 +57,7 @@ public class SettingsFragment extends Fragment { private static final String PREF_WATERMARK_OPTION = "watermark_option"; static final String PREF_LOCATION_DATA = "location_data"; + private static final String PREF_DEBUG_DATA = "debug_data"; private static final int REQUEST_PERMISSIONS = 1; private static final String PREF_FIRST_LAUNCH = "first_launch"; @@ -238,6 +239,9 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa MaterialSwitch locationSwitch = view.findViewById(R.id.location_toggle_group); setupLocationSwitch(locationSwitch); + MaterialSwitch debugSwitch = view.findViewById(R.id.debug_toggle_group); + setupDebugSwitch(debugSwitch); + // Initialize the Review Button MaterialButton reviewButton = view.findViewById(R.id.review_button); reviewButton.setOnClickListener(v -> openInAppBrowser("https://forms.gle/DvUoc1v9kB2bkFiS6")); @@ -288,6 +292,16 @@ private void setupLocationSwitch(MaterialSwitch locationSwitch) { }); } + private void setupDebugSwitch(MaterialSwitch debugSwitch) { + boolean isDebugEnabled = sharedPreferencesManager.isDebugLoggingEnabled(); + debugSwitch.setChecked(isDebugEnabled); + + debugSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + sharedPreferencesManager.sharedPreferences.edit().putBoolean(PREF_DEBUG_DATA, isChecked).apply(); + Log.setDebugEnabled(isChecked); + vibrateTouch(); + }); + } private void showLocationPermissionDialog(MaterialSwitch locationSwitch) { new MaterialAlertDialogBuilder(requireContext()) diff --git a/app/src/main/res/drawable/ic_debug.xml b/app/src/main/res/drawable/ic_debug.xml new file mode 100644 index 00000000..7853f61c --- /dev/null +++ b/app/src/main/res/drawable/ic_debug.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml index 6a219fbe..7372f32e 100644 --- a/app/src/main/res/layout/fragment_settings.xml +++ b/app/src/main/res/layout/fragment_settings.xml @@ -43,7 +43,6 @@ android:textStyle="bold" /> - + android:layout_marginEnd="8dp"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -437,19 +398,6 @@ android:layout_marginBottom="16dp"/> - - - - - - - - - - - - - + + + + + + + + + - - - - - - + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginBottom="10dp"> + + android:src="@drawable/ic_debug" + android:layout_marginEnd="8dp"/> - + + android:layout_weight="1" + android:orientation="vertical"> + + + + + + + + - + + + + + + + + - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 391d04a4..98bbd8ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ Send email Join community for help, sharing ideas, and future app updates Privacy Information - © 2024 fadedhood.com\nLicensed under GPL-3.0 + 2024 fadedhood.com\nLicensed under GPL-3.0 7680x4320 3840x2160 @@ -112,7 +112,7 @@ Location Data Include location data in watermark. \nRequires GPS enabled \n(Default: Disabled) Enable phone\'s GPS for adding latitude and longitude data to the watermark. - Write a Review 🌟 + Write a Review Choose Clock Display Time Only English Date with time @@ -120,7 +120,7 @@ Preview Area Alert! This is a restricted area. Start recording to gain access. - You\’ve found the secret button! It does nothing lol + You’ve found the secret button! It does nothing lol Hurry! The system is about to explode… or maybe it just needs a recording to calm down. Well, this is awkward now… What color is your Bugatti? @@ -199,10 +199,10 @@ Delete Forever? Are you sure you want to delete this video? Delete - Eradicate Video(s)? 💣💣 - Are you absolutely, positively sure you want to nuke these video(s) out of existence? 🚀💥 - Yes, Nuke \'Em! 🌋 - No, Keep \'Em! 😅 + Eradicate Video(s)? + Are you absolutely, positively sure you want to nuke these video(s) out of existence? + Yes, Nuke \'Em! + No, Keep \'Em! App\'s Language Select from dropdown.\n(Default: English) Location Permission @@ -229,4 +229,7 @@ Cancel Cannot change torch while recording Both Torches + Debug Logging + Enable debug log file generation \n(Default: Disabled) + When enabled, a debug log file will be created in the Downloads/FadCam folder. This can help diagnose app issues. \ No newline at end of file From e38885ec4facc8a0e529a3065a2054b81ade8b44 Mon Sep 17 00:00:00 2001 From: Faded Date: Fri, 10 Jan 2025 15:48:02 +0500 Subject: [PATCH 02/10] fix: debug file name prefix added --- app/src/main/java/com/fadcam/Log.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/fadcam/Log.java b/app/src/main/java/com/fadcam/Log.java index d245a706..0d8052ff 100644 --- a/app/src/main/java/com/fadcam/Log.java +++ b/app/src/main/java/com/fadcam/Log.java @@ -96,21 +96,24 @@ else if(object instanceof Exception) public static Uri createHtmlFile(Context context, String fileName) { try { - Uri existingFileUri = checkIfFileExists(context, fileName); + // Use a static filename for debug log + String debugFileName = "FADCAM_debug.html"; + + Uri existingFileUri = checkIfFileExists(context, debugFileName); if (existingFileUri != null) { return existingFileUri; } ContentValues contentValues = new ContentValues(); - contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); + contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, debugFileName); contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "text/html"); contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/FadCam"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { fileUri = context.getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues); } else { - fileUri = Uri.parse("file://" + context.getExternalFilesDir(null).getPath() + "/Download/" + fileName); + fileUri = Uri.parse("file://" + context.getExternalFilesDir(null).getPath() + "/Download/" + debugFileName); } } catch (Exception e) { e.printStackTrace(); From 97a52532777a795e741ac97b624aaad2a060b102 Mon Sep 17 00:00:00 2001 From: Faded Date: Fri, 10 Jan 2025 18:19:39 +0500 Subject: [PATCH 03/10] feat(torch): add system-wide torch toggle shortcut Changed files: - app/src/main/java/com/fadcam/MainActivity.java - app/src/main/java/com/fadcam/receivers/TorchToggleReceiver.java - app/src/main/java/com/fadcam/services/TorchService.java - app/src/main/AndroidManifest.xml --- app/src/main/AndroidManifest.xml | 37 ++- app/src/main/java/com/fadcam/Constants.java | 6 +- .../main/java/com/fadcam/MainActivity.java | 30 +++ .../java/com/fadcam/TorchToggleActivity.java | 76 +++++++ .../fadcam/receivers/TorchStateReceiver.java | 28 +++ .../fadcam/receivers/TorchToggleReceiver.java | 44 ++++ .../com/fadcam/services/TorchService.java | 210 ++++++++++++------ app/src/main/res/drawable/ic_torch.xml | 10 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/shortcuts.xml | 14 ++ 10 files changed, 385 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/com/fadcam/TorchToggleActivity.java create mode 100644 app/src/main/java/com/fadcam/receivers/TorchStateReceiver.java create mode 100644 app/src/main/java/com/fadcam/receivers/TorchToggleReceiver.java create mode 100644 app/src/main/res/drawable/ic_torch.xml create mode 100644 app/src/main/res/xml/shortcuts.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 33796ae8..0ed9be96 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,7 +35,8 @@ - + + @@ -43,14 +44,12 @@ - - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/fadcam/Constants.java b/app/src/main/java/com/fadcam/Constants.java index ead897ce..5de20a35 100644 --- a/app/src/main/java/com/fadcam/Constants.java +++ b/app/src/main/java/com/fadcam/Constants.java @@ -12,8 +12,9 @@ public abstract class Constants { public static final String PREF_VIDEO_FRAME_RATE = "video_frame_rate"; public static final String PREF_CAMERA_SELECTION = "camera_selection"; public static final String PREF_IS_PREVIEW_ENABLED = "isPreviewEnabled"; - public static final String PREF_BOTH_TORCHES_ENABLED = "both_torches_enabled"; - public static final String PREF_SELECTED_TORCH_SOURCE = "selected_torch_source"; + public static final String PREF_BOTH_TORCHES_ENABLED = "pref_both_torches_enabled"; + public static final String PREF_SELECTED_TORCH_SOURCE = "pref_selected_torch_source"; + public static final String PREF_TORCH_STATE = "pref_torch_state"; public static final String PREF_LOCATION_DATA = "location_data"; public static final String PREF_DEBUG_DATA = "debug_data"; public static final String PREF_WATERMARK_OPTION = "watermark_option"; @@ -38,6 +39,7 @@ public abstract class Constants { public static final String INTENT_EXTRA_RECORDING_STATE = "RECORDING_STATE"; public static final String INTENT_EXTRA_RECORDING_START_TIME = "RECORDING_START_TIME"; public static final String INTENT_EXTRA_TORCH_STATE = "TORCH_STATE"; + public static final String INTENT_EXTRA_TORCH_STATE_CHANGED = "TORCH_STATE_CHANGED"; public static final String RECORDING_DIRECTORY = "FadCam"; public static final String RECORDING_FILE_EXTENSION = "mp4"; diff --git a/app/src/main/java/com/fadcam/MainActivity.java b/app/src/main/java/com/fadcam/MainActivity.java index ae7b5467..9f96e418 100644 --- a/app/src/main/java/com/fadcam/MainActivity.java +++ b/app/src/main/java/com/fadcam/MainActivity.java @@ -1,11 +1,17 @@ package com.fadcam; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.drawable.Icon; +import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; +import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.viewpager2.widget.ViewPager2; @@ -14,6 +20,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView; import java.io.File; +import java.util.Collections; import java.util.Locale; public class MainActivity extends AppCompatActivity { @@ -88,6 +95,29 @@ public void onPageSelected(int position) { File osmdroidTileCache = new File(osmdroidBasePath, "tiles"); org.osmdroid.config.Configuration.getInstance().setOsmdroidBasePath(osmdroidBasePath); org.osmdroid.config.Configuration.getInstance().setOsmdroidTileCache(osmdroidTileCache); + + // Add dynamic shortcut for torch + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + createDynamicShortcuts(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.N_MR1) + private void createDynamicShortcuts() { + ShortcutManager shortcutManager = getSystemService(ShortcutManager.class); + + // Torch Toggle Shortcut + Intent torchIntent = new Intent(this, TorchToggleActivity.class); + torchIntent.setAction(Intent.ACTION_VIEW); + + ShortcutInfo torchShortcut = new ShortcutInfo.Builder(this, "torch_toggle") + .setShortLabel(getString(R.string.torch_shortcut_short_label)) + .setLongLabel(getString(R.string.torch_shortcut_long_label)) + .setIcon(Icon.createWithResource(this, R.drawable.ic_flashlight_on)) + .setIntent(torchIntent) + .build(); + + shortcutManager.setDynamicShortcuts(Collections.singletonList(torchShortcut)); } public void applyLanguage(String languageCode) { diff --git a/app/src/main/java/com/fadcam/TorchToggleActivity.java b/app/src/main/java/com/fadcam/TorchToggleActivity.java new file mode 100644 index 00000000..8153812d --- /dev/null +++ b/app/src/main/java/com/fadcam/TorchToggleActivity.java @@ -0,0 +1,76 @@ +package com.fadcam; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.WindowManager; + +import com.fadcam.services.TorchService; + +public class TorchToggleActivity extends Activity { + private static final String TAG = "TorchToggleActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Prevent the activity from being visible or interactive + getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + + // Prevent app from coming to foreground + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + Log.d(TAG, "TorchToggleActivity onCreate called"); + Log.d(TAG, "Intent: " + getIntent()); + + try { + // Create an intent for TorchService + Intent torchIntent = new Intent(this, TorchService.class); + torchIntent.setAction(Constants.INTENT_ACTION_TOGGLE_TORCH); + + // Log service start attempt + Log.d(TAG, "Attempting to start TorchService"); + + // Start service based on Android version + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Log.d(TAG, "Starting foreground service"); + startForegroundService(torchIntent); + } else { + Log.d(TAG, "Starting service"); + startService(torchIntent); + } + + // Log success + Log.d(TAG, "TorchService started successfully"); + } catch (Exception e) { + // Log any errors + Log.e(TAG, "Error starting TorchService", e); + } + + // Always finish the activity immediately + finish(); + + // Prevent any animation + overridePendingTransition(0, 0); + } + + @Override + protected void onStop() { + super.onStop(); + // Ensure the activity is completely hidden + finish(); + } + + @Override + protected void onPause() { + super.onPause(); + // Prevent app from coming to foreground + moveTaskToBack(true); + } +} diff --git a/app/src/main/java/com/fadcam/receivers/TorchStateReceiver.java b/app/src/main/java/com/fadcam/receivers/TorchStateReceiver.java new file mode 100644 index 00000000..54bb8aff --- /dev/null +++ b/app/src/main/java/com/fadcam/receivers/TorchStateReceiver.java @@ -0,0 +1,28 @@ +package com.fadcam.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import com.fadcam.Constants; +import com.fadcam.ui.HomeFragment; + +public class TorchStateReceiver extends BroadcastReceiver { + private HomeFragment homeFragment; + + public TorchStateReceiver(HomeFragment homeFragment) { + this.homeFragment = homeFragment; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (Constants.BROADCAST_ON_TORCH_STATE_CHANGED.equals(intent.getAction())) { + boolean isTorchOn = intent.getBooleanExtra(Constants.INTENT_EXTRA_TORCH_STATE, false); + + // Update UI in HomeFragment + if (homeFragment != null) { + homeFragment.updateTorchUI(isTorchOn); + } + } + } +} diff --git a/app/src/main/java/com/fadcam/receivers/TorchToggleReceiver.java b/app/src/main/java/com/fadcam/receivers/TorchToggleReceiver.java new file mode 100644 index 00000000..562a7f9a --- /dev/null +++ b/app/src/main/java/com/fadcam/receivers/TorchToggleReceiver.java @@ -0,0 +1,44 @@ +package com.fadcam.receivers; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.util.Log; + +import com.fadcam.Constants; +import com.fadcam.services.TorchService; + +public class TorchToggleReceiver extends BroadcastReceiver { + private static final String TAG = "TorchToggleReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive called with intent: " + intent); + Log.d(TAG, "Intent action: " + (intent != null ? intent.getAction() : "null")); + Log.d(TAG, "Intent categories: " + (intent != null ? intent.getCategories() : "null")); + + // Create an intent for TorchService + Intent torchIntent = new Intent(context, TorchService.class); + torchIntent.setAction(Constants.INTENT_ACTION_TOGGLE_TORCH); + + try { + // Use a foreground service start to prevent app from opening + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Log.d(TAG, "Starting foreground service for torch toggle"); + context.startForegroundService(torchIntent); + } else { + Log.d(TAG, "Starting service for torch toggle"); + context.startService(torchIntent); + } + } catch (Exception e) { + Log.e(TAG, "Error starting TorchService", e); + } + + // Prevent the shortcut from opening the app + if (intent != null && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) { + Log.d(TAG, "Aborting broadcast to prevent app launch"); + abortBroadcast(); + } + } +} diff --git a/app/src/main/java/com/fadcam/services/TorchService.java b/app/src/main/java/com/fadcam/services/TorchService.java index e61b4f5f..5c2b648c 100644 --- a/app/src/main/java/com/fadcam/services/TorchService.java +++ b/app/src/main/java/com/fadcam/services/TorchService.java @@ -1,31 +1,49 @@ package com.fadcam.services; -import android.app.ActivityManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; +import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.preference.PreferenceManager; import android.util.Log; +import androidx.core.app.NotificationCompat; + import com.fadcam.Constants; +import com.fadcam.R; import com.fadcam.ui.HomeFragment; import java.lang.ref.WeakReference; +import java.util.concurrent.atomic.AtomicBoolean; public class TorchService extends Service { private static final String TAG = "TorchService"; - private boolean isTorchOn = false; - private HandlerThread handlerThread; - private Handler handler; + private static final int NOTIFICATION_ID = 1002; + private static final String CHANNEL_ID = "TorchServiceChannel"; + private SharedPreferences sharedPreferences; + private CameraManager cameraManager; + private NotificationManager notificationManager; + private HandlerThread handlerThread; + private Handler backgroundHandler; + private AtomicBoolean isTorchOn = new AtomicBoolean(false); private static WeakReference homeFragmentRef; + private String selectedTorchSource; + // Static method to set the home fragment public static void setHomeFragment(HomeFragment fragment) { homeFragmentRef = new WeakReference<>(fragment); } @@ -33,93 +51,149 @@ public static void setHomeFragment(HomeFragment fragment) { @Override public void onCreate() { super.onCreate(); - handlerThread = new HandlerThread("TorchThread", android.os.Process.THREAD_PRIORITY_BACKGROUND); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper()); + + // Initialize system services + cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); + notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + + // Create notification channel for Android Oreo and above + createNotificationChannel(); + + // Setup background thread for torch operations + handlerThread = new HandlerThread("TorchServiceThread"); + handlerThread.start(); + backgroundHandler = new Handler(handlerThread.getLooper()) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case 1: // Toggle Torch + toggleTorchInternal(); + break; + } + } + }; + + Log.d(TAG, "TorchService created"); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "Torch Service", + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("Keeps torch service running"); + notificationManager.createNotificationChannel(channel); + } } @Override public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null && Constants.INTENT_ACTION_TOGGLE_TORCH.equals(intent.getAction())) { - // Check if recording is in progress - if (isRecordingInProgress()) { - // Delegate to RecordingService - Intent recordingIntent = new Intent(this, RecordingService.class); - recordingIntent.setAction(Constants.BROADCAST_ON_TORCH_STATE_REQUEST); - startService(recordingIntent); - } else { - handler.post(this::toggleTorch); + if (intent != null) { + String action = intent.getAction(); + if (Constants.INTENT_ACTION_TOGGLE_TORCH.equals(action)) { + // Send message to background thread to toggle torch + backgroundHandler.sendEmptyMessage(1); } } + + // Ensure service stays alive return START_STICKY; } - private boolean isRecordingInProgress() { - // You can check this by either: - // 1. Using SharedPreferences to track recording state - // 2. Checking if RecordingService is running - ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); - for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { - if (RecordingService.class.getName().equals(service.service.getClassName())) { - return true; + private void toggleTorchInternal() { + try { + // Toggle torch state atomically + boolean newState = !isTorchOn.get(); + selectedTorchSource = sharedPreferences.getString(Constants.PREF_SELECTED_TORCH_SOURCE, null); + + if (selectedTorchSource != null) { + // Safely toggle torch + cameraManager.setTorchMode(selectedTorchSource, newState); + isTorchOn.set(newState); + + // Update UI and broadcast state + updateUIAndBroadcastState(newState); + + // Manage foreground service and notification + manageServiceNotification(newState); + + Log.d(TAG, "Torch turned " + (newState ? "ON" : "OFF")); } + } catch (CameraAccessException e) { + Log.e(TAG, "Camera access error: " + e.getMessage()); } - return false; } - private void toggleTorch() { - try { - CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); - boolean useBothTorches = sharedPreferences.getBoolean(Constants.PREF_BOTH_TORCHES_ENABLED, false); - - if (useBothTorches) { - // Toggle both available torches - for (String id : cameraManager.getCameraIdList()) { - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); - Boolean hasFlash = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - if (hasFlash != null && hasFlash) { - cameraManager.setTorchMode(id, !isTorchOn); - } - } - isTorchOn = !isTorchOn; - } else { - // Use selected single torch - String selectedTorchSource = sharedPreferences.getString(Constants.PREF_SELECTED_TORCH_SOURCE, null); - - if (selectedTorchSource != null) { - isTorchOn = !isTorchOn; - cameraManager.setTorchMode(selectedTorchSource, isTorchOn); - Log.d(TAG, "Torch turned " + (isTorchOn ? "ON" : "OFF") + " using source: " + selectedTorchSource); - - // Update UI through callback - if (homeFragmentRef != null && homeFragmentRef.get() != null) { - homeFragmentRef.get().updateTorchUI(isTorchOn); - } - } - } - - // Broadcast state change - Intent intent = new Intent(Constants.BROADCAST_ON_TORCH_STATE_CHANGED); - intent.putExtra(Constants.INTENT_EXTRA_TORCH_STATE, isTorchOn); - sendBroadcast(intent); - - Log.d(TAG, "Torch(es) turned " + (isTorchOn ? "ON" : "OFF")); - } catch (Exception e) { - Log.e(TAG, "Error toggling torch: " + e.getMessage()); + private void updateUIAndBroadcastState(boolean state) { + // Update UI callback if available + if (homeFragmentRef != null && homeFragmentRef.get() != null) { + new Handler(Looper.getMainLooper()).post(() -> + homeFragmentRef.get().updateTorchUI(state) + ); } + + // Broadcast state change + Intent stateIntent = new Intent(Constants.BROADCAST_ON_TORCH_STATE_CHANGED); + stateIntent.putExtra(Constants.INTENT_EXTRA_TORCH_STATE, state); + sendBroadcast(stateIntent); } - @Override - public IBinder onBind(Intent intent) { - return null; + private void manageServiceNotification(boolean isTorchOn) { + if (isTorchOn) { + // Create an intent for turning off the torch + Intent turnOffIntent = new Intent(this, TorchService.class); + turnOffIntent.setAction(Constants.INTENT_ACTION_TOGGLE_TORCH); + + // Create a PendingIntent for the notification + PendingIntent pendingIntent = PendingIntent.getService( + this, + 0, + turnOffIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + // Create and show foreground notification with tap-to-turn-off functionality + Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("Torch is On") + .setContentText("Tap to turn off") + .setSmallIcon(R.drawable.ic_flashlight_on) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setOngoing(true) + .setContentIntent(pendingIntent) // Add tap action to turn off + .build(); + + startForeground(NOTIFICATION_ID, notification); + } else { + // Stop foreground service + stopForeground(true); + } } @Override public void onDestroy() { + // Turn off torch safely + try { + if (isTorchOn.get() && selectedTorchSource != null) { + cameraManager.setTorchMode(selectedTorchSource, false); + } + } catch (Exception e) { + Log.e(TAG, "Error turning off torch: " + e.getMessage()); + } + + // Clean up background thread if (handlerThread != null) { handlerThread.quitSafely(); } + super.onDestroy(); + Log.d(TAG, "TorchService destroyed"); + } + + @Override + public IBinder onBind(Intent intent) { + return null; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_torch.xml b/app/src/main/res/drawable/ic_torch.xml new file mode 100644 index 00000000..8946bdda --- /dev/null +++ b/app/src/main/res/drawable/ic_torch.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 98bbd8ac..c6e99260 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -232,4 +232,7 @@ Debug Logging Enable debug log file generation \n(Default: Disabled) When enabled, a debug log file will be created in the Downloads/FadCam folder. This can help diagnose app issues. + Torch + Toggle Torch + Torch toggle is currently unavailable \ No newline at end of file diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml new file mode 100644 index 00000000..f6ad5faf --- /dev/null +++ b/app/src/main/res/xml/shortcuts.xml @@ -0,0 +1,14 @@ + + + + + + From 37cf3a4b70aa1187b30cc9786527a368aa00bf01 Mon Sep 17 00:00:00 2001 From: Faded Date: Fri, 10 Jan 2025 22:59:59 +0500 Subject: [PATCH 04/10] fix: android 13 torch and camera bug fix, for now added funny strings when torch don't work during recording. --- app/src/main/AndroidManifest.xml | 5 +- app/src/main/java/com/fadcam/Constants.java | 1 + .../com/fadcam/services/RecordingService.java | 216 ++++++++++-------- .../com/fadcam/services/TorchService.java | 22 +- app/src/main/res/values/strings.xml | 8 + 5 files changed, 159 insertions(+), 93 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ed9be96..3fc1e3ba 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -117,8 +117,9 @@ + android:foregroundServiceType="camera" + android:exported="true"> + { if (ReturnCode.isSuccess(session.getReturnCode())) { - Log.d(TAG, "Watermark added successfully."); + android.util.Log.d(TAG, "Watermark added successfully."); // Start monitoring temp files startMonitoring(); @@ -616,7 +652,7 @@ private void executeFFmpegCommand(String ffmpegCommand) { stopSelf(); } else { - Log.e(TAG, "Failed to add watermark: " + session.getFailStackTrace()); + android.util.Log.e(TAG, "Failed to add watermark: " + session.getFailStackTrace()); } }); } @@ -637,15 +673,15 @@ private void checkAndDeleteSpecificTempFile() { if (outputFile.exists()) { // Delete temp file if (tempFileBeingProcessed.delete()) { - Log.d(TAG, "Temp file deleted successfully."); + android.util.Log.d(TAG, "Temp file deleted successfully."); } else { - Log.e(TAG, "Failed to delete temp file."); + android.util.Log.e(TAG, "Failed to delete temp file."); } // Reset tempFileBeingProcessed to null after deletion tempFileBeingProcessed = null; } else { // FADCAM_ file does not exist yet - Log.d(TAG, "Matching " + Constants.RECORDING_DIRECTORY + "_ file not found. Temp file remains."); + android.util.Log.d(TAG, "Matching " + Constants.RECORDING_DIRECTORY + "_ file not found. Temp file remains."); } } } @@ -771,9 +807,9 @@ private void createNotificationChannel() { NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); if(manager != null) { manager.createNotificationChannel(channel); - Log.d(TAG, "Notification channel created"); + android.util.Log.d(TAG, "Notification channel created"); } else { - Log.e(TAG, "NotificationManager is null, unable to create notification channel"); + android.util.Log.e(TAG, "NotificationManager is null, unable to create notification channel"); } } } @@ -834,10 +870,10 @@ private void toggleTorch() { torchManager.setTorchMode(selectedTorchSource, false); } - Log.d(TAG, "Recording torch turned " + (isTorchOn ? "ON" : "OFF") + " using source: " + selectedTorchSource); + android.util.Log.d(TAG, "Recording torch turned " + (isTorchOn ? "ON" : "OFF") + " using source: " + selectedTorchSource); } } catch (CameraAccessException e) { - Log.e(TAG, "Error accessing torch during recording: " + e.getMessage()); + android.util.Log.e(TAG, "Error accessing torch during recording: " + e.getMessage()); } } } diff --git a/app/src/main/java/com/fadcam/services/TorchService.java b/app/src/main/java/com/fadcam/services/TorchService.java index 5c2b648c..a6c057e7 100644 --- a/app/src/main/java/com/fadcam/services/TorchService.java +++ b/app/src/main/java/com/fadcam/services/TorchService.java @@ -8,6 +8,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.ServiceInfo; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; @@ -19,6 +20,7 @@ import android.os.Message; import android.preference.PreferenceManager; import android.util.Log; +import android.widget.Toast; import androidx.core.app.NotificationCompat; @@ -28,6 +30,7 @@ import java.lang.ref.WeakReference; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Random; public class TorchService extends Service { private static final String TAG = "TorchService"; @@ -124,6 +127,7 @@ private void toggleTorchInternal() { } } catch (CameraAccessException e) { Log.e(TAG, "Camera access error: " + e.getMessage()); + showTorchErrorToast(e.getMessage()); } } @@ -165,13 +169,29 @@ private void manageServiceNotification(boolean isTorchOn) { .setContentIntent(pendingIntent) // Add tap action to turn off .build(); - startForeground(NOTIFICATION_ID, notification); + // For Android 13 and above, explicitly start as a foreground service with a type + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA); + } else { + startForeground(NOTIFICATION_ID, notification); + } } else { // Stop foreground service stopForeground(true); } } + private void showTorchErrorToast(String errorMessage) { + // Use string resources for torch error messages + String[] errorMessages = getResources().getStringArray(R.array.torch_error_messages); + String humorousMessage = errorMessages[new Random().nextInt(errorMessages.length)]; + + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> { + Toast.makeText(this, humorousMessage, Toast.LENGTH_LONG).show(); + }); + } + @Override public void onDestroy() { // Turn off torch safely diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6e99260..14b72bfd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -235,4 +235,12 @@ Torch Toggle Torch Torch toggle is currently unavailable + + + Torch is compiling... please don\'t wait 🖥️ + Camera says: No flash today 🚦 + Torch.exe not responding 🔦 + Fix delayed... Developer hit a mental breakpoint lol 🧠 + Patch coming soon™... Developer is debugging life 🐛 + \ No newline at end of file From f46cb1fcd8df97c8a31e134db99e12f0805f64e6 Mon Sep 17 00:00:00 2001 From: Faded Date: Fri, 10 Jan 2025 23:09:14 +0500 Subject: [PATCH 05/10] fix: Video rename won't delete the videos instead increment a copy number --- .../java/com/fadcam/ui/RecordsAdapter.java | 38 ++++++++++--------- app/src/main/res/values/strings.xml | 1 + 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/fadcam/ui/RecordsAdapter.java b/app/src/main/java/com/fadcam/ui/RecordsAdapter.java index f90a5b61..0488042d 100644 --- a/app/src/main/java/com/fadcam/ui/RecordsAdapter.java +++ b/app/src/main/java/com/fadcam/ui/RecordsAdapter.java @@ -46,7 +46,6 @@ public class RecordsAdapter extends RecyclerView.Adapter selectedVideos = new ArrayList<>(); private List videoFiles; - public RecordsAdapter(Context context, List records, OnVideoClickListener clickListener, OnVideoLongClickListener longClickListener) { this.context = context; this.records = records; @@ -261,38 +260,41 @@ private void showRenameDialog(final int position) { builder.show(); } - - private void renameVideo(int position, String newName) { - if (videoFiles == null || position < 0 || position >= videoFiles.size()) { - Toast.makeText(context, R.string.toast_rename_bad_video, Toast.LENGTH_SHORT).show(); - return; - } - // Replace spaces with underscores String formattedName = newName.trim().replace(" ", "_"); File oldFile = videoFiles.get(position); - File newFile = new File(oldFile.getParent(), formattedName + "." + Constants.RECORDING_FILE_EXTENSION); + File parentDir = oldFile.getParentFile(); + String fileExtension = Constants.RECORDING_FILE_EXTENSION; + + // Check for existing files with similar names + File newFile = new File(parentDir, formattedName + "." + fileExtension); + int copyNumber = 1; + + // Find a unique filename + while (newFile.exists()) { + newFile = new File(parentDir, formattedName + "_" + copyNumber + "." + fileExtension); + copyNumber++; + } if (oldFile.renameTo(newFile)) { // Update the list and notify the adapter videoFiles.set(position, newFile); records.set(position, newFile); // Also update the records list if necessary notifyDataSetChanged(); - Toast.makeText(context, R.string.toast_rename_success, Toast.LENGTH_SHORT).show(); + + // Show a toast with the new filename if a copy number was added + String toastMessage = copyNumber > 1 ? + context.getString(R.string.toast_rename_with_copy, newFile.getName()) : + context.getString(R.string.toast_rename_success); + + Toast.makeText(context, toastMessage, Toast.LENGTH_LONG).show(); } else { - Toast.makeText(context, R.string.toast_rename_failed, Toast.LENGTH_SHORT).show(); + Toast.makeText(context, R.string.toast_rename_failed, Toast.LENGTH_LONG).show(); } } - - - - - - - static class RecordViewHolder extends RecyclerView.ViewHolder { ImageView imageViewThumbnail; TextView textViewRecord; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14b72bfd..c2489db4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -196,6 +196,7 @@ Failed to rename video Video renamed successfully Invalid position or video list is null + Renamed to %1$s (duplicate name) Delete Forever? Are you sure you want to delete this video? Delete From 9bc0b73fc564f11249fba2b0809ea761d974c9ee Mon Sep 17 00:00:00 2001 From: Faded Date: Fri, 10 Jan 2025 23:31:12 +0500 Subject: [PATCH 06/10] enhance: replace delete all button to be inside a bottom sheet in red color --- .../java/com/fadcam/ui/RecordsFragment.java | 27 +++++++++- .../layout/bottom_sheet_records_options.xml | 52 +++++++++++++++++++ app/src/main/res/menu/records_menu.xml | 8 ++- app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/layout/bottom_sheet_records_options.xml diff --git a/app/src/main/java/com/fadcam/ui/RecordsFragment.java b/app/src/main/java/com/fadcam/ui/RecordsFragment.java index e084e861..9cb4caa2 100644 --- a/app/src/main/java/com/fadcam/ui/RecordsFragment.java +++ b/app/src/main/java/com/fadcam/ui/RecordsFragment.java @@ -25,6 +25,7 @@ import com.fadcam.Constants; import com.fadcam.R; +import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; @@ -200,6 +201,24 @@ private void deleteSelectedVideos() { loadRecordsList(); } + private void showRecordsSidebar() { + // Create a bottom sheet dialog + BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(requireContext()); + View bottomSheetView = getLayoutInflater().inflate(R.layout.bottom_sheet_records_options, null); + + // Setup delete all option + View deleteAllOption = bottomSheetView.findViewById(R.id.option_delete_all); + deleteAllOption.setOnClickListener(v -> { + bottomSheetDialog.dismiss(); + confirmDeleteAll(); + }); + + // Add more options here in future + + bottomSheetDialog.setContentView(bottomSheetView); + bottomSheetDialog.show(); + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.records_menu, menu); @@ -208,10 +227,16 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { @Override public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.action_delete_all) { + int itemId = item.getItemId(); + + if (itemId == R.id.action_delete_all) { confirmDeleteAll(); return true; + } else if (itemId == R.id.action_more_options) { + showRecordsSidebar(); + return true; } + return super.onOptionsItemSelected(item); } diff --git a/app/src/main/res/layout/bottom_sheet_records_options.xml b/app/src/main/res/layout/bottom_sheet_records_options.xml new file mode 100644 index 00000000..dc187ad0 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_records_options.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/records_menu.xml b/app/src/main/res/menu/records_menu.xml index 6d4d8fa8..25ec532a 100644 --- a/app/src/main/res/menu/records_menu.xml +++ b/app/src/main/res/menu/records_menu.xml @@ -1,9 +1,15 @@ + + android:visible="false" + app:showAsAction="never" /> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index bde5c49a..0f30d00d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,4 +12,6 @@ #FFB300 #4CAF50 #F44336 + #D32F2F + #F44336 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c2489db4..a7a1f617 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -244,4 +244,6 @@ Fix delayed... Developer hit a mental breakpoint lol 🧠 Patch coming soon™... Developer is debugging life 🐛 + More Options + Records Options \ No newline at end of file From e972d0ffec47afd097f8223f94839302a67ac075 Mon Sep 17 00:00:00 2001 From: Faded Date: Sat, 11 Jan 2025 01:06:43 +0500 Subject: [PATCH 07/10] feature: added serial numbering, sorting, video duration and size in thumbnail card, bottom sheet to replace delete all button improved cards design, video info options --- .../java/com/fadcam/ui/RecordsAdapter.java | 277 +++++++++++++----- .../java/com/fadcam/ui/RecordsFragment.java | 201 ++++++++++--- .../res/drawable/file_size_background.xml | 5 + app/src/main/res/drawable/ic_content_copy.xml | 10 + app/src/main/res/drawable/ic_delete_red.xml | 10 + .../layout/bottom_sheet_records_options.xml | 91 ++++-- app/src/main/res/layout/dialog_video_info.xml | 160 ++++++++++ app/src/main/res/layout/item_record.xml | 121 +++++--- app/src/main/res/menu/menu_video_options.xml | 23 ++ 9 files changed, 735 insertions(+), 163 deletions(-) create mode 100644 app/src/main/res/drawable/file_size_background.xml create mode 100644 app/src/main/res/drawable/ic_content_copy.xml create mode 100644 app/src/main/res/drawable/ic_delete_red.xml create mode 100644 app/src/main/res/layout/dialog_video_info.xml create mode 100644 app/src/main/res/menu/menu_video_options.xml diff --git a/app/src/main/java/com/fadcam/ui/RecordsAdapter.java b/app/src/main/java/com/fadcam/ui/RecordsAdapter.java index 0488042d..b973fdae 100644 --- a/app/src/main/java/com/fadcam/ui/RecordsAdapter.java +++ b/app/src/main/java/com/fadcam/ui/RecordsAdapter.java @@ -3,11 +3,15 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.MediaStore; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -17,7 +21,6 @@ import androidx.annotation.NonNull; import androidx.appcompat.widget.PopupMenu; -import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; @@ -34,8 +37,11 @@ import java.io.OutputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Locale; public class RecordsAdapter extends RecyclerView.Adapter { @@ -72,9 +78,24 @@ public RecordViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewTy @Override public void onBindViewHolder(@NonNull RecordViewHolder holder, int position) { File video = records.get(position); - setThumbnail(holder, video); + + // Set serial number based on current position + holder.textViewSerialNumber.setText(String.valueOf(position + 1)); + + // Set video name holder.textViewRecord.setText(video.getName()); - + + // Set file size + long fileSize = video.length(); + holder.textViewFileSize.setText(formatFileSize(fileSize)); + + // Set video duration + long duration = getVideoDuration(video); + holder.textViewFileTime.setText(formatVideoDuration(duration)); + + // Set thumbnail + setThumbnail(holder, video); + holder.itemView.setOnClickListener(v -> clickListener.onVideoClick(video)); holder.itemView.setOnLongClickListener(v -> { boolean isSelected = !selectedVideos.contains(video); @@ -83,7 +104,7 @@ public void onBindViewHolder(@NonNull RecordViewHolder holder, int position) { return true; }); - holder.menuButton.setOnClickListener(v -> showPopupMenu(v, video)); + setupPopupMenu(holder, video); updateSelectionState(holder, selectedVideos.contains(video)); } @@ -114,51 +135,137 @@ private void updateSelectionState(RecordViewHolder holder, boolean isSelected) { holder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE); } - private void showPopupMenu(View v, File video) { - int position = records.indexOf(video); // Find position from video - if (position == -1) { - Toast.makeText(context, R.string.toast_video_not_found, Toast.LENGTH_SHORT).show(); - return; - } - - PopupMenu popup = new PopupMenu(v.getContext(), v); - popup.getMenuInflater().inflate(R.menu.video_item_menu, popup.getMenu()); - - popup.getMenu().findItem(R.id.action_delete).setIcon(R.drawable.ic_delete); - popup.getMenu().findItem(R.id.action_save_to_gallery).setIcon(R.drawable.ic_save); - popup.getMenu().findItem(R.id.action_rename).setIcon(R.drawable.ic_rename); - - popup.setOnMenuItemClickListener(item -> { - if (item.getItemId() == R.id.action_delete) { - confirmDelete(v.getContext(), records.get(position)); - return true; + private void setupPopupMenu(RecordViewHolder holder, File video) { + holder.menuButton.setOnClickListener(v -> { + PopupMenu popupMenu = new PopupMenu(context, holder.menuButton); + popupMenu.getMenuInflater().inflate(R.menu.menu_video_options, popupMenu.getMenu()); + + // Force show icons + try { + Field field = PopupMenu.class.getDeclaredField("mPopup"); + field.setAccessible(true); + Object menuPopupHelper = field.get(popupMenu); + Class classPopupHelper = Class.forName(menuPopupHelper.getClass().getName()); + Method setForceIcons = classPopupHelper.getMethod("setForceShowIcon", boolean.class); + setForceIcons.invoke(menuPopupHelper, true); + } catch (Exception e) { + Log.e("PopupMenu", "Error forcing icon display", e); } - if (item.getItemId() == R.id.action_save_to_gallery) { - saveToGallery(v.getContext(), records.get(position)); - return true; - } - if (item.getItemId() == R.id.action_rename) { - showRenameDialog(position); - return true; - } - return false; + + popupMenu.setOnMenuItemClickListener(item -> { + int itemId = item.getItemId(); + + if (itemId == R.id.action_rename) { + showRenameDialog(records.indexOf(video)); + return true; + } else if (itemId == R.id.action_delete) { + confirmDelete(context, video); + return true; + } else if (itemId == R.id.action_save) { + saveToGallery(context, video); + return true; + } else if (itemId == R.id.action_info) { + showVideoInfoDialog(video); + return true; + } + + return false; + }); + + popupMenu.show(); }); + } + private void showVideoInfoDialog(File video) { + // Create dialog + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); + View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_video_info, null); + + // Find views + TextView tvFileName = dialogView.findViewById(R.id.tv_file_name); + TextView tvFileSize = dialogView.findViewById(R.id.tv_file_size); + TextView tvFilePath = dialogView.findViewById(R.id.tv_file_path); + TextView tvLastModified = dialogView.findViewById(R.id.tv_last_modified); + TextView tvDuration = dialogView.findViewById(R.id.tv_duration); + TextView tvResolution = dialogView.findViewById(R.id.tv_resolution); + + ImageView ivCopyToClipboard = dialogView.findViewById(R.id.iv_copy_to_clipboard); + + // Gather video information + String fileName = video.getName(); + long fileSize = video.length(); + String filePath = video.getAbsolutePath(); + long lastModified = video.lastModified(); + long duration = getVideoDuration(video); + + // Format information + String formattedFileSize = formatFileSize(fileSize); + String formattedLastModified = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + .format(new Date(lastModified)); + String formattedDuration = formatVideoDuration(duration); + + // Get video metadata + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { - Field field = popup.getClass().getDeclaredField("mPopup"); - field.setAccessible(true); - Object menuPopupHelper = field.get(popup); - Class classPopupHelper = Class.forName(menuPopupHelper.getClass().getName()); - Method setForceIcons = classPopupHelper.getMethod("setForceShowIcon", boolean.class); - setForceIcons.invoke(menuPopupHelper, true); + retriever.setDataSource(video.getAbsolutePath()); + + // Resolution + String width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); + String height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); + String resolution = (width != null && height != null) ? width + " x " + height : "N/A"; + + // Prepare full video info text for clipboard + String videoInfo = "File Name: " + fileName + "\n" + + "File Size: " + formattedFileSize + "\n" + + "File Path: " + filePath + "\n" + + "Last Modified: " + formattedLastModified + "\n" + + "Duration: " + formattedDuration; + + // Set views + tvFileName.setText(fileName); + tvFileSize.setText(formattedFileSize); + tvFilePath.setText(filePath); + tvLastModified.setText(formattedLastModified); + tvDuration.setText(formattedDuration); + tvResolution.setText(resolution); + + + // Set up copy to clipboard + ivCopyToClipboard.setOnClickListener(v -> { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("Video Info", videoInfo); + clipboard.setPrimaryClip(clip); + + // Show toast notification + Toast.makeText(context, "Video info copied to clipboard", Toast.LENGTH_SHORT).show(); + }); + } catch (Exception e) { - e.printStackTrace(); + Log.e("VideoInfoDialog", "Error retrieving video metadata", e); + + // Set default/fallback values + tvFileName.setText(fileName); + tvFileSize.setText(formattedFileSize); + tvFilePath.setText(filePath); + tvLastModified.setText(formattedLastModified); + tvDuration.setText(formattedDuration); + tvResolution.setText("N/A"); + + } finally { + try { + retriever.release(); + } catch (IOException e) { + Log.e("VideoInfoDialog", "Error releasing MediaMetadataRetriever", e); + } } - - popup.show(); + + // Build and show dialog + builder.setTitle("Video Information") + .setView(dialogView) + .setPositiveButton("Close", (dialog, which) -> dialog.dismiss()) + .show(); } - private void confirmDelete(Context context, File video) { new MaterialAlertDialogBuilder(context) .setTitle(context.getString(R.string.dialog_del_title)) @@ -235,8 +342,45 @@ private void saveToGalleryLegacy(Context context, File video) { } } + // Helper method to format file size + private String formatFileSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(1024)); + String pre = "KMGTPE".charAt(exp - 1) + "B"; + return String.format("%.1f %s", bytes / Math.pow(1024, exp), pre); + } + // Helper method to format video duration + private String formatVideoDuration(long durationMs) { + long totalSeconds = durationMs / 1000; + long minutes = totalSeconds / 60; + long seconds = totalSeconds % 60; + + if (minutes > 0) { + return String.format(Locale.getDefault(), "%d min", minutes); + } else { + return String.format(Locale.getDefault(), "%d sec", seconds); + } + } + // Helper method to get video duration + private long getVideoDuration(File videoFile) { + MediaMetadataRetriever retriever = new MediaMetadataRetriever(); + try { + retriever.setDataSource(videoFile.getAbsolutePath()); + String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + return duration != null ? Long.parseLong(duration) : 0; + } catch (Exception e) { + Log.e("RecordsAdapter", "Error getting video duration", e); + return 0; + } finally { + try { + retriever.release(); + } catch (IOException e) { + Log.e("RecordsAdapter", "Error releasing MediaMetadataRetriever", e); + } + } + } private void showRenameDialog(final int position) { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context); @@ -295,9 +439,24 @@ private void renameVideo(int position, String newName) { } } + public void updateRecords(List newRecords) { + if (newRecords != null) { + records.clear(); + records.addAll(newRecords); + notifyDataSetChanged(); + } + } + + public void updateThumbnail(String videoFilePath) { + notifyDataSetChanged(); + } + static class RecordViewHolder extends RecyclerView.ViewHolder { ImageView imageViewThumbnail; TextView textViewRecord; + TextView textViewFileSize; + TextView textViewFileTime; + TextView textViewSerialNumber; // New serial number TextView ImageView checkIcon; ImageView menuButton; @@ -305,41 +464,11 @@ static class RecordViewHolder extends RecyclerView.ViewHolder { super(itemView); imageViewThumbnail = itemView.findViewById(R.id.image_view_thumbnail); textViewRecord = itemView.findViewById(R.id.text_view_record); + textViewFileSize = itemView.findViewById(R.id.text_view_file_size); + textViewFileTime = itemView.findViewById(R.id.text_view_file_time); + textViewSerialNumber = itemView.findViewById(R.id.text_view_serial_number); // Initialize serial number TextView checkIcon = itemView.findViewById(R.id.check_icon); menuButton = itemView.findViewById(R.id.menu_button); } } - - public void updateRecords(List newRecords) { - DiffUtil.Callback diffCallback = new DiffUtil.Callback() { - @Override - public int getOldListSize() { - return records.size(); - } - - @Override - public int getNewListSize() { - return newRecords.size(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - return records.get(oldItemPosition).equals(newRecords.get(newItemPosition)); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - return records.get(oldItemPosition).equals(newRecords.get(newItemPosition)); - } - }; - - DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); - this.records = newRecords; - this.videoFiles = new ArrayList<>(newRecords); // Update videoFiles to match new records - diffResult.dispatchUpdatesTo(this); - } - - public void updateThumbnail(String videoFilePath) { - notifyDataSetChanged(); - } } diff --git a/app/src/main/java/com/fadcam/ui/RecordsFragment.java b/app/src/main/java/com/fadcam/ui/RecordsFragment.java index 9cb4caa2..8c9d83a6 100644 --- a/app/src/main/java/com/fadcam/ui/RecordsFragment.java +++ b/app/src/main/java/com/fadcam/ui/RecordsFragment.java @@ -9,12 +9,16 @@ import android.os.Looper; import android.os.VibrationEffect; import android.os.Vibrator; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.RadioGroup; +import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; @@ -31,6 +35,8 @@ import java.io.File; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -38,12 +44,14 @@ public class RecordsFragment extends Fragment implements RecordsAdapter.OnVideoClickListener, RecordsAdapter.OnVideoLongClickListener { private RecyclerView recyclerView; - private RecordsAdapter adapter; + private RecordsAdapter recordsAdapter; private boolean isGridView = true; private FloatingActionButton fabToggleView; private FloatingActionButton fabDeleteSelected; private List selectedVideos = new ArrayList<>(); private ExecutorService executorService = Executors.newSingleThreadExecutor(); // Executor for background tasks + private SortOption currentSortOption = SortOption.LATEST_FIRST; + private List videoFiles = new ArrayList<>(); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -71,8 +79,8 @@ public void onResume() { private void setupRecyclerView() { setLayoutManager(); - adapter = new RecordsAdapter(getContext(), new ArrayList<>(), this, this); - recyclerView.setAdapter(adapter); + recordsAdapter = new RecordsAdapter(getContext(), new ArrayList<>(), this, this); + recyclerView.setAdapter(recordsAdapter); } private void setLayoutManager() { @@ -112,7 +120,20 @@ private void updateFabIcons() { private void loadRecordsList() { executorService.submit(() -> { List recordsList = getRecordsList(); - requireActivity().runOnUiThread(() -> adapter.updateRecords(recordsList)); + + // Sort initially by latest first + recordsList.sort((a, b) -> { + long timeA = extractVideoTime(a); + long timeB = extractVideoTime(b); + return Long.compare(timeB, timeA); + }); + + videoFiles = new ArrayList<>(recordsList); + + requireActivity().runOnUiThread(() -> { + recordsAdapter.updateRecords(recordsList); + Log.d("RecordsFragment", "Initial records loaded and sorted. Count: " + recordsList.size()); + }); }); } @@ -120,8 +141,6 @@ private List getRecordsList() { List recordsList = new ArrayList<>(); File recordsDir = new File(requireContext().getExternalFilesDir(null), Constants.RECORDING_DIRECTORY); if (recordsDir.exists()) { - // Introduce a delay before refreshing the list - new Handler(Looper.getMainLooper()).postDelayed(this::loadRecordsList, 500); File[] files = recordsDir.listFiles(); if (files != null) { for (File file : files) { @@ -131,7 +150,6 @@ private List getRecordsList() { } } } - new Handler(Looper.getMainLooper()).postDelayed(this::loadRecordsList, 500); return recordsList; } @@ -202,42 +220,130 @@ private void deleteSelectedVideos() { } private void showRecordsSidebar() { - // Create a bottom sheet dialog - BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(requireContext()); View bottomSheetView = getLayoutInflater().inflate(R.layout.bottom_sheet_records_options, null); + BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(requireContext()); + bottomSheetDialog.setContentView(bottomSheetView); - // Setup delete all option - View deleteAllOption = bottomSheetView.findViewById(R.id.option_delete_all); - deleteAllOption.setOnClickListener(v -> { - bottomSheetDialog.dismiss(); - confirmDeleteAll(); - }); + RadioGroup sortOptionsGroup = bottomSheetView.findViewById(R.id.sort_options_group); + LinearLayout deleteAllOption = bottomSheetView.findViewById(R.id.option_delete_all); + + if (sortOptionsGroup != null) { + // Preselect current sort option + switch (currentSortOption) { + case LATEST_FIRST: + sortOptionsGroup.check(R.id.sort_latest); + break; + case OLDEST_FIRST: + sortOptionsGroup.check(R.id.sort_oldest); + break; + case SMALLEST_FILES: + sortOptionsGroup.check(R.id.sort_smallest); + break; + case LARGEST_FILES: + sortOptionsGroup.check(R.id.sort_largest); + break; + } - // Add more options here in future + sortOptionsGroup.setOnCheckedChangeListener((group, checkedId) -> { + SortOption newSortOption; + if (checkedId == R.id.sort_latest) { + newSortOption = SortOption.LATEST_FIRST; + } else if (checkedId == R.id.sort_oldest) { + newSortOption = SortOption.OLDEST_FIRST; + } else if (checkedId == R.id.sort_smallest) { + newSortOption = SortOption.SMALLEST_FILES; + } else if (checkedId == R.id.sort_largest) { + newSortOption = SortOption.LARGEST_FILES; + } else { + return; // No valid option selected + } + + // Only sort if the option has changed + if (newSortOption != currentSortOption) { + currentSortOption = newSortOption; + performVideoSort(); + } + + bottomSheetDialog.dismiss(); + }); + } + + if (deleteAllOption != null) { + deleteAllOption.setOnClickListener(v -> { + bottomSheetDialog.dismiss(); + confirmDeleteAll(); + }); + } - bottomSheetDialog.setContentView(bottomSheetView); bottomSheetDialog.show(); } - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.records_menu, menu); - super.onCreateOptionsMenu(menu, inflater); + private void performVideoSort() { + if (videoFiles == null || videoFiles.isEmpty()) { + Log.e("RecordsFragment", "Cannot sort: videoFiles is null or empty"); + return; + } + + // Create a copy of the original list to preserve original order + List sortedVideos = new ArrayList<>(videoFiles); + + try { + switch (currentSortOption) { + case LATEST_FIRST: + Collections.sort(sortedVideos, (a, b) -> { + long timeA = extractVideoTime(a); + long timeB = extractVideoTime(b); + return Long.compare(timeB, timeA); + }); + break; + case OLDEST_FIRST: + Collections.sort(sortedVideos, (a, b) -> { + long timeA = extractVideoTime(a); + long timeB = extractVideoTime(b); + return Long.compare(timeA, timeB); + }); + break; + case SMALLEST_FILES: + Collections.sort(sortedVideos, Comparator.comparingLong(File::length)); + break; + case LARGEST_FILES: + Collections.sort(sortedVideos, (a, b) -> Long.compare(b.length(), a.length())); + break; + } + + // Update the list and adapter on the main thread + requireActivity().runOnUiThread(() -> { + videoFiles = sortedVideos; + recordsAdapter.updateRecords(videoFiles); + recyclerView.scrollToPosition(0); + + Log.d("RecordsFragment", "Sorted videos. Option: " + currentSortOption + + ", Total videos: " + videoFiles.size()); + }); + + } catch (Exception e) { + Log.e("RecordsFragment", "Error sorting videos", e); + } } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int itemId = item.getItemId(); - - if (itemId == R.id.action_delete_all) { - confirmDeleteAll(); - return true; - } else if (itemId == R.id.action_more_options) { - showRecordsSidebar(); - return true; + private long extractVideoTime(File videoFile) { + try { + String fileName = videoFile.getName(); + // Assuming filename format: video_2024-01-11_12-30-45.mp4 + int dateStartIndex = fileName.indexOf('_') + 1; + int dateEndIndex = fileName.indexOf('_', dateStartIndex); + + if (dateStartIndex > 0 && dateEndIndex > dateStartIndex) { + String dateTimeStr = fileName.substring(dateStartIndex, dateEndIndex); + Log.d("RecordsFragment", "Extracted datetime: " + dateTimeStr + " from " + fileName); + return videoFile.lastModified(); // Fallback to last modified time + } + + return videoFile.lastModified(); + } catch (Exception e) { + Log.e("RecordsFragment", "Error extracting time from " + videoFile.getName(), e); + return 0; } - - return super.onOptionsItemSelected(item); } private void confirmDeleteAll() { @@ -274,4 +380,33 @@ private void deleteAllVideos() { } loadRecordsList(); } + + // Enum for sort options + private enum SortOption { + LATEST_FIRST, + OLDEST_FIRST, + SMALLEST_FILES, + LARGEST_FILES + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.records_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + + if (itemId == R.id.action_delete_all) { + confirmDeleteAll(); + return true; + } else if (itemId == R.id.action_more_options) { + showRecordsSidebar(); + return true; + } + + return super.onOptionsItemSelected(item); + } } diff --git a/app/src/main/res/drawable/file_size_background.xml b/app/src/main/res/drawable/file_size_background.xml new file mode 100644 index 00000000..3d2b5546 --- /dev/null +++ b/app/src/main/res/drawable/file_size_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_content_copy.xml b/app/src/main/res/drawable/ic_content_copy.xml new file mode 100644 index 00000000..54c65c92 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_delete_red.xml b/app/src/main/res/drawable/ic_delete_red.xml new file mode 100644 index 00000000..2b8bd491 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_red.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/bottom_sheet_records_options.xml b/app/src/main/res/layout/bottom_sheet_records_options.xml index dc187ad0..a470fe41 100644 --- a/app/src/main/res/layout/bottom_sheet_records_options.xml +++ b/app/src/main/res/layout/bottom_sheet_records_options.xml @@ -14,39 +14,92 @@ android:layout_marginBottom="16dp"/> + android:orientation="vertical"> - + - - + + + + + + + android:text="Largest Files First"/> + + + - + + + + + android:orientation="vertical"> + + + + + + diff --git a/app/src/main/res/layout/dialog_video_info.xml b/app/src/main/res/layout/dialog_video_info.xml new file mode 100644 index 00000000..33bd58da --- /dev/null +++ b/app/src/main/res/layout/dialog_video_info.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_record.xml b/app/src/main/res/layout/item_record.xml index bebdff77..4695e1a2 100644 --- a/app/src/main/res/layout/item_record.xml +++ b/app/src/main/res/layout/item_record.xml @@ -3,11 +3,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="8dp" - android:paddingHorizontal="-14dp" - android:paddingVertical="10dp" + android:layout_margin="4dp" app:cardCornerRadius="8dp" - app:cardElevation="4dp" + app:cardElevation="2dp" app:cardBackgroundColor="@color/gray" android:background="?android:attr/selectableItemBackground"> @@ -15,54 +13,103 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/thumbnail_container" + app:layout_constraintVertical_chainStyle="packed" + app:layout_constraintBottom_toBottomOf="parent"/> + app:layout_constraintTop_toTopOf="@id/text_view_record" + app:layout_constraintBottom_toBottomOf="@id/text_view_record"/> - + app:layout_constraintTop_toTopOf="parent"/> - diff --git a/app/src/main/res/menu/menu_video_options.xml b/app/src/main/res/menu/menu_video_options.xml new file mode 100644 index 00000000..84f2b77d --- /dev/null +++ b/app/src/main/res/menu/menu_video_options.xml @@ -0,0 +1,23 @@ + + + + + + + + + From ac937972e5110e2d6974195f9ee7175c8952c517 Mon Sep 17 00:00:00 2001 From: Faded Date: Sat, 11 Jan 2025 01:15:26 +0500 Subject: [PATCH 08/10] added star emoji in review button --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a7a1f617..14901a4f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -112,7 +112,7 @@ Location Data Include location data in watermark. \nRequires GPS enabled \n(Default: Disabled) Enable phone\'s GPS for adding latitude and longitude data to the watermark. - Write a Review + Write a Review 🌟 Choose Clock Display Time Only English Date with time From a558b75c6296375f26ce841fce263fa38b40a096 Mon Sep 17 00:00:00 2001 From: Faded Date: Sat, 11 Jan 2025 03:39:07 +0500 Subject: [PATCH 09/10] Feature: added amoled black theme --- app/src/main/AndroidManifest.xml | 2 +- app/src/main/java/com/fadcam/Constants.java | 1 + .../main/java/com/fadcam/MainActivity.java | 19 +- .../java/com/fadcam/ui/SettingsFragment.java | 74 +++++++ app/src/main/res/drawable/ic_theme.xml | 10 + app/src/main/res/layout/fragment_home.xml | 6 +- app/src/main/res/layout/fragment_settings.xml | 185 ++++++------------ app/src/main/res/values/colors.xml | 8 + app/src/main/res/values/strings.xml | 6 + app/src/main/res/values/themes.xml | 36 +++- 10 files changed, 209 insertions(+), 138 deletions(-) create mode 100644 app/src/main/res/drawable/ic_theme.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3fc1e3ba..123291bc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -58,7 +58,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.FadCam" + android:theme="@style/Base.Theme.FadCam" android:requestLegacyExternalStorage="true" tools:targetApi="q"> diff --git a/app/src/main/java/com/fadcam/Constants.java b/app/src/main/java/com/fadcam/Constants.java index afd268cd..54121258 100644 --- a/app/src/main/java/com/fadcam/Constants.java +++ b/app/src/main/java/com/fadcam/Constants.java @@ -19,6 +19,7 @@ public abstract class Constants { public static final String PREF_DEBUG_DATA = "debug_data"; public static final String PREF_WATERMARK_OPTION = "watermark_option"; public static final String PREF_VIDEO_CODEC = "video_codec"; + public static final String PREF_APP_THEME = "app_theme"; public static final String BROADCAST_ON_RECORDING_STARTED = "ON_RECORDING_STARTED"; public static final String BROADCAST_ON_RECORDING_RESUMED = "ON_RECORDING_RESUMED"; diff --git a/app/src/main/java/com/fadcam/MainActivity.java b/app/src/main/java/com/fadcam/MainActivity.java index 9f96e418..67def6fa 100644 --- a/app/src/main/java/com/fadcam/MainActivity.java +++ b/app/src/main/java/com/fadcam/MainActivity.java @@ -32,14 +32,29 @@ public class MainActivity extends AppCompatActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Apply saved theme on startup + SharedPreferencesManager sharedPreferencesManager = SharedPreferencesManager.getInstance(this); + String savedTheme = sharedPreferencesManager.sharedPreferences.getString(Constants.PREF_APP_THEME, "Dark Mode"); + + Log.d("MainActivity", "Saved theme: " + savedTheme); + + // Always force dark mode + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + + // Only set AMOLED theme if explicitly selected + if ("AMOLED Black".equals(savedTheme)) { + Log.d("MainActivity", "Setting AMOLED theme"); + getTheme().applyStyle(R.style.Theme_FadCam_Amoled, true); + } else { + Log.d("MainActivity", "Using default dark theme"); + } + // Load and apply the saved language preference before anything else SharedPreferences prefs = getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE); String savedLanguageCode = prefs.getString(Constants.LANGUAGE_KEY, Locale.getDefault().getLanguage()); applyLanguage(savedLanguageCode); // Apply the language preference - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); //force dark theme even on light themed devices - // Check if current locale is Pashto if (getResources().getConfiguration().locale.getLanguage().equals("ps")) { getWindow().getDecorView().setLayoutDirection(View.LAYOUT_DIRECTION_LTR); diff --git a/app/src/main/java/com/fadcam/ui/SettingsFragment.java b/app/src/main/java/com/fadcam/ui/SettingsFragment.java index 768cb73b..e083ee5d 100644 --- a/app/src/main/java/com/fadcam/ui/SettingsFragment.java +++ b/app/src/main/java/com/fadcam/ui/SettingsFragment.java @@ -25,6 +25,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatDelegate; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; @@ -62,10 +63,13 @@ public class SettingsFragment extends Fragment { private static final int REQUEST_PERMISSIONS = 1; private static final String PREF_FIRST_LAUNCH = "first_launch"; + private static final String PREF_APP_THEME = "app_theme"; + private Spinner resolutionSpinner; private Spinner frameRateSpinner; private Spinner codecSpinner; private Spinner watermarkSpinner; + private Spinner themeSpinner; MaterialButtonToggleGroup cameraSelectionToggle; @@ -252,6 +256,9 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa // Set up codec note text setupCodecNoteText(); + // Setup theme spinner + setupThemeSpinner(view); + return view; } @@ -928,4 +935,71 @@ private int getCamcorderProfileIndexPreferences(CameraType cameraType) } return 0; } + + private void setupThemeSpinner(View view) { + themeSpinner = view.findViewById(R.id.theme_spinner); + String[] themeOptions = {"Dark Mode", "AMOLED Black"}; + ArrayAdapter themeAdapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, themeOptions); + themeAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + themeSpinner.setAdapter(themeAdapter); + + // Set the current theme in the spinner + String currentTheme = sharedPreferencesManager.sharedPreferences.getString(Constants.PREF_APP_THEME, "Dark Mode"); + Log.d("SettingsFragment", "Current theme: " + currentTheme); + + int currentThemeIndex = Arrays.asList(themeOptions).indexOf(currentTheme); + Log.d("SettingsFragment", "Current theme index: " + currentThemeIndex); + + // Ensure valid index, default to 0 if not found + themeSpinner.setSelection(currentThemeIndex != -1 ? currentThemeIndex : 0); + + themeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + String selectedTheme = themeOptions[position]; + Log.d("SettingsFragment", "Selected theme: " + selectedTheme); + + // Check if theme is actually changing + String currentSavedTheme = sharedPreferencesManager.sharedPreferences.getString(Constants.PREF_APP_THEME, "Dark Mode"); + if (selectedTheme.equals(currentSavedTheme)) { + Log.d("SettingsFragment", "Theme not changed, skipping recreation"); + return; + } + + // Save the selected theme + SharedPreferences.Editor editor = sharedPreferencesManager.sharedPreferences.edit(); + editor.putString(Constants.PREF_APP_THEME, selectedTheme); + boolean saveSuccess = editor.commit(); // Use commit instead of apply for synchronous save + Log.d("SettingsFragment", "Theme save success: " + saveSuccess); + + // Always force dark mode + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + + // Optional: Add haptic feedback + vibrateTouch(); + + // Prompt user to restart for theme change + new MaterialAlertDialogBuilder(requireContext()) + .setTitle("Theme Change") + .setMessage("Restart the app to apply the new theme?") + .setPositiveButton("Restart", (dialog, which) -> { + // Restart the entire application + Intent intent = requireActivity().getPackageManager() + .getLaunchIntentForPackage(requireActivity().getPackageName()); + if (intent != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + requireActivity().finish(); + } + }) + .setNegativeButton("Later", null) + .show(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + Log.d("SettingsFragment", "No theme selected"); + } + }); + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_theme.xml b/app/src/main/res/drawable/ic_theme.xml new file mode 100644 index 00000000..3034027a --- /dev/null +++ b/app/src/main/res/drawable/ic_theme.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index b4fe5400..38cc3188 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -184,11 +184,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" - app:cardBackgroundColor="#121116" + app:cardBackgroundColor="#00000000" app:cardCornerRadius="8dp" - app:cardElevation="4dp" - app:strokeColor="#4CAF50" - app:strokeWidth="2dp"> + app:cardElevation="0dp"> - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - + android:layout_marginEnd="8dp" + app:tint="@color/white" /> - - - - - - - - - - - - - - - #F44336 #D32F2F #F44336 + + + #000000 + #121212 + #FFFFFF + #B3FFFFFF + #1AFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14901a4f..ee502d48 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -246,4 +246,10 @@ More Options Records Options + App Theme + Choose your preferred app theme + + Dark Mode + AMOLED Black + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index a968655a..b2302318 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -8,6 +8,40 @@ @color/gray + + + - \ No newline at end of file From de5e3b1f585bce2180314f018593f5d08f007b3f Mon Sep 17 00:00:00 2001 From: Faded Date: Sat, 11 Jan 2025 03:42:58 +0500 Subject: [PATCH 10/10] update: eighteen replace with 31 featured number --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee502d48..926203f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,7 +9,7 @@
- FadCam has been featured in popular YouTube channels like HowToMen and Xtream Droid, as well as discussed in GrapheneOS and eighteen other notable spots. + FadCam has been featured in popular YouTube channels like HowToMen and Xtream Droid, as well as discussed in GrapheneOS and thirty one other notable spots. ]]>