2012年7月4日水曜日

Android 3rd party のランチャーで直接 AppWidget を bind する

Jelly Bean で AppWidget 関係にいろいろ手が入りました。

これまで 3rd party のランチャーに AppWidget を bind するには、AppWidgetManager.ACTION_APPWIDGET_PICK を呼び出して、AppWidget を選択する標準のダイアログを表示するしかありませんでした。
  1. private void pickAppWidget() {  
  2.     int appWidgetId = mAppWidgetHost.allocateAppWidgetId();  
  3.   
  4.     Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_PICK);  
  5.     intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);  
  6.     startActivityForResult(intent, 1);  
  7. }  


ちなみに Jelly Bean でこの Intent を呼ぶとこうなります。
ちょっとどうなのよ。。。



一方で、ICS 以降のデフォルトの Launcher アプリでは、Launcher 内に AppWidget のプレビューを表示し、そこからドラッグ&ドロップして選択されたものを直接 bind しています(つまり、上記のダイアログを呼び出さない)。

http://tools.oesf.biz/android-4.0.1_r1.0/xref/packages/apps/Launcher2/src/com/android/launcher2/Launcher.java#1509
  1. 1520         int appWidgetId = getAppWidgetHost().allocateAppWidgetId();  
  2. 1521         AppWidgetManager.getInstance(this).bindAppWidgetId(appWidgetId, info.componentName);  
  3. 1522         addAppWidgetImpl(appWidgetId, info);  

この bindAppWidgetId() メソッドは ICS までは 3rd party からも呼べました(@hide ではなかった)。 呼べますが、android.permission.BIND_APPWIDGET パーミッションが必要で、かつこのパーミッションは system レベルなので 3rd party から呼ぶと SecurityException で落ちます。 (でも BIND_APPWIDGET のドキュメントにはその情報がないので、ちょっと不親切。まぁ、ちょっとぐぐればでるけどね http://stackoverflow.com/questions/3520564/security-exception-while-calling-bindappwidgetid とか。)

例えば、以下のコードは上記のパーミッションを宣言していても SecurityException で落ちます。

  1. private void bindAppWidget() {  
  2.     // this will be fail because of SecurityException  
  3.   
  4.     ComponentName componentName = new ComponentName(  
  5.             "com.example.android.mybatterywidget",  
  6.             "MyBatteryWidgetProvider");  
  7.   
  8.     int appWidgetId = mAppWidgetHost.allocateAppWidgetId();  
  9.   
  10.     AppWidgetManager.getInstance(self).bindAppWidgetId(appWidgetId,  
  11.             componentName);  
  12. }  


Jelly Bean では新しくメソッドが追加され、3rd party のランチャーでも直接 AppWidget を bind することができるようになっています。それに伴って bindAppWidgetId() メソッドはなくなり(たぶん @hide になったのだと思われるが、現状2012/7/4ではコードがまだ公開されていないのでわからない。)、bindAppWidgetIfAllowed() メソッドが追加されました。

このメソッドを呼んで bind が成功した場合は true が返ってきます。これまでの標準 Launcher のように android.permission.BIND_APPWIDGET パーミッションがあるアプリは成功します。3rd party アプリから呼んだ場合は、ユーザーがこのコンポーネントに対して常に AppWidget の bind を有効にしている場合は成功します。有効になっていない場合は失敗するので false が返ってきます。この場合はユーザーに有効にしてくださいとお願いするための Intent を呼びます。それが AppWidgetManager.ACTION_APPWIDGET_BIND です。

こんな感じで使います。

  1. private void bindAppWidget() {  
  2.     ComponentName componentName = new ComponentName(  
  3.             "com.example.android.mybatterywidget",  
  4.             "com.example.android.mybatterywidget.MyBatteryWidgetProvider");  
  5.   
  6.     int appWidgetId = mAppWidgetHost.allocateAppWidgetId();  
  7.   
  8.     boolean allowed = AppWidgetManager.getInstance(self)  
  9.             .bindAppWidgetIdIfAllowed(appWidgetId, componentName);  
  10.   
  11.     if (!allowed) {  
  12.         Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_BIND);  
  13.         intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);  
  14.         intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_PROVIDER,  
  15.                 componentName);  
  16.         startActivityForResult(intent, 2);  
  17.     }  
  18. }  
  19.   
  20. @Override  
  21. protected void onActivityResult(int requestCode, int resultCode, Intent data) {  
  22.     if (requestCode == 2) {  
  23.         AppWidgetManager mAppWidgetManager = AppWidgetManager  
  24.                 .getInstance(this);  
  25.   
  26.         if (resultCode == RESULT_OK) {  
  27.             int appWidgetId = data.getIntExtra(  
  28.                     AppWidgetManager.EXTRA_APPWIDGET_ID,  
  29.                     AppWidgetManager.INVALID_APPWIDGET_ID);  
  30.   
  31.             if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {  
  32.                 AppWidgetProviderInfo appWidget = mAppWidgetManager  
  33.                         .getAppWidgetInfo(appWidgetId);  
  34.                 // 必要であれば AppWidget の configure を呼びだしたり、ビューを自分のアプリに追加したりする  
  35.                   
  36.                 Log.d("appWidgetId", appWidgetId + "");  
  37.             }  
  38.         } else if (resultCode == RESULT_CANCELED) {  
  39.             int appWidgetId = data.getIntExtra(  
  40.                     AppWidgetManager.EXTRA_APPWIDGET_ID,  
  41.                     AppWidgetManager.INVALID_APPWIDGET_ID);  
  42.   
  43.             if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {  
  44.                 // キャンセルされたので削除  
  45.                 mAppWidgetHost.deleteAppWidgetId(appWidgetId);  
  46.             }  
  47.         }  
  48.           
  49.         return;  
  50.     }  
  51.   
  52.     super.onActivityResult(requestCode, resultCode, data);  
  53. }  


AppWidgetManager.ACTION_APPWIDGET_BIND を呼ぶとこんな感じの確認ダイアログがでます。



ここでチェックボックスにチェックを入れて Create にすると、この 3rd party ランチャーに常に AppWidget を bind する許可を与えることになります(つまり bindAppWidgetIfAllowed() が成功します)。このデフォルト設定は設定アプリから Clear defaults で外すことができます。

注意!
AppWidgetManager.ACTION_APPWIDGET_BIND のドキュメントには

The system will respond with an onActivityResult call with the following extras in the intent:

EXTRA_APPWIDGET_ID The appWidgetId that you supplied in the original intent.

のように extras として EXTRA_APPWIDGET_ID というキーで AppWidget の Id が返ってくる、とあるのですが、なぜか試したみたところ onActivityResult() に返ってくる Intent が null なのです(上記のコードだと data という変数が null になるので data.getIntExtra() のところで落ちる)。 よって startActivity() するときに現状の AppWidgetId を持っておいてそれを使うしかない感じです。

うーん。。。


1 件のコメント: