diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml
new file mode 100644
index 0000000..b611b36
--- /dev/null
+++ b/.github/workflows/android-build.yml
@@ -0,0 +1,265 @@
+name: Android Benchmark Build & Release
+
+on:
+ push:
+ branches:
+ - test
+ tags:
+ - 'v*'
+
+env:
+ JAVA_VERSION: '21'
+ JAVA_DISTRIBUTION: 'temurin'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ name: Build APK
+ outputs:
+ version: ${{ steps.get-version.outputs.version }}
+ is-release: ${{ steps.check-release.outputs.is-release }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Check if this is a release
+ id: check-release
+ run: |
+ if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
+ echo "is-release=true" >> $GITHUB_OUTPUT
+ else
+ echo "is-release=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Get version
+ id: get-version
+ run: |
+ if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
+ version="${{ github.ref_name }}"
+ echo "version=$version" >> $GITHUB_OUTPUT
+ else
+ echo "version=test-${{ github.sha }}" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Set up JDK ${{ env.JAVA_VERSION }}
+ uses: actions/setup-java@v4
+ with:
+ distribution: ${{ env.JAVA_DISTRIBUTION }}
+ java-version: ${{ env.JAVA_VERSION }}
+
+ - name: Cache Gradle packages
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: |
+ gradle-${{ runner.os }}-
+
+ - name: Grant execute permission for gradlew
+ run: chmod +x ./gradlew
+
+ - name: Validate Gradle wrapper
+ uses: gradle/wrapper-validation-action@v2
+
+ # For releases, create signed APK; for test builds, create unsigned APK
+ - name: Build signed release APK
+ if: steps.check-release.outputs.is-release == 'true'
+ run: |
+ echo "Building signed release APK..."
+ ./gradlew :app:assembleBenchmarkRelease
+ env:
+ KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
+ KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
+ KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
+ KEYSTORE_FILE: ${{ secrets.KEYSTORE_FILE }}
+
+ - name: Build unsigned test APK
+ if: steps.check-release.outputs.is-release == 'false'
+ run: |
+ echo "Building unsigned test APK..."
+ ./gradlew :app:assembleBenchmarkRelease
+
+ - name: Setup keystore for signing (Release only)
+ if: steps.check-release.outputs.is-release == 'true'
+ run: |
+ echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > keystore.jks
+ echo "Keystore file created"
+
+ - name: Sign APK (Release only)
+ if: steps.check-release.outputs.is-release == 'true'
+ run: |
+ # Find the APK file
+ APK_FILE=$(find app/build/outputs/apk/benchmarkRelease -name "*.apk" | head -1)
+ if [ -z "$APK_FILE" ]; then
+ echo "Error: No APK file found"
+ exit 1
+ fi
+
+ echo "Found APK: $APK_FILE"
+
+ # Sign the APK
+ $ANDROID_HOME/build-tools/*/apksigner sign \
+ --ks keystore.jks \
+ --ks-key-alias "${{ secrets.KEY_ALIAS }}" \
+ --ks-pass pass:"${{ secrets.KEYSTORE_PASSWORD }}" \
+ --key-pass pass:"${{ secrets.KEY_PASSWORD }}" \
+ --out "${APK_FILE%.apk}-signed.apk" \
+ "$APK_FILE"
+
+ # Verify the signature
+ $ANDROID_HOME/build-tools/*/apksigner verify "${APK_FILE%.apk}-signed.apk"
+
+ echo "APK signed and verified successfully"
+
+ # Replace unsigned APK with signed one
+ mv "${APK_FILE%.apk}-signed.apk" "$APK_FILE"
+
+ - name: Rename APK with version
+ run: |
+ APK_FILE=$(find app/build/outputs/apk/benchmarkRelease -name "*.apk" | head -1)
+ if [ -z "$APK_FILE" ]; then
+ echo "Error: No APK file found"
+ exit 1
+ fi
+
+ APK_DIR=$(dirname "$APK_FILE")
+ APK_NAME=$(basename "$APK_FILE" .apk)
+ VERSION="${{ steps.get-version.outputs.version }}"
+
+ if [[ "${{ steps.check-release.outputs.is-release }}" == "true" ]]; then
+ NEW_NAME="ClipSync-Android-${VERSION}-signed.apk"
+ else
+ NEW_NAME="ClipSync-Android-${VERSION}-unsigned.apk"
+ fi
+
+ mv "$APK_FILE" "$APK_DIR/$NEW_NAME"
+ echo "APK renamed to: $NEW_NAME"
+ echo "apk-path=$APK_DIR/$NEW_NAME" >> $GITHUB_OUTPUT
+ id: rename-apk
+
+ - name: Get APK info
+ run: |
+ APK_FILE=$(find app/build/outputs/apk/benchmarkRelease -name "*.apk" | head -1)
+ if [ -n "$APK_FILE" ]; then
+ echo "APK Size: $(du -h "$APK_FILE" | cut -f1)"
+ echo "APK Path: $APK_FILE"
+
+ # Get APK details using aapt if available
+ if command -v aapt >/dev/null 2>&1; then
+ echo "APK Package Info:"
+ aapt dump badging "$APK_FILE" | grep -E "package:|application-label:|platformBuildVersionName"
+ fi
+ fi
+
+ - name: Cleanup keystore
+ if: always() && steps.check-release.outputs.is-release == 'true'
+ run: |
+ if [ -f keystore.jks ]; then
+ rm -f keystore.jks
+ echo "Keystore file cleaned up"
+ fi
+
+ - name: Upload APK artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: clipsync-android-${{ steps.get-version.outputs.version }}
+ path: app/build/outputs/apk/benchmarkRelease/*.apk
+ retention-days: ${{ steps.check-release.outputs.is-release == 'true' && 90 || 30 }}
+
+ test-release:
+ needs: build
+ if: github.ref == 'refs/heads/test'
+ runs-on: ubuntu-latest
+ name: Test Release Upload
+
+ steps:
+ - name: Download APK artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: clipsync-android-${{ needs.build.outputs.version }}
+ path: ./apk/
+
+ - name: List test artifacts
+ run: |
+ echo "Test build artifacts:"
+ find ./apk -name "*.apk" -exec ls -lh {} \;
+
+ release:
+ needs: build
+ if: startsWith(github.ref, 'refs/tags/v')
+ runs-on: ubuntu-latest
+ name: Create GitHub Release
+
+ steps:
+ - name: Download APK artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: clipsync-android-${{ needs.build.outputs.version }}
+ path: ./apk/
+
+ - name: Verify release artifacts
+ run: |
+ echo "Release artifacts:"
+ find ./apk -name "*.apk" -exec ls -lh {} \;
+
+ # Verify we have signed APK for release
+ if ! find ./apk -name "*-signed.apk" | grep -q .; then
+ echo "Warning: No signed APK found for release"
+ else
+ echo "Signed APK found for release"
+ fi
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ name: ClipSync Android ${{ needs.build.outputs.version }}
+ body: |
+ # π Introducing ClipSync Android ${{ needs.build.outputs.version }} π
+
+ Welcome to the very first official release of **ClipSync for Android**! π
+
+ ClipSync bridges your clipboard between Android and Windows devicesβinstantly and wirelessly. Copy text on your phone, paste it on your PC, and vice versa. No cloud, no ads, just seamless Bluetooth-powered productivity!
+
+ ## What is ClipSync?
+ ClipSync is a privacy-first clipboard sharing app that connects your Android and Windows devices over Bluetooth. Effortlessly share text between your phone and computer with a single tapβperfect for students, professionals, and anyone tired of emailing themselves snippets!
+
+ - π **Instant clipboard sharing** between Android and Windows
+ - π **Private & local**: No data ever leaves your devices
+ - π **Dark & Light themes**
+ - β‘ **Optimized for speed** with our special benchmarkRelease build
+
+ ## Installation
+
+ 1. Download the APK file below
+ 2. Enable "Install from unknown sources" in your Android settings
+ 3. Install the APK file
+ 4. Pair with your Windows device (see README for details)
+
+ ## System Requirements
+ - Android 7.0 (API level 24) or higher
+ - Bluetooth capability (for device synchronization)
+
+ ## Security
+ This APK is signed with our release key for security and authenticity. Your clipboard data stays on your devicesβno servers, no snooping.
+
+ ## What's New
+ This is our **first release**! π
+ - Android β Windows clipboard sync
+ - Bluetooth device discovery & pairing
+ - Service-based background listening
+ - Theme switching & intuitive UI
+
+ For more details, check the [CHANGELOG](CHANGELOG.md).
+
+ ---
+ Thank you for trying ClipSync! If you love it, star the repo and share feedback. Happy syncing!
+
+ files: ./apk/*.apk
+ draft: false
+ prerelease: false
+ generate_release_notes: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index aa724b7..a72f377 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@
.gradle
/local.properties
/.idea/caches
+/.idea
+/.kotlin
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
@@ -13,3 +15,5 @@
.externalNativeBuild
.cxx
local.properties
+# gradlew
+# gradle/wrapper/gradle-wrapper.jar
diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md
new file mode 100644
index 0000000..965d8ec
--- /dev/null
+++ b/DEVELOPER_GUIDE.md
@@ -0,0 +1,224 @@
+# ClipSync Developer Guide
+
+## Architecture Overview
+
+ClipSync follows a service-oriented architecture designed for background clipboard synchronization:
+
+### Core Components
+
+1. **MainActivity**: Entry point for user interaction and service management
+2. **BluetoothService**: Foreground service handling all Bluetooth operations
+3. **Essentials**: Global state manager for service communication
+4. **NotificationReceiver**: Broadcast receiver for notification actions
+
+### Design Patterns
+
+#### Service-Oriented Architecture
+
+- **BluetoothService** runs independently as a foreground service
+- Survives Activity lifecycle for continuous background operation
+- Communicates via the Essentials singleton for real-time updates
+
+#### Global State Management (Essentials Pattern)
+
+While typically considered an anti-pattern, the Essentials singleton is justified here because:
+
+- Service needs to persist beyond Activity lifecycle
+- Multiple components require real-time service state access
+- Avoids service restart overhead for configuration updates
+- Properly cleaned up when service stops
+
+### Key Features
+
+- **Background Operation**: Service continues running when app is closed
+- **Notification Control**: Start/stop service via persistent notification
+- **Real-time Updates**: Update service configuration without restart
+- **Thread Safety**: Mutex-protected state management
+
+## Development Setup
+
+### Prerequisites
+
+- Android Studio Arctic Fox or newer
+- Android SDK 24+ (Android 7.0)
+- Kotlin 1.8+
+- Gradle 8.0+
+
+### Build Commands
+
+```bash
+# Debug build
+./gradlew assembleDebug
+
+# Release build
+./gradlew assembleRelease
+
+# Run tests
+./gradlew test
+
+# Run instrumented tests
+./gradlew connectedAndroidTest
+```
+
+## Code Style Guidelines
+
+### Kotlin Conventions
+
+- Follow official Kotlin coding conventions
+- Use meaningful variable names
+- Document public APIs with KDoc
+- Prefer immutable data structures
+
+### Architecture Guidelines
+
+- Keep Activities lightweight - delegate to services
+- Use coroutines for async operations
+- Implement proper error handling
+- Follow single responsibility principle
+
+## Security Considerations
+
+### Data Validation
+
+- Validate clipboard content size (max 1MB)
+- Check for potentially sensitive data patterns
+- Sanitize device names for display
+
+### Bluetooth Security
+
+- Use secure RFCOMM connections
+- Validate device addresses
+- Implement connection timeouts
+
+### Privacy
+
+- No cloud storage - all data local
+- Temporary clipboard storage only
+- Clear sensitive data on service stop
+
+## Testing Strategy
+
+### Unit Tests
+
+- Core business logic
+- Data validation
+- State management
+
+### Integration Tests
+
+- Bluetooth service operations
+- Notification handling
+- Permission management
+
+### Manual Testing Checklist
+
+- [ ] Service starts/stops correctly
+- [ ] Clipboard sync between devices
+- [ ] Notification actions work
+- [ ] Permission handling
+- [ ] Battery optimization compatibility
+
+## Performance Optimizations
+
+### Memory Management
+
+- Use weak references where appropriate
+- Clean up resources in onDestroy()
+- Avoid memory leaks in long-running service
+
+### Battery Optimization
+
+- Efficient Bluetooth operations
+- Minimize wake locks
+- Use appropriate service types
+
+### Network Efficiency
+
+- Connection pooling for multiple devices
+- Retry logic with exponential backoff
+- Timeout handling
+
+## Debugging Tips
+
+### Logging
+
+- Use structured logging with tags
+- Different log levels for debug/release
+- Include relevant context in logs
+
+### Common Issues
+
+1. **Service not starting**: Check permissions and Bluetooth state
+2. **Devices not connecting**: Verify pairing and RFCOMM setup
+3. **Memory leaks**: Use LeakCanary for detection
+4. **Battery drain**: Profile with Android Studio
+
+## Release Process
+
+### Version Management
+
+- Update versionCode and versionName in build.gradle.kts
+- Follow semantic versioning (MAJOR.MINOR.PATCH)
+- Update CHANGELOG.md
+
+### Build Configuration
+
+- Use release build type for production
+- Enable ProGuard/R8 optimization
+- Test thoroughly before release
+
+### GitHub Release Steps
+
+1. Create release branch
+2. Update version numbers
+3. Run full test suite
+4. Build signed APK
+5. Create GitHub release with changelog
+6. Upload APK to release
+
+## Contributing
+
+### Code Review Checklist
+
+- [ ] Code follows style guidelines
+- [ ] Tests added for new features
+- [ ] Documentation updated
+- [ ] No breaking changes without migration
+- [ ] Performance impact considered
+
+### Pull Request Template
+
+- Clear description of changes
+- Link to related issues
+- Screenshots for UI changes
+- Test results included
+
+## Troubleshooting
+
+### Build Issues
+
+- Clean and rebuild project
+- Invalidate caches and restart
+- Check Gradle and SDK versions
+
+### Runtime Issues
+
+- Check device logs with `adb logcat`
+- Verify permissions in device settings
+- Test on different Android versions
+
+## Future Enhancements
+
+### Planned Features
+
+- File sharing support
+- Encryption for sensitive data
+- Multi-device group management
+- Cross-platform compatibility
+
+### Technical Debt
+
+- Migrate to Bluetooth LE for better efficiency
+- Implement proper dependency injection
+- Add comprehensive error recovery
+- Improve test coverage
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3ab84c7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,216 @@
+
+
+
+# ClipSync
+
+

+
+**Share your clipboard instantly across Android and Windows devices via Bluetooth**
+
+[](https://android.com)
+[](LICENSE)
+
+
+---
+
+## π What is ClipSync?
+
+ClipSync is an Android application that enables seamless clipboard sharing between your
+Android and Windows devices using Bluetooth technology. Copy text on one device and instantly access it on
+another - no internet connection required!
+
+### β¨ Key Features
+
+- **π Instant Clipboard Sharing**: Copy text on one device, access it immediately on paired devices
+- **π± Background Operation**: Works silently in the background - no need to keep the app open
+- **π΅ Bluetooth-Based**: Uses secure Bluetooth RFCOMM protocol - no internet required
+- **β‘ Auto-Copy**: Automatically copies received text to your clipboard - can be turned off
+- **π Smart Notifications**: Get notified when new clipboard content arrives - when auto-copy is off
+- **π Dark Mode Support**: Beautiful interface that adapts to your system theme
+- **π Privacy-First**: All data stays between your devices - no cloud storage
+
+---
+
+## π± How It Works
+
+1. **Pair Your Devices**: Connect your Android devices via Bluetooth
+2. **Select Devices**: Choose which paired devices to share clipboard with
+3. **Start Sharing**: Enable the ClipSync service with one tap
+4. **Copy & Share**: Copy text on any device and press share from the notification action button
+5. **Background**: ClipSync works in the background, even when the app is closed
+
+---
+
+## π οΈ Installation
+
+### Download Options
+
+#### Option 1: GitHub Releases (Recommended)
+
+1. Go to [Releases](../../releases)
+2. Download the latest `ClipSync-v1.0.0.apk` file
+3. Enable "Install from Unknown Sources" in your Android settings
+4. Install the APK file
+
+#### Option 2: Build from Source
+
+```bash
+git clone https://github.com/aubynsamuel/clipSync-android.git
+cd clipSync-android
+./gradlew assembleRelease
+```
+
+---
+
+## π₯οΈ Windows Companion App
+
+To sync your clipboard between Android and Windows, you need the ClipSync Windows Companion App:
+
+β‘οΈ [**Download or build it from here**](https://github.com/aubynsamuel/clipsync-windows.git)
+
+Follow the instructions in the Windows app README to set up and pair with your Android device.
+
+---
+
+## π― Quick Start Guide
+
+### First Time Setup
+
+1. **Install ClipSync** on all devices you want to share text with
+2. **Enable Bluetooth** on all devices
+3. **Pair your devices** through Android Bluetooth settings
+4. **Open ClipSync** and grant necessary permissions
+
+### Using ClipSync
+
+#### Starting Clipboard Sharing
+
+1. Open ClipSync
+2. Select the devices you want to share with
+3. Tap "Start Sharing"
+4. The service will run in the background
+
+#### Sharing Clipboard Content
+
+1. Copy any text on your device (long press β Copy)
+2. The text automatically appears on your paired devices
+3. If Auto-Copy is enabled, it's instantly available in the clipboard
+4. If Auto-Copy is disabled, you'll get a notification to manually copy
+
+#### Managing the Service
+
+- **View Status**: Check the persistent notification
+- **Stop Service**: Tap "Dismiss" in the notification or use the stop button in the app
+- **Update Settings**: Change Auto-Copy mode or selected devices anytime
+
+---
+
+## βοΈ Settings & Configuration
+
+### Auto-Copy Mode
+
+- **Enabled**: Received text is automatically copied to your clipboard
+- **Disabled**: You'll receive a notification to manually copy the text
+
+### Device Selection
+
+- Choose which paired Bluetooth devices to share clipboard with
+- You can update your selection anytime without restarting the service
+
+### Theme Options
+
+- **Light Mode**: Clean, bright interface
+- **Dark Mode**: Easy on the eyes, battery-friendly
+
+---
+
+## π Privacy & Security
+
+ClipSync is designed with privacy in mind:
+
+- **Local Communication**: All data transfers happen directly between your devices via Bluetooth
+- **No Cloud Storage**: Your clipboard content never leaves your devices
+- **No Internet Required**: Works completely offline
+- **Temporary Storage**: Clipboard content is not permanently stored
+- **Secure Protocol**: Uses Bluetooth RFCOMM with built-in security features
+
+### Security Best Practices
+
+- Only pair with devices you trust
+- Be mindful when sharing sensitive information
+
+---
+
+## π§ Troubleshooting
+
+### Common Issues
+
+#### Devices Not Connecting
+
+- Ensure both devices have Bluetooth enabled
+- Check that devices are properly paired in Android Bluetooth settings
+- Try unpairing and re-pairing the devices
+- Restart the ClipSync service
+
+#### Clipboard Not Syncing
+
+- Verify the ClipSync service is running (check notification)
+- Ensure both devices have ClipSync installed and running
+- Check that the target device is selected in your device list
+- Try copying different text content
+
+#### App Permissions
+
+- Grant all requested permissions during setup
+- Check Android Settings β Apps β ClipSync β Permissions
+- Ensure Bluetooth and Notification permissions are enabled
+
+### Still Having Issues?
+
+- Check our [Issues](../../issues) page for known problems
+- Create a new issue with detailed information about your problem
+- Include your Android version and device model
+
+---
+
+## π€ Contributing
+
+We welcome contributions! Whether it's bug reports, feature requests, or code contributions, every bit helps make ClipSync better.
+
+### How to Contribute
+
+1. Fork the repository
+2. Create a feature branch
+3. Make your changes
+4. Submit a pull request
+
+### Development Setup
+
+```bash
+git clone https://github.com/aubynsamuel/clipsync-android.git
+cd ClipSync-android
+./gradlew build
+```
+
+---
+
+## π License
+
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
+
+---
+
+## π Acknowledgments
+
+- Built with modern Android development practices
+- Uses Jetpack Compose for beautiful UI
+- Implements Material Design 3 guidelines
+- Special thanks to the Android development community
+
+---
+
+
+ Made with β€οΈ for seamless device connectivity
+
+ β Star this repo if ClipSync helps you!
+
diff --git a/app/src/main/res/AppIcon.png b/app/src/main/res/AppIcon.png
new file mode 100644
index 0000000..dc2ea3e
Binary files /dev/null and b/app/src/main/res/AppIcon.png differ
diff --git a/app/src/test/java/com/aubynsamuel/clipsync/BluetoothServiceConnectionTest.kt b/app/src/test/java/com/aubynsamuel/clipsync/BluetoothServiceConnectionTest.kt
index 069b1b6..2395441 100644
--- a/app/src/test/java/com/aubynsamuel/clipsync/BluetoothServiceConnectionTest.kt
+++ b/app/src/test/java/com/aubynsamuel/clipsync/BluetoothServiceConnectionTest.kt
@@ -1,68 +1,68 @@
package com.aubynsamuel.clipsync
-//
-//import android.content.ComponentName
-//import com.aubynsamuel.clipsync.bluetooth.BluetoothService
-//import com.aubynsamuel.clipsync.bluetooth.BluetoothServiceConnection
-//import junit.framework.TestCase
-//import org.junit.Before
-//import org.junit.Test
-//import org.mockito.Mock
-//import org.mockito.MockitoAnnotations
-//import org.mockito.kotlin.mock
-//import org.mockito.kotlin.whenever
-//
-//class BluetoothServiceConnectionTest {
-//
-// @Mock
-// private lateinit var binder: BluetoothService.LocalBinder
-//
-// @Mock
-// private lateinit var bluetoothService: BluetoothService
-//
-// private var onServiceConnectedCalled = false
-// private var onServiceDisconnectedCalled = false
-// private var connectedService: BluetoothService? = null
-//
-// private lateinit var serviceConnection: BluetoothServiceConnection
-//
-// @Before
-// fun setup() {
-// MockitoAnnotations.openMocks(this)
-//
-// serviceConnection = BluetoothServiceConnection(
-// onServiceConnected = { service ->
-// onServiceConnectedCalled = true
-// connectedService = service
-// },
-// onServiceDisconnected = {
-// onServiceDisconnectedCalled = true
-// }
-// )
-// }
-//
-// @Test
-// fun `onServiceConnected calls callback with service`() {
-// // Given
-// val componentName = mock()
-// whenever(binder.getService()).thenReturn(bluetoothService)
-//
-// // When
-// serviceConnection.onServiceConnected(componentName, binder)
-//
-// // Then
-// TestCase.assertEquals(true, onServiceConnectedCalled)
-// TestCase.assertEquals(bluetoothService, connectedService)
-// }
-//
-// @Test
-// fun `onServiceDisconnected calls callback`() {
-// // Given
-// val componentName = mock()
-//
-// // When
-// serviceConnection.onServiceDisconnected(componentName)
-//
-// // Then
-// TestCase.assertEquals(true, onServiceDisconnectedCalled)
-// }
-//}
\ No newline at end of file
+
+import android.content.ComponentName
+import com.aubynsamuel.clipsync.bluetooth.BluetoothService
+import com.aubynsamuel.clipsync.bluetooth.BluetoothServiceConnection
+import junit.framework.TestCase
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+class BluetoothServiceConnectionTest {
+
+ @Mock
+ private lateinit var binder: BluetoothService.LocalBinder
+
+ @Mock
+ private lateinit var bluetoothService: BluetoothService
+
+ private var onServiceConnectedCalled = false
+ private var onServiceDisconnectedCalled = false
+ private var connectedService: BluetoothService? = null
+
+ private lateinit var serviceConnection: BluetoothServiceConnection
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.openMocks(this)
+
+ serviceConnection = BluetoothServiceConnection(
+ onServiceConnected = { service ->
+ onServiceConnectedCalled = true
+ connectedService = service
+ },
+ onServiceDisconnected = {
+ onServiceDisconnectedCalled = true
+ }
+ )
+ }
+
+ @Test
+ fun `onServiceConnected calls callback with service`() {
+ // Given
+ val componentName = mock()
+ whenever(binder.getService()).thenReturn(bluetoothService)
+
+ // When
+ serviceConnection.onServiceConnected(componentName, binder)
+
+ // Then
+ TestCase.assertEquals(true, onServiceConnectedCalled)
+ TestCase.assertEquals(bluetoothService, connectedService)
+ }
+
+ @Test
+ fun `onServiceDisconnected calls callback`() {
+ // Given
+ val componentName = mock()
+
+ // When
+ serviceConnection.onServiceDisconnected(componentName)
+
+ // Then
+ TestCase.assertEquals(true, onServiceDisconnectedCalled)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/aubynsamuel/clipsync/BluetoothServiceTest.kt b/app/src/test/java/com/aubynsamuel/clipsync/BluetoothServiceTest.kt
index 9db3ec7..a021d50 100644
--- a/app/src/test/java/com/aubynsamuel/clipsync/BluetoothServiceTest.kt
+++ b/app/src/test/java/com/aubynsamuel/clipsync/BluetoothServiceTest.kt
@@ -1,264 +1,245 @@
package com.aubynsamuel.clipsync
-//
-//import android.Manifest
-//import android.bluetooth.BluetoothAdapter
-//import android.bluetooth.BluetoothDevice
-//import android.bluetooth.BluetoothManager
-//import android.bluetooth.BluetoothServerSocket
-//import android.bluetooth.BluetoothSocket
-//import android.content.Context
-//import android.content.Intent
-//import android.os.Build
-//import androidx.test.core.app.ApplicationProvider
-//import com.aubynsamuel.clipsync.bluetooth.BluetoothService
-//import com.aubynsamuel.clipsync.bluetooth.SharingResult
-//import com.aubynsamuel.clipsync.core.Essentials
-//import kotlinx.coroutines.ExperimentalCoroutinesApi
-//import kotlinx.coroutines.test.runTest
-//import org.json.JSONObject
-//import org.junit.After
-//import org.junit.Assert
-//import org.junit.Before
-//import org.junit.Test
-//import org.junit.runner.RunWith
-//import org.mockito.Mock
-//import org.mockito.Mockito.timeout
-//import org.mockito.Mockito.`when`
-//import org.mockito.MockitoAnnotations
-//import org.mockito.kotlin.any
-//import org.mockito.kotlin.doReturn
-//import org.mockito.kotlin.mock
-//import org.mockito.kotlin.verify
-//import org.robolectric.Robolectric
-//import org.robolectric.RobolectricTestRunner
-//import org.robolectric.android.controller.ServiceController
-//import org.robolectric.annotation.Config
-//import org.robolectric.shadows.ShadowApplication
-//import java.io.ByteArrayInputStream
-//import java.io.ByteArrayOutputStream
-//import java.io.IOException
-//import java.util.UUID
-//
-//@RunWith(RobolectricTestRunner::class)
-//@Config(sdk = [Build.VERSION_CODES.S])
-//@ExperimentalCoroutinesApi
-//class BluetoothServiceTest {
-//
-// @Mock
-// private lateinit var bluetoothManager: BluetoothManager
-//
-// @Mock
-// private lateinit var bluetoothAdapter: BluetoothAdapter
-//
-// @Mock
-// private lateinit var serverSocket: BluetoothServerSocket
-//
-// @Mock
-// private lateinit var clientSocket: BluetoothSocket
-//
-// @Mock
-// private lateinit var remoteDevice: BluetoothDevice
-//
-// private lateinit var controller: ServiceController
-// private lateinit var service: BluetoothService
-// private lateinit var context: Context
-//
-// private val testUuid: UUID = UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66")
-// private val testDeviceAddress = "00:11:22:33:AA:BB"
-//
-// @Before
-// fun setUp() {
-// MockitoAnnotations.openMocks(this)
-//
-// // Get the real application context from Robolectric
-// context = ApplicationProvider.getApplicationContext()
-//
-// // Mock the system services
-// val shadowApplication = ShadowApplication()
-// shadowApplication.setSystemService(Context.BLUETOOTH_SERVICE, bluetoothManager)
-//
-// // Setup mocks
-// `when`(bluetoothManager.adapter).thenReturn(bluetoothAdapter)
-// `when`(bluetoothAdapter.listenUsingRfcommWithServiceRecord(any(), any())).thenReturn(
-// serverSocket
-// )
-// `when`(bluetoothAdapter.getRemoteDevice(testDeviceAddress)).thenReturn(remoteDevice)
-//
-// // Mock permissions - grant by default
-// shadowApplication.grantPermissions(Manifest.permission.BLUETOOTH_CONNECT)
-//
-// // Create service controller and service
-// controller = Robolectric.buildService(BluetoothService::class.java)
-// service = controller.create().get()
-//
-// // Reset Essentials state
-// Essentials.addresses = arrayOf()
-// Essentials.isServiceBound = false
-// }
-//
-// @After
-// fun tearDown() {
-// // Clean up
-// controller.destroy()
-// Essentials.addresses = arrayOf()
-// Essentials.isServiceBound = false
-// }
-//
-// @Test
-// fun `onStartCommand starts bluetooth server`() {
-// val intent = Intent().putExtra("SELECTED_DEVICES", arrayOf(testDeviceAddress))
-// controller.withIntent(intent).startCommand(0, 1)
-//
-// // Give some time for the coroutine to execute
-// Thread.sleep(100)
-//
-// verify(bluetoothAdapter).listenUsingRfcommWithServiceRecord("ClipSync", testUuid)
-// }
-//
-// @Test
-// fun `shareClipboard returns NO_SELECTED_DEVICES when addresses are empty`() = runTest {
-// // Ensure no devices are selected
-// service.updateSelectedDevices(arrayOf())
-//
-// val result = service.shareClipboard("test")
-// Assert.assertEquals(SharingResult.NO_SELECTED_DEVICES, result)
-// }
-//
-// @Test
-// fun `shareClipboard returns PERMISSION_NOT_GRANTED when permission is missing`() = runTest {
-// // Deny the permission
-// val shadowApplication = ShadowApplication()
-// shadowApplication.denyPermissions(Manifest.permission.BLUETOOTH_CONNECT)
-//
-// service.updateSelectedDevices(arrayOf(testDeviceAddress))
-//
-// val result = service.shareClipboard("test clipboard")
-// Assert.assertEquals(SharingResult.PERMISSION_NOT_GRANTED, result)
-// }
-//
-// @Test
-// fun `shareClipboard returns SUCCESS on successful send`() = runTest {
-// val outputStream = ByteArrayOutputStream()
-// `when`(remoteDevice.createRfcommSocketToServiceRecord(testUuid)).thenReturn(clientSocket)
-// `when`(clientSocket.outputStream).thenReturn(outputStream)
-//
-// service.updateSelectedDevices(arrayOf(testDeviceAddress))
-//
-// val result = service.shareClipboard("hello")
-//
-// Assert.assertEquals(SharingResult.SUCCESS, result)
-//
-// // Verify the JSON was written correctly
-// val sentData = outputStream.toString().trim()
-// val json = JSONObject(sentData)
-// Assert.assertEquals("hello", json.getString("clip"))
-//
-// verify(clientSocket).connect()
-// verify(clientSocket).close()
-// }
-//
-// @Test
-// fun `shareClipboard returns SENDING_ERROR on connection failure`() = runTest {
-// `when`(remoteDevice.createRfcommSocketToServiceRecord(testUuid)).thenReturn(clientSocket)
-// `when`(clientSocket.connect()).thenThrow(IOException("Connection failed!"))
-//
-// service.updateSelectedDevices(arrayOf(testDeviceAddress))
-//
-// val result = service.shareClipboard("test")
-// Assert.assertEquals(SharingResult.SENDING_ERROR, result)
-// }
-//
-// @Test
-// fun `server handles incoming connection and parses data`() {
-// val testMessage = "{\"clip\":\"test clip data from another device\"}\n"
-// val inputStream = ByteArrayInputStream(testMessage.toByteArray())
-// val incomingSocket: BluetoothSocket = mock {
-// on { this.inputStream } doReturn inputStream
-// }
-//
-// // Mock serverSocket.accept() to return the incoming socket once, then throw exception to break the loop
-// `when`(serverSocket.accept())
-// .thenReturn(incomingSocket)
-// .thenThrow(IOException("Server stopped"))
-//
-// // Start the service to initialize the server
-// controller.startCommand(0, 1)
-//
-// // Give the background thread time to run and process the connection
-// Thread.sleep(500)
-//
-// // Verify the socket was closed after processing
-// verify(incomingSocket, timeout(1000)).close()
-// }
-//
-// @Test
-// fun `updateSelectedDevices updates addresses correctly`() = runTest {
-// val testAddresses = arrayOf("11:22:33:44:55:66", "AA:BB:CC:DD:EE:FF")
-//
-// service.updateSelectedDevices(testAddresses)
-//
-// // by trying to share clipboard - it should not return NO_SELECTED_DEVICES
-// // Mock the required dependencies for shareClipboard
-// val mockDevice1 = mock()
-// val mockDevice2 = mock()
-// val mockSocket1 = mock()
-// val mockSocket2 = mock()
-// val outputStream1 = ByteArrayOutputStream()
-// val outputStream2 = ByteArrayOutputStream()
-//
-// `when`(bluetoothAdapter.getRemoteDevice(testAddresses[0])).thenReturn(mockDevice1)
-// `when`(bluetoothAdapter.getRemoteDevice(testAddresses[1])).thenReturn(mockDevice2)
-// `when`(mockDevice1.createRfcommSocketToServiceRecord(testUuid)).thenReturn(mockSocket1)
-// `when`(mockDevice2.createRfcommSocketToServiceRecord(testUuid)).thenReturn(mockSocket2)
-// `when`(mockSocket1.outputStream).thenReturn(outputStream1)
-// `when`(mockSocket2.outputStream).thenReturn(outputStream2)
-//
-// val result = service.shareClipboard("test")
-// // Should not be NO_SELECTED_DEVICES since we have addresses
-// assert(result != SharingResult.NO_SELECTED_DEVICES)
-// }
-//
-// @Test
-// fun `service sets isServiceBound flag in onCreate`() {
-// // Reset the flag first to test the actual behavior
-// Essentials.isServiceBound = false
-//
-// // Create a new service to trigger onCreate
-// val newController = Robolectric.buildService(BluetoothService::class.java)
-// newController.create()
-//
-// // Check if flag is set after onCreate
-// Assert.assertEquals(true, Essentials.isServiceBound)
-//
-// // Clean up
-// newController.destroy()
-// }
-//
-// @Test
-// fun `service sets bluetoothService in Essentials object in onCreate`() {
-// // Reset the flag first to test the actual behavior
-// Essentials.bluetoothService = null
-//
-// // Create a new service to trigger onCreate
-// val newController = Robolectric.buildService(BluetoothService::class.java)
-// val newService = newController.create().get()
-//
-// // Check if flag is set after onCreate
-// Assert.assertEquals(newService, Essentials.bluetoothService)
-//
-// // Clean up
-// newController.destroy()
-// }
-//
-// @Test
-// fun `service clears isServiceBound flag in onDestroy`() {
-// controller.destroy()
-// Assert.assertEquals(null, Essentials.isServiceBound)
-// }
-//
-// @Test
-// fun `service clears bluetoothService in Essentials object in onDestroy`() {
-// controller.destroy()
-// Assert.assertEquals(null, Essentials.bluetoothService)
-// }
-//}
\ No newline at end of file
+
+import android.Manifest
+import android.app.NotificationManager
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothManager
+import android.bluetooth.BluetoothServerSocket
+import android.bluetooth.BluetoothSocket
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.test.core.app.ApplicationProvider
+import com.aubynsamuel.clipsync.bluetooth.BluetoothService
+import com.aubynsamuel.clipsync.bluetooth.SharingResult
+import com.aubynsamuel.clipsync.core.Essentials
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.json.JSONObject
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.android.controller.ServiceController
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowApplication
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.util.UUID
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [Build.VERSION_CODES.S])
+@ExperimentalCoroutinesApi
+class BluetoothServiceTest {
+
+ @Mock
+ private lateinit var bluetoothManager: BluetoothManager
+
+ @Mock
+ private lateinit var bluetoothAdapter: BluetoothAdapter
+
+ @Mock
+ private lateinit var serverSocket: BluetoothServerSocket
+
+ @Mock
+ private lateinit var clientSocket: BluetoothSocket
+
+ @Mock
+ private lateinit var remoteDevice: BluetoothDevice
+
+ @Mock
+ private lateinit var notificationManager: NotificationManager
+
+ @Mock
+ private lateinit var clipboardManager: ClipboardManager
+
+ private lateinit var controller: ServiceController
+ private lateinit var service: BluetoothService
+ private lateinit var context: Context
+
+ private val testUuid: UUID = UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66")
+ private val testDeviceAddress = "00:11:22:33:AA:BB"
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+
+ context = ApplicationProvider.getApplicationContext()
+
+ val shadowApplication = shadowOf(context.applicationContext)
+ shadowApplication.setSystemService(Context.BLUETOOTH_SERVICE, bluetoothManager)
+ shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, notificationManager)
+ shadowApplication.setSystemService(Context.CLIPBOARD_SERVICE, clipboardManager)
+
+ `when`(bluetoothManager.adapter).thenReturn(bluetoothAdapter)
+ `when`(bluetoothAdapter.listenUsingRfcommWithServiceRecord(any(), any())).thenReturn(
+ serverSocket
+ )
+ `when`(bluetoothAdapter.getRemoteDevice(testDeviceAddress)).thenReturn(remoteDevice)
+
+ shadowApplication.grantPermissions(Manifest.permission.BLUETOOTH_CONNECT)
+
+ controller = Robolectric.buildService(BluetoothService::class.java)
+ service = controller.create().get()
+
+ Essentials.addresses = arrayOf()
+ Essentials.isServiceBound = false
+ }
+
+ @After
+ fun tearDown() {
+ controller.destroy()
+ Essentials.addresses = arrayOf()
+ Essentials.isServiceBound = false
+ }
+
+ @Test
+ fun `onStartCommand starts bluetooth server`() = runTest {
+ val intent = Intent().putExtra("SELECTED_DEVICES", arrayOf(testDeviceAddress))
+ controller.withIntent(intent).startCommand(0, 1)
+
+ verify(bluetoothAdapter).listenUsingRfcommWithServiceRecord("ClipSync", testUuid)
+ }
+
+ @Test
+ fun `shareClipboard returns NO_SELECTED_DEVICES when addresses are empty`() = runTest {
+ service.updateSelectedDevices(arrayOf())
+
+ val result = service.shareClipboard("test")
+ Assert.assertEquals(SharingResult.NO_SELECTED_DEVICES, result)
+ }
+
+ @Test
+ fun `shareClipboard returns PERMISSION_NOT_GRANTED when permission is missing`() = runTest {
+ val shadowApplication = shadowOf(context.applicationContext)
+ shadowApplication.denyPermissions(Manifest.permission.BLUETOOTH_CONNECT)
+
+ service.updateSelectedDevices(arrayOf(testDeviceAddress))
+
+ val result = service.shareClipboard("test clipboard")
+ Assert.assertEquals(SharingResult.PERMISSION_NOT_GRANTED, result)
+ }
+
+ @Test
+ fun `shareClipboard returns SUCCESS on successful send`() = runTest {
+ val outputStream = ByteArrayOutputStream()
+ `when`(remoteDevice.createRfcommSocketToServiceRecord(testUuid)).thenReturn(clientSocket)
+ `when`(clientSocket.outputStream).thenReturn(outputStream)
+
+ service.updateSelectedDevices(arrayOf(testDeviceAddress))
+
+ val result = service.shareClipboard("hello")
+
+ Assert.assertEquals(SharingResult.SUCCESS, result)
+
+ val sentData = outputStream.toString().trim()
+ val json = JSONObject(sentData)
+ Assert.assertEquals("hello", json.getString("clip"))
+
+ verify(clientSocket).connect()
+ verify(clientSocket).close()
+ }
+
+ @Test
+ fun `shareClipboard returns SENDING_ERROR on connection failure`() = runTest {
+ `when`(remoteDevice.createRfcommSocketToServiceRecord(testUuid)).thenReturn(clientSocket)
+ `when`(clientSocket.connect()).thenThrow(IOException("Connection failed!"))
+
+ service.updateSelectedDevices(arrayOf(testDeviceAddress))
+
+ val result = service.shareClipboard("test")
+ Assert.assertEquals(SharingResult.SENDING_ERROR, result)
+ }
+
+ @Test
+ fun `server handles incoming connection and parses data`() = runTest {
+ val testMessage = "{\"clip\":\"test clip data from another device\"}\n"
+ val inputStream = ByteArrayInputStream(testMessage.toByteArray())
+ val incomingSocket: BluetoothSocket = mock {
+ on { this.inputStream } doReturn inputStream
+ }
+
+ `when`(serverSocket.accept())
+ .thenReturn(incomingSocket)
+ .thenThrow(IOException("Server stopped"))
+
+ controller.startCommand(0, 1)
+
+ verify(incomingSocket, timeout(1000)).close()
+ }
+
+ @Test
+ fun `updateSelectedDevices updates addresses correctly`() = runTest {
+ val testAddresses = arrayOf("11:22:33:44:55:66", "AA:BB:CC:DD:EE:FF")
+
+ service.updateSelectedDevices(testAddresses)
+
+ val mockDevice1 = mock()
+ val mockDevice2 = mock()
+ val mockSocket1 = mock()
+ val mockSocket2 = mock()
+ val outputStream1 = ByteArrayOutputStream()
+ val outputStream2 = ByteArrayOutputStream()
+
+ `when`(bluetoothAdapter.getRemoteDevice(testAddresses[0])).thenReturn(mockDevice1)
+ `when`(bluetoothAdapter.getRemoteDevice(testAddresses[1])).thenReturn(mockDevice2)
+ `when`(mockDevice1.createRfcommSocketToServiceRecord(testUuid)).thenReturn(mockSocket1)
+ `when`(mockDevice2.createRfcommSocketToServiceRecord(testUuid)).thenReturn(mockSocket2)
+ `when`(mockSocket1.outputStream).thenReturn(outputStream1)
+ `when`(mockSocket2.outputStream).thenReturn(outputStream2)
+
+ val result = service.shareClipboard("test")
+ assert(result != SharingResult.NO_SELECTED_DEVICES)
+ }
+
+ @Test
+ fun `service sets isServiceBound flag in onCreate`() {
+ Essentials.isServiceBound = false
+
+ val newController = Robolectric.buildService(BluetoothService::class.java)
+ newController.create()
+
+ Assert.assertEquals(true, Essentials.isServiceBound)
+
+ newController.destroy()
+ }
+
+ @Test
+ fun `service sets bluetoothService in Essentials object in onCreate`() {
+ Essentials.bluetoothService = null
+
+ val newController = Robolectric.buildService(BluetoothService::class.java)
+ val newService = newController.create().get()
+
+ Assert.assertEquals(newService, Essentials.bluetoothService)
+
+ newController.destroy()
+ }
+
+ @Test
+ fun `service clears isServiceBound flag in onDestroy`() {
+ controller.destroy()
+ Assert.assertEquals(false, Essentials.isServiceBound)
+ }
+
+ @Test
+ fun `service clears bluetoothService in Essentials object in onDestroy`() {
+ controller.destroy()
+ Assert.assertEquals(null, Essentials.bluetoothService)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/aubynsamuel/clipsync/MainActivityTest.kt b/app/src/test/java/com/aubynsamuel/clipsync/MainActivityTest.kt
index 0507630..5a0d0b6 100644
--- a/app/src/test/java/com/aubynsamuel/clipsync/MainActivityTest.kt
+++ b/app/src/test/java/com/aubynsamuel/clipsync/MainActivityTest.kt
@@ -1,376 +1,376 @@
package com.aubynsamuel.clipsync
-//
-//import android.Manifest
-//import android.bluetooth.BluetoothAdapter
-//import android.bluetooth.BluetoothDevice
-//import android.bluetooth.BluetoothManager
-//import android.content.Context
-//import android.os.Build
-//import androidx.test.core.app.ApplicationProvider
-//import com.aubynsamuel.clipsync.activities.MainActivity
-//import com.aubynsamuel.clipsync.bluetooth.BluetoothService
-//import kotlinx.coroutines.ExperimentalCoroutinesApi
-//import kotlinx.coroutines.test.runTest
-//import org.junit.Assert.assertEquals
-//import org.junit.Assert.assertTrue
-//import org.junit.Before
-//import org.junit.Test
-//import org.junit.runner.RunWith
-//import org.mockito.Mock
-//import org.mockito.Mockito.never
-//import org.mockito.Mockito.verify
-//import org.mockito.Mockito.`when`
-//import org.mockito.MockitoAnnotations
-//import org.robolectric.Robolectric
-//import org.robolectric.RobolectricTestRunner
-//import org.robolectric.android.controller.ActivityController
-//import org.robolectric.annotation.Config
-//import org.robolectric.shadows.ShadowApplication
-//import org.robolectric.shadows.ShadowToast
-//import org.robolectric.util.ReflectionHelpers
-//
-//@RunWith(RobolectricTestRunner::class)
-//@Config(sdk = [Build.VERSION_CODES.S])
-//@ExperimentalCoroutinesApi
-//class MainActivityTest {
-//
-// @Mock
-// private lateinit var mockBluetoothManager: BluetoothManager
-//
-// @Mock
-// private lateinit var mockBluetoothAdapter: BluetoothAdapter
-//
-// @Mock
-// private lateinit var mockBluetoothDevice1: BluetoothDevice
-//
-// @Mock
-// private lateinit var mockBluetoothDevice2: BluetoothDevice
-//
-// private lateinit var controller: ActivityController
-// private lateinit var activity: MainActivity
-// private lateinit var context: Context
-// private lateinit var shadowApplication: ShadowApplication
-//
-// @Before
-// fun setUp() {
-// MockitoAnnotations.openMocks(this)
-//
-// // Get the real application context from Robolectric
-// context = ApplicationProvider.getApplicationContext()
-// shadowApplication = ShadowApplication()
-//
-// // Mock system services
-// shadowApplication.setSystemService(Context.BLUETOOTH_SERVICE, mockBluetoothManager)
-//
-// // Setup mock bluetooth manager to return mock adapter
-// `when`(mockBluetoothManager.adapter).thenReturn(mockBluetoothAdapter)
-//
-// // Grant permissions by default
-// shadowApplication.grantPermissions(
-// Manifest.permission.BLUETOOTH_CONNECT,
-// Manifest.permission.BLUETOOTH_SCAN,
-// Manifest.permission.BLUETOOTH_ADVERTISE,
-// Manifest.permission.POST_NOTIFICATIONS,
-// Manifest.permission.BLUETOOTH,
-// Manifest.permission.BLUETOOTH_ADMIN
-// )
-//
-// // Create activity controller and activity
-// controller = Robolectric.buildActivity(MainActivity::class.java)
-// activity = controller.create().resume().get() // Resume to trigger onCreate
-//
-// // Clear any previous toasts
-// ShadowToast.reset()
-// }
-//
-// @Test
-// fun `onCreate should initialize bluetooth components`() {
-// // Given - activity is already created and resumed in setUp
-//
-// // Then - verify activity was created successfully
-// assertTrue(true)
-// }
-//
-// @Test
-// fun `checkPermissions should request missing permissions on Android S and above`() {
-// // Given - deny one permission
-// shadowApplication.denyPermissions(Manifest.permission.BLUETOOTH_CONNECT)
-//
-// // When
+
+import android.Manifest
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothManager
+import android.content.Context
+import android.os.Build
+import androidx.test.core.app.ApplicationProvider
+import com.aubynsamuel.clipsync.activities.MainActivity
+import com.aubynsamuel.clipsync.bluetooth.BluetoothService
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.robolectric.Robolectric
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.android.controller.ActivityController
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowApplication
+import org.robolectric.shadows.ShadowToast
+import org.robolectric.util.ReflectionHelpers
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [Build.VERSION_CODES.S])
+@ExperimentalCoroutinesApi
+class MainActivityTest {
+
+ @Mock
+ private lateinit var mockBluetoothManager: BluetoothManager
+
+ @Mock
+ private lateinit var mockBluetoothAdapter: BluetoothAdapter
+
+ @Mock
+ private lateinit var mockBluetoothDevice1: BluetoothDevice
+
+ @Mock
+ private lateinit var mockBluetoothDevice2: BluetoothDevice
+
+ private lateinit var controller: ActivityController
+ private lateinit var activity: MainActivity
+ private lateinit var context: Context
+ private lateinit var shadowApplication: ShadowApplication
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+
+ // Get the real application context from Robolectric
+ context = ApplicationProvider.getApplicationContext()
+ shadowApplication = ShadowApplication()
+
+ // Mock system services
+ shadowApplication.setSystemService(Context.BLUETOOTH_SERVICE, mockBluetoothManager)
+
+ // Setup mock bluetooth manager to return mock adapter
+ `when`(mockBluetoothManager.adapter).thenReturn(mockBluetoothAdapter)
+
+ // Grant permissions by default
+ shadowApplication.grantPermissions(
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_ADVERTISE,
+ Manifest.permission.POST_NOTIFICATIONS,
+ Manifest.permission.BLUETOOTH,
+ Manifest.permission.BLUETOOTH_ADMIN
+ )
+
+ // Create activity controller and activity
+ controller = Robolectric.buildActivity(MainActivity::class.java)
+ activity = controller.create().resume().get() // Resume to trigger onCreate
+
+ // Clear any previous toasts
+ ShadowToast.reset()
+ }
+
+ @Test
+ fun `onCreate should initialize bluetooth components`() {
+ // Given - activity is already created and resumed in setUp
+
+ // Then - verify activity was created successfully
+ assertTrue(true)
+ }
+
+ @Test
+ fun `checkPermissions should request missing permissions on Android S and above`() {
+ // Given - deny one permission
+ shadowApplication.denyPermissions(Manifest.permission.BLUETOOTH_CONNECT)
+
+ // When
+ ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
+
+ // Then - verify permission request flow was triggered
+ // In a real scenario, this would trigger the permission launcher
+ assertTrue("Activity should handle missing permissions", true)
+ }
+
+ @Test
+ fun `checkPermissions should proceed to bluetooth check when all permissions granted`() {
+ // Given - all permissions already granted in setUp
+ `when`(mockBluetoothAdapter.isEnabled).thenReturn(true)
+ `when`(mockBluetoothAdapter.bondedDevices).thenReturn(emptySet())
+
+ /**
+ * No need to call checkPermissions explicitly, it is called in onCreate
+ * which is called in setUp
+ */
// ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
-//
-// // Then - verify permission request flow was triggered
-// // In a real scenario, this would trigger the permission launcher
-// assertTrue("Activity should handle missing permissions", true)
-// }
-//
-// @Test
-// fun `checkPermissions should proceed to bluetooth check when all permissions granted`() {
-// // Given - all permissions already granted in setUp
-// `when`(mockBluetoothAdapter.isEnabled).thenReturn(true)
-// `when`(mockBluetoothAdapter.bondedDevices).thenReturn(emptySet())
-//
-// /**
-// * No need to call checkPermissions explicitly, it is called in onCreate
-// * which is called in setUp
-// */
-//// ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
-//
-// verify(mockBluetoothAdapter).isEnabled
-// }
-//
-// @Test
-// fun `checkBluetoothEnabled should request enable when bluetooth disabled`() {
-// // Given
-// `when`(mockBluetoothAdapter.isEnabled).thenReturn(false)
-//
-// // When
-// ReflectionHelpers.callInstanceMethod(activity, "checkBluetoothEnabled")
-//
-// // Then - verify enable bluetooth intent was started
-// val nextStartedActivity = shadowApplication.nextStartedActivity
-// assertEquals(BluetoothAdapter.ACTION_REQUEST_ENABLE, nextStartedActivity.action)
-// }
-//
-// @Test
-// fun `checkBluetoothEnabled should load paired devices when bluetooth enabled`() {
-// // Given
-// `when`(mockBluetoothAdapter.isEnabled).thenReturn(true)
-// val mockDevices = setOf(mockBluetoothDevice1, mockBluetoothDevice2)
-// `when`(mockBluetoothAdapter.bondedDevices).thenReturn(mockDevices)
-//
-// // When
-// ReflectionHelpers.callInstanceMethod(activity, "checkBluetoothEnabled")
-//
-// // Then
-// verify(mockBluetoothAdapter).bondedDevices
-// }
-//
-// @Test
-// fun `loadPairedDevices should return empty set when permission denied`() {
-// // Given - deny bluetooth permission
-// shadowApplication.denyPermissions(Manifest.permission.BLUETOOTH_CONNECT)
-//
-// // When
-// val result = ReflectionHelpers.callInstanceMethod>(
-// activity, "loadPairedDevices"
-// )
-//
-// // Then
-// assertTrue(result.isEmpty())
-// verify(mockBluetoothAdapter, never()).bondedDevices
-// }
-//
-// @Test
-// fun `loadPairedDevices should return bonded devices when permission granted`() {
-// // Given
-// val mockDevices = setOf(mockBluetoothDevice1, mockBluetoothDevice2)
-// `when`(mockBluetoothAdapter.bondedDevices).thenReturn(mockDevices)
-//
-// // When
-// val result = ReflectionHelpers.callInstanceMethod>(
-// activity, "loadPairedDevices"
-// )
-//
-// // Then
-// assertEquals(mockDevices, result)
-// verify(mockBluetoothAdapter).bondedDevices
-// }
-//
-// @Test
-// fun `loadPairedDevices should handle null bonded devices`() {
-// // Given
-// `when`(mockBluetoothAdapter.bondedDevices).thenReturn(null)
-//
-// // When
-// val result = ReflectionHelpers.callInstanceMethod>(
-// activity, "loadPairedDevices"
-// )
-//
-// // Then
-// assertTrue(result.isEmpty())
-// }
-//
-// @Test
-// @Config(sdk = [Build.VERSION_CODES.O])
-// fun `startBluetoothService should start foreground service on Android O and above`() {
-// // Given
-// val selectedDevices = setOf("device1", "device2")
-//
-// // When
-// ReflectionHelpers.callInstanceMethod(
-// activity, "startBluetoothService",
-// ReflectionHelpers.ClassParameter(Set::class.java, selectedDevices)
-// )
-//
-// // Then
-// val nextStartedService = shadowApplication.nextStartedService
-// assertEquals(BluetoothService::class.java.name, nextStartedService.component?.className)
-// val extras = nextStartedService.getStringArrayExtra("SELECTED_DEVICES")
-// assertTrue(extras?.toSet() == selectedDevices)
-// }
-//
-// @Test
-// @Config(sdk = [Build.VERSION_CODES.N])
-// fun `startBluetoothService should start regular service on pre-Android O`() {
-// // Given
-// val selectedDevices = setOf("device1", "device2")
-//
-// // When
-// ReflectionHelpers.callInstanceMethod(
-// activity, "startBluetoothService",
-// ReflectionHelpers.ClassParameter(Set::class.java, selectedDevices)
-// )
-//
-// // Then
-// val nextStartedService = shadowApplication.nextStartedService
-// assertEquals(BluetoothService::class.java.name, nextStartedService.component?.className)
-// }
-//
-// @Test
-// fun `stopBluetoothService should stop the bluetooth service`() {
-// // When
-// ReflectionHelpers.callInstanceMethod(activity, "stopBluetoothService")
-//
-// // Then
-// val nextStoppedService = shadowApplication.nextStoppedService
-// assertEquals(BluetoothService::class.java.name, nextStoppedService.component?.className)
-// }
-//
-// @Test
-// fun `requestEnableBluetooth callback should load paired devices on success`() = runTest {
-// // Given
-// `when`(mockBluetoothAdapter.bondedDevices).thenReturn(setOf(mockBluetoothDevice1))
-// `when`(mockBluetoothAdapter.isEnabled).thenReturn(true)
-//
-// // When - simulate the callback flow by directly calling checkBluetoothEnabled after enabling
-// ReflectionHelpers.callInstanceMethod(activity, "checkBluetoothEnabled")
-//
-// // Then - verify paired devices were loaded
-// verify(mockBluetoothAdapter).bondedDevices
-// }
-//
-// @Test
-// fun `requestEnableBluetooth callback should show toast and retry on failure`() {
-// // Given
-// `when`(mockBluetoothAdapter.isEnabled).thenReturn(false)
-//
-// // When - simulate failure by checking bluetooth when disabled
-// ReflectionHelpers.callInstanceMethod(activity, "checkBluetoothEnabled")
-//
-// // Then - verify enable bluetooth request was made (retry behavior)
-// val nextStartedActivity = shadowApplication.nextStartedActivity
-// assertEquals(BluetoothAdapter.ACTION_REQUEST_ENABLE, nextStartedActivity.action)
-// }
-//
-// @Test
-// fun `permissions callback should proceed when all permissions granted`() = runTest {
-// // Given - all permissions granted (already set in setUp)
-// `when`(mockBluetoothAdapter.isEnabled).thenReturn(true)
-// `when`(mockBluetoothAdapter.bondedDevices).thenReturn(setOf(mockBluetoothDevice1))
-//
-// // When
-// ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
-//
-// // Then - verify that `checkBluetoothEnabled` which loads devices is called.
-// verify(mockBluetoothAdapter).bondedDevices
-// }
-//
-// @Test
-// fun `permissions callback should handle denied permissions gracefully`() {
-// // Given - deny a critical permission
-// shadowApplication.denyPermissions(Manifest.permission.BLUETOOTH_CONNECT)
-//
-// // When
-// ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
-//
-// // Then - verify graceful handling (no crash, appropriate flow)
-// assertTrue("Activity should handle denied permissions gracefully", true)
-// }
-//
-// @Test
-// @Config(sdk = [Build.VERSION_CODES.S])
-// fun `activity should request correct permissions for Android S and above`() {
-// // Given - deny all Android S permissions
-// shadowApplication.denyPermissions(
-// Manifest.permission.BLUETOOTH_CONNECT,
-// Manifest.permission.BLUETOOTH_SCAN,
-// Manifest.permission.BLUETOOTH_ADVERTISE,
-// Manifest.permission.POST_NOTIFICATIONS
-// )
-//
-// // When
-// ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
-//
-// // Then - verify the method doesn't crash and handles S+ permissions
-// assertTrue("Should handle Android S+ permissions correctly", true)
-// }
-//
-// @Test
-// @Config(sdk = [Build.VERSION_CODES.R])
-// fun `activity should request correct permissions for pre-Android S`() {
-// // Given - deny legacy permissions
-// shadowApplication.denyPermissions(
-// Manifest.permission.BLUETOOTH,
-// Manifest.permission.BLUETOOTH_ADMIN
-// )
-//
-// // When
-// ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
-//
-// // Then - verify the method handles legacy permissions
-// assertTrue("Should handle pre-Android S permissions correctly", true)
-// }
-//
-// @Test
-// fun `startBluetoothService should pass correct device addresses`() {
-// // Given
-// val testDevices = setOf("AA:BB:CC:DD:EE:FF", "11:22:33:44:55:66")
-//
-// // When
-// ReflectionHelpers.callInstanceMethod(
-// activity, "startBluetoothService",
-// ReflectionHelpers.ClassParameter(Set::class.java, testDevices)
-// )
-//
-// // Then
-// val startedService = shadowApplication.nextStartedService
-// val deviceArray = startedService.getStringArrayExtra("SELECTED_DEVICES")
-// assertEquals(testDevices, deviceArray?.toSet())
-// }
-//
-// @Test
-// fun `loadPairedDevices should update pairedDevices state`() {
-// // Given
-// val testDevices = setOf(mockBluetoothDevice1, mockBluetoothDevice2)
-// `when`(mockBluetoothAdapter.bondedDevices).thenReturn(testDevices)
-//
-// // When
-// ReflectionHelpers.callInstanceMethod>(
-// activity, "loadPairedDevices"
-// )
-//
-// // Then
-// // We can't directly access and assert the MutableState.
-// // Instead, we verify that the method that *uses* this state
-// // (or the method that updates it) was called as expected.
-// // For this test, verifying that `getBondedDevices` was called
-// // implies that the state update logic within `loadPairedDevices` ran.
-// verify(mockBluetoothAdapter).bondedDevices
-// // Further testing of the UI or other methods that observe this state
-// // would be needed for complete verification of the state update.
-// }
-//
-// @Test
-// fun `activity should handle bluetooth adapter initialization`() {
-// // Given - create a new activity to test initialization
-// val newController = Robolectric.buildActivity(MainActivity::class.java)
-// val newActivity = newController.create().get()
-//
-// // Then - verify activity initialized without crashing
-// assertTrue(newActivity is MainActivity)
-//
-// // Clean up
-// newController.destroy()
-// }
-//}
\ No newline at end of file
+
+ verify(mockBluetoothAdapter).isEnabled
+ }
+
+ @Test
+ fun `checkBluetoothEnabled should request enable when bluetooth disabled`() {
+ // Given
+ `when`(mockBluetoothAdapter.isEnabled).thenReturn(false)
+
+ // When
+ ReflectionHelpers.callInstanceMethod(activity, "checkBluetoothEnabled")
+
+ // Then - verify enable bluetooth intent was started
+ val nextStartedActivity = shadowApplication.nextStartedActivity
+ assertEquals(BluetoothAdapter.ACTION_REQUEST_ENABLE, nextStartedActivity.action)
+ }
+
+ @Test
+ fun `checkBluetoothEnabled should load paired devices when bluetooth enabled`() {
+ // Given
+ `when`(mockBluetoothAdapter.isEnabled).thenReturn(true)
+ val mockDevices = setOf(mockBluetoothDevice1, mockBluetoothDevice2)
+ `when`(mockBluetoothAdapter.bondedDevices).thenReturn(mockDevices)
+
+ // When
+ ReflectionHelpers.callInstanceMethod(activity, "checkBluetoothEnabled")
+
+ // Then
+ verify(mockBluetoothAdapter).bondedDevices
+ }
+
+ @Test
+ fun `loadPairedDevices should return empty set when permission denied`() {
+ // Given - deny bluetooth permission
+ shadowApplication.denyPermissions(Manifest.permission.BLUETOOTH_CONNECT)
+
+ // When
+ val result = ReflectionHelpers.callInstanceMethod>(
+ activity, "loadPairedDevices"
+ )
+
+ // Then
+ assertTrue(result.isEmpty())
+ verify(mockBluetoothAdapter, never()).bondedDevices
+ }
+
+ @Test
+ fun `loadPairedDevices should return bonded devices when permission granted`() {
+ // Given
+ val mockDevices = setOf(mockBluetoothDevice1, mockBluetoothDevice2)
+ `when`(mockBluetoothAdapter.bondedDevices).thenReturn(mockDevices)
+
+ // When
+ val result = ReflectionHelpers.callInstanceMethod>(
+ activity, "loadPairedDevices"
+ )
+
+ // Then
+ assertEquals(mockDevices, result)
+ verify(mockBluetoothAdapter).bondedDevices
+ }
+
+ @Test
+ fun `loadPairedDevices should handle null bonded devices`() {
+ // Given
+ `when`(mockBluetoothAdapter.bondedDevices).thenReturn(null)
+
+ // When
+ val result = ReflectionHelpers.callInstanceMethod>(
+ activity, "loadPairedDevices"
+ )
+
+ // Then
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.O])
+ fun `startBluetoothService should start foreground service on Android O and above`() {
+ // Given
+ val selectedDevices = setOf("device1", "device2")
+
+ // When
+ ReflectionHelpers.callInstanceMethod(
+ activity, "startBluetoothService",
+ ReflectionHelpers.ClassParameter(Set::class.java, selectedDevices)
+ )
+
+ // Then
+ val nextStartedService = shadowApplication.nextStartedService
+ assertEquals(BluetoothService::class.java.name, nextStartedService.component?.className)
+ val extras = nextStartedService.getStringArrayExtra("SELECTED_DEVICES")
+ assertTrue(extras?.toSet() == selectedDevices)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.N])
+ fun `startBluetoothService should start regular service on pre-Android O`() {
+ // Given
+ val selectedDevices = setOf("device1", "device2")
+
+ // When
+ ReflectionHelpers.callInstanceMethod(
+ activity, "startBluetoothService",
+ ReflectionHelpers.ClassParameter(Set::class.java, selectedDevices)
+ )
+
+ // Then
+ val nextStartedService = shadowApplication.nextStartedService
+ assertEquals(BluetoothService::class.java.name, nextStartedService.component?.className)
+ }
+
+ @Test
+ fun `stopBluetoothService should stop the bluetooth service`() {
+ // When
+ ReflectionHelpers.callInstanceMethod(activity, "stopBluetoothService")
+
+ // Then
+ val nextStoppedService = shadowApplication.nextStoppedService
+ assertEquals(BluetoothService::class.java.name, nextStoppedService.component?.className)
+ }
+
+ @Test
+ fun `requestEnableBluetooth callback should load paired devices on success`() = runTest {
+ // Given
+ `when`(mockBluetoothAdapter.bondedDevices).thenReturn(setOf(mockBluetoothDevice1))
+ `when`(mockBluetoothAdapter.isEnabled).thenReturn(true)
+
+ // When - simulate the callback flow by directly calling checkBluetoothEnabled after enabling
+ ReflectionHelpers.callInstanceMethod(activity, "checkBluetoothEnabled")
+
+ // Then - verify paired devices were loaded
+ verify(mockBluetoothAdapter).bondedDevices
+ }
+
+ @Test
+ fun `requestEnableBluetooth callback should show toast and retry on failure`() {
+ // Given
+ `when`(mockBluetoothAdapter.isEnabled).thenReturn(false)
+
+ // When - simulate failure by checking bluetooth when disabled
+ ReflectionHelpers.callInstanceMethod(activity, "checkBluetoothEnabled")
+
+ // Then - verify enable bluetooth request was made (retry behavior)
+ val nextStartedActivity = shadowApplication.nextStartedActivity
+ assertEquals(BluetoothAdapter.ACTION_REQUEST_ENABLE, nextStartedActivity.action)
+ }
+
+ @Test
+ fun `permissions callback should proceed when all permissions granted`() = runTest {
+ // Given - all permissions granted (already set in setUp)
+ `when`(mockBluetoothAdapter.isEnabled).thenReturn(true)
+ `when`(mockBluetoothAdapter.bondedDevices).thenReturn(setOf(mockBluetoothDevice1))
+
+ // When
+ ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
+
+ // Then - verify that `checkBluetoothEnabled` which loads devices is called.
+ verify(mockBluetoothAdapter).bondedDevices
+ }
+
+ @Test
+ fun `permissions callback should handle denied permissions gracefully`() {
+ // Given - deny a critical permission
+ shadowApplication.denyPermissions(Manifest.permission.BLUETOOTH_CONNECT)
+
+ // When
+ ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
+
+ // Then - verify graceful handling (no crash, appropriate flow)
+ assertTrue("Activity should handle denied permissions gracefully", true)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.S])
+ fun `activity should request correct permissions for Android S and above`() {
+ // Given - deny all Android S permissions
+ shadowApplication.denyPermissions(
+ Manifest.permission.BLUETOOTH_CONNECT,
+ Manifest.permission.BLUETOOTH_SCAN,
+ Manifest.permission.BLUETOOTH_ADVERTISE,
+ Manifest.permission.POST_NOTIFICATIONS
+ )
+
+ // When
+ ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
+
+ // Then - verify the method doesn't crash and handles S+ permissions
+ assertTrue("Should handle Android S+ permissions correctly", true)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.R])
+ fun `activity should request correct permissions for pre-Android S`() {
+ // Given - deny legacy permissions
+ shadowApplication.denyPermissions(
+ Manifest.permission.BLUETOOTH,
+ Manifest.permission.BLUETOOTH_ADMIN
+ )
+
+ // When
+ ReflectionHelpers.callInstanceMethod(activity, "checkPermissions")
+
+ // Then - verify the method handles legacy permissions
+ assertTrue("Should handle pre-Android S permissions correctly", true)
+ }
+
+ @Test
+ fun `startBluetoothService should pass correct device addresses`() {
+ // Given
+ val testDevices = setOf("AA:BB:CC:DD:EE:FF", "11:22:33:44:55:66")
+
+ // When
+ ReflectionHelpers.callInstanceMethod(
+ activity, "startBluetoothService",
+ ReflectionHelpers.ClassParameter(Set::class.java, testDevices)
+ )
+
+ // Then
+ val startedService = shadowApplication.nextStartedService
+ val deviceArray = startedService.getStringArrayExtra("SELECTED_DEVICES")
+ assertEquals(testDevices, deviceArray?.toSet())
+ }
+
+ @Test
+ fun `loadPairedDevices should update pairedDevices state`() {
+ // Given
+ val testDevices = setOf(mockBluetoothDevice1, mockBluetoothDevice2)
+ `when`(mockBluetoothAdapter.bondedDevices).thenReturn(testDevices)
+
+ // When
+ ReflectionHelpers.callInstanceMethod>(
+ activity, "loadPairedDevices"
+ )
+
+ // Then
+ // We can't directly access and assert the MutableState.
+ // Instead, we verify that the method that *uses* this state
+ // (or the method that updates it) was called as expected.
+ // For this test, verifying that `getBondedDevices` was called
+ // implies that the state update logic within `loadPairedDevices` ran.
+ verify(mockBluetoothAdapter).bondedDevices
+ // Further testing of the UI or other methods that observe this state
+ // would be needed for complete verification of the state update.
+ }
+
+ @Test
+ fun `activity should handle bluetooth adapter initialization`() {
+ // Given - create a new activity to test initialization
+ val newController = Robolectric.buildActivity(MainActivity::class.java)
+ val newActivity = newController.create().get()
+
+ // Then - verify activity initialized without crashing
+ assertTrue(newActivity is MainActivity)
+
+ // Clean up
+ newController.destroy()
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/aubynsamuel/clipsync/NotificationReceiverTest.kt b/app/src/test/java/com/aubynsamuel/clipsync/NotificationReceiverTest.kt
index 934d148..6e974c4 100644
--- a/app/src/test/java/com/aubynsamuel/clipsync/NotificationReceiverTest.kt
+++ b/app/src/test/java/com/aubynsamuel/clipsync/NotificationReceiverTest.kt
@@ -1,284 +1,284 @@
package com.aubynsamuel.clipsync
-//
-//import android.app.NotificationManager
-//import android.content.ClipData
-//import android.content.ClipboardManager
-//import android.content.Context
-//import android.content.Intent
-//import android.os.Build
-//import androidx.test.core.app.ApplicationProvider
-//import com.aubynsamuel.clipsync.bluetooth.BluetoothService
-//import com.aubynsamuel.clipsync.notification.NotificationReceiver
-//import org.junit.Assert.assertEquals
-//import org.junit.Assert.assertNull
-//import org.junit.Before
-//import org.junit.Test
-//import org.junit.runner.RunWith
-//import org.mockito.ArgumentCaptor
-//import org.mockito.Mock
-//import org.mockito.Mockito.never
-//import org.mockito.Mockito.times
-//import org.mockito.Mockito.verify
-//import org.mockito.MockitoAnnotations
-//import org.mockito.kotlin.any
-//import org.robolectric.RobolectricTestRunner
-//import org.robolectric.annotation.Config
-//import org.robolectric.shadows.ShadowApplication
-//import org.robolectric.shadows.ShadowToast
-//
-//@RunWith(RobolectricTestRunner::class)
-//@Config(sdk = [Build.VERSION_CODES.S]) // Testing with Android 12 (API 31)
-//class NotificationReceiverTest {
-//
-// private lateinit var notificationReceiver: NotificationReceiver
-// private lateinit var context: Context
-// private lateinit var shadowApplication: ShadowApplication
-//
-// @Mock
-// private lateinit var mockNotificationManager: NotificationManager
-//
-// @Mock
-// private lateinit var mockClipboardManager: ClipboardManager
-//
-// @Before
-// fun setUp() {
-// MockitoAnnotations.openMocks(this)
-//
-// notificationReceiver = NotificationReceiver()
-// context = ApplicationProvider.getApplicationContext()
-// shadowApplication = ShadowApplication()
-//
-// // Mock system services
-// shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mockNotificationManager)
-// shadowApplication.setSystemService(Context.CLIPBOARD_SERVICE, mockClipboardManager)
-// }
-//
-// @Test
-// fun `onReceive with ACTION_DISMISS stops service and cancels notification`() {
-// // Arrange
-// val intent = Intent().apply {
-// action = "ACTION_DISMISS"
-// }
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// // Verify service was stopped
-// val nextStoppedService = shadowApplication.nextStoppedService
-// assertEquals(BluetoothService::class.java.name, nextStoppedService.component?.className)
-//
-// // Verify notification was cancelled
-// verify(mockNotificationManager).cancel(1001)
-// }
-//
-// @Test
-// fun `onReceive with ACTION_COPY copies text to clipboard and shows toast on older Android`() {
-// // Arrange
-// val testClipText = "Test clipboard text"
-// val testNotificationId = 12345
-// val intent = Intent().apply {
-// action = "ACTION_COPY"
-// putExtra("CLIP_TEXT", testClipText)
-// putExtra("NOTIFICATION_ID", testNotificationId)
-// }
-//
-// val clipDataCaptor = ArgumentCaptor.forClass(ClipData::class.java)
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// // Verify clipboard was set
-// verify(mockClipboardManager).setPrimaryClip(clipDataCaptor.capture())
-//
-// // Verify the ClipData contains correct text
-// val capturedClipData = clipDataCaptor.value
-// assertEquals(testClipText, capturedClipData.getItemAt(0).text.toString())
-// assertEquals("Received Text", capturedClipData.description.label.toString())
-//
-// // Verify toast was shown (on Android < 13) using Robolectric's ShadowToast
-// val latestToast = ShadowToast.getLatestToast()
-// assertEquals("Copied to clipboard", ShadowToast.getTextOfLatestToast())
-//
-// // Verify notification was cancelled with correct ID
-// verify(mockNotificationManager).cancel(testNotificationId)
-// }
-//
-// @Test
-// @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) // Android 13 (API 33)
-// fun `onReceive with ACTION_COPY does not show toast on Android 13+`() {
-// // Arrange
-// val testClipText = "Test clipboard text"
-// val testNotificationId = 12345
-// val intent = Intent().apply {
-// action = "ACTION_COPY"
-// putExtra("CLIP_TEXT", testClipText)
-// putExtra("NOTIFICATION_ID", testNotificationId)
-// }
-//
-// // Clear any previous toasts
-// ShadowToast.reset()
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// // Verify clipboard was still set
-// verify(mockClipboardManager).setPrimaryClip(any())
-//
-// // Verify toast was NOT shown (on Android 13+)
-// assertNull("No toast should be shown on Android 13+", ShadowToast.getLatestToast())
-//
-// // Verify notification was still cancelled
-// verify(mockNotificationManager).cancel(testNotificationId)
-// }
-//
-// @Test
-// fun `onReceive with ACTION_COPY and null CLIP_TEXT returns early`() {
-// // Arrange
-// val intent = Intent().apply {
-// action = "ACTION_COPY"
-// // No CLIP_TEXT extra
-// }
-//
-// // Clear any previous toasts
-// ShadowToast.reset()
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// // Verify nothing was called since we returned early
-// verify(mockClipboardManager, never()).setPrimaryClip(any())
-// assertNull("No toast should be shown when CLIP_TEXT is null", ShadowToast.getLatestToast())
-// verify(mockNotificationManager, never()).cancel(any())
-// }
-//
-// @Test
-// fun `onReceive with ACTION_COPY and empty CLIP_TEXT still processes`() {
-// // Arrange
-// val testClipText = ""
-// val testNotificationId = 12345
-// val intent = Intent().apply {
-// action = "ACTION_COPY"
-// putExtra("CLIP_TEXT", testClipText)
-// putExtra("NOTIFICATION_ID", testNotificationId)
-// }
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// // Verify clipboard was set with empty text
-// verify(mockClipboardManager).setPrimaryClip(any())
-// assertEquals("Copied to clipboard", ShadowToast.getTextOfLatestToast())
-// verify(mockNotificationManager).cancel(testNotificationId)
-// }
-//
-// @Test
-// fun `onReceive with ACTION_COPY and no NOTIFICATION_ID does not cancel notification`() {
-// // Arrange
-// val testClipText = "Test text"
-// val intent = Intent().apply {
-// action = "ACTION_COPY"
-// putExtra("CLIP_TEXT", testClipText)
-// // No NOTIFICATION_ID extra (defaults to 0)
-// }
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// // Verify clipboard operations still happened
-// verify(mockClipboardManager).setPrimaryClip(any())
-// assertEquals("Copied to clipboard", ShadowToast.getTextOfLatestToast())
-//
-// // Verify notification was NOT cancelled since ID was 0
-// verify(mockNotificationManager, never()).cancel(any())
-// }
-//
-// @Test
-// fun `onReceive with unknown action does nothing`() {
-// // Arrange
-// val intent = Intent().apply {
-// action = "UNKNOWN_ACTION"
-// }
-//
-// // Clear any previous toasts
-// ShadowToast.reset()
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// // Verify no interactions occurred
-// verify(mockNotificationManager, never()).cancel(any())
-// verify(mockClipboardManager, never()).setPrimaryClip(any())
-// assertNull("No toast should be shown for unknown action", ShadowToast.getLatestToast())
-//
-// // Verify no services were stopped
-// val nextStoppedService = shadowApplication.nextStoppedService
-// assertNull(nextStoppedService)
-// }
-//
-// @Test
-// fun `onReceive with null action does nothing`() {
-// // Arrange
-// val intent = Intent() // No action set
-//
-// // Clear any previous toasts
-// ShadowToast.reset()
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// // Verify no interactions occurred
-// verify(mockNotificationManager, never()).cancel(any())
-// verify(mockClipboardManager, never()).setPrimaryClip(any())
-// assertNull("No toast should be shown for null action", ShadowToast.getLatestToast())
-// }
-//
-// @Test
-// fun `onReceive with ACTION_COPY verifies ClipData creation parameters`() {
-// // Arrange
-// val testClipText = "Sample clipboard content"
-// val intent = Intent().apply {
-// action = "ACTION_COPY"
-// putExtra("CLIP_TEXT", testClipText)
-// putExtra("NOTIFICATION_ID", 999)
-// }
-//
-// val clipDataCaptor = ArgumentCaptor.forClass(ClipData::class.java)
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// verify(mockClipboardManager).setPrimaryClip(clipDataCaptor.capture())
-//
-// val capturedClipData = clipDataCaptor.value
-// // Verify ClipData properties
-// assertEquals("Received Text", capturedClipData.description.label.toString())
-// assertEquals(1, capturedClipData.itemCount)
-// assertEquals(testClipText, capturedClipData.getItemAt(0).text.toString())
-// }
-//
-// @Test
-// fun `onReceive with ACTION_DISMISS only cancels notification 1001`() {
-// // Arrange
-// val intent = Intent().apply {
-// action = "ACTION_DISMISS"
-// }
-//
-// // Act
-// notificationReceiver.onReceive(context, intent)
-//
-// // Assert
-// // Verify only notification 1001 was cancelled, not any other ID
-// verify(mockNotificationManager, times(1)).cancel(1001)
-// verify(mockNotificationManager, never()).cancel(0)
-// verify(mockNotificationManager, never()).cancel(999)
-// }
-//}
\ No newline at end of file
+
+import android.app.NotificationManager
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.test.core.app.ApplicationProvider
+import com.aubynsamuel.clipsync.bluetooth.BluetoothService
+import com.aubynsamuel.clipsync.notification.NotificationReceiver
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowApplication
+import org.robolectric.shadows.ShadowToast
+
+@RunWith(RobolectricTestRunner::class)
+@Config(sdk = [Build.VERSION_CODES.S]) // Testing with Android 12 (API 31)
+class NotificationReceiverTest {
+
+ private lateinit var notificationReceiver: NotificationReceiver
+ private lateinit var context: Context
+ private lateinit var shadowApplication: ShadowApplication
+
+ @Mock
+ private lateinit var mockNotificationManager: NotificationManager
+
+ @Mock
+ private lateinit var mockClipboardManager: ClipboardManager
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+
+ notificationReceiver = NotificationReceiver()
+ context = ApplicationProvider.getApplicationContext()
+ shadowApplication = ShadowApplication()
+
+ // Mock system services
+ shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mockNotificationManager)
+ shadowApplication.setSystemService(Context.CLIPBOARD_SERVICE, mockClipboardManager)
+ }
+
+ @Test
+ fun `onReceive with ACTION_DISMISS stops service and cancels notification`() {
+ // Arrange
+ val intent = Intent().apply {
+ action = "ACTION_DISMISS"
+ }
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ // Verify service was stopped
+ val nextStoppedService = shadowApplication.nextStoppedService
+ assertEquals(BluetoothService::class.java.name, nextStoppedService.component?.className)
+
+ // Verify notification was cancelled
+ verify(mockNotificationManager).cancel(1001)
+ }
+
+ @Test
+ fun `onReceive with ACTION_COPY copies text to clipboard and shows toast on older Android`() {
+ // Arrange
+ val testClipText = "Test clipboard text"
+ val testNotificationId = 12345
+ val intent = Intent().apply {
+ action = "ACTION_COPY"
+ putExtra("CLIP_TEXT", testClipText)
+ putExtra("NOTIFICATION_ID", testNotificationId)
+ }
+
+ val clipDataCaptor = ArgumentCaptor.forClass(ClipData::class.java)
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ // Verify clipboard was set
+ verify(mockClipboardManager).setPrimaryClip(clipDataCaptor.capture())
+
+ // Verify the ClipData contains correct text
+ val capturedClipData = clipDataCaptor.value
+ assertEquals(testClipText, capturedClipData.getItemAt(0).text.toString())
+ assertEquals("Received Text", capturedClipData.description.label.toString())
+
+ // Verify toast was shown (on Android < 13) using Robolectric's ShadowToast
+ val latestToast = ShadowToast.getLatestToast()
+ assertEquals("Copied to clipboard", ShadowToast.getTextOfLatestToast())
+
+ // Verify notification was cancelled with correct ID
+ verify(mockNotificationManager).cancel(testNotificationId)
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) // Android 13 (API 33)
+ fun `onReceive with ACTION_COPY does not show toast on Android 13+`() {
+ // Arrange
+ val testClipText = "Test clipboard text"
+ val testNotificationId = 12345
+ val intent = Intent().apply {
+ action = "ACTION_COPY"
+ putExtra("CLIP_TEXT", testClipText)
+ putExtra("NOTIFICATION_ID", testNotificationId)
+ }
+
+ // Clear any previous toasts
+ ShadowToast.reset()
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ // Verify clipboard was still set
+ verify(mockClipboardManager).setPrimaryClip(any())
+
+ // Verify toast was NOT shown (on Android 13+)
+ assertNull("No toast should be shown on Android 13+", ShadowToast.getLatestToast())
+
+ // Verify notification was still cancelled
+ verify(mockNotificationManager).cancel(testNotificationId)
+ }
+
+ @Test
+ fun `onReceive with ACTION_COPY and null CLIP_TEXT returns early`() {
+ // Arrange
+ val intent = Intent().apply {
+ action = "ACTION_COPY"
+ // No CLIP_TEXT extra
+ }
+
+ // Clear any previous toasts
+ ShadowToast.reset()
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ // Verify nothing was called since we returned early
+ verify(mockClipboardManager, never()).setPrimaryClip(any())
+ assertNull("No toast should be shown when CLIP_TEXT is null", ShadowToast.getLatestToast())
+ verify(mockNotificationManager, never()).cancel(any())
+ }
+
+ @Test
+ fun `onReceive with ACTION_COPY and empty CLIP_TEXT still processes`() {
+ // Arrange
+ val testClipText = ""
+ val testNotificationId = 12345
+ val intent = Intent().apply {
+ action = "ACTION_COPY"
+ putExtra("CLIP_TEXT", testClipText)
+ putExtra("NOTIFICATION_ID", testNotificationId)
+ }
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ // Verify clipboard was set with empty text
+ verify(mockClipboardManager).setPrimaryClip(any())
+ assertEquals("Copied to clipboard", ShadowToast.getTextOfLatestToast())
+ verify(mockNotificationManager).cancel(testNotificationId)
+ }
+
+ @Test
+ fun `onReceive with ACTION_COPY and no NOTIFICATION_ID does not cancel notification`() {
+ // Arrange
+ val testClipText = "Test text"
+ val intent = Intent().apply {
+ action = "ACTION_COPY"
+ putExtra("CLIP_TEXT", testClipText)
+ // No NOTIFICATION_ID extra (defaults to 0)
+ }
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ // Verify clipboard operations still happened
+ verify(mockClipboardManager).setPrimaryClip(any())
+ assertEquals("Copied to clipboard", ShadowToast.getTextOfLatestToast())
+
+ // Verify notification was NOT cancelled since ID was 0
+ verify(mockNotificationManager, never()).cancel(any())
+ }
+
+ @Test
+ fun `onReceive with unknown action does nothing`() {
+ // Arrange
+ val intent = Intent().apply {
+ action = "UNKNOWN_ACTION"
+ }
+
+ // Clear any previous toasts
+ ShadowToast.reset()
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ // Verify no interactions occurred
+ verify(mockNotificationManager, never()).cancel(any())
+ verify(mockClipboardManager, never()).setPrimaryClip(any())
+ assertNull("No toast should be shown for unknown action", ShadowToast.getLatestToast())
+
+ // Verify no services were stopped
+ val nextStoppedService = shadowApplication.nextStoppedService
+ assertNull(nextStoppedService)
+ }
+
+ @Test
+ fun `onReceive with null action does nothing`() {
+ // Arrange
+ val intent = Intent() // No action set
+
+ // Clear any previous toasts
+ ShadowToast.reset()
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ // Verify no interactions occurred
+ verify(mockNotificationManager, never()).cancel(any())
+ verify(mockClipboardManager, never()).setPrimaryClip(any())
+ assertNull("No toast should be shown for null action", ShadowToast.getLatestToast())
+ }
+
+ @Test
+ fun `onReceive with ACTION_COPY verifies ClipData creation parameters`() {
+ // Arrange
+ val testClipText = "Sample clipboard content"
+ val intent = Intent().apply {
+ action = "ACTION_COPY"
+ putExtra("CLIP_TEXT", testClipText)
+ putExtra("NOTIFICATION_ID", 999)
+ }
+
+ val clipDataCaptor = ArgumentCaptor.forClass(ClipData::class.java)
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ verify(mockClipboardManager).setPrimaryClip(clipDataCaptor.capture())
+
+ val capturedClipData = clipDataCaptor.value
+ // Verify ClipData properties
+ assertEquals("Received Text", capturedClipData.description.label.toString())
+ assertEquals(1, capturedClipData.itemCount)
+ assertEquals(testClipText, capturedClipData.getItemAt(0).text.toString())
+ }
+
+ @Test
+ fun `onReceive with ACTION_DISMISS only cancels notification 1001`() {
+ // Arrange
+ val intent = Intent().apply {
+ action = "ACTION_DISMISS"
+ }
+
+ // Act
+ notificationReceiver.onReceive(context, intent)
+
+ // Assert
+ // Verify only notification 1001 was cancelled, not any other ID
+ verify(mockNotificationManager, times(1)).cancel(1001)
+ verify(mockNotificationManager, never()).cancel(0)
+ verify(mockNotificationManager, never()).cancel(999)
+ }
+}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"