2011年6月16日木曜日

Android AppWidget

ベースとなる package は android.app.widget です。

app widget とは、ホームスクリーンのような別のアプリに埋め込むことができるミニチュアアプリのようなもので、新しい Activity を起動せずにアプリケーションのデータやサービスに簡単にすばやくアクセスすることができます。

詳しくは App Widget developer guide を参照すること。

どんなアプリケーションでも app widget provider として app widget を発行できます。app widget を発行するためにアプリケーションが行うことは ACTION_APPWIDGET_UPDATE intent を受け取る BroadcastReceiver と app widget についての metadata を提供することだけです。Android では、BroadcastReceiver を継承した AppWidgetProvider クラスを提供しています。AppWidgetProvider は、app widget の振る舞いを定義し、broadcast の処理を援助する便利なクラスです。

app widget host が widget を配置するためのコンテナです。widget のルック&フィールの詳細のほとんどは host に任されています。例えば、ホームスクリーンはウィジェットを見る方法の一つですが、ロックスクリーンも同様に widget を含むことができます。この場合、追加、削除、その他の widget 管理に関する別の方法を持つことになります。

このパッケージ内のクラスは以下のとおり

AppWidgetHost
  AppWidget サービスとのやり取りを、自身の UI 内に AppWidget を
  埋め込みたいアプリ(ホームスクリーンとか)に提供する

AppWidgetHostView
  AppWidget のビューを表示するためのグルー(糊)を提供する

AppWidgetManager
  AppWidget の状態をアップデートする。インストールされている
  AppWidget provider の情報や他の AppWidget の関連する状態を
  取得する

AppWidgetProvider
  AppWidget provider の実装を支援する便利なクラス

AppWidgetProviderInfo
  インストールされている AppWidget provider のメタデータを
  記述する
  このクラスのフィールドは <appwidget-provider> XML タグに
  対応する

この他に関連するクラスとして ComponentNameRemoteView があります。

ComponentName
  利用可能な特定のアプリケーションコンポーネント(Activity,
  Service, BroadcastReceiver, ContentProvider) の識別子
  コンポーネントを識別するために必要な2つの情報がここに
  カプセル化される。1つは存在しているパッケージ名(String)、
  もう1つはパッケージ内のクラス名(String)

RemoteViews
  他のプロセスに表示可能な View 階層を記述したクラス
  階層はレイアウトリソースファイルからインフレートされ、
  インフレートされた階層のコンテンツを編集する基本的な操作を
  提供する


---

AppWidgetManager のインスタンスを取得するには、getInstance(Context context) を使います。


AppWidgetManager manager = AppWidgetManager.getInstance(this);


現在インストールされている AppWidget の一覧を取得するには getInstalledProviders() を使います。


List<AppWidgetProviderInfo> widgetList = manager.getInstalledProvider();


AppWidgetProviderInfo は Parcelable を implements しているクラスです。
Android 3.1 (API Level 12) から resizable feature 用の定数
 ・RESIZE_BOTH
 ・RESIZE_HORIZONTAL
 ・RESIZE_NONE
 ・RESIZE_VERTICAL
が追加されています。

各フィールドと XML の attribute の関係は次の通り

・int autoAdvanceViewId - android:autoAdvanceViewId
  (AppWidget meta-data file)
  API Level 11 (Android 3.0) から

・ComponentName configure - android:configure
  (AppWidget meta-data file)

・int icon - android:icon
  (AndroidManifest.xml の <receiver>)

・int initialLayout - android:initialLayout
  (AppWidget meta-data file)

・String label - android:label
  (AndroidManifest.xml の <receiver>)

・int minHeight - android:minHeight
  (AppWidget meta-data file)

・int minWidth - android:minWidth
  (AppWidget meta-data file)

・int previewImage - android:previewImage
  (AndroidManifest.xml の <receiver>)
  API Level 11 (Android 3.0) から

・ComponentName provider - android:name
  (AndroidManifest.xml の <receiver>)

・int resizeMode - android:resizeMode
  (AppWidget meta-data file)
  API Level 12 (Android 3.1) から

・int updatePeriodMillis - android:updatePeriodMillis
  (AppWidget meta-data file)
  注意:30分に配送されるアップデートリクエストは1回まで


public class AppWidgetManagerTestActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

AppWidgetManager manager = AppWidgetManager.getInstance(this);

List widgetList = manager.getInstalledProviders();

for(AppWidgetProviderInfo info : widgetList) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
Log.d("autoAdvanceViewId", info.autoAdvanceViewId + "");

if(info.configure != null)
Log.d("configure", info.configure.flattenToString());

Log.d("icon", info.icon + "");
Log.d("initialLayout", info.initialLayout + "");
Log.d("label", info.label);
Log.d("minHeight", info.minHeight + "");
Log.d("minWidth", info.minWidth + "");

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
Log.d("previewImage", info.previewImage + "");

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
Log.d("resizeMode", info.resizeMode + "");

