2013年5月8日水曜日

プレビューのPDF書き出しでバイト数を削減しようとQuartzフィルタを設定しても一覧に表示されなくて困ったら

スキャンしたデータを、Macのアプリ「プレビュー」でPDFへ書きだすと、めちゃめちゃバイト数が高くなる。

Quartzフィルタの「Reduce File Size」を指定すると、もの凄くバイト数が削減されるのだが、粗くなりすぎて、人様に送れるレベルではない(^_^;)

そこで、Reduce File Sizeの「アラアラ」な設定をカスタマイズして、「アラ」程度に調整みようと、ColorSyncユーティリティでフィルタの複製を試みた。



でも、・・・出ない。
PDF書き出しのQuartzフィルタのリストに複製したフィルタ名が表示されない。



調べたら、ColorSyncユーティリティで書き出されるパス「$HOME/Library/Filters/」下のQuartzフィルタは、プレビューでは表示されないとのこと。

独自のPDFのQuartzフィルタを追加する - ザリガニが見ていた...。


「/Library/Filters/」下だったら、プレビューでも表示されるようになるようだ。

つまり、「/System/Library/Filters/」下にある「Reduce File Size.qfilter」をコピーして「/Library/Filters/」下へ配置し、そのファイルをカスタマイズすればよい。

そのためには、rootになって/Library/へアクセスできるようにしないと。



ということで、rootになれるように、ターミナルを開いてrootのパスワードを設定。
$sudo passwd root
Password:
Changing password for root.
New password:
Retype new password:

その後、rootへsu。
$ su -
Password:

/Library/にFiltersディレクトリを作り、Reduce File Size のフィルタファイルをコピーしてくる。ファイル名は末尾に_customizeをつけるなり、適当に。このファイル名がプレビューでフィルタ名として表示されるわけではないので。

root# mkdir /Library/Filters
root# cp /System/Library/Filters/Reduce\ File\ Size.qfilter /Library/Filters/Reduce\ File\ Size_customize.qfilter

コピーできたら、自分好みの設定にカスタマイズ編集。viなんかで。

自分が変更したのは3点。

  1. ImageScaleFactorの値を「0.5」から「1.0」へ
  2. ImageSizeMaxの値を「512」から「1024」へ
  3. Nameの値を「Reduce File Size」から「Reduce File Size(customize)」へ

2点目のImageSizeMaxの値が特にバイト数へ影響を与える。大きければ大きいほどバイト数は高くなる。1点目のImageScaleFactorを1.0へ設定したので、このフィルタを使うと倍率1.0の1024のサイズへ画像をスケールすることになる。

3点目で設定した名前は、Quartzフィルタ名としてプレビューで表示される。



今度はちゃんと出た〜(・∀・)




OS 10.9.2(Mavericks)のプレビューでの注意点
追記 2014.03.28

・「PDFで書き出し」
・「印刷」→「PDF」→「PDFとして保存」
これらではQuartzフィルタが選べないようです。

「書き出す」→フォーマット「PDF」選択と進むと、Quartzフィルタの欄が表示されます。

2013年3月14日木曜日

MediaProviderのgetType()でIllegalStateExceptionが発生して困ったらmimeTypeの指定を確認すべし

画像をダウンロードした際に、そのメタ情報をMediaProviderへ登録するために、MediaScannerConnectionを使っている。
MediaScannerConnection.scanFile(context,
          new String[] { uri.getPath() },
          new String[] { "image/*" },
          new MediaScannerConnection.OnScanCompletedListener() {
            public void onScanCompleted(String path, Uri uri) {
              // Notificationを表示する
              notifyDownloadComplete(context, path, uri, sms);
            }
          });

ところが、MediaProviderのgetType()でIllegalStateExceptionが発生して、Activity Managerがクラッシュしてしまうことが多くて困った。




原因は、MediaScannerConnectionのscanFile()へmimeTypeを指定しているところ。
new String[] { "image/*" },

