smartwatch - something more than an additional screen for notifications?
TRANSCRIPT
SMARTWATCH - Something more than an additional screen for notifications?
by Tomek Czerw & Marcin Nycz
● Software House located in Krakow ● Ruby on Rails, Android and iOS● Specialized in building web and mobile applications● Collaborating with many companies and startups from all over
the world
ABOUT US:
2009 - software house was founded50 projects created
40 employees
Awards:
HISTORY:
Top Web & Software Developers in Poland 2015
Top Tens Ruby on Rails Development Companies
● Application Kitchen - Android
smartphone/tablet
● Application Waiter - Tizen smartwatch
● Communication with a working REST API
ASSUMPTIONS:
● Tablet/smartphone creates kitchen
● There may be up to 20 Kitchens
● The waiter connects to the API and downloads the
list of kitchen
● Tracking issued meals
FUNCTIONAL ASSUMPTIONS:
● https://pusher.com/
● Endpoints provided by the previously prepared API
NEEDED ISSUES
NEEDED ISSUES:
● https://pusher.com/
● Endpoints provided by the previously prepared API
● coffee :)
APPLICATION KITCHEN PERFORMANCE:
● Create a kitchen and register it
● Display the menu of the kitchen
● Deliver dishes
● Wait for information from the waiters
public class Kitchen {
private UUID id; private String name; private List<MenuItem> queue = new ArrayList<>(); private List<MenuItem> menu = new ArrayList<>();
public Kitchen(UUID id, String name, List<MenuItem> queue) { this.id = id; this.name = name; this.queue = queue; }/** gettery i settery */}
BASIC OBJECTS:
public class MenuItem {
private UUID id; private String name; private boolean selected; private Date takeDate;
public MenuItem(UUID id, String name, boolean selection) { this.id = id; this.name = name; this.selected = selection; this.takeDate = null; }/** gettery i settery */}
BASIC OBJECTS:
public class Waiter { private String id; private String name;
public Waiter(String id, String name) { this.id = id; this.name = name; }
/** gettery i settery */}
BASIC OBJECTS:
HOST: https://kuchnia-api-railwaymen.herokuapp.com/api/
// CREATE LOCATION AND GET PUSHER CHANNELPOST host/kitchensBody:{ name : "name", deviceId : "deviceId"}
// DELETE KITCHENDELETE host/kitchens/{kitchen_id}
// COMPLETE MEALPOST host/kitchens/{kitchen_id}/menu_items/{menu_item_id}Body:{ id : "uuid", name : "name", date : "date"}
KITCHEN ENDPOINTS:
// CREATE LOCATION AND GET PUSHER CHANNEL@POST("kitchens")Call<Kitchen> register(@Body Map<String, Object> map);
// DELETE KITCHEN@DELETE("kitchens/{kitchen_id}")Call<Void> removeKitchen(@Path("kitchen_id") UUID kitchenId);
// COMPLETE MEAL@POST("kitchens/{kitchen_id}/menu_items/{menu_item_id}")Call<Void> completeMeal(@Path("kitchen_id") UUID kitchenId, @Path("menu_item_id") UUID menuItemId, @Body Map<String, Object> map);
COMMUNICATION REST API - RETROFIT:
public static Endpoints buildRetrofit() {OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor()
.setLevel(HttpLoggingInterceptor.Level.BODY)).build();Retrofit retrofit = new Retrofit.Builder().baseUrl(Constants.API_URL).client(okHttpClient).addConverterFactory(GsonConverterFactory.create()).build();
return retrofit.create(Endpoints.class);}
COMMUNICATION REST API - RETROFIT:
try { Map<String, Object> map = new HashMap<>(); map.put(Constants.Keys.NAME, name); map.put(Constants.Keys.DEVICE_ID, Utils.getDeviceId(this)); Call<Kitchen> call = getEndpoints().register(map); call.enqueue(callback);} catch (Exception e) { Log.d(MainActivity.class.getSimpleName(), e.toString());}/** ....private Callback<Kitchen> callback = new Callback<Kitchen>() { @Override public void onResponse(Call<Kitchen> call, Response<Kitchen> response) { if (response != null) { case 200: kitchen = response.body(); if (kitchen != null) { buildMenu(); syncPusher = new SyncPusher(MainActivity.this, kitchen.getId().toString());} } }
@Override public void onFailure(Call<Kitchen> call, Throwable t) { }};
COMMUNICATION - DOWNLOAD KITCHEN MENU:
recyclerView.setVisibility(View.VISIBLE);recyclerView.setHasFixedSize(true);recyclerView.addItemDecoration(new SpaceItemDecorator(10));layoutManager = new GridLayoutManager(this, 2);recyclerView.setLayoutManager(layoutManager);adapter = new MenuRecyclerAdapter(this);recyclerView.setAdapter(adapter);
DISPLAYING THE MENU:
@Overridepublic MenuHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(hostActivity).inflate(R.layout.recycler_menu_item, parent, false); MenuHolder holder = new MenuHolder(view); view.setOnClickListener(new OnMenuClickListener(holder, hostActivity)); return holder;}@Overridepublic int getItemCount() { return hostActivity.getKitchen().getMenu().size();}
public class MenuHolder extends RecyclerView.ViewHolder { @Bind(R.id.text) TextView text; @Bind(R.id.date) TextView dateText; @Bind(R.id.card_view) CardView cardView; public MenuHolder(View itemView) { super(itemView); ButterKnife.bind(this, itemView); }}
DISPLAYING THE MENU - ADAPTER:
private static final DateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("HH:mm");
@Overridepublic void onBindViewHolder(MenuHolder holder, int position) { MenuItem menuElement = hostActivity.getKitchen().getMenu().get(position); holder.text.setText(menuElement.getName()); if (menuElement.isSelected()) { holder.text.setTextColor(hostActivity.getResources().getColor(android.R.color.white)); holder.cardView .setBackgroundColor(hostActivity.getResources().getColor(R.color.colorPrimary)); if (menuElement.getTakeDate() != null) { holder.dateText.setText(SIMPLE_DATE_FORMAT.format(menuElement.getTakeDate())); holder.dateText.setVisibility(View.VISIBLE); } } else { holder.text.setTextColor(hostActivity.getResources().getColor(R.color.colorPrimary)); holder.cardView.setBackgroundColor( hostActivity.getResources().getColor(android.R.color.white)); holder.dateText.setVisibility(View.GONE); }
}
DISPLAYING THE MENU - ADAPTER:
@Overridepublic void onClick(View view) { if (Utils.isDeviceConnected(hostActivity)) { int position = holder.getAdapterPosition(); MenuItem menuElement = hostActivity.getKitchen().getMenu().get(position); Date date = new Date(); menuElement.setSelected(true); menuElement.setTakeDate(date); hostActivity.getAdapter().notifyItemChanged(position); Map<String, Object> map = new HashMap<>(); map.put(Constants.Keys.ID, menuElement.getId()); map.put(Constants.Keys.NAME, menuElement.getName()); map.put(Constants.Keys.DATE, date); Call<Void> call = hostActivity.getEndpoints() .completeMeal(hostActivity.getKitchen().getId(), menuElement.getId(), map); call.enqueue(callback); } }private Callback<Void> callback = new Callback<Void>() { @Override public void onResponse(Call<Void> call, Response<Void> response) { } @Override public void onFailure(Call<Void> call, Throwable t) { }};
ISSUE OBJECT FROM THE KITCHEN:
private void connectPusher() { pusher = new Pusher(Constants.Pusher.APP_KEY, new PusherOptions().setCluster("eu")); pusher.connect(connectionEventListener, ConnectionState.ALL); subscribeChannel(channelName);}
private void subscribeChannel(String channelName) { channel = pusher.subscribe(channelName); for (String eventName : Constants.WISHLIST_EVENT_LIST) { channel.bind(eventName, subscriptionEventListener); }}
private ConnectionEventListener connectionEventListener = new ConnectionEventListener() { @Override public void onConnectionStateChange(ConnectionStateChange change) { Log.e(SyncPusher.class.getSimpleName(), change.getCurrentState().toString()); }
@Override public void onError(String message, String code, Exception e) { Log.e(SyncPusher.class.getSimpleName(), message); }};
COMMUNICATION PUSHER:
private SubscriptionEventListener subscriptionEventListener = new SubscriptionEventListener() { @Override public void onEvent(final String channelName, final String eventName, final String data) { Runnable eventRunnable = new Runnable() { @Override public void run() { AbstractEvent event = EventFactory.getEvent(eventName, data, mainActivity); if (event != null) { event.handleEvent(); } } }; mainActivity.getExecutor().submit(eventRunnable); }};
PUSHER:
public class EventFactory {
private EventFactory() { };
public static AbstractEvent getEvent(String eventName, String data, MainActivity mainActivity) { if (eventName.equals(Constants.Event.CONNECTED)) { return new ConnectedEvent(data, mainActivity); } else if (eventName.equals(Constants.Event.DISCONNECTED)) { return new DisconnectedEvent(data, mainActivity); } else if (eventName.equals(Constants.Event.TAKED)) { return new TakedEvent(data, mainActivity); } return null; }}
SIMPLIFIED FABRIC EVENTS:
public abstract class AbstractEvent { protected MainActivity mainActivity;
public AbstractEvent(String data, MainActivity mainActivity) { this.mainActivity = mainActivity; parseData(data); }
public abstract void handleEvent();
abstract void parseData(String data);}
PUSHER - EVENTS:
public class TakedEvent extends AbstractEvent { private MenuItem menuItem; private Waiter waiter; @Override public void handleEvent() { if (menuItem.getId() != null) { int position = Utils.findMenuItemPosition(mainActivity.getKitchen().getMenu(), menuItem.getId()); MenuItem menuItem = mainActivity.getKitchen().getMenu().get(position); menuItem.setSelected(false); menuItem.setTakeDate(null); mainActivity.getAdapter().notifyItemChanged(position); }} @Override protected void parseData(String data) { JSONObject jsonObject = null; try { jsonObject = new JSONObject(data); menuItem = mainActivity.gson.fromJson( jsonObject.get(Constants.Keys.MENU_ITEM).toString(), MenuItem.class); waiter = mainActivity.gson .fromJson(jsonObject.get(Constants.Keys.WAITER).toString(), Waiter.class); } catch (JSONException e) { Log.e(TakedEvent.class.getSimpleName(), e.toString()); }}}
PUSHER - RECEIVE FOOD:
public class ConnectedEvent extends AbstractEvent { private Waiter waiter;
public ConnectedEvent(String data, MainActivity mainActivity) { super(data, mainActivity); }
@Override public void handleEvent() { mainActivity.showSnackbar(waiter == null ? "Kelner" : waiter.getName() +" " +mainActivity.getString(R.string.waiter_connected)); }
@Override protected void parseData(String data) { if (data != null) { waiter = MainActivity.gson.fromJson(data, Waiter.class); } }}
PUSHER - CONNECT WAITER:
public class DisconnectedEvent extends AbstractEvent { private Waiter waiter;
public DisconnectedEvent(String data, MainActivity mainActivity) { super(data, mainActivity); }
@Override public void handleEvent() { mainActivity.showSnackbar(waiter == null ? "Kelner" : waiter.getName() + " " + mainActivity.getString(R.string.waiter_disconnected)); }
@Override protected void parseData(String data) { if (data != null) { waiter = MainActivity.gson.fromJson(data, Waiter.class); } }}
PUSHER - DISCONNECT WAITER:
window.onload = function() { document.addEventListener('tizenhwkey', function(e) { if (e.keyName === "back") { var currentPage = $(".ui-page-active").attr('id'); if (currentPage === 'no_kitchens') { try { tizen.application.getCurrentApplication().exit(); } catch (ignore) { } } else if (currentPage === 'kitchens') { tau.changePage("#no_kitchens"); stopServices() }
// ....}
}); $("#no-kitchens").click(function() { getKitchens(); }); tau.changePage("#no_kitchens"); Pusher.log = function(message) { if (window.console && window.console.log) { window.console.log(message); } };};
main.js:
<?xml version="1.0" encoding="UTF-8"?><widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets" id="http://railwaymen.org/Kelner" version="1.0.0" viewmodes="maximized"> <access origin="*" subdomains="true"></access> <tizen:application id="JRp9qBI8KW.Kelner" package="JRp9qBI8KW" required_version="2.2"/> <content src="index.html"/> <feature name="http://tizen.org/feature/screen.size.all"/> <feature name="http://tizen.org/api/tizen" required="true"/> <feature name="http://tizen.org/feature/download"/> <feature name="http://tizen.org/feature/network.internet"/> <feature name="http://tizen.org/feature/screen.size.all"/> <icon src="icon.png"/> <name>Kelner</name> <tizen:privilege name="http://tizen.org/privilege/application.launch"/> <tizen:privilege name="http://tizen.org/privilege/download"/> <tizen:privilege name="http://tizen.org/privilege/power"/> <tizen:privilege name="http://tizen.org/privilege/push"/> <tizen:privilege name="http://tizen.org/privilege/internet"/> <tizen:profile name="wearable"/></widget>
config.xml:
WAITER APPLICATION PERFORMANCE:
● Refresh Kitchen
● List of Kitchen
● Download issued dishes
● Report willing to receive food
● Receive food
<!--...--><div id="no_kitchens" class="ui-page"> <div class="ui-content"> <div class="center-block"> <span id="no-kitchens" class="center-text">Odśwież kuchnie.</span> </div> </div></div><div id="kitchens" class="ui-page"> <header class="ui-header ui-has-more"> <h2 class="ui-title">Kuchnie</h2> </header> <div class="ui-content" id="mainPage"> <ul id="kitchenList" data-role="listview" class="ui-listview"></ul> </div></div><div id="menu_items" class="ui-page"> <header class="ui-header"> <h2 class="ui-title waiter_name">Stefan</h2> </header> <div class="ui-content" id="mainPage" data-add-back-btn="true"> <ul id="menuItemsList" class="ui-listview"></ul> </div></div><!--...-->
VIEW - VISIBLE SCREENS:
HOST: https://kuchnia-api-railwaymen.herokuapp.com/api/
// GET KITCHENSGET host/kitchens
// GET MENU ITEMSPOST host/kitchens/{kitchen_id}/menu_itemsBody:{ id : "deviceId", name : "deviceName"}
// LOGOUT WAITERDELETE host/kitchens/{kitchen_id}/waiters/{waiter_id}
// TAKE MENU ITEMDELETE host/kitchens/{kitchen_id}/menu_items/{menu_item_id}Body:{ id : "deviceId", name : "deviceName"}
SMARTWATCH ENDPOINTS:
var getKitchens = function() { sendRequest('GET', buildUrl('/kitchens', ''), function(request, data) {}, function(data) {});}var logoutWaiter = function(kitchenId, waiterId) { sendRequest('DELETE', buildUrl('/kitchens/' + kitchenId + '/waiters/' + waiterId), function(request, data) {}, function(data) {});}var getMenuItems = function(kitchenId, waiter) { sendRequestWithBody('POST', buildUrl('/kitchens/' + kitchenId + '/menu_items'), waiter, function(request, data) {}, function(data) {});}var takeMenuItem = function(menuItem, waiter) { sendRequestWithBody('DELETE', buildUrl('/kitchens/' + window.currentKitchen.id + '/menu_items/' + menuItem.id, ''), waiter, function(request, data) {}, function(request, data) {});}
COMMUNICATION REST API:
var formatTime = function(date) { var tmp = new Date(date); return tmp.getHours() + ":" + (tmp.getMinutes() < 10 ? "0" : "") + tmp.getMinutes();}var sortQueue = function() { window.currentKitchen.queue.sort(function(menuItem1, menuItem2) { return menuItem1.date ? -1 : menuItem2.date ? 1 : 0; });}var removeFromQueue = function(menuItemId) { window.currentKitchen.queue = _.reject(window.currentKitchen.queue, function(menuItem) { return menuItem.id === menuItemId; });}
AUXILIARY FUNCTIONS:
var renderKitchens = function() { if (window.kitchens.length != 0) { if (window.kitchens.length) tau.changePage("#kitchens"); $("#kitchenList").empty(); window.kitchens.sort(function compare(lok1, lok2) { if (lok1.name.toLowerCase() < lok2.name.toLowerCase()) return -1; if (lok1.name.toLowerCase() > lok2.name.toLowerCase()) return 1; return 0;}); window.kitchens.forEach(function(kitchen) { $("#kitchenList").append(renderKitchen(kitchen)); }); $("#kitchenList li").click(kitchenClickListener); } else { $("#no-kitchens").text('Brak zarejestrowanych kuchni'); }}var renderKitchen = function(kitchen) { return "<li data-id=\"" + kitchen.id + "\"><a href=\"#\" class=\"ui-li-text-sub\">" + kitchen.name + "</a></li>";}
DISPLAYING THE KITCHEN:
var kitchenClickListener = function(e) { var kitchenId = $(e.currentTarget).data("id"); var deviceId = tizen.systeminfo.getCapabilities().duid if (deviceId) { window.waiter = { id : deviceId, name : chance.name() } } else { window.waiter = { id : uuid(), name : chance.name() } } getMenuItems(kitchenId, JSON.stringify(window.waiter))}
LOGIN TO THE SELECTED KITCHEN:
var checkQueue = function() { $(".waiter_name").text(window.waiter.name); if (window.currentKitchen.queue.length === 0) { tau.changePage("#no_menu_items"); } else if (window.currentKitchen.queue.length === 1) { tau.changePage("#single_menu_item_container"); renderSinglePageMenuItem(window.currentKitchen.queue[0]) } else { tau.changePage("#menu_items"); renderMenuItems(); }}
CHECK ISSUED DISHES:
var renderSinglePageMenuItem = function(menuItem) { $("#single_menu_item").text(menuItem.name); $("#single_menu_item_date").text(formatTime(menuItem.date)); $('#single_menu_item_container').unbind("click"); $('#single_menu_item_container').click( function() { takeMenuItem(window.currentKitchen.queue[0], JSON .stringify(window.waiter)); });}var renderMenuItems = function() { $("#menuItemsList").empty(); if (window.currentKitchen.queue.length != 1) { tau.changePage("#menu_items"); sortQueue() window.currentKitchen.queue.forEach(function(menuItem) { $("#menuItemsList").append(renderMenuItem(menuItem)); }); $("#menuItemsList li").click(menuItemClickListener); } else { tau.changePage("#single_menu_item_container"); renderSinglePageMenuItem(window.currentKitchen.queue[0]) }}
DISPLAY THE DISHES:
var menuItemClickListener = function(e) { var menuItemId = $(e.currentTarget).data("id"); var menuItem = _.findWhere(window.currentKitchen.queue, { id : menuItemId }); takeMenuItem(menuItem, JSON.stringify(window.waiter))}
RECEIVING DISHES:
var bindResponsibility = function(menuItem) { tau.changePage("#well_done"); $("#menu_item").text(menuItem.name); $("#menu_item_date").text(formatTime(menuItem.date)); $('#well_done').unbind("click"); var timeTask = window.setTimeout(function() { checkQueue(); }, 3000); $('#well_done').click(function() { window.clearTimeout(timeTask) checkQueue(); });}
removeFromQueue(menuItem.id)if (request.status === 204) {
tau.changePage("#to_slow");}
else {bindResponsibility(menuItem)}
RESULTS:
var startServices = function(kitchenId) { window.pusher = new Pusher('be378fc6b685348ae04c', { cluster : 'eu', encrypted : true }); window.channel = window.pusher.subscribe(kitchenId);}
COMMUNICATION PUSHER:
window.channel.bind('event_complete', function(menuItemJson) {});
window.channel.bind('event_take',function(eventJson) {});
window.channel.bind('event_kitchen_disconnected', function() {});
PUSHER - EVENTS:
Na zjeździe 1130-527 Krakow, Polandtel: +48 12 391 60 76
Silicon Valley Acceleration Center. 180 Sansome Street San Francisco, CA 94104tel: 1-415-449-4791
www.railwaymen.org
@Railwaymen_org
railwaymen.software.development
/company/railwaymen