2011年10月25日火曜日

進捗その3

ううむ、中々進みません。。。 取りあえず現在のところこんな感じ。

1.接続画面

まず接続先登録で、接続先のPCのアドレス、ポートを登録します。 すると、真ん中に登録したものが出るので、それをタップすると接続が開始されます。 接続先PCに別途作成したサーバプログラムを起動しておく必要があります。

2.接続先PCの表示

2.1.ファイルリスト表示

PCに接続すると、サーバ側で設定したデフォルトディレクトリが表示されます。 対応する画像ファイルをタップすると、アプリ内のビューワで画像が閲覧できます。 ちなみに上部のパスを直接編集して移動もできます。

2.2.圧縮ファイルの表示

対応する圧縮ファイル(ZIP/RAR)をタップすると、中身のファイルが閲覧できます。 ここで、2.1.と同じように画像ファイルをタップすると、そのまま画像が閲覧可能です。

ちなみに、自作した専用サーバを使う理由はこの為です。 sambaとかcifsとか扱うJavaのjcifsってライブラリを使えば、ファイルリスト表示とかダウンロード/アップロードは可能ですが、 圧縮ファイルを一旦ローカルにダウンロードしないと閲覧することができません。 ZipFileクラスのコンストラクタが、Fileクラスとかローカルパスしか受け付けないのでしょうがないです。 ので作成したサーバプログラムでは、この辺の処理をサーバ側で行って、その結果を返却してくれます。

2.3.ファイルリストのフィルタ

左上のPCっぽいアイコンをタップすると、虫眼鏡マークに切り替わるので ここで文字を入力して、右のボタンをタップすると、ファイルリスト内の要素をフィルタリングできます。

2.4.ファイルメニュー

リストの要素を長押しでメニューが表示され、他のアプリで開くことができます。 候補に出るアプリは、 1.接続画面でチラッと見えた設定に、拡張子とコンテントタイプの対応設定があるので そっから候補が選出されます。 他のアプリで開く場合、接続先PCのファイルは、アプリ用のディレクトリに一時ファイルとしてダウンロードしてから 開かれるのでPDFとかも開くことが可能です。

3.ローカル(Android)内の表示

右下にあるAndroidっぽいボタンと、PCっぽいボタンで 接続先PCの表示とAndroidの表示を切り替えることができます。 操作感は2.接続先PC と同じ感じです。

4.ローカルと接続先PC

4.1.デュアル表示

右下のアイコンの切り替え方によっては、 左右にローカルと接続先PCを表示することができます。

4.2.アップロード/ダウンロード

デュアル表示されている場合、メニューにアップロード/ダウンロードがでてきます。 これを選択すると、現在表示されているディレクトリに選択したファイルがコピーされます。

5.音楽ファイル再生

ファイルリストの音楽ファイルをタップすると、音楽ファイルが再生されます。自前で作成したプレーヤーです。 このとき、同ディレクトリ内の他の音楽ファイルも一緒にプレイリストに入り、連続再生されます。

取りあえずこんな感じになってます。ネットワーク先のPCのZIP/RARを直で閲覧できるので、結構使えてます。 ユーザ認証とか暗号化とかしていないので超ノーガードですが、まぁ、ローカル内で使うにはいいか、って感じになってるので後回し。

2011年10月2日日曜日

EditableなSpinner

1つ前の投稿で作成した拡張子とContent-TypeのDBを 利用するためにこんな↓感じの画面を加えました。  

ここから、新しく追加、変更を設定するダイアログで、 直接入力が可能なSpinnerを作ってみようと思いました。

で、できたのがコレ↓


ちょっと画像ではわかりにくいですね。
作成方法はSpinnerに設定するアダプタに対して

作成時、EditTextのレイアウトを食わせる
setDropDownViewResourceでテキストのレイアウトを設定する

な感じの操作をする。
具体的にはこんな感じ。

    //Spinnerの設定
    Spinner spn = (Spinner)inputView.findViewById(id.spinnerType);

    //Spinnerに設定するアダプタのレイアウトをEditTextのレイアウトで作成する
    //指定している [dialog_spinner_layout]はホントにEditTextだけのレイアウト
    ArrayAdapter adp = ArrayAdapter.createFromResource(parentAct.getApplicationContext(),
            R.array.MimeTypeArray,R.layout.dialog_spinner_layout);

    //ドロップダウンのレイアウトをTextView?のレイアウトで作成する
    // android.R.layout.simple_dropdown_item_1line はandroid側で定義済みのレイアウト
    // 普通にTextViewを指定するとドロップダウンで表示されたものがはみ出ることがある
    adp.setDropDownViewResource(android.R.layout.simple_dropdown_item_1line);

    spn.setAdapter(adp);

取りあえず一応できました。
が、細かいところの設定が色々難しい。

初期値として、EditTextの文字列を変更させようかと思ったのですが、どうやっても初期化時に、EditTextが取得できない。
表示後なら、
spinner.getSelectedView();
でEditTextを取得できるのですが。。。

やっぱり素直にAutoCompleteTextViewクラスで作成するべき、というのが今回の結論でした。

2011年9月22日木曜日

暗黙のインテントとContent-Type

色々と躓きながら作成しています。
今回は、暗黙のインテントを飛ばす時に 拡張子でContent-Typeを色々指定する必要があるのですが、 ifとかで判別して一々やるのはめんどいなぁってことで、 ちょっとしたDBとクラスを作成してみました。 拡張子とContent-Typeを紐付けしたDBを作成して、 暗黙のインテントを飛ばす時にDBからContent-Typeを取得してやろうという試みです。
まずは拡張子とContent-Typeを紐付けたDBのソースです。

public class ContentTypeDB {
 /**
  * DBの要素
  * _EXT:拡張子
  * _TYPE:content-type文字列
  */
 public interface DataColumns extends BaseColumns {
  public static final String DISPLAY_NAME ="_display_name";
  public static final String _EXT = "_EXTENSION";
  public static final String _TYPE = "_TYPE";
 }
 
 private SQLiteDatabase contentTypeDB;
 private static final String DATABASE_TABLE = "IntentParamTable";
 
 private static class DatabaseHelper extends SQLiteOpenHelper {
  private static final String DATABASE_NAME = "IntentParamList";
  private static final int DATABASE_VERSION =1;
  //初期化判別用
  private boolean notInitialize = false;
  
  private static final String DATABASE_CREATE = 
   "create table " + 
   DATABASE_TABLE + " (" +
   DataColumns._EXT + " text primary key, " +
   DataColumns._TYPE + " text not null" +
   ");";
   
  public DatabaseHelper(Context context) {
   super(context, DATABASE_NAME, null, DATABASE_VERSION);
  }
  
  @Override
  public void onCreate(SQLiteDatabase db) {
   db.execSQL(DATABASE_CREATE);
   //DBがない場合、trueにしておく
   notInitialize = true;
  }
  
  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
   db.execSQL("DROP TABLE IF EXIST IntentParamList");
   onCreate(db);
  }
  
  /**
   * DBの初期化を行う
   * @param content
   */
  public void initialize(ContentTypeDB content){
   if(notInitialize){
    //DBを作成した場合、デフォルトのContent-Typeを設定する
    ContentTypeHelper.setDefaultContentType(content);
   }
  }
 }

 /**
  * コンストラクタ
  * @param context
  */
 public ContentTypeDB(Context context){
  DatabaseHelper dbHelper = new DatabaseHelper(context);
  contentTypeDB = dbHelper.getWritableDatabase();
  //DBの初期化を試みる
  dbHelper.initialize(this);
 }
 
 /**
  * 指定要素を削除する
  * @param ipAddress
  * @return
  */
 public int delete(ContentElement elem){
  int count = contentTypeDB.delete(DATABASE_TABLE,
    DataColumns._EXT + " = '" + elem.getExtension()+"'",null);
  return count;
 }
 
 /**
  * 要素を追加する
  * @param element
  * @return
  */
 public int registerElement(ContentElement element){
  if(!existElement(element.getExtension())){
   insert(element);
  }
  return 0;
 }
 
 /**
  * DB内の要素を全て取得する
  * @return
  */
 public ArrayList getAllElements(){
  ArrayList elements = new ArrayList();
  Cursor c = query(null, null, null, DataColumns._EXT + " asc");
  if(c.moveToFirst()){
   do {
    elements.add(new ContentElement(
      c.getString(c.getColumnIndex(DataColumns._EXT)),
      c.getString(c.getColumnIndex(DataColumns._TYPE))
      ));
   } while(c.moveToNext());
  }
  return elements;
 }
 
 public void close(){
  contentTypeDB.close();
 }
 
 public Cursor query(String[] projection, String selection,
   String[] selectionArgs, String sortOrder) {
  SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder();
  sqlBuilder.setTables(DATABASE_TABLE);
  
  if(sortOrder == null || sortOrder == ""){
   sortOrder = DataColumns._EXT;
  }
  
  Cursor c = sqlBuilder.query(contentTypeDB, projection, selection,
    selectionArgs, null, null, sortOrder);
  return c;
 }
 
 /**
  * DBを拡張子で検索し、Content-Type文字列を取得する
  * @param extension 検索対象の拡張子文字列
  * @return 見つかったContent-Type文字列。DBにない場合nullが返却される。
  */
 public String getType(String extension){
  Cursor c = query(null, DataColumns._EXT+"= \""+extension+"\"", null, null);
  String retType = null;
  if(c.moveToFirst()){
   retType = c.getString(c.getColumnIndex(DataColumns._TYPE));
  }
  return retType;
 }
 
 /**
  * トランザクション開始
  */
 public void startTransaction(){
  contentTypeDB.beginTransaction();
 }
 
 /**
  * コミット
  */
 public void commit(){
  contentTypeDB.setTransactionSuccessful();
 }
 
 /**
  * トランザクション終了
  */
 public void endTransaction(){
  contentTypeDB.endTransaction();
 }

 /**
  * DB内に指定要素が存在するか確認する
  * @param extension
  * @return
  */
 private boolean existElement(String extension){
  boolean exist = false;
  String [] targetCol = {DataColumns._EXT};
  Cursor c = query(targetCol, 
    DataColumns._EXT +"= \""+ extension+"\"", null, null);
  if(c.moveToFirst()){
   exist = true;
  }
  c.close();
  return exist;
 }

 private long insert(ContentElement element){
  long rowID = contentTypeDB.insert(DATABASE_TABLE, "", element.getContentValues());
  if(rowID>0){
   return rowID;
  }
  throw new SQLException();
 }
}


このContentTypeDBクラスのインスタンスを作成することでDBを作成します。 ここで、DBのCreate時にContentTypeHelperクラスを使ってデフォルトの値を DBに突っ込んでます。
ContentTypeHelperクラスのソースはこちらです。