Log.d("updatePeriodMilis", info.updatePeriodMillis + "");

if(info.provider != null)
Log.d("provider", info.provider.flattenToString());
}
}
}




Android 2.3.4 の Emulator

D/configure( 682): com.android.quicksearchbox/com.android.quicksearchbox.SearchWidgetConfigActivity
D/icon ( 682): 2130837545
D/initialLayout( 682): 2130903050
D/label ( 682): 検索
D/minHeight( 682): 18433
D/minWidth( 682): 75265
D/updatePeriodMilis( 682): 0
D/provider( 682): com.android.quicksearchbox/com.android.quicksearchbox.SearchWidgetProvider

D/icon ( 682): 2130837507
D/initialLayout( 682): 2130903040
D/label ( 682): 音楽
D/minHeight( 682): 18433
D/minWidth( 682): 75265
D/updatePeriodMilis( 682): 0
D/provider( 682): com.android.music/com.android.music.MediaAppWidgetProvider

D/configure( 682): com.android.gallery/com.android.camera.PhotoAppWidgetConfigure
D/icon ( 682): 2130837549
D/initialLayout( 682): 2130903049
D/label ( 682): 写真フレーム
D/minHeight( 682): 37377
D/minWidth( 682): 37377
D/updatePeriodMilis( 682): 0
D/provider( 682): com.android.gallery/com.android.camera.PhotoAppWidgetProvider

D/icon ( 682): 2130837544
D/initialLayout( 682): 2130903102
D/label ( 682): 電源管理
D/minHeight( 682): 18433
D/minWidth( 682): 75265
D/updatePeriodMilis( 682): 0
D/provider( 682): com.android.settings/com.android.settings.widget.SettingsAppWidgetProvider

D/icon ( 682): 2130837551
D/initialLayout( 682): 2130903043
D/label ( 682): アナログ時計
D/minHeight( 682): 37377
D/minWidth( 682): 37377
D/updatePeriodMilis( 682): 0
D/provider( 682): com.android.deskclock/com.android.alarmclock.AnalogAppWidgetProvider

D/icon ( 682): 2130837513
D/initialLayout( 682): 2130903042
D/label ( 682): ホーム画面のヒント
D/minHeight( 682): 18433
D/minWidth( 682): 75265
D/updatePeriodMilis( 682): 0
D/provider( 682): com.android.protips/com.android.protips.ProtipWidget


android:configure は最初に widget を配置したときに呼び出される設定用の Activity です。
検索 Widget では検索対象を選択するためのリストダイアログが、写真フレーム Widget では表示する写真を選ぶ Activity が開始するようになっています。

minHeight と minWidth は AppWidget の dp単位での縦横幅なんですけど
 72dp = 18433
 146dp = 37377
 294dp = 75265
のように、256倍して 1 足した値になってました。




---
AppWidget の作り方

AppWidget アプリを作るのに必要なのが

 ・AppWidgetProviderInfo オブジェクト
   通常は res/xml/hoge.xml に <appwidget-provider>
   エレメントを使って記述する

 ・AppWidgetProvider の実装クラス
   AppWidgetProvider クラスを継承した独自クラスを作る

 ・AppWidget の View レイアウト
   res/layout/ に XML で定義する

の3つです。

必要に応じて
 ・App Widget 用の Configuration Activity
   AppWidget の追加時起動される Activity で、
   生成時にユーザーが App Widget の設定を
   できるようにするためのもの

も用意します。

1. App Widget を Manifest に宣言する

AndroidManifest.xml に AppWidgetProvider を継承した独自クラスを宣言します。

<receiver android:name=".HelloAppWidgetProvider"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/appwidget"/>
</receiver>


このクラスが ACTION_APPWIDGET_UPDATE broadcast を受け取れるように <intent-filter> で指定します。
ここで指定しなければならない Action はこれだけで、他の ACTION_APPWIDGET_DELETED や ACTION_APPWIDGET_ENABLED などの broadcast は AppWidgetManager が必要に応じて自動的に AppWidgetProvider に送ってくれます。


2. AppWidgetProviderInfo Metadata を追加する

AppWidgetProviderInfo は最小レイアウトサイズ、初期レイアウトレソース、どのくらいの頻度で AppWidget を更新するか、生成時に起動する設定Activity など AppWidget の基礎情報を定義します。AppWidgetProviderInfo オブジェクトを定義するには、XML ファイルで <appwidget-provider> エレメントを使って宣言し res/xml/ フォルダ内に保存します。


<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="294dp"
android:minHeight="72dp"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/preview"
android:initialLayout="@layout/example_appwidget"
android:configure="com.example.android.ExampleAppWidgetConfigure"
android:resizeMode="horizontal|vertical">
</appwidget-provider>

