Index: device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java |
diff --git a/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java b/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8ca718d50b2849e968f69f146ca71648c9069060 |
--- /dev/null |
+++ b/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java |
@@ -0,0 +1,653 @@ |
+// Copyright 2016 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.device.nfc; |
+ |
+import android.Manifest; |
+import android.app.Activity; |
+import android.app.PendingIntent; |
+import android.content.BroadcastReceiver; |
+import android.content.Context; |
+import android.content.Intent; |
+import android.content.IntentFilter; |
+import android.content.IntentFilter.MalformedMimeTypeException; |
+import android.content.pm.PackageManager; |
+import android.nfc.FormatException; |
+import android.nfc.NdefMessage; |
+import android.nfc.NdefRecord; |
+import android.nfc.NfcAdapter; |
+import android.nfc.NfcManager; |
+import android.nfc.Tag; |
+import android.nfc.TagLostException; |
+import android.nfc.tech.Ndef; |
+import android.nfc.tech.NdefFormatable; |
+import android.nfc.tech.TagTechnology; |
+import android.os.Build; |
+import android.os.Process; |
+import android.support.annotation.Nullable; |
+import android.support.v4.content.LocalBroadcastManager; |
+ |
+import org.chromium.base.Log; |
+import org.chromium.mojo.bindings.Callbacks; |
+import org.chromium.mojo.system.MojoException; |
+import org.chromium.mojom.device.Nfc; |
+import org.chromium.mojom.device.NfcError; |
+import org.chromium.mojom.device.NfcErrorType; |
+import org.chromium.mojom.device.NfcMessage; |
+import org.chromium.mojom.device.NfcPushOptions; |
+import org.chromium.mojom.device.NfcPushTarget; |
+import org.chromium.mojom.device.NfcRecord; |
+import org.chromium.mojom.device.NfcRecordType; |
+import org.chromium.mojom.device.NfcWatchOptions; |
+ |
+import java.io.IOException; |
+import java.io.UnsupportedEncodingException; |
+import java.util.ArrayList; |
+import java.util.List; |
+ |
+/** |
+ * Android implementation of the NFC mojo service defined in |
+ * device/nfc/nfc.mojom. |
+ */ |
+public class NfcImpl extends BroadcastReceiver implements Nfc { |
+ private static final String TAG = "NfcImpl"; |
+ private static final String DOMAIN = "w3.org"; |
+ private static final String TYPE = "webnfc"; |
+ private static final String TEXT_MIME = "text/plain"; |
+ private static final String JSON_MIME = "application/json"; |
+ private static final String OPAQUE_MIME = "application/octet-stream"; |
+ private static final String CHARSET_UTF8 = ";charset=UTF-8"; |
+ private static final String CHARSET_UTF16 = ";charset=UTF-16"; |
+ |
+ /** |
+ * Used to get instance of NFC adapter, @see android.nfc.NfcManager |
+ */ |
+ private final NfcManager mNfcManager; |
+ |
+ /** |
+ * NFC adapter. @see android.nfc.NfcAdapter |
+ */ |
+ private final NfcAdapter mNfcAdapter; |
+ |
+ /** |
+ * Activity object that is requred to enable / disable NFC reader mode operations. |
+ */ |
+ private final Activity mActivity; |
+ |
+ /** |
+ * Flag that indicates whether NFC permission is granted. |
+ */ |
+ private final boolean mHasPermission; |
+ |
+ /** |
+ * Object that contains data that was passed to method |
+ * #push(NfcMessage message, NfcPushOptions options, PushResponse callback) |
+ * @see PendingPushOperation |
+ */ |
+ private PendingPushOperation mPendingPushOperation; |
+ |
+ /** |
+ * Utility that provides I/O operations for a Tag. Created on demand when |
+ * Tag is found. @see NfcTagWriter |
+ */ |
+ private NfcTagWriter mTagWriter; |
+ |
+ /** |
+ * Pending intent that is used for foreground dispatch. |
+ * @see android.nfc.NfcAdapter.enableForegroundDispatch |
+ */ |
+ private PendingIntent mPendingIntent; |
+ |
+ public NfcImpl(Activity activity) { |
+ mActivity = activity; |
+ |
+ if (mActivity != null) { |
+ int permission = mActivity.checkPermission( |
+ Manifest.permission.NFC, Process.myPid(), Process.myUid()); |
+ mHasPermission = permission == PackageManager.PERMISSION_GRANTED; |
+ } else { |
+ mHasPermission = false; |
+ } |
+ |
+ if (mHasPermission) { |
+ mNfcManager = (NfcManager) mActivity.getSystemService(Context.NFC_SERVICE); |
+ if (mNfcManager != null) { |
+ mNfcAdapter = mNfcManager.getDefaultAdapter(); |
+ } else { |
+ Log.w(TAG, "NFC is not supported."); |
+ mNfcAdapter = null; |
+ } |
+ } else { |
+ Log.w(TAG, "NFC operations are not permitted."); |
+ mNfcAdapter = null; |
+ mNfcManager = null; |
+ } |
+ } |
+ |
+ /** |
+ * Pushes NfcMessage to Tag or Peer, whenever NFC device is in proximity. |
+ * At the moment, only passive NFC devices are supported (NfcPushTarget.TAG). |
+ * |
+ * @param message that should be pushed to NFC device. |
+ * @param options that contain information about timeout and target device type. |
+ * @param callback that is used to notify when push operation is completed. |
+ */ |
+ @Override |
+ public void push(NfcMessage message, NfcPushOptions options, PushResponse callback) { |
+ if (!checkIfReady(callback)) return; |
+ |
+ if (options.target == NfcPushTarget.PEER) { |
+ callback.call(createError(NfcErrorType.NOT_SUPPORTED)); |
+ return; |
+ } |
+ |
+ // If previous pending push operation is not completed, subsequent call |
+ // should cancel pending operation. |
+ if (mPendingPushOperation != null) { |
+ mPendingPushOperation.complete(createError(NfcErrorType.OPERATION_CANCELLED)); |
+ } |
+ |
+ mPendingPushOperation = new PendingPushOperation(message, options, callback); |
+ enableForegroundDispatch(); |
+ processPendingPushOperation(); |
+ } |
+ |
+ /** |
+ * Cancels pending push operation. |
+ * At the moment, only passive NFC devices are supported (NfcPushTarget.TAG). |
+ * |
+ * @param target @see NfcPushTarget |
+ * @param callback that is used to notify caller when cancelPush() is completed. |
+ */ |
+ @Override |
+ public void cancelPush(int target, CancelPushResponse callback) { |
+ if (!checkIfReady(callback)) return; |
+ |
+ if (target == NfcPushTarget.PEER) { |
+ callback.call(createError(NfcErrorType.NOT_SUPPORTED)); |
+ return; |
+ } |
+ |
+ if (mPendingPushOperation != null) { |
+ mPendingPushOperation.complete(createError(NfcErrorType.OPERATION_CANCELLED)); |
+ mPendingPushOperation = null; |
+ callback.call(null); |
+ disableForegroundDispatch(); |
+ } else { |
+ callback.call(createError(NfcErrorType.NOT_FOUND)); |
+ } |
+ } |
+ |
+ /** |
+ * Watch method allows to set filtering criteria for NfcMessages that are |
+ * found when NFC device is within proximity. On success, watch ID is |
+ * returned to caller through WatchResponse callback. When NfcMessage that |
+ * matches NfcWatchOptions is found, it is returned through |
+ * SubscribeToWatchEventsResponse callback interface. |
+ * @see SubscribeToWatchEventsResponse |
+ * |
+ * @param options used to filter NfcMessages, @see NfcWatchOptions. |
+ * @param callback that is used to notify caller when watch() is completed and return watch ID. |
+ */ |
+ @Override |
+ public void watch(NfcWatchOptions options, WatchResponse callback) { |
+ if (!checkIfReady(callback)) return; |
+ // TODO(shalamov): Not implemented. |
+ callback.call(0, createError(NfcErrorType.NOT_SUPPORTED)); |
+ } |
+ |
+ /** |
+ * Cancels NFC watch operation. |
+ * |
+ * @param id of watch operation. |
+ * @param callback that is used to notify caller when cancelWatch() is completed. |
+ */ |
+ @Override |
+ public void cancelWatch(int id, CancelWatchResponse callback) { |
+ if (!checkIfReady(callback)) return; |
+ // TODO(shalamov): Not implemented. |
+ callback.call(createError(NfcErrorType.NOT_SUPPORTED)); |
+ } |
+ |
+ /** |
+ * Cancels all NFC watch operations. |
+ * |
+ * @param callback that is used to notify caller when cancelAllWatches() is completed. |
+ */ |
+ @Override |
+ public void cancelAllWatches(CancelAllWatchesResponse callback) { |
+ if (!checkIfReady(callback)) return; |
+ // TODO(shalamov): Not implemented. |
+ callback.call(createError(NfcErrorType.NOT_SUPPORTED)); |
+ } |
+ |
+ @Override |
+ public void subscribeToWatchEvents(SubscribeToWatchEventsResponse callback) { |
+ // TODO(shalamov): Not implemented. |
+ } |
+ |
+ /** |
+ * Suspends all pending watch / push operations. Should be called when web |
+ * page visibility is lost. |
+ */ |
+ @Override |
+ public void suspendNfcOperations() { |
+ disableForegroundDispatch(); |
+ } |
+ |
+ /** |
+ * Resumes all pending watch / push operations. Should be called when web |
+ * page becomes visible. |
+ */ |
+ @Override |
+ public void resumeNfcOperations() { |
+ if (mPendingPushOperation != null) enableForegroundDispatch(); |
+ } |
+ |
+ @Override |
+ public void close() { |
+ disableForegroundDispatch(); |
+ } |
+ |
+ @Override |
+ public void onConnectionError(MojoException e) { |
+ close(); |
+ } |
+ |
+ @Override |
+ public void onReceive(Context context, Intent intent) { |
+ Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); |
+ if (tag != null) onTagDiscovered(tag); |
+ } |
+ |
+ /** |
+ * Holds information about pending push operation. |
+ */ |
+ private static class PendingPushOperation { |
+ private final NfcMessage mNfcMessage; |
+ private final NfcPushOptions mNfcPushOptions; |
+ private final PushResponse mPushResponseCallback; |
+ |
+ public PendingPushOperation( |
+ NfcMessage message, NfcPushOptions options, PushResponse callback) { |
+ mNfcMessage = message; |
+ mNfcPushOptions = options; |
+ mPushResponseCallback = callback; |
+ } |
+ |
+ /** |
+ * Completes pending push operation. |
+ * |
+ * @param error should be null when operation is completed successfully, |
+ * otherwise, error object with corresponding NfcErrorType must be provided. |
+ */ |
+ public void complete(NfcError error) { |
+ if (mPushResponseCallback != null) mPushResponseCallback.call(error); |
+ } |
+ |
+ public NfcMessage message() { |
+ return mNfcMessage; |
+ } |
+ public NfcPushOptions pushOptions() { |
+ return mNfcPushOptions; |
+ } |
+ } |
+ |
+ /** |
+ * Helper method that creates NfcError object from NfcErrorType. |
+ * |
+ * @param errorType @see NfcErrorType. |
+ * @return NfcError |
+ * @see NfcError |
+ */ |
+ private NfcError createError(int errorType) { |
+ NfcError error = new NfcError(); |
+ error.errorType = errorType; |
+ return error; |
+ } |
+ |
+ /** |
+ * Checks if NFC funcionality can be used by the mojo service. |
+ * If permission to use NFC is granted and hardware is enabled, returns null. |
+ * |
+ * @return NfcError |
+ */ |
+ @Nullable |
+ private NfcError checkIfReady() { |
+ if (!mHasPermission) { |
+ return createError(NfcErrorType.SECURITY); |
+ } else if (mNfcManager == null || mNfcAdapter == null |
+ || Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD_MR1) { |
+ return createError(NfcErrorType.NOT_SUPPORTED); |
+ } else if (!mNfcAdapter.isEnabled()) { |
+ return createError(NfcErrorType.DEVICE_DISABLED); |
+ } |
+ return null; |
+ } |
+ |
+ /** |
+ * Uses checkIfReady() method and if NFC functionality cannot be used, |
+ * calls mojo callback with NfcError. |
+ * |
+ * @param WatchResponse Callback that is provided to watch() method. |
+ * @return boolean true if NFC functionality can be used, false otherwise. |
+ */ |
+ private boolean checkIfReady(WatchResponse callback) { |
+ NfcError error = checkIfReady(); |
+ if (error == null) return true; |
+ callback.call(0, error); |
+ return false; |
+ } |
+ |
+ /** |
+ * Uses checkIfReady() method and if NFC functionality cannot be used, |
+ * calls mojo callback NfcError. |
+ * |
+ * @param callback Generic callback that is provided to push(), cancelPush(), |
+ * cancelWatch() and cancelAllWatches() methods. |
+ * @return boolean true if NFC functionality can be used, false otherwise. |
+ */ |
+ private boolean checkIfReady(Callbacks.Callback1<NfcError> callback) { |
+ NfcError error = checkIfReady(); |
+ if (error == null) return true; |
+ callback.call(error); |
+ return false; |
+ } |
+ |
+ /** |
+ * Enables foreground dispatch. |
+ */ |
+ private void enableForegroundDispatch() { |
+ if (mPendingIntent == null) { |
+ mPendingIntent = PendingIntent.getActivity(mActivity, 0, |
+ new Intent(mActivity, mActivity.getClass()) |
+ .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), |
+ 0); |
+ |
+ List<IntentFilter> filters = new ArrayList<IntentFilter>(); |
+ IntentFilter tag_filter = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED); |
+ IntentFilter ndef_filter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED); |
+ filters.add(tag_filter); |
+ |
+ try { |
+ ndef_filter.addDataType(TEXT_MIME); |
+ ndef_filter.addDataType(JSON_MIME); |
+ ndef_filter.addDataType(OPAQUE_MIME); |
+ filters.add(ndef_filter); |
+ } catch (MalformedMimeTypeException e) { |
+ Log.w(TAG, "Invalid MIME type was provided to Intent filter."); |
+ ndef_filter = null; |
+ } |
+ |
+ IntentFilter[] filterArray = new IntentFilter[filters.size()]; |
+ filters.toArray(filterArray); |
+ |
+ String[][] techLists = new String[][] { |
+ new String[] {Ndef.class.getName(), NdefFormatable.class.getName()}}; |
+ |
+ LocalBroadcastManager manager = LocalBroadcastManager.getInstance(mActivity); |
+ manager.registerReceiver(this, tag_filter); |
+ if (ndef_filter != null) manager.registerReceiver(this, ndef_filter); |
+ mNfcAdapter.enableForegroundDispatch(mActivity, mPendingIntent, filterArray, techLists); |
+ } |
+ } |
+ |
+ /** |
+ * Disables foreground dispatch. |
+ */ |
+ private void disableForegroundDispatch() { |
+ if (mPendingIntent != null) { |
+ mNfcAdapter.disableForegroundDispatch(mActivity); |
+ LocalBroadcastManager manager = LocalBroadcastManager.getInstance(mActivity); |
+ manager.unregisterReceiver(this); |
+ mPendingIntent = null; |
+ } |
+ } |
+ |
+ /** |
+ * NdefFormatable and Ndef interfaces have different signatures for writing |
+ * NdefMessage to a tag. This interface provides generic write method. |
+ */ |
+ private interface TagTechnologyWriter { |
+ public void write(NdefMessage message) |
+ throws IOException, TagLostException, FormatException; |
+ } |
+ |
+ /** |
+ * Implementation of TagTechnologyWriter that can write NdefMessage to NFC tag. |
+ */ |
+ private static class NdefWriter implements TagTechnologyWriter { |
+ private final Ndef mNdef; |
+ |
+ NdefWriter(Ndef ndef) { |
+ mNdef = ndef; |
+ } |
+ |
+ public void write(NdefMessage message) |
+ throws IOException, TagLostException, FormatException { |
+ mNdef.writeNdefMessage(message); |
+ } |
+ } |
+ |
+ /** |
+ * Implementation of TagTechnologyWriter that can format empty NFC tag |
+ * with provided NFCMessage. |
+ */ |
+ private static class NdefFormattableWriter implements TagTechnologyWriter { |
+ private final NdefFormatable mNdefFormattable; |
+ |
+ NdefFormattableWriter(NdefFormatable ndefFormattable) { |
+ mNdefFormattable = ndefFormattable; |
+ } |
+ |
+ public void write(NdefMessage message) |
+ throws IOException, TagLostException, FormatException { |
+ mNdefFormattable.format(message); |
+ } |
+ } |
+ |
+ /** |
+ * Utility class that holds TagTechnology and TagTechnologyWriter objects. |
+ * Provides connectivity and I/O related operations for NFC tag. |
+ */ |
+ private static class NfcTagWriter { |
+ private final TagTechnology mTech; |
+ private final TagTechnologyWriter mTechWriter; |
+ private boolean mWasConnected = false; |
+ |
+ /** |
+ * Factory method that creates NfcTagWriter with TagTechnologyWriter |
+ * appropriate for a given NFC Tag. |
+ * |
+ * @param tag @see android.nfc.Tag |
+ * @return NfcTagWriter or null when unsupported Tag is provided. |
+ */ |
+ public static NfcTagWriter create(Tag tag) { |
+ if (tag == null) return null; |
+ |
+ Ndef ndef = Ndef.get(tag); |
+ if (ndef != null) return new NfcTagWriter(ndef, new NdefWriter(ndef)); |
+ |
+ NdefFormatable formattable = NdefFormatable.get(tag); |
+ if (formattable != null) { |
+ return new NfcTagWriter(formattable, new NdefFormattableWriter(formattable)); |
+ } |
+ |
+ return null; |
+ } |
+ |
+ private NfcTagWriter(TagTechnology tech, TagTechnologyWriter writer) { |
+ mTech = tech; |
+ mTechWriter = writer; |
+ } |
+ |
+ /** |
+ * Connects to NFC tag. |
+ */ |
+ public void connect() throws IOException, TagLostException { |
+ if (!mTech.isConnected()) { |
+ mTech.connect(); |
+ mWasConnected = true; |
+ } |
+ } |
+ |
+ /** |
+ * Closes connection. |
+ */ |
+ public void close() throws IOException { |
+ mTech.close(); |
+ } |
+ |
+ /** |
+ * Writes NdefMessage to NFC tag. |
+ * |
+ * @param message @see android.nfc.NdefMessage |
+ */ |
+ public void write(NdefMessage message) |
+ throws IOException, TagLostException, FormatException { |
+ mTechWriter.write(message); |
+ } |
+ |
+ /** |
+ * If tag was previously connected and subsequent connection to the same |
+ * tag fails, consider tag to be out of ragne. |
+ */ |
+ public boolean isTagOutOfRange() { |
+ try { |
+ connect(); |
+ } catch (IOException e) { |
+ return mWasConnected; |
+ } |
+ return false; |
+ } |
+ } |
+ |
+ /** |
+ * Exception that is raised when mojo NfcMessage cannot be coverted to NdefMessage. |
+ */ |
+ private static class InvalidMessageException extends Exception {} |
+ |
+ /** |
+ * Converts mojo NfcMessage to android.nfc.NdefMessage. |
+ * |
+ * @param message mojo NfcMessage |
+ * @return NdefMessage |
+ * @see android.nfc.NdefMessage |
+ */ |
+ private NdefMessage toNdefMessage(NfcMessage message) throws InvalidMessageException { |
+ if (message == null || message.data.length == 0) throw new InvalidMessageException(); |
+ |
+ try { |
+ List<NdefRecord> records = new ArrayList<NdefRecord>(); |
+ for (NfcRecord record : message.data) { |
+ records.add(toNdefRecord(record)); |
+ } |
+ records.add(NdefRecord.createExternal(DOMAIN, TYPE, message.url.getBytes())); |
+ NdefRecord[] ndefRecords = new NdefRecord[records.size()]; |
+ records.toArray(ndefRecords); |
+ return new NdefMessage(ndefRecords); |
+ } catch (UnsupportedEncodingException | InvalidMessageException |
+ | IllegalArgumentException e) { |
+ throw new InvalidMessageException(); |
+ } |
+ } |
+ |
+ /** |
+ * Returns charset of mojo NfcRecord. Only applicable for URL and TEXT records. |
+ * If charset cannot be determined, UTF-8 charset is used by default. |
+ * |
+ * @param record |
+ * @return String |
+ */ |
+ private String getCharset(NfcRecord record) { |
+ if (record.mediaType.endsWith(CHARSET_UTF8)) return "UTF-8"; |
+ |
+ if (record.mediaType.endsWith(CHARSET_UTF16)) return "UTF-16LE"; |
+ |
+ Log.w(TAG, "Unknown charset, defaulting to UTF-8."); |
+ return "UTF-8"; |
+ } |
+ |
+ /** |
+ * Converts mojo NfcRecord to android.nfc.NdefRecord. |
+ * |
+ * @param record mojo NfcRecord |
+ * @return NdefRecord |
+ * @see android.nfc.NdefRecord |
+ */ |
+ private NdefRecord toNdefRecord(NfcRecord record) |
+ throws InvalidMessageException, IllegalArgumentException, UnsupportedEncodingException { |
+ switch (record.recordType) { |
+ case NfcRecordType.URL: |
+ return NdefRecord.createUri(new String(record.data, getCharset(record))); |
+ case NfcRecordType.TEXT: |
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
+ return NdefRecord.createTextRecord( |
+ "en-US", new String(record.data, getCharset(record))); |
+ } else { |
+ return NdefRecord.createMime(TEXT_MIME, record.data); |
+ } |
+ case NfcRecordType.JSON: |
+ case NfcRecordType.OPAQUE_RECORD: |
+ return NdefRecord.createMime(record.mediaType, record.data); |
+ default: |
+ throw new InvalidMessageException(); |
+ } |
+ } |
+ |
+ /** |
+ * Completes pending push operation. On error, invalidates #mTagWriter. |
+ * |
+ * @param error |
+ */ |
+ private void pendingPushOperationCompleted(NfcError error) { |
+ if (mPendingPushOperation != null) { |
+ mPendingPushOperation.complete(error); |
+ mPendingPushOperation = null; |
+ } |
+ |
+ if (error != null) mTagWriter = null; |
+ } |
+ |
+ /** |
+ * Checks whether there is a #mPendingPushOperation and writes data to NFC tag. |
+ * In case of exception calls pendingPushOperationCompleted() with appropriate |
+ * error object. |
+ */ |
+ private void processPendingPushOperation() { |
+ if (mTagWriter == null || mPendingPushOperation == null) return; |
+ |
+ if (mTagWriter.isTagOutOfRange()) { |
+ mTagWriter = null; |
+ return; |
+ } |
+ |
+ try { |
+ mTagWriter.connect(); |
+ mTagWriter.write(toNdefMessage(mPendingPushOperation.message())); |
+ pendingPushOperationCompleted(null); |
+ mTagWriter.close(); |
+ } catch (InvalidMessageException e) { |
+ Log.w(TAG, "Cannot write data to NFC tag. Invalid NfcMessage."); |
+ pendingPushOperationCompleted(createError(NfcErrorType.INVALID_MESSAGE)); |
+ } catch (TagLostException e) { |
+ Log.w(TAG, "Cannot write data to NFC tag. Tag is lost."); |
+ pendingPushOperationCompleted(createError(NfcErrorType.IO_ERROR)); |
+ } catch (FormatException | IOException e) { |
+ Log.w(TAG, "Cannot write data to NFC tag. IO_ERROR."); |
+ pendingPushOperationCompleted(createError(NfcErrorType.IO_ERROR)); |
+ } |
+ } |
+ |
+ /** |
+ * Called by ReaderCallbackHandler when NFC tag is in proximity. |
+ * calls processPendingPushOperation() that will write data to a tag. |
+ * |
+ * @param tag |
+ */ |
+ private void onTagDiscovered(Tag tag) { |
+ mTagWriter = NfcTagWriter.create(tag); |
+ processPendingPushOperation(); |
+ } |
+} |