1 /*
2 * Copyright 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 
17 package com.example.android.batchstepsensor;
18 
19 import android.app.Activity;
20 import android.content.pm.PackageManager;
21 import android.hardware.Sensor;
22 import android.hardware.SensorEvent;
23 import android.hardware.SensorEventListener;
24 import android.hardware.SensorManager;
25 import android.os.Bundle;
26 import android.support.v4.app.Fragment;
27 
28 import com.example.android.common.logger.Log;
29 import com.example.android.batchstepsensor.cardstream.Card;
30 import com.example.android.batchstepsensor.cardstream.CardStream;
31 import com.example.android.batchstepsensor.cardstream.CardStreamFragment;
32 import com.example.android.batchstepsensor.cardstream.OnCardClickListener;
33 
34 public class BatchStepSensorFragment extends Fragment implements OnCardClickListener {
35 
36     public static final String TAG = "StepSensorSample";
37     // Cards
38     private CardStreamFragment mCards = null;
39 
40     // Card tags
41     public static final String CARD_INTRO = "intro";
42     public static final String CARD_REGISTER_DETECTOR = "register_detector";
43     public static final String CARD_REGISTER_COUNTER = "register_counter";
44     public static final String CARD_BATCHING_DESCRIPTION = "register_batching_description";
45     public static final String CARD_COUNTING = "counting";
46     public static final String CARD_EXPLANATION = "explanation";
47     public static final String CARD_NOBATCHSUPPORT = "error";
48 
49     // Actions from REGISTER cards
50     public static final int ACTION_REGISTER_DETECT_NOBATCHING = 10;
51     public static final int ACTION_REGISTER_DETECT_BATCHING_5s = 11;
52     public static final int ACTION_REGISTER_DETECT_BATCHING_10s = 12;
53     public static final int ACTION_REGISTER_COUNT_NOBATCHING = 21;
54     public static final int ACTION_REGISTER_COUNT_BATCHING_5s = 22;
55     public static final int ACTION_REGISTER_COUNT_BATCHING_10s = 23;
56     // Action from COUNTING card
57     public static final int ACTION_UNREGISTER = 1;
58     // Actions from description cards
59     private static final int ACTION_BATCHING_DESCRIPTION_DISMISS = 2;
60     private static final int ACTION_EXPLANATION_DISMISS = 3;
61 
62     // State of application, used to register for sensors when app is restored
63     public static final int STATE_OTHER = 0;
64     public static final int STATE_COUNTER = 1;
65     public static final int STATE_DETECTOR = 2;
66 
67     // Bundle tags used to store data when restoring application state
68     private static final String BUNDLE_STATE = "state";
69     private static final String BUNDLE_LATENCY = "latency";
70     private static final String BUNDLE_STEPS = "steps";
71 
72     // max batch latency is specified in microseconds
73     private static final int BATCH_LATENCY_0 = 0; // no batching
74     private static final int BATCH_LATENCY_10s = 10000000;
75     private static final int BATCH_LATENCY_5s = 5000000;
76 
77     /*
78     For illustration we keep track of the last few events and show their delay from when the
79     event occurred until it was received by the event listener.
80     These variables keep track of the list of timestamps and the number of events.
81      */
82     // Number of events to keep in queue and display on card
83     private static final int EVENT_QUEUE_LENGTH = 10;
84     // List of timestamps when sensor events occurred
85     private float[] mEventDelays = new float[EVENT_QUEUE_LENGTH];
86 
87     // number of events in event list
88     private int mEventLength = 0;
89     // pointer to next entry in sensor event list
90     private int mEventData = 0;
91 
92     // Steps counted in current session
93     private int mSteps = 0;
94     // Value of the step counter sensor when the listener was registered.
95     // (Total steps are calculated from this value.)
96     private int mCounterSteps = 0;
97     // Steps counted by the step counter previously. Used to keep counter consistent across rotation
98     // changes
99     private int mPreviousCounterSteps = 0;
100     // State of the app (STATE_OTHER, STATE_COUNTER or STATE_DETECTOR)
101     private int mState = STATE_OTHER;
102     // When a listener is registered, the batch sensor delay in microseconds
103     private int mMaxDelay = 0;
104 
105     @Override
onResume()106     public void onResume() {
107         super.onResume();
108 
109         CardStreamFragment stream = getCardStream();
110         if (stream.getVisibleCardCount() < 1) {
111             // No cards are visible, started for the first time
112             // Prepare all cards and show the intro card.
113             initialiseCards();
114             showIntroCard();
115             // Show the registration card if the hardware is supported, show an error otherwise
116             if (isKitkatWithStepSensor()) {
117                 showRegisterCard();
118             } else {
119                 showErrorCard();
120             }
121         }
122     }
123 
124     @Override
onPause()125     public void onPause() {
126         super.onPause();
127         // BEGIN_INCLUDE(onpause)
128         // Unregister the listener when the application is paused
129         unregisterListeners();
130         // END_INCLUDE(onpause)
131     }
132 
133     /**
134      * Returns true if this device is supported. It needs to be running Android KitKat (4.4) or
135      * higher and has a step counter and step detector sensor.
136      * This check is useful when an app provides an alternative implementation or different
137      * functionality if the step sensors are not available or this code runs on a platform version
138      * below Android KitKat. If this functionality is required, then the minSDK parameter should
139      * be specified appropriately in the AndroidManifest.
140      *
141      * @return True iff the device can run this sample
142      */
isKitkatWithStepSensor()143     private boolean isKitkatWithStepSensor() {
144         // BEGIN_INCLUDE(iskitkatsensor)
145         // Require at least Android KitKat
146         int currentApiVersion = android.os.Build.VERSION.SDK_INT;
147         // Check that the device supports the step counter and detector sensors
148         PackageManager packageManager = getActivity().getPackageManager();
149         return currentApiVersion >= android.os.Build.VERSION_CODES.KITKAT
150                 && packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_STEP_COUNTER)
151                 && packageManager.hasSystemFeature(PackageManager.FEATURE_SENSOR_STEP_DETECTOR);
152         // END_INCLUDE(iskitkatsensor)
153     }
154 
155     /**
156      * Handles a click on a card action.
157      * Registers a SensorEventListener (see {@link #registerEventListener(int, int)}) with the
158      * selected delay, dismisses cards and unregisters the listener
159      * (see {@link #unregisterListeners()}).
160      * Actions are defined when a card is created.
161      *
162      * @param cardActionId
163      * @param cardTag
164      */
165     @Override
onCardClick(int cardActionId, String cardTag)166     public void onCardClick(int cardActionId, String cardTag) {
167 
168         switch (cardActionId) {
169             // BEGIN_INCLUDE(onclick)
170             // Register Step Counter card
171             case ACTION_REGISTER_COUNT_NOBATCHING:
172                 registerEventListener(BATCH_LATENCY_0, Sensor.TYPE_STEP_COUNTER);
173                 break;
174             case ACTION_REGISTER_COUNT_BATCHING_5s:
175                 registerEventListener(BATCH_LATENCY_5s, Sensor.TYPE_STEP_COUNTER);
176                 break;
177             case ACTION_REGISTER_COUNT_BATCHING_10s:
178                 registerEventListener(BATCH_LATENCY_10s, Sensor.TYPE_STEP_COUNTER);
179                 break;
180 
181             // Register Step Detector card
182             case ACTION_REGISTER_DETECT_NOBATCHING:
183                 registerEventListener(BATCH_LATENCY_0, Sensor.TYPE_STEP_DETECTOR);
184                 break;
185             case ACTION_REGISTER_DETECT_BATCHING_5s:
186                 registerEventListener(BATCH_LATENCY_5s, Sensor.TYPE_STEP_DETECTOR);
187                 break;
188             case ACTION_REGISTER_DETECT_BATCHING_10s:
189                 registerEventListener(BATCH_LATENCY_10s, Sensor.TYPE_STEP_DETECTOR);
190                 break;
191 
192             // Unregister card
193             case ACTION_UNREGISTER:
194                 showRegisterCard();
195                 unregisterListeners();
196                 // reset the application state when explicitly unregistered
197                 mState = STATE_OTHER;
198                 break;
199             // END_INCLUDE(onclick)
200             // Explanation cards
201             case ACTION_BATCHING_DESCRIPTION_DISMISS:
202                 // permanently remove the batch description card, it will not be shown again
203                 getCardStream().removeCard(CARD_BATCHING_DESCRIPTION);
204                 break;
205             case ACTION_EXPLANATION_DISMISS:
206                 // permanently remove the explanation card, it will not be shown again
207                 getCardStream().removeCard(CARD_EXPLANATION);
208         }
209 
210         // For register cards, display the counting card
211         if (cardTag.equals(CARD_REGISTER_COUNTER) || cardTag.equals(CARD_REGISTER_DETECTOR)) {
212             showCountingCards();
213         }
214     }
215 
216     /**
217      * Register a {@link android.hardware.SensorEventListener} for the sensor and max batch delay.
218      * The maximum batch delay specifies the maximum duration in microseconds for which subsequent
219      * sensor events can be temporarily stored by the sensor before they are delivered to the
220      * registered SensorEventListener. A larger delay allows the system to handle sensor events more
221      * efficiently, allowing the system to switch to a lower power state while the sensor is
222      * capturing events. Once the max delay is reached, all stored events are delivered to the
223      * registered listener. Note that this value only specifies the maximum delay, the listener may
224      * receive events quicker. A delay of 0 disables batch mode and registers the listener in
225      * continuous mode.
226      * The optimium batch delay depends on the application. For example, a delay of 5 seconds or
227      * higher may be appropriate for an  application that does not update the UI in real time.
228      *
229      * @param maxdelay
230      * @param sensorType
231      */
registerEventListener(int maxdelay, int sensorType)232     private void registerEventListener(int maxdelay, int sensorType) {
233         // BEGIN_INCLUDE(register)
234 
235         // Keep track of state so that the correct sensor type and batch delay can be set up when
236         // the app is restored (for example on screen rotation).
237         mMaxDelay = maxdelay;
238         if (sensorType == Sensor.TYPE_STEP_COUNTER) {
239             mState = STATE_COUNTER;
240             /*
241             Reset the initial step counter value, the first event received by the event listener is
242             stored in mCounterSteps and used to calculate the total number of steps taken.
243              */
244             mCounterSteps = 0;
245             Log.i(TAG, "Event listener for step counter sensor registered with a max delay of "
246                     + mMaxDelay);
247         } else {
248             mState = STATE_DETECTOR;
249             Log.i(TAG, "Event listener for step detector sensor registered with a max delay of "
250                     + mMaxDelay);
251         }
252 
253         // Get the default sensor for the sensor type from the SenorManager
254         SensorManager sensorManager =
255                 (SensorManager) getActivity().getSystemService(Activity.SENSOR_SERVICE);
256         // sensorType is either Sensor.TYPE_STEP_COUNTER or Sensor.TYPE_STEP_DETECTOR
257         Sensor sensor = sensorManager.getDefaultSensor(sensorType);
258 
259         // Register the listener for this sensor in batch mode.
260         // If the max delay is 0, events will be delivered in continuous mode without batching.
261         final boolean batchMode = sensorManager.registerListener(
262                 mListener, sensor, SensorManager.SENSOR_DELAY_NORMAL, maxdelay);
263 
264         if (!batchMode) {
265             // Batch mode could not be enabled, show a warning message and switch to continuous mode
266             getCardStream().getCard(CARD_NOBATCHSUPPORT)
267                     .setDescription(getString(R.string.warning_nobatching));
268             getCardStream().showCard(CARD_NOBATCHSUPPORT);
269             Log.w(TAG, "Could not register sensor listener in batch mode, " +
270                     "falling back to continuous mode.");
271         }
272 
273         if (maxdelay > 0 && batchMode) {
274             // Batch mode was enabled successfully, show a description card
275             getCardStream().showCard(CARD_BATCHING_DESCRIPTION);
276         }
277 
278         // Show the explanation card
279         getCardStream().showCard(CARD_EXPLANATION);
280 
281         // END_INCLUDE(register)
282 
283     }
284 
285     /**
286      * Unregisters the sensor listener if it is registered.
287      */
unregisterListeners()288     private void unregisterListeners() {
289         // BEGIN_INCLUDE(unregister)
290         SensorManager sensorManager =
291                 (SensorManager) getActivity().getSystemService(Activity.SENSOR_SERVICE);
292         sensorManager.unregisterListener(mListener);
293         Log.i(TAG, "Sensor listener unregistered.");
294 
295         // END_INCLUDE(unregister)
296     }
297 
298     /**
299      * Resets the step counter by clearing all counting variables and lists.
300      */
resetCounter()301     private void resetCounter() {
302         // BEGIN_INCLUDE(reset)
303         mSteps = 0;
304         mCounterSteps = 0;
305         mEventLength = 0;
306         mEventDelays = new float[EVENT_QUEUE_LENGTH];
307         mPreviousCounterSteps = 0;
308         // END_INCLUDE(reset)
309     }
310 
311 
312     /**
313      * Listener that handles step sensor events for step detector and step counter sensors.
314      */
315     private final SensorEventListener mListener = new SensorEventListener() {
316         @Override
317         public void onSensorChanged(SensorEvent event) {
318             // BEGIN_INCLUDE(sensorevent)
319             // store the delay of this event
320             recordDelay(event);
321             final String delayString = getDelayString();
322 
323             if (event.sensor.getType() == Sensor.TYPE_STEP_DETECTOR) {
324                 // A step detector event is received for each step.
325                 // This means we need to count steps ourselves
326 
327                 mSteps += event.values.length;
328 
329                 // Update the card with the latest step count
330                 getCardStream().getCard(CARD_COUNTING)
331                         .setTitle(getString(R.string.counting_title, mSteps))
332                         .setDescription(getString(R.string.counting_description,
333                                 getString(R.string.sensor_detector), mMaxDelay, delayString));
334 
335                 Log.i(TAG,
336                         "New step detected by STEP_DETECTOR sensor. Total step count: " + mSteps);
337 
338             } else if (event.sensor.getType() == Sensor.TYPE_STEP_COUNTER) {
339 
340                 /*
341                 A step counter event contains the total number of steps since the listener
342                 was first registered. We need to keep track of this initial value to calculate the
343                 number of steps taken, as the first value a listener receives is undefined.
344                  */
345                 if (mCounterSteps < 1) {
346                     // initial value
347                     mCounterSteps = (int) event.values[0];
348                 }
349 
350                 // Calculate steps taken based on first counter value received.
351                 mSteps = (int) event.values[0] - mCounterSteps;
352 
353                 // Add the number of steps previously taken, otherwise the counter would start at 0.
354                 // This is needed to keep the counter consistent across rotation changes.
355                 mSteps = mSteps + mPreviousCounterSteps;
356 
357                 // Update the card with the latest step count
358                 getCardStream().getCard(CARD_COUNTING)
359                         .setTitle(getString(R.string.counting_title, mSteps))
360                         .setDescription(getString(R.string.counting_description,
361                                 getString(R.string.sensor_counter), mMaxDelay, delayString));
362                 Log.i(TAG, "New step detected by STEP_COUNTER sensor. Total step count: " + mSteps);
363                 // END_INCLUDE(sensorevent)
364             }
365         }
366 
367         @Override
368         public void onAccuracyChanged(Sensor sensor, int accuracy) {
369 
370         }
371     };
372 
373     /**
374      * Records the delay for the event.
375      *
376      * @param event
377      */
recordDelay(SensorEvent event)378     private void recordDelay(SensorEvent event) {
379         // Calculate the delay from when event was recorded until it was received here in ms
380         // Event timestamp is recorded in us accuracy, but ms accuracy is sufficient here
381         mEventDelays[mEventData] = System.currentTimeMillis() - (event.timestamp / 1000000L);
382 
383         // Increment length counter
384         mEventLength = Math.min(EVENT_QUEUE_LENGTH, mEventLength + 1);
385         // Move pointer to the next (oldest) location
386         mEventData = (mEventData + 1) % EVENT_QUEUE_LENGTH;
387     }
388 
389     private final StringBuffer mDelayStringBuffer = new StringBuffer();
390 
391     /**
392      * Returns a string describing the sensor delays recorded in
393      * {@link #recordDelay(android.hardware.SensorEvent)}.
394      *
395      * @return
396      */
getDelayString()397     private String getDelayString() {
398         // Empty the StringBuffer
399         mDelayStringBuffer.setLength(0);
400 
401         // Loop over all recorded delays and append them to the buffer as a decimal
402         for (int i = 0; i < mEventLength; i++) {
403             if (i > 0) {
404                 mDelayStringBuffer.append(", ");
405             }
406             final int index = (mEventData + i) % EVENT_QUEUE_LENGTH;
407             final float delay = mEventDelays[index] / 1000f; // convert delay from ms into s
408             mDelayStringBuffer.append(String.format("%1.1f", delay));
409         }
410 
411         return mDelayStringBuffer.toString();
412     }
413 
414     /**
415      * Records the state of the application into the {@link android.os.Bundle}.
416      *
417      * @param outState
418      */
419     @Override
onSaveInstanceState(Bundle outState)420     public void onSaveInstanceState(Bundle outState) {
421         // BEGIN_INCLUDE(saveinstance)
422         super.onSaveInstanceState(outState);
423         // Store all variables required to restore the state of the application
424         outState.putInt(BUNDLE_LATENCY, mMaxDelay);
425         outState.putInt(BUNDLE_STATE, mState);
426         outState.putInt(BUNDLE_STEPS, mSteps);
427         // END_INCLUDE(saveinstance)
428     }
429 
430     @Override
onActivityCreated(Bundle savedInstanceState)431     public void onActivityCreated(Bundle savedInstanceState) {
432         super.onActivityCreated(savedInstanceState);
433         // BEGIN_INCLUDE(restore)
434         // Fragment is being restored, reinitialise its state with data from the bundle
435         if (savedInstanceState != null) {
436             resetCounter();
437             mSteps = savedInstanceState.getInt(BUNDLE_STEPS);
438             mState = savedInstanceState.getInt(BUNDLE_STATE);
439             mMaxDelay = savedInstanceState.getInt(BUNDLE_LATENCY);
440 
441             // Register listeners again if in detector or counter states with restored delay
442             if (mState == STATE_DETECTOR) {
443                 registerEventListener(mMaxDelay, Sensor.TYPE_STEP_DETECTOR);
444             } else if (mState == STATE_COUNTER) {
445                 // store the previous number of steps to keep  step counter count consistent
446                 mPreviousCounterSteps = mSteps;
447                 registerEventListener(mMaxDelay, Sensor.TYPE_STEP_COUNTER);
448             }
449         }
450         // END_INCLUDE(restore)
451     }
452 
453     /**
454      * Hides the registration cards, reset the counter and show the step counting card.
455      */
showCountingCards()456     private void showCountingCards() {
457         // Hide the registration cards
458         getCardStream().hideCard(CARD_REGISTER_DETECTOR);
459         getCardStream().hideCard(CARD_REGISTER_COUNTER);
460 
461         // Show the explanation card if it has not been dismissed
462         getCardStream().showCard(CARD_EXPLANATION);
463 
464         // Reset the step counter, then show the step counting card
465         resetCounter();
466 
467         // Set the inital text for the step counting card before a step is recorded
468         String sensor = "-";
469         if (mState == STATE_COUNTER) {
470             sensor = getString(R.string.sensor_counter);
471         } else if (mState == STATE_DETECTOR) {
472             sensor = getString(R.string.sensor_detector);
473         }
474         // Set initial text
475         getCardStream().getCard(CARD_COUNTING)
476                 .setTitle(getString(R.string.counting_title, 0))
477                 .setDescription(getString(R.string.counting_description, sensor, mMaxDelay, "-"));
478 
479         // Show the counting card and make it undismissable
480         getCardStream().showCard(CARD_COUNTING, false);
481 
482     }
483 
484     /**
485      * Show the introduction card
486      */
showIntroCard()487     private void showIntroCard() {
488         Card c = new Card.Builder(this, CARD_INTRO)
489                 .setTitle(getString(R.string.intro_title))
490                 .setDescription(getString(R.string.intro_message))
491                 .build(getActivity());
492         getCardStream().addCard(c, true);
493     }
494 
495     /**
496      * Show two registration cards, one for the step detector and counter sensors.
497      */
showRegisterCard()498     private void showRegisterCard() {
499         // Hide the counting and explanation cards
500         getCardStream().hideCard(CARD_BATCHING_DESCRIPTION);
501         getCardStream().hideCard(CARD_EXPLANATION);
502         getCardStream().hideCard(CARD_COUNTING);
503 
504         // Show two undismissable registration cards, one for each step sensor
505         getCardStream().showCard(CARD_REGISTER_DETECTOR, false);
506         getCardStream().showCard(CARD_REGISTER_COUNTER, false);
507     }
508 
509     /**
510      * Show the error card.
511      */
showErrorCard()512     private void showErrorCard() {
513         getCardStream().showCard(CARD_NOBATCHSUPPORT, false);
514     }
515 
516     /**
517      * Initialise Cards.
518      */
initialiseCards()519     private void initialiseCards() {
520         // Step counting
521         Card c = new Card.Builder(this, CARD_COUNTING)
522                 .setTitle("Steps")
523                 .setDescription("")
524                 .addAction("Unregister Listener", ACTION_UNREGISTER, Card.ACTION_NEGATIVE)
525                 .build(getActivity());
526         getCardStream().addCard(c);
527 
528         // Register step detector listener
529         c = new Card.Builder(this, CARD_REGISTER_DETECTOR)
530                 .setTitle(getString(R.string.register_detector_title))
531                 .setDescription(getString(R.string.register_detector_description))
532                 .addAction(getString(R.string.register_0),
533                         ACTION_REGISTER_DETECT_NOBATCHING, Card.ACTION_NEUTRAL)
534                 .addAction(getString(R.string.register_5),
535                         ACTION_REGISTER_DETECT_BATCHING_5s, Card.ACTION_NEUTRAL)
536                 .addAction(getString(R.string.register_10),
537                         ACTION_REGISTER_DETECT_BATCHING_10s, Card.ACTION_NEUTRAL)
538                 .build(getActivity());
539         getCardStream().addCard(c);
540 
541         // Register step counter listener
542         c = new Card.Builder(this, CARD_REGISTER_COUNTER)
543                 .setTitle(getString(R.string.register_counter_title))
544                 .setDescription(getString(R.string.register_counter_description))
545                 .addAction(getString(R.string.register_0),
546                         ACTION_REGISTER_COUNT_NOBATCHING, Card.ACTION_NEUTRAL)
547                 .addAction(getString(R.string.register_5),
548                         ACTION_REGISTER_COUNT_BATCHING_5s, Card.ACTION_NEUTRAL)
549                 .addAction(getString(R.string.register_10),
550                         ACTION_REGISTER_COUNT_BATCHING_10s, Card.ACTION_NEUTRAL)
551                 .build(getActivity());
552         getCardStream().addCard(c);
553 
554 
555         // Batching description
556         c = new Card.Builder(this, CARD_BATCHING_DESCRIPTION)
557                 .setTitle(getString(R.string.batching_queue_title))
558                 .setDescription(getString(R.string.batching_queue_description))
559                 .addAction(getString(R.string.action_notagain),
560                         ACTION_BATCHING_DESCRIPTION_DISMISS, Card.ACTION_POSITIVE)
561                 .build(getActivity());
562         getCardStream().addCard(c);
563 
564         // Explanation
565         c = new Card.Builder(this, CARD_EXPLANATION)
566                 .setDescription(getString(R.string.explanation_description))
567                 .addAction(getString(R.string.action_notagain),
568                         ACTION_EXPLANATION_DISMISS, Card.ACTION_POSITIVE)
569                 .build(getActivity());
570         getCardStream().addCard(c);
571 
572         // Error
573         c = new Card.Builder(this, CARD_NOBATCHSUPPORT)
574                 .setTitle(getString(R.string.error_title))
575                 .setDescription(getString(R.string.error_nosensor))
576                 .build(getActivity());
577         getCardStream().addCard(c);
578     }
579 
580     /**
581      * Returns the cached CardStreamFragment used to show cards.
582      *
583      * @return
584      */
getCardStream()585     private CardStreamFragment getCardStream() {
586         if (mCards == null) {
587             mCards = ((CardStream) getActivity()).getCardStream();
588         }
589         return mCards;
590     }
591 
592 }
593