סקירה כללית על Wi-Fi Aware

יכולות Wi-Fi Aware מאפשרות למכשירים עם Android מגרסה 8.0 (רמת API‏ 26) ומעלה לגלות מכשירים אחרים ולהתחבר אליהם ישירות, בלי שום סוג אחר של קישוריות ביניהם. ‫Wi-Fi Aware נקרא גם Neighbor Awareness Networking‏ (NAN).

כדי ליצור רשת Wi-Fi Aware, המכשיר יוצר אשכולות עם מכשירים סמוכים, או יוצר אשכול חדש אם הוא המכשיר הראשון באזור. התנהגות האשכולות הזו חלה על המכשיר כולו ומנוהלת על ידי שירות המערכת של Wi-Fi Aware. לאפליקציות אין שליטה על התנהגות האשכולות. האפליקציות משתמשות בממשקי ה-API של Wi-Fi Aware כדי לתקשר עם שירות המערכת של Wi-Fi Aware, שמנהל את החומרה של Wi-Fi Aware במכשיר.

ממשקי ה-API של Wi-Fi Aware מאפשרים לאפליקציות לבצע את הפעולות הבאות:

  • גילוי מכשירים אחרים: ל-API יש מנגנון למציאת מכשירים אחרים בקרבת מקום. התהליך מתחיל כשמכשיר אחד מפרסם שירות אחד או יותר שניתן לגלות. לאחר מכן, כשמכשיר נרשם לשירות אחד או יותר ונכנס לטווח ה-Wi-Fi של המפרסם, המנוי מקבל התראה על כך שזוהה מפרסם תואם. אחרי שהמנוי מגלה מפרסם, הוא יכול לשלוח הודעה קצרה או ליצור חיבור לרשת עם המכשיר שהתגלה. מכשירים יכולים להיות גם מפרסמים וגם מנויים בו-זמנית.

  • יצירת חיבור לרשת: אחרי ששני מכשירים זיהו אחד את השני, הם יכולים ליצור חיבור דו-כיווני לרשת Wi-Fi Aware בלי נקודת גישה.

חיבורים לרשתות Wi-Fi Aware תומכים בקצבי תפוקה גבוהים יותר למרחקים ארוכים יותר מאשר חיבורי Bluetooth. סוגי החיבורים האלה שימושיים לאפליקציות שמשתפות כמויות גדולות של נתונים בין משתמשים, כמו אפליקציות לשיתוף תמונות.

שיפורים ב-Android 13 (רמת API‏ 33)

במכשירים עם Android מגרסה 13 (רמת API‏ 33) ומעלה שתומכים במצב תקשורת מיידית, אפליקציות יכולות להשתמש בשיטות PublishConfig.Builder.setInstantCommunicationModeEnabled() ו-SubscribeConfig.Builder.setInstantCommunicationModeEnabled() כדי להפעיל או להשבית את מצב התקשורת המיידית בסשן גילוי של בעל תוכן דיגיטלי או מנוי. מצב תקשורת מיידית מאיץ את חילופי ההודעות, את זיהוי השירותים ואת כל הגדרות נתיב הנתונים כחלק מסשן זיהוי של מפרסם או מנוי. כדי לבדוק אם המכשיר תומך במצב תקשורת מיידית, משתמשים ב-method‏ isInstantCommunicationModeSupported().

שיפורים ב-Android 12 (רמת API ‏31)