public class ContentTypeHelper {
 //拡張子とContent-Typeのデフォルト値
 static final String[][] defaultTypeArray = {
  {"application/envoy", "evy"},
  {"application/fractals", "fif"},
  {"application/futuresplash", "spl"},
  {"application/hta", "hta"},
  {"application/internet-property-stream", "acx"},
  {"application/mac-binhex40", "hqx"},
  {"application/msword", "doc"},
  {"application/msword", "dot"},
  {"application/octet-stream", "bin"},
  {"application/octet-stream", "class"},
  {"application/octet-stream", "dms"},
  {"application/octet-stream", "exe"},
  {"application/octet-stream", "lha"},
  {"application/octet-stream", "lzh"},
  {"application/oda", "oda"},
  {"application/olescript", "axs"},
  {"application/pdf", "pdf"},
  {"application/pics-rules", "prf"},
  {"application/pkcs10", "p10"},
  {"application/pkix-crl", "crl"},
  {"application/postscript", "ai"},
  {"application/postscript", "eps"},
  {"application/postscript", "ps"},
  {"application/rtf", "rtf"},
  {"application/set-payment-initiation", "setpay"},
  {"application/set-registration-initiation", "setreg"},
  {"application/vnd.ms-excel", "xla"},
  {"application/vnd.ms-excel", "xlc"},
  {"application/vnd.ms-excel", "xlm"},
  {"application/vnd.ms-excel", "xls"},
  {"application/vnd.ms-excel", "xlt"},
  {"application/vnd.ms-excel", "xlw"},
  {"application/vnd.ms-outlook", "msg"},
  {"application/vnd.ms-pkicertstore", "sst"},
  {"application/vnd.ms-pkiseccat", "cat"},
  {"application/vnd.ms-pkistl", "stl"},
  {"application/vnd.ms-powerpoint", "pot"},
  {"application/vnd.ms-powerpoint", "pps"},
  {"application/vnd.ms-powerpoint", "ppt"},
  {"application/vnd.ms-project", "mpp"},
  {"application/vnd.ms-works", "wcm"},
  {"application/vnd.ms-works", "wdb"},
  {"application/vnd.ms-works", "wks"},
  {"application/vnd.ms-works", "wps"},
  {"application/winhlp", "hlp"},
  {"application/x-bcpio", "bcpio"},
  {"application/x-cdf", "cdf"},
  {"application/x-compress", "z"},
  {"application/x-compressed", "tgz"},
  {"application/x-cpio", "cpio"},
  {"application/x-csh", "csh"},
  {"application/x-director", "dcr"},
  {"application/x-director", "dir"},
  {"application/x-director", "dxr"},
  {"application/x-dvi", "dvi"},
  {"application/x-gtar", "gtar"},
  {"application/x-gzip", "gz"},
  {"application/x-hdf", "hdf"},
  {"application/x-internet-signup", "ins"},
  {"application/x-internet-signup", "isp"},
  {"application/x-iphone", "iii"},
  {"application/x-javascript", "js"},
  {"application/x-latex", "latex"},
  {"application/x-msaccess", "mdb"},
  {"application/x-mscardfile", "crd"},
  {"application/x-msclip", "clp"},
  {"application/x-msdownload", "dll"},
  {"application/x-msmediaview", "m13"},
  {"application/x-msmediaview", "m14"},
  {"application/x-msmediaview", "mvb"},
  {"application/x-msmetafile", "wmf"},
  {"application/x-msmoney", "mny"},
  {"application/x-mspublisher", "pub"},
  {"application/x-msschedule", "scd"},
  {"application/x-msterminal", "trm"},
  {"application/x-mswrite", "wri"},
  {"application/x-netcdf", "cdf"},
  {"application/x-netcdf", "nc"},
  {"application/x-perfmon", "pma"},
  {"application/x-perfmon", "pmc"},
  {"application/x-perfmon", "pml"},
  {"application/x-perfmon", "pmr"},
  {"application/x-perfmon", "pmw"},
  {"application/x-pkcs12", "p12"},
  {"application/x-pkcs12", "pfx"},
  {"application/x-pkcs7-certificates", "p7b"},
  {"application/x-pkcs7-certificates", "spc"},
  {"application/x-pkcs7-certreqresp", "p7r"},
  {"application/x-pkcs7-mime", "p7c"},
  {"application/x-pkcs7-mime", "p7m"},
  {"application/x-pkcs7-signature", "p7s"},
  {"application/x-sh", "sh"},
  {"application/x-shar", "shar"},
  {"application/x-shockwave-flash", "swf"},
  {"application/x-stuffit", "sit"},
  {"application/x-sv4cpio", "sv4cpio"},
  {"application/x-sv4crc", "sv4crc"},
  {"application/x-tar", "tar"},
  {"application/x-tcl", "tcl"},
  {"application/x-tex", "tex"},
  {"application/x-texinfo", "texi"},
  {"application/x-texinfo", "texinfo"},
  {"application/x-troff", "roff"},
  {"application/x-troff", "t"},
  {"application/x-troff", "tr"},
  {"application/x-troff-man", "man"},
  {"application/x-troff-me", "me"},
  {"application/x-troff-ms", "ms"},
  {"application/x-ustar", "ustar"},
  {"application/x-wais-source", "src"},
  {"application/x-x509-ca-cert", "cer"},
  {"application/x-x509-ca-cert", "crt"},
  {"application/x-x509-ca-cert", "der"},
  {"application/ynd.ms-pkipko", "pko"},
  {"application/zip", "zip"},
  {"audio/basic", "au"},
  {"audio/basic", "snd"},
  {"audio/mid", "mid"},
  {"audio/mid", "rmi"},
  {"audio/mpeg", "mp3"},
  {"audio/x-aiff", "aif"},
  {"audio/x-aiff", "aifc"},
  {"audio/x-aiff", "aiff"},
  {"audio/x-mpegurl", "m3u"},
  {"audio/x-pn-realaudio", "ra"},
  {"audio/x-pn-realaudio", "ram"},
  {"audio/x-wav", "wav"},
  {"image/bmp", "bmp"},
  {"image/cis-cod", "cod"},
  {"image/gif", "gif"},
  {"image/ief", "ief"},
  {"image/jpeg", "jpe"},
  {"image/jpeg", "jpeg"},
  {"image/jpeg", "jpg"},
  {"image/pipeg", "jfif"},
  {"image/svg+xml", "svg"},
  {"image/tiff", "tif"},
  {"image/tiff", "tiff"},
  {"image/x-cmu-raster", "ras"},
  {"image/x-cmx", "cmx"},
  {"image/x-icon", "ico"},
  {"image/x-portable-anymap", "pnm"},
  {"image/x-portable-bitmap", "pbm"},
  {"image/x-portable-graymap", "pgm"},
  {"image/x-portable-pixmap", "ppm"},
  {"image/x-rgb", "rgb"},
  {"image/x-xbitmap", "xbm"},
  {"image/x-xpixmap", "xpm"},
  {"image/x-xwindowdump", "xwd"},
  {"message/rfc822", "mht"},
  {"message/rfc822", "mhtml"},
  {"message/rfc822", "nws"},
  {"text/css", "css"},
  {"text/h323", "323"},
  {"text/html", "htm"},
  {"text/html", "html"},
  {"text/html", "stm"},
  {"text/iuls", "uls"},
  {"text/plain", "bas"},
  {"text/plain", "c"},
  {"text/plain", "h"},
  {"text/plain", "txt"},
  {"text/richtext", "rtx"},
  {"text/scriptlet", "sct"},
  {"text/tab-separated-values", "tsv"},
  {"text/webviewhtml", "htt"},
  {"text/x-component", "htc"},
  {"text/x-setext", "etx"},
  {"text/x-vcard", "vcf"},
  {"video/mpeg", "mp2"},
  {"video/mpeg", "mpa"},
  {"video/mpeg", "mpe"},
  {"video/mpeg", "mpeg"},
  {"video/mpeg", "mpg"},
  {"video/mpeg", "mpv2"},
  {"video/quicktime", "mov"},
  {"video/quicktime", "qt"},
  {"video/x-la-asf", "lsf"},
  {"video/x-la-asf", "lsx"},
  {"video/x-ms-asf", "asf"},
  {"video/x-ms-asf", "asr"},
  {"video/x-ms-asf", "asx"},
  {"video/x-msvideo", "avi"},
  {"video/x-sgi-movie", "movie"},
  {"x-world/x-vrml", "flr"},
  {"x-world/x-vrml", "vrml"},
  {"x-world/x-vrml", "wrl"},
  {"x-world/x-vrml", "wrz"},
  {"x-world/x-vrml", "xaf"},
  {"x-world/x-vrml", "xof"}
 };
 
