Register now for Android Dev Summit 2019!

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

このレッスンでは、ネットワーク リソースの使用について微調整可能なアプリを作成する方法を説明します。アプリケーションでネットワーク操作の多くを実行する場合、アプリの実行するデータ同期の頻度、アップロード/ダウンロードを Wi-Fi でのみ実行するのかどうか、ローミング中にデータを使用するかどうかなど、ユーザーがアプリのデータ使用を制御できるようにするユーザー設定を提供する必要があります。これらをユーザーが管理できる場合、データ使用量の限度に近づいても、バックグラウンドデータへのアプリのアクセスをユーザーが無効にする可能性はずっと少なくなります。ユーザーはアプリのアクセスを無効にする代わりに、アプリのデータの使用量を厳密に管理できるからです。

一定期間のネットワーク接続の数と種類など、アプリのネットワーク使用の詳細については、ウェブアプリおよび Network Profiler を使用してネットワークトラフィックを検査するをご覧ください。ダウンロードとネットワーク接続がバッテリーの寿命に与える影響を最小限に抑えるアプリの作成方法に関する一般的なガイドラインについては、電池寿命を伸ばすおよび電池を消耗させずにデータを転送するをご覧ください。

また、ネットワーク接続のサンプルも参考にしてください。

端末のネットワーク接続を確認する

1 つの端末に対して、複数の種類のネットワーク接続が可能です。このレッスンでは、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 アクションのインテント フィルタ(Android 4.0 で導入)を宣言することができます。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>

プリファレンス アクティビティを実装する

上記に引用したマニフェストで示されているように、サンプル アプリのアクティビティ SettingsActivity には、ACTION_MANAGE_NETWORK_USAGE アクションのためのインテント フィルタがあります。SettingsActivity は、PreferenceActivity のサブクラスです。これは、次のことを指定するプリファレンス画面(図 1)を表示します。

  • XML フィードの各エントリごとに要約を表示するか、それとも各エントリのリンクのみ表示するか
  • XML フィードをダウンロードするのは、任意のネットワーク接続が利用可能な場合か、それとも Wi-Fi が利用可能な場合のみか。
プリファレンス パネル ネットワーク プリファレンスの設定

図 1. プリファレンス アクティビティ

次の SettingsActivity を示します。これが OnSharedPreferenceChangeListener を実装していることに注意してください。ユーザーがプリファレンスの 1 つを変更すると、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 をインターセプトし、ネットワーク接続のステータスを調べ、それに応じて wifiConnectedmobileConnected のフラグを 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();
        }
    }
}