Android
 Computer >> コンピューター >  >> プログラミング >> Android

AndroidでWi-Fi接続を使ってファイルを転送する方法|サーバー・クライアント実装チュートリアル

本記事では、Androidデバイス間でWi-Fiネットワーク経由のTCPソケット通信を利用して、ファイルやメッセージを送受信する方法を解説します。実装には「サーバー」と「クライアント」の2つのプロジェクトが必要です。

それぞれの役割は以下の通りです。

  • サーバー側: 端末のIPアドレスとポート番号を取得・表示し、クライアントからの接続を待ち受けます。
  • クライアント側: サーバーのIPアドレスとポート番号を入力して接続し、双方向でメッセージをやり取りします。

なお、サンプルコードは旧サポートライブラリ(android.support)を使用しています。最新のプロジェクトでは androidx ライブラリへの置き換えが必要になる点にご注意ください。


サーバーアプリの作成

ステップ1:新規プロジェクトを作成

Android Studioで「File」⇒「New Project」を選択し、必要事項を入力して新しいプロジェクトを作成します。

ステップ2:レイアウトファイル(res/layout/activity_main.xml)

以下のコードを activity_main.xml に追加します。IPアドレス・ポート番号・接続状態・メッセージ履歴を表示するTextViewと、メッセージ入力用のEditText、送信ボタンを配置しています。

<?xml version = "1.0" encoding = "utf-8"?>
<RelativeLayout xmlns:android = "https://schemas.android.com/apk/res/android"
    xmlns:tools = "https://schemas.android.com/tools"
    android:layout_width = "match_parent"
    android:layout_height = "match_parent"
    android:layout_margin = "16dp"
    tools:context = ".MainActivity">
    <TextView
        android:id = "@+id/tvIP"
        android:layout_width = "wrap_content"
        android:layout_height = "wrap_content"
        android:textAppearance = "@style/Base.TextAppearance.AppCompat.Medium" />
    <TextView
        android:id = "@+id/tvPort"
        android:layout_width = "wrap_content"
        android:layout_height = "wrap_content"
        android:layout_below = "@+id/tvIP"
        android:textAppearance = "@style/Base.TextAppearance.AppCompat.Medium" />
    <TextView
        android:id = "@+id/tvConnectionStatus"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:layout_below = "@+id/tvPort"
        android:textAppearance = "@style/Base.TextAppearance.AppCompat.Medium" />
    <TextView
        android:id = "@+id/tvMessages"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:layout_above = "@+id/etMessage"
        android:layout_below = "@+id/tvConnectionStatus"
        android:inputType = "textMultiLine"
        android:textAppearance = "@style/Base.TextAppearance.AppCompat.Medium" />
    <EditText
        android:id = "@+id/etMessage"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:layout_above = "@+id/btnSend"
        android:hint = "Enter Message"
        android:inputType = "text" />
    <Button
        android:id = "@+id/btnSend"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:layout_alignParentBottom = "true"
        android:text = "SEND" />
</RelativeLayout>

ステップ3:アクティビティ(src/MainActivity.java)

次に、MainActivity.java に以下のコードを追加します。WifiManagerからローカルIPアドレスを取得し、指定ポート(8080)でServerSocketを起動してクライアントの接続を待ち受ける構造になっています。

package com.server.myapplication.server;
import android.annotation.SuppressLint;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

@SuppressLint("SetTextI18n")
public class MainActivity extends AppCompatActivity {
    ServerSocket serverSocket;
    Thread Thread1 = null;
    TextView tvIP, tvPort;
    TextView tvMessages;
    EditText etMessage;
    Button btnSend;
    public static String SERVER_IP = "";
    public static final int SERVER_PORT = 8080;
    String message;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tvIP = findViewById(R.id.tvIP);
        tvPort = findViewById(R.id.tvPort);
        tvMessages = findViewById(R.id.tvMessages);
        etMessage = findViewById(R.id.etMessage);
        btnSend = findViewById(R.id.btnSend);
        try {
            SERVER_IP = getLocalIpAddress();
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
        Thread1 = new Thread(new Thread1());
        Thread1.start();
        btnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                message = etMessage.getText().toString().trim();
                if (!message.isEmpty()) {
                    new Thread(new Thread3(message)).start();
                }
            }
        });
    }
    private String getLocalIpAddress() throws UnknownHostException {
        WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
        assert wifiManager != null;
        WifiInfo wifiInfo = wifiManager.getConnectionInfo();
        int ipInt = wifiInfo.getIpAddress();
        return InetAddress.getByAddress(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(ipInt).array()).getHostAddress();
    }
    private PrintWriter output;
    private BufferedReader input;
    class Thread1 implements Runnable {
        @Override
        public void run() {
            Socket socket;
            try {
                serverSocket = new ServerSocket(SERVER_PORT);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tvMessages.setText("Not connected");
                        tvIP.setText("IP: " + SERVER_IP);
                        tvPort.setText("Port: " + String.valueOf(SERVER_PORT));
                    }
                });
                try {
                    socket = serverSocket.accept();
                    output = new PrintWriter(socket.getOutputStream());
                    input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            tvMessages.setText("Connected\n");
                        }
                    });
                    new Thread(new Thread2()).start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    private class Thread2 implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    final String message = input.readLine();
                    if (message != null) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                tvMessages.append("client:" + message + "\n");
                            }
                        });
                    } else {
                        Thread1 = new Thread(new Thread1());
                        Thread1.start();
                        return;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    class Thread3 implements Runnable {
        private String message;
        Thread3(String message) {
            this.message = message;
        }
        @Override
        public void run() {
            output.write(message);
            output.flush();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    tvMessages.append("server: " + message + "\n");
                    etMessage.setText("");
                }
            });
        }
    }
}

ステップ4:マニフェスト(androidManifest.xml)

Wi-Fi状態の取得とインターネット通信を行うため、以下のパーミッションを追加します。

<?xml version = "1.0" encoding = "utf-8"?>
<manifest xmlns:android = "https://schemas.android.com/apk/res/android"
    package = "com.example.myapplication">
    <uses-permission android:name = "android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name = "android.permission.INTERNET"/>
    <application
        android:allowBackup = "true"
        android:icon = "@mipmap/ic_launcher"
        android:label = "@string/app_name"
        android:roundIcon = "@mipmap/ic_launcher_round"
        android:supportsRtl = "true"
        android:theme = "@style/AppTheme">
        <activity android:name = ".MainActivity"
            android:label = "Server">
            <intent-filter>
                <action android:name = "android.intent.action.MAIN" />
                <category android:name = "android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

クライアントアプリの作成

ステップ1:新規プロジェクトを作成

同様に、Android Studioで「File」⇒「New Project」から新しいプロジェクトを作成します。

ステップ2:レイアウトファイル(res/layout/activity_main.xml)

以下のコードを activity_main.xml に追加します。サーバーのIPアドレスとポート番号を入力するフィールド、接続ボタン、メッセージ表示エリア、送信ボタンを配置しています。

<?xml version = "1.0" encoding = "utf-8"?>
<RelativeLayout xmlns:android = "https://schemas.android.com/apk/res/android"
    xmlns:tools = "https://schemas.android.com/tools"
    android:layout_width = "match_parent"
    android:layout_height = "match_parent"
    android:layout_margin = "16dp"
    tools:context = ".MainActivity">
    <EditText
        android:id = "@+id/etIP"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:hint = "IP-Address"
        android:inputType = "text" />
    <EditText
        android:id = "@+id/etPort"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:layout_below = "@+id/etIP"
        android:hint = "Port No"
        android:inputType = "number" />
    <Button
        android:id = "@+id/btnConnect"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:layout_below = "@+id/etPort"
        android:layout_gravity = "center"
        android:layout_marginTop = "16dp"
        android:text = "Connect To Server" />
    <TextView
        android:id = "@+id/tvMessages"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:layout_above = "@+id/etMessage"
        android:layout_below = "@+id/btnConnect"
        android:inputType = "textMultiLine"
        android:textAppearance = "@style/Base.TextAppearance.AppCompat.Medium" />
    <EditText
        android:id = "@+id/etMessage"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:layout_above = "@+id/btnSend"
        android:hint = "Enter Message"
        android:inputType = "text" />
    <Button
        android:id = "@+id/btnSend"
        android:layout_width = "match_parent"
        android:layout_height = "wrap_content"
        android:layout_alignParentBottom = "true"
        android:text = "SEND" />
</RelativeLayout>

ステップ3:アクティビティ(res/layout/MainActivity.java)

次に、MainActivity.java に以下のコードを追加します。入力されたIPアドレスとポート番号をもとにSocket接続を確立し、サーバーとメッセージを送受信します。

package com.client.myapplication.client;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

@SuppressLint("SetTextI18n")
public class MainActivity extends AppCompatActivity {
    Thread Thread1 = null;
    EditText etIP, etPort;
    TextView tvMessages;
    EditText etMessage;
    Button btnSend;
    String SERVER_IP;
    int SERVER_PORT;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        etIP = findViewById(R.id.etIP);
        etPort = findViewById(R.id.etPort);
        tvMessages = findViewById(R.id.tvMessages);
        etMessage = findViewById(R.id.etMessage);
        btnSend = findViewById(R.id.btnSend);
        Button btnConnect = findViewById(R.id.btnConnect);
        btnConnect.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tvMessages.setText("");
                SERVER_IP = etIP.getText().toString().trim();
                SERVER_PORT = Integer.parseInt(etPort.getText().toString().trim());
                Thread1 = new Thread(new Thread1());
                Thread1.start();
            }
        });
        btnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String message = etMessage.getText().toString().trim();
                if (!message.isEmpty()) {
                    new Thread(new Thread3(message)).start();
                }
            }
        });
    }
    private PrintWriter output;
    private BufferedReader input;
    class Thread1 implements Runnable {
        @Override
        public void run() {
            Socket socket;
            try {
                socket = new Socket(SERVER_IP, SERVER_PORT);
                output = new PrintWriter(socket.getOutputStream());
                input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tvMessages.setText("Connected\n");
                    }
                });
                new Thread(new Thread2()).start();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    class Thread2 implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    final String message = input.readLine();
                    if (message != null) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                tvMessages.append("server: " + message + "\n");
                            }
                        });
                    } else {
                        Thread1 = new Thread(new Thread1());
                        Thread1.start();
                        return;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    class Thread3 implements Runnable {
        private String message;
        Thread3(String message) {
            this.message = message;
        }
        @Override
        public void run() {
            output.write(message);
            output.flush();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    tvMessages.append("client: " + message + "\n");
                    etMessage.setText("");
                }
            });
        }
    }
}

ステップ4:マニフェスト(androidManifest.xml)

クライアント側にも、インターネット通信のためのパーミッションを追加します。

<?xml version = "1.0" encoding = "utf-8"?>
<manifest xmlns:android = "https://schemas.android.com/apk/res/android"
    package = "com.client.myapplication.client">
    <uses-permission android:name = "android.permission.INTERNET" />
    <uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
    <application
        android:allowBackup = "true"
        android:icon = "@mipmap/ic_launcher"
        android:label = "@string/app_name"
        android:roundIcon = "@mipmap/ic_launcher_round"
        android:supportsRtl = "true"
        android:theme = "@style/AppTheme">
        <activity
            android:name = "com.client.myapplication.client.MainActivity"
            android:label = "Client">
            <intent-filter>
                <action android:name = "android.intent.action.MAIN" />
                <category android:name = "android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

アプリの実行と動作確認

それでは、サーバーアプリとクライアントアプリの両方を実行してみましょう。ここでは、実際のAndroidスマートフォンがパソコンに接続されているものとします。Android Studioからアプリを実行するには、プロジェクト内のいずれかのアクティビティファイルを開き、ツールバーの「Run」アイコンをクリックします。ドロップダウンから対象のモバイルデバイスを選択すると、端末の画面に初期画面が表示されます。

動作の流れは以下の通りです。

  1. 両端末を同じWi-Fiネットワークに接続します。
  2. サーバーアプリを起動すると、画面にIPアドレスとポート番号が表示されます。
  3. クライアントアプリにそのIPアドレスとポート番号を入力し、「Connect To Server」をタップします。
  4. 接続が確立されると、双方の画面に「Connected」と表示され、メッセージの送受信が可能になります。

サーバー画面(起動直後)

AndroidでWi-Fi接続を使ってファイルを転送する方法|サーバー・クライアント実装チュートリアル

クライアント画面

AndroidでWi-Fi接続を使ってファイルを転送する方法|サーバー・クライアント実装チュートリアル

サーバー画面(接続後・メッセージ送受信)

AndroidでWi-Fi接続を使ってファイルを転送する方法|サーバー・クライアント実装チュートリアル

まとめ

このように、ServerSocketとSocketを組み合わせたTCP通信を実装することで、Android端末同士をWi-Fi経由で直接つなぎ、メッセージやデータを送受信できます。本記事の仕組みをベースに、ファイルストリームの送受信処理を追加すれば、実際のファイル転送アプリへと発展させることが可能です。

  1. パソコンとAndroidスマホ間でファイルを転送する5つの方法【初心者向けガイド】

    最近Androidスマートフォンに機種変更した方や、新しいパソコンを購入した方は、いずれファイルのやり取りが必要になる場面に直面するでしょう。「やり方がわからない…」と不安になる必要はありません。この記事では、パソコンとAndroidスマホの間でファイルを転送するための基本的な方法を、初心者にもわかりやすく解説します。スマホとパソコンの間でファイルを移動したい理由はさまざまです。大切なデータをバックアップとしてパソコンに保存しておきたい、Androidスマホで編集したい画像ファイルをパソコンから受け取りたい――そんなときに役立つのがファイル転送です。ここでは、最もよく使われている代表的な方法を

  2. Windows 10の「近距離共有」機能でファイルをワイヤレス転送する方法

    MicrosoftのWindows 10 April 2018 Update(バージョン1803)には、実用的な新機能が数多く搭載されています。そのひとつが、AppleのAirDropに似た「近距離共有(Nearby Sharing)」です。この機能を使えば、近くにある他のデバイスとファイルやリンクをワイヤレスで共有できます。BluetoothとWi-Fiを利用して、ファイルやフォルダーを手軽に送受信することが可能です。 近距離共有を有効にする方法 この機能を利用するには、まず両方のデバイスでBluetoothまたはWi-Fiがオンになっている必要があります。加えて、設定画面から近距離共有機