 /**
  * DBにデフォルト値をセットする
  * @param db
  */
 public static void setDefaultContentType(ContentTypeDB db){
  ContentElement element;
  
  db.startTransaction();
  for(int i=0; i< defaultTypeArray.length; i++){
   element = new ContentElement(defaultTypeArray[i][1], defaultTypeArray[i][0]);
   db.registerElement(element);
  }
  db.commit();
  db.endTransaction();
 }
}


こんな感じ。デフォルトの配列を作成するのがちょっと面倒ですがしょうがないです。 これだけできれば後はこいつを使ってインテントを投げましょう。
今回は暗黙のインテントを投げる用のクラスを作成しています。 ソースは以下。

/**
 * 暗黙のインテント呼び出し用クラス
 *
 */
public class IntentHelper {
 /**
  * @param target 受け渡すファイル
  * @param act 呼び出し元アクティビティ
  */
 static public void callIntentByFile(File target, Activity act){
  String fileExtension = getExtension(target.getName());
  ContentTypeDB db = new ContentTypeDB(act.getApplicationContext());
  String contentType = db.getType(fileExtension);
  db.close();
  if(contentType!=null){
   callIntent(act,target, getActionType(fileExtension), contentType);
  }
 }
 
 /**
  * 拡張子取得メソッド
  * @param path
  * @return
  */
 static protected String getExtension(String path){
  int pos = path.lastIndexOf(".");
  String ext = "";
  if(pos != -1){
   pos++;
   ext = path.substring(pos);
  }
  return ext;
 }
 
 /**
  * 取りあえず今はACTION_VIEW固定
  */
 static private String getActionType(String fileExtension){
  return Intent.ACTION_VIEW;
 }
 
 static private void callIntent(Activity calleeAct,File file,String action,String contentType){
  if(file==null||action==null||contentType==null){
   return;
  }
  Intent intent = new Intent();
  intent.setAction(action);
  intent.setDataAndType(Uri.fromFile(file),contentType);
  calleeAct.startActivity(intent);
 }
}



IntentHelperクラスのcallIntentByFileメソッドに受け渡すファイルと、自アクティビティを指定して 呼び出せばアラ不思議、多分候補のアプリ選択ダイアログが出てくるはずです。はずです。
後は独自に拡張子を増やしたりなんだりしてやればいいはず。そんな感じ。

10/29 追記

初期化時のデフォルト値設定でかなり時間がかかってましたが、 transactionを追加することで解消できました。 ソースのContentTypeDbクラスとContentTypeHelperクラスが変更されています。 データベース関係には今まで触れたことがあまりなかったからな。。。 ちょっとは勉強しとかないと。

2011年9月14日水曜日

イメージをグレースケールにしたりカラーに戻したり

今回はイメージボタンの画像を動的にグレースケールにしたりカラーに戻したりと いうようなことをやってみました。 使用するクラスは、ColorMatrixColorFilterクラス。
ColorMatrixクラスのインスタンスメソッドのsetSaturationで HSV色空間のS(彩度)を0に設定したマトリクスを作成します。
そのマトリクスを使ってColorMatrixColorFilterを生成することで、 グレースケール用のフィルタを作成しています。
後は、イベントをトリガーに対象のイメージボタンの ColorFilterに設定したり、nullで取り除いたりすることで、 動的にグレースケールとの切り替えが行えます。

以下適当なサンプルコード


    private ColorMatrixColorFilter grayScaleFilter;
    private ImageButton imgBtn;

    private void initialize(){
        imgBtn = (ImageButton) findViewById(id.imageButtonA);
        ColorMatrix matrix = new ColorMatrix();
        matrix.setSaturation(0);
        grayScaleFilter = new ColorMatrixColorFilter(matrix);
    }

    public void changeGrayScaleMode(){
        imgBtn.setColorFilter(grayScaleFilter);
    }
    public void changeColorMode(){
        imgBtn.setColorFilter(null);
    }


追記
厳密に言うと彩度を0にするのと、グレースケールとは違うのだけどまぁいいか。

2011年9月3日土曜日

進捗~ その2