ב-Android 12 (רמת API‏ 31) נוספו כמה שיפורים ל-Wi-Fi Aware:

  • במכשירים עם Android בגרסה 12 (רמת API‏ 31) ומעלה, אפשר להשתמש בקריאה החוזרת (callback) onServiceLost() כדי לקבל התראה כשהאפליקציה מאבדת שירות שזוהה, כי השירות הופסק או יצא מהטווח.
  • ההגדרה של נתיבי נתונים ב-Wi-Fi Aware פשוטה יותר. בגרסאות קודמות נעשה שימוש בהעברת הודעות ב-L2 כדי לספק את כתובת ה-MAC של המכשיר שיזם את החיבור, מה שגרם לזמן אחזור. במכשירים עם Android מגרסה 12 ואילך, אפשר להגדיר את המשיב (השרת) כך שיקבל כל עמית – כלומר, הוא לא צריך לדעת מראש את כתובת ה-MAC של היוזם. השינוי הזה מאיץ את הפעלת נתיב הנתונים ומאפשר כמה קישורים מנקודה לנקודה באמצעות בקשה אחת לאחזור מהרשת.
  • אפליקציות שפועלות ב-Android מגרסה 12 ואילך יכולות להשתמש ב-method‏ WifiAwareManager.getAvailableAwareResources() כדי לקבל את מספר נתיבי הנתונים הזמינים, סשנים של פרסום וסשנים של הרשמה. כך האפליקציה יכולה לקבוע אם יש מספיק משאבים זמינים כדי להפעיל את הפונקציונליות הרצויה.

הגדרה ראשונית

כדי להגדיר את האפליקציה כך שתשתמש בגילוי וברשתות של Wi-Fi Aware, מבצעים את השלבים הבאים:

  1. מבקשים את ההרשאות הבאות במניפסט של האפליקציה:

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- If your app targets Android 13 (API level 33)
         or higher, you must declare the NEARBY_WIFI_DEVICES permission. -->
    <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
                     <!-- If your app derives location information from
                          Wi-Fi APIs, don't include the "usesPermissionFlags"
                          attribute. -->
                     android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
                     <!-- If any feature in your app relies on precise location
                          information, don't include the "maxSdkVersion"
                          attribute. -->
                     android:maxSdkVersion="32" />
    
  2. בודקים אם המכשיר תומך ב-Wi-Fi Aware באמצעות ה-API‏ PackageManager:

    Kotlin

    context.packageManager.hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE)
    

    Java

    context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WIFI_AWARE);
    
  3. בודקים אם Wi-Fi Aware זמין. יכול להיות שהתכונה Wi-Fi Aware קיימת במכשיר, אבל לא זמינה כי המשתמש השבית את ה-Wi-Fi או את המיקום. בהתאם ליכולות החומרה והקושחה, יכול להיות שמכשירים מסוימים לא יתמכו ב-Wi-Fi Aware אם נעשה שימוש ב-Wi-Fi ישיר, ב-SoftAP או ב-Tethering. כדי לבדוק אם Wi-Fi Aware זמין, מפעילים את isAvailable().

    הזמינות של Wi-Fi Aware יכולה להשתנות בכל שלב. רושמים BroadcastReceiver כדי לקבל ACTION_WIFI_AWARE_STATE_CHANGED, שנשלח בכל פעם שהזמינות משתנה. כשהאפליקציה מקבלת את ה-intent של השידור, צריך למחוק את כל הסשנים הקיימים (בהנחה שהשירות של Wi-Fi Aware הופסק), ואז לבדוק את מצב הזמינות ולשנות את ההתנהגות של האפליקציה בהתאם. לדוגמה:

    Kotlin

    val wifiAwareManager = context.getSystemService(Context.WIFI_AWARE_SERVICE) as WifiAwareManager?
    val filter = IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED)
    val myReceiver = object : BroadcastReceiver() {
    
        override fun onReceive(context: Context, intent: Intent) {
            // discard current sessions
            if (wifiAwareManager?.isAvailable) {
                ...
            } else {
                ...
            }
        }
    }
    context.registerReceiver(myReceiver, filter)
    

    Java

    WifiAwareManager wifiAwareManager =
            (WifiAwareManager) context.getSystemService(Context.WIFI_AWARE_SERVICE);
    IntentFilter filter =
            new IntentFilter(WifiAwareManager.ACTION_WIFI_AWARE_STATE_CHANGED);
    BroadcastReceiver myReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // discard current sessions
            if (wifiAwareManager.isAvailable()) {
                ...
            } else {
                ...
            }
        }
    };
    context.registerReceiver(myReceiver, filter);
    

    מידע נוסף זמין במאמר בנושא שידורים.

