diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c9b3698..bbe0044 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -9,6 +9,7 @@ android:targetSdkVersion="19" /> + @@ -46,7 +47,15 @@ android:value=".activities.MainActivity" /> - + + + + + + + + + Error + Unable to load the session details.\nMake sure the database has been updated to the latest version. An error occurred while updating the schedule. Please try again later. The minimum search text length is 3 chars. diff --git a/src/be/digitalia/fosdem/activities/EventDetailsActivity.java b/src/be/digitalia/fosdem/activities/EventDetailsActivity.java index 633ea67..283ceb4 100644 --- a/src/be/digitalia/fosdem/activities/EventDetailsActivity.java +++ b/src/be/digitalia/fosdem/activities/EventDetailsActivity.java @@ -5,17 +5,20 @@ import android.content.Intent; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.TaskStackBuilder; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.app.NavUtils; +import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.Loader; import android.support.v7.app.ActionBarActivity; import android.view.MenuItem; +import android.widget.Toast; import be.digitalia.fosdem.R; import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.fragments.EventDetailsFragment; import be.digitalia.fosdem.loaders.LocalCacheLoader; import be.digitalia.fosdem.model.Event; +import be.digitalia.fosdem.utils.NfcUtils; +import be.digitalia.fosdem.utils.NfcUtils.CreateNfcAppDataCallback; /** * Displays a single event passed either as a complete Parcelable object in extras or as an id in data. @@ -23,7 +26,7 @@ import be.digitalia.fosdem.model.Event; * @author Christophe Beyls * */ -public class EventDetailsActivity extends ActionBarActivity implements LoaderCallbacks { +public class EventDetailsActivity extends ActionBarActivity implements LoaderCallbacks, CreateNfcAppDataCallback { public static final String EXTRA_EVENT = "event"; @@ -38,11 +41,11 @@ public class EventDetailsActivity extends ActionBarActivity implements LoaderCal getSupportActionBar().setTitle(R.string.event_details); - event = getIntent().getParcelableExtra(EXTRA_EVENT); + Event event = getIntent().getParcelableExtra(EXTRA_EVENT); if (event != null) { // The event has been passed as parameter, it can be displayed immediately - initActionBar(); + initEvent(event); if (savedInstanceState == null) { Fragment f = EventDetailsFragment.newInstance(event); getSupportFragmentManager().beginTransaction().add(R.id.content, f).commit(); @@ -54,11 +57,14 @@ public class EventDetailsActivity extends ActionBarActivity implements LoaderCal } /** - * Initialize event-related ActionBar configuration after the event has been loaded. + * Initialize event-related configuration after the event has been loaded. */ - private void initActionBar() { + private void initEvent(Event event) { + this.event = event; // Enable up navigation only after getting the event details getSupportActionBar().setDisplayHomeAsUpEnabled(true); + // Enable Android Beam + NfcUtils.setAppDataPushMessageCallbackIfAvailable(this, this); } @Override @@ -83,6 +89,11 @@ public class EventDetailsActivity extends ActionBarActivity implements LoaderCal return false; } + @Override + public byte[] createNfcAppData() { + return String.valueOf(event.getId()).getBytes(); + } + private static class EventLoader extends LocalCacheLoader { private final long eventId; @@ -100,23 +111,32 @@ public class EventDetailsActivity extends ActionBarActivity implements LoaderCal @Override public Loader onCreateLoader(int id, Bundle args) { - return new EventLoader(this, Long.parseLong(getIntent().getDataString())); + Intent intent = getIntent(); + String eventIdString; + if (NfcUtils.hasAppData(intent)) { + // NFC intent + eventIdString = new String(NfcUtils.extractAppData(intent)); + } else { + // Normal in-app intent + eventIdString = intent.getDataString(); + } + return new EventLoader(this, Long.parseLong(eventIdString)); } @Override public void onLoadFinished(Loader loader, Event data) { if (data == null) { // Event not found, quit + Toast.makeText(this, getString(R.string.event_not_found_error), Toast.LENGTH_LONG).show(); finish(); return; } - event = data; - initActionBar(); + initEvent(data); FragmentManager fm = getSupportFragmentManager(); if (fm.findFragmentById(R.id.content) == null) { - Fragment f = EventDetailsFragment.newInstance(event); + Fragment f = EventDetailsFragment.newInstance(data); fm.beginTransaction().add(R.id.content, f).commitAllowingStateLoss(); } } diff --git a/src/be/digitalia/fosdem/activities/TrackScheduleActivity.java b/src/be/digitalia/fosdem/activities/TrackScheduleActivity.java index 9f0dbff..9efd8c3 100644 --- a/src/be/digitalia/fosdem/activities/TrackScheduleActivity.java +++ b/src/be/digitalia/fosdem/activities/TrackScheduleActivity.java @@ -16,6 +16,8 @@ import be.digitalia.fosdem.fragments.TrackScheduleListFragment; import be.digitalia.fosdem.model.Day; import be.digitalia.fosdem.model.Event; import be.digitalia.fosdem.model.Track; +import be.digitalia.fosdem.utils.NfcUtils; +import be.digitalia.fosdem.utils.NfcUtils.CreateNfcAppDataCallback; /** * Track Schedule container, works in both single pane and dual pane modes. @@ -23,7 +25,7 @@ import be.digitalia.fosdem.model.Track; * @author Christophe Beyls * */ -public class TrackScheduleActivity extends ActionBarActivity implements TrackScheduleListFragment.Callbacks { +public class TrackScheduleActivity extends ActionBarActivity implements TrackScheduleListFragment.Callbacks, CreateNfcAppDataCallback { public static final String EXTRA_DAY = "day"; public static final String EXTRA_TRACK = "track"; @@ -33,6 +35,7 @@ public class TrackScheduleActivity extends ActionBarActivity implements TrackSch private Day day; private Track track; private boolean isTabletLandscape; + private Event lastSelectedEvent; @Override protected void onCreate(Bundle savedInstanceState) { @@ -72,6 +75,19 @@ public class TrackScheduleActivity extends ActionBarActivity implements TrackSch } } trackScheduleListFragment.setSelectionEnabled(isTabletLandscape); + + if (isTabletLandscape) { + // Enable Android Beam + NfcUtils.setAppDataPushMessageCallbackIfAvailable(this, this); + } + } + + @Override + public byte[] createNfcAppData() { + if (lastSelectedEvent == null) { + return null; + } + return String.valueOf(lastSelectedEvent.getId()).getBytes(); } @Override @@ -88,6 +104,8 @@ public class TrackScheduleActivity extends ActionBarActivity implements TrackSch public void onEventSelected(int position, Event event) { if (isTabletLandscape) { // Tablet mode: Show event details in the right pane fragment + lastSelectedEvent = event; + FragmentManager fm = getSupportFragmentManager(); EventDetailsFragment currentFragment = (EventDetailsFragment) fm.findFragmentById(R.id.event); if (event != null) { diff --git a/src/be/digitalia/fosdem/activities/TrackScheduleEventActivity.java b/src/be/digitalia/fosdem/activities/TrackScheduleEventActivity.java index 18c6765..ab6d09c 100644 --- a/src/be/digitalia/fosdem/activities/TrackScheduleEventActivity.java +++ b/src/be/digitalia/fosdem/activities/TrackScheduleEventActivity.java @@ -2,6 +2,7 @@ package be.digitalia.fosdem.activities; import android.database.Cursor; import android.os.Bundle; +import android.provider.BaseColumns; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; @@ -18,6 +19,8 @@ import be.digitalia.fosdem.fragments.EventDetailsFragment; import be.digitalia.fosdem.loaders.TrackScheduleLoader; import be.digitalia.fosdem.model.Day; import be.digitalia.fosdem.model.Track; +import be.digitalia.fosdem.utils.NfcUtils; +import be.digitalia.fosdem.utils.NfcUtils.CreateNfcAppDataCallback; import com.viewpagerindicator.PageIndicator; @@ -27,7 +30,7 @@ import com.viewpagerindicator.PageIndicator; * @author Christophe Beyls * */ -public class TrackScheduleEventActivity extends ActionBarActivity implements LoaderCallbacks { +public class TrackScheduleEventActivity extends ActionBarActivity implements LoaderCallbacks, CreateNfcAppDataCallback { public static final String EXTRA_DAY = "day"; public static final String EXTRA_TRACK = "track"; @@ -69,6 +72,9 @@ public class TrackScheduleEventActivity extends ActionBarActivity implements Loa bar.setTitle(R.string.event_details); bar.setSubtitle(track.getName()); + // Enable Android Beam + NfcUtils.setAppDataPushMessageCallbackIfAvailable(this, this); + setCustomProgressVisibility(true); getSupportLoaderManager().initLoader(EVENTS_LOADER_ID, null, this); } @@ -77,6 +83,18 @@ public class TrackScheduleEventActivity extends ActionBarActivity implements Loa progress.setVisibility(isVisible ? View.VISIBLE : View.GONE); } + @Override + public byte[] createNfcAppData() { + if (adapter.getCount() == 0) { + return null; + } + long eventId = adapter.getItemId(pager.getCurrentItem()); + if (eventId == -1L) { + return null; + } + return String.valueOf(eventId).getBytes(); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -145,5 +163,12 @@ public class TrackScheduleEventActivity extends ActionBarActivity implements Loa cursor.moveToPosition(position); return EventDetailsFragment.newInstance(DatabaseManager.toEvent(cursor)); } + + public long getItemId(int position) { + if (!cursor.moveToPosition(position)) { + return -1L; + } + return cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID)); + } } } diff --git a/src/be/digitalia/fosdem/utils/NfcReceiverUtils.java b/src/be/digitalia/fosdem/utils/NfcReceiverUtils.java new file mode 100644 index 0000000..fe7b7fa --- /dev/null +++ b/src/be/digitalia/fosdem/utils/NfcReceiverUtils.java @@ -0,0 +1,28 @@ +package be.digitalia.fosdem.utils; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.nfc.NdefMessage; +import android.nfc.NfcAdapter; +import android.os.Build; +import android.os.Parcelable; + +/** + * NFC helper methods for receiving data sent by NfcSenderUtils. This class wraps API 10+ code. + * + * @author Christophe Beyls + * + */ +@TargetApi(Build.VERSION_CODES.GINGERBREAD_MR1) +class NfcReceiverUtils { + + public static boolean hasAppData(Intent intent) { + return NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction()); + } + + public static byte[] extractAppData(Intent intent) { + Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); + NdefMessage msg = (NdefMessage) rawMsgs[0]; + return msg.getRecords()[0].getPayload(); + } +} diff --git a/src/be/digitalia/fosdem/utils/NfcSenderUtils.java b/src/be/digitalia/fosdem/utils/NfcSenderUtils.java new file mode 100644 index 0000000..a5cdf10 --- /dev/null +++ b/src/be/digitalia/fosdem/utils/NfcSenderUtils.java @@ -0,0 +1,51 @@ +package be.digitalia.fosdem.utils; + +import java.nio.charset.Charset; + +import be.digitalia.fosdem.utils.NfcUtils.CreateNfcAppDataCallback; +import android.annotation.TargetApi; +import android.app.Activity; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.nfc.NfcEvent; +import android.nfc.NfcAdapter.CreateNdefMessageCallback; +import android.os.Build; + +/** + * NFC helper methods for Android Beam foreground push. This class wraps API 14+ code. + * + * @author Christophe Beyls + * + */ +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +class NfcSenderUtils { + + public static boolean setAppDataPushMessageCallbackIfAvailable(Activity activity, final CreateNfcAppDataCallback callback) { + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(activity); + if (adapter == null) { + return false; + } + final String packageName = activity.getPackageName(); + adapter.setNdefPushMessageCallback(new CreateNdefMessageCallback() { + + @Override + public NdefMessage createNdefMessage(NfcEvent event) { + byte[] appData = callback.createNfcAppData(); + if (appData == null) { + return null; + } + NdefRecord[] records = new NdefRecord[] { createMimeRecord("application/" + packageName, appData), + NdefRecord.createApplicationRecord(packageName) }; + return new NdefMessage(records); + } + + }, activity); + return true; + } + + private static NdefRecord createMimeRecord(String mimeType, byte[] payload) { + byte[] mimeBytes = mimeType.getBytes(Charset.forName("US-ASCII")); + return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, mimeBytes, new byte[0], payload); + } +} diff --git a/src/be/digitalia/fosdem/utils/NfcUtils.java b/src/be/digitalia/fosdem/utils/NfcUtils.java new file mode 100644 index 0000000..dfd12d1 --- /dev/null +++ b/src/be/digitalia/fosdem/utils/NfcUtils.java @@ -0,0 +1,67 @@ +package be.digitalia.fosdem.utils; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; + +/** + * NFC helper methods compatible with all API levels. + * + * @author Christophe Beyls + * + */ +public class NfcUtils { + + /** + * Implement this interface to create application-specific data to be shared through Android Beam. + */ + public interface CreateNfcAppDataCallback { + /** + * + * @return The app data, or null if no data is currently available for sharing. + */ + public byte[] createNfcAppData(); + } + + /** + * Call this method in an Activity, between onCreate() and onDestroy(), to make its content sharable using Android Beam if available. MIME type of the data + * to share will be "application/" followed by the app's package name. Declare it in your Manifest's intent filters as the data type with an action of + * android.nfc.action.NDEF_DISCOVERED to handle the NFC Intents on the receiver side. + * + * @param activity + * @param callback + * @return true if NFC is available and the content was made available, false if not. + */ + public static boolean setAppDataPushMessageCallbackIfAvailable(Activity activity, final CreateNfcAppDataCallback callback) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + return NfcSenderUtils.setAppDataPushMessageCallbackIfAvailable(activity, callback); + } + return false; + } + + /** + * Determines if the intent contains NFC NDEF application-specific data to be extracted. + * + * @param intent + * @return + */ + public static boolean hasAppData(Intent intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) { + return NfcReceiverUtils.hasAppData(intent); + } + return false; + } + + /** + * Extracts application-specific data sent through NFC from an intent. You must first ensure that the intent contains NFC data by calling hasAppData(). + * + * @param intent + * @return The extracted data + */ + public static byte[] extractAppData(Intent intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD_MR1) { + return NfcReceiverUtils.extractAppData(intent); + } + return null; + } +}