作成してるもののメモその2です。 前回から半月しますが、なかなか進みませんね。 現在は下のような感じになってます。
左右で、接続先PCのファイルと、ローカルのファイルが表示されるようになってます。 あと、ローカルファイルの画像閲覧、ZIP/RARファイル内の閲覧とかができるようにもなってます。
音楽ファイルの再生時に表示される画面です。 ダウンロード最中でも再生されるようになっています。 時間がnullになっていますが気にしないでください。1秒後にはきちんと表示されます。
一応ファイル転送機能とかもつけてる最中ですが、GUIて難しいですね。 あとは、ファイルリスト画面の表示/非表示機能をつけて、転送中の画面つけて、 ダイアログ出したり送信状況出したり、 うーん、まだまだですね。

2011年8月29日月曜日

MediaPlayerでStreaming的な再生 その3

前回、4つの方法を試そう、とありましたが、
最初に試した方法でなかなかな感じに仕上がりましたので
2以降は試していません。
さて、1の方法について書きます。


RandomAccessFileを使用して、ダウンロードファイルサイズのファイルを作成、書き込みを行う


一番最初に試した方法では、ファイルへの書き込み自体はうまくいってましたが、
肝心の再生が、setDataSourceを行った時点までしか再生されない結果になっていました。
ここで、考えたのが、setした時点のファイル情報から、シーク可能な範囲が決定されているのではないのか、ということです。
そのため、前のエントリでの1と4の方法を考えたわけです。

で、この方法では、RandomAccessFileクラスを利用して、あらかじめ音楽ファイルのサイズのファイルを作成して、順次書き込んでいこうという方法です。
色々とややこしいですが
以下に必要な分のソースを置いておきます。

public class StreamingPlayer implements OnCompletionListener {

    private RandomAccessFile tempFile;
    private MediaPlayer player;
    private long fileSize;
    private long bufferdSize;
    private long musicLeng;
    private ScheduledExecutorService scheduled;
    private ScheduledFuture schFuture = null;
    private PlayerState state;
    private boolean isWatching = false;

    //独自のConnectionManagerクラス
    //sendMusicObjectメソッドを使うことで、対象のサーバに接続、
    //送信が行えます。そういうものです。
    private ConnectionManager manager;

    private final int THREAD_WATCH_INTERVAL = 1;

    //現在のプレイヤーの状態
    static private enum PlayerState{
        STOP,        //停止中
        PLAYING,    //再生中
        PAUSE,        //一時停止中
        BUFFERING,    //バッファ待ち
    }

    public StreamingPlayer(ConnectionManager manager) {
        this.manager = manager;
        player = new MediaPlayer();
        player.setOnCompletionListener(this);
        scheduled = Executors.newSingleThreadScheduledExecutor();
    }