קבלת סשן

כדי להתחיל להשתמש ב-Wi-Fi Aware, האפליקציה צריכה לקבל WifiAwareSession באמצעות קריאה ל-attach(). ה-method הזו מבצעת את הפעולות הבאות:

  • מפעילה את החומרה של Wi-Fi Aware.
  • מצטרף לאשכול Wi-Fi Aware או יוצר אשכול כזה.
  • יוצר סשן Wi-Fi Aware עם מרחב שמות ייחודי שמשמש כמאגר לכל סשני הגילוי שנוצרו בתוכו.

אם האפליקציה מצורפת בהצלחה, המערכת מפעילה את פונקציית ה-callback‏ onAttached(). הקריאה החוזרת הזו מספקת אובייקט WifiAwareSession שהאפליקציה צריכה להשתמש בו לכל הפעולות הבאות בסשן. אפליקציה יכולה להשתמש בסשן כדי לפרסם שירות או להירשם לשירות.

האפליקציה צריכה להפעיל את attach() רק פעם אחת. אם האפליקציה קוראת ל-attach() כמה פעמים, היא מקבלת סשן שונה לכל קריאה, ולכל סשן יש מרחב שמות משלו. האפשרות הזו יכולה להיות שימושית בתרחישים מורכבים, אבל בדרך כלל כדאי להימנע ממנה.

פרסום שירות

כדי להפוך שירות לגלוי, מפעילים את השיטה publish(), שמקבלת את הפרמטרים הבאים:

  • ‫PublishConfig מציין את שם השירות ומאפייני הגדרה אחרים, כמו מסנן התאמה.
  • DiscoverySessionCallback מציין את הפעולות שצריך לבצע כשמתרחשים אירועים, למשל כשהמנוי מקבל הודעה.

הנה דוגמה:

Kotlin

val config: PublishConfig = PublishConfig.Builder()
        .setServiceName(AWARE_FILE_SHARE_SERVICE_NAME)
        .build()
awareSession.publish(config, object : DiscoverySessionCallback() {

    override fun onPublishStarted(session: PublishDiscoverySession) {
        ...
    }

    override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
        ...
    }
})

Java

PublishConfig config = new PublishConfig.Builder()
    .setServiceName("Aware_File_Share_Service_Name")
    .build();

awareSession.publish(config, new DiscoverySessionCallback() {
    @Override
    public void onPublishStarted(PublishDiscoverySession session) {
        ...
    }
    @Override
    public void onMessageReceived(PeerHandle peerHandle, byte[] message) {
        ...
    }
}, null);

אם הפרסום מצליח, מתבצעת קריאה לשיטת הקריאה החוזרת onPublishStarted().

אחרי הפרסום, כשמכשירים שמריצים אפליקציות מנויים תואמות נכנסים לטווח ה-Wi-Fi של המכשיר המפרסם, המנויים מגלים את השירות. כשאפליקציה רשומה מגלה אפליקציה מפרסמת, האפליקציה המפרסמת לא מקבלת התראה. אבל אם האפליקציה הרשומה שולחת הודעה לאפליקציה המפרסמת, האפליקציה המפרסמת מקבלת התראה. במקרה כזה, המערכת קוראת לשיטת הקריאה החוזרת onMessageReceived(). אתם יכולים להשתמש בארגומנט PeerHandle מהשיטה הזו כדי לשלוח הודעה בחזרה למנוי או ליצור חיבור אליו.

כדי להפסיק את פרסום השירות, מפעילים את DiscoverySession.close(). סשנים של Discovery משויכים ל-WifiAwareSession הראשי שלהם. אם סוגרים את הסשן הראשי, גם הסשנים המשויכים של החיפוש נסגרים. גם אובייקטים שנמחקים נסגרים, אבל המערכת לא מבטיחה מתי סשנים מחוץ להיקף נסגרים, ולכן מומלץ להפעיל באופן מפורש את השיטות של close().

