Quản lý mức sử dụng mạng

Bài học này mô tả cách viết các ứng dụng có quyền kiểm soát chi tiết về mức sử dụng tài nguyên mạng. Nếu ứng dụng của bạn thực hiện nhiều hoạt động mạng, bạn nên cung cấp các chế độ cài đặt người dùng cho phép người dùng kiểm soát thói quen dữ liệu của ứng dụng, chẳng hạn như tần suất ứng dụng của bạn đồng bộ hoá dữ liệu, tuỳ chọn tải lên/tải xuống chỉ khi dùng Wi-Fi, tuỳ chọn sử dụng dữ liệu khi chuyển vùng, v.v. Khi được cung cấp các quyền kiểm soát này, người dùng ít có khả năng tắt quyền của ứng dụng trong việc truy cập vào dữ liệu nền khi đạt đến giới hạn, vì người dùng có thể kiểm soát chính xác lượng dữ liệu mà ứng dụng của bạn sử dụng.

Để tìm hiểu thêm về mức sử dụng mạng của ứng dụng, bao gồm số lượng và loại kết nối mạng trong một khoảng thời gian, hãy đọc Ứng dụng webKiểm tra lưu lượng truy cập mạng bằng trình phân tích mạng. Để biết hướng dẫn chung về cách viết ứng dụng giúp giảm thiểu tác động đến thời lượng pin khi tải xuống và kết nối mạng, xem Tối ưu hoá thời lượng pinChuyển dữ liệu mà không làm tiêu hao pin.

Bạn cũng có thể xem mẫu NetworkConnect.

Kiểm tra kết nối mạng của thiết bị

Một thiết bị có thể có nhiều loại kết nối mạng. Bài học này tập trung vào việc sử dụng Wi-Fi hoặc kết nối mạng di động. Để biết danh sách đầy đủ các loại mạng có thể có, hãy xem ConnectivityManager.

Wi-Fi thường có tốc độ nhanh hơn. Ngoài ra, dữ liệu di động thường có đo lượng dữ liệu và có thể gây tốn kém. Một chiến lược phổ biến cho các ứng dụng là chỉ tìm nạp dữ liệu lớn nếu có mạng Wi-Fi.

Trước khi thực hiện các hoạt động mạng, bạn nên kiểm tra trạng thái kết nối mạng. Ngoài ra, điều này có thể ngăn ứng dụng của bạn vô tình sử dụng nhầm đài. Nếu không có kết nối mạng, ứng dụng của bạn cần phản hồi linh hoạt. Để kiểm tra kết nối mạng, bạn thường sử dụng các lớp sau:

  • ConnectivityManager: Trả lời các truy vấn về trạng thái kết nối mạng. Ngoài ra, mã còn thông báo cho các ứng dụng khi kết nối mạng thay đổi.
  • NetworkInfo: Mô tả trạng thái của giao diện mạng thuộc một loại nhất định (hiện là Di động hoặc Wi-Fi).

Đoạn mã này kiểm thử khả năng kết nối mạng Wi-Fi và di động. Đoạn mã này xác định xem các giao diện mạng đó có sẵn hay không (nghĩa là có thể kết nối mạng hay không) và/hoặc được kết nối hay chưa (nghĩa là liệu có kết nối mạng không và có thể thiết lập ổ cắm và truyền dữ liệu hay không):

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);

Lưu ý: Bạn không nên quyết định dựa trên việc một mạng có "có sẵn" hay không. Bạn phải luôn kiểm tra isConnected() trước khi thực hiện các hoạt động mạng do isConnected() xử lý các trường hợp như mạng di động không ổn định, chế độ trên máy bay và dữ liệu nền bị hạn chế.

Sau đây là cách ngắn gọn hơn để kiểm tra xem có giao diện mạng hay không. Phương thức getActiveNetworkInfo() sẽ trả về một bản sao NetworkInfo đại diện cho giao diện mạng đã kết nối đầu tiên mà phương thức đó có thể tìm thấy hoặc null nếu không có giao diện được kết nối (nghĩa là không có kết nối Internet):

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());
}

Để truy vấn trạng thái chi tiết hơn, bạn có thể sử dụng NetworkInfo.DetailedState, nhưng việc này ít khi cần thiết.

Quản lý mức sử dụng mạng

Bạn có thể triển khai hoạt động lựa chọn ưu tiên để giúp người dùng kiểm soát rõ ràng mức sử dụng tài nguyên mạng của ứng dụng. Ví dụ:

  • Bạn có thể cho phép người dùng tải video lên chỉ khi thiết bị được kết nối với mạng Wi-Fi.
  • Bạn có thể đồng bộ hoá (hoặc không) tuỳ thuộc vào các tiêu chí cụ thể như khả năng sử dụng mạng, khoảng thời gian, v.v.

Để viết một ứng dụng hỗ trợ truy cập mạng và quản lý mức sử dụng mạng, tệp kê khai của bạn phải có quyền và bộ lọc ý định phù hợp.

  • Tệp kê khai được trích dẫn ở phần sau bao gồm các quyền sau:
  • Bạn có thể khai báo bộ lọc ý định cho thao tác ACTION_MANAGE_NETWORK_USAGE để cho biết rằng ứng dụng của bạn xác định hoạt động cung cấp các tuỳ chọn kiểm soát mức sử dụng dữ liệu. ACTION_MANAGE_NETWORK_USAGE hiển thị chế độ cài đặt để quản lý mức sử dụng dữ liệu mạng của một ứng dụng cụ thể. Khi ứng dụng của bạn có hoạt động về chế độ cài đặt cho phép người dùng kiểm soát mức sử dụng mạng, bạn nên khai báo bộ lọc ý định này cho hoạt động đó.

Trong ứng dụng mẫu, thao tác này được xử lý bằng lớp SettingsActivity hiển thị một giao diện người dùng lựa chọn ưu tiên để cho phép người dùng quyết định thời điểm tải nguồn cấp dữ liệu xuống.

<?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>

Các ứng dụng xử lý dữ liệu nhạy cảm của người dùng và nhắm mục tiêu đến Android 11 trở lên có thể cấp quyền truy cập vào mạng theo từng quy trình. Bằng cách chỉ định rõ ràng các quy trình nào được phép truy cập mạng, bạn sẽ tách biệt tất cả mã không cần tải dữ liệu lên.

Mặc dù không đảm bảo ngăn được ứng dụng của bạn vô tình tải dữ liệu lên, nhưng mã đưa ra một cách giúp bạn giảm nguy cơ lỗi trong ứng dụng gây ra rò rỉ dữ liệu.

Dưới đây là ví dụ về mẫu tệp kê khai sử dụng chức năng theo từng quy trình này:

<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>

Triển khai một hoạt động lựa chọn ưu tiên

Như bạn có thể thấy trong tệp kê khai trích dẫn trước đó của chủ đề này, hoạt động SettingsActivity của ứng dụng mẫu có bộ lọc ý định cho thao tác ACTION_MANAGE_NETWORK_USAGE. SettingsActivity là lớp con của PreferenceActivity. Lớp này hiển thị màn hình lựa chọn ưu tiên (như trong hình 1) cho phép người dùng chỉ định các nội dung sau:

  • Hiển thị nội dung tóm tắt cho từng mục nhập nguồn cấp dữ liệu XML hay chỉ hiển thị một đường liên kết cho mỗi mục nhập.
  • Tải nguồn cấp dữ liệu XML xuống nếu có kết nối mạng, hay chỉ khi có Wi-Fi.

Bảng lựa chọn ưu tiên Đặt lựa chọn ưu tiên mạng

Hình 1. Hoạt động lựa chọn ưu tiên.

Dưới đây là SettingsActivity. Xin lưu ý rằng mã này sẽ triển khai OnSharedPreferenceChangeListener. Khi người dùng thay đổi một lựa chọn ưu tiên, lựa chọn này sẽ kích hoạt onSharedPreferenceChanged(), đặt refreshDisplay thành đúng. Điều này dẫn đến màn hình cho phép làm mới khi người dùng quay lại hoạt động chính:

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;
    }
}

Phản hồi các thay đổi về lựa chọn ưu tiên

Khi người dùng thay đổi lựa chọn ưu tiên trong màn hình cài đặt, sự thay đổi đó thường dẫn đến kết quả cho hành vi của ứng dụng. Trong đoạn mã này, ứng dụng sẽ kiểm tra cài đặt lựa chọn ưu tiên trong onStart(). nếu có sự trùng khớp giữa chế độ cài đặt này và kết nối mạng của thiết bị (ví dụ: nếu chế độ cài đặt là "Wi-Fi" và thiết bị có kết nối Wi-Fi), ứng dụng sẽ tải nguồn cấp dữ liệu xuống và làm mới màn hình hiển thị.

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();
        }
    }
...

}

Phát hiện các thay đổi về kết nối

Phần cuối cùng của vấn đề cần giải quyết là lớp con BroadcastReceiver, NetworkReceiver. Khi kết nối mạng của thiết bị thay đổi, NetworkReceiver sẽ chặn thao tác CONNECTIVITY_ACTION, xác định trạng thái kết nối mạng và đặt cờ wifiConnectedmobileConnected thành đúng/sai cho phù hợp. Kết quả cuối cùng là lần tiếp theo người dùng quay lại ứng dụng, ứng dụng sẽ chỉ tải nguồn cấp dữ liệu mới nhất xuống và cập nhật màn hình nếu NetworkActivity.refreshDisplay được đặt thành true.

Việc thiết lập BroadcastReceiver nhận lệnh gọi một cách không cần thiết có thể làm tiêu hao tài nguyên hệ thống. Ứng dụng mẫu sẽ đăng ký BroadcastReceiver NetworkReceiver trong onCreate() và huỷ đăng ký trong onDestroy(). Thao tác này sẽ nhẹ hơn so với việc khai báo <receiver> trong tệp kê khai. Khi bạn khai báo <receiver> trong tệp kê khai, công cụ này có thể đánh thức ứng dụng của bạn bất cứ lúc nào, ngay cả khi bạn chưa chạy ứng dụng đó trong nhiều tuần. Bằng cách đăng ký và huỷ đăng ký NetworkReceiver trong hoạt động chính, bạn đảm bảo rằng ứng dụng sẽ không bị đánh thức sau khi người dùng rời khỏi ứng dụng. Nếu khai báo <receiver> trong tệp kê khai và biết chính xác nơi bạn cần công cụ này, bạn có thể sử dụng setComponentEnabledSetting() để bật và tắt tính năng này khi thích hợp.

Dưới đây là 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();
        }
    }
}