```python
from typing import Dict, Any, Callable, Optional

class JobQueue:
    """
    A queue implementation that supports job processing with retry logic
    and exponential backoff.
    """
    MAX_RETRIES = 3

    def __init__(self):
        # Stores job data: {job_id: job_data}
        self.jobs: Dict[str, Dict[str, Any]] = {}
        # Tracks job state: {job_id: {'retries': int, 'status': str}}
        self.job_states: Dict[str, Dict[str, Any]] = {}

    def add_job(self, job_id: str, job_data: Dict[str, Any]):
        """Adds a new job to the queue."""
        if job_id in self.jobs:
            raise ValueError(f"Job ID '{job_id}' already exists.")
        self.jobs[job_id] = job_data
        self.job_states[job_id] = {'retries': 0, 'status': 'pending'}

    def _calculate_backoff_delay(self, attempt: int) -> int:
        """
        Calculates the exponential backoff delay for the given attempt.
        Attempt 1 (first retry) -> 1s
        Attempt 2 (second retry) -> 2s
        Attempt 3 (third retry) -> 4s
        """
        # The delay is calculated based on the number of *previous* failures (attempt - 1)
        # Since we start counting retries from 1, the exponent is (attempt - 1)
        return 2 ** (attempt - 1)

    def process_job(self, job_id: str, processor: Callable[[Dict[str, Any]], Any]) -> bool:
        """
        Processes a job, implementing retry logic and exponential backoff.

        Args:
            job_id: The ID of the job to process.
            processor: The function that attempts to execute the job.

        Returns:
            True if the job succeeded, False if all retries were exhausted.
        """
        if job_id not in self.jobs:
            raise ValueError(f"Job ID '{job_id}' not found.")

        job_data = self.jobs[job_id]
        state = self.job_states[job_id]
        
        success = False
        
        # Loop through the maximum number of attempts (1 initial + MAX_RETRIES - 1 retries)
        for attempt in range(1, self.MAX_RETRIES + 1):
            try:
                # Attempt to run the processor
                processor(job_data)
                
                # Success
                state['status'] = 'completed'
                success = True
                break
                
            except Exception as e:
                # Failure occurred
                print(f"Job {job_id} failed on attempt {attempt}: {e}")
                
                # Check if we have remaining retries
                if attempt >= self.MAX_RETRIES:
                    state['status'] = 'failed'
                    print(f"Job {job_id} failed permanently after {self.MAX_RETRIES} attempts.")
                    return False

                # Log failure and determine backoff delay
                state['retries'] += 1
                
                # Calculate the delay for the *next* retry
                delay = self._calculate_backoff_delay(attempt)
                
                print(f"Job {job_id} scheduling retry {state['retries']} in {delay}s...")
                # In a real system, we would sleep(delay) here. 
                # We skip the actual sleep as per requirements.

        return success

# --- Example Usage ---

# 1. Define a mock processor that fails a specified number of times
class MockProcessor:
    def __init__(self, fail_count: int):
        self.fail_count = fail_count
        self.attempts = 0

    def __call__(self, data: Dict[str, Any]):
        self.attempts += 1
        print(f"--- Processing attempt {self.attempts} for {data['id']} ---")
        if self.attempts <= self.fail_count:
            raise ConnectionError(f"Simulated network failure on attempt {self.attempts}")
        print("--- Processing successful! ---")
        return True

# --- Test Case 1: Success after retries (Fails 2 times, succeeds on 3rd) ---
print("=================================================")
print("Test Case 1: Successful Retry")
queue1 = JobQueue()
processor1 = MockProcessor(fail_count=2) 
queue1.add_job("job_success", {"id": "job_success", "url": "http://example.com"})

success1 = queue1.process_job("job_success", processor1)
print(f"\nJob 'job_success' final status: {'SUCCESS' if success1 else 'FAILURE'}")
print("=================================================\n")


# --- Test Case 2: Permanent Failure (Fails 4 times, max retries = 3) ---
print("=================================================")
print("Test Case 2: Permanent Failure")
queue2 = JobQueue()
processor2 = MockProcessor(fail_count=4) 
queue2.add_job("job_failure", {"id": "job_failure", "url": "http://example.com"})

success2 = queue2.process_job("job_failure", processor2)
print(f"\nJob 'job_failure' final status: {'SUCCESS' if success2 else 'FAILURE'}")
print("=================================================\n")


# --- Test Case 3: Immediate Success (Succeeds on 1st attempt) ---
print("=================================================")
print("Test Case 3: Immediate Success")
queue3 = JobQueue()
processor3 = MockProcessor(fail_count=0) 
queue3.add_job("job_immediate", {"id": "job_immediate", "url": "http://example.com"})

success3 = queue3.process_job("job_immediate", processor3)
print(f"\nJob 'job_immediate' final status: {'SUCCESS' if success3 else 'FAILURE'}")
print("=================================================")
```