* android:previewImage は API Level 11 から、android:resizeMode は API Level 12 からです。

android:minWidthandroid:minHeight に指定するサイズは次の式が推奨されています。
(number of cells * 74) - 2 [dp]

android:updatePeriodMillis にはどのくらいの頻度で AppWidgetProvider から App Widget framework に update を依頼するかを宣言します。
86400000 は1日のミリ秒数なので、この場合は1日に1回 update のリクエストをするということです。頻繁なアップデートは電池を消耗するため、少なくとも 1時間に1回以上は行わないことが推奨されています。もしくはユーザーが更新頻度を設定できるようにするもの1つの方法ですし、updatePeriodMillis を使わずに AlarmManager や Service で Intent を投げる方法もあります。特に updatePeriodMillis は update 時にデバイスがスリープ状態だと、update のためにスリープ状態から抜けます。起動時にのみ更新したい場合は AlarmManager や Service を使いましょう。

android:previewImage は Android 3.0 (API Level 11) で追加された属性で、ユーザーが追加する AppWidget を選択するときに、実際に追加したときの見た目を確認できるようにするためのものです。指定されていない場合は通常のランチャーアイコンが表示されます。

android:autoAdvanceViewId 属性は Android 3.0 (API Level 11) で追加された属性で、AppWidget subview の view ID を指定します。AppWidget subview は widget のホストによって自動切り替えされるものです。

android:resizeMode 属性は Android 3.1 (API Level 12) で追加された属性で、Widget がリサイズできるかどうかのルールを指定します。指定できる値は "horizontal", "vertical", "none", "horizontal|vertical" です。


3. App Widget のレイアウトを作成する

通常のアプリのレイアウトと同様に res/layout/ フォルダ内に XML で定義します。
ただし、App Widget のレイアウトは RemoteViews を基づいているため、すべてのレイアウトや View Widget をサポートしているわけではありません。

RemoteView がサポートできるは、次のレイアウトクラスと

 ・FrameLayout
 ・LinearLayout
 ・RelativeLayout

Widget クラスだけです

 ・AnalogClock
 ・Button
 ・Chronometer
 ・ImageButton
 ・ImageView
 ・ProgressBar
 ・TextView
 ・ViewFlipper

これらのクラスを継承したクラスはサポート外です。


4. AppWidgetProvider クラスを使う

AppWidgetProvider を継承したクラスを作ります。
この AppWidgetProvider クラスには次の5つの public mothod があり、このうち onReceive が AppWidgetProvider が継承している BroadcastReceiver からきたもの。それ以外が AppWidgetProvider 独自のものです。
 ・onReceive(Context context, Intent intent)

 ・onEnabled(Context context)
   最初の AppWidget が追加されるときに呼ばれる

 ・onUpdate(Context context,
   AppWidgetManager appWidgetManager,
   int[] appWidgetIds)

   AppWidget が追加されるとき、もしくは updatePeriodMillis
   で指定されたインターバルに達したときに呼ばれる

 ・onDisabled(Context context)
   App Widget の最後のインスタンスが削除されたときに呼ばれる

 ・onDeleted(Context context, int[] appWidgetIds)
   App Widget のインスタンスが削除されたときに呼ばれる

AppWidgetManager が各 AppWidget に対する操作に応じた Intent をなげます。
上記のメソッドはそれらに応じて呼ばれるのですが、実際は onReceive が Intent を一括して受け取って処理しています。
ちょっと AppWidgetProvider のコードを見てましょう。そんなに長くありません。

http://tools.oesf.biz/android-2.3_r1.0/xref/frameworks/base/core/java/android/appwidget/AppWidgetProvider.java

public class AppWidgetProvider extends BroadcastReceiver {
public AppWidgetProvider() {
}

public void onReceive(Context context, Intent intent) {
// Protect against rogue update broadcasts (not really a security issue,
// just filter bad broacasts out so subclasses are less likely to crash).
String action = intent.getAction();
if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
if (appWidgetIds != null && appWidgetIds.length > 0) {
this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
}
}
}
else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
Bundle extras = intent.getExtras();
if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
this.onDeleted(context, new int[] { appWidgetId });
}
}
else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
this.onEnabled(context);
}
else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
this.onDisabled(context);
}
}
// END_INCLUDE(onReceive)

public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
}

public void onDeleted(Context context, int[] appWidgetIds) {
}

public void onEnabled(Context context) {
}

public void onDisabled(Context context) {
}
}


ようは、onReceive で受け取った Intent の Action 応じて onUpdate, onDelete, onEnabled, onDisabled のいずれかを呼んでいるだけです。

その際、ACTION_APPWIDGET_UPDATE の Intent や ACTION_APPWIDGET_DELETED の Intent から AppWidgetId を取り出して引数で渡してくれています。

