Skip to content

Commit 4389751

Browse files
author
Vladimir Vilimaitis
committed
-Added a file crop on picking an image
-Added a project description -Added a readme and a license -Added an icon
1 parent f8e8fcc commit 4389751

35 files changed

Lines changed: 387 additions & 56 deletions

.idea/markdown.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

LICENSE

Lines changed: 200 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Fake Call App
2+
3+
| **Settings Screen** | **Incoming Call** | **Accepted Call** |
4+
|:-------------------------------------:|:-----------------------------------------------:|-------------------------------------------------|
5+
| ![Settings](screenshots/settings.png) | ![Incoming Call](screenshots/incoming_call.png) | ![Accepted Call](screenshots/accepted_call.png) |
6+
7+
An open-source call simulation app to escape awkward situations or play pranks.
8+
9+
Not only I couldn't find an open-source fake caller on the F-Droid or Google Play stores, I couldn't
10+
even find one that looked reasonably modern and not too obvious, worked with the screen off and wasn't run by an obvious ad farm,
11+
so I decided to vibe code my own. This app will never have any ads or collect any data, forever.
12+
13+
Tested on Google Pixel 9 Pro XL. Contributions are welcome.

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ dependencies {
4242
testImplementation(libs.junit)
4343
androidTestImplementation(libs.androidx.junit)
4444
androidTestImplementation(libs.androidx.espresso.core)
45+
implementation(libs.image.cropper)
4546
}

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3-
xmlns:tools="http://schemas.android.com/tools">
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
43

54
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
65
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
@@ -12,6 +11,7 @@
1211
<application
1312
android:allowBackup="true"
1413
android:icon="@mipmap/ic_launcher"
14+
android:roundIcon="@mipmap/ic_launcher_round"
1515
android:label="@string/app_name"
1616
android:theme="@style/Theme.FakeCallApp">
1717

@@ -33,6 +33,8 @@
3333

3434
<receiver android:name=".CallReceiver" />
3535

36+
<activity android:name=".CropActivity" android:screenOrientation="portrait"/>
37+
3638
</application>
3739

3840
</manifest>
329 KB
Loading
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.example.fakecallapp
2+
3+
import android.app.Activity
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import android.widget.Button
7+
import androidx.appcompat.app.AppCompatActivity
8+
import com.canhub.cropper.CropImageView
9+
10+
class CropActivity : AppCompatActivity() {
11+
12+
override fun onCreate(savedInstanceState: Bundle?) {
13+
super.onCreate(savedInstanceState)
14+
setContentView(R.layout.activity_crop)
15+
16+
val cropImageView = findViewById<CropImageView>(R.id.cropImageView)
17+
val btnSave = findViewById<Button>(R.id.btnSave)
18+
val btnCancel = findViewById<Button>(R.id.btnCancel)
19+
20+
// 1. Load the image passed from MainActivity
21+
val sourceUri = intent.data
22+
if (sourceUri != null) {
23+
cropImageView.setImageUriAsync(sourceUri)
24+
}
25+
26+
// 2. Button Logic
27+
btnCancel.setOnClickListener { finish() }
28+
29+
btnSave.setOnClickListener {
30+
// Trigger the crop
31+
cropImageView.croppedImageAsync()
32+
}
33+
34+
// 3. Listen for Crop Result
35+
cropImageView.setOnCropImageCompleteListener { _, result ->
36+
if (result.isSuccessful) {
37+
// Send the cropped image back to Main
38+
val resultIntent = Intent()
39+
resultIntent.data = result.uriContent
40+
setResult(Activity.RESULT_OK, resultIntent)
41+
finish()
42+
} else {
43+
// Handle error if needed
44+
}
45+
}
46+
}
47+
}

app/src/main/java/com/example/fakecallapp/MainActivity.kt

Lines changed: 49 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,115 +27,120 @@ import java.io.FileOutputStream
2727

2828
class MainActivity : AppCompatActivity() {
2929

30-
// --- 1. CONTACT PICKER LAUNCHER ---
30+
// --- 1. CROPPER LAUNCHER ---
31+
// Receives the FINAL cropped image from your custom CropActivity
32+
private val cropActivityLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
33+
if (result.resultCode == RESULT_OK) {
34+
val croppedUri = result.data?.data
35+
if (croppedUri != null) {
36+
// Show it
37+
findViewById<android.widget.ImageView>(R.id.ivPreview).setImageURI(croppedUri)
38+
// Save it locally
39+
saveImageToInternalStorage(croppedUri)
40+
}
41+
}
42+
}
43+
44+
// --- 2. CLASSIC GALLERY LAUNCHER (Re-implemented) ---
45+
// Receives the raw image from the Gallery, then immediately sends it to CropActivity
46+
private val pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
47+
if (result.resultCode == RESULT_OK) {
48+
val originalUri = result.data?.data
49+
if (originalUri != null) {
50+
// Immediately launch CropActivity with this image
51+
val intent = Intent(this, CropActivity::class.java)
52+
intent.data = originalUri
53+
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
54+
cropActivityLauncher.launch(intent)
55+
}
56+
}
57+
}
58+
59+
// --- 3. CONTACT PICKER LAUNCHER ---
3160
private val pickContactLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
3261
if (result.resultCode == RESULT_OK) {
3362
val contactUri = result.data?.data ?: return@registerForActivityResult
3463
populateContactDetails(contactUri)
3564
}
3665
}
3766

