smartwatch - something more than an additional screen for notifications?

66
SMARTWATCH - Something more than an additional screen for notifications? by Tomek Czerw & Marcin Nycz

Upload: railwaymen

Post on 11-Apr-2017

511 views

Category:

Software


0 download

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

OUR PROJECTS:

HOMEAHEAD

PROEST

Software for gastronomy

● Application Kitchen - Android

smartphone/tablet

● Application Waiter - Tizen smartwatch

● Communication with a working REST API

ASSUMPTIONS:

API

COMMUNICATION:

COMMUNICATION:

Pusher client Pusher client

APIPusher trigger

Pusher.com

● 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:

NEEDED ITEMS:

● https://pusher.com/

● 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 :)

Questions?

KITCHEN - Android app

LIBRARIES USED:

● Pusher

● RecyclerView

● Retrofit

● Gson

APPLICATION KITCHEN PERFORMANCE:

● Create a kitchen and register it

● Display the menu of the kitchen

● Deliver dishes

● Wait for information from the waiters

The basic objects?

CLASSES:

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:

CLASSES:

Communication REST API

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:

Displaying the menu:

CLASSES:

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:

Issue object from the kitchen

CLASSES:

@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:

Communication Pusher

CLASSES:

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:

Pusher - events

CLASSES:

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:

Waiter - smartwatch app with Tizen system

LIBRARIES USED:

● Underscore

● jQuery

● Chance

● Pusher

● TAU

APLICATION TIZEN STRUCTURE:

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:

Displaying items and interaction

MAIN.JS:

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:

sync.js:

Receiving 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:

syncPusher.js:

Communication Pusher

var startServices = function(kitchenId) { window.pusher = new Pusher('be378fc6b685348ae04c', { cluster : 'eu', encrypted : true }); window.channel = window.pusher.subscribe(kitchenId);}

COMMUNICATION PUSHER:

Pusher - events

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:

TEST!

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

[email protected]

www.railwaymen.org

@Railwaymen_org

railwaymen.software.development

/company/railwaymen