古事連記帖

趣味のこと、技術的なこと、適当につらつら書きます。

Background TransferでTwitterに画像を投げよう

WP7でWeb上にデータを転送したい時、WebRequest使ったりReactiveOAuth使ったりして投げると思います。ただそれだと大きめなデータの転送中にアプリを閉じたりすると途中で止まってしまいます。
そこで登場するのがBackgroundTransfer。これを使うと、転送はOS側がその名前の通りバックグラウンドで転送してくれます。なのでアプリを閉じたりしても最後まで転送してくれます。


使い方は、MSDNの「How to: Implement Background File Transfers for Windows Phone」を見ると大体OKなのですが、このサンプルコードはサーバーからのデータダウンロードすることぐらいしか書いてなくて、アップロードについてはほとんど触れられていません。
そんなわけでアップロードの方法について簡単に書いてみることにします。


Twitterへ投稿するので、OAuth認証が必要となります。ReactiveOAuthを使います。(Rx的な使い方は今回しません)
以下のコードは、Sendメソッドを呼び出すと転送開始して終わったら勝手に終わる簡単なサンプル。

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Media.Imaging;
using Codeplex.OAuth;
using Microsoft.Phone.BackgroundTransfer;

namespace Sample
{
	public class PictTwitter: OAuthBase
	{
		// 適宜変えてネ
		private const string ConsumerKey = "ConsumerKey";
		private const string ConsumerSecret = "ConsumerSecret";
		private const string AccessTokenKey = "AccessTokenKey";
		private const string AccessTokenSecret = "AccessTokenSecret";

		private const string RequestUrl = "https://upload.twitter.com/1/statuses/update_with_media.xml";


		// コンストラクター
		public PictTwitterBlogCode()
			: base(ConsumerKey, ConsumerSecret)
		{ }

		/// <summary>
		/// Twitter に画像を送信します。
		/// </summary>
		/// <param name="message">メッセージ</param>
		/// <param name="image">画像</param>
		public void Send(string message, WriteableBitmap image)
		{
			if (image != null)
				return;

			var accessToken = new AccessToken(AccessTokenKey, AccessTokenSecret);
			// Multipart Form Dataの区切り文字。適当でOK
			var boundary = DateTime.Now.ToString("MMddHHmmss");

			var request = new BackgroundTransferRequest(new Uri(RequestUrl));
			// 認証ヘッダーを作る
			var authHeader = BuildAuthorizationHeader(
				new[] { new Parameter("Realm", "http://api.twitter.com/") }
				.Concat(ConstructBasicParameters(RequestUrl, MethodType.Post, accessToken)));
			// リクエストヘッダー
			request.Headers["Content-Type"] = "multipart/form-data; boundary=" + boundary;
			request.Headers["Authorization"] = authHeader;
			request.Method = "POST";
			
			// 端末がどういう状態であれば送信できるかを指定できる。
			// 既定では TransferPreferences.None (電源駆動でかつWi-Fi通信のときのみ) になっていて、
			// TransferPreferences.AllowBattery (電源かバッテリー駆動でWi-Fi通信のとき)
			// TransferPreferences.AllowCellular (電源駆動でWi-Fiや3G回線の通信ができるとき)
			// が他に選ぶことが出来る。下の場合はとりあえず通信できれば送信できるような設定。			
			request.TransferPreferences = TransferPreferences.AllowCellularAndBattery;

			// 実際の画像転送はファイルに固める
			var bodyDataFile = "/shared/transfers/" + boundary + ".pdata";
			var responseDataFile = "/shared/transfers/" + boundary + ".xml";

			// 転送データはDictionaryにしておく
			var parameter = new Dictionary<string, object>();
			parameter.Add("status", message);
			// 画像データはMemoryStreamに
			var ms = new MemoryStream();
			image.SaveJpeg(ms, image.PixelWidth, image.PixelHeight, 0, 100);
			parameter.Add("media[]", ms);

			var content = CreateMultipartFormData(parameter, boundary);

			using (var isolate = IsolatedStorageFile.GetUserStoreForApplication())
			{
				// 転送データは、分離ストレージの「/shared/transfers」にないとダメ
				if (!isolate.DirectoryExists("/shared"))
					isolate.CreateDirectory("/shared");
				if (!isolate.DirectoryExists("/shared/transfers"))
					isolate.CreateDirectory("/shared/transfers");

				// 転送データを保存
				using (var fs = new IsolatedStorageFileStream(bodyDataFile, FileMode.Create, isolate))
				{
					fs.Write(content, 0, content.Length);
					fs.Flush();
				}
			}

			// Tagプロパティにはいろいろな情報を組み込める。ここではboundaryだけ入れておく
			// xmlなどにしておけばたくさん情報入れられて便利
			request.Tag = boundary;

			// BackgroundTransferServiceに追加するとすぐさま転送がスタート
			try
			{
				BackgroundTransferService.Add(request);
				request.TransferStatusChanged += new EventHandler<BackgroundTransferEventArgs>(request_TransferStatusChanged);
				request.TransferProgressChanged += new EventHandler<BackgroundTransferEventArgs>(request_TransferProgressChanged);
			}
			catch (InvalidOperationException ex)
			{
				MessageBox.Show("Error: " + ex.Message);
			}
		}

		/// <summary>
		/// Multipart Form データのヘッダー情報を作成します。
		/// </summary>
		/// <param name="parameter"></param>
		/// <param name="boundary"></param>
		/// <returns></returns>
		public byte[] CreateMultipartFormData(Dictionary<string, object> parameter, string boundary)
		{
			var dataBytes = new List<byte>();

			foreach (var p in parameter)
			{
				dataBytes.AddRange(Encoding.UTF8.GetBytes("--" + boundary + "\r\n"));

				if (p.Value is Stream)
				{
					// 現在のキーの中身がStreamすなわち画像データ
					var ms = p.Value as MemoryStream;
					var sendData = new List<byte>();
					var data = string.Empty;

					// 画像データをバイト配列にしておく
					ms.Seek(0, SeekOrigin.Begin);
					var readBytes = 0;
					while (true)
					{
						var rb = new byte[1024];
						readBytes = ms.Read(rb, 0, 1024);
						if (readBytes <= 0)
							break;

						sendData.AddRange(rb);
					}

					data = data.Insert(data.Length,
								string.Format("Content-Disposition: file; name=\"{0}\"; filename=\"upload_{1}.jpg\"\r\n", p.Key, boundary));
					data = data.Insert(data.Length, "Content-Type: application/octet-stream\r\n");
					data = data.Insert(data.Length, "Content-Transfer-Encoding: binary\r\n\r\n");
					dataBytes.AddRange(Encoding.UTF8.GetBytes(data));
					dataBytes.AddRange(sendData);
					dataBytes.AddRange(Encoding.UTF8.GetBytes("\r\n"));
				}
				else
				{
					// その他テキストなパラメーター
					var data = string.Empty;
					var value = p.Value as string;

					data = data.Insert(data.Length,
						string.Format("Content-Disposition: form-data; name=\"{0}\"\r\n\r\n", p.Key));
					data = data.Insert(data.Length, value + "\r\n");

					dataBytes.AddRange(Encoding.UTF8.GetBytes(data));
				}
			}

			dataBytes.AddRange(Encoding.UTF8.GetBytes("--" + boundary + "--"));

			return dataBytes.ToArray();
		}

		void request_TransferProgressChanged(object sender, BackgroundTransferEventArgs e)
		{
			// 通信の進捗が更新されるとここが通知される
		}

		void request_TransferStatusChanged(object sender, BackgroundTransferEventArgs e)
		{
			// 通信のステータスが変わるとここが通知される
			switch (e.Request.TransferStatus)
			{
				case TransferStatus.Completed:
					// 転送が一応終わったらここになる
					if (e.Request.StatusCode == 200 || e.Request.StatusCode == 206)
					{
						// 完了コードがきていれば成功
						// 使ったファイルは削除
						using (var isolate = IsolatedStorageFile.GetUserStoreForApplication())
						{
							if (isolate.FileExists("/shared/transfers/" + e.Request.Tag + ".pdata"))
								isolate.DeleteFile("/shared/transfers/" + e.Request.Tag + ".pdata");
							if (isolate.FileExists("/shared/transfers/" + e.Request.Tag + ".xml"))
								isolate.DeleteFile("/shared/transfers/" + e.Request.Tag + ".xml");
						}
						// リクエストも削除
						BackgroundTransferService.Remove(e.Request);
					}
					else
					{
						// 転送が終わってもエラーコードが返ってきてたらそれは失敗
						BackgroundTransferService.Remove(e.Request);
					}
					break;
			}
		}
	}
}


こんな感じで、パラメーターはファイルとして分離ストレージに固めるようになっていて、よくあるやり方とはちょっと違うところはありますが、それ以外はそれほど変わりません。
このコードでは全てブラックボックス的にやってしまうので、例えばユーザーに進捗状況を表示したり、エラーがあったことを通知するなどをする場合は改変しなきゃいけません。
そのあたりはさきほどのMSDNドキュメントの通りでいけます。上のコードのSendメソッドをBackgroundTransferRequestを返り値にしてPage上でイベントを作ったりすればいいと思います。


BackgroundTransferを使うにあたって注意しなければならないのは以下の通りです。

  • アップロードできるファイルサイズは 5MBまで
  • BackgroundTransferServiceに登録できるRequest数は 5つまで
  • 3G回線やWi-Fi接続でのみ動作する
  • バッテリーセーバーが有効だと動作しない


複数のRequestを管理するときは、BackgroundTransferService.Requestsプロパティに今キューされているRequestのリストがでますので、それを持ってきてListBoxなどにいじいじしてあげればオーケイ。
RequestのTagプロパティを駆使すれば、例えば転送している画像の題名をListBoxに載せたりもできます。


僕が作ったアプリ「もばうぷ」は、この仕組みを使ってます。