diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2012-12-13 16:44:23 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2012-12-13 16:44:23 -0800 |
commit | be1939b4b6003ac7a65fcb95a3912f5e1ce8e75f (patch) | |
tree | 694dc2546dd2397528d4b78d067716f451792662 | |
parent | 525c260303268a83da4c3413b953d13c9084e834 (diff) | |
download | packages_apps_nfc-be1939b4b6003ac7a65fcb95a3912f5e1ce8e75f.zip packages_apps_nfc-be1939b4b6003ac7a65fcb95a3912f5e1ce8e75f.tar.gz packages_apps_nfc-be1939b4b6003ac7a65fcb95a3912f5e1ce8e75f.tar.bz2 |
Snapshot b80adb2c263702442cf2f2d771168400e6ceb9f8
Change-Id: I391d8e1be1a61e68b01f0db371dbb4ed3e5b5933
-rwxr-xr-x | AndroidManifest.xml | 3 | ||||
-rwxr-xr-x | nci/jni/NativeNfcManager.cpp | 10 | ||||
-rw-r--r-- | nci/jni/PeerToPeer.cpp | 4 | ||||
-rwxr-xr-x | nci/jni/SecureElement.cpp | 72 | ||||
-rw-r--r-- | src/com/android/nfc/NfcApplication.java | 28 | ||||
-rw-r--r-- | src/com/android/nfc/RegisteredComponentCache.java | 5 | ||||
-rw-r--r-- | src/com/android/nfc/handover/BluetoothHeadsetHandover.java | 58 | ||||
-rw-r--r-- | src/com/android/nfc/handover/BluetoothOppHandover.java | 72 | ||||
-rw-r--r-- | src/com/android/nfc/handover/HandoverManager.java | 818 | ||||
-rw-r--r-- | src/com/android/nfc/handover/HandoverServer.java | 3 | ||||
-rw-r--r-- | src/com/android/nfc/handover/HandoverService.java | 403 | ||||
-rw-r--r-- | src/com/android/nfc/handover/HandoverTransfer.java | 423 | ||||
-rw-r--r-- | src/com/android/nfc/handover/PendingHandoverTransfer.java | 63 |
13 files changed, 1163 insertions, 799 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 710e2ee..7b41af2 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -52,5 +52,8 @@ android:noHistory="true" /> + <service android:name=".handover.HandoverService" + android:process=":handover" + /> </application> </manifest> diff --git a/nci/jni/NativeNfcManager.cpp b/nci/jni/NativeNfcManager.cpp index f15c92f..61fc052 100755 --- a/nci/jni/NativeNfcManager.cpp +++ b/nci/jni/NativeNfcManager.cpp @@ -1276,6 +1276,7 @@ static void nfcManager_doDeselectSecureElement(JNIEnv *e, jobject o) { ALOGD ("%s: enter", __FUNCTION__); bool stat = false; + bool bRestartDiscovery = false; if (! sIsSecElemSelected) { @@ -1290,6 +1291,12 @@ static void nfcManager_doDeselectSecureElement(JNIEnv *e, jobject o) goto TheEnd; } + if (sRfEnabled) { + // Stop RF Discovery if we were polling + startRfDiscovery (false); + bRestartDiscovery = true; + } + stat = SecureElement::getInstance().routeToDefault (); sIsSecElemSelected = false; @@ -1299,6 +1306,9 @@ static void nfcManager_doDeselectSecureElement(JNIEnv *e, jobject o) SecureElement::getInstance().deactivate (0xABCDEF); TheEnd: + if (bRestartDiscovery) + startRfDiscovery (true); + //if nothing is active after this, then tell the controller to power down if (! PowerSwitch::getInstance ().setModeOff (PowerSwitch::SE_ROUTING)) PowerSwitch::getInstance ().setLevel (PowerSwitch::LOW_POWER); diff --git a/nci/jni/PeerToPeer.cpp b/nci/jni/PeerToPeer.cpp index 7994e2a..b40e366 100644 --- a/nci/jni/PeerToPeer.cpp +++ b/nci/jni/PeerToPeer.cpp @@ -1431,9 +1431,9 @@ void PeerToPeer::nfaClientCallback (tNFA_P2P_EVT p2pEvent, tNFA_P2P_EVT_DATA* ev if ((pConn = sP2p.findConnection(eventData->disc.handle)) == NULL) { // If no connection, may be a client that is trying to connect - if ((pClient = sP2p.findClientCon ((tNFA_HANDLE)NFA_HANDLE_INVALID)) == NULL) + if ((pClient = sP2p.findClient (eventData->disc.handle)) == NULL) { - ALOGE ("%s: NFA_P2P_DISC_EVT: can't find conn for NFA handle: 0x%04x", fn, eventData->disc.handle); + ALOGE ("%s: NFA_P2P_DISC_EVT: can't find client for NFA handle: 0x%04x", fn, eventData->disc.handle); return; } // Unblock createDataLinkConn() diff --git a/nci/jni/SecureElement.cpp b/nci/jni/SecureElement.cpp index 3c9256b..a4c4d57 100755 --- a/nci/jni/SecureElement.cpp +++ b/nci/jni/SecureElement.cpp @@ -156,12 +156,12 @@ bool SecureElement::initialize (nfc_jni_native_data* native) if (GetNumValue("NFA_HCI_DEFAULT_DEST_GATE", &num, sizeof(num))) mDestinationGate = num; - ALOGD ("%s: Default destination gate: %d", fn, mDestinationGate); + ALOGD ("%s: Default destination gate: 0x%X", fn, mDestinationGate); // active SE, if not set active all SEs if (GetNumValue("ACTIVE_SE", &num, sizeof(num))) mActiveSeOverride = num; - ALOGD ("%s: Active SE override: %d", fn, mActiveSeOverride); + ALOGD ("%s: Active SE override: 0x%X", fn, mActiveSeOverride); if (GetNumValue("OBERTHUR_WARM_RESET_COMMAND", &num, sizeof(num))) { @@ -442,7 +442,7 @@ jintArray SecureElement::getListOfEeHandles (JNIEnv* e) ** Function: activate ** ** Description: Turn on the secure element. -** seID: ID of secure element. +** seID: ID of secure element; 0xF3 or 0xF4. ** ** Returns: True if ok. ** @@ -474,24 +474,16 @@ bool SecureElement::activate (jint seID) return false; } - mActiveEeHandle = getDefaultEeHandle(); - ALOGD ("%s: active ee h=0x%X, override se=0x%X", fn, mActiveEeHandle, mActiveSeOverride); - if (mActiveEeHandle == NFA_HANDLE_INVALID) - { - ALOGE ("%s: ee not found", fn); - return false; - } - - UINT16 override_se = 0; + UINT16 overrideEeHandle = 0; if (mActiveSeOverride) - override_se = NFA_HANDLE_GROUP_EE | mActiveSeOverride; + overrideEeHandle = NFA_HANDLE_GROUP_EE | mActiveSeOverride; if (mRfFieldIsOn) { ALOGE("%s: RF field indication still on, resetting", fn); mRfFieldIsOn = false; } - ALOGD ("%s: override seid=0x%X", fn, override_se ); + ALOGD ("%s: override ee h=0x%X", fn, overrideEeHandle ); //activate every discovered secure element for (int index=0; index < mActualNumEe; index++) { @@ -499,7 +491,7 @@ bool SecureElement::activate (jint seID) if ((eeItem.ee_handle == EE_HANDLE_0xF3) || (eeItem.ee_handle == EE_HANDLE_0xF4)) { - if (override_se && (override_se != eeItem.ee_handle) ) + if (overrideEeHandle && (overrideEeHandle != eeItem.ee_handle) ) continue; // do not enable all SEs; only the override one if (eeItem.ee_status != NFC_NFCEE_STATUS_INACTIVE) @@ -524,18 +516,11 @@ bool SecureElement::activate (jint seID) } } //for - for (UINT8 xx = 0; xx < mActualNumEe; xx++) - { - if ((mEeInfo[xx].num_interface != 0) && (mEeInfo[xx].ee_interface[0] != NCI_NFCEE_INTERFACE_HCI_ACCESS) && - (mEeInfo[xx].ee_status != NFC_NFCEE_STATUS_INACTIVE)) - { - mActiveEeHandle = mEeInfo[xx].ee_handle; - break; - } - } - - ALOGD ("%s: exit; ok=%u", fn, numActivatedEe > 0); - return numActivatedEe > 0; + mActiveEeHandle = getDefaultEeHandle(); + if (mActiveEeHandle == NFA_HANDLE_INVALID) + ALOGE ("%s: ee handle not found", fn); + ALOGD ("%s: exit; active ee h=0x%X", fn, mActiveEeHandle); + return mActiveEeHandle != NFA_HANDLE_INVALID; } @@ -544,7 +529,7 @@ bool SecureElement::activate (jint seID) ** Function: deactivate ** ** Description: Turn off the secure element. -** seID: ID of secure element. +** seID: ID of secure element; 0xF3 or 0xF4. ** ** Returns: True if ok. ** @@ -1417,7 +1402,7 @@ void SecureElement::adjustProtocolRoutes (RouteDataSet::Database* db, RouteSelec { tNFA_HANDLE eeHandle = NFA_EE_HANDLE_DH; if (routeSelection == SecElemRoute) - eeHandle = getDefaultEeHandle (); + eeHandle = mActiveEeHandle; ALOGD ("%s: route to default EE h=0x%X", fn, eeHandle); SyncEventGuard guard (mRoutingEvent); nfaStat = NFA_EeSetDefaultProtoRouting (eeHandle, protoMask, 0, 0); @@ -1573,7 +1558,7 @@ void SecureElement::adjustTechnologyRoutes (RouteDataSet::Database* db, RouteSel { tNFA_HANDLE eeHandle = NFA_EE_HANDLE_DH; if (routeSelection == SecElemRoute) - eeHandle = getDefaultEeHandle (); + eeHandle = mActiveEeHandle; ALOGD ("%s: route to default EE h=0x%X", fn, eeHandle); SyncEventGuard guard (mRoutingEvent); nfaStat = NFA_EeSetDefaultTechRouting (eeHandle, techMask, 0, 0); @@ -1975,26 +1960,31 @@ tNFA_EE_INFO *SecureElement::findEeByHandle (tNFA_HANDLE eeHandle) *******************************************************************************/ tNFA_HANDLE SecureElement::getDefaultEeHandle () { + UINT16 overrideEeHandle = NFA_HANDLE_GROUP_EE | mActiveSeOverride; // Find the first EE that is not the HCI Access i/f. for (UINT8 xx = 0; xx < mActualNumEe; xx++) { - if ((mEeInfo[xx].num_interface != 0) && (mEeInfo[xx].ee_interface[0] != NCI_NFCEE_INTERFACE_HCI_ACCESS) ) + if (mActiveSeOverride && (overrideEeHandle != mEeInfo[xx].ee_handle)) + continue; //skip all the EE's that are ignored + if ((mEeInfo[xx].num_interface != 0) && + (mEeInfo[xx].ee_interface[0] != NCI_NFCEE_INTERFACE_HCI_ACCESS) && + (mEeInfo[xx].ee_status != NFC_NFCEE_STATUS_INACTIVE)) return (mEeInfo[xx].ee_handle); } return NFA_HANDLE_INVALID; } - /******************************************************************************* - ** - ** Function: findUiccByHandle - ** - ** Description: Find information about an execution environment. - ** eeHandle: Handle of the execution environment. - ** - ** Returns: Information about the execution environment. - ** - *******************************************************************************/ +/******************************************************************************* +** +** Function: findUiccByHandle +** +** Description: Find information about an execution environment. +** eeHandle: Handle of the execution environment. +** +** Returns: Information about the execution environment. +** +*******************************************************************************/ tNFA_EE_DISCOVER_INFO *SecureElement::findUiccByHandle (tNFA_HANDLE eeHandle) { for (UINT8 index = 0; index < mUiccInfo.num_ee; index++) diff --git a/src/com/android/nfc/NfcApplication.java b/src/com/android/nfc/NfcApplication.java index 867b8bb..3e7194d 100644 --- a/src/com/android/nfc/NfcApplication.java +++ b/src/com/android/nfc/NfcApplication.java @@ -1,12 +1,18 @@ package com.android.nfc; +import android.app.ActivityManager; +import android.app.ActivityManager.RunningAppProcessInfo; import android.app.Application; +import android.os.Process; import android.os.UserHandle; -import android.util.Log; +import java.util.Iterator; +import java.util.List; public class NfcApplication extends Application { - public static final String TAG = "NfcApplication"; + static final String TAG = "NfcApplication"; + static final String NFC_PROCESS = "com.android.nfc"; + NfcService mNfcService; public NfcApplication() { @@ -17,7 +23,23 @@ public class NfcApplication extends Application { public void onCreate() { super.onCreate(); - if (UserHandle.myUserId() == 0) { + boolean isMainProcess = false; + // We start a service in a separate process to do + // handover transfer. We don't want to instantiate an NfcService + // object in those cases, hence check the name of the process + // to determine whether we're the main NFC service, or the + // handover process + ActivityManager am = (ActivityManager)this.getSystemService(ACTIVITY_SERVICE); + List processes = am.getRunningAppProcesses(); + Iterator i = processes.iterator(); + while (i.hasNext()) { + RunningAppProcessInfo appInfo = (RunningAppProcessInfo)(i.next()); + if (appInfo.pid == Process.myPid()) { + isMainProcess = (NFC_PROCESS.equals(appInfo.processName)); + break; + } + } + if (UserHandle.myUserId() == 0 && isMainProcess) { mNfcService = new NfcService(this); } } diff --git a/src/com/android/nfc/RegisteredComponentCache.java b/src/com/android/nfc/RegisteredComponentCache.java index 5da2cd4..8d73317 100644 --- a/src/com/android/nfc/RegisteredComponentCache.java +++ b/src/com/android/nfc/RegisteredComponentCache.java @@ -43,6 +43,7 @@ import java.util.concurrent.atomic.AtomicReference; */ public class RegisteredComponentCache { private static final String TAG = "RegisteredComponentCache"; + private static final boolean DEBUG = false; final Context mContext; final String mAction; @@ -165,7 +166,9 @@ public class RegisteredComponentCache { } } - dump(components); + if (DEBUG) { + dump(components); + } synchronized (this) { mComponents = components; diff --git a/src/com/android/nfc/handover/BluetoothHeadsetHandover.java b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java index 1377160..c845f89 100644 --- a/src/com/android/nfc/handover/BluetoothHeadsetHandover.java +++ b/src/com/android/nfc/handover/BluetoothHeadsetHandover.java @@ -16,7 +16,6 @@ package com.android.nfc.handover; -import android.app.ActivityManager; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -29,12 +28,10 @@ import android.content.IntentFilter; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.UserHandle; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; -import com.android.nfc.handover.HandoverManager.HandoverPowerManager; import com.android.nfc.R; /** @@ -57,14 +54,13 @@ public class BluetoothHeadsetHandover implements BluetoothProfile.ServiceListene static final int TIMEOUT_MS = 20000; static final int STATE_INIT = 0; - static final int STATE_TURNING_ON = 1; - static final int STATE_WAITING_FOR_PROXIES = 2; - static final int STATE_INIT_COMPLETE = 3; - static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 4; - static final int STATE_BONDING = 5; - static final int STATE_CONNECTING = 6; - static final int STATE_DISCONNECTING = 7; - static final int STATE_COMPLETE = 8; + static final int STATE_WAITING_FOR_PROXIES = 1; + static final int STATE_INIT_COMPLETE = 2; + static final int STATE_WAITING_FOR_BOND_CONFIRMATION = 3; + static final int STATE_BONDING = 4; + static final int STATE_CONNECTING = 5; + static final int STATE_DISCONNECTING = 6; + static final int STATE_COMPLETE = 7; static final int RESULT_PENDING = 0; static final int RESULT_CONNECTED = 1; @@ -80,7 +76,6 @@ public class BluetoothHeadsetHandover implements BluetoothProfile.ServiceListene final Context mContext; final BluetoothDevice mDevice; final String mName; - final HandoverPowerManager mHandoverPowerManager; final Callback mCallback; final BluetoothAdapter mBluetoothAdapter; @@ -101,18 +96,21 @@ public class BluetoothHeadsetHandover implements BluetoothProfile.ServiceListene } public BluetoothHeadsetHandover(Context context, BluetoothDevice device, String name, - HandoverPowerManager powerManager, Callback callback) { + Callback callback) { checkMainThread(); // mHandler must get get constructed on Main Thread for toasts to work mContext = context; mDevice = device; mName = name; - mHandoverPowerManager = powerManager; mCallback = callback; mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mState = STATE_INIT; } + public boolean hasStarted() { + return mState != STATE_INIT; + } + /** * Main entry point. This method is usually called after construction, * to begin the BT sequence. Must be called on Main thread. @@ -156,18 +154,6 @@ public class BluetoothHeadsetHandover implements BluetoothProfile.ServiceListene void nextStepInit() { switch (mState) { case STATE_INIT: - if (!mHandoverPowerManager.isBluetoothEnabled()) { - if (mHandoverPowerManager.enableBluetooth()) { - // Bluetooth is being enabled - mState = STATE_TURNING_ON; - } else { - toast(mContext.getString(R.string.failed_to_enable_bt)); - complete(false); - } - break; - } - // fall-through - case STATE_TURNING_ON: if (mA2dp == null || mHeadset == null) { mState = STATE_WAITING_FOR_PROXIES; if (!getProfileProxys()) { @@ -310,18 +296,7 @@ public class BluetoothHeadsetHandover implements BluetoothProfile.ServiceListene void handleIntent(Intent intent) { String action = intent.getAction(); - if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action) && mState == STATE_TURNING_ON) { - int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); - if (state == BluetoothAdapter.STATE_ON) { - nextStep(); - } else if (state == BluetoothAdapter.STATE_OFF) { - toast(mContext.getString(R.string.failed_to_enable_bt)); - complete(false); - } - return; - } - - // Everything else requires the device to match... + // Everything requires the device to match... BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (!mDevice.equals(device)) return; @@ -387,19 +362,18 @@ public class BluetoothHeadsetHandover implements BluetoothProfile.ServiceListene Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)); - mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null, null, null, 0, null, null); + mContext.sendOrderedBroadcast(intent, null); intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY)); - mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null, null, null, 0, null, null); + mContext.sendOrderedBroadcast(intent, null); } void requestPairConfirmation() { Intent dialogIntent = new Intent(mContext, ConfirmConnectActivity.class); dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - dialogIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); - mContext.startActivityAsUser(dialogIntent, new UserHandle(UserHandle.USER_CURRENT)); + mContext.startActivity(dialogIntent); } final Handler mHandler = new Handler() { diff --git a/src/com/android/nfc/handover/BluetoothOppHandover.java b/src/com/android/nfc/handover/BluetoothOppHandover.java index ceb3c62..fdb5eff 100644 --- a/src/com/android/nfc/handover/BluetoothOppHandover.java +++ b/src/com/android/nfc/handover/BluetoothOppHandover.java @@ -16,25 +16,17 @@ package com.android.nfc.handover; -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; -import android.content.ActivityNotFoundException; -import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.net.Uri; import android.os.Handler; import android.os.Message; import android.os.SystemClock; +import android.os.UserHandle; import android.util.Log; import android.webkit.MimeTypeMap; -import android.widget.Toast; - -import com.android.nfc.handover.HandoverManager.HandoverPowerManager; -import com.android.nfc.R; - import java.util.ArrayList; import java.util.Arrays; @@ -61,20 +53,19 @@ public class BluetoothOppHandover implements Handler.Callback { final BluetoothDevice mDevice; final Uri[] mUris; - final HandoverPowerManager mHandoverPowerManager; final boolean mRemoteActivating; final Handler mHandler; + final Long mCreateTime; int mState; - Long mStartTime; public BluetoothOppHandover(Context context, BluetoothDevice device, Uri[] uris, - HandoverPowerManager powerManager, boolean remoteActivating) { + boolean remoteActivating) { mContext = context; mDevice = device; mUris = uris; - mHandoverPowerManager = powerManager; mRemoteActivating = remoteActivating; + mCreateTime = SystemClock.elapsedRealtime(); mHandler = new Handler(context.getMainLooper(),this); mState = STATE_INIT; @@ -104,33 +95,24 @@ public class BluetoothOppHandover implements Handler.Callback { * to begin the BT sequence. Must be called on Main thread. */ public void start() { - mStartTime = SystemClock.elapsedRealtime(); - - IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); - mContext.registerReceiver(mReceiver, filter); - - if (!mHandoverPowerManager.isBluetoothEnabled()) { - if (mHandoverPowerManager.enableBluetooth()) { - mState = STATE_TURNING_ON; - } else { - Toast.makeText(mContext, mContext.getString(R.string.beam_failed), - Toast.LENGTH_SHORT).show(); - complete(); - } - } else { - // BT already enabled - if (mRemoteActivating) { - mHandler.sendEmptyMessageDelayed(MSG_START_SEND, REMOTE_BT_ENABLE_DELAY_MS); + if (mRemoteActivating) { + Long timeElapsed = SystemClock.elapsedRealtime() - mCreateTime; + if (timeElapsed < REMOTE_BT_ENABLE_DELAY_MS) { + mHandler.sendEmptyMessageDelayed(MSG_START_SEND, + REMOTE_BT_ENABLE_DELAY_MS - timeElapsed); } else { - // Remote BT enabled too, start send immediately + // Already waited long enough for BT to come up + // - start send. sendIntent(); } + } else { + // Remote BT enabled already, start send immediately + sendIntent(); } } void complete() { mState = STATE_COMPLETE; - mContext.unregisterReceiver(mReceiver); } void sendIntent() { @@ -153,32 +135,6 @@ public class BluetoothOppHandover implements Handler.Callback { complete(); } - void handleIntent(Intent intent) { - String action = intent.getAction(); - if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action) && mState == STATE_TURNING_ON) { - int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); - if (state == BluetoothAdapter.STATE_ON) { - // Add additional delay if needed - Long timeElapsed = SystemClock.elapsedRealtime() - mStartTime; - if (mRemoteActivating && timeElapsed < REMOTE_BT_ENABLE_DELAY_MS) { - mHandler.sendEmptyMessageDelayed(MSG_START_SEND, - REMOTE_BT_ENABLE_DELAY_MS - timeElapsed); - } else { - sendIntent(); - } - } else if (state == BluetoothAdapter.STATE_OFF) { - complete(); - } - return; - } - } - - final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - handleIntent(intent); - } - }; @Override public boolean handleMessage(Message msg) { diff --git a/src/com/android/nfc/handover/HandoverManager.java b/src/com/android/nfc/handover/HandoverManager.java index e7e807d..6d2271a 100644 --- a/src/com/android/nfc/handover/HandoverManager.java +++ b/src/com/android/nfc/handover/HandoverManager.java @@ -16,134 +16,71 @@ package com.android.nfc.handover; -import java.io.File; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Date; import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; import java.util.Random; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Notification.Builder; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.BroadcastReceiver; -import android.content.ContentResolver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.media.MediaScannerConnection; +import android.content.ServiceConnection; import android.net.Uri; import android.nfc.FormatException; import android.nfc.NdefMessage; import android.nfc.NdefRecord; -import android.os.Environment; +import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.Message; -import android.os.SystemClock; +import android.os.Messenger; +import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; -import android.util.Pair; - -import com.android.nfc.NfcService; -import com.android.nfc.R; - /** * Manages handover of NFC to other technologies. */ -public class HandoverManager implements BluetoothHeadsetHandover.Callback { +public class HandoverManager { static final String TAG = "NfcHandover"; static final boolean DBG = true; + static final String ACTION_WHITELIST_DEVICE = + "android.btopp.intent.action.WHITELIST_DEVICE"; + static final byte[] TYPE_NOKIA = "nokia.com:bt".getBytes(Charset.forName("US_ASCII")); static final byte[] TYPE_BT_OOB = "application/vnd.bluetooth.ep.oob". getBytes(Charset.forName("US_ASCII")); static final byte[] RTD_COLLISION_RESOLUTION = {0x63, 0x72}; // "cr"; - static final String ACTION_BT_OPP_TRANSFER_PROGRESS = - "android.btopp.intent.action.BT_OPP_TRANSFER_PROGRESS"; - - static final String ACTION_BT_OPP_TRANSFER_DONE = - "android.btopp.intent.action.BT_OPP_TRANSFER_DONE"; - - static final String EXTRA_BT_OPP_TRANSFER_STATUS = - "android.btopp.intent.extra.BT_OPP_TRANSFER_STATUS"; - - static final String EXTRA_BT_OPP_TRANSFER_MIMETYPE = - "android.btopp.intent.extra.BT_OPP_TRANSFER_MIMETYPE"; - - static final String EXTRA_BT_OPP_ADDRESS = - "android.btopp.intent.extra.BT_OPP_ADDRESS"; - - static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0; - - static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1; - - static final String EXTRA_BT_OPP_TRANSFER_DIRECTION = - "android.btopp.intent.extra.BT_OPP_TRANSFER_DIRECTION"; - - static final int DIRECTION_BLUETOOTH_INCOMING = 0; - - static final int DIRECTION_BLUETOOTH_OUTGOING = 1; - - static final String EXTRA_BT_OPP_TRANSFER_ID = - "android.btopp.intent.extra.BT_OPP_TRANSFER_ID"; - - static final String EXTRA_BT_OPP_TRANSFER_PROGRESS = - "android.btopp.intent.extra.BT_OPP_TRANSFER_PROGRESS"; - - static final String EXTRA_BT_OPP_TRANSFER_URI = - "android.btopp.intent.extra.BT_OPP_TRANSFER_URI"; - - // permission needed to be able to receive handover status requests - static final String HANDOVER_STATUS_PERMISSION = - "com.android.permission.HANDOVER_STATUS"; - - static final int MSG_HANDOVER_POWER_CHECK = 0; - - // We poll whether we can safely disable BT every POWER_CHECK_MS - static final int POWER_CHECK_MS = 20000; - - static final String ACTION_WHITELIST_DEVICE = - "android.btopp.intent.action.WHITELIST_DEVICE"; - - static final String ACTION_CANCEL_HANDOVER_TRANSFER = - "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER"; - static final String EXTRA_SOURCE_ADDRESS = - "com.android.nfc.handover.extra.SOURCE_ADDRESS"; - - static final int SOURCE_BLUETOOTH_INCOMING = 0; - - static final int SOURCE_BLUETOOTH_OUTGOING = 1; - static final int CARRIER_POWER_STATE_INACTIVE = 0; static final int CARRIER_POWER_STATE_ACTIVE = 1; static final int CARRIER_POWER_STATE_ACTIVATING = 2; static final int CARRIER_POWER_STATE_UNKNOWN = 3; + static final int MSG_HANDOVER_COMPLETE = 0; + static final int MSG_HEADSET_CONNECTED = 1; + static final int MSG_HEADSET_NOT_CONNECTED = 2; + final Context mContext; final BluetoothAdapter mBluetoothAdapter; - final NotificationManager mNotificationManager; - final HandoverPowerManager mHandoverPowerManager; + final Messenger mMessenger = new Messenger (new MessageHandler()); - // Variables below synchronized on HandoverManager.this - final HashMap<Pair<String, Boolean>, HandoverTransfer> mTransfers; - - BluetoothHeadsetHandover mBluetoothHeadsetHandover; + final Object mLock = new Object(); + // Variables below synchronized on mLock + HashMap<Integer, PendingHandoverTransfer> mPendingTransfers; boolean mBluetoothHeadsetConnected; - + int mHandoverTransferId; + Messenger mService = null; + boolean mBound; String mLocalBluetoothAddress; - int mNotificationId; static class BluetoothHandoverData { public boolean valid = false; @@ -152,487 +89,93 @@ public class HandoverManager implements BluetoothHeadsetHandover.Callback { public boolean carrierActivating = false; } - class HandoverPowerManager implements Handler.Callback { - final Handler handler; - final Context context; - - public HandoverPowerManager(Context context) { - this.handler = new Handler(this); - this.context = context; - } - - /** - * Enables Bluetooth and will automatically disable it - * when there is no Bluetooth activity intitiated by NFC - * anymore. - */ - synchronized boolean enableBluetooth() { - // Enable BT - boolean result = mBluetoothAdapter.enableNoAutoConnect(); - - if (result) { - // Start polling for BT activity to make sure we eventually disable - // it again. - handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS); - } - return result; - } - - synchronized boolean isBluetoothEnabled() { - return mBluetoothAdapter.isEnabled(); - } - - synchronized void resetTimer() { - if (handler.hasMessages(MSG_HANDOVER_POWER_CHECK)) { - handler.removeMessages(MSG_HANDOVER_POWER_CHECK); - handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS); - } - } - - void stopMonitoring() { - handler.removeMessages(MSG_HANDOVER_POWER_CHECK); - } - + class MessageHandler extends Handler { @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_HANDOVER_POWER_CHECK: - // Check for any alive transfers - boolean transferAlive = false; - synchronized (HandoverManager.this) { - for (HandoverTransfer transfer : mTransfers.values()) { - if (transfer.isRunning()) { - transferAlive = true; - } - } - - if (!transferAlive && !mBluetoothHeadsetConnected) { - mBluetoothAdapter.disable(); - handler.removeMessages(MSG_HANDOVER_POWER_CHECK); + public void handleMessage(Message msg) { + synchronized (mLock) { + switch (msg.what) { + case MSG_HANDOVER_COMPLETE: + int transferId = msg.arg1; + Log.d(TAG, "Completed transfer id: " + Integer.toString(transferId)); + if (mPendingTransfers.containsKey(transferId)) { + mPendingTransfers.remove(transferId); } else { - handler.sendEmptyMessageDelayed(MSG_HANDOVER_POWER_CHECK, POWER_CHECK_MS); + Log.e(TAG, "Could not find completed transfer id: " + Integer.toString(transferId)); } - } - return true; - } - return false; - } - } - - /** - * A HandoverTransfer object represents a set of files - * that were received through NFC connection handover - * from the same source address. - * - * For Bluetooth, files are received through OPP, and - * we have no knowledge how many files will be transferred - * as part of a single transaction. - * Hence, a transfer has a notion of being "alive": if - * the last update to a transfer was within WAIT_FOR_NEXT_TRANSFER_MS - * milliseconds, we consider a new file transfer from the - * same source address as part of the same transfer. - * The corresponding URIs will be grouped in a single folder. - * - */ - class HandoverTransfer implements Handler.Callback, - MediaScannerConnection.OnScanCompletedListener { - // In the states below we still accept new file transfer - static final int STATE_NEW = 0; - static final int STATE_IN_PROGRESS = 1; - static final int STATE_W4_NEXT_TRANSFER = 2; - - // In the states below no new files are accepted. - static final int STATE_W4_MEDIA_SCANNER = 3; - static final int STATE_FAILED = 4; - static final int STATE_SUCCESS = 5; - static final int STATE_CANCELLED = 6; - - static final int MSG_NEXT_TRANSFER_TIMER = 0; - static final int MSG_TRANSFER_TIMEOUT = 1; - - // We need to receive an update within this time period - // to still consider this transfer to be "alive" (ie - // a reason to keep the handover transport enabled). - static final int ALIVE_CHECK_MS = 20000; - - // The amount of time to wait for a new transfer - // once the current one completes. - static final int WAIT_FOR_NEXT_TRANSFER_MS = 4000; - - static final String BEAM_DIR = "beam"; - - final BluetoothDevice device; - final String sourceAddress; - final boolean incoming; // whether this is an incoming transfer - final int notificationId; // Unique ID of this transfer used for notifications - final Handler handler; - final PendingIntent cancelIntent; - - int state; - Long lastUpdate; // Last time an event occurred for this transfer - float progress; // Progress in range [0..1] - ArrayList<Uri> btUris; // Received uris from Bluetooth OPP - ArrayList<String> btMimeTypes; // Mime-types received from Bluetooth OPP - - ArrayList<String> paths; // Raw paths on the filesystem for Beam-stored files - HashMap<String, String> mimeTypes; // Mime-types associated with each path - HashMap<String, Uri> mediaUris; // URIs found by the media scanner for each path - int urisScanned; - - public HandoverTransfer(String sourceAddress, boolean incoming) { - synchronized (HandoverManager.this) { - this.notificationId = mNotificationId++; - } - this.lastUpdate = SystemClock.elapsedRealtime(); - this.progress = 0.0f; - this.state = STATE_NEW; - this.btUris = new ArrayList<Uri>(); - this.btMimeTypes = new ArrayList<String>(); - this.paths = new ArrayList<String>(); - this.mimeTypes = new HashMap<String, String>(); - this.mediaUris = new HashMap<String, Uri>(); - this.sourceAddress = sourceAddress; - this.incoming = incoming; - this.handler = new Handler(mContext.getMainLooper(), this); - this.cancelIntent = buildCancelIntent(); - this.urisScanned = 0; - this.device = mBluetoothAdapter.getRemoteDevice(sourceAddress); - - handler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS); - } - - public synchronized void updateFileProgress(float progress) { - if (!isRunning()) return; // Ignore when we're no longer running - - handler.removeMessages(MSG_NEXT_TRANSFER_TIMER); - - this.progress = progress; - - // We're still receiving data from this device - keep it in - // the whitelist for a while longer - if (incoming) whitelistOppDevice(device); - - updateStateAndNotification(STATE_IN_PROGRESS); - } - - public synchronized void finishTransfer(boolean success, Uri uri, String mimeType) { - if (!isRunning()) return; // Ignore when we're no longer running - - if (success && uri != null) { - if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType); - this.progress = 1.0f; - if (mimeType == null) { - mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri); - } - if (mimeType != null) { - btUris.add(uri); - btMimeTypes.add(mimeType); - } else { - if (DBG) Log.d(TAG, "Could not get mimeType for file."); + break; + case MSG_HEADSET_CONNECTED: + mBluetoothHeadsetConnected = true; + break; + case MSG_HEADSET_NOT_CONNECTED: + mBluetoothHeadsetConnected = false; + break; + default: + break; } - } else { - Log.e(TAG, "Handover transfer failed"); - // Do wait to see if there's another file coming. } - handler.removeMessages(MSG_NEXT_TRANSFER_TIMER); - handler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS); - updateStateAndNotification(STATE_W4_NEXT_TRANSFER); - } - - public synchronized boolean isRunning() { - if (state != STATE_NEW && state != STATE_IN_PROGRESS && state != STATE_W4_NEXT_TRANSFER) { - return false; - } else { - return true; - } - } - - synchronized void cancel() { - if (!isRunning()) return; - - // Delete all files received so far - for (Uri uri : btUris) { - File file = new File(uri.getPath()); - if (file.exists()) file.delete(); - } - - updateStateAndNotification(STATE_CANCELLED); - } - - synchronized void updateNotification() { - if (!incoming) return; // No notifications for outgoing transfers - - Builder notBuilder = new Notification.Builder(mContext); - - if (state == STATE_NEW || state == STATE_IN_PROGRESS || - state == STATE_W4_NEXT_TRANSFER || state == STATE_W4_MEDIA_SCANNER) { - notBuilder.setAutoCancel(false); - notBuilder.setSmallIcon(android.R.drawable.stat_sys_download); - notBuilder.setTicker(mContext.getString(R.string.beam_progress)); - notBuilder.setContentTitle(mContext.getString(R.string.beam_progress)); - notBuilder.addAction(R.drawable.ic_menu_cancel_holo_dark, - mContext.getString(R.string.cancel), cancelIntent); - notBuilder.setDeleteIntent(cancelIntent); - // We do have progress indication on a per-file basis, but in a multi-file - // transfer we don't know the total progress. So for now, just show an - // indeterminate progress bar. - notBuilder.setProgress(100, 0, true); - } else if (state == STATE_SUCCESS) { - notBuilder.setAutoCancel(true); - notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); - notBuilder.setTicker(mContext.getString(R.string.beam_complete)); - notBuilder.setContentTitle(mContext.getString(R.string.beam_complete)); - notBuilder.setContentText(mContext.getString(R.string.beam_touch_to_view)); - - Intent viewIntent = buildViewIntent(); - PendingIntent contentIntent = PendingIntent.getActivityAsUser( - mContext, 0, viewIntent, 0, null, UserHandle.CURRENT); - - notBuilder.setContentIntent(contentIntent); - - // Play Beam success sound - NfcService.getInstance().playSound(NfcService.SOUND_END); - } else if (state == STATE_FAILED) { - notBuilder.setAutoCancel(false); - notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); - notBuilder.setTicker(mContext.getString(R.string.beam_failed)); - notBuilder.setContentTitle(mContext.getString(R.string.beam_failed)); - } else if (state == STATE_CANCELLED) { - notBuilder.setAutoCancel(false); - notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); - notBuilder.setTicker(mContext.getString(R.string.beam_canceled)); - notBuilder.setContentTitle(mContext.getString(R.string.beam_canceled)); - } else { - return; - } - - mNotificationManager.notifyAsUser(null, mNotificationId, notBuilder.build(), - UserHandle.CURRENT); - } - - synchronized void updateStateAndNotification(int newState) { - this.state = newState; - this.lastUpdate = SystemClock.elapsedRealtime(); - - if (handler.hasMessages(MSG_TRANSFER_TIMEOUT)) { - // Update timeout timer - handler.removeMessages(MSG_TRANSFER_TIMEOUT); - handler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS); - } - updateNotification(); - } - - synchronized void processFiles() { - // Check the amount of files we received in this transfer; - // If more than one, create a separate directory for it. - String extRoot = Environment.getExternalStorageDirectory().getPath(); - File beamPath = new File(extRoot + "/" + BEAM_DIR); - - if (!checkMediaStorage(beamPath) || btUris.size() == 0) { - Log.e(TAG, "Media storage not valid or no uris received."); - updateStateAndNotification(STATE_FAILED); - return; - } - - if (btUris.size() > 1) { - beamPath = generateMultiplePath(extRoot + "/" + BEAM_DIR + "/"); - if (!beamPath.isDirectory() && !beamPath.mkdir()) { - Log.e(TAG, "Failed to create multiple path " + beamPath.toString()); - updateStateAndNotification(STATE_FAILED); - return; - } - } - - for (int i = 0; i < btUris.size(); i++) { - Uri uri = btUris.get(i); - String mimeType = btMimeTypes.get(i); - - File srcFile = new File(uri.getPath()); - - File dstFile = generateUniqueDestination(beamPath.getAbsolutePath(), - uri.getLastPathSegment()); - if (!srcFile.renameTo(dstFile)) { - if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile); - srcFile.delete(); - return; - } else { - paths.add(dstFile.getAbsolutePath()); - mimeTypes.put(dstFile.getAbsolutePath(), mimeType); - if (DBG) Log.d(TAG, "Did successful rename from " + srcFile + " to " + dstFile); - } - } - - // We can either add files to the media provider, or provide an ACTION_VIEW - // intent to the file directly. We base this decision on the mime type - // of the first file; if it's media the platform can deal with, - // use the media provider, if it's something else, just launch an ACTION_VIEW - // on the file. - String mimeType = mimeTypes.get(paths.get(0)); - if (mimeType.startsWith("image/") || mimeType.startsWith("video/") || - mimeType.startsWith("audio/")) { - String[] arrayPaths = new String[paths.size()]; - MediaScannerConnection.scanFile(mContext, paths.toArray(arrayPaths), null, this); - updateStateAndNotification(STATE_W4_MEDIA_SCANNER); - } else { - // We're done. - updateStateAndNotification(STATE_SUCCESS); - } - } + }; - public boolean handleMessage(Message msg) { - if (msg.what == MSG_NEXT_TRANSFER_TIMER) { - // We didn't receive a new transfer in time, finalize this one - if (incoming) { - processFiles(); - } else { - updateStateAndNotification(STATE_SUCCESS); - } - return true; - } else if (msg.what == MSG_TRANSFER_TIMEOUT) { - // No update on this transfer for a while, check - // to see if it's still running, and fail it if it is. - if (isRunning()) { - updateStateAndNotification(STATE_FAILED); + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + synchronized (mLock) { + mService = new Messenger(service); + mBound = true; + // Register this client + Message msg = Message.obtain(null, HandoverService.MSG_REGISTER_CLIENT); + msg.replyTo = mMessenger; + try { + mService.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Failed to register client"); } } - return false; } - public synchronized void onScanCompleted(String path, Uri uri) { - if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri); - if (uri != null) { - mediaUris.put(path, uri); - } - urisScanned++; - if (urisScanned == paths.size()) { - // We're done - updateStateAndNotification(STATE_SUCCESS); - } - } - - boolean checkMediaStorage(File path) { - if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { - if (!path.isDirectory() && !path.mkdir()) { - Log.e(TAG, "Not dir or not mkdir " + path.getAbsolutePath()); - return false; + @Override + public void onServiceDisconnected(ComponentName name) { + synchronized (mLock) { + if (mBound) { + try { + Message msg = Message.obtain(null, HandoverService.MSG_DEREGISTER_CLIENT); + msg.replyTo = mMessenger; + mService.send(msg); + } catch (RemoteException e) { + // Service may have crashed - ignore + } } - return true; - } else { - Log.e(TAG, "External storage not mounted, can't store file."); - return false; - } - } - - synchronized Intent buildViewIntent() { - if (paths.size() == 0) return null; - - Intent viewIntent = new Intent(Intent.ACTION_VIEW); - - String filePath = paths.get(0); - Uri mediaUri = mediaUris.get(filePath); - Uri uri = mediaUri != null ? mediaUri : - Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath); - viewIntent.setDataAndTypeAndNormalize(uri, mimeTypes.get(filePath)); - viewIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return viewIntent; - } - - PendingIntent buildCancelIntent() { - Intent intent = new Intent(ACTION_CANCEL_HANDOVER_TRANSFER); - intent.putExtra(EXTRA_SOURCE_ADDRESS, sourceAddress); - PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0); - - return pi; - } - - synchronized File generateUniqueDestination(String path, String fileName) { - int dotIndex = fileName.lastIndexOf("."); - String extension = null; - String fileNameWithoutExtension = null; - if (dotIndex < 0) { - extension = ""; - fileNameWithoutExtension = fileName; - } else { - extension = fileName.substring(dotIndex); - fileNameWithoutExtension = fileName.substring(0, dotIndex); - } - File dstFile = new File(path + File.separator + fileName); - int count = 0; - while (dstFile.exists()) { - dstFile = new File(path + File.separator + fileNameWithoutExtension + "-" + - Integer.toString(count) + extension); - count++; + mService = null; + mBound = false; } - return dstFile; } - - synchronized File generateMultiplePath(String beamRoot) { - // Generate a unique directory with the date - String format = "yyyy-MM-dd"; - SimpleDateFormat sdf = new SimpleDateFormat(format); - String newPath = beamRoot + "beam-" + sdf.format(new Date()); - File newFile = new File(newPath); - int count = 0; - while (newFile.exists()) { - newPath = beamRoot + "beam-" + sdf.format(new Date()) + "-" + - Integer.toString(count); - newFile = new File(newPath); - count++; - } - - return newFile; - } - } - - synchronized HandoverTransfer getOrCreateHandoverTransfer(String sourceAddress, boolean incoming, - boolean create) { - Pair<String, Boolean> key = new Pair<String, Boolean>(sourceAddress, incoming); - if (mTransfers.containsKey(key)) { - HandoverTransfer transfer = mTransfers.get(key); - if (transfer.isRunning()) { - return transfer; - } else { - if (create) mTransfers.remove(key); // new one created below - } - } - if (create) { - HandoverTransfer transfer = new HandoverTransfer(sourceAddress, incoming); - mTransfers.put(key, transfer); - - return transfer; - } else { - return null; - } - } + }; public HandoverManager(Context context) { mContext = context; mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - mNotificationManager = (NotificationManager) mContext.getSystemService( - Context.NOTIFICATION_SERVICE); + mPendingTransfers = new HashMap<Integer, PendingHandoverTransfer>(); - mTransfers = new HashMap<Pair<String, Boolean>, HandoverTransfer>(); - mHandoverPowerManager = new HandoverPowerManager(context); + IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED); + mContext.registerReceiver(mReceiver, filter, null, null); - IntentFilter filter = new IntentFilter(ACTION_BT_OPP_TRANSFER_DONE); - filter.addAction(ACTION_BT_OPP_TRANSFER_PROGRESS); - filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); - filter.addAction(ACTION_CANCEL_HANDOVER_TRANSFER); - mContext.registerReceiver(mReceiver, filter, HANDOVER_STATUS_PERMISSION, null); + mContext.bindService(new Intent(mContext, HandoverService.class), mConnection, + Context.BIND_AUTO_CREATE, UserHandle.USER_CURRENT); } - synchronized void cleanupTransfers() { - Iterator<Map.Entry<Pair<String, Boolean>, HandoverTransfer>> it = mTransfers.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry<Pair<String, Boolean>, HandoverTransfer> pair = it.next(); - HandoverTransfer transfer = pair.getValue(); - if (!transfer.isRunning()) { - it.remove(); + final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(Intent.ACTION_USER_SWITCHED)) { + // Re-bind a service for the current user + mContext.unbindService(mConnection); + mContext.bindService(new Intent(mContext, HandoverService.class), mConnection, + Context.BIND_AUTO_CREATE, UserHandle.USER_CURRENT); } } - } + }; static NdefRecord createCollisionRecord() { byte[] random = new byte[2]; @@ -659,7 +202,7 @@ public class HandoverManager implements BluetoothHeadsetHandover.Callback { payload[0] = (byte) (payload.length & 0xFF); payload[1] = (byte) ((payload.length >> 8) & 0xFF); - synchronized (HandoverManager.this) { + synchronized (mLock) { if (mLocalBluetoothAddress == null) { mLocalBluetoothAddress = mBluetoothAdapter.getAddress(); } @@ -698,7 +241,6 @@ public class HandoverManager implements BluetoothHeadsetHandover.Callback { payload.get(payloadBytes); return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_HANDOVER_SELECT, null, payloadBytes); - } NdefRecord createHandoverRequestRecord() { @@ -742,24 +284,32 @@ public class HandoverManager implements BluetoothHeadsetHandover.Callback { } if (bluetoothData == null) return null; - boolean bluetoothActivating = false; - - synchronized(HandoverManager.this) { - if (!mHandoverPowerManager.isBluetoothEnabled()) { - if (!mHandoverPowerManager.enableBluetooth()) { - return null; - } - bluetoothActivating = true; - } else { - mHandoverPowerManager.resetTimer(); + // Note: there could be a race where we conclude + // that Bluetooth is already enabled, and shortly + // after the user turns it off. That will cause + // the transfer to fail, but there's nothing + // much we can do about it anyway. It shouldn't + // be common for the user to be changing BT settings + // while waiting to receive a picture. + boolean bluetoothActivating = !mBluetoothAdapter.isEnabled(); + synchronized (mLock) { + if (!mBound) { + Log.e(TAG, "Could not connect to handover service"); + return null; + } + Message msg = Message.obtain(null, HandoverService.MSG_START_INCOMING_TRANSFER); + PendingHandoverTransfer transfer = registerInTransferLocked(bluetoothData.device); + Bundle transferData = new Bundle(); + transferData.putParcelable(HandoverService.BUNDLE_TRANSFER, transfer); + msg.setData(transferData); + try { + mService.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Could not connect to handover service"); + removeTransferLocked(transfer.id); + return null; } - - // Create the initial transfer object - HandoverTransfer transfer = getOrCreateHandoverTransfer( - bluetoothData.device.getAddress(), true, true); - transfer.updateNotification(); } - // BT OOB found, whitelist it for incoming OPP data whitelistOppDevice(bluetoothData.device); @@ -767,13 +317,6 @@ public class HandoverManager implements BluetoothHeadsetHandover.Callback { return (createHandoverSelectMessage(bluetoothActivating)); } - void whitelistOppDevice(BluetoothDevice device) { - if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP"); - Intent intent = new Intent(ACTION_WHITELIST_DEVICE); - intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); - mContext.sendBroadcast(intent); - } - public boolean tryHandover(NdefMessage m) { if (m == null) return false; if (mBluetoothAdapter == null) return false; @@ -784,18 +327,26 @@ public class HandoverManager implements BluetoothHeadsetHandover.Callback { if (handover == null) return false; if (!handover.valid) return true; - synchronized (HandoverManager.this) { + synchronized (mLock) { if (mBluetoothAdapter == null) { if (DBG) Log.d(TAG, "BT handover, but BT not available"); return true; } - if (mBluetoothHeadsetHandover != null) { - if (DBG) Log.d(TAG, "BT handover already in progress, ignoring"); - return true; + if (!mBound) { + Log.e(TAG, "Could not connect to handover service"); + return false; + } + + Message msg = Message.obtain(null, HandoverService.MSG_HEADSET_HANDOVER, 0, 0); + Bundle headsetData = new Bundle(); + headsetData.putParcelable(HandoverService.EXTRA_HEADSET_DEVICE, handover.device); + headsetData.putString(HandoverService.EXTRA_HEADSET_NAME, handover.name); + msg.setData(headsetData); + try { + mService.send(msg); + } catch (RemoteException e) { + return false; } - mBluetoothHeadsetHandover = new BluetoothHeadsetHandover(mContext, handover.device, - handover.name, mHandoverPowerManager, this); - mBluetoothHeadsetHandover.start(); } return true; } @@ -807,13 +358,53 @@ public class HandoverManager implements BluetoothHeadsetHandover.Callback { BluetoothHandoverData data = parse(m); if (data != null && data.valid) { // Register a new handover transfer object - getOrCreateHandoverTransfer(data.device.getAddress(), false, true); - BluetoothOppHandover handover = new BluetoothOppHandover(mContext, data.device, - uris, mHandoverPowerManager, data.carrierActivating); - handover.start(); + synchronized (mLock) { + if (!mBound) { + Log.e(TAG, "Could not connect to handover service"); + return; + } + + Message msg = Message.obtain(null, HandoverService.MSG_START_OUTGOING_TRANSFER, 0, 0); + PendingHandoverTransfer transfer = registerOutTransferLocked(data, uris); + Bundle transferData = new Bundle(); + transferData.putParcelable(HandoverService.BUNDLE_TRANSFER, transfer); + msg.setData(transferData); + try { + mService.send(msg); + } catch (RemoteException e) { + removeTransferLocked(transfer.id); + } + } } } + PendingHandoverTransfer registerInTransferLocked(BluetoothDevice remoteDevice) { + PendingHandoverTransfer transfer = new PendingHandoverTransfer( + mHandoverTransferId++, true, remoteDevice, false, null); + mPendingTransfers.put(transfer.id, transfer); + + return transfer; + } + + PendingHandoverTransfer registerOutTransferLocked(BluetoothHandoverData data, + Uri[] uris) { + PendingHandoverTransfer transfer = new PendingHandoverTransfer( + mHandoverTransferId++, false, data.device, data.carrierActivating, uris); + mPendingTransfers.put(transfer.id, transfer); + return transfer; + } + + void removeTransferLocked(int id) { + mPendingTransfers.remove(id); + } + + void whitelistOppDevice(BluetoothDevice device) { + if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP"); + Intent intent = new Intent(ACTION_WHITELIST_DEVICE); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } + boolean isCarrierActivating(NdefRecord handoverRec, byte[] carrierId) { byte[] payload = handoverRec.getPayload(); if (payload == null || payload.length <= 1) return false; @@ -972,79 +563,4 @@ public class HandoverManager implements BluetoothHeadsetHandover.Callback { return result; } - - @Override - public void onBluetoothHeadsetHandoverComplete(boolean connected) { - synchronized (HandoverManager.this) { - mBluetoothHeadsetHandover = null; - mBluetoothHeadsetConnected = connected; - } - } - - final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - - if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { - int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); - if (state == BluetoothAdapter.STATE_OFF) { - mHandoverPowerManager.stopMonitoring(); - } - - return; - } else if (action.equals(ACTION_CANCEL_HANDOVER_TRANSFER)) { - String sourceAddress = intent.getStringExtra(EXTRA_SOURCE_ADDRESS); - HandoverTransfer transfer = getOrCreateHandoverTransfer(sourceAddress, true, - false); - if (transfer != null) { - transfer.cancel(); - } - } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS) || - action.equals(ACTION_BT_OPP_TRANSFER_DONE)) { - // Clean up old transfers no longer in progress - cleanupTransfers(); - - int direction = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_DIRECTION, -1); - int id = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_ID, -1); - String sourceAddress = intent.getStringExtra(EXTRA_BT_OPP_ADDRESS); - - if (direction == -1 || id == -1 || sourceAddress == null) return; - boolean incoming = (direction == DIRECTION_BLUETOOTH_INCOMING); - - HandoverTransfer transfer = getOrCreateHandoverTransfer(sourceAddress, incoming, - false); - if (transfer == null) { - // There is no transfer running for this source address; most likely - // the transfer was cancelled. We need to tell BT OPP to stop transferring - // in case this was an incoming transfer - Intent cancelIntent = new Intent("android.btopp.intent.action.STOP_HANDOVER_TRANSFER"); - cancelIntent.putExtra(EXTRA_BT_OPP_TRANSFER_ID, id); - mContext.sendBroadcast(cancelIntent); - return; - } - - if (action.equals(ACTION_BT_OPP_TRANSFER_DONE)) { - int handoverStatus = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_STATUS, - HANDOVER_TRANSFER_STATUS_FAILURE); - - if (handoverStatus == HANDOVER_TRANSFER_STATUS_SUCCESS) { - String uriString = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_URI); - String mimeType = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_MIMETYPE); - Uri uri = Uri.parse(uriString); - if (uri.getScheme() == null) { - uri = Uri.fromFile(new File(uri.getPath())); - } - transfer.finishTransfer(true, uri, mimeType); - } else { - transfer.finishTransfer(false, null, null); - } - } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS)) { - float progress = intent.getFloatExtra(EXTRA_BT_OPP_TRANSFER_PROGRESS, 0.0f); - transfer.updateFileProgress(progress); - } - } - } - }; - } diff --git a/src/com/android/nfc/handover/HandoverServer.java b/src/com/android/nfc/handover/HandoverServer.java index e789387..093d1dd 100644 --- a/src/com/android/nfc/handover/HandoverServer.java +++ b/src/com/android/nfc/handover/HandoverServer.java @@ -210,7 +210,8 @@ public final class HandoverServer { } // We're done mCallback.onHandoverRequestReceived(); - break; + // We can process another handover transfer + byteStream = new ByteArrayOutputStream(); } synchronized (HandoverServer.this) { diff --git a/src/com/android/nfc/handover/HandoverService.java b/src/com/android/nfc/handover/HandoverService.java new file mode 100644 index 0000000..261cbca --- /dev/null +++ b/src/com/android/nfc/handover/HandoverService.java @@ -0,0 +1,403 @@ +package com.android.nfc.handover; + +import android.app.Service; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.SoundPool; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.RemoteException; +import android.util.Log; +import android.util.Pair; + +import com.android.nfc.R; + +import java.io.File; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Queue; + +public class HandoverService extends Service implements HandoverTransfer.Callback, + BluetoothHeadsetHandover.Callback { + + static final String TAG = "HandoverService"; + + static final int MSG_REGISTER_CLIENT = 0; + static final int MSG_DEREGISTER_CLIENT = 1; + static final int MSG_START_INCOMING_TRANSFER = 2; + static final int MSG_START_OUTGOING_TRANSFER = 3; + static final int MSG_HEADSET_HANDOVER = 4; + + static final String BUNDLE_TRANSFER = "transfer"; + + static final String EXTRA_HEADSET_DEVICE = "device"; + static final String EXTRA_HEADSET_NAME = "headsetname"; + + static final String ACTION_CANCEL_HANDOVER_TRANSFER = + "com.android.nfc.handover.action.CANCEL_HANDOVER_TRANSFER"; + static final String EXTRA_SOURCE_ADDRESS = + "com.android.nfc.handover.extra.SOURCE_ADDRESS"; + + static final String ACTION_BT_OPP_TRANSFER_PROGRESS = + "android.btopp.intent.action.BT_OPP_TRANSFER_PROGRESS"; + + static final String ACTION_BT_OPP_TRANSFER_DONE = + "android.btopp.intent.action.BT_OPP_TRANSFER_DONE"; + + static final String EXTRA_BT_OPP_TRANSFER_STATUS = + "android.btopp.intent.extra.BT_OPP_TRANSFER_STATUS"; + + static final String EXTRA_BT_OPP_TRANSFER_MIMETYPE = + "android.btopp.intent.extra.BT_OPP_TRANSFER_MIMETYPE"; + + static final String EXTRA_BT_OPP_ADDRESS = + "android.btopp.intent.extra.BT_OPP_ADDRESS"; + + static final int HANDOVER_TRANSFER_STATUS_SUCCESS = 0; + + static final int HANDOVER_TRANSFER_STATUS_FAILURE = 1; + + static final String EXTRA_BT_OPP_TRANSFER_DIRECTION = + "android.btopp.intent.extra.BT_OPP_TRANSFER_DIRECTION"; + + static final int DIRECTION_BLUETOOTH_INCOMING = 0; + + static final int DIRECTION_BLUETOOTH_OUTGOING = 1; + + static final String EXTRA_BT_OPP_TRANSFER_ID = + "android.btopp.intent.extra.BT_OPP_TRANSFER_ID"; + + static final String EXTRA_BT_OPP_TRANSFER_PROGRESS = + "android.btopp.intent.extra.BT_OPP_TRANSFER_PROGRESS"; + + static final String EXTRA_BT_OPP_TRANSFER_URI = + "android.btopp.intent.extra.BT_OPP_TRANSFER_URI"; + + // permission needed to be able to receive handover status requests + static final String HANDOVER_STATUS_PERMISSION = + "com.android.permission.HANDOVER_STATUS"; + + // Variables below only accessed on main thread + final Queue<BluetoothOppHandover> mPendingOutTransfers; + final HashMap<Pair<String, Boolean>, HandoverTransfer> mTransfers; + final Messenger mMessenger; + + SoundPool mSoundPool; + int mSuccessSound; + + BluetoothAdapter mBluetoothAdapter; + Messenger mClient; + Handler mHandler; + BluetoothHeadsetHandover mBluetoothHeadsetHandover; + boolean mBluetoothHeadsetConnected; + boolean mBluetoothEnabledByNfc; + + public HandoverService() { + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + mPendingOutTransfers = new LinkedList<BluetoothOppHandover>(); + mTransfers = new HashMap<Pair<String, Boolean>, HandoverTransfer>(); + mHandler = new MessageHandler(); + mMessenger = new Messenger(mHandler); + mBluetoothHeadsetConnected = false; + mBluetoothEnabledByNfc = false; + } + + @Override + public void onCreate() { + super.onCreate(); + + mSoundPool = new SoundPool(1, AudioManager.STREAM_NOTIFICATION, 0); + mSuccessSound = mSoundPool.load(this, R.raw.end, 1); + + IntentFilter filter = new IntentFilter(ACTION_BT_OPP_TRANSFER_DONE); + filter.addAction(ACTION_BT_OPP_TRANSFER_PROGRESS); + filter.addAction(ACTION_CANCEL_HANDOVER_TRANSFER); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + registerReceiver(mReceiver, filter, HANDOVER_STATUS_PERMISSION, mHandler); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mSoundPool != null) { + mSoundPool.release(); + } + unregisterReceiver(mReceiver); + } + + void doOutgoingTransfer(Message msg) { + Bundle msgData = msg.getData(); + + msgData.setClassLoader(getClassLoader()); + PendingHandoverTransfer pendingTransfer = (PendingHandoverTransfer) + msgData.getParcelable(BUNDLE_TRANSFER); + createHandoverTransfer(pendingTransfer); + + // Create the actual transfer + BluetoothOppHandover handover = new BluetoothOppHandover(HandoverService.this, + pendingTransfer.remoteDevice, pendingTransfer.uris, + pendingTransfer.remoteActivating); + if (mBluetoothAdapter.isEnabled()) { + // Start the transfer + handover.start(); + } else { + if (!enableBluetooth()) { + Log.e(TAG, "Error enabling Bluetooth."); + notifyClientTransferComplete(pendingTransfer.id); + return; + } + mPendingOutTransfers.add(handover); + // Queue the transfer and enable Bluetooth - when it is enabled + // the transfer will be started. + } + } + + void doIncomingTransfer(Message msg) { + Bundle msgData = msg.getData(); + + msgData.setClassLoader(getClassLoader()); + PendingHandoverTransfer pendingTransfer = (PendingHandoverTransfer) + msgData.getParcelable(BUNDLE_TRANSFER); + if (!mBluetoothAdapter.isEnabled() && !enableBluetooth()) { + Log.e(TAG, "Error enabling Bluetooth."); + notifyClientTransferComplete(pendingTransfer.id); + return; + } + createHandoverTransfer(pendingTransfer); + // Remote device will connect and finish the transfer + } + + void doHeadsetHandover(Message msg) { + Bundle msgData = msg.getData(); + BluetoothDevice device = (BluetoothDevice) msgData.getParcelable(EXTRA_HEADSET_DEVICE); + String name = (String) msgData.getString(EXTRA_HEADSET_NAME); + mBluetoothHeadsetHandover = new BluetoothHeadsetHandover(HandoverService.this, + device, name, HandoverService.this); + if (mBluetoothAdapter.isEnabled()) { + mBluetoothHeadsetHandover.start(); + } else { + // Once BT is enabled, the headset pairing will be started + if (!enableBluetooth()) { + Log.e(TAG, "Error enabling Bluetooth."); + mBluetoothHeadsetHandover = null; + } + } + } + + void startPendingTransfers() { + while (!mPendingOutTransfers.isEmpty()) { + BluetoothOppHandover handover = mPendingOutTransfers.remove(); + handover.start(); + } + } + + class MessageHandler extends Handler { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REGISTER_CLIENT: + mClient = msg.replyTo; + break; + case MSG_DEREGISTER_CLIENT: + mClient = null; + break; + case MSG_START_INCOMING_TRANSFER: + doIncomingTransfer(msg); + break; + case MSG_START_OUTGOING_TRANSFER: + doOutgoingTransfer(msg); + break; + case MSG_HEADSET_HANDOVER: + doHeadsetHandover(msg); + break; + } + } + } + + boolean enableBluetooth() { + if (!mBluetoothAdapter.isEnabled()) { + mBluetoothEnabledByNfc = true; + return mBluetoothAdapter.enableNoAutoConnect(); + } + return true; + } + + void disableBluetoothIfNeeded() { + if (!mBluetoothEnabledByNfc) return; + + if (mTransfers.size() == 0 && !mBluetoothHeadsetConnected) { + mBluetoothAdapter.disable(); + mBluetoothEnabledByNfc = false; + } + } + + void createHandoverTransfer(PendingHandoverTransfer pendingTransfer) { + Pair<String, Boolean> key = new Pair<String, Boolean>( + pendingTransfer.remoteDevice.getAddress(), pendingTransfer.incoming); + if (mTransfers.containsKey(key)) { + HandoverTransfer transfer = mTransfers.get(key); + if (!transfer.isRunning()) { + mTransfers.remove(key); // new one created below + } else { + // There is already a transfer running to this + // device - it will automatically get combined + // with the existing transfer. + notifyClientTransferComplete(pendingTransfer.id); + return; + } + } + + HandoverTransfer transfer = new HandoverTransfer(this, this, pendingTransfer); + mTransfers.put(key, transfer); + transfer.updateNotification(); + } + + HandoverTransfer findHandoverTransfer(String sourceAddress, boolean incoming) { + Pair<String, Boolean> key = new Pair<String, Boolean>(sourceAddress, incoming); + if (mTransfers.containsKey(key)) { + HandoverTransfer transfer = mTransfers.get(key); + if (transfer.isRunning()) { + return transfer; + } + } + return null; + } + + @Override + public IBinder onBind(Intent intent) { + return mMessenger.getBinder(); + } + + final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); + if (state == BluetoothAdapter.STATE_ON) { + // If there is a pending headset pairing, start it + if (mBluetoothHeadsetHandover != null && + !mBluetoothHeadsetHandover.hasStarted()) { + mBluetoothHeadsetHandover.start(); + } + + // Start any pending transfers + startPendingTransfers(); + } else if (state == BluetoothAdapter.STATE_OFF) { + mBluetoothEnabledByNfc = false; + mBluetoothHeadsetConnected = false; + } + } + else if (action.equals(ACTION_CANCEL_HANDOVER_TRANSFER)) { + String sourceAddress = intent.getStringExtra(EXTRA_SOURCE_ADDRESS); + HandoverTransfer transfer = findHandoverTransfer(sourceAddress, true); + if (transfer != null) { + transfer.cancel(); + } + } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS) || + action.equals(ACTION_BT_OPP_TRANSFER_DONE)) { + int direction = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_DIRECTION, -1); + int id = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_ID, -1); + String sourceAddress = intent.getStringExtra(EXTRA_BT_OPP_ADDRESS); + + if (direction == -1 || id == -1 || sourceAddress == null) return; + boolean incoming = (direction == DIRECTION_BLUETOOTH_INCOMING); + + HandoverTransfer transfer = findHandoverTransfer(sourceAddress, incoming); + if (transfer == null) { + // There is no transfer running for this source address; most likely + // the transfer was cancelled. We need to tell BT OPP to stop transferring + // in case this was an incoming transfer + Intent cancelIntent = new Intent("android.btopp.intent.action.STOP_HANDOVER_TRANSFER"); + cancelIntent.putExtra(EXTRA_BT_OPP_TRANSFER_ID, id); + sendBroadcast(cancelIntent); + return; + } + + if (action.equals(ACTION_BT_OPP_TRANSFER_DONE)) { + int handoverStatus = intent.getIntExtra(EXTRA_BT_OPP_TRANSFER_STATUS, + HANDOVER_TRANSFER_STATUS_FAILURE); + if (handoverStatus == HANDOVER_TRANSFER_STATUS_SUCCESS) { + String uriString = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_URI); + String mimeType = intent.getStringExtra(EXTRA_BT_OPP_TRANSFER_MIMETYPE); + Uri uri = Uri.parse(uriString); + if (uri.getScheme() == null) { + uri = Uri.fromFile(new File(uri.getPath())); + } + transfer.finishTransfer(true, uri, mimeType); + } else { + transfer.finishTransfer(false, null, null); + } + } else if (action.equals(ACTION_BT_OPP_TRANSFER_PROGRESS)) { + float progress = intent.getFloatExtra(EXTRA_BT_OPP_TRANSFER_PROGRESS, 0.0f); + transfer.updateFileProgress(progress); + } + } + } + }; + + void notifyClientTransferComplete(int transferId) { + if (mClient != null) { + Message msg = Message.obtain(null, HandoverManager.MSG_HANDOVER_COMPLETE); + msg.arg1 = transferId; + try { + mClient.send(msg); + } catch (RemoteException e) { + // Ignore + } + } + } + + @Override + public void onTransferComplete(HandoverTransfer transfer, boolean success) { + // Called on the main thread + + // First, remove the transfer from our list + Iterator it = mTransfers.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry hashPair = (Map.Entry)it.next(); + HandoverTransfer transferEntry = (HandoverTransfer) hashPair.getValue(); + if (transferEntry == transfer) { + it.remove(); + } + } + + // Notify any clients of the service + notifyClientTransferComplete(transfer.getTransferId()); + + // Play success sound + if (success) { + mSoundPool.play(mSuccessSound, 1.0f, 1.0f, 0, 0, 1.0f); + } + disableBluetoothIfNeeded(); + } + + @Override + public void onBluetoothHeadsetHandoverComplete(boolean connected) { + // Called on the main thread + mBluetoothHeadsetHandover = null; + mBluetoothHeadsetConnected = connected; + if (mClient != null) { + Message msg = Message.obtain(null, + connected ? HandoverManager.MSG_HEADSET_CONNECTED + : HandoverManager.MSG_HEADSET_NOT_CONNECTED); + try { + mClient.send(msg); + } catch (RemoteException e) { + // Ignore + } + } + disableBluetoothIfNeeded(); + } +} diff --git a/src/com/android/nfc/handover/HandoverTransfer.java b/src/com/android/nfc/handover/HandoverTransfer.java new file mode 100644 index 0000000..98b59a6 --- /dev/null +++ b/src/com/android/nfc/handover/HandoverTransfer.java @@ -0,0 +1,423 @@ +package com.android.nfc.handover; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Notification.Builder; +import android.bluetooth.BluetoothDevice; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.Log; + +import com.android.nfc.R; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; + +/** + * A HandoverTransfer object represents a set of files + * that were received through NFC connection handover + * from the same source address. + * + * For Bluetooth, files are received through OPP, and + * we have no knowledge how many files will be transferred + * as part of a single transaction. + * Hence, a transfer has a notion of being "alive": if + * the last update to a transfer was within WAIT_FOR_NEXT_TRANSFER_MS + * milliseconds, we consider a new file transfer from the + * same source address as part of the same transfer. + * The corresponding URIs will be grouped in a single folder. + * + */ +public class HandoverTransfer implements Handler.Callback, + MediaScannerConnection.OnScanCompletedListener { + + interface Callback { + void onTransferComplete(HandoverTransfer transfer, boolean success); + }; + + static final String TAG = "HandoverTransfer"; + + static final Boolean DBG = true; + + // In the states below we still accept new file transfer + static final int STATE_NEW = 0; + static final int STATE_IN_PROGRESS = 1; + static final int STATE_W4_NEXT_TRANSFER = 2; + + // In the states below no new files are accepted. + static final int STATE_W4_MEDIA_SCANNER = 3; + static final int STATE_FAILED = 4; + static final int STATE_SUCCESS = 5; + static final int STATE_CANCELLED = 6; + + static final int MSG_NEXT_TRANSFER_TIMER = 0; + static final int MSG_TRANSFER_TIMEOUT = 1; + + // We need to receive an update within this time period + // to still consider this transfer to be "alive" (ie + // a reason to keep the handover transport enabled). + static final int ALIVE_CHECK_MS = 20000; + + // The amount of time to wait for a new transfer + // once the current one completes. + static final int WAIT_FOR_NEXT_TRANSFER_MS = 4000; + + static final String BEAM_DIR = "beam"; + + final boolean mIncoming; // whether this is an incoming transfer + final int mTransferId; // Unique ID of this transfer used for notifications + final PendingIntent mCancelIntent; + final Context mContext; + final Handler mHandler; + final NotificationManager mNotificationManager; + final BluetoothDevice mRemoteDevice; + final Callback mCallback; + + // Variables below are only accessed on the main thread + int mState; + boolean mCalledBack; + Long mLastUpdate; // Last time an event occurred for this transfer + float mProgress; // Progress in range [0..1] + ArrayList<Uri> mBtUris; // Received uris from Bluetooth OPP + ArrayList<String> mBtMimeTypes; // Mime-types received from Bluetooth OPP + + ArrayList<String> mPaths; // Raw paths on the filesystem for Beam-stored files + HashMap<String, String> mMimeTypes; // Mime-types associated with each path + HashMap<String, Uri> mMediaUris; // URIs found by the media scanner for each path + int mUrisScanned; + + public HandoverTransfer(Context context, Callback callback, + PendingHandoverTransfer pendingTransfer) { + mContext = context; + mCallback = callback; + mRemoteDevice = pendingTransfer.remoteDevice; + mIncoming = pendingTransfer.incoming; + mTransferId = pendingTransfer.id; + mLastUpdate = SystemClock.elapsedRealtime(); + mProgress = 0.0f; + mState = STATE_NEW; + mBtUris = new ArrayList<Uri>(); + mBtMimeTypes = new ArrayList<String>(); + mPaths = new ArrayList<String>(); + mMimeTypes = new HashMap<String, String>(); + mMediaUris = new HashMap<String, Uri>(); + mCancelIntent = buildCancelIntent(); + mUrisScanned = 0; + + mHandler = new Handler(Looper.getMainLooper(), this); + mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS); + mNotificationManager = (NotificationManager) mContext.getSystemService( + Context.NOTIFICATION_SERVICE); + } + + void whitelistOppDevice(BluetoothDevice device) { + if (DBG) Log.d(TAG, "Whitelisting " + device + " for BT OPP"); + Intent intent = new Intent(HandoverManager.ACTION_WHITELIST_DEVICE); + intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); + mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); + } + + public void updateFileProgress(float progress) { + if (!isRunning()) return; // Ignore when we're no longer running + + mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER); + + this.mProgress = progress; + + // We're still receiving data from this device - keep it in + // the whitelist for a while longer + if (mIncoming) whitelistOppDevice(mRemoteDevice); + + updateStateAndNotification(STATE_IN_PROGRESS); + } + + public void finishTransfer(boolean success, Uri uri, String mimeType) { + if (!isRunning()) return; // Ignore when we're no longer running + + if (success && uri != null) { + if (DBG) Log.d(TAG, "Transfer success, uri " + uri + " mimeType " + mimeType); + this.mProgress = 1.0f; + if (mimeType == null) { + mimeType = BluetoothOppHandover.getMimeTypeForUri(mContext, uri); + } + if (mimeType != null) { + mBtUris.add(uri); + mBtMimeTypes.add(mimeType); + } else { + if (DBG) Log.d(TAG, "Could not get mimeType for file."); + } + } else { + Log.e(TAG, "Handover transfer failed"); + // Do wait to see if there's another file coming. + } + mHandler.removeMessages(MSG_NEXT_TRANSFER_TIMER); + mHandler.sendEmptyMessageDelayed(MSG_NEXT_TRANSFER_TIMER, WAIT_FOR_NEXT_TRANSFER_MS); + updateStateAndNotification(STATE_W4_NEXT_TRANSFER); + } + + public boolean isRunning() { + if (mState != STATE_NEW && mState != STATE_IN_PROGRESS && mState != STATE_W4_NEXT_TRANSFER) { + return false; + } else { + return true; + } + } + + void cancel() { + if (!isRunning()) return; + + // Delete all files received so far + for (Uri uri : mBtUris) { + File file = new File(uri.getPath()); + if (file.exists()) file.delete(); + } + + updateStateAndNotification(STATE_CANCELLED); + } + + void updateNotification() { + if (!mIncoming) return; // No notifications for outgoing transfers + + Builder notBuilder = new Notification.Builder(mContext); + + if (mState == STATE_NEW || mState == STATE_IN_PROGRESS || + mState == STATE_W4_NEXT_TRANSFER || mState == STATE_W4_MEDIA_SCANNER) { + notBuilder.setAutoCancel(false); + notBuilder.setSmallIcon(android.R.drawable.stat_sys_download); + notBuilder.setTicker(mContext.getString(R.string.beam_progress)); + notBuilder.setContentTitle(mContext.getString(R.string.beam_progress)); + notBuilder.addAction(R.drawable.ic_menu_cancel_holo_dark, + mContext.getString(R.string.cancel), mCancelIntent); + notBuilder.setDeleteIntent(mCancelIntent); + // We do have progress indication on a per-file basis, but in a multi-file + // transfer we don't know the total progress. So for now, just show an + // indeterminate progress bar. + notBuilder.setProgress(100, 0, true); + } else if (mState == STATE_SUCCESS) { + notBuilder.setAutoCancel(true); + notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); + notBuilder.setTicker(mContext.getString(R.string.beam_complete)); + notBuilder.setContentTitle(mContext.getString(R.string.beam_complete)); + notBuilder.setContentText(mContext.getString(R.string.beam_touch_to_view)); + + Intent viewIntent = buildViewIntent(); + PendingIntent contentIntent = PendingIntent.getActivity( + mContext, 0, viewIntent, 0, null); + + notBuilder.setContentIntent(contentIntent); + } else if (mState == STATE_FAILED) { + notBuilder.setAutoCancel(false); + notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); + notBuilder.setTicker(mContext.getString(R.string.beam_failed)); + notBuilder.setContentTitle(mContext.getString(R.string.beam_failed)); + } else if (mState == STATE_CANCELLED) { + notBuilder.setAutoCancel(false); + notBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done); + notBuilder.setTicker(mContext.getString(R.string.beam_canceled)); + notBuilder.setContentTitle(mContext.getString(R.string.beam_canceled)); + } else { + return; + } + + mNotificationManager.notify(null, mTransferId, notBuilder.build()); + } + + void updateStateAndNotification(int newState) { + this.mState = newState; + this.mLastUpdate = SystemClock.elapsedRealtime(); + + if (mHandler.hasMessages(MSG_TRANSFER_TIMEOUT)) { + // Update timeout timer + mHandler.removeMessages(MSG_TRANSFER_TIMEOUT); + mHandler.sendEmptyMessageDelayed(MSG_TRANSFER_TIMEOUT, ALIVE_CHECK_MS); + } + + updateNotification(); + + if ((mState == STATE_SUCCESS || mState == STATE_FAILED || mState == STATE_CANCELLED) + && !mCalledBack) { + mCalledBack = true; + // Notify that we're done with this transfer + mCallback.onTransferComplete(this, mState == STATE_SUCCESS); + } + } + + void processFiles() { + // Check the amount of files we received in this transfer; + // If more than one, create a separate directory for it. + String extRoot = Environment.getExternalStorageDirectory().getPath(); + File beamPath = new File(extRoot + "/" + BEAM_DIR); + + if (!checkMediaStorage(beamPath) || mBtUris.size() == 0) { + Log.e(TAG, "Media storage not valid or no uris received."); + updateStateAndNotification(STATE_FAILED); + return; + } + + if (mBtUris.size() > 1) { + beamPath = generateMultiplePath(extRoot + "/" + BEAM_DIR + "/"); + if (!beamPath.isDirectory() && !beamPath.mkdir()) { + Log.e(TAG, "Failed to create multiple path " + beamPath.toString()); + updateStateAndNotification(STATE_FAILED); + return; + } + } + + for (int i = 0; i < mBtUris.size(); i++) { + Uri uri = mBtUris.get(i); + String mimeType = mBtMimeTypes.get(i); + + File srcFile = new File(uri.getPath()); + + File dstFile = generateUniqueDestination(beamPath.getAbsolutePath(), + uri.getLastPathSegment()); + if (!srcFile.renameTo(dstFile)) { + if (DBG) Log.d(TAG, "Failed to rename from " + srcFile + " to " + dstFile); + srcFile.delete(); + return; + } else { + mPaths.add(dstFile.getAbsolutePath()); + mMimeTypes.put(dstFile.getAbsolutePath(), mimeType); + if (DBG) Log.d(TAG, "Did successful rename from " + srcFile + " to " + dstFile); + } + } + + // We can either add files to the media provider, or provide an ACTION_VIEW + // intent to the file directly. We base this decision on the mime type + // of the first file; if it's media the platform can deal with, + // use the media provider, if it's something else, just launch an ACTION_VIEW + // on the file. + String mimeType = mMimeTypes.get(mPaths.get(0)); + if (mimeType.startsWith("image/") || mimeType.startsWith("video/") || + mimeType.startsWith("audio/")) { + String[] arrayPaths = new String[mPaths.size()]; + MediaScannerConnection.scanFile(mContext, mPaths.toArray(arrayPaths), null, this); + updateStateAndNotification(STATE_W4_MEDIA_SCANNER); + } else { + // We're done. + updateStateAndNotification(STATE_SUCCESS); + } + + } + + public int getTransferId() { + return mTransferId; + } + + public boolean handleMessage(Message msg) { + if (msg.what == MSG_NEXT_TRANSFER_TIMER) { + // We didn't receive a new transfer in time, finalize this one + if (mIncoming) { + processFiles(); + } else { + updateStateAndNotification(STATE_SUCCESS); + } + return true; + } else if (msg.what == MSG_TRANSFER_TIMEOUT) { + // No update on this transfer for a while, check + // to see if it's still running, and fail it if it is. + if (isRunning()) { + updateStateAndNotification(STATE_FAILED); + } + } + return false; + } + + public synchronized void onScanCompleted(String path, Uri uri) { + if (DBG) Log.d(TAG, "Scan completed, path " + path + " uri " + uri); + if (uri != null) { + mMediaUris.put(path, uri); + } + mUrisScanned++; + if (mUrisScanned == mPaths.size()) { + // We're done + updateStateAndNotification(STATE_SUCCESS); + } + } + + boolean checkMediaStorage(File path) { + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + if (!path.isDirectory() && !path.mkdir()) { + Log.e(TAG, "Not dir or not mkdir " + path.getAbsolutePath()); + return false; + } + return true; + } else { + Log.e(TAG, "External storage not mounted, can't store file."); + return false; + } + } + + Intent buildViewIntent() { + if (mPaths.size() == 0) return null; + + Intent viewIntent = new Intent(Intent.ACTION_VIEW); + + String filePath = mPaths.get(0); + Uri mediaUri = mMediaUris.get(filePath); + Uri uri = mediaUri != null ? mediaUri : + Uri.parse(ContentResolver.SCHEME_FILE + "://" + filePath); + viewIntent.setDataAndTypeAndNormalize(uri, mMimeTypes.get(filePath)); + viewIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return viewIntent; + } + + PendingIntent buildCancelIntent() { + Intent intent = new Intent(HandoverService.ACTION_CANCEL_HANDOVER_TRANSFER); + intent.putExtra(HandoverService.EXTRA_SOURCE_ADDRESS, mRemoteDevice.getAddress()); + PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, intent, 0); + + return pi; + } + + File generateUniqueDestination(String path, String fileName) { + int dotIndex = fileName.lastIndexOf("."); + String extension = null; + String fileNameWithoutExtension = null; + if (dotIndex < 0) { + extension = ""; + fileNameWithoutExtension = fileName; + } else { + extension = fileName.substring(dotIndex); + fileNameWithoutExtension = fileName.substring(0, dotIndex); + } + File dstFile = new File(path + File.separator + fileName); + int count = 0; + while (dstFile.exists()) { + dstFile = new File(path + File.separator + fileNameWithoutExtension + "-" + + Integer.toString(count) + extension); + count++; + } + return dstFile; + } + + File generateMultiplePath(String beamRoot) { + // Generate a unique directory with the date + String format = "yyyy-MM-dd"; + SimpleDateFormat sdf = new SimpleDateFormat(format); + String newPath = beamRoot + "beam-" + sdf.format(new Date()); + File newFile = new File(newPath); + int count = 0; + while (newFile.exists()) { + newPath = beamRoot + "beam-" + sdf.format(new Date()) + "-" + + Integer.toString(count); + newFile = new File(newPath); + count++; + } + return newFile; + } +} + diff --git a/src/com/android/nfc/handover/PendingHandoverTransfer.java b/src/com/android/nfc/handover/PendingHandoverTransfer.java new file mode 100644 index 0000000..db5c68b --- /dev/null +++ b/src/com/android/nfc/handover/PendingHandoverTransfer.java @@ -0,0 +1,63 @@ +package com.android.nfc.handover; + +import android.bluetooth.BluetoothDevice; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +public class PendingHandoverTransfer implements Parcelable { + public int id; + public boolean incoming; + public BluetoothDevice remoteDevice; + public boolean remoteActivating; + public Uri[] uris; + + PendingHandoverTransfer(int id, boolean incoming, BluetoothDevice remoteDevice, + boolean remoteActivating, Uri[] uris) { + this.id = id; + this.incoming = incoming; + this.remoteDevice = remoteDevice; + this.remoteActivating = remoteActivating; + this.uris = uris; + } + + public static final Parcelable.Creator<PendingHandoverTransfer> CREATOR + = new Parcelable.Creator<PendingHandoverTransfer>() { + public PendingHandoverTransfer createFromParcel(Parcel in) { + int id = in.readInt(); + boolean incoming = (in.readInt() == 1) ? true : false; + BluetoothDevice remoteDevice = in.readParcelable(getClass().getClassLoader()); + boolean remoteActivating = (in.readInt() == 1) ? true : false; + int numUris = in.readInt(); + Uri[] uris = null; + if (numUris > 0) { + uris = new Uri[numUris]; + in.readTypedArray(uris, Uri.CREATOR); + } + return new PendingHandoverTransfer(id, incoming, remoteDevice, + remoteActivating, uris); + } + + @Override + public PendingHandoverTransfer[] newArray(int size) { + return new PendingHandoverTransfer[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(id); + dest.writeInt(incoming ? 1 : 0); + dest.writeParcelable(remoteDevice, 0); + dest.writeInt(remoteActivating ? 1 : 0); + dest.writeInt(uris != null ? uris.length : 0); + if (uris != null && uris.length > 0) { + dest.writeTypedArray(uris, 0); + } + } +} |