ZenModePanel.java revision 980f9925bb044bc87c1de1f0e21372f46d7d596d
1/* 2 * Copyright (C) 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 17package com.android.systemui.volume; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.content.Context; 22import android.content.Intent; 23import android.content.SharedPreferences; 24import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 25import android.net.Uri; 26import android.os.Handler; 27import android.os.Looper; 28import android.os.Message; 29import android.provider.Settings; 30import android.provider.Settings.Global; 31import android.service.notification.Condition; 32import android.service.notification.ZenModeConfig; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.view.ContextThemeWrapper; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.animation.AnimationUtils; 39import android.view.animation.Interpolator; 40import android.widget.CompoundButton; 41import android.widget.CompoundButton.OnCheckedChangeListener; 42import android.widget.ImageView; 43import android.widget.LinearLayout; 44import android.widget.RadioButton; 45import android.widget.TextView; 46 47import com.android.systemui.R; 48import com.android.systemui.statusbar.policy.ZenModeController; 49 50import java.util.Arrays; 51import java.util.Objects; 52 53public class ZenModePanel extends LinearLayout { 54 private static final String TAG = "ZenModePanel"; 55 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 56 57 private static final int[] MINUTE_BUCKETS = DEBUG 58 ? new int[] { 1, 2, 5, 15, 30, 45, 60, 120, 180, 240, 480 } 59 : new int[] { 15, 30, 45, 60, 120, 180, 240, 480 }; 60 private static final int MIN_BUCKET_MINUTES = MINUTE_BUCKETS[0]; 61 private static final int MAX_BUCKET_MINUTES = MINUTE_BUCKETS[MINUTE_BUCKETS.length - 1]; 62 private static final int DEFAULT_BUCKET_INDEX = Arrays.binarySearch(MINUTE_BUCKETS, 60); 63 private static final int FOREVER_CONDITION_INDEX = 0; 64 private static final int TIME_CONDITION_INDEX = 1; 65 private static final int FIRST_CONDITION_INDEX = 2; 66 private static final float SILENT_HINT_PULSE_SCALE = 1.1f; 67 68 private static final int SECONDS_MS = 1000; 69 private static final int MINUTES_MS = 60 * SECONDS_MS; 70 71 public static final Intent ZEN_SETTINGS = new Intent(Settings.ACTION_ZEN_MODE_SETTINGS); 72 73 private final Context mContext; 74 private final LayoutInflater mInflater; 75 private final H mHandler = new H(); 76 private final Favorites mFavorites; 77 private final Interpolator mFastOutSlowInInterpolator; 78 79 private char mLogTag = '?'; 80 private String mTag; 81 82 private SegmentedButtons mZenButtons; 83 private View mZenSubhead; 84 private TextView mZenSubheadCollapsed; 85 private TextView mZenSubheadExpanded; 86 private View mMoreSettings; 87 private LinearLayout mZenConditions; 88 89 private Callback mCallback; 90 private ZenModeController mController; 91 private boolean mRequestingConditions; 92 private Uri mExitConditionId; 93 private int mBucketIndex = -1; 94 private boolean mExpanded; 95 private int mAttachedZen; 96 private String mExitConditionText; 97 98 public ZenModePanel(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 mContext = context; 101 mFavorites = new Favorites(); 102 mInflater = LayoutInflater.from(mContext.getApplicationContext()); 103 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, 104 android.R.interpolator.fast_out_slow_in); 105 updateTag(); 106 if (DEBUG) Log.d(mTag, "new ZenModePanel"); 107 } 108 109 private void updateTag() { 110 mTag = TAG + "/" + mLogTag + "/" + Integer.toHexString(System.identityHashCode(this)); 111 } 112 113 @Override 114 protected void onFinishInflate() { 115 super.onFinishInflate(); 116 117 mZenButtons = (SegmentedButtons) findViewById(R.id.zen_buttons); 118 mZenButtons.addButton(R.string.interruption_level_none, Global.ZEN_MODE_NO_INTERRUPTIONS); 119 mZenButtons.addButton(R.string.interruption_level_priority, 120 Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS); 121 mZenButtons.addButton(R.string.interruption_level_all, Global.ZEN_MODE_OFF); 122 mZenButtons.setCallback(mZenButtonsCallback); 123 124 mZenSubhead = findViewById(R.id.zen_subhead); 125 126 mZenSubheadCollapsed = (TextView) findViewById(R.id.zen_subhead_collapsed); 127 mZenSubheadCollapsed.setOnClickListener(new View.OnClickListener() { 128 @Override 129 public void onClick(View v) { 130 setExpanded(true); 131 fireInteraction(); 132 } 133 }); 134 135 mZenSubheadExpanded = (TextView) findViewById(R.id.zen_subhead_expanded); 136 mZenSubheadExpanded.setOnClickListener(new View.OnClickListener() { 137 @Override 138 public void onClick(View v) { 139 setExpanded(false); 140 fireInteraction(); 141 } 142 }); 143 144 mMoreSettings = findViewById(R.id.zen_more_settings); 145 mMoreSettings.setOnClickListener(new View.OnClickListener() { 146 @Override 147 public void onClick(View v) { 148 fireMoreSettings(); 149 fireInteraction(); 150 } 151 }); 152 153 mZenConditions = (LinearLayout) findViewById(R.id.zen_conditions); 154 } 155 156 @Override 157 protected void onAttachedToWindow() { 158 super.onAttachedToWindow(); 159 if (DEBUG) Log.d(mTag, "onAttachedToWindow"); 160 mAttachedZen = getSelectedZen(-1); 161 refreshExitConditionText(); 162 } 163 164 @Override 165 protected void onDetachedFromWindow() { 166 super.onDetachedFromWindow(); 167 if (DEBUG) Log.d(mTag, "onDetachedFromWindow"); 168 mAttachedZen = -1; 169 setExpanded(false); 170 } 171 172 private void setExpanded(boolean expanded) { 173 if (expanded == mExpanded) return; 174 mExpanded = expanded; 175 updateWidgets(); 176 setRequestingConditions(mExpanded); 177 fireExpanded(); 178 } 179 180 /** Start or stop requesting relevant zen mode exit conditions */ 181 private void setRequestingConditions(boolean requesting) { 182 if (mRequestingConditions == requesting) return; 183 if (DEBUG) Log.d(mTag, "setRequestingConditions " + requesting); 184 mRequestingConditions = requesting; 185 if (mController != null) { 186 mController.requestConditions(mRequestingConditions); 187 } 188 if (mRequestingConditions) { 189 Condition timeCondition = parseExistingTimeCondition(mExitConditionId); 190 if (timeCondition != null) { 191 mBucketIndex = -1; 192 } else { 193 mBucketIndex = DEFAULT_BUCKET_INDEX; 194 timeCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]); 195 } 196 if (DEBUG) Log.d(mTag, "Initial bucket index: " + mBucketIndex); 197 handleUpdateConditions(new Condition[0]); // ensures forever exists 198 bind(timeCondition, mZenConditions.getChildAt(TIME_CONDITION_INDEX)); 199 checkForDefault(); 200 } else { 201 mZenConditions.removeAllViews(); 202 } 203 } 204 205 public void init(ZenModeController controller, char logTag) { 206 mController = controller; 207 mLogTag = logTag; 208 updateTag(); 209 setExitConditionId(mController.getExitConditionId()); 210 refreshExitConditionText(); 211 mAttachedZen = getSelectedZen(-1); 212 handleUpdateZen(mController.getZen()); 213 if (DEBUG) Log.d(mTag, "init mExitConditionId=" + mExitConditionId); 214 mZenConditions.removeAllViews(); 215 mController.addCallback(mZenCallback); 216 } 217 218 private void setExitConditionId(Uri exitConditionId) { 219 if (Objects.equals(mExitConditionId, exitConditionId)) return; 220 mExitConditionId = exitConditionId; 221 refreshExitConditionText(); 222 } 223 224 private void refreshExitConditionText() { 225 if (mExitConditionId == null) { 226 mExitConditionText = mContext.getString(R.string.zen_mode_forever); 227 } else if (ZenModeConfig.isValidCountdownConditionId(mExitConditionId)) { 228 mExitConditionText = parseExistingTimeCondition(mExitConditionId).summary; 229 } else { 230 mExitConditionText = "(until condition ends)"; // TODO persist current description 231 } 232 } 233 234 public void setCallback(Callback callback) { 235 mCallback = callback; 236 } 237 238 public void showSilentHint() { 239 if (DEBUG) Log.d(mTag, "showSilentHint"); 240 if (mZenButtons == null || mZenButtons.getChildCount() == 0) return; 241 final View noneButton = mZenButtons.getChildAt(0); 242 if (noneButton.getScaleX() != 1) return; // already running 243 noneButton.animate().cancel(); 244 noneButton.animate().scaleX(SILENT_HINT_PULSE_SCALE).scaleY(SILENT_HINT_PULSE_SCALE) 245 .setInterpolator(mFastOutSlowInInterpolator) 246 .setListener(new AnimatorListenerAdapter() { 247 @Override 248 public void onAnimationEnd(Animator animation) { 249 noneButton.animate().scaleX(1).scaleY(1).setListener(null); 250 } 251 }); 252 } 253 254 private void handleUpdateZen(int zen) { 255 if (mAttachedZen != -1 && mAttachedZen != zen) { 256 setExpanded(zen != Global.ZEN_MODE_OFF); 257 mAttachedZen = zen; 258 } 259 mZenButtons.setSelectedValue(zen); 260 updateWidgets(); 261 } 262 263 private int getSelectedZen(int defValue) { 264 final Object zen = mZenButtons.getSelectedValue(); 265 return zen != null ? (Integer) zen : defValue; 266 } 267 268 private void updateWidgets() { 269 final int zen = getSelectedZen(Global.ZEN_MODE_OFF); 270 final boolean zenOff = zen == Global.ZEN_MODE_OFF; 271 final boolean zenImportant = zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; 272 final boolean zenNone = zen == Global.ZEN_MODE_NO_INTERRUPTIONS; 273 274 mZenSubhead.setVisibility(!zenOff ? VISIBLE : GONE); 275 mZenSubheadExpanded.setVisibility(mExpanded ? VISIBLE : GONE); 276 mZenSubheadCollapsed.setVisibility(!mExpanded ? VISIBLE : GONE); 277 mMoreSettings.setVisibility(zenImportant && mExpanded ? VISIBLE : GONE); 278 mZenConditions.setVisibility(!zenOff && mExpanded ? VISIBLE : GONE); 279 280 if (zenNone) { 281 mZenSubheadExpanded.setText(R.string.zen_no_interruptions_with_warning); 282 mZenSubheadCollapsed.setText(mExitConditionText); 283 } else if (zenImportant) { 284 mZenSubheadExpanded.setText(R.string.zen_important_interruptions); 285 mZenSubheadCollapsed.setText(mExitConditionText); 286 } 287 } 288 289 private Condition parseExistingTimeCondition(Uri conditionId) { 290 final long time = ZenModeConfig.tryParseCountdownConditionId(conditionId); 291 if (time == 0) return null; 292 final long span = time - System.currentTimeMillis(); 293 if (span <= 0 || span > MAX_BUCKET_MINUTES * MINUTES_MS) return null; 294 return timeCondition(time, Math.round(span / (float)MINUTES_MS)); 295 } 296 297 private Condition newTimeCondition(int minutesFromNow) { 298 final long now = System.currentTimeMillis(); 299 return timeCondition(now + minutesFromNow * MINUTES_MS, minutesFromNow); 300 } 301 302 private Condition timeCondition(long time, int minutes) { 303 final int num = minutes < 60 ? minutes : Math.round(minutes / 60f); 304 final int resId = minutes < 60 305 ? R.plurals.zen_mode_duration_minutes 306 : R.plurals.zen_mode_duration_hours; 307 final String caption = mContext.getResources().getQuantityString(resId, num, num); 308 final Uri id = ZenModeConfig.toCountdownConditionId(time); 309 return new Condition(id, caption, "", "", 0, Condition.STATE_TRUE, 310 Condition.FLAG_RELEVANT_NOW); 311 } 312 313 private void handleUpdateConditions(Condition[] conditions) { 314 final int newCount = conditions == null ? 0 : conditions.length; 315 if (DEBUG) Log.d(mTag, "handleUpdateConditions newCount=" + newCount); 316 for (int i = mZenConditions.getChildCount(); i >= newCount + FIRST_CONDITION_INDEX; i--) { 317 mZenConditions.removeViewAt(i); 318 } 319 bind(null, mZenConditions.getChildAt(FOREVER_CONDITION_INDEX)); 320 for (int i = 0; i < newCount; i++) { 321 bind(conditions[i], mZenConditions.getChildAt(FIRST_CONDITION_INDEX + i)); 322 } 323 } 324 325 private ConditionTag getConditionTagAt(int index) { 326 return (ConditionTag) mZenConditions.getChildAt(index).getTag(); 327 } 328 329 private void checkForDefault() { 330 // are we left without anything selected? if so, set a default 331 for (int i = 0; i < mZenConditions.getChildCount(); i++) { 332 if (getConditionTagAt(i).rb.isChecked()) { 333 return; 334 } 335 } 336 if (DEBUG) Log.d(mTag, "Selecting a default"); 337 final int favoriteIndex = mFavorites.getMinuteIndex(); 338 if (favoriteIndex == -1) { 339 getConditionTagAt(FOREVER_CONDITION_INDEX).rb.setChecked(true); 340 } else { 341 final Condition c = newTimeCondition(MINUTE_BUCKETS[favoriteIndex]); 342 mBucketIndex = favoriteIndex; 343 bind(c, mZenConditions.getChildAt(TIME_CONDITION_INDEX)); 344 getConditionTagAt(TIME_CONDITION_INDEX).rb.setChecked(true); 345 } 346 } 347 348 private void handleExitConditionChanged(Uri exitCondition) { 349 setExitConditionId(exitCondition); 350 if (DEBUG) Log.d(mTag, "handleExitConditionChanged " + mExitConditionId); 351 final int N = mZenConditions.getChildCount(); 352 for (int i = 0; i < N; i++) { 353 final ConditionTag tag = getConditionTagAt(i); 354 tag.rb.setChecked(Objects.equals(tag.conditionId, exitCondition)); 355 } 356 } 357 358 private void bind(final Condition condition, View convertView) { 359 final boolean enabled = condition == null || condition.state == Condition.STATE_TRUE; 360 final View row; 361 if (convertView == null) { 362 row = mInflater.inflate(R.layout.zen_mode_condition, this, false); 363 if (DEBUG) Log.d(mTag, "Adding new condition view for: " + condition); 364 mZenConditions.addView(row); 365 } else { 366 row = convertView; 367 } 368 final ConditionTag tag = 369 row.getTag() != null ? (ConditionTag) row.getTag() : new ConditionTag(); 370 row.setTag(tag); 371 if (tag.rb == null) { 372 tag.rb = (RadioButton) row.findViewById(android.R.id.checkbox); 373 } 374 tag.conditionId = condition != null ? condition.id : null; 375 tag.rb.setEnabled(enabled); 376 tag.rb.setOnCheckedChangeListener(new OnCheckedChangeListener() { 377 @Override 378 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 379 if (mExpanded && isChecked) { 380 if (DEBUG) Log.d(mTag, "onCheckedChanged " + tag.conditionId); 381 final int N = mZenConditions.getChildCount(); 382 for (int i = 0; i < N; i++) { 383 ConditionTag childTag = getConditionTagAt(i); 384 if (childTag == tag) continue; 385 childTag.rb.setChecked(false); 386 } 387 select(tag.conditionId); 388 fireInteraction(); 389 } 390 } 391 }); 392 final TextView title = (TextView) row.findViewById(android.R.id.title); 393 if (condition == null) { 394 title.setText(R.string.zen_mode_forever); 395 } else { 396 title.setText(condition.summary); 397 } 398 title.setEnabled(enabled); 399 title.setAlpha(enabled ? 1 : .4f); 400 final ImageView button1 = (ImageView) row.findViewById(android.R.id.button1); 401 button1.setOnClickListener(new OnClickListener() { 402 @Override 403 public void onClick(View v) { 404 onClickTimeButton(row, tag, false /*down*/); 405 } 406 }); 407 408 final ImageView button2 = (ImageView) row.findViewById(android.R.id.button2); 409 button2.setOnClickListener(new OnClickListener() { 410 @Override 411 public void onClick(View v) { 412 onClickTimeButton(row, tag, true /*up*/); 413 } 414 }); 415 title.setOnClickListener(new OnClickListener() { 416 @Override 417 public void onClick(View v) { 418 tag.rb.setChecked(true); 419 fireInteraction(); 420 } 421 }); 422 423 final long time = ZenModeConfig.tryParseCountdownConditionId(tag.conditionId); 424 if (time > 0) { 425 if (mBucketIndex > -1) { 426 button1.setEnabled(mBucketIndex > 0); 427 button2.setEnabled(mBucketIndex < MINUTE_BUCKETS.length - 1); 428 } else { 429 final long span = time - System.currentTimeMillis(); 430 button1.setEnabled(span > MIN_BUCKET_MINUTES * MINUTES_MS); 431 final Condition maxCondition = newTimeCondition(MAX_BUCKET_MINUTES); 432 button2.setEnabled(!Objects.equals(condition.summary, maxCondition.summary)); 433 } 434 435 button1.setAlpha(button1.isEnabled() ? 1f : .5f); 436 button2.setAlpha(button2.isEnabled() ? 1f : .5f); 437 } else { 438 button1.setVisibility(View.GONE); 439 button2.setVisibility(View.GONE); 440 } 441 } 442 443 private void onClickTimeButton(View row, ConditionTag tag, boolean up) { 444 Condition newCondition = null; 445 final int N = MINUTE_BUCKETS.length; 446 if (mBucketIndex == -1) { 447 // not on a known index, search for the next or prev bucket by time 448 final long time = ZenModeConfig.tryParseCountdownConditionId(tag.conditionId); 449 final long now = System.currentTimeMillis(); 450 for (int i = 0; i < N; i++) { 451 int j = up ? i : N - 1 - i; 452 final int bucketMinutes = MINUTE_BUCKETS[j]; 453 final long bucketTime = now + bucketMinutes * MINUTES_MS; 454 if (up && bucketTime > time || !up && bucketTime < time) { 455 mBucketIndex = j; 456 newCondition = timeCondition(bucketTime, bucketMinutes); 457 break; 458 } 459 } 460 if (newCondition == null) { 461 mBucketIndex = DEFAULT_BUCKET_INDEX; 462 newCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]); 463 } 464 } else { 465 // on a known index, simply increment or decrement 466 mBucketIndex = Math.max(0, Math.min(N - 1, mBucketIndex + (up ? 1 : -1))); 467 newCondition = newTimeCondition(MINUTE_BUCKETS[mBucketIndex]); 468 } 469 bind(newCondition, row); 470 tag.rb.setChecked(true); 471 select(newCondition.id); 472 fireInteraction(); 473 } 474 475 private void select(Uri conditionId) { 476 if (DEBUG) Log.d(mTag, "select " + conditionId); 477 if (mController != null) { 478 mController.setExitConditionId(conditionId); 479 } 480 setExitConditionId(conditionId); 481 if (conditionId == null) { 482 mFavorites.setMinuteIndex(-1); 483 } else if (ZenModeConfig.isValidCountdownConditionId(conditionId) && mBucketIndex != -1) { 484 mFavorites.setMinuteIndex(mBucketIndex); 485 } 486 } 487 488 private void fireMoreSettings() { 489 if (mCallback != null) { 490 mCallback.onMoreSettings(); 491 } 492 } 493 494 private void fireInteraction() { 495 if (mCallback != null) { 496 mCallback.onInteraction(); 497 } 498 } 499 500 private void fireExpanded() { 501 if (mCallback != null) { 502 mCallback.onExpanded(mExpanded); 503 } 504 } 505 506 private final ZenModeController.Callback mZenCallback = new ZenModeController.Callback() { 507 @Override 508 public void onZenChanged(int zen) { 509 mHandler.obtainMessage(H.UPDATE_ZEN, zen, 0).sendToTarget(); 510 } 511 @Override 512 public void onConditionsChanged(Condition[] conditions) { 513 mHandler.obtainMessage(H.UPDATE_CONDITIONS, conditions).sendToTarget(); 514 } 515 516 @Override 517 public void onExitConditionChanged(Uri exitConditionId) { 518 mHandler.obtainMessage(H.EXIT_CONDITION_CHANGED, exitConditionId).sendToTarget(); 519 } 520 }; 521 522 private final class H extends Handler { 523 private static final int UPDATE_CONDITIONS = 1; 524 private static final int EXIT_CONDITION_CHANGED = 2; 525 private static final int UPDATE_ZEN = 3; 526 527 private H() { 528 super(Looper.getMainLooper()); 529 } 530 531 @Override 532 public void handleMessage(Message msg) { 533 if (msg.what == UPDATE_CONDITIONS) { 534 handleUpdateConditions((Condition[])msg.obj); 535 checkForDefault(); 536 } else if (msg.what == EXIT_CONDITION_CHANGED) { 537 handleExitConditionChanged((Uri)msg.obj); 538 } else if (msg.what == UPDATE_ZEN) { 539 handleUpdateZen(msg.arg1); 540 } 541 } 542 } 543 544 public interface Callback { 545 void onMoreSettings(); 546 void onInteraction(); 547 void onExpanded(boolean expanded); 548 } 549 550 // used as the view tag on condition rows 551 private static class ConditionTag { 552 RadioButton rb; 553 Uri conditionId; 554 } 555 556 private final class Favorites implements OnSharedPreferenceChangeListener { 557 private static final String KEY_MINUTE_INDEX = "minuteIndex"; 558 559 private int mMinuteIndex; 560 561 private Favorites() { 562 prefs().registerOnSharedPreferenceChangeListener(this); 563 updateMinuteIndex(); 564 } 565 566 public int getMinuteIndex() { 567 return mMinuteIndex; 568 } 569 570 public void setMinuteIndex(int minuteIndex) { 571 minuteIndex = clamp(minuteIndex); 572 if (minuteIndex == mMinuteIndex) return; 573 mMinuteIndex = clamp(minuteIndex); 574 if (DEBUG) Log.d(mTag, "Setting favorite minute index: " + mMinuteIndex); 575 prefs().edit().putInt(KEY_MINUTE_INDEX, mMinuteIndex).apply(); 576 } 577 578 @Override 579 public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { 580 updateMinuteIndex(); 581 } 582 583 private SharedPreferences prefs() { 584 return mContext.getSharedPreferences(ZenModePanel.class.getSimpleName(), 0); 585 } 586 587 private void updateMinuteIndex() { 588 mMinuteIndex = clamp(prefs().getInt(KEY_MINUTE_INDEX, DEFAULT_BUCKET_INDEX)); 589 if (DEBUG) Log.d(mTag, "Favorite minute index: " + mMinuteIndex); 590 } 591 592 private int clamp(int index) { 593 return Math.max(-1, Math.min(MINUTE_BUCKETS.length - 1, index)); 594 } 595 } 596 597 private final SegmentedButtons.Callback mZenButtonsCallback = new SegmentedButtons.Callback() { 598 @Override 599 public void onSelected(Object value) { 600 if (value != null && mZenButtons.isShown()) { 601 if (DEBUG) Log.d(mTag, "mZenButtonsCallback selected=" + value); 602 mController.setZen((Integer) value); 603 mController.setExitConditionId(null); 604 } 605 } 606 }; 607} 608