mimeType判断として、"image/*"と指定して、「何らかの画像」と加えたのが、逆にmimeType判断に時間をかけさせてしまい、IllegalStateException発生の温床にしてしまっていたらしい。

mimeTypeが特定できない場合にスキャンさせる時は、mimeTypeを指定せず、nullを与えて、拡張子から自動的に判断させるようにする方が好ましいようだ。


よって、mimeTypeの指定をnullへ変更。
MediaScannerConnection.scanFile(context,
          new String[] { uri.getPath() },
          null,
          new MediaScannerConnection.OnScanCompletedListener() {
            public void onScanCompleted(String path, Uri uri) {
              // Notificationを表示する
              notifyDownloadComplete(context, path, uri, sms);
            }
          });


面積や、JPEG、PNGなど種類を変えて、様々な画像のダウンロードを試してみたが、変更後は、IllegalStateExceptionは全く発生しなくなった(^。^)

2013年3月4日月曜日

MacでAndroidデバッグ用証明書を生成するときにターミナルの文字化けを防ぐには

Macでkeytoolコマンドを使ってAndroidデバッグ用の証明書を生成しようとしたら、メッセージが文字化けして何が何だか…(´・ω・`)



コマンド出力時のエンコードがUTF-8ではなく、SJISになっていることが原因。Appleが配布しているMac用のJavaの問題と思われ。


文字化けを解消するには、環境変数_JAVA_OPTIONSを指定してUTF-8のエンコードを指定する。

$_JAVA_OPTIONS='-Dfile.encoding=UTF-8' keytool -genkey -v -keystore .android/CUSTOM_debug.keystore -alias androiddebugkey -keyalg RSA -validity 10000 -dname "CN=Android Debug, O=Android,C=JP"
CUSTOM_debug.keystoreは任意の名前に置き換え





生成済みの証明書を確認する際も、文字化けを防ぐためにオプションを指定する。

$_JAVA_OPTIONS='-Dfile.encoding=UTF-8' keytool -exportcert -alias androiddebugkey -keystore .android/CUSTOM_debug.keystore -list -v

パスワードを入力すると、以下のように生成済みの証明書が表示される。





複数の開発マシーンでデバッグキーを共有するとちょっと便利

ちなみに、作成した.keystoreファイルを複数の開発マシーンで共有すると、同じデバイス宛へアプリを上書きインストールする際にも便利。

通常、同じデバイスに複数の開発マシーンからデバッグビルドでアプリを上書きインストールすると、開発マシーン毎に証明書が異なるので、アプリのアップデートができず、一度アプリをアンインストールしなければならない。

デバッグ用証明書を共有しておくと、アンインストールせずにアプリのアップデートができる。


Mac OS Xの場合、Eclipseのメニューから、「環境設定」→「Android」→「Build」→「Custom Debug keystore」に共通のデバッグ用証明書を設定すればOK。

2013年2月6日水曜日

Eclipseで設定したXMLのインデント幅がAndroidManifest.xmlに反映されないなと思ったら

Eclipseでxmlのインデントを半角スペース2つ分と設定している。



その設定をAndroidで編集するAndroidManifest.xmlなどのxmlにも反映するためには、
「Android > Editors > Use Eclipse setting for indentation width and space or tab character indentation(Android default is 4 space characters)」のチェックが必要。



ちょっと困ったのでメモ。

2013年2月4日月曜日

minSdkVersion16以上ではGCMBroadcastReceiverのintent-filterにcategory指定が要らない

4.1以上の端末と、それより下のAPIレベルの端末で、GCMサーバーからメッセージを受信した際のインテントの内容が異なることに気がついた。


4.1(APIレベル16)以上
02-04 13:25:37.552: V/GCMIntentService.onMessage:70(11555): Intent { act=com.google.android.c2dm.intent.RECEIVE flg=0x10 pkg=my.app.package cmp=my.app.package/.GCMIntentService (has extras) }


4.0.3(APIレベル15)

02-04 13:46:33.894: V/GCMIntentService.onMessage:70(380): Intent { act=com.google.android.c2dm.intent.RECEIVE cat=[my.app.package] cmp=my.app.package/.GCMIntentService (has extras) }


16以上だと、インテント内にカテゴリー指定がなく、パッケージに変わっている。

GCMのドキュメントにも、minSdkVersionを16以上に指定した場合は、マニフェストに記述するcom.google.android.gcm.GCMBroadcastReceiverのintent-filterで、categoryのname値にパッケージ名を指定する必要がないとある。

Notice that android:name in the category tag must be replaced by your application's package name (and the category tag is not required for applications targeted to minSdkVersion 16 and higher).


メッセージが送られる端末に応じて、インテントにカテゴリーを含めるのか、パッケージ名を含めるのか、振り分けているようだ。


minSDKVersionを16以上にすると、さらに、permission.C2D_MESSAGEの要求も要らなくなるし、マニフェストがスッキリするね。

DownloadManagerのDOWNLOAD_COMPLETEアクションに対応するBroadcastReceiverを書いていてふと疑問に思ったこと

DownloadManagerのDOWNLOAD_COMPLETEアクションに対応するBroadcastReceiverを書いていて、ふと疑問。

DownloadManagerはシステムレベルで提供されているので、DOWNLOAD_COMPLETEアクションが発生した場合、自分のアプリだけにかかわらず、対応するアプリ全てにアクションがブロードキャストされるのではないかと。

そうだとしたら、フィルタリングする何らかの処理を書いておかないと、他のアプリが叩いたDownloadManagerのDOWNLOAD_COMPLETEアクションに対しても、自分のアプリのBroadcastReceiverが起動してしまうのではないかと。


試しに、DOWNLOAD_COMPLETEアクションに対応するBroadcastReceiverの記述を持つアプリをコピーして、それぞれのアプリをインストール。DOWNLOAD_COMPLETEアクションに対応して両方のBroadcastReceiverが起動するのか確認してみた。

DownlaodManagerにキュー入れする部分のコード。
Request req = new Request(uri);
req.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
req.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, subPath);
req.setMimeType(contentType);
req.setTitle(getResources().getString(R.string.app_name));
req.setDescription(getResources().getString(R.string.dl_start_str));
Log.v(getTag(), "ダウンロード開始:" + uri);

//ダウンロードマネージャにダウンロードリクエストをキュー
DownloadManager dlman = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
dlman.enqueue(req);

DOWNLOAD_COMPLETEアクションをレシーブするDownloadManagerBroadcastReceiverクラス。
public class DownloadManagerBroadcastReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {

  // 関係のあるメッセージがどうか調べる
  String pkg = intent.getPackage();
  if (pkg.equals("my.app.package")) {
       Log.v(getTag(), "関係あるブロードキャスト受信:" + intent);
  } else {
       Log.v(getTag(), "無関係なブロードキャスト受信:" + intent);
       return;
  }

} // onReceive

} // class

my.app.packageのAndroidManifest.xml抜粋




 


my.app.package_dummyのAndroidManifest.xml抜粋






my.app.package_dummyのDownloadManagerBroadcastReceiverクラス。
public class DownloadManagerBroadcastReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {

    // 関係のあるメッセージがどうか調べる
    String pkg = intent.getPackage();
    if (pkg.equals("my.app.package_dummy")) {
       Log.v(getTag(), "関係あるブロードキャスト受信:" + intent);
    } else {
       Log.v(getTag(), "無関係なブロードキャスト受信:" + intent);
       return;
    }

  } // onReceive

} // class


結果、my.app.packageでDOWNLOAD_COMPLETEアクションが発生した場合は、my.app.package.DownloadManagerBroadcastReceiverが起動、

関係あるブロードキャスト受信:Intent { act=android.intent.action.DOWNLOAD_COMPLETE flg=0x10 pkg=my.app.package cmp=my.app.package/.DownloadManagerBroadcastReceiver (has extras) }


my.app.package_dummyでDOWNLOAD_COMPLETEアクションが発生した場合は、my.app.package_dummy.DownloadManagerBroadcastReceiverが起動した。

関係あるブロードキャスト受信:Intent { act=android.intent.action.DOWNLOAD_COMPLETE flg=0x10 pkg=my.app.package_dummy cmp=my.app.package_dummy/.DownloadManagerBroadcastReceiver (has extras) }


両方のDownloadManagerBroadcastReceiverが同時に起動することはなかった。

つまり、それぞれのアプリ内でDownloadManagerを使ってダウンロードした時に発生するDOWNLOAD_COMPLETEアクションには、それぞれのマニフェストに明記しているBroadcastReceiverクラスが対応することになる。


onReceive()内でパッケージ名をチェックする必要などはない。

ということがわかった。


では、さらに、同一パッケージ内で、DOWNLOAD_COMPLETEアクションに対応するBroadcastReceiverが複数登録されていたらどうなる?というテストをしてみた。

AndroidManifest.xml抜粋





        






02-04 16:23:35.943: V/DownloadManagerBroadcastReceiver.onReceive:42(1695): 関係あるブロードキャスト受信:Intent { act=android.intent.action.DOWNLOAD_COMPLETE flg=0x10 pkg=my.app.package cmp=my.app.package/.DownloadManagerBroadcastReceiver (has extras) }
02-04 16:23:36.033: V/DownloadManagerBroadcastReceiver2.onReceive:42(1695): 関係あるブロードキャスト受信:Intent { act=android.intent.action.DOWNLOAD_COMPLETE flg=0x10 pkg=my.app.package cmp=my.app.package/.DownloadManagerBroadcastReceiver2 (has extras) }

両方のBroadcastReceiverが呼ばれた。起動順序はマニフェストに記述した順序かな?

2013年1月26日土曜日

NotificationのBigPictureStyleで画像全体を表示させてみる

BigPictureStyleでは、画像の中央を中心にして、最大幅450dpに合う大きさへ画像がクロップされる。

でもそれだと、画像が縦長の場合は上下が大きくクロップされてしまい、何が写っているのかよく分からない。



画像の比率によらず、bigPicture部分で、常に画像全体が見られるようにしてみた。
private Bitmap getBigPicture(Resources res, String path) {
 Bitmap canvasBmp = null;

 final float WIDTH_DP = 450.0f;
 final float HEIGHT_DP = 192.0f;

 // ピクセル密度を取得する
 DisplayMetrics metrics = new DisplayMetrics();
 getWindowManager().getDefaultDisplay().getMetrics(metrics);
 float scaleDensity = metrics.scaledDensity;

 // ピクセル密度を加味してターゲットの幅と高さのピクセル数を計算する
 int targetWidthPx = (int) (WIDTH_DP * scaleDensity);
 int targetHeightPx = (int) (HEIGHT_DP * scaleDensity);

 // 画像読み込みオプションを作成
 BitmapFactory.Options options = new BitmapFactory.Options();
 // メモリが少なくなったらパージできるようにオプションを設定
 options.inPurgeable = true;
 // 画像サイズだけを読み込むようにオプションを設定
 options.inJustDecodeBounds = true;

 // 画像読み込み
 Bitmap srcBmp = BitmapFactory.decodeFile(path, options);

 // 画像の幅と高さのピクセル数を取得する
 int srcWidth = options.outWidth;
 int srcHeight = options.outHeight;

 // Matrixを取得する パディングは0で小さな画像は拡大しない
 Matrix mat = getMatrix(srcWidth, srcHeight, targetWidthPx, targetHeightPx, 0, false);

 // Paintを作成する
 Paint bmpPaint = new Paint();
 bmpPaint.setFilterBitmap(true);

 // ターゲット幅と高さで背景が透明のキャンバスを作成
 canvasBmp = Bitmap.createBitmap(targetWidthPx, targetHeightPx, Bitmap.Config.ARGB_8888);
 Canvas canvas = new Canvas(canvasBmp);

 // 画像サイズだけでなく画像そのものを読み込むようにオプションを設定しなおす
 options.inJustDecodeBounds = false;

 // 実際に画像を読み込み
 srcBmp = BitmapFactory.decodeFile(path, options);

 // 背景が透明のキャンバスに画像を合成する
 canvas.drawBitmap(srcBmp, mat, bmpPaint);

 // メモリ解放
 srcBmp.recycle();
 srcBmp = null;

 Log.i("#getBigPicture()", "scaleDensity=" + scaleDensity + " srcWidth="
   + srcWidth + " srcHeight=" + srcHeight + " targetWidthPx="
   + targetWidthPx + " targetHeightPx=" + targetHeightPx);
 return canvasBmp;
}

/**
 * @param sw ソース幅
 * @param sh ソース高さ
 * @param pw この幅にフィットさせる
 * @param ph この高さにフィットさせる
 * @param padding フィット時に考慮するパディング
 * @param enableMagnify 画像が小さい場合に拡大するか否か
 * @return フィットさせるためのスケール値
 */
private static final Matrix getMatrix(int sw, int sh, int pw, int ph,
  int padding, boolean enableMagnify) {
 float scale = getMaxScaleToParent(sw, sh, pw, ph, padding);
 if (!enableMagnify && scale > 1.0f) {
  scale = 1;
 }
 Log.i("#getMatrix()", "scale=" + scale);
 Matrix mat = new Matrix();
 mat.postScale(scale, scale);
 mat.postTranslate((pw - (int) (sw * scale)) / 2,
   (ph - (int) (sh * scale)) / 2);
 return mat;
}

private static final float getMaxScaleToParent(int sw, int sh, int pw,
  int ph, int padding) {
 float hScale = (float) (pw - padding) / (float) sw;
 float vScale = (float) (ph - padding) / (float) sh;
 return Math.min(hScale, vScale);
}

上記では、bigPictureの最大幅・高さのキャンバスを作り、そのキャンバスサイズに合わせて画像を縮小し、中央に配置する。

BigPictureStyleでは、450dpに合わせて、上下左右に隙間なく画像を表示するよう設計されている。しかしこの設計では、多くの場合に上下や左右に隙間ができても、常に画像全体が写るようにして、どんな画像か把握出来るようにしている。


上記メソッドを呼び出してBigPictureStyleを作るコードのスニペット。
NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(
      notificationBuilder);
bigPictureStyle.bigPicture(getBigPicture(res, path));
bigPictureStyle.setBigContentTitle(contentTitle);
bigPictureStyle.setSummaryText(contentText);
notificationBuilder.setStyle(bigPictureStyle);


結果、bigPicture部分はこんなふうに表示される。

NotificationのBigPictureStyleで表示されるbigPictureの大きさはいくつなんだ

4.1のJelly Bean以降でサポートされるようになった、リッチなNotificationスタイル。

このうちのBigPictureStyleで表示できるbigPicture(画像部分)の大きさにあわせて、元画像全体をリサイズして表示したいと思い、サイズを調べてみた。


・高さは192dp

Android Developersのサイトでは、高さの上限は最大256dpと書いてある。
Through an improved notification builder, apps can create notifications that use a larger area, up to 256 dp in height.

同じ内容がGoogle I/O 2012のセッション(32分40秒あたり)でも触れられている。そのスライドでは、BigContentView全体で、256dpが最大の高さと示してある。



つまり、256dpはbigPictureの高さではなく、BigPictureStyleのNotification全体での高さ上限だということ。よって、bigPictureの高さはNotificationを広げた256dp(4U)の状態から、Notificationを広げていない64dp(1U)の状態を引いた値になる。

256dp(4U) - 64dp(1U) = 192dp(3U)


・幅は450dp

幅は、同じく上記のGoogle I/O 2012のセッション(36分30秒あたり)で、最大450dpだとある。



まとめるとこんな感じ



・pxだとどのくらい?

例えば、Nexus 7はtvdpiなので、ピクセル密度(density)は1.33125。

450dp x 1.33125 = 約599px
192dp x 1.33125 = 約255px

bigPicture部分には、幅599px、高さ255px程度の大きさの画像が表示される。