flashair x androidアプリ開発ワークショップ コーディングパート

56

Click here to load reader

Upload: flashair

Post on 22-Jun-2015

8.467 views

Category:

Technology


8 download

DESCRIPTION

FlashAir x Androidアプリ開発ワークショップは、FlashAirと連携したAndroidアプリを作ることを目的に、FlashAirにAndroidデバイスから通信するための基本から、開発に役立つ機能や設定をご説明し、実際のコーディングを行うワークショップです。 この資料はコーディングパートで使用したスライドです。 【開催日】2013年9月18日 【講師】あんざいゆき 【主催】株式会社フィックスターズ FlashAirで使用可能なAPIの一覧や開発チュートリアル、Q&AフォーラムはFlashAirデベロッパーズにて公開しています。> https://flashair-developers.com

TRANSCRIPT

Page 1: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAir対応Androidアプリ開発

2013年9月18日あんざいゆき (株式会社ウフィカ)

Page 2: FlashAir x Androidアプリ開発ワークショップ コーディングパート

Steps

• FlashAirにアクセスする

• FlashAirに保存されているファイルの一覧を表示する

• FlashAirに保存されている画像ファイルのサムネイルを表示する

• FlashAirに保存されている画像ファイルをダウンロードする

Page 3: FlashAir x Androidアプリ開発ワークショップ コーディングパート

プロジェクトの用意

Page 4: FlashAir x Androidアプリ開発ワークショップ コーディングパート

• [File] - [New] - [Android Application Project]

• Application Name: FlashAirSample

• Project Name: FlashAirSample

• Package Name: com.example.flashairsample

• Mininum Required SDK: API 9

• 以降はすべてデフォルト設定

Page 5: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirにアクセスする

Page 6: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirにアクセスする(1)

• FlashAirにアクセスするには、AndroidデバイスがFlashAirのアクセスポイントに接続している必要があります

• 設定アプリのWiFi接続画面を開くメニューを用意しましょう

Page 7: FlashAir x Androidアプリ開発ワークショップ コーディングパート

res/menu/main.xml<menu xmlns:android="http://schemas.android.com/apk/res/android" >

<item android:id="@+id/action_wifi_settings" android:showAsAction="never" android:title="@string/action_wifi_settings"/>

</menu>

res/values/strings.xml<?xml version="1.0" encoding="utf-8"?><resources>

... <string name="action_wifi_settings">WiFi Settings</string> </resources>

Page 8: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java

public class MainActivity extends Activity {

...

@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; }

@Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); switch (itemId) { case R.id.action_wifi_settings: Intent intent = new Intent(Settings.ACTION_WIFI_SETTINGS); startActivity(intent); return true; } return super.onOptionsItemSelected(item); }}

Page 9: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirにアクセスする(2)

• FlashAirにはHTTPでアクセスするため、アプリにはInternetパーミッションが必要です

• デフォルト SSID: flashair_xxxxx

• デフォルト Pass: 12345678

Page 10: FlashAir x Androidアプリ開発ワークショップ コーディングパート

AndroidManifest.xml

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

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

...

</manifest>

Page 11: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirにアクセスする(3)• ライブラリプロジェクトを利用する

• FlashAirDev

• https://github.com/yanzm/FlashAirDev

• git clone https://github.com/yanzm/FlashAirDev.git

• [File] - [Import] - [Android] - [Existing Android Code Into Workspace]

• Select root directory:

• FlashAirDevフォルダを指定

Page 12: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirにアクセスする(4)

• FlashAirSampleのプロパティを開く

• [Android] - [Library] - [Add]

• FlashAirDev を選択

Page 13: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirにアクセスする(5)

• フォルダ内のファイル数を取得する

• http://flashair/command.cgi?op=101&DIR=[path]

• https://www.flashair-developers.com/ja/documents/api/commandcgi/#101

• レスポンスはファイル数(数字)

Page 14: FlashAir x Androidアプリ開発ワークショップ コーディングパート

res/layout/activity_main.xml<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" ... tools:context=".MainActivity" >

<TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" />

</RelativeLayout>

