同僚、こんにちは。 前回の記事で始まったトピックを継続し、デバイスでアカウントを作成するメカニズムを検討しました。 これは、SyncAdapter Frameworkを使用するための最初の前提条件でした。
2番目の条件はContentProviderの存在であり 、その作成プロセスはドキュメントに記載されています。 正直に言うと、ここでの説明があまり好きではありません。すべてがかさばって複雑に見えます。 したがって、少しサイクルを繰り返し、このトピックをもう一度経験します。 スタブプロバイダーを使用することもできますが、私たちは真剣な人々であり、このツールの全機能を使用します。
前の部分へのコメントでは、承認が必要ではなく同期のみが必要な場合を考慮して、リクエストがフラッシュされました。 このような場合を検討します。 例として、私たちの最愛のhabrを読むためだけでなく、単純なrssリーダーを取り上げて書いてみましょう。 はい、とてもつまらないです。
アプリケーションには、フィードを追加/削除したり、ニュースのリストを表示したり、ブラウザーで開いたりする機能があります。 同期プロセスを視覚化し、最近サポートライブラリに追加されたSwipeRefreshLayoutクラスを使用して同期プロセスを開始します。 ここでそれが何であり、どのように使用するかを読むことができます 。
特定の間隔で自動同期を設定するには、このような設定画面が必要です。 アプリケーションへのアクセスだけでなく、アカウントのシステム画面からもアクセスすることが望ましいです(記事のスクリーンショットのように)。 これにはPreferenceFragmentsを使用します。 機能を決定しました。始めましょう。
アカウント
前のパートからアプリケーションにアカウントを追加する方法はすでに知っています。 ただし、アプリケーションの場合、それぞれ承認は必要ありません。Authenticatorを空の実装に置き換えます。
Authenticator.java
public class Authenticator extends AbstractAccountAuthenticator { public Authenticator(Context context) { super(context); } @Override public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { throw new UnsupportedOperationException(); } @Override public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public String getAuthTokenLabel(String authTokenType) { throw new UnsupportedOperationException(); } @Override public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } @Override public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException { throw new UnsupportedOperationException(); } }
res / xml / authenticator.xmlファイルを少し変更して、同期設定画面に移動する機能を追加する必要があります。 これらの設定をプルアップする必要があるファイルにパラメーターandroid:accountPreferencesを追加します。 「同期」要素をクリックすると、アプリケーションのSyncSettingsActivityが開きます。
authenticator.xml
<?xml version="1.0" encoding="utf-8"?> <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" android:accountPreferences="@xml/account_prefs" android:accountType="com.elegion.newsfeed.account" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:smallIcon="@drawable/ic_launcher" />
account_prefs.xml
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:persistent="true"> <PreferenceCategory android:title="@string/general_settings" /> <PreferenceScreen android:key="com.elegion.newsfeed.KEY_ACCOUNT_SYNC" android:summary="@string/sync_settings_summary" android:title="@string/sync"> <intent android:action="com.elegion.newsfeed.ACTION_SYNC_SETTINGS" android:targetClass="com.elegion.newsfeed.activity.SyncSettingsActivity" android:targetPackage="com.elegion.newsfeed" /> </PreferenceScreen> </PreferenceScreen>
ContentProvider
プロバイダーは、ニュースを保存するSQLiteデータベースのラッパーになります。 少し詳しく見て、その実装をより詳細に検討してみましょう。 プロバイダーは、2種類のUriを使用できます。
content://権限/テーブル -テーブルからすべての値を選択
content://権限/テーブル/ _id-主キーで1つの値を選択
PackageManager.getProviderInfoを使用するonCreateメソッドで、このプロバイダーの権限を取得し、SQLiteUriMatcherに登録します。 メソッドで行われること:プロバイダーはuriからテーブルの名前を取得し、SQLiteTableProviderの特定の実装(テーブルのプロバイダー)がこのテーブルのSCHEMAから取得されます。 対応するメソッドはSQLiteTableProviderで呼び出されます(実際、呼び出しはプロキシされます)。 このアプローチにより、各テーブルでデータの処理をカスタマイズできます。 結果に応じて、ContentResolver(およびそれとともにアプリケーション)は、データの変更に関する通知を受け取ります。 タイプcontent:// authority / table / _idの uriの場合 、where 句は書き換えられ、主キーでの操作を保証します。 必要に応じて、このプロバイダーをわずかにひねり、ライブラリクラスに配置できます。 実践が示すように、このような実装はタスクの90%に十分です(残りの10個はnocase検索のような全文検索です)。
SQLiteContentProvider.java
public class SQLiteContentProvider extends ContentProvider { private static final String DATABASE_NAME = "newsfeed.db"; private static final int DATABASE_VERSION = 1; private static final String MIME_DIR = "vnd.android.cursor.dir/"; private static final String MIME_ITEM = "vnd.android.cursor.item/"; private static final Map<String, SQLiteTableProvider> SCHEMA = new ConcurrentHashMap<>(); static { SCHEMA.put(FeedProvider.TABLE_NAME, new FeedProvider()); SCHEMA.put(NewsProvider.TABLE_NAME, new NewsProvider()); } private final SQLiteUriMatcher mUriMatcher = new SQLiteUriMatcher(); private SQLiteOpenHelper mHelper; private static ProviderInfo getProviderInfo(Context context, Class<? extends ContentProvider> provider, int flags) throws PackageManager.NameNotFoundException { return context.getPackageManager() .getProviderInfo(new ComponentName(context.getPackageName(), provider.getName()), flags); } private static String getTableName(Uri uri) { return uri.getPathSegments().get(0); } @Override public boolean onCreate() { try { final ProviderInfo pi = getProviderInfo(getContext(), getClass(), 0); final String[] authorities = TextUtils.split(pi.authority, ";"); for (final String authority : authorities) { mUriMatcher.addAuthority(authority); } mHelper = new SQLiteOpenHelperImpl(getContext()); return true; } catch (PackageManager.NameNotFoundException e) { throw new SQLiteException(e.getMessage()); } } @Override public Cursor query(Uri uri, String[] columns, String where, String[] whereArgs, String orderBy) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } final String tableName = getTableName(uri); final SQLiteTableProvider tableProvider = SCHEMA.get(tableName); if (tableProvider == null) { throw new SQLiteException("No such table " + tableName); } if (matchResult == SQLiteUriMatcher.MATCH_ID) { where = BaseColumns._ID + "=?"; whereArgs = new String[]{uri.getLastPathSegment()}; } final Cursor cursor = tableProvider.query(mHelper.getReadableDatabase(), columns, where, whereArgs, orderBy); cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } @Override public String getType(Uri uri) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } else if (matchResult == SQLiteUriMatcher.MATCH_ID) { return MIME_ITEM + getTableName(uri); } return MIME_DIR + getTableName(uri); } @Override public Uri insert(Uri uri, ContentValues values) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } final String tableName = getTableName(uri); final SQLiteTableProvider tableProvider = SCHEMA.get(tableName); if (tableProvider == null) { throw new SQLiteException("No such table " + tableName); } if (matchResult == SQLiteUriMatcher.MATCH_ID) { final int affectedRows = updateInternal( tableProvider.getBaseUri(), tableProvider, values, BaseColumns._ID + "=?", new String[]{uri.getLastPathSegment()} ); if (affectedRows > 0) { return uri; } } final long lastId = tableProvider.insert(mHelper.getWritableDatabase(), values); getContext().getContentResolver().notifyChange(tableProvider.getBaseUri(), null); final Bundle extras = new Bundle(); extras.putLong(SQLiteOperation.KEY_LAST_ID, lastId); tableProvider.onContentChanged(getContext(), SQLiteOperation.INSERT, extras); return uri; } @Override public int delete(Uri uri, String where, String[] whereArgs) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } final String tableName = getTableName(uri); final SQLiteTableProvider tableProvider = SCHEMA.get(tableName); if (tableProvider == null) { throw new SQLiteException("No such table " + tableName); } if (matchResult == SQLiteUriMatcher.MATCH_ID) { where = BaseColumns._ID + "=?"; whereArgs = new String[]{uri.getLastPathSegment()}; } final int affectedRows = tableProvider.delete(mHelper.getWritableDatabase(), where, whereArgs); if (affectedRows > 0) { getContext().getContentResolver().notifyChange(uri, null); final Bundle extras = new Bundle(); extras.putLong(SQLiteOperation.KEY_AFFECTED_ROWS, affectedRows); tableProvider.onContentChanged(getContext(), SQLiteOperation.DELETE, extras); } return affectedRows; } @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { final int matchResult = mUriMatcher.match(uri); if (matchResult == SQLiteUriMatcher.NO_MATCH) { throw new SQLiteException("Unknown uri " + uri); } final String tableName = getTableName(uri); final SQLiteTableProvider tableProvider = SCHEMA.get(tableName); if (tableProvider == null) { throw new SQLiteException("No such table " + tableName); } if (matchResult == SQLiteUriMatcher.MATCH_ID) { where = BaseColumns._ID + "=?"; whereArgs = new String[]{uri.getLastPathSegment()}; } return updateInternal(tableProvider.getBaseUri(), tableProvider, values, where, whereArgs); } private int updateInternal(Uri uri, SQLiteTableProvider provider, ContentValues values, String where, String[] whereArgs) { final int affectedRows = provider.update(mHelper.getWritableDatabase(), values, where, whereArgs); if (affectedRows > 0) { getContext().getContentResolver().notifyChange(uri, null); final Bundle extras = new Bundle(); extras.putLong(SQLiteOperation.KEY_AFFECTED_ROWS, affectedRows); provider.onContentChanged(getContext(), SQLiteOperation.UPDATE, extras); } return affectedRows; } private static final class SQLiteOpenHelperImpl extends SQLiteOpenHelper { public SQLiteOpenHelperImpl(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { db.beginTransactionNonExclusive(); try { for (final SQLiteTableProvider table : SCHEMA.values()) { table.onCreate(db); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.beginTransactionNonExclusive(); try { for (final SQLiteTableProvider table : SCHEMA.values()) { table.onUpgrade(db, oldVersion, newVersion); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } } }
ここで、AndroidManifest.xmlにプロバイダーを登録し、 Androidパラメーターsyncable = "true"に注意する必要があります。 このフラグは、プロバイダーが同期をサポートしていることを示します。
AndroidManifest.xml
<provider android:name=".sqlite.SQLiteContentProvider" android:authorities="com.elegion.newsfeed" android:exported="false" android:syncable="true" />
また、ニュースフィードを操作するためのSQLiteTableProvider実装であるFeedProviderクラスも興味深いものです。 このテーブルに(!)を挿入すると(新しいフィードをサブスクライブする)、強制同期が呼び出されます。 onContentChangedメソッドは、データの変更(挿入/更新/削除)時にSQLiteContentProviderから作動するthisを担当します。 テーブルに対してトリガー( onCreate )が作成され、フィードに関連するニュースが削除されます。 挿入時にのみ同期を呼び出す価値があるのはなぜですか? ループを避けるため、プロバイダーがテーブルを更新するため(タイトル、写真へのリンク、公開日などを追加するため)。 追加の同期パラメーターはsyncExtrasを介して渡されます。
FeedProvider.java
public class FeedProvider extends SQLiteTableProvider { public static final String TABLE_NAME = "feeds"; public static final Uri URI = Uri.parse("content://com.elegion.newsfeed/" + TABLE_NAME); public FeedProvider() { super(TABLE_NAME); } public static long getId(Cursor c) { return c.getLong(c.getColumnIndex(Columns._ID)); } public static String getIconUrl(Cursor c) { return c.getString(c.getColumnIndex(Columns.IMAGE_URL)); } public static String getTitle(Cursor c) { return c.getString(c.getColumnIndex(Columns.TITLE)); } public static String getLink(Cursor c) { return c.getString(c.getColumnIndex(Columns.LINK)); } public static long getPubDate(Cursor c) { return c.getLong(c.getColumnIndex(Columns.PUB_DATE)); } public static String getRssLink(Cursor c) { return c.getString(c.getColumnIndex(Columns.RSS_LINK)); } @Override public Uri getBaseUri() { return URI; } @Override public void onContentChanged(Context context, int operation, Bundle extras) { if (operation == INSERT) { extras.keySet(); final Bundle syncExtras = new Bundle(); syncExtras.putLong(SyncAdapter.KEY_FEED_ID, extras.getLong(KEY_LAST_ID, -1)); ContentResolver.requestSync(AppDelegate.sAccount, AppDelegate.AUTHORITY, syncExtras); } } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("create table if not exists " + TABLE_NAME + "(" + Columns._ID + " integer primary key on conflict replace, " + Columns.TITLE + " text, " + Columns.LINK + " text, " + Columns.IMAGE_URL + " text, " + Columns.LANGUAGE + " text, " + Columns.PUB_DATE + " integer, " + Columns.RSS_LINK + " text unique on conflict ignore)"); db.execSQL("create trigger if not exists after delete on " + TABLE_NAME + " begin " + " delete from " + NewsProvider.TABLE_NAME + " where " + NewsProvider.Columns.FEED_ID + "=old." + Columns._ID + ";" + " end;"); } public interface Columns extends BaseColumns { String TITLE = "title"; String LINK = "link"; String IMAGE_URL = "imageUrl"; String LANGUAGE = "language"; String PUB_DATE = "pubDate"; String RSS_LINK = "rssLink"; } }
シムの後ろで、ウサギのミンクが終わり、見ているガラスが始まります。
同期アダプタ
SyncAdapter'aの作成プロセスに入る前に、なぜこれが必要なのか、どのような利点があるのかを考えてみましょう。 あなたがドキュメントを信じているなら、少なくとも私たちは得るでしょう:
- ステータスを確認し、ネットワークが利用可能になったら同期を開始します。
- 基準および/またはスケジュールによって同期するスケジューラ。
- 何らかの理由で前回失敗した場合、同期を自動的に開始します。
- システムが無線モジュールを切り替える可能性が低くなるため、バッテリーの電力を節約してください。 さらに、重要な充電レベルでは同期が開始されません。
- システム設定インターフェイスへの統合。
すでにいいですね ContentProviderを使用するときに、そのデータが変更されたときに同期を開始できることを追加します。 これにより、アプリケーションのデータ変更を追跡し、「手動モード」で同期する必要が完全になくなります。
このようなものを統合するプロセスは、アカウントをアプリケーションに統合するプロセスと非常に似ています。 システムに統合するには、 AbstractThreadedSyncAdapterとServiceの実装が必要です。 AbstractThreadedSyncAdapterには、すべての魔法が発生する抽象onPerformSyncメソッドが1つだけあります。 ここで何が起こっているのでしょうか? 送信された追加パラメータに応じて(FeedProvider.onContentChangedのsyncExtrasを思い出してください)、1つのテープまたはすべてが同期されます。 一般に、データベースからフィードを選択し、参照によってrssを解析し、 ContentProviderClientプロバイダーを使用してデータベースに追加します 。 SyncResult syncResultは 、同期のステータス(更新、エラーなどの数)についてシステムに通知するために使用されます。
Syncadapter.java
public class SyncAdapter extends AbstractThreadedSyncAdapter { public static final String KEY_FEED_ID = "com.elegion.newsfeed.sync.KEY_FEED_ID"; public SyncAdapter(Context context) { super(context, true); } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { final long feedId = extras.getLong(KEY_FEED_ID, -1); if (feedId > 0) { syncFeeds(provider, syncResult, FeedProvider.Columns._ID + "=?", new String[]{String.valueOf(feedId)}); } else { syncFeeds(provider, syncResult, null, null); } } private void syncFeeds(ContentProviderClient provider, SyncResult syncResult, String where, String[] whereArgs) { try { final Cursor feeds = provider.query( FeedProvider.URI, new String[]{ FeedProvider.Columns._ID, FeedProvider.Columns.RSS_LINK }, where, whereArgs, null ); try { if (feeds.moveToFirst()) { do { syncFeed(feeds.getString(0), feeds.getString(1), provider, syncResult); } while (feeds.moveToNext()); } } finally { feeds.close(); } } catch (RemoteException e) { Log.e(SyncAdapter.class.getName(), e.getMessage(), e); ++syncResult.stats.numIoExceptions; } } private void syncFeed(String feedId, String feedUrl, ContentProviderClient provider, SyncResult syncResult) { try { final HttpURLConnection cn = (HttpURLConnection) new URL(feedUrl).openConnection(); try { final RssFeedParser parser = new RssFeedParser(cn.getInputStream()); try { parser.parse(feedId, provider, syncResult); } finally { parser.close(); } } finally { cn.disconnect(); } } catch (IOException e) { Log.e(SyncAdapter.class.getName(), e.getMessage(), e); ++syncResult.stats.numIoExceptions; } } }
SyncServiceの実装も非常に簡単です。 必要なのは、システムにIBinderオブジェクトを与えてSyncAdapterと通信することだけです。 システムが登録するアダプターの種類を理解するには、xml-metaファイルsync_adapter.xmlが必要です。また、これらすべてをAndroidManifest.xmlに登録する必要があります。
SyncService.java
public class SyncService extends Service { private static SyncAdapter sSyncAdapter; @Override public void onCreate() { super.onCreate(); if (sSyncAdapter == null) { sSyncAdapter = new SyncAdapter(getApplicationContext()); } } @Override public IBinder onBind(Intent intent) { return sSyncAdapter.getSyncAdapterBinder(); } }
sync_adapter.xml
<?xml version="1.0" encoding="utf-8"?> <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="com.elegion.newsfeed.account" android:allowParallelSyncs="false" android:contentAuthority="com.elegion.newsfeed" android:isAlwaysSyncable="true" android:supportsUploading="false" android:userVisible="true" />
AndroidManifest.xml
<service android:name=".sync.SyncService" android:exported="false" android:process=":sync"> <intent-filter> <action android:name="android.content.SyncAdapter" /> </intent-filter> <meta-data android:name="android.content.SyncAdapter" android:resource="@xml/sync_adapter" /> </service>
そして今、例
これは、テープのリストを含むウィンドウの外観です。 ご記憶のとおり 、 SwipeRefreshLayoutを使用してこのプロセスの同期と視覚化を強制することに同意しました。 FeedList.javaフィードリストとNewsList.javaフィードリストは、共通の親SwipeToRefreshList.javaから継承されます。
同期のステータスを追跡するには、ContentResolverにObserverを登録する必要があります( SwipeToRefreshList.onResume()メソッド)。 これを行うには、 ContentResolver.addStatusChangeListenerメソッドを使用します。 SwipeToRefreshList.onStatusChangedメソッドで 、 ContentResolver.isSyncActiveメソッドを使用して同期ステータスを確認し、この結果をSwipeToRefreshList.onSyncStatusChangedメソッドに渡します。これは相続人によってオーバーライドされます。 このメソッドが行うことは、 SwipeRefreshLayoutの進行状況バーを非表示/表示することだけです。 SyncStatusObserver.onStatusChangedは別のスレッドから呼び出されるため、結果をハンドラーでラップします。 子孫のSwipeToRefreshList.onRefreshメソッドは、 ContentResolver.requestSyncを使用して強制同期を開始します 。
すべてのリストは、 CursorLoader + CursorAdapterを使用してロードおよび表示されます。これは、ContentProviderと連携しても機能し、リストの関連性を監視する必要がなくなります。 新しいアイテムがプロバイダーに追加されるとすぐに、すべてのCursorLoadersは通知を受信し、CursorAdapter'ahのデータを更新します。
SwipeToRefreshList.java
public class SwipeToRefreshList extends Fragment implements SwipeRefreshLayout.OnRefreshListener, SyncStatusObserver, AdapterView.OnItemClickListener, SwipeToDismissCallback { private SwipeRefreshLayout mRefresher; private ListView mListView; private Object mSyncMonitor; private SwipeToDismissController mSwipeToDismissController; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fmt_swipe_to_refresh_list, container, false); mListView = (ListView) view.findViewById(android.R.id.list); return (mRefresher = (SwipeRefreshLayout) view); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mRefresher.setColorScheme( android.R.color.holo_blue_light, android.R.color.holo_red_light, android.R.color.holo_green_light, android.R.color.holo_orange_light ); mSwipeToDismissController = new SwipeToDismissController(mListView, this); } @Override public void onResume() { super.onResume(); mRefresher.setOnRefreshListener(this); mSyncMonitor = ContentResolver.addStatusChangeListener( ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE | ContentResolver.SYNC_OBSERVER_TYPE_PENDING, this ); mListView.setOnItemClickListener(this); mListView.setOnTouchListener(mSwipeToDismissController); mListView.setOnScrollListener(mSwipeToDismissController); } @Override public void onPause() { mRefresher.setOnRefreshListener(null); ContentResolver.removeStatusChangeListener(mSyncMonitor); mListView.setOnItemClickListener(null); mListView.setOnTouchListener(null); mListView.setOnScrollListener(null); super.onPause(); } @Override public final void onRefresh() { onRefresh(AppDelegate.sAccount); } @Override public final void onStatusChanged(int which) { mRefresher.post(new Runnable() { @Override public void run() { onSyncStatusChanged(AppDelegate.sAccount, ContentResolver .isSyncActive(AppDelegate.sAccount, AppDelegate.AUTHORITY)); } }); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { } @Override public boolean canDismissView(View view, int position) { return false; } @Override public void dismissView(View view, int position) { } public void setListAdapter(ListAdapter adapter) { final DataSetObserver dataSetObserver = mSwipeToDismissController.getDataSetObserver(); final ListAdapter oldAdapter = mListView.getAdapter(); if (oldAdapter != null) { oldAdapter.unregisterDataSetObserver(dataSetObserver); } mListView.setAdapter(adapter); adapter.registerDataSetObserver(dataSetObserver); } protected void onRefresh(Account account) { } protected void onSyncStatusChanged(Account account, boolean isSyncActive) { } protected void setRefreshing(boolean refreshing) { mRefresher.setRefreshing(refreshing); } }
したがって、強制同期は整理されています。 しかし、ジュース自体は自動同期です。 アカウントに設定画面のサポートを追加したことを覚えていますか? ユーザーに不必要なアクションを強制することはお勧めしません。 したがって、この画面へのアクセスは、アクションバーのボタンによって複製されます。
彼が何であるか-左側に表示されます。 技術的には、これは1つのPreferenceFragment( SyncSettings.java )を持つアクティビティであり、その設定はres / xml / sync_prefs.xmlから取得されます。
パラメータの変更は、 onSharedPreferenceChangedメソッド( OnSharedPreferenceChangeListenerの実装)で追跡されます。 定期的な同期を有効にするために、 ContentResolver.addPeriodicSyncがあり 、奇妙なことにContentResolver.removePeriodicSyncを無効にします。 同期間隔を更新するには、 ContentResolver.addPeriodicSyncメソッドも使用されます。 なぜなら、この方法のドキュメントには次のように書かれているからです:「アカウント、オーソリティ、およびエクストラで既に別の定期同期がスケジュールされている場合、新しい定期同期は追加されず、代わりに前の同期の頻度が更新されます。」(同期がすでに計画されている場合は、新しい同期に余分な権限が追加されず、代わりに前の同期の間隔が更新されます)。
sync_prefs.xml
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceCategory android:key="com.elegion.newsfeed.KEY_SYNC" android:title="@string/sync"> <CheckBoxPreference android:defaultValue="false" android:key="com.elegion.newsfeed.KEY_AUTO_SYNC" android:summary="@string/auto_sync_summary" android:title="@string/auto_sync" /> <ListPreference android:defaultValue="@string/auto_sync_interval_default" android:dependency="com.elegion.newsfeed.KEY_AUTO_SYNC" android:entries="@array/auto_sync_intervals" android:entryValues="@array/auto_sync_interval_values" android:key="com.elegion.newsfeed.KEY_AUTO_SYNC_INTERVAL" android:title="@string/auto_sync_interval" /> </PreferenceCategory> </PreferenceScreen>
SyncSettings.java
public class SyncSettings extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String KEY_AUTO_SYNC = "com.elegion.newsfeed.KEY_AUTO_SYNC"; private static final String KEY_AUTO_SYNC_INTERVAL = "com.elegion.newsfeed.KEY_AUTO_SYNC_INTERVAL"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.sync_prefs); final ListPreference interval = (ListPreference) getPreferenceManager() .findPreference(KEY_AUTO_SYNC_INTERVAL); interval.setSummary(interval.getEntry()); } @Override public void onResume() { super.onResume(); getPreferenceManager().getSharedPreferences() .registerOnSharedPreferenceChangeListener(this); } @Override public void onPause() { getPreferenceManager().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(this); super.onPause(); } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (TextUtils.equals(KEY_AUTO_SYNC, key)) { if (prefs.getBoolean(key, false)) { final long interval = Long.parseLong(prefs.getString( KEY_AUTO_SYNC_INTERVAL, getString(R.string.auto_sync_interval_default) )); ContentResolver.addPeriodicSync(AppDelegate.sAccount, AppDelegate.AUTHORITY, Bundle.EMPTY, interval); } else { ContentResolver.removePeriodicSync(AppDelegate.sAccount, AppDelegate.AUTHORITY, new Bundle()); } } else if (TextUtils.equals(KEY_AUTO_SYNC_INTERVAL, key)) { final ListPreference interval = (ListPreference) getPreferenceManager().findPreference(key); interval.setSummary(interval.getEntry()); ContentResolver.addPeriodicSync( AppDelegate.sAccount, AppDelegate.AUTHORITY, Bundle.EMPTY, Long.parseLong(interval.getValue()) ); } } }
これらすべてをヒープに収集すると、Androidシステムが提供するすべての機能を備えた実用的なアプリケーションが得られます。 舞台裏にはおいしいものがたくさんありますが、SyncAdapter Frameworkのパワーを理解するにはこれで十分です。
それだけです。 完全なプロジェクトソースはこちらにあります 。 ご清聴ありがとうございました。 建設的な批判は大歓迎です。
Androidアプリケーションでの同期。 パート1