Loading src/com/android/deskclock/alarms/AlarmActivity.java +79 −21 Original line number Diff line number Diff line Loading @@ -24,9 +24,11 @@ import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.graphics.Color; import android.graphics.Rect; Loading @@ -34,6 +36,7 @@ import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.graphics.ColorUtils; Loading @@ -59,17 +62,6 @@ import com.android.deskclock.widget.CircleView; public class AlarmActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener { /** * AlarmActivity listens for this broadcast intent, so that other applications can snooze the * alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION). */ public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE"; /** * AlarmActivity listens for this broadcast intent, so that other applications can dismiss * the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION). */ public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS"; private static final String LOGTAG = AlarmActivity.class.getSimpleName(); private static final TimeInterpolator PULSE_INTERPOLATOR = Loading @@ -95,10 +87,10 @@ public class AlarmActivity extends AppCompatActivity if (!mAlarmHandled) { switch (action) { case ALARM_SNOOZE_ACTION: case AlarmService.ALARM_SNOOZE_ACTION: snooze(); break; case ALARM_DISMISS_ACTION: case AlarmService.ALARM_DISMISS_ACTION: dismiss(); break; case AlarmService.ALARM_DONE_ACTION: Loading @@ -114,11 +106,26 @@ public class AlarmActivity extends AppCompatActivity } }; private final ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { LogUtils.i("Finished binding to AlarmService"); } @Override public void onServiceDisconnected(ComponentName name) { LogUtils.i("Disconnected from AlarmService"); } }; private AlarmInstance mAlarmInstance; private boolean mAlarmHandled; private String mVolumeBehavior; private int mCurrentHourColor; private boolean mReceiverRegistered; /** Whether the AlarmService is currently bound */ private boolean mServiceBound; private ViewGroup mAlertView; private TextView mAlertTitleView; Loading Loading @@ -219,22 +226,48 @@ public class AlarmActivity extends AppCompatActivity // Set the animators to their initial values. setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */); } @Override protected void onStart() { super.onStart(); // Bind to AlarmService bindService(new Intent(this, AlarmService.class), mConnection, Context.BIND_AUTO_CREATE); mServiceBound = true; } @Override protected void onResume() { super.onResume(); // Verify that the alarm is still firing before showing the activity if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) { LogUtils.i(LOGTAG, "Skip displaying alarm for instance: %s", mAlarmInstance); finish(); return; } if (!mReceiverRegistered) { // Register to get the alarm done/snooze/dismiss intent. final IntentFilter filter = new IntentFilter(AlarmService.ALARM_DONE_ACTION); filter.addAction(ALARM_SNOOZE_ACTION); filter.addAction(ALARM_DISMISS_ACTION); filter.addAction(AlarmService.ALARM_SNOOZE_ACTION); filter.addAction(AlarmService.ALARM_DISMISS_ACTION); registerReceiver(mReceiver, filter); mReceiverRegistered = true; } } @Override public void onDestroy() { super.onDestroy(); protected void onPause() { super.onPause(); unbindAlarmService(); // Skip if register didn't happen to avoid IllegalArgumentException if (mReceiverRegistered) { unregisterReceiver(mReceiver); mReceiverRegistered = false; } } Loading Loading @@ -362,6 +395,9 @@ public class AlarmActivity extends AppCompatActivity } } /** * Perform snooze animation and send snooze intent. */ private void snooze() { mAlarmHandled = true; LogUtils.v(LOGTAG, "Snoozed: %s", mAlarmInstance); Loading @@ -377,18 +413,40 @@ public class AlarmActivity extends AppCompatActivity getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText, accessibilityText, alertColor, alertColor).start(); AlarmStateManager.setSnoozeState(this, mAlarmInstance, false /* showToast */); // Unbind here, otherwise alarm will keep ringing until activity finishes. unbindAlarmService(); } /** * Perform dismiss animation and send dismiss intent. */ private void dismiss() { mAlarmHandled = true; LogUtils.v(LOGTAG, "Dismissed: %s", mAlarmInstance); setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */); getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */, getString(R.string.alarm_alert_off_text) /* accessibilityText */, Color.WHITE, mCurrentHourColor).start(); AlarmStateManager.setDismissState(this, mAlarmInstance); // Unbind here, otherwise alarm will keep ringing until activity finishes. unbindAlarmService(); } /** * Unbind AlarmService if bound. */ private void unbindAlarmService() { if (mServiceBound) { unbindService(mConnection); mServiceBound = false; } } private void setAnimatedFractions(float snoozeFraction, float dismissFraction) { Loading src/com/android/deskclock/alarms/AlarmService.java +92 −15 Original line number Diff line number Diff line Loading @@ -16,9 +16,12 @@ package com.android.deskclock.alarms; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Binder; import android.os.IBinder; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; Loading @@ -28,22 +31,59 @@ import com.android.deskclock.LogUtils; import com.android.deskclock.provider.AlarmInstance; /** * This service is in charge of starting/stoping the alarm. It will bring up and manage the * This service is in charge of starting/stopping the alarm. It will bring up and manage the * {@link AlarmActivity} as well as {@link AlarmKlaxon}. * * Registers a broadcast receiver to listen for snooze/dismiss intents. The broadcast receiver * exits early if AlarmActivity is bound to prevent double-processing of the snooze/dismiss intents. */ public class AlarmService extends Service { // A public action send by AlarmService when the alarm has started. /** * AlarmActivity and AlarmService (when unbound0 listen for this broadcast intent * so that other applications can snooze the alarm (after ALARM_ALERT_ACTION and before * ALARM_DONE_ACTION). */ public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE"; /** * AlarmActivity and AlarmService listen for this broadcast intent so that other * applications can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION). */ public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS"; /** A public action sent by AlarmService when the alarm has started. */ public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT"; // A public action sent by AlarmService when the alarm has stopped for any reason. /** A public action sent by AlarmService when the alarm has stopped for any reason. */ public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE"; // Private action used to start an alarm with this service. /** Private action used to start an alarm with this service. */ public static final String START_ALARM_ACTION = "START_ALARM"; // Private action used to stop an alarm with this service. /** Private action used to stop an alarm with this service. */ public static final String STOP_ALARM_ACTION = "STOP_ALARM"; /** Binder given to AlarmActivity */ private final IBinder mBinder = new Binder(); /** Whether the service is currently bound to AlarmActivity */ private boolean mIsBound = false; /** Whether the receiver is currently registered */ private boolean mIsRegistered = false; @Override public IBinder onBind(Intent intent) { mIsBound = true; return mBinder; } @Override public boolean onUnbind(Intent intent) { mIsBound = false; return super.onUnbind(intent); } /** * Utility method to help start alarm properly. If alarm is already firing, it * will mark it as missed and start the new one. Loading Loading @@ -116,44 +156,81 @@ public class AlarmService extends Service { return; } LogUtils.v("AlarmService.stop with instance: " + mCurrentAlarm.mId); LogUtils.v("AlarmService.stop with instance: %s", (Object) mCurrentAlarm.mId); AlarmKlaxon.stop(this); mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); sendBroadcast(new Intent(ALARM_DONE_ACTION)); mCurrentAlarm = null; AlarmAlertWakeLock.releaseCpuLock(); } private final BroadcastReceiver mActionsReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); LogUtils.i("AlarmService received intent %s", action); if (mCurrentAlarm == null || mCurrentAlarm.mAlarmState != AlarmInstance.FIRED_STATE) { LogUtils.i("No valid firing alarm"); return; } if (mIsBound) { LogUtils.i("AlarmActivity bound; AlarmService no-op"); return; } switch (action) { case ALARM_SNOOZE_ACTION: // Set the alarm state to snoozed. // If this broadcast receiver is handling the snooze intent then AlarmActivity // must not be showing, so always show snooze toast. AlarmStateManager.setSnoozeState(context, mCurrentAlarm, true /* showToast */); break; case ALARM_DISMISS_ACTION: // Set the alarm state to dismissed. AlarmStateManager.setDismissState(context, mCurrentAlarm); break; default: break; } } }; @Override public void onCreate() { super.onCreate(); mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); // Register the broadcast receiver final IntentFilter filter = new IntentFilter(ALARM_SNOOZE_ACTION); filter.addAction(ALARM_DISMISS_ACTION); registerReceiver(mActionsReceiver, filter); mIsRegistered = true; } @Override public int onStartCommand(Intent intent, int flags, int startId) { LogUtils.v("AlarmService.onStartCommand() with intent: " + intent.toString()); LogUtils.v("AlarmService.onStartCommand() with intent: %s", intent.toString()); long instanceId = AlarmInstance.getId(intent.getData()); if (START_ALARM_ACTION.equals(intent.getAction())) { ContentResolver cr = this.getContentResolver(); AlarmInstance instance = AlarmInstance.getInstance(cr, instanceId); if (instance == null) { LogUtils.e("No instance found to start alarm: " + instanceId); LogUtils.e("No instance found to start alarm: %d", instanceId); if (mCurrentAlarm != null) { // Only release lock if we are not firing alarm AlarmAlertWakeLock.releaseCpuLock(); } return Service.START_NOT_STICKY; } else if (mCurrentAlarm != null && mCurrentAlarm.mId == instanceId) { LogUtils.e("Alarm already started for instance: " + instanceId); LogUtils.e("Alarm already started for instance: %d", instanceId); return Service.START_NOT_STICKY; } startAlarm(instance); } else if(STOP_ALARM_ACTION.equals(intent.getAction())) { if (mCurrentAlarm != null && mCurrentAlarm.mId != instanceId) { LogUtils.e("Can't stop alarm for instance: " + instanceId + " because current alarm is: " + mCurrentAlarm.mId); LogUtils.e("Can't stop alarm for instance: %d because current alarm is: %d", instanceId, mCurrentAlarm.mId); return Service.START_NOT_STICKY; } stopSelf(); Loading @@ -167,10 +244,10 @@ public class AlarmService extends Service { LogUtils.v("AlarmService.onDestroy() called"); super.onDestroy(); stopCurrentAlarm(); } @Override public IBinder onBind(Intent intent) { return null; if (mIsRegistered) { unregisterReceiver(mActionsReceiver); mIsRegistered = false; } } } Loading
src/com/android/deskclock/alarms/AlarmActivity.java +79 −21 Original line number Diff line number Diff line Loading @@ -24,9 +24,11 @@ import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.graphics.Color; import android.graphics.Rect; Loading @@ -34,6 +36,7 @@ import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.graphics.ColorUtils; Loading @@ -59,17 +62,6 @@ import com.android.deskclock.widget.CircleView; public class AlarmActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener { /** * AlarmActivity listens for this broadcast intent, so that other applications can snooze the * alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION). */ public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE"; /** * AlarmActivity listens for this broadcast intent, so that other applications can dismiss * the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION). */ public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS"; private static final String LOGTAG = AlarmActivity.class.getSimpleName(); private static final TimeInterpolator PULSE_INTERPOLATOR = Loading @@ -95,10 +87,10 @@ public class AlarmActivity extends AppCompatActivity if (!mAlarmHandled) { switch (action) { case ALARM_SNOOZE_ACTION: case AlarmService.ALARM_SNOOZE_ACTION: snooze(); break; case ALARM_DISMISS_ACTION: case AlarmService.ALARM_DISMISS_ACTION: dismiss(); break; case AlarmService.ALARM_DONE_ACTION: Loading @@ -114,11 +106,26 @@ public class AlarmActivity extends AppCompatActivity } }; private final ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { LogUtils.i("Finished binding to AlarmService"); } @Override public void onServiceDisconnected(ComponentName name) { LogUtils.i("Disconnected from AlarmService"); } }; private AlarmInstance mAlarmInstance; private boolean mAlarmHandled; private String mVolumeBehavior; private int mCurrentHourColor; private boolean mReceiverRegistered; /** Whether the AlarmService is currently bound */ private boolean mServiceBound; private ViewGroup mAlertView; private TextView mAlertTitleView; Loading Loading @@ -219,22 +226,48 @@ public class AlarmActivity extends AppCompatActivity // Set the animators to their initial values. setAnimatedFractions(0.0f /* snoozeFraction */, 0.0f /* dismissFraction */); } @Override protected void onStart() { super.onStart(); // Bind to AlarmService bindService(new Intent(this, AlarmService.class), mConnection, Context.BIND_AUTO_CREATE); mServiceBound = true; } @Override protected void onResume() { super.onResume(); // Verify that the alarm is still firing before showing the activity if (mAlarmInstance.mAlarmState != AlarmInstance.FIRED_STATE) { LogUtils.i(LOGTAG, "Skip displaying alarm for instance: %s", mAlarmInstance); finish(); return; } if (!mReceiverRegistered) { // Register to get the alarm done/snooze/dismiss intent. final IntentFilter filter = new IntentFilter(AlarmService.ALARM_DONE_ACTION); filter.addAction(ALARM_SNOOZE_ACTION); filter.addAction(ALARM_DISMISS_ACTION); filter.addAction(AlarmService.ALARM_SNOOZE_ACTION); filter.addAction(AlarmService.ALARM_DISMISS_ACTION); registerReceiver(mReceiver, filter); mReceiverRegistered = true; } } @Override public void onDestroy() { super.onDestroy(); protected void onPause() { super.onPause(); unbindAlarmService(); // Skip if register didn't happen to avoid IllegalArgumentException if (mReceiverRegistered) { unregisterReceiver(mReceiver); mReceiverRegistered = false; } } Loading Loading @@ -362,6 +395,9 @@ public class AlarmActivity extends AppCompatActivity } } /** * Perform snooze animation and send snooze intent. */ private void snooze() { mAlarmHandled = true; LogUtils.v(LOGTAG, "Snoozed: %s", mAlarmInstance); Loading @@ -377,18 +413,40 @@ public class AlarmActivity extends AppCompatActivity getAlertAnimator(mSnoozeButton, R.string.alarm_alert_snoozed_text, infoText, accessibilityText, alertColor, alertColor).start(); AlarmStateManager.setSnoozeState(this, mAlarmInstance, false /* showToast */); // Unbind here, otherwise alarm will keep ringing until activity finishes. unbindAlarmService(); } /** * Perform dismiss animation and send dismiss intent. */ private void dismiss() { mAlarmHandled = true; LogUtils.v(LOGTAG, "Dismissed: %s", mAlarmInstance); setAnimatedFractions(0.0f /* snoozeFraction */, 1.0f /* dismissFraction */); getAlertAnimator(mDismissButton, R.string.alarm_alert_off_text, null /* infoText */, getString(R.string.alarm_alert_off_text) /* accessibilityText */, Color.WHITE, mCurrentHourColor).start(); AlarmStateManager.setDismissState(this, mAlarmInstance); // Unbind here, otherwise alarm will keep ringing until activity finishes. unbindAlarmService(); } /** * Unbind AlarmService if bound. */ private void unbindAlarmService() { if (mServiceBound) { unbindService(mConnection); mServiceBound = false; } } private void setAnimatedFractions(float snoozeFraction, float dismissFraction) { Loading
src/com/android/deskclock/alarms/AlarmService.java +92 −15 Original line number Diff line number Diff line Loading @@ -16,9 +16,12 @@ package com.android.deskclock.alarms; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Binder; import android.os.IBinder; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; Loading @@ -28,22 +31,59 @@ import com.android.deskclock.LogUtils; import com.android.deskclock.provider.AlarmInstance; /** * This service is in charge of starting/stoping the alarm. It will bring up and manage the * This service is in charge of starting/stopping the alarm. It will bring up and manage the * {@link AlarmActivity} as well as {@link AlarmKlaxon}. * * Registers a broadcast receiver to listen for snooze/dismiss intents. The broadcast receiver * exits early if AlarmActivity is bound to prevent double-processing of the snooze/dismiss intents. */ public class AlarmService extends Service { // A public action send by AlarmService when the alarm has started. /** * AlarmActivity and AlarmService (when unbound0 listen for this broadcast intent * so that other applications can snooze the alarm (after ALARM_ALERT_ACTION and before * ALARM_DONE_ACTION). */ public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE"; /** * AlarmActivity and AlarmService listen for this broadcast intent so that other * applications can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION). */ public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS"; /** A public action sent by AlarmService when the alarm has started. */ public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT"; // A public action sent by AlarmService when the alarm has stopped for any reason. /** A public action sent by AlarmService when the alarm has stopped for any reason. */ public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE"; // Private action used to start an alarm with this service. /** Private action used to start an alarm with this service. */ public static final String START_ALARM_ACTION = "START_ALARM"; // Private action used to stop an alarm with this service. /** Private action used to stop an alarm with this service. */ public static final String STOP_ALARM_ACTION = "STOP_ALARM"; /** Binder given to AlarmActivity */ private final IBinder mBinder = new Binder(); /** Whether the service is currently bound to AlarmActivity */ private boolean mIsBound = false; /** Whether the receiver is currently registered */ private boolean mIsRegistered = false; @Override public IBinder onBind(Intent intent) { mIsBound = true; return mBinder; } @Override public boolean onUnbind(Intent intent) { mIsBound = false; return super.onUnbind(intent); } /** * Utility method to help start alarm properly. If alarm is already firing, it * will mark it as missed and start the new one. Loading Loading @@ -116,44 +156,81 @@ public class AlarmService extends Service { return; } LogUtils.v("AlarmService.stop with instance: " + mCurrentAlarm.mId); LogUtils.v("AlarmService.stop with instance: %s", (Object) mCurrentAlarm.mId); AlarmKlaxon.stop(this); mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); sendBroadcast(new Intent(ALARM_DONE_ACTION)); mCurrentAlarm = null; AlarmAlertWakeLock.releaseCpuLock(); } private final BroadcastReceiver mActionsReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); LogUtils.i("AlarmService received intent %s", action); if (mCurrentAlarm == null || mCurrentAlarm.mAlarmState != AlarmInstance.FIRED_STATE) { LogUtils.i("No valid firing alarm"); return; } if (mIsBound) { LogUtils.i("AlarmActivity bound; AlarmService no-op"); return; } switch (action) { case ALARM_SNOOZE_ACTION: // Set the alarm state to snoozed. // If this broadcast receiver is handling the snooze intent then AlarmActivity // must not be showing, so always show snooze toast. AlarmStateManager.setSnoozeState(context, mCurrentAlarm, true /* showToast */); break; case ALARM_DISMISS_ACTION: // Set the alarm state to dismissed. AlarmStateManager.setDismissState(context, mCurrentAlarm); break; default: break; } } }; @Override public void onCreate() { super.onCreate(); mTelephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); // Register the broadcast receiver final IntentFilter filter = new IntentFilter(ALARM_SNOOZE_ACTION); filter.addAction(ALARM_DISMISS_ACTION); registerReceiver(mActionsReceiver, filter); mIsRegistered = true; } @Override public int onStartCommand(Intent intent, int flags, int startId) { LogUtils.v("AlarmService.onStartCommand() with intent: " + intent.toString()); LogUtils.v("AlarmService.onStartCommand() with intent: %s", intent.toString()); long instanceId = AlarmInstance.getId(intent.getData()); if (START_ALARM_ACTION.equals(intent.getAction())) { ContentResolver cr = this.getContentResolver(); AlarmInstance instance = AlarmInstance.getInstance(cr, instanceId); if (instance == null) { LogUtils.e("No instance found to start alarm: " + instanceId); LogUtils.e("No instance found to start alarm: %d", instanceId); if (mCurrentAlarm != null) { // Only release lock if we are not firing alarm AlarmAlertWakeLock.releaseCpuLock(); } return Service.START_NOT_STICKY; } else if (mCurrentAlarm != null && mCurrentAlarm.mId == instanceId) { LogUtils.e("Alarm already started for instance: " + instanceId); LogUtils.e("Alarm already started for instance: %d", instanceId); return Service.START_NOT_STICKY; } startAlarm(instance); } else if(STOP_ALARM_ACTION.equals(intent.getAction())) { if (mCurrentAlarm != null && mCurrentAlarm.mId != instanceId) { LogUtils.e("Can't stop alarm for instance: " + instanceId + " because current alarm is: " + mCurrentAlarm.mId); LogUtils.e("Can't stop alarm for instance: %d because current alarm is: %d", instanceId, mCurrentAlarm.mId); return Service.START_NOT_STICKY; } stopSelf(); Loading @@ -167,10 +244,10 @@ public class AlarmService extends Service { LogUtils.v("AlarmService.onDestroy() called"); super.onDestroy(); stopCurrentAlarm(); } @Override public IBinder onBind(Intent intent) { return null; if (mIsRegistered) { unregisterReceiver(mActionsReceiver); mIsRegistered = false; } } }