ネットワーク使用状況を管理する

このレッスンでは、ネットワーク リソースの使用量を微調整できるアプリを作成する方法について説明します。多量のネットワーク オペレーションを実行するアプリの場合、アプリのデータ慣行(データの同期頻度、アップロードやダウンロードは Wi-Fi 経由のみに制限されているか、ローミング時にモバイルデータを使用するかなど)を、ユーザー自身が制御できるようにユーザー設定を設けることをおすすめします。こうした設定をユーザーが管理できるようにすることで、データ使用量の上限に近づいても、アプリのデータ使用量を自分で細かく管理できるため、アプリでバックグラウンド データへのアクセスを無効にする可能性は少なくなります。

一定期間におけるネットワーク接続の数やタイプなど、アプリのネットワーク使用状況については、ウェブアプリNetwork Profiler を使用してネットワーク トラフィックを検査するをご覧ください。ダウンロードやネットワーク接続がバッテリー寿命に与える影響を最小限に抑える方法の一般的なガイドラインについては、バッテリー寿命を最適化するバッテリー消費を抑えてデータを転送するをご覧ください。

また、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 インスタンスを返します。インターフェースが 1 つも接続されていない場合(つまり、インターネット接続を利用できない場合)は、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 クラスによって処理されます。このクラスにより、環境設定用 UI が表示され、ユーザーはフィードをダウンロードするタイミングを決定できます。

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

ユーザーのプライベート データを扱う、Android 11 以降をターゲットとするアプリでは、プロセス単位でネットワーク アクセスを許可できます。ネットワークへのアクセスを許可するプロセスを明示することにより、データのアップロードが不要なコードをすべて分離します。

このようにすれば、アプリから不本意にデータがアップロードされなくなるとまでは保証できせんが、アプリのバグが原因でデータが漏洩する可能性を低くすることができます。

次に、プロセスごとの機能を使用するマニフェスト ファイルのサンプルを示します。

<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 アクションのインテント フィルタがあります。SettingsActivityPreferenceActivity のサブクラスです。このサブクラスにより、環境設定画面(図 1 を参照)が表示され、ユーザーは以下を指定できます。

  • 各 XML フィード エントリの概要を表示するか、各エントリのリンクのみを表示するか。
  • XML フィードを、任意のネットワーク接続が利用可能なときにダウンロードするのか、Wi-Fi が利用可能なときにだけダウンロードするのか。

環境設定パネル ネットワーク環境設定を指定する

図 1. 環境設定アクティビティ

SettingsActivity を以下に示します。OnSharedPreferenceChangeListener を実装しています。ユーザーが環境設定を変更すると、onSharedPreferenceChanged() をトリガーし、refreshDisplay を true に設定します。これにより、ユーザーがメイン アクティビティに戻ったときに、表示が更新されます。

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 フラグを true または false に設定します。最終的に、ユーザーが次にアプリに戻ったとき、NetworkActivity.refreshDisplaytrue に設定されていた場合に限り、アプリは最新のフィードをダウンロードして表示を更新します。

不必要に呼び出される BroadcastReceiver を実装すると、システム リソースの消耗につながる可能性があります。サンプルアプリは、onCreate()BroadcastReceiver NetworkReceiver を登録し、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();
        }
    }
}