הרשמה לשירות

כדי להירשם לשירות, מפעילים את ה-method‏ subscribe(), שמקבלת את הפרמטרים הבאים:

  • ‫SubscribeConfig מציין את שם השירות שאליו רוצים להירשם ומאפייני הגדרה אחרים, כמו מסנן התאמה.
  • DiscoverySessionCallback מציין את הפעולות שיש לבצע כשמתרחשים אירועים, כמו למשל כשמתגלה בעל תוכן דיגיטלי.

הנה דוגמה:

Kotlin

val config: SubscribeConfig = SubscribeConfig.Builder()
        .setServiceName(AWARE_FILE_SHARE_SERVICE_NAME)
        .build()
awareSession.subscribe(config, object : DiscoverySessionCallback() {

    override fun onSubscribeStarted(session: SubscribeDiscoverySession) {
        ...
    }

    override fun onServiceDiscovered(
            peerHandle: PeerHandle,
            serviceSpecificInfo: ByteArray,
            matchFilter: List<ByteArray>
    ) {
        ...
    }
}, null)

Java

SubscribeConfig config = new SubscribeConfig.Builder()
    .setServiceName("Aware_File_Share_Service_Name")
    .build();

awareSession.subscribe(config, new DiscoverySessionCallback() {
    @Override
    public void onSubscribeStarted(SubscribeDiscoverySession session) {
        ...
    }

    @Override
    public void onServiceDiscovered(PeerHandle peerHandle,
            byte[] serviceSpecificInfo, List<byte[]> matchFilter) {
        ...
    }
}, null);

אם פעולת ההרשמה מצליחה, המערכת מפעילה את פונקציית ה-callback‏ onSubscribeStarted() באפליקציה. מכיוון שאפשר להשתמש בארגומנט SubscribeDiscoverySession בפונקציית ה-callback כדי לתקשר עם מפרסם אחרי שהאפליקציה מזהה אותו, כדאי לשמור את ההפניה הזו. אתם יכולים לעדכן את סשן ההרשמה בכל שלב על ידי קריאה ל-updateSubscribe() בסשן הגילוי.

בשלב הזה, המינוי ממתין לבעלי תוכן דיגיטלי תואמים שייכנסו לטווח ה-Wi-Fi. במקרים כאלה, המערכת מפעילה את שיטת הקריאה החוזרת onServiceDiscovered(). אתם יכולים להשתמש בארגומנט PeerHandle מהקריאה החוזרת הזו כדי לשלוח הודעה או ליצור חיבור לאותו מפרסם.

כדי לבטל את המינוי לשירות, מתקשרים למספר DiscoverySession.close(). סשנים של Discovery משויכים ל-WifiAwareSession הראשי שלהם. אם סוגרים את הסשן הראשי, גם הסשנים המשויכים של החיפוש נסגרים.

גם אובייקטים שנמחקו נסגרים, אבל המערכת לא מבטיחה מתי ייסגרו סשנים מחוץ להיקף, ולכן מומלץ להפעיל באופן מפורש את השיטות של close().

שליחת הודעה

כדי לשלוח הודעה למכשיר אחר, צריך את האובייקטים הבאים:

  • DiscoverySession. האובייקט הזה מאפשר לכם להפעיל את sendMessage(). האפליקציה מקבלת DiscoverySession על ידי פרסום שירות או הרשמה לשירות.

  • PeerHandle של המכשיר השני, כדי לנתב את ההודעה. האפליקציה מקבלת את PeerHandle של מכשיר אחר באחת משתי דרכים:

    • האפליקציה שלכם מפרסמת שירות ומקבלת הודעה ממנוי. האפליקציה מקבלת את ה-PeerHandle של המנוי מהקריאה החוזרת (callback) של onMessageReceived().
    • האפליקציה שלכם נרשמת לשירות. כשהאפליקציה תזהה בעל אפליקציה תואם, היא תקבל את ה-PeerHandle שלו מהקריאה החוזרת (callback) של onServiceDiscovered().