Page 15: FlashAir x Androidアプリ開発ワークショップ コーディングパート

res/menu/main.xml<menu xmlns:android="http://schemas.android.com/apk/res/android" >

<item android:id="@+id/action_reload" android:showAsAction="ifRoom" android:title="@string/action_reload"/> ...</menu>

res/values/strings.xml<?xml version="1.0" encoding="utf-8"?><resources>

... <string name="action_reload">Reload</string> <string name="image_count_format">%1$d images</string> </resources>

Page 16: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.javapublic class MainActivity extends Activity {

...

@Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); switch (itemId) { ... case R.id.action_reload: String dir = "/DCIM"; getFileCount(dir); return true; } return super.onOptionsItemSelected(item); }

...

}

Page 17: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java

public class MainActivity extends Activity {

...

private void getFileCount(final String dir) { new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... params) { return FlashAirUtils.getFileCount(dir); }

@Override protected void onPostExecute(Integer result) { TextView tv = (TextView) findViewById(R.id.textView1); tv.setText(getString(R.string.image_count_format, result)); } }.execute(); }

}

Page 18: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirUtils.java

public class FlashAirUtils {

public static final String BASE = "http://flashair/";

public static final String COMMAND = BASE + "command.cgi?"; public static final String FILE_COUNT = COMMAND + "op=101&DIR=";

public static int getFileCount(String dir) { try { String result = Utils.accessToFlashAir(FILE_COUNT + dir); return Integer.parseInt(result); } catch (NumberFormatException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }

return -1; }

...}

ファイル数取得

Page 19: FlashAir x Androidアプリ開発ワークショップ コーディングパート

Utils.java

public class Utils {

public static String accessToFlashAir(String uri) throws IOException { URL url = new URL(uri); HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();

String result = null; try { InputStream in = new BufferedInputStream(urlConnection.getInputStream()); result = inputStreamToString(in); in.close(); } finally { urlConnection.disconnect(); }

return result; }

...}

HTTPアクセス用

Page 20: FlashAir x Androidアプリ開発ワークショップ コーディングパート

Utils.java

public class Utils { ...

private static String inputStreamToString(InputStream stream) throws IOException { Reader reader = new InputStreamReader(stream, "UTF-8"); StringBuilder sb = new StringBuilder(); char[] buffer = new char[1024]; int num; while (0 < (num = reader.read(buffer))) { sb.append(buffer, 0, num); } return sb.toString(); }

...}

HTTPアクセス用

Page 21: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirに保存されているファイルの一覧を表示する

Page 22: FlashAir x Androidアプリ開発ワークショップ コーディングパート

ファイルの一覧を表示する(1)

• DCIMフォルダ内のファイル一覧を取得する

• http://flashair/command.cgi?op=100&DIR=[path]

• https://www.flashair-developers.com/ja/documents/api/commandcgi/#100

• レスポンスは<ディレクトリ>,<ファイル名>,<ファイルサイズ>,<属性>,<日付>,<時間>

• 例)/DCIM,100__TSB,0,16,9944,129

• ファイル名やディレクトリ名に,が入ってる場合もありうる!

Page 23: FlashAir x Androidアプリ開発ワークショップ コーディングパート

ファイルの一覧を表示する(2)

• ファイルサイズはバイト単位

• 属性は16ビット整数の10進数表記

• ビット5 : アーカイブ

• ビット4 : ディレクトリ

• ビット3 : ボリューム

• ビット2 : システムファイル

• ビット1 : 隠しファイル

• ビット0 : 読み取り専用

Page 24: FlashAir x Androidアプリ開発ワークショップ コーディングパート

ファイルの一覧を表示する(3)

• 日付は16ビット整数の10進数表記

• ビット15-9 : 1980年を0とした値

• ビット8-5 : 月(1~12)

• ビット4-0 : 日(1~31)

• 時刻は16ビット整数の10進数表記

• ビット15-11 : 時

• ビット10-5 : 分

• ビット4-0 : 秒/2

Page 25: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirFileInfo.java