    /**
     * 再生準備
     * writeObjectを使って独自のファイル情報を持つオブジェクトを送信しています。
     * 送信後、指定のファイルのバイナリが返却されます。
     * OrigFileInfoクラスのgetNameメソッドはファイル名、
     * getExtensionメソッドは拡張子を、
     * getSizeメソッドはファイルのサイズが取得できます。
     */
    public void prepareMusic(OrigFileInfo musicFile) {
        try {
            //新規のデータソースを入れるため、
            //再生中のものを止めて、resetをかけています。
            player.stop();
            player.reset();
            //キャッシュディレクトリに新規一時ファイル作成
            tempFile = new RandomAccessFile(File.createTempFile(
                    musicFile.getName(), musicFile.getExtension(),
                    getCacheDir()), "rw");
            //ファイルサイズの設定
            tempFile.setLength(musicFile.getSize());
            tempFile.seek(0);
            
            //バッファ済みサイズの初期化
            bufferdSize = 0;
            fileSize = musicFile.getSize();
            //プレイヤー状態の更新
            state = PlayerState.STOP;
            //オブジェクトの送信
            manager.sendMusicObject(this, musicFile);
            if(isWatching){
                schFuture.cancel(true);
            }
            //ダウンロード状態監視のためのスケジューラ
            //1秒ごとに実行させる
            schFuture = scheduled.scheduleAtFixedRate(new Watcher(), 0,THREAD_WATCH_INTERVAL, TimeUnit.SECONDS);
            isWatching = true;
            return;
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
    }

    /**
     * 一時停止操作のためのメソッド
     */
    public void pause() {
        if (state==PlayerState.PLAYING) {
            state = PlayerState.PAUSE;
            player.pause();
        }
        else if(state==PlayerState.PAUSE){
            state = PlayerState.PLAYING;
            player.start();
            if(!isWatching){
                schFuture = scheduled.scheduleAtFixedRate(new Watcher(), 0,THREAD_WATCH_INTERVAL, TimeUnit.SECONDS);
                isWatching = true;
            }
        }
    }

    /**
     * ストップ操作のためのメソッド
     */
    public void stop() {
        if (state!=PlayerState.STOP) {
            state = PlayerState.STOP;
            player.pause();
            player.seekTo(0);
        }
    }
    
    /**
     * シーク操作のためのメソッド
     */
    public void seekTo(int seekToPercent) {
        int downRate = getBufferRate();
        //ダウンロード済みよりもシークが超えないようにする
        //今回、seekのMAXは1000になってます
        if(seekToPercent < downRate) {
            int dur = player.getDuration();
            player.seekTo((int)(dur * ((double)seekToPercent / 1000)));
        }
    }

    /**
     * バッファ取得時の初回時にだけ呼ぶメソッド
     * ※少なくとも音楽ファイルのヘッダ情報を全て含むまで
     *   取得してから呼ぶ必要があります
     * @param buf 取得バッファのバイト配列
     */
    public void startMusicBuffer(byte[] buf) {
        try {
            //ファイル書き込み
            tempFile.write(buf);
            //バッファ済みサイズ更新
            bufferdSize = buf.length;
            //データソースに設定
            player.setDataSource(tempFile.getFD());
            player.prepare();
            //なくてもいいかも
            player.setAudioStreamType(AudioManager.STREAM_MUSIC);
            //音楽ファイルの長さ(ミリ秒数)取得
            musicLeng = player.getDuration();
            //プレイヤーの状態をバッファリング中に変更
            state = PlayerState.BUFFERING;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 2回目以降のバッファ取得時に呼ぶメソッド
     */
    public void receiveBuffer(byte[] buf) {
        try {
            //書き込み位置にシーク
            tempFile.seek(bufferdSize);
            //ファイル書き込み
            tempFile.write(buf);
            tempFile.getFD().sync();
            //バッファ済みサイズ更新
            bufferdSize += buf.length;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 再生するために十分なファイル書き込みが行われているか判定する。
     * ファイルサイズとバッファ済みサイズ
     * MediaPlayerの再生位置と音楽ファイルの秒数から判定する
     * 計算式には自身なし。
     */
    private boolean isEnoughBuffer() {
        //全てダウンロード完了済み
        if(fileSize <= bufferdSize){
            return true;
        }
        
        int downRate = getBufferRate();
        int playRate = getPlayedRate();
        //10秒の割合を算出
        int tenSecPer = (int)(( 1000 / (double) musicLeng) * 10);
        if (1000 < tenSecPer) {
            tenSecPer = 1000;
        }
        else if (tenSecPer <= 0) {
            tenSecPer = 1;
        }

        //再生位置の割合と、ダウンロード済みサイズの割合の差が
        //閾値以下の場合、falseを返却
        if ((downRate - playRate) < tenSecPer) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * ダウンロード済みのサイズとファイルの全体の長さから、割合を計算(MAX 1000)
     */
    private int getBufferRate() {
        if(fileSize==0){
            return 0;
        }
        return (int) (((double) bufferdSize / (double) fileSize) * 1000);
    }

    /**
     * 再生位置と全体の秒数から、現在の割合を計算(MAX 1000)
     */
    private int getPlayedRate() {
        if(musicLeng==0){
            return 1000;
        }
        return (int) (((double) player.getCurrentPosition() / (double) musicLeng) * 1000);
    }

    /**
     * 再生終了時
     */
    @Override
    public void onCompletion(MediaPlayer mp) {
        if(state==PlayerState.PLAYING){
            state = PlayerState.STOP;
            //監視スケジューラを停止
            schFuture.cancel(true);
            isWatching = false;
        }
        else if(isWatching) {
            //監視スケジューラを停止
            schFuture.cancel(true);
            isWatching = false;
        }
    }

    /**
     * ダウンロード進捗監視タイマ
     */
    class Watcher implements Runnable {
        public Watcher() {

        }

        @Override
        public void run() {
            if(state==PlayerState.PLAYING){
                //プレイヤーが再生中
                if (!isEnoughBuffer()) {
                    //バッファが足りないため一時停止
                    player.pause();
                    state = PlayerState.BUFFERING;
                }
            } else if(state==PlayerState.BUFFERING){
                //バッファリングで一時停止中
                if (isEnoughBuffer()){
                    //バッファが足りている場合、再開する
                    player.start();
                    state = PlayerState.PLAYING;
                }
            }
            else {
                //その他(PAUSE/STOP)
                int buffRate = getBufferRate();
                if(buffRate==1000 && isWatching){
                    //ダウンロード完了している場合、監視を終了する
                    schFuture.cancel(true);
                    isWatching = false;
                }
            }
        }
    }
}




プレイヤー部分はこんな感じ。
通信部分に関しては力尽きました省略させていただきます。
最初にprepareMusic()で再生準備を行って
定期的に溜まったbyteデータを
初回時は、startMusicBufferメソッドを、
2回目以降は、receiveBufferメソッドを使うだけです
ただし、prepareMusic()を呼ぶときに
あらかじめファイルのサイズを知る必要があります。
通信部で、intのファイルサイズを送ってから
バイナリ送信を始めるなりしてなんとかしてください。


実装したコードの一部分を引っ張ってきているため、
このままじゃ動かないかもです。

12/17 追記

android2.x系ではうまく動かない。。。 3.xや4.0では動作するみたいだけど何が悪いのかわからん。。。

MediaPlayerでStreaming的な再生 その2

作戦その1が見事に失敗に終わりました。

めげずに次に行きます。


2.ファイルに追加書き込み時にデータソースを読み直す

今度は、ここを参考にして、

・一時ファイルを作成
・先頭のある程度のバイトのバッファを書き込み
・MediaPlayerで再生
・通信部である程度のバッファがたまったら、以下の手順で追加し、再生を続ける
pause() → カレントポジション保存 → 一時ファイルにバッファを追加書き込み → reset() → setDataSource() → prepare() → seekTo() → start()

という感じを試してみました。
が、バッファの書き込みタイミング毎で音がプチプチ飛ぶため、
とても聞けたものにはなりませんでした。

失敗です。。。



さて、どうしたもんかと、色々考えた結果
今度は次の4つを試してみようということになりました。

1RandomAccessFileを使用して、ダウンロードファイルサイズのファイルを作成、書き込みを行う
2AudioTrackを使用してのStreaming再生
3MediaPlayerを2つ使用してのダブルバッファリング的に音飛びを解消できないか試す
4C++ソースをいじって、ファイルシークの最大値を更新できるようにする

が、結果的には1を試した時点でいい感じになったので、2~4は試してません。
次から1について書いていきます。

その3へ続きます。

MediaPlayerでStreaming的な再生 その1

作成中のアプリですが、
現在一応音楽ファイルを再生できる、と書いてましたが、
PC側からファイル全体をダウンロード完了しなければ
再生が始まりませんでした。
で、今回なんとかダウンロード途中で再生ができるように
かなり強引にやってみました。


Androidで音楽ファイルは
MediaPlayerを使うことで簡単に再生することができます。
さて、このMediaPlayerでは、

・ファイルパス
・ファイルディスクリプタ(javaのクラス)
・URI

がsetDataSourceで入力として使えます。
どのバージョンのAndroidからかは忘れましたが、
3番目のURIで、入力値として、
httpで音楽ファイルを指定すれば
ストリーミング再生が可能みたいです。試してないですが。

今回、ダウンロード先が自作の独自のJavaで作成しているサーバのため、HTTPプロトコルではなく、
SocketのgetInputStream/getOutputStreamから
ObjectInputStream/ObjectOutputStreamを生成して、通信を行っています。

そのため、
while ((c = inStream.read(buff)) != -1) {
os.write(buff,0,c);
os.flush();
}

のようにして、読み込みを行ってます。
HTTPリクエストに対してレスポンスを返すようにサーバサイドを作り変えれば簡単かもしれませんが、他のディレクトリ構造の情報とか画像とかをやり取りするため、
サーバサイドはObjectStream系のreadObject()で読み込み待ちをしています。
そのため、setDataSourceでサーバのアドレスをHTTPプロトコルで入力してやると、例外が吐かれてしまいます。

ので、上のようなbyte読み込みで、Streaming的に再生ができないか色々と試行錯誤しました。



1.setDataSourceで指定したファイルに対して、追加で書き込みを行う

まず最初に考えたのが上の方法です。
これは、
File.createTempFileで一時ファイルを作成。
FileOutputStreamで音楽ファイルの先頭のある程度のバイト数を書き込む。
FileInputStreamで同ファイルを開き、setDataSource(inputStream.getFD())で指定して、再生を開始。
通信で追加のデータを受信する毎にこのファイルに対して書き込みを行えば再生されないかなー?
ということです。
setDataSource()では、ファイルロックがかかってないので、書き込み自体は、可能でしたが、肝心の音楽の再生がsetDataSource()時点で書き込まれているところまでしか、再生してくれませんでした。

失敗です。。。

続きます。

2011年8月22日月曜日

進捗~

取りあえず色々いじってる最中です。

今のところこんな感じに追加されてます。

・パスの直接入力でフォルダにジャンプ
 (PCのパスツリーを覚えてないと意味ない)
・ついでにフォルダの更新ボタン追加
・最上部のアイコンが通信時にくるくる回るプログレスバーになる
・フォルダ/画像/ZIPアーカイブ/その他データ に(暫定)アイコン追加
 (適当につけてるのでその内変えないと・・・)
・ファイルのサイズと最終更新時が出るように
・画像表示で、原寸表示時にタッチでスクロールできる

フォルダのリスト表示はこんな感じです。


アイコンどうしようかなぁ・・・。

2011年8月21日日曜日

ObjectStreamでcode71

作成中のアプリの内部構造をいじってると、
java.io.StreamCorruptedException: invalid type code: 71
な例外が発生するようになってました。

スタックトレースを見ると、

java.io.StreamCorruptedException: invalid type code: 71
at java.io.ObjectInputStream.readString(Unknown Source)
at java.io.ObjectInputStream.readEnum(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.defaultReadFields(Unknown Source)
at java.io.ObjectInputStream.readSerialData(Unknown Source)
at java.io.ObjectInputStream.readOrdinaryObject(Unknown Source)
at java.io.ObjectInputStream.readObject0(Unknown Source)
at java.io.ObjectInputStream.readObject(Unknown Source)

で、どうやらEnum周り?でエラーが起こってるようです。

結果的には、Enumの識別子名が被ってたため
デシリアライズで問題が起こったようです。

定義を見直して、Enumの識別子の名前を変更したところ
問題は発生しなくなりました。

めでたしめでたし。

2011年8月18日木曜日

ImageViewでスクロール

Androidアプリの画像閲覧ソフトで、
画像が大きいと、ドラッグでスクロールとか、
フリックで慣性っぽいスクロールとかよくあるので
できるかやってみました。

参考サイト様はこちら


使うのは、Scrollerというクラスです。ScrollViewは使いません。
コードは結構適当なので注意してください。

public class ScrollImageView extends ImageView implements OnGestureListener,OnDoubleTapListener{
  private GestureDetector gesDetector;
  private Scroller viewScroller = null;
  //ビューの最大/最小 X座標
  private int _limitX;
  //ビューの最大/最小 Y座標
  private int _limitY;
  //画像の幅
  private int _imageWidth;
  //画像の高さ
  private int _imageHeight;

  public ScrollImageView(Context context,AttributeSet attrs) {
    super(context,attrs);
    this.setClickable(true);
    //Scrollerの生成
    //DecelerateInterpolatorで、徐々に減速する動作になる
    viewScroller = new Scroller(getContext(),new DecelerateInterpolator());
    gesDetector = new GestureDetector(this.getContext(),this);
    gesDetector.setIsLongpressEnabled(true);
    gesDetector.setOnDoubleTapListener(this);
    this.setOnTouchListener(new View.OnTouchListener() {
      @Override
      public boolean onTouch(View v, MotionEvent event) {
        if(gesDetector.onTouchEvent(event)){
          return true;
        }
        return false;
      }
    });
  }

  //画像の大きさを記録するため、setImageBitmapをオーバーライドする
  @Override
  public void setImageBitmap(Bitmap image){
    //画像を設定する際に、幅と高さを記憶しておく
    _imageWidth = image.getWidth();
    _imageHeight = image.getHeight();
    super.setImageBitmap(image);
  }

  @Override
  public void computeScroll() {
    //現在のスクロール位置をシミュレートする?
    if ( viewScroller.computeScrollOffset() ) {
      //スクロール位置を取得
      int setX = viewScroller.getCurrX();
      int setY = viewScroller.getCurrY();

      if(setX > _limitX){
        //右端の座標を越えていないかチェック
        setX = _limitX;
      }
      else if(setX < -_limitX){
        //左端の座標を越えていないかチェック
        setX = -_limitX;
      }
      if(setY > _limitY){
        //下端の座標を越えていないかチェック
        setY = _limitY;
      }
      else if(setY < -_limitY){
        //上端の座標を越えていないかチェック
        setY = - _limitY;
      }
      //setX,setYの絶対座標へスクロールする
      scrollTo(setX, setY);
    }
  }

  @Override
  public boolean onDown(MotionEvent e) {
    //スクリーンダウン時に境界値を再計算する
    //機体の向き変更とかに対応するため、だが、
    //タッチ時に一々再計算が入るので、別の位置に入れたい・・・
    refreshLimit();
    return false;
  }

  @Override
  public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    //フリック時の挙動
    int currentX = getScrollX();
    int currentY = getScrollY();
    int targetX = 0;
    int targetY = 0;

    targetX = currentX - (int)velocityX / 2;
    targetY = currentY - (int)velocityY / 2;
    viewScroller.startScroll(currentX, currentY, targetX - currentX, targetY - currentY,500);
    invalidate();
    return true;
  }

  @Override
  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    //ドラッグ時のスクロール
    //移動距離取得
    int setX = (int)distanceX;
    int setY = (int)distanceY;
    //移動後の座標位置を計算
    int afterX = this.getScrollX() + setX;
    int afterY = this.getScrollY() + setY;

    //はみ出てないかチェック
    if(afterX > _limitX || afterX < -_limitX){
      //はみ出す場合、移動距離を0にする
      setX = 0;
    }
    if(afterY > _limitY || afterY < -_limitY){
      setY = 0;
    }
    //ビューのスクロール(相対距離指定)
    scrollBy(setX, setY);
    return true;
  }

  //座標の限界値を計算する
  private void refreshLimit(){

    //画像サイズから、移動範囲を計算する
    //ScaleTypeがCENTERだからか、画像の中心が(0,0)のため、
    //X座標は、画像の幅/2の数値が右端、-(画像の幅/2)の数値が左端となる
    //Y座標も同様
    //また、ビュー自体のサイズも関係するため、
    //ビューが移動できる座標の限界値は、
    // (画像幅 - ビュー幅) /2 になる
    if(_imageWidth < getWidth()){
    _limitX = 0;
    }
    else{
      _limitX = (_imageWidth - getWidth())/2;
    }
    if(_imageHeight < getHeight()){
      _limitY = 0;
    }
    else{
      _limitY = (_imageHeight - getHeight())/2;
    }
  }

}

こんな感じです。 今作成中のやつから必要なところを抜粋してきたので、 このままだと動作しないかもしれません。

次は慣性スクロール中にドラッグしたときに 慣性スクロールを止めるやり方を調べます。

8/21追記

スクロールの中断は、上記参考サイト様に載ってましたね。 viewScroller.forceFinished(true); で止められます。

onDownにのrefreshLimit()の前にforceFinished(true) を追加でストップできました。

11/06追記

持ってる実機がAsusのタブレットなのでandroid3.0向けにコードを書いているんですが、 ためしにandroid2.1のエミュレータで実行してみたところ、画像が表示されないことがわかりました。 3.0や4.0のエミュレータだと表示されるんですが。。。うーん、原因がわかりません。

12/19追記

やっと原因がわかりました。 というか、上に挙げてるコードだと問題ないですね。 抜粋元(自分で実装した)のコード内にあった、onMeasureをオーバーライドしていた箇所で 変なことをやってたみたいです。。。なんでこんなことやってたんだろ? アホの子か俺は。

2011年8月16日火曜日

取りあえず



作成中のアプリの記録を残しておこうと思います。

取りあえず、今のところは、

フォルダ階層の移動


Windowsの論理ドライブ表示


画像表示とフリックでの次の画像へ移動


ZIPファイル内のファイル表示(UTF8で固めていないと化けます)と画像ファイルの表示
MP3とZIP内のMP3の再生(するだけ。。。)

が可能な感じ。
バグ多し。でかい画像読むとOutOfMemory吐かれます。。。

先は長い。

2011年8月15日月曜日

ObjectInputStreamの進捗を知りたい

早速メモ1つ目。。。

Socketでの通信で、ダウンロードの進捗をプログレスで出したい時の方法です。

読み込みのループ処理で、読み込みバイト数をカウントする方法もありますが、
ObjectInputStreamのreadObjectでは使えないので。。。


結論から言うと、CountingInputStreamを使用します。
CountingInputStreamは、org.apache.commons.ioパッケージにありますので、
この辺から入手してください。
http://commons.apache.org/io/

使い方は簡単です。
例えば、ObjectInputStreamの読み込み状況を知りたいときは、以下のように書きます。

private CountingInputStream cis = null;
.
.
.
public void readObjectBySocket(InetAddress address, int port){
  try{
    Socket sock = new Socket(address,port);
    cis = new CountingInputStream(sock.getInputStream());
    ObjectInputStream inStream = new ObjectInputStream(cis);
    receiveObject = inStream.readObject();
 
    inStream.close();
    cis.close();
    sock.close();
  }
  catch(Exception e){
    e.printStackTrace();
  }
}

//cisからダウンロード完了したバイトサイズを取得するには、getByteCountを使用します。
public long getCount(){
  if(cis==null){
    return 0;
  }
  return cis.getByteCount();
}


こんな感じ。例外とかは適当です。
ただし、readObject()で受信完了待ちになるので、
別スレッドなどから
cisにアクセスしましょう。
3.0からは、通信処理は、AsyncTaskでやる必要があるので、ちょうどいいです。

多分これで取得できるはず。。。

久々に更新

久々にメモ。

6月末に ASUS Eee Pad Transformer TF101 を購入しました。
ので、今Androidのアプリ側のプログラムをセコセコ楽しんでます。

作っているのは、
画像再生+音楽再生機能を持ったアプリですが、
デスクトップPC側に自作のJavaサーバプログラムを仕込んで、
PC側に繋げて、ネットワーク経由でPCのファイルをワサワサと閲覧しよう。
というアプリです。
Androidの中のファイルは見れませんが。

というわけで、現在、Android側と、Java側のプログラムを
モサモサと作成中です。


ちょくちょく詰まるので、メモを残そうと思います。