כדי לשלוח הודעה, צריך להתקשר אל sendMessage(). יכול להיות שיופעלו הקריאות החוזרות הבאות:

  • כשההודעה מתקבלת בהצלחה על ידי המכשיר השני, המערכת קוראת ל-callback של onMessageSendSucceeded() באפליקציה השולחת.
  • כשהמשתמש השני מקבל הודעה, המערכת מפעילה את פונקציית ה-callback‏ onMessageReceived() באפליקציה המקבלת.

למרות שצריך את PeerHandle כדי לתקשר עם עמיתים, לא כדאי להסתמך עליו כמזהה קבוע של עמיתים. האפליקציה יכולה להשתמש במזהים ברמה גבוהה יותר, שמוטמעים בשירות הגילוי עצמו או בהודעות הבאות. אפשר להטמיע מזהה בשירות הגילוי באמצעות השיטה setMatchFilter() או setServiceSpecificInfo() של PublishConfig או SubscribeConfig. השיטה setMatchFilter() משפיעה על הגילוי, ואילו השיטה setServiceSpecificInfo() לא משפיעה על הגילוי.

הטמעת מזהה בהודעה פירושה שינוי של מערך הבייטים של ההודעה כך שיכלול מזהה (לדוגמה, בתור צמד הבייטים הראשון).

יצירת חיבור

‫Wi-Fi Aware תומך ברשתות לקוח-שרת בין שני מכשירי Wi-Fi Aware.

כדי להגדיר את החיבור בין הלקוח לשרת:

  1. אתם יכולים להשתמש ב-Wi-Fi Aware כדי לפרסם שירות ולהירשם לשירות.

  2. אחרי שהמנוי מגלה את בעל התוכן הדיגיטלי, שולחים הודעה מהמנוי לבעל התוכן הדיגיטלי.

  3. מפעילים ServerSocket במכשיר המפרסם ומגדירים או מקבלים את היציאה שלו:

    Kotlin

    val ss = ServerSocket(0)
    val port = ss.localPort
    

    Java

    ServerSocket ss = new ServerSocket(0);
    int port = ss.getLocalPort();
    
  4. משתמשים ב-ConnectivityManager כדי לבקש רשת Wi-Fi Aware במכשיר המפרסם באמצעות WifiAwareNetworkSpecifier, ומציינים את סשן הגילוי ואת ה-PeerHandle של המנוי, שקיבלתם מההודעה שהמנוי שלח:

    Kotlin

    val networkSpecifier = WifiAwareNetworkSpecifier.Builder(discoverySession, peerHandle)
        .setPskPassphrase("somePassword")
        .setPort(port)
        .build()
    val myNetworkRequest = NetworkRequest.Builder()
        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
        .setNetworkSpecifier(networkSpecifier)
        .build()
    val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            ...
        }
    
        override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
            ...
        }
    
        override fun onLost(network: Network) {
            ...
        }
    }
    
    connMgr.requestNetwork(myNetworkRequest, callback);
    

    Java

    NetworkSpecifier networkSpecifier = new WifiAwareNetworkSpecifier.Builder(discoverySession, peerHandle)
        .setPskPassphrase("somePassword")
        .setPort(port)
        .build();
    NetworkRequest myNetworkRequest = new NetworkRequest.Builder()
        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
        .setNetworkSpecifier(networkSpecifier)
        .build();
    ConnectivityManager.NetworkCallback callback = new ConnectivityManager.NetworkCallback() {
        @Override
        public void onAvailable(Network network) {
            ...
        }
    
        @Override
        public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) {
            ...
        }
    
        @Override
        public void onLost(Network network) {
            ...
        }
    };
    
    connMgr.requestNetwork(myNetworkRequest, callback);
    
  5. אחרי שבעל האתר מבקש רשת, שולחים הודעה למנוי.

  6. אחרי שהמנוי מקבל את ההודעה מהמפרסם, מבקשים רשת Wi-Fi Aware במנוי באותה שיטה כמו במפרסם. אל תציינו יציאה כשאתם יוצרים את NetworkSpecifier. שיטות הקריאה החוזרת המתאימות נקראות כשהחיבור לרשת זמין, משתנה או אובד.

  7. אחרי שה-method‏ onAvailable() נקראת במנוי, אובייקט Network זמין, ואפשר לפתוח איתו Socket כדי לתקשר עם ServerSocket בבעל התוכן הדיגיטלי, אבל צריך לדעת את כתובת ה-IPv6 והיציאה של ServerSocket. אפשר לקבל אותם מהאובייקט NetworkCapabilities שסופק בקריאה החוזרת (callback) של onCapabilitiesChanged():

    Kotlin

    val peerAwareInfo = networkCapabilities.transportInfo as WifiAwareNetworkInfo
    val peerIpv6 = peerAwareInfo.peerIpv6Addr
    val peerPort = peerAwareInfo.port
    ...
    val socket = network.getSocketFactory().createSocket(peerIpv6, peerPort)
    

    Java

    WifiAwareNetworkInfo peerAwareInfo = (WifiAwareNetworkInfo) networkCapabilities.getTransportInfo();
    Inet6Address peerIpv6 = peerAwareInfo.getPeerIpv6Addr();
    int peerPort = peerAwareInfo.getPort();
    ...
    Socket socket = network.getSocketFactory().createSocket(peerIpv6, peerPort);
    
  8. כשמסיימים עם החיבור לרשת, קוראים ל-unregisterNetworkCallback().