public class FlashAirFileInfo {

public FlashAirFileInfo(String info, String dir) { int start; int end;

start = info.lastIndexOf(","); int time = Integer.parseInt(info.substring(start + 1).trim());

end = start; start = info.lastIndexOf(",", end - 1); int date = Integer.parseInt(info.substring(start + 1, end).trim());

end = start; start = info.lastIndexOf(",", end - 1); mAttribute = Integer.parseInt(info.substring(start + 1, end).trim());

end = start; start = info.lastIndexOf(",", end - 1); mSize = info.substring(start + 1, end);

end = start; start = info.indexOf(",", dir.length()); mFileName = info.substring(start + 1, end);...

ファイル情報用のクラス

Page 26: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirFileInfo.java

... mDir = dir;

int year = ((date >> 9) & 0x0000007f) + 1980; int month = (date >> 5) & 0x0000000f - 1; int day = (date) & 0x0000001f;

int hourOfDay = (time >> 11) & 0x0000001f; int minute = (time >> 5) & 0x0000003f; int second = ((time) & 0x0000001f) * 2;

mCalendar = Calendar.getInstance(); mCalendar.set(year, month, day, hourOfDay, minute, second); }

public String mDir; public String mFileName; public String mSize; public int mAttribute; public Calendar mCalendar;...

ファイル情報用のクラス

Page 27: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirFileInfo.java

... public static final int ATTR_MASK_ARCHIVE = 0x00000020; public static final int ATTR_MASK_DIRECTORY = 0x00000010; public static final int ATTR_MASK_VOLUME = 0x00000008; public static final int ATTR_MASK_SYSTEM_FILE = 0x00000004; public static final int ATTR_MASK_HIDDEN_FILE = 0x00000002; public static final int ATTR_MASK_READ_ONLY = 0x00000001;

public boolean isDirectory() { return (mAttribute & ATTR_MASK_DIRECTORY) > 0; }

@Override public String toString() { return "DIR=" + mDir + " FILENAME=" + mFileName + " SIZE=" + mSize + " ATTRIBUTE=" + mAttribute + " DATE=" + DateFormat.format("yyyy-MM-dd kk:mm:ss", mCalendar); }}

ファイル情報用のクラス

Page 28: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirUtils.javapublic class FlashAirUtils { ...

public static List<FlashAirFileInfo> getFileList(String dir) { try { String result = Utils.accessToFlashAir(FILE_LIST + dir); if (TextUtils.isEmpty(result)) { return null; }

ArrayList<FlashAirFileInfo> list = new ArrayList<FlashAirFileInfo>(); for (String line : result.split("\n")) { if (TextUtils.isEmpty(line)) { continue; } if (line.split(",").length < 6) { continue; } FlashAirFileInfo info = new FlashAirFileInfo(line, dir); list.add(info); } return list;...

ファイル情報取得

Page 29: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirUtils.java

...

} catch (IOException e) { e.printStackTrace(); }

return null; }

ファイル情報取得

Page 30: FlashAir x Androidアプリ開発ワークショップ コーディングパート

res/layout/activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" ... tools:context=".MainActivity" >

<TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/hello_world" />

<ListView android:id="@+id/listView1" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/textView1" />

</RelativeLayout>

一覧用ListView追加

Page 31: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.javapublic class MainActivity extends Activity {

...

@Override public boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); switch (itemId) { ... case R.id.action_reload: String dir = "/DCIM"; getFileCount(dir); getFileList(dir); return true; } return super.onOptionsItemSelected(item); }

...

}

ファイル一覧取得

Page 32: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.javapublic class MainActivity extends Activity { ...

private void getFileList(final String dir) { new AsyncTask<Void, Void, List<FlashAirFileInfo>>() { @Override protected List<FlashAirFileInfo> doInBackground(Void... params) { return FlashAirUtils.getFileList(dir); }

@Override protected void onPostExecute(List<FlashAirFileInfo> result) { ListView lv = (ListView) findViewById(R.id.listView1); lv.setAdapter(new FileListAdapter(MainActivity.this, result)); } }.execute(); }

public class FileListAdapter extends ArrayAdapter<FlashAirFileInfo> {

public FileListAdapter(Context context, List<FlashAirFileInfo> data) { super(context, android.R.layout.simple_list_item_1, data); } }}