38-
// --- 2. PERMISSION LAUNCHER ---
3967
private val requestContactPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
4068
if (isGranted) {
4169
launchContactPicker()
4270
} else {
43-
Toast.makeText(this, "Permission denied. Cannot pick contact.", Toast.LENGTH_SHORT).show()
44-
}
45-
}
46-
47-
private val pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
48-
if (result.resultCode == RESULT_OK) {
49-
val uri = result.data?.data
50-
if (uri != null) {
51-
findViewById<android.widget.ImageView>(R.id.ivPreview).setImageURI(uri)
52-
saveImageToInternalStorage(uri)
53-
}
71+
Toast.makeText(this, "Permission denied.", Toast.LENGTH_SHORT).show()
5472
}
5573
}
5674

5775
override fun onCreate(savedInstanceState: Bundle?) {
5876
super.onCreate(savedInstanceState)
5977
setContentView(R.layout.activity_main)
6078

61-
checkPermissions() // Standard permissions (Notif, Overlay, Alarm)
79+
checkPermissions()
6280

63-
// Setup UI
6481
val ivPreview = findViewById<android.widget.ImageView>(R.id.ivPreview)
65-
val btnPickContact = findViewById<ImageButton>(R.id.btnPickContact) // <--- NEW BUTTON
82+
val btnPickContact = findViewById<ImageButton>(R.id.btnPickContact)
6683
val btnSchedule = findViewById<Button>(R.id.btnSchedule)
6784
val npDelay = findViewById<NumberPicker>(R.id.npDelay)
6885

69-
// Setup Spinner
7086
npDelay.minValue = 0
7187
npDelay.maxValue = 120
7288
npDelay.value = 10
7389
npDelay.wrapSelectorWheel = false
7490

75-
// 1. IMAGE CLICK (Manual Photo)
91+
// --- 4. CLICK LISTENER: Use Standard Gallery Intent ---
7692
ivPreview.setOnClickListener {
93+
// ACTION_PICK is the "Classic" single-select intent you wanted
7794
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
7895
pickImageLauncher.launch(intent)
7996
}
8097

81-
// 2. CONTACT BUTTON CLICK
8298
btnPickContact.setOnClickListener {
83-
// Check if we have permission. If not, ask. If yes, open picker.
8499
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
85100
launchContactPicker()
86101
} else {
87102
requestContactPermissionLauncher.launch(android.Manifest.permission.READ_CONTACTS)
88103
}
89104
}
90105

91-
// 3. SCHEDULE CLICK
92106
btnSchedule.setOnClickListener {
93107
val name = findViewById<EditText>(R.id.etCallerName).text.toString()
94108
val phone = findViewById<EditText>(R.id.etPhoneNumber).text.toString()
95109
val delaySeconds = npDelay.value.toLong()
96110
scheduleCall(name, phone, delaySeconds)
97111
}
98112

99-
// Cleanup old image on start
113+
// Reset image on fresh launch
100114
val file = File(filesDir, "custom_avatar.jpg")
101115
if (file.exists()) file.delete()
102116
}
103117

104118
private fun launchContactPicker() {
105-
// We strictly want phones, so we pick from CommonDataKinds.Phone
106119
val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
107120
pickContactLauncher.launch(intent)
108121
}
109122