מדידת מרחק בין מכשירים וגילוי מכשירים לפי מיקום

מכשיר עם יכולות מיקום Wi-Fi RTT יכול למדוד ישירות את המרחק למכשירים אחרים ולהשתמש במידע הזה כדי להגביל את זיהוי השירותים של Wi-Fi Aware.

‫Wi-Fi RTT API מאפשר מדידת טווח ישירה למכשיר Wi-Fi Aware באמצעות כתובת ה-MAC שלו או באמצעות PeerHandle.

אפשר להגביל את החיפוש של Wi-Fi Aware כך שימצא רק שירותים בתוך גבולות וירטואליים מסוימים. לדוגמה, אתם יכולים להגדיר גבול וירטואלי שמאפשר לגלות מכשיר שמפרסם שירות Aware_File_Share_Service_Name במרחק של לפחות 3 מטרים (מצוין כ-3,000 מ"מ) ולא יותר מ-10 מטרים (מצוין כ-10,000 מ"מ).

כדי להפעיל גבולות וירטואליים, גם בעל האפליקציה וגם המנוי צריכים לבצע פעולה:

  • בעל התוכן הדיגיטלי צריך להפעיל את טווח המרחק בשירות שפורסם באמצעות setRangingEnabled(true).

    אם בעל התוכן הדיגיטלי לא מפעיל את טווח המרחק, המערכת מתעלמת מכל מגבלות הגבולות הווירטואליים שהמנוי ציין, ומבצעת גילוי רגיל תוך התעלמות מהמרחק.

  • המנוי צריך לציין גבול וירטואלי באמצעות שילוב כלשהו של setMinDistanceMm() ו-setMaxDistanceMm().

    אם לא מציינים מרחק, אין מגבלה. אם מציינים רק את המרחק המקסימלי, המרחק המינימלי הוא 0. אם מציינים רק את המרחק המינימלי, אין מרחק מקסימלי.

כשמזוהה שירות עמית בתוך גבול וירטואלי, מופעלת פונקציית ה-callback‏ onServiceDiscoveredWithinRange, שמספקת את המרחק שנמדד לעמית. לאחר מכן אפשר להפעיל את ה-API הישיר של Wi-Fi RTT לפי הצורך כדי למדוד מרחק במועד מאוחר יותר.