כל AP שנקלט → מסומן בנקודה על המפה.
נקודה שמראה איפה אתה נמצא ברגע זה.
זהו.
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE))
מעולה אבי — מבצע!
אני מכין לך פרויקט אנדרואיד מלא עם “מפה חיה” (ללא שרת), שמבצע:
- סריקת Wi‑Fi + GPS בזמן אמת
- ציור המיקום שלך + נקודות ה־AP שנקלטו על גבי Google Maps
- שכבת Heatmap לפי עוצמות RSSI (אופציונלי במתג)
- שמירת הנתונים לקובץ JSON במכשיר (לייצוא/ניתוח)
- הכל חינמי ומבוסס Android Studio
כדי שלא נעצור אותך — בחרתי ברירות מחדל:
B) קוד מלא ל‑Android Studio, ו‑D) הכל ביחד (APs + מסלול + Heatmap + יצוא).
תרצה APK חתום מוכן להתקנה? אגיד לך איך לבנות בסוף, או שאכין לך לפי בקשה.
🗺️ שרטוט (ארכיטקטורה)
[טלפון אנדרואיד]
├─ Wifi Scanner (WifiManager) ─┐
├─ GPS (FusedLocation) ├─→ DataStore (JSON מקומי)
└─ Live Map (Google Maps) ←──┘ └─ Export (wifi_map.json)
UI:
[התחל] [עצור] [Heatmap] [ייצוא] [מרכז עלי]
1) מה מתקינים (חינם)
במחשב
- Android Studio – חינמי.
- מיועד לכתיבת קוד, Build והרצה לטלפון.
- (אופציונלי) Python – רק אם תרצה לנתח קבצים במחשב (לא חובה לניסוי “מפה חיה”).
בטלפון (Android)
- לא מתקינים כלום.
- מפעילים Developer options + USB debugging (כמו שהסברתי לך קודם).
2) יצירת פרויקט חדש (Android Studio)
- פתח Android Studio → New Project → Empty Activity → Finish.
- סגור את האפליקציה שרצה ברקע אם נפתחה — נתחיל להחליף קבצים.
3) קבצי הפרויקט (העתק/הדבק)
להלן כל הקבצים שתצטרך. שמור על שמות החבילות כפי שמופיע בקוד (com.example.wifilivemap) או שנה לפי רצונך בעקביות בכל הקבצים.
3.1 settings.gradle (ברמת ה‑Root)
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "WifiLiveMap"
include(":app")
3.2 build.gradle (ברמת ה‑Root)
// יכול להישאר ריק בפרויקטים חדשים; נשאר בסיסי לשקט נפשי
``
3.3 app/build.gradle
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
namespace 'com.example.wifilivemap'
compileSdk 34
defaultConfig {
applicationId 'com.example.wifilivemap'
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// מפות גוגל
implementation 'com.google.android.gms:play-services-maps:18.2.0'
// מיקום (FusedLocationProvider)
implementation 'com.google.android.gms:play-services-location:21.0.1'
// Heatmap + Utilities
implementation 'com.google.maps.android:android-maps-utils:3.8.2'
// קורוטינות (לא חובה, אבל נוח)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
}
אם Android Studio יבקש לעדכן גרסאות – אשר. כל התלויות חינמיות.
3.4 app/src/main/AndroidManifest.xml
שים לב: צריך מפתח API של Google Maps — הסבר כיצד להשיג בהמשך.
package="com.example.wifilivemap">
<!-- הרשאות מיקום ו-WiFi -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Android 13+ -->
<uses-permission
android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation" />
<application
android:allowBackup="true"
android:label="Wifi Live Map"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Google Maps API Key -->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_GOOGLE_MAPS_API_KEY_HERE" />
<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
טיפ: ודא שה־Location (GPS) פעיל במכשיר בזמן הריצה, אחרת סריקת Wi‑Fi תוחזר ריקה בגרסאות חדשות.
3.5 app/src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- מפה -->
<fragment
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!-- לוח בקרה שקוף מעל המפה -->
<LinearLayout
android:orientation="vertical"
android:padding="12dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#55FFFFFF"
android:layout_margin="12dp">
<Button
android:id="@+id/btnStart"
android:text="התחל סריקה"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btnStop"
android:text="עצור סריקה"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"/>
<Button
android:id="@+id/btnMyLocation"
android:text="מרכז עליי"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"/>
<Button
android:id="@+id/btnHeatmap"
android:text="הפעל/כבה Heatmap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"/>
<Button
android:id="@+id/btnExport"
android:text="ייצוא JSON"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"/>
</LinearLayout>
</FrameLayout>
3.6 app/src/main/java/com/example/wifilivemap/MainActivity.kt
package com.example.wifilivemap
import android.Manifest
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.wifi.WifiManager
import android.os.
import android.widget.Button
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.google.android.gms.location.
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.*
import com.google.maps.android.heatmaps.HeatmapTileProvider
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import kotlin.math.exp
data class ApRecord(
val bssid: String,
val ssid: String?,
val rssi: Int,
val lat: Double,
val lng: Double,
val timeMs: Long
)
class MainActivity : AppCompatActivity(), OnMapReadyCallback {
private lateinit var map: GoogleMap
private lateinit var wifiManager: WifiManager
private lateinit var fused: FusedLocationProviderClient
private var userMarker: Marker? = null
private var scanning = false
private val handler = Handler(Looper.getMainLooper())
// נתונים
private val apRecords = mutableListOf<ApRecord>()
private val apLatestByBssid = linkedMapOf<String, ApRecord>() // מצטבר לפי AP
private var heatmapProvider: HeatmapTileProvider? = null
private var heatmapOverlay: TileOverlay? = null
// הרשאות רלוונטיות
private val requiredPermissions: Array<String> by lazy {
val base = mutableListOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
base += Manifest.permission.NEARBY_WIFI_DEVICES
}
base.toTypedArray()
}
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { granted ->
if (granted.values.all { it }) {
initAfterPermissions()
} else {
Toast.makeText(this, "יש לאשר הרשאות מיקום/ Wi‑Fi", Toast.LENGTH_LONG).show()
}
}
private val scanReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// כאשר סריקה הסתיימה – נאסוף תוצאות ונצייר
collectScanAndUpdate()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
fused = LocationServices.getFusedLocationProviderClient(this)
// מפה
val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
mapFragment.getMapAsync(this)
findViewById<Button>(R.id.btnStart).setOnClickListener { startScanning() }
findViewById<Button>(R.id.btnStop).setOnClickListener { stopScanning() }
findViewById<Button>(R.id.btnMyLocation).setOnClickListener { centerOnMe() }
findViewById<Button>(R.id.btnHeatmap).setOnClickListener { toggleHeatmap() }
findViewById<Button>(R.id.btnExport).setOnClickListener { exportJson() }
// רישום Receiver לסריקה
registerReceiver(
scanReceiver,
IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
)
// בקשת הרשאות
requestNeededPermissions()
}
override fun onDestroy() {
super.onDestroy()
stopScanning()
unregisterReceiver(scanReceiver)
}
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
map.uiSettings.isZoomControlsEnabled = true
// מיקום פתיחה (תל-אביב כנקודת ברירת מחדל)
map.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(32.0853, 34.7818), 13f))
// אם כבר יש הרשאות – נאתחל
if (hasAllPermissions()) initAfterPermissions()
}
// === הרשאות ===
private fun hasAllPermissions(): Boolean =
requiredPermissions.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
private fun requestNeededPermissions() {
if (!hasAllPermissions()) {
permissionLauncher.launch(requiredPermissions)
} else {
initAfterPermissions()
}
}
private fun initAfterPermissions() {
// אפשר להציג שכבת "המיקום שלי" של גוגל אם תרצה
try {
map.isMyLocationEnabled = hasAllPermissions()
} catch (: SecurityException) {
}
// התחלת עדכוני מיקום רציפים
startLocationUpdates()
}
// === GPS רציף (FusedLocationProvider) ===
private fun startLocationUpdates() {
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 3000L)
.setMinUpdateIntervalMillis(1500L)
.setWaitForAccurateLocation(true)
.build()
try {
fused.requestLocationUpdates(
request,
object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
val loc = result.lastLocation ?: return
updateUserMarker(loc.latitude, loc.longitude)
}
},
Looper.getMainLooper()
)
} catch (: SecurityException) {
}
}
private fun updateUserMarker(lat: Double, lng: Double) {
val pos = LatLng(lat, lng)
if (userMarker == null) {
userMarker = map.addMarker(
MarkerOptions()
.position(pos)
.title("אתה פה")
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))
)
map.animateCamera(CameraUpdateFactory.newLatLngZoom(pos, 18f))
} else {
userMarker!!.position = pos
}
}
private fun centerOnMe() {
userMarker?.let {
map.animateCamera(CameraUpdateFactory.newLatLngZoom(it.position, 18f))
}
}
// === סריקות Wi‑Fi מחזוריות ===
private fun startScanning() {
if (scanning) return
scanning = true
Toast.makeText(this, "סריקה התחילה (ייתכן throttle של המערכת)", Toast.LENGTH_SHORT).show()
// לולאת סריקה כל 5 שניות (אנדרואיד עשוי לדלל את התדירות)
handler.post(object : Runnable {
override fun run() {
if (!scanning) return
val ok = wifiManager.startScan()
if (!ok) {
// אם המערכת לא מאפשרת כרגע – ננסה לקרוא תוצאות אחרונות
collectScanAndUpdate()
}
handler.postDelayed(this, 5000L)
}
})
}
private fun stopScanning() {
scanning = false
Toast.makeText(this, "סריקה נעצרה", Toast.LENGTH_SHORT).show()
}
private fun collectScanAndUpdate() {
val results = wifiManager.scanResults
if (results.isNullOrEmpty()) return
// ננסה לקבל נ״צ עדכני
try {
fused.lastLocation.addOnSuccessListener { loc ->
val lat = loc?.latitude
val lng = loc?.longitude
for (ap in results) {
val rec = ApRecord(
bssid = ap.BSSID.lowercase(),
ssid = ap.SSID,
rssi = ap.level,
lat = lat ?: Double.NaN,
lng = lng ?: Double.NaN,
timeMs = System.currentTimeMillis()
)
apRecords += rec
// נשמור מיקום אחרון ל‑BSSID; אם יש LatLng, נעדיף ממוצע פשוט
val prev = apLatestByBssid[rec.bssid]
apLatestByBssid[rec.bssid] =
if (prev == null || prev.lat.isNaN() || prev.lng.isNaN()) rec
else {
// ממוצע רך כדי לייצב
val mixLat = (prev.lat + (lat ?: prev.lat)) / 2.0
val mixLng = (prev.lng + (lng ?: prev.lng)) / 2.0
prev.copy(lat = mixLat, lng = mixLng, rssi = rec.rssi, timeMs = rec.timeMs, ssid = rec.ssid)
}
}
drawLayers()
}
} catch (: SecurityException) {
}
}
// === ציור שכבות ===
private fun drawLayers() {
map.clear()
// סמן המשתמש
userMarker?.let {
userMarker = map.addMarker(
MarkerOptions()
.position(it.position)
.title("אתה פה")
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))
)
}
// נקודות AP (האחרונות לכל BSSID)
for ((, ap) in apLatestByBssid) {
if (ap.lat.isNaN() || ap.lng.isNaN()) continue
val color = when {
ap.rssi >= -60 -> BitmapDescriptorFactory.HUE_GREEN
ap.rssi >= -75 -> BitmapDescriptorFactory.HUE_YELLOW
else -> BitmapDescriptorFactory.HUE_RED
}
map.addMarker(
MarkerOptions()
.position(LatLng(ap.lat, ap.lng))
.title(ap.ssid ?: ap.bssid)
.snippet("BSSID: ${ap.bssid} | RSSI: ${ap.rssi} dBm")
.icon(BitmapDescriptorFactory.defaultMarker(color))
)
}
// אם Heatmap פעיל — נצייר (שומר את מצב המתג)
if (heatmapOverlay != null) {
rebuildHeatmap()
}
}
private fun toggleHeatmap() {
if (heatmapOverlay == null) {
rebuildHeatmap()
Toast.makeText(this, "Heatmap הופעל", Toast.LENGTH_SHORT).show()
} else {
heatmapOverlay?.remove()
heatmapOverlay = null
heatmapProvider = null
Toast.makeText(this, "Heatmap כובה", Toast.LENGTH_SHORT).show()
}
}
private fun rebuildHeatmap() {
val points = mutableListOf<LatLng>()
val weights = mutableListOf<Double>()
for ((_, ap) in apLatestByBssid) {
if (ap.lat.isNaN() || ap.lng.isNaN()) continue
points += LatLng(ap.lat, ap.lng)
// המרה מרמת dBm (שלילי) למשקל חיובי ~[0..1]
val w = rssiToWeight(ap.rssi)
weights += w
}
if (points.isEmpty()) return
val provider = HeatmapTileProvider.Builder()
.weightedData(points.zip(weights) { latlng, w ->
com.google.maps.android.heatmaps.WeightedLatLng(latlng, w)
})
.radius(30) // אפשר לכוונן
.build()
heatmapProvider = provider
heatmapOverlay?.remove()
heatmapOverlay = map.addTileOverlay(TileOverlayOptions().tileProvider(provider))
}
private fun rssiToWeight(rssi: Int): Double {
// הֵוריסטיקה: ננרמל בין [-100..-40] → [0.01..1.0]
val clamped = rssi.coerceIn(-100, -40)
val norm = (clamped + 100) / 60.0 // 0..1
// הדגשה אקספוננציאלית קלה
return (0.01 + 0.99 * norm * norm)
}
// === ייצוא לקובץ JSON ===
private fun exportJson() {
val arr = JSONArray()
apRecords.forEach { ap ->
val o = JSONObject()
o.put("bssid", ap.bssid)
o.put("ssid", ap.ssid ?: "")
o.put("rssi", ap.rssi)
o.put("lat", ap.lat)
o.put("lng", ap.lng)
o.put("time", ap.timeMs)
arr.put(o)
}
val file = File(getExternalFilesDir(null), "wifi_map.json")
file.writeText(arr.toString(2))
Toast.makeText(this, "נשמר: ${file.absolutePath}", Toast.LENGTH_LONG).show()
}
}
4) איך משיגים מפתח Google Maps API (חינמי לשימוש פיתוח)
- היכנס ל‑Google Cloud Console בחשבון גוגל שלך.
- צור פרויקט חדש או בחר קיים.
- עבור ל‑APIs & Services → Library והפעל Maps SDK for Android.
- עבור ל‑APIs & Services → Credentials → Create credentials → API key.
- העתק את המפתח והדבק ב‑
AndroidManifest.xml במקום: YOUR_GOOGLE_MAPS_API_KEY_HERE. - (מומלץ) הגבלות מפתח: Application restrictions → Android apps והוסף
- Package name: לדוגמה
com.example.wifilivemap - SHA‑1 של ה‑debug keystore: ב‑Android Studio → Gradle (בצד) →
:app → Tasks → android → signingReport. העתק את SHA‑1.
לפיתוח – גם מפתח ללא הגבלות יעבוד, אך מומלץ להגדיר הגבלות לפי Package + SHA‑1.
5) הרצה על הטלפון (שלב‑שלב)
- הפעל Developer options ו‑USB debugging בטלפון.
- חבר כבל USB למחשב ואשר Allow USB debugging.
- פתח Android Studio: למעלה ליד ▶ יופיע שם המכשיר.
- לחץ ▶ Run.
- בפעם הראשונה האפליקציה תבקש הרשאות מיקום/Wi‑Fi — אשר.
- לחץ התחל סריקה → תראה נקודות AP מופיעות.
- לחץ Heatmap כדי להדליק/לכבות שכבת עוצמות.
- ייצוא JSON ישמור קובץ בנתיב:
Android/data/com.example.wifilivemap/files/wifi_map.json
6) בניית APK להתקנה בלי מחשב (אופציונלי)
- Android Studio → Build > Build APK(s) → ייווצר APK ב־
app/build/outputs/apk/… - העבר לטלפון והתקן (אפשר לאשר “Unknown sources” אם צריך).
7) הערות חשובות להצלחה
- מיקום (GPS) חייב להיות פעיל במכשיר, אחרת סריקות Wi‑Fi עשויות להחזיר ריק.
- אנדרואיד מגביל תדירות סריקות (throttle). לכן לולאת 5 שניות היא “בקשה” — המערכת עלולה לדלל בפועל.
- קח כמה נקודות בשטח — ההצטברות תוצג במפה, וה‑Heatmap יבליט אזורים חזקים.
- אם תרצה לשמור ב‑SQLite במקום JSON – אכין לך גרסה עם Room.
רוצה שאכין לך גם ZIP מוכן (מבנה פרויקט מלא) או APK חתום?
תגיד לי:
- להשאיר את שם החבילה כ‑
com.example.wifilivemap או לשנות לשם שלך? - האם להוסיף מסלול תנועה (Polyline) של המשתמש על המפה בזמן אמת?
- האם להוסיף טעינת JSON שנשמר בעת פתיחה מחדש, כדי לצייר את הנקודות הקודמות אוטומטית?
מוכן להתאים את זה עבורך מייד.
אין תגובות:
הוסף רשומת תגובה