110123
private fun populateContactDetails(uri: Uri) {
111-
// Query the content provider for the specific contact clicked
112124
val cursor: Cursor? = contentResolver.query(uri, null, null, null, null)
113-
114125
cursor?.use {
115126
if (it.moveToFirst()) {
116-
// 1. Get Name
117127
val nameIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
118128
val name = if (nameIndex >= 0) it.getString(nameIndex) else "Unknown"
119-
120-
// 2. Get Number
121129
val numberIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
122130
val number = if (numberIndex >= 0) it.getString(numberIndex) else ""
123-
124-
// 3. Get Photo URI (Thumbnail or High Res)
125131
val photoUriIndex = it.getColumnIndex(ContactsContract.CommonDataKinds.Phone.PHOTO_URI)
126132
val photoUriString = if (photoUriIndex >= 0) it.getString(photoUriIndex) else null
127133

128-
// --- UPDATE UI ---
129134
findViewById<EditText>(R.id.etCallerName).setText(name)
130135
findViewById<EditText>(R.id.etPhoneNumber).setText(number)
131136

132-
// --- HANDLE PHOTO ---
133137
if (photoUriString != null) {
134138
val photoUri = photoUriString.toUri()
139+
// If from contacts, also let them crop it if they want, or just save it.
140+
// For now, let's just save it directly to keep it simple.
135141
findViewById<android.widget.ImageView>(R.id.ivPreview).setImageURI(photoUri)
136-
saveImageToInternalStorage(photoUri) // Save it for the fake call later
142+
saveImageToInternalStorage(photoUri)
137143
} else {
138-
// Reset to default if contact has no photo
139144
findViewById<android.widget.ImageView>(R.id.ivPreview).setImageResource(R.drawable.ic_avatar_default)
140145
val file = File(filesDir, "custom_avatar.jpg")
141146
if (file.exists()) file.delete()
@@ -158,7 +163,6 @@ class MainActivity : AppCompatActivity() {
158163
}
159164

160165
private fun checkPermissions() {
161-
// ... (Keep your existing checks for Notif, Overlay, Alarm) ...
162166
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
163167
ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.POST_NOTIFICATIONS), 101)
164168
}
@@ -174,7 +178,6 @@ class MainActivity : AppCompatActivity() {
174178
}
175179

176180
private fun scheduleCall(name: String, phone: String, delaySeconds: Long) {
177-
// ... (Keep your existing schedule logic exactly as it was) ...
178181
if (delaySeconds == 0L) {
179182
val intent = Intent(this, FakeCallActivity::class.java).apply {
180183
putExtra("caller_name", name)
@@ -195,10 +198,16 @@ class MainActivity : AppCompatActivity() {
195198
putExtra("caller_name", name)
196199
putExtra("caller_phone", phone)
197200
}
198-
val pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
199-
200-
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (delaySeconds * 1000), pendingIntent)
201-
Toast.makeText(this, "Call scheduled in $delaySeconds second${if (delaySeconds != 1L) "s" else ""}.", Toast.LENGTH_LONG).show()
201+
val pendingIntent = PendingIntent.getBroadcast(
202+
this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
203+
)
204+
205+
alarmManager.setExactAndAllowWhileIdle(
206+
AlarmManager.RTC_WAKEUP,
207+
System.currentTimeMillis() + (delaySeconds * 1000),
208+
pendingIntent
209+
)
210+
Toast.makeText(this, "Call scheduled in $delaySeconds second${if (delaySeconds != 1L) "s" else ""}", Toast.LENGTH_SHORT).show()
202211

203212
}
204213
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:app="http://schemas.android.com/apk/res-auto"
4+
android:layout_width="match_parent"
5+
android:layout_height="match_parent"
6+
android:background="#000000">
7+
8+
<com.canhub.cropper.CropImageView
9+
android:id="@+id/cropImageView"
10+
android:layout_width="match_parent"
11+
android:layout_height="match_parent"
12+
android:layout_above="@id/bottomBar"
13+
app:cropAspectRatioX="1"
14+
app:cropAspectRatioY="1"
15+
app:cropFixAspectRatio="true"
16+
app:cropGuidelines="on"/>
17+
<LinearLayout
18+
android:id="@+id/bottomBar"
19+
android:layout_width="match_parent"
20+
android:layout_height="wrap_content"
21+
android:orientation="horizontal"
22+
android:layout_alignParentBottom="true"
23+
android:padding="20dp"
24+
android:gravity="center">
25+
26+
<Button
27+
android:id="@+id/btnCancel"
28+
android:layout_width="0dp"
29+
android:layout_height="wrap_content"
30+
android:layout_weight="1"
31+
android:text="Cancel"
32+
android:backgroundTint="#FF453A"
33+
android:textColor="#FFFFFF"
34+
android:layout_marginEnd="10dp"/>
35+
36+
<Button
37+
android:id="@+id/btnSave"
38+
android:layout_width="0dp"
39+
android:layout_height="wrap_content"
40+
android:layout_weight="1"
41+
android:text="Save Photo"
42+
android:backgroundTint="#30D158"
43+
android:textColor="#FFFFFF"
44+
android:layout_marginStart="10dp"/>
45+
</LinearLayout>
46+
47+
</RelativeLayout>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3+
<background android:drawable="@color/ic_launcher_background"/>
4+
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
5+
</adaptive-icon>

0 commit comments

Comments
 (0)