これをみると ACTION_APPWIDGET_UPDATE では複数の ID が EXTRA_APPWIDGET_IDS に、ACTION_APPWIDGET_DELETED では1つの ID が EXTRA_APPWIDGET_ID に入っていることがわかります。onDeleted に渡される appWidgetIds には必ず1つしか含まれないのに配列になっているのは onUpdate との対称性を持たせるためでしょうかね。


AppWidget 上の View に ClickListener を設定する処理等は onUpdate で行うのが普通です。


public class ExampleAppWidgetProvider extends AppWidgetProvider {

public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
final int N = appWidgetIds.length;

// Perform this loop procedure for each App Widget that belongs to this provider
for (int i=0; i<N; i++) {
int appWidgetId = appWidgetIds[i];

// Create an Intent to launch ExampleActivity
Intent intent = new Intent(context, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);

// Get the layout for the App Widget and attach an on-click listener
// to the button
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_provider_layout);
views.setOnClickPendingIntent(R.id.button, pendingIntent);

// Tell the AppWidgetManager to perform an update on the current app widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
}


onUpdate ではネットワークにアクセスするなどの時間のかかる処理を行うべきではありません。それは Application Not Responding (ANR) によって AppWidgetProvider が閉じる要因になりえます。onUpdate() 内で Service を起動して Service 内で必要な処理をするようにしましょう。
Service を使った AppWidget のサンプルが Wikitionary Sample's AppWidgetProvider です。

AppWigetProvider を利用せずに BroadcastReceiver を継承して直接 Intent を受け取って処理することもできます。その場合は次の4つの Intent を受け取るようにし、onReceive(Context, Intent) を override します。

 ・ACTION_APPWIDGET_UPDATE
 ・ACTION_APPWIDGET_DELETED
 ・ACTION_APPWIDGET_ENABLED
 ・ACTION_APPWIDGET_DISABLED



5. App Widget 設定 Activity を作成する (オプション)

先ほども出てきた ウィジェット用の設定画面を指定する android:configure ですが、ウィジェットの追加時に呼ばれるので、同じアプリの AppWidget の2個目を追加するときでも呼ばれます。AppWidget の起動時に自動で呼ばれるので、Widget の色やサイズなどをユーザーに選択させることができます。

android:configure には、絶対パスで指定します。


android:configure="yanzm.example.appwidget.hello.HelloAppWidgetConfigure"


もちろん android:configure に指定する Activity も Manifest に指定しておく必要があります。
このとき、android.appwidget.action.APPWIDGET_CONFIGURE に対応するようにしておきます。


<activity android:name=".HelloAppWidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>


この Activity では呼び出し元の Activity に結果を返さなければいけません。
RESULT_CANCEL を返した場合、AppWidget は追加されません。
RESULT_OK を返した場合に AppWidget が追加されるのですが、Extras に WidgetId を入れて返さなければなりません。
通常は onCreate で Intent から渡された WidgetId を取り出して保持しておき、Activity を終了するときにそれを返すようにします。
(EXTRA_APPWIDGET_ID という extra にいれる)


public class HelloAppWidgetConfigure extends Activity {

int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;

public void onCreate(Bundle icicle) {
super.onCreate(icicle);

setResult(RESULT_CANCELED);

setContentView(R.layout.main);

// Find the widget id from the intent.
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
}

// If they gave us an intent without the widget id, just bail.
if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish();
}
}

...

private void finishConfigure() {
// Push widget update to surface with newly set prefix
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
ExampleAppWidgetProvider.updateAppWidget(context, appWidgetManager,
mAppWidgetId, titlePrefix);

// Make sure we pass back the original appWidgetId
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
// setResult(RESULT_OK); だとランチャーアプリが落ちることがある
}
}



もう1つ重要な注意点が、Configuration Activity が起動した場合 ACTION_APPWIDGET_UPDATE が broadcast されない = onUpdate() メソッドが呼ばれない、という点です。Configuration Activity が AppWidgetManager に対して update のリクエストを行う責任を持ちます。これ以降は onUpdate() は呼ばれます。スキップされるのは最初だけということです。

App Widget を Configuration Activity からアップデートするには AppWidgetManager を使って次のようします。

  1. Activity を起動した Intent から App Widget ID を取得する

Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}


  2. App Widget の設定を行う

  3. 設定が終わったら、getInstance(Context) を呼んで
   AppWigetManager のインスタンスを取得する

AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);


  4. updateAppWidget(int, RemoteViews) を呼んで
   RemoteViews レイアウトの App Widget をアップデートする

RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.example_appwidget);
appWidgetManager.updateAppWidget(mAppWidgetId, views);

1 件のコメント:

  1. AppWidgetと設定 Activityが出来上がったのですが、AppWidgetから設定 Activityを呼び出すことはできるでしょうか。

    返信削除