إدارة استخدام الشبكة

يصف هذا الدرس كيفية كتابة التطبيقات التي تتمتع بدقة التحكم في استخدامها لموارد الشبكة. إذا كان تطبيقك يجري الكثير من العمليات على الشبكة، يجب توفير إعدادات للمستخدم تتيح للمستخدمين التحكّم في عادات استخدام البيانات في التطبيق، مثل عدد مرات مزامنة التطبيق للبيانات، وما إذا كان سيتم إجراء عمليات التحميل أو التنزيل عند الاتصال بشبكة Wi-Fi فقط، وما إذا كان سيتم استخدام البيانات أثناء التجوال، وما إلى ذلك. مع عناصر التحكم هذه المتاحة لهم، يقل احتمال إيقاف وصول التطبيق إلى بيانات الخلفية عند بلوغ الحدود المخصصة لهم، لأنه يمكنهم بدلاً من ذلك التحكم بدقة في مقدار البيانات التي يستخدمها تطبيقك.

لمعرفة المزيد من المعلومات عن استخدام تطبيقك للشبكة، بما في ذلك عدد اتصالات الشبكة وأنواعها على مدار فترة زمنية، يمكنك الاطّلاع على تطبيقات الويب وفحص حركة مرور بيانات الشبكة باستخدام ملف تعريف الشبكة. للحصول على إرشادات عامة حول كيفية كتابة التطبيقات التي تقلل من تأثير عمر البطارية لعمليات التنزيل واتصالات الشبكة، راجِع تحسين عمر البطارية ونقل البيانات بدون استنزاف البطارية.

يمكنك أيضًا الاطّلاع على نموذج NetworkConnect.

التحقّق من اتصال شبكة الجهاز

يمكن أن يتضمن الجهاز أنواعًا مختلفة من اتصالات الشبكة. يركز هذا الدرس على استخدام إما اتصال Wi-Fi أو اتصال شبكة محمول. للحصول على القائمة الكاملة بأنواع الشبكات المحتملة، يُرجى الاطّلاع على ConnectivityManager.

تكون شبكة Wi-Fi أسرع عادةً. بالإضافة إلى ذلك، غالبًا ما تفرض تكلفة استخدام بيانات الجوّال، ما قد يكون مكلفًا. من الاستراتيجيات الشائعة للتطبيقات جلب البيانات الكبيرة فقط عند توفر شبكة Wi-Fi.

قبل تنفيذ أي عمليات على الشبكة، من الممارسات الجيدة التحقّق من حالة اتصال الشبكة. من بين أمور أخرى، قد يمنع ذلك استخدام تطبيقك بدون قصد لاسلكيًا. إذا كان الاتصال بالشبكة غير متاح، فمن المفترض أن يستجيب التطبيق بشكل مناسب. للتحقق من اتصال الشبكة، يمكنك عادةً استخدام الفئات التالية:

  • ConnectivityManager: يجيب عن طلبات البحث عن حالة الاتصال بالشبكة. كما يُبلِغ التطبيقات عند تغير اتصال الشبكة.
  • NetworkInfo: يصف حالة واجهة شبكة من نوع معيّن (إما الجوّال أو Wi-Fi حاليًا).

يختبر مقتطف الرمز هذا إمكانية اتصال الشبكة لكل من شبكة Wi-Fi والجوّال. وهو يحدد ما إذا كانت واجهات الشبكات هذه متاحة (أي ما إذا كانت إمكانية الاتصال بالشبكة ممكنة) و/أو متصلة (أي ما إذا كانت هناك إمكانية اتصال بالشبكة وما إذا كان من الممكن إنشاء مآخذ وتمرير البيانات):

Kotlin

private const val DEBUG_TAG = "NetworkStatusExample"
...
val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
var isWifiConn: Boolean = false
var isMobileConn: Boolean = false
connMgr.allNetworks.forEach { network ->
    connMgr.getNetworkInfo(network).apply {
        if (type == ConnectivityManager.TYPE_WIFI) {
            isWifiConn = isWifiConn or isConnected
        }
        if (type == ConnectivityManager.TYPE_MOBILE) {
            isMobileConn = isMobileConn or isConnected
        }
    }
}
Log.d(DEBUG_TAG, "Wifi connected: $isWifiConn")
Log.d(DEBUG_TAG, "Mobile connected: $isMobileConn")

Java

private static final String DEBUG_TAG = "NetworkStatusExample";
...
ConnectivityManager connMgr =
        (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
boolean isWifiConn = false;
boolean isMobileConn = false;
for (Network network : connMgr.getAllNetworks()) {
    NetworkInfo networkInfo = connMgr.getNetworkInfo(network);
    if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
        isWifiConn |= networkInfo.isConnected();
    }
    if (networkInfo.getType() == ConnectivityManager.TYPE_MOBILE) {
        isMobileConn |= networkInfo.isConnected();
    }
}
Log.d(DEBUG_TAG, "Wifi connected: " + isWifiConn);
Log.d(DEBUG_TAG, "Mobile connected: " + isMobileConn);

تجدر الإشارة إلى أنه يجب ألا تستند القرارات إلى ما إذا كانت الشبكة "متاحة". يجب دائمًا التحقق من isConnected() قبل إجراء عمليات الشبكة، لأن isConnected() يعالج حالات مثل شبكات الجوّال غير المستقرة ووضع الطائرة وبيانات الخلفية المحظورة.

هناك طريقة أكثر إيجازًا للتحقق مما إذا كانت واجهة الشبكة متاحة هي على النحو التالي. تعرض الطريقة getActiveNetworkInfo() مثيل NetworkInfo يمثل أول واجهة شبكة متصلة يمكن العثور عليها، أو null إذا لم تكن أي من الواجهات متصلة (بمعنى أن الاتصال بالإنترنت غير متاح):

Kotlin

fun isOnline(): Boolean {
    val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val networkInfo: NetworkInfo? = connMgr.activeNetworkInfo
    return networkInfo?.isConnected == true
}

Java

public boolean isOnline() {
    ConnectivityManager connMgr = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    return (networkInfo != null && networkInfo.isConnected());
}

للبحث عن حالة أكثر دقة، يمكنك استخدام NetworkInfo.DetailedState، ولكن نادرًا ما يكون ذلك ضروريًا.

إدارة استخدام الشبكة

يمكنك تنفيذ نشاط إعدادات مفضَّلة يمنح المستخدمين تحكُّمًا صريحًا في استخدام تطبيقك لموارد الشبكة. مثلاً:

  • يمكنك السماح للمستخدمين بتحميل الفيديوهات فقط عندما يكون الجهاز متصلاً بشبكة Wi-Fi.
  • يمكنك إجراء المزامنة (أو عدم المزامنة) اعتمادًا على معايير محددة مثل توفر الشبكة والفاصل الزمني وما إلى ذلك.

لكتابة تطبيق يتيح الوصول إلى الشبكة وإدارة استخدام الشبكة، يجب أن يتضمن البيان الأذونات وفلاتر الأهداف المناسبة.

  • يتضمن البيان المقتبس لاحقًا في هذا القسم الأذونات التالية:
  • يمكنك تعريف فلتر الأهداف لإجراء ACTION_MANAGE_NETWORK_USAGE للإشارة إلى أنّ تطبيقك يحدّد نشاطًا يقدّم خيارات للتحكّم في استخدام البيانات. تعرض علامة ACTION_MANAGE_NETWORK_USAGE الإعدادات لإدارة استخدام بيانات الشبكة لتطبيق معيّن. عندما يتضمّن تطبيقك نشاط إعدادات يسمح للمستخدمين بالتحكّم في استخدام الشبكة، يجب الإفصاح عن فلتر الأهداف هذا لذلك النشاط.

في نموذج التطبيق، تعالج الفئة SettingsActivity هذا الإجراء، التي تعرض واجهة مستخدم الإعدادات المفضّلة للسماح للمستخدمين بتحديد الوقت المناسب لتنزيل الخلاصة.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.networkusage"
    ...>

    <uses-sdk android:minSdkVersion="4"
           android:targetSdkVersion="14" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        ...>
        ...
        <activity android:label="SettingsActivity" android:name=".SettingsActivity">
             <intent-filter>
                <action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
                <category android:name="android.intent.category.DEFAULT" />
          </intent-filter>
        </activity>
    </application>
</manifest>

ويمكن للتطبيقات التي تتعامل مع بيانات المستخدمين الحساسة والتي تستهدف الإصدار 11 من نظام التشغيل Android والإصدارات الأحدث منح إذن الوصول إلى الشبكة لكل عملية. ومن خلال التحديد الصريح للعمليات المسموح بها للوصول إلى الشبكة، يمكنك عزل جميع الرموز التي لا تحتاج إلى تحميل البيانات.

رغم عدم ضمان منع تطبيقك من تحميل البيانات عن طريق الخطأ، فإنه يوفر لك وسيلة لتقليل فرصة وجود أخطاء في تطبيقك تسبب تسربًا للبيانات.

يوضح ما يلي عينة من ملف البيان الذي يستخدم الوظائف لكل عملية:

<processes>
    <process />
    <deny-permission android:name="android.permission.INTERNET" />
    <process android:process=":withoutnet1" />
    <process android:process="com.android.cts.useprocess.withnet1">
        <allow-permission android:name="android.permission.INTERNET" />
    </process>
    <allow-permission android:name="android.permission.INTERNET" />
    <process android:process=":withoutnet2">
        <deny-permission android:name="android.permission.INTERNET" />
    </process>
    <process android:process="com.android.cts.useprocess.withnet2" />
</processes>

تنفيذ نشاط تفضيل

كما ترى في مقتطف البيان السابق في هذا الموضوع، يتضمّن نموذج نشاط التطبيق SettingsActivity فلتر أهداف للإجراء ACTION_MANAGE_NETWORK_USAGE. SettingsActivity هي فئة فرعية من PreferenceActivity. ويعرض شاشة الإعدادات المفضّلة (المعروضة في الشكل 1) تتيح للمستخدمين تحديد ما يلي:

  • ما إذا كان سيتم عرض ملخصات لكل إدخال في خلاصة XML، أو رابط فقط لكل إدخال.
  • تحديد ما إذا كان سيتم تنزيل خلاصة XML في حال توفّر أي اتصال بالشبكة أو في حال توفّر شبكة Wi-Fi فقط

لوحة الإعدادات المفضّلة ضبط تفضيل الشبكة

الشكل 1. نشاط الإعدادات المفضّلة

آدِي SettingsActivity. يُرجى العلم أنّه يتم تنفيذ OnSharedPreferenceChangeListener. عندما يغيّر المستخدم الإعدادات المفضّلة، يتم تنشيط onSharedPreferenceChanged()، ما يؤدي إلى ضبط refreshDisplay على "صحيح". يؤدي ذلك إلى تحديث الشاشة عندما يعود المستخدم إلى النشاط الرئيسي:

Kotlin

class SettingsActivity : PreferenceActivity(), OnSharedPreferenceChangeListener {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Loads the XML preferences file
        addPreferencesFromResource(R.xml.preferences)
    }

    override fun onResume() {
        super.onResume()

        // Registers a listener whenever a key changes
        preferenceScreen?.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
    }

    override fun onPause() {
        super.onPause()

        // Unregisters the listener set in onResume().
        // It's best practice to unregister listeners when your app isn't using them to cut down on
        // unnecessary system overhead. You do this in onPause().
        preferenceScreen?.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
    }

    // When the user changes the preferences selection,
    // onSharedPreferenceChanged() restarts the main activity as a new
    // task. Sets the refreshDisplay flag to "true" to indicate that
    // the main activity should update its display.
    // The main activity queries the PreferenceManager to get the latest settings.

    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
        // Sets refreshDisplay to true so that when the user returns to the main
        // activity, the display refreshes to reflect the new settings.
        NetworkActivity.refreshDisplay = true
    }
}

Java

public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Loads the XML preferences file
        addPreferencesFromResource(R.xml.preferences);
    }

    @Override
    protected void onResume() {
        super.onResume();

        // Registers a listener whenever a key changes
        getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }

    @Override
    protected void onPause() {
        super.onPause();

       // Unregisters the listener set in onResume().
       // It's best practice to unregister listeners when your app isn't using them to cut down on
       // unnecessary system overhead. You do this in onPause().
       getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
    }

    // When the user changes the preferences selection,
    // onSharedPreferenceChanged() restarts the main activity as a new
    // task. Sets the refreshDisplay flag to "true" to indicate that
    // the main activity should update its display.
    // The main activity queries the PreferenceManager to get the latest settings.

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        // Sets refreshDisplay to true so that when the user returns to the main
        // activity, the display refreshes to reflect the new settings.
        NetworkActivity.refreshDisplay = true;
    }
}

الاستجابة لتغييرات الإعدادات المفضّلة

وعندما يغيّر المستخدم الإعدادات المفضّلة في شاشة الإعدادات، يكون لذلك عادةً تداعيات على سلوك التطبيق. في هذا المقتطف، يتحقّق التطبيق من إعدادات الإعدادات المفضّلة في onStart(). وإذا كان هناك تطابق بين الإعداد واتصال شبكة الجهاز (على سبيل المثال، إذا كان الإعداد "Wi-Fi" وكان الجهاز متصلاً بشبكة Wi-Fi)، ينزّل التطبيق الخلاصة ويعيد تحميل شاشة العرض.

Kotlin

class NetworkActivity : Activity() {

    // The BroadcastReceiver that tracks network connectivity changes.
    private lateinit var receiver: NetworkReceiver

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Registers BroadcastReceiver to track network connection changes.
        val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
        receiver = NetworkReceiver()
        this.registerReceiver(receiver, filter)
    }

    public override fun onDestroy() {
        super.onDestroy()
        // Unregisters BroadcastReceiver when app is destroyed.
        this.unregisterReceiver(receiver)
    }

    // Refreshes the display if the network connection and the
    // pref settings allow it.

    public override fun onStart() {
        super.onStart()

        // Gets the user's network preference settings
        val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)

        // Retrieves a string value for the preferences. The second parameter
        // is the default value to use if a preference value is not found.
        sPref = sharedPrefs.getString("listPref", "Wi-Fi")

        updateConnectedFlags()

        if (refreshDisplay) {
            loadPage()
        }
    }

    // Checks the network connection and sets the wifiConnected and mobileConnected
    // variables accordingly.
    fun updateConnectedFlags() {
        val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

        val activeInfo: NetworkInfo? = connMgr.activeNetworkInfo
        if (activeInfo?.isConnected == true) {
            wifiConnected = activeInfo.type == ConnectivityManager.TYPE_WIFI
            mobileConnected = activeInfo.type == ConnectivityManager.TYPE_MOBILE
        } else {
            wifiConnected = false
            mobileConnected = false
        }
    }

    // Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
    fun loadPage() {
        if (sPref == ANY && (wifiConnected || mobileConnected) || sPref == WIFI && wifiConnected) {
            // AsyncTask subclass
            DownloadXmlTask().execute(URL)
        } else {
            showErrorPage()
        }
    }

    companion object {

        const val WIFI = "Wi-Fi"
        const val ANY = "Any"
        const val SO_URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort;=newest"

        // Whether there is a Wi-Fi connection.
        private var wifiConnected = false
        // Whether there is a mobile connection.
        private var mobileConnected = false
        // Whether the display should be refreshed.
        var refreshDisplay = true

        // The user's current network preference setting.
        var sPref: String? = null
    }
...

}

Java

public class NetworkActivity extends Activity {
    public static final String WIFI = "Wi-Fi";
    public static final String ANY = "Any";
    private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort;=newest";

    // Whether there is a Wi-Fi connection.
    private static boolean wifiConnected = false;
    // Whether there is a mobile connection.
    private static boolean mobileConnected = false;
    // Whether the display should be refreshed.
    public static boolean refreshDisplay = true;

    // The user's current network preference setting.
    public static String sPref = null;

    // The BroadcastReceiver that tracks network connectivity changes.
    private NetworkReceiver receiver = new NetworkReceiver();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Registers BroadcastReceiver to track network connection changes.
        IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
        receiver = new NetworkReceiver();
        this.registerReceiver(receiver, filter);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // Unregisters BroadcastReceiver when app is destroyed.
        if (receiver != null) {
            this.unregisterReceiver(receiver);
        }
    }

    // Refreshes the display if the network connection and the
    // pref settings allow it.

    @Override
    public void onStart () {
        super.onStart();

        // Gets the user's network preference settings
        SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);

        // Retrieves a string value for the preferences. The second parameter
        // is the default value to use if a preference value is not found.
        sPref = sharedPrefs.getString("listPref", "Wi-Fi");

        updateConnectedFlags();

        if(refreshDisplay){
            loadPage();
        }
    }

    // Checks the network connection and sets the wifiConnected and mobileConnected
    // variables accordingly.
    public void updateConnectedFlags() {
        ConnectivityManager connMgr = (ConnectivityManager)
                getSystemService(Context.CONNECTIVITY_SERVICE);

        NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
        if (activeInfo != null && activeInfo.isConnected()) {
            wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI;
            mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE;
        } else {
            wifiConnected = false;
            mobileConnected = false;
        }
    }

    // Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
    public void loadPage() {
        if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected))
                || ((sPref.equals(WIFI)) && (wifiConnected))) {
            // AsyncTask subclass
            new DownloadXmlTask().execute(URL);
        } else {
            showErrorPage();
        }
    }
...

}

رصد التغييرات في الاتصال

الجزء الأخير من اللغز هو الفئة الفرعية BroadcastReceiver، NetworkReceiver. عندما يتغير اتصال الجهاز بالشبكة، يعترض تطبيق NetworkReceiver الإجراء CONNECTIVITY_ACTION ويحدّد حالة الاتصال بالشبكة ويضبط العلامتين wifiConnected وmobileConnected على "صحيح" أو "خطأ" وفقًا لذلك. والنتيجة هي أنّه في المرة التالية التي يعود فيها المستخدم إلى التطبيق، لن ينزّل التطبيق إلا أحدث خلاصة ويحدّث الشاشة إذا تم ضبط السمة NetworkActivity.refreshDisplay على true.

قد يؤدي إعداد BroadcastReceiver التي يتم استدعائها بدون داعٍ إلى استنزاف موارد النظام. ويسجّل نموذج التطبيق BroadcastReceiver NetworkReceiver في onCreate()، ويلغي تسجيله في onDestroy(). وهذه الميزة أقل أهمية من الإعلان عن <receiver> في البيان. عند الإعلان عن علامة <receiver> في البيان، يمكنها تنشيط تطبيقك في أي وقت، حتى ولو لم تشغّله لمدة أسابيع. من خلال التسجيل وإلغاء التسجيل NetworkReceiver في النشاط الرئيسي، تضمن عدم تفعيل التطبيق بعد مغادرة المستخدم للتطبيق. إذا أعلنت عن <receiver> في البيان وتعرفت بالضبط المكان الذي تحتاج إليه، يمكنك استخدام setComponentEnabledSetting() لتفعيله وإيقافه حسب الاقتضاء.

فِي مَا يَلِي NetworkReceiver:

Kotlin

class NetworkReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
        val conn = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val networkInfo: NetworkInfo? = conn.activeNetworkInfo

        // Checks the user prefs and the network connection. Based on the result, decides whether
        // to refresh the display or keep the current display.
        // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection.
        if (WIFI == sPref && networkInfo?.type == ConnectivityManager.TYPE_WIFI) {
            // If device has its Wi-Fi connection, sets refreshDisplay
            // to true. This causes the display to be refreshed when the user
            // returns to the app.
            refreshDisplay = true
            Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show()

            // If the setting is ANY network and there is a network connection
            // (which by process of elimination would be mobile), sets refreshDisplay to true.
        } else if (ANY == sPref && networkInfo != null) {
            refreshDisplay = true

            // Otherwise, the app can't download content--either because there is no network
            // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there
            // is no Wi-Fi connection.
            // Sets refreshDisplay to false.
        } else {
            refreshDisplay = false
            Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show()
        }
    }
}

Java

public class NetworkReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        ConnectivityManager conn =  (ConnectivityManager)
            context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = conn.getActiveNetworkInfo();

        // Checks the user prefs and the network connection. Based on the result, decides whether
        // to refresh the display or keep the current display.
        // If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection.
        if (WIFI.equals(sPref) && networkInfo != null
            && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
            // If device has its Wi-Fi connection, sets refreshDisplay
            // to true. This causes the display to be refreshed when the user
            // returns to the app.
            refreshDisplay = true;
            Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show();

        // If the setting is ANY network and there is a network connection
        // (which by process of elimination would be mobile), sets refreshDisplay to true.
        } else if (ANY.equals(sPref) && networkInfo != null) {
            refreshDisplay = true;

        // Otherwise, the app can't download content--either because there is no network
        // connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there
        // is no Wi-Fi connection.
        // Sets refreshDisplay to false.
        } else {
            refreshDisplay = false;
            Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show();
        }
    }
}