ファイル一覧をListViewにセット

Page 33: FlashAir x Androidアプリ開発ワークショップ コーディングパート
Page 34: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirに保存されている画像ファイルのサムネイル

を表示する

Page 35: FlashAir x Androidアプリ開発ワークショップ コーディングパート

サムネイルを表示する(1)

• 画像ファイルのサムネイルを取得する

• http://flashair/thumbnail.cgi?[path]

• 例)http://flashair/thumbnail.cgi?/DCIM/IMG_xxx.jpg

• https://www.flashair-developers.com/ja/documents/api/thumbnailcgi/

• EXIF規格で定められているサムネイル画像

• JPEG(image/jpeg)形式

• JPEGでない場合、EXIF規格で定められたサムネイル画像がない場合、404 Not Found が返る

Page 36: FlashAir x Androidアプリ開発ワークショップ コーディングパート

サムネイルを表示する(2)

• ListViewにサムネイルを表示する

• Volleyを利用する

• Androidのネットワーク処理用のライブラリ

• https://android.googlesource.com/platform/frameworks/volley/

• http://y-anz-m.blogspot.jp/2013/05/google-io-2013-android-volley-easy-fast.html

• 通信処理が組み込まれた ImageView である NetworkImageView が用意されている

Page 37: FlashAir x Androidアプリ開発ワークショップ コーディングパート

サムネイルを表示する(3)

• VolleyのNetworkImagView

• <com.android.volley.toolbox.NetworkImageView>

• setImageUrl(String url, ImageLoader loader)

Page 38: FlashAir x Androidアプリ開発ワークショップ コーディングパート

• Volleyはライブラリプロジェクト

• git clone https://android.googlesource.com/platform/frameworks/volley

• [File] - [Import] - [Android] - [Existing Android Code Into Workspace]

• Select root directory:

• volleyフォルダを指定

サムネイルを表示する(4)

Page 39: FlashAir x Androidアプリ開発ワークショップ コーディングパート

• FlashAirSampleのプロパティを開く

• [Android] - [Library] - [Add]

• volley を選択

サムネイルを表示する(5)

Volleyが選択肢に出てこない場合は、VolleyプロジェクトのIs Libraryにチェックが付いてることを確認

Page 40: FlashAir x Androidアプリ開発ワークショップ コーディングパート

res/layout/list_row.xml

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" >

<com.android.volley.toolbox.NetworkImageView android:id="@+id/imageView1" android:layout_width="100dp" android:layout_height="80dp" android:src="@drawable/ic_launcher" />

<TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Medium Text" android:textAppearance="?android:attr/textAppearanceMedium" />

</LinearLayout>

リスト用レイアウト

Page 41: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java

public class MainActivity extends Activity {

private RequestQueue mQueue; private ImageLoader mImageLoader;

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mQueue = Volley.newRequestQueue(getApplicationContext()); mImageLoader = new ImageLoader(mQueue, new BitmapCache()); }...

Volleyの準備

Page 42: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java... public class BitmapCache implements ImageCache {

private LruCache<String, Bitmap> mCache;

public BitmapCache() { int maxSize = 5 * 1024 * 1024; // 5MB mCache = new LruCache<String, Bitmap>(maxSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes() * value.getHeight(); } }; }

@Override public Bitmap getBitmap(String url) { return mCache.get(url); }

@Override public void putBitmap(String url, Bitmap bitmap) { mCache.put(url, bitmap); } }

Volleyの準備

Page 43: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java

public class MainActivity extends Activity { ...

public class FileListAdapter extends ArrayAdapter<FlashAirFileInfo> {

LayoutInflater mInflater;

public FileListAdapter(Context context, List<FlashAirFileInfo> data) { super(context, 0, data); mInflater = LayoutInflater.from(context); }

@Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mInflater.inflate(R.layout.list_row, parent, false); }

FlashAirFileInfo item = getItem(position);

TextView tv = (TextView) convertView.findViewById(R.id.textView1); tv.setText(item.mFileName);

...

リスト用Adapterを拡張

Page 44: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java...

NetworkImageView niv = (NetworkImageView) convertView .findViewById(R.id.imageView1);

if (item.mFileName.endsWith(".jpg") || item.mFileName.endsWith(".jpeg")) { niv.setImageUrl( FlashAirUtils.getThumbnailUrl(item.mDir, item.mFileName), mImageLoader); } else { niv.setImageUrl(null, mImageLoader); } return convertView; } }}

NetworkImageViewにURLをセット

Page 45: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirUtils.javapublic class FlashAirUtils {

public static final String BASE = "http://flashair/";

public static final String THUMBNAIL = BASE + "thumbnail.cgi?";

public static String getThumbnailUrl(String dir, String fileName) { return THUMBNAIL + dir + "/" + fileName; }

...}

Page 46: FlashAir x Androidアプリ開発ワークショップ コーディングパート
Page 47: FlashAir x Androidアプリ開発ワークショップ コーディングパート

FlashAirに保存されている画像ファイルをダウンロードする

Page 49: FlashAir x Androidアプリ開発ワークショップ コーディングパート

画像をダウンロードする(2)

• DownloadManager

• getSystemService(Context.DOWNLOAD_SERVICE) でインスタンスを取得

• Request request = new DownloadManager.Request(uri) でダウンロードリクエストを作成

• downloadManager.enqueue(request) でダウンロードリクエストを追加

• SDカードにダウンロードする場合はパーミッションが必要

Page 50: FlashAir x Androidアプリ開発ワークショップ コーディングパート

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

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

...

</manifest>

Page 51: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java

public class MainActivity extends Activity implements OnItemClickListener { ...

private void getFileList(final String dir) { new AsyncTask<Void, Void, List<FlashAirFileInfo>>() { @Override protected List<FlashAirFileInfo> doInBackground(Void... params) { return FlashAirUtils.getFileList(dir); }

@Override protected void onPostExecute(List<FlashAirFileInfo> result) { ListView lv = (ListView) findViewById(R.id.listView1); lv.setAdapter(new FileListAdapter(MainActivity.this, result)); lv.setOnItemClickListener(MainActivity.this); } }.execute(); }...

リストにリスナーをセット

Page 52: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java

public class MainActivity extends Activity implements OnItemClickListener { ...

@Override public void onItemClick(AdapterView<?> adapter, View v, int position, long l) {

FlashAirFileInfo info = (FlashAirFileInfo) adapter .getItemAtPosition(position);

File path = Environment .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); File file = new File(path, info.mFileName); if (!file.exists()) { startDownload(info); return; }

openDownloadedFile(file.toString()); }...

リストにリスナーをセット

Page 53: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java...

private void openDownloadedFile(String filePath) { MediaScannerConnection.scanFile(this, new String[] { filePath }, null, new MediaScannerConnection.OnScanCompletedListener() { public void onScanCompleted(String path, Uri uri) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(uri); startActivity(intent); } }); }

private void startDownload(FlashAirFileInfo info) { Uri uri = FlashAirUtils.getFileUri(info.mDir, info.mFileName); DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); DownloadManager.Request request = new DownloadManager.Request(uri); request.allowScanningByMediaScanner(); request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DCIM, info.mFileName); manager.enqueue(request); }

...

Page 54: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java

...

@Override protected void onResume() { super.onResume(); IntentFilter filter = new IntentFilter( DownloadManager.ACTION_DOWNLOAD_COMPLETE); registerReceiver(receiver, filter); }

@Override protected void onPause() { super.onPause(); unregisterReceiver(receiver); }

...

Page 55: FlashAir x Androidアプリ開発ワークショップ コーディングパート

MainActivity.java

...

BroadcastReceiver receiver = new BroadcastReceiver() {

@Override public void onReceive(Context context, Intent intent) { long id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1); if (id > 0) { DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); Uri fileUri = manager.getUriForDownloadedFile(id); openDownloadedFile(fileUri.getPath()); } } };}

Page 56: FlashAir x Androidアプリ開発ワークショップ コーディングパート