1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-27 12:26:48 +01:00

Cleanup main Activity, move debug code to DebugActivity, added Call related debug buttons, add state tracking to PhoneCallReceiver, support in-call display of incoming calls (untested)

This commit is contained in:
Andreas Shimokawa 2015-02-07 12:58:18 +01:00
parent d80e98d26a
commit a47ba3c96f
11 changed files with 355 additions and 128 deletions

View File

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="nodomain.freeyourgadget.gadgetbridge">
package="nodomain.freeyourgadget.gadgetbridge" >
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="com.fsck.k9.permission.READ_MESSAGES" />
@ -12,15 +13,16 @@
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme" >
<activity
android:name=".SettingsActivity"
android:label="@string/app_name"></activity>
android:label="@string/app_name" />
<activity
android:name=".ControlCenter"
android:label="@string/title_activity_main">
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
@ -28,29 +30,46 @@
<service
android:name=".NotificationListener"
android:label="@string/app_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" >
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service android:name=".BluetoothCommunicationService" >
</service>
<receiver android:name=".PhoneCallReceiver" android:enabled="false">
<receiver
android:name=".PhoneCallReceiver"
android:enabled="false" >
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE"/>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter>
</receiver>
<receiver android:name=".SMSReceiver" android:enabled="false">
<receiver
android:name=".SMSReceiver"
android:enabled="false" >
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver android:name=".K9Receiver" android:enabled="false">
<receiver
android:name=".K9Receiver"
android:enabled="false" >
<intent-filter>
<data android:scheme="email" />
<action android:name="com.fsck.k9.intent.action.EMAIL_RECEIVED" />
</intent-filter>
</receiver>
<receiver android:name=".StopServiceReceiver"/>
<receiver android:name=".StopServiceReceiver" />
<activity
android:name=".DebugActivity"
android:label="@string/title_activity_debug" >
</activity>
</application>
</manifest>

View File

@ -42,8 +42,8 @@ public class BluetoothCommunicationService extends Service {
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.notification_sms";
public static final String ACTION_NOTIFICATION_EMAIL
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.notification_email";
public static final String ACTION_INCOMINGCALL
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.incomingcall";
public static final String ACTION_CALLSTATE
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.callstate";
public static final String ACTION_SETTIME
= "nodomain.freeyourgadget.gadgetbride.bluetoothcommunicationservice.action.settime";
@ -58,6 +58,7 @@ public class BluetoothCommunicationService extends Service {
PhoneCallReceiver.class,
SMSReceiver.class,
K9Receiver.class,
NotificationListener.class,
};
int newState;
@ -180,11 +181,17 @@ public class BluetoothCommunicationService extends Service {
String body = intent.getStringExtra("notification_body");
byte[] msg = PebbleProtocol.encodeEmail(sender, subject, body);
mBtSocketIoThread.write(msg);
} else if (intent.getAction().equals(ACTION_INCOMINGCALL)) {
String phoneNumber = intent.getStringExtra("incomingcall_phonenumber");
byte phoneState = intent.getByteExtra("incomingcall_state", (byte) 0);
String callerName = getContactDisplayNameByNumber(phoneNumber);
byte[] msg = PebbleProtocol.encodeIncomingCall(phoneNumber, callerName, phoneState);
} else if (intent.getAction().equals(ACTION_CALLSTATE)) {
byte phoneState = intent.getByteExtra("call_state", (byte) 0);
String phoneNumber = null;
if (intent.hasExtra("call_phonenumber")) {
phoneNumber = intent.getStringExtra("call_phonenumber");
}
String callerName = null;
if (phoneNumber != null) {
callerName = getContactDisplayNameByNumber(phoneNumber);
}
byte[] msg = PebbleProtocol.encodePhoneState(phoneNumber, callerName, phoneState);
mBtSocketIoThread.write(msg);
} else if (intent.getAction().equals(ACTION_SETTIME)) {
byte[] msg = PebbleProtocol.encodeSetTime(-1);

View File

@ -14,19 +14,13 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class ControlCenter extends ActionBarActivity {
public static final String ACTION_QUIT
= "nodomain.freeyourgadget.gadgetbride.controlcenter.action.quit";
Button sendButton;
Button testNotificationButton;
Button startServiceButton;
Button setTimeButton;
EditText editTitle;
EditText editContent;
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
@ -44,8 +38,6 @@ public class ControlCenter extends ActionBarActivity {
registerReceiver(mReceiver, new IntentFilter(ACTION_QUIT));
editTitle = (EditText) findViewById(R.id.editTitle);
editContent = (EditText) findViewById(R.id.editContent);
startServiceButton = (Button) findViewById(R.id.startServiceButton);
startServiceButton.setOnClickListener(new View.OnClickListener() {
@Override
@ -55,35 +47,6 @@ public class ControlCenter extends ActionBarActivity {
startService(startIntent);
}
});
sendButton = (Button) findViewById(R.id.sendButton);
sendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(ControlCenter.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_NOTIFICATION_GENERIC);
startIntent.putExtra("notification_title", editTitle.getText().toString());
startIntent.putExtra("notification_body", editContent.getText().toString());
startService(startIntent);
}
});
setTimeButton = (Button) findViewById(R.id.setTimeButton);
setTimeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(ControlCenter.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_SETTIME);
startService(startIntent);
}
});
testNotificationButton = (Button) findViewById(R.id.testNotificationButton);
testNotificationButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
testNotification();
}
});
/*
* Ask for permission to intercept notifications on first run.
* TODO: allow re-request in preferences
@ -131,9 +94,15 @@ public class ControlCenter extends ActionBarActivity {
//startActivity(intent);
return true;
}
if (id == R.id.action_debug) {
Intent intent = new Intent(this, DebugActivity.class);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onDestroy() {
super.onDestroy();

View File

@ -0,0 +1,147 @@
package nodomain.freeyourgadget.gadgetbridge;
import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
public class DebugActivity extends ActionBarActivity {
Button sendSMSButton;
Button sendEmailButton;
Button incomingCallButton;
Button outgoingCallButton;
Button startCallButton;
Button endCallButton;
Button testNotificationButton;
Button setTimeButton;
EditText editContent;
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ControlCenter.ACTION_QUIT)) {
finish();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_debug);
registerReceiver(mReceiver, new IntentFilter(ControlCenter.ACTION_QUIT));
editContent = (EditText) findViewById(R.id.editContent);
sendSMSButton = (Button) findViewById(R.id.sendSMSButton);
sendSMSButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_NOTIFICATION_GENERIC);
startIntent.putExtra("notification_title", "Gadgetbridge");
startIntent.putExtra("notification_body", editContent.getText().toString());
startService(startIntent);
}
});
sendEmailButton = (Button) findViewById(R.id.sendEmailButton);
sendEmailButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_NOTIFICATION_EMAIL);
startIntent.putExtra("notification_sender", "Gadgetbridge");
startIntent.putExtra("notification_subject", "Test");
startIntent.putExtra("notification_body", editContent.getText().toString());
startService(startIntent);
}
});
incomingCallButton = (Button) findViewById(R.id.incomingCallButton);
incomingCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_CALLSTATE);
startIntent.putExtra("call_phonenumber", editContent.getText().toString());
startIntent.putExtra("call_state", PebbleProtocol.PHONECONTROL_INCOMINGCALL);
startService(startIntent);
}
});
outgoingCallButton = (Button) findViewById(R.id.outgoingCallButton);
outgoingCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_CALLSTATE);
startIntent.putExtra("call_phonenumber", editContent.getText().toString());
startIntent.putExtra("call_state", PebbleProtocol.PHONECONTROL_OUTGOINGCALL);
startService(startIntent);
}
});
startCallButton = (Button) findViewById(R.id.startCallButton);
startCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_CALLSTATE);
startIntent.putExtra("call_state", PebbleProtocol.PHONECONTROL_START);
startService(startIntent);
}
});
endCallButton = (Button) findViewById(R.id.endCallButton);
endCallButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_CALLSTATE);
startIntent.putExtra("call_state", PebbleProtocol.PHONECONTROL_END);
startService(startIntent);
}
});
setTimeButton = (Button) findViewById(R.id.setTimeButton);
setTimeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent startIntent = new Intent(DebugActivity.this, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_SETTIME);
startService(startIntent);
}
});
testNotificationButton = (Button) findViewById(R.id.testNotificationButton);
testNotificationButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
testNotification();
}
});
}
private void testNotification() {
NotificationManager nManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
NotificationCompat.Builder ncomp = new NotificationCompat.Builder(this);
ncomp.setContentTitle("Test Notification");
ncomp.setContentText("This is a Test Notification from Gadgetbridge");
ncomp.setTicker("This is a Test Notification from Gadgetbridge");
ncomp.setSmallIcon(R.drawable.ic_launcher);
ncomp.setAutoCancel(true);
nManager.notify((int) System.currentTimeMillis(), ncomp.build());
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
}
}

View File

@ -133,7 +133,7 @@ public class PebbleProtocol {
return buf.array();
}
public static byte[] encodeIncomingCall(String number, String name, byte state) {
public static byte[] encodePhoneState(String number, String name, byte state) {
String cookie = "000"; // That's a dirty trick to make the cookie part 4 bytes long :P
String[] parts = {cookie, number, name};
return encodeMessage(ENDPOINT_PHONECONTROL, state, parts);

View File

@ -5,26 +5,61 @@ import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
public class PhoneCallReceiver extends BroadcastReceiver {
private static int mLastState = TelephonyManager.CALL_STATE_IDLE;
private static String mSavedNumber;
@Override
public void onReceive(Context context, Intent intent) {
String phoneState = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
byte state = 0;
if (phoneState.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
state = PebbleProtocol.PHONECONTROL_INCOMINGCALL;
} else if (phoneState.equals(TelephonyManager.EXTRA_STATE_IDLE) || phoneState.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {
state = PebbleProtocol.PHONECONTROL_END;
}
if (intent.getAction().equals("android.intent.action.NEW_OUTGOING_CALL")) {
mSavedNumber = intent.getExtras().getString("android.intent.extra.PHONE_NUMBER");
} else {
String stateStr = intent.getExtras().getString(TelephonyManager.EXTRA_STATE);
String number = intent.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
int state = 0;
if (stateStr.equals(TelephonyManager.EXTRA_STATE_IDLE)) {
state = TelephonyManager.CALL_STATE_IDLE;
} else if (stateStr.equals(TelephonyManager.EXTRA_STATE_OFFHOOK)) {
state = TelephonyManager.CALL_STATE_OFFHOOK;
} else if (stateStr.equals(TelephonyManager.EXTRA_STATE_RINGING)) {
state = TelephonyManager.CALL_STATE_RINGING;
}
if (state != 0) {
String phoneNumber = intent.hasExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) ? intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) : "";
Intent startIntent = new Intent(context, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_INCOMINGCALL);
startIntent.putExtra("incomingcall_phonenumber", phoneNumber);
startIntent.putExtra("incomingcall_state", state);
context.startService(startIntent);
onCallStateChanged(context, state, number);
}
}
public void onCallStateChanged(Context context, int state, String number) {
if (mLastState == state) {
return;
}
byte pebblePhoneCommand = -1; // TODO: dont assume Pebble here
switch (state) {
case TelephonyManager.CALL_STATE_RINGING:
mSavedNumber = number;
pebblePhoneCommand = PebbleProtocol.PHONECONTROL_INCOMINGCALL;
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
if (mLastState == TelephonyManager.CALL_STATE_RINGING) {
pebblePhoneCommand = PebbleProtocol.PHONECONTROL_START;
} else {
pebblePhoneCommand = PebbleProtocol.PHONECONTROL_OUTGOINGCALL;
}
break;
case TelephonyManager.CALL_STATE_IDLE:
pebblePhoneCommand = PebbleProtocol.PHONECONTROL_END;
break;
}
if (pebblePhoneCommand != -1) {
Intent startIntent = new Intent(context, BluetoothCommunicationService.class);
startIntent.setAction(BluetoothCommunicationService.ACTION_CALLSTATE);
startIntent.putExtra("call_phonenumber", mSavedNumber);
startIntent.putExtra("call_state", pebblePhoneCommand);
context.startService(startIntent);
}
mLastState = state;
}
}

View File

@ -6,67 +6,13 @@
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.ControlCenter">
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="From:" />
<EditText
android:id="@+id/editTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/label"
android:singleLine="true"
android:inputType="textEmailAddress" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_below="@+id/editTitle"
android:text="Message"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:ems="10"
android:id="@+id/editContent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignRight="@+id/editTitle"
android:layout_alignEnd="@+id/editTitle"
android:layout_below="@+id/textView" />
<Button
android:id="@+id/sendButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="send to Pebble"
android:layout_below="@+id/editContent"
android:layout_alignParentStart="true" />
<Button
android:id="@+id/testNotificationButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="create test notification"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="connect"
android:id="@+id/startServiceButton"
android:layout_above="@+id/setTimeButton"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:layout_alignEnd="@+id/setTimeButton" />
android:layout_alignParentEnd="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="set time"
android:id="@+id/setTimeButton"
android:layout_above="@+id/testNotificationButton"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true" />
</RelativeLayout>

View File

@ -0,0 +1,94 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.ControlCenter">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Message / Caller"/>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:ems="10"
android:id="@+id/editContent"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/textView"
android:text="Test" />
<Button
android:id="@+id/sendSMSButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="send as SMS"
android:layout_below="@+id/editContent"
android:layout_alignParentStart="true"
android:layout_alignEnd="@+id/incomingCallButton" />
<Button
android:id="@+id/testNotificationButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="create test notification"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="set time"
android:id="@+id/setTimeButton"
android:layout_above="@+id/testNotificationButton"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="incoming call"
android:id="@+id/incomingCallButton"
android:layout_below="@+id/sendSMSButton"
android:layout_alignParentStart="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="outgoing call"
android:id="@+id/outgoingCallButton"
android:layout_alignTop="@+id/incomingCallButton"
android:layout_alignEnd="@+id/setTimeButton"
android:layout_toEndOf="@+id/incomingCallButton" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="send as E-Mail"
android:id="@+id/sendEmailButton"
android:layout_below="@+id/editContent"
android:layout_alignStart="@+id/outgoingCallButton"
android:layout_alignParentEnd="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="start call"
android:id="@+id/startCallButton"
android:layout_below="@+id/incomingCallButton"
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/outgoingCallButton" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="end call"
android:id="@+id/endCallButton"
android:layout_below="@+id/outgoingCallButton"
android:layout_alignEnd="@+id/outgoingCallButton"
android:layout_toEndOf="@+id/incomingCallButton" />
</RelativeLayout>

View File

@ -0,0 +1,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="nodomain.freeyourgadget.gadgetbridge.DebugActivity">
<item android:id="@+id/action_settings" android:title="@string/action_settings"
android:orderInCategory="100" app:showAsAction="never" />
</menu>

View File

@ -4,4 +4,6 @@
tools:context="nodomain.freeyourgadget.gadgetbridge.ControlCenter">
<item android:id="@+id/action_settings" android:title="@string/action_settings"
android:orderInCategory="100" app:showAsAction="never" />
<item android:id="@+id/action_debug" android:title="@string/action_debug"
android:orderInCategory="100" app:showAsAction="never" />
</menu>

View File

@ -3,7 +3,8 @@
<string name="app_name">Gadgetbridge</string>
<string name="title_activity_main">Gadgetbridge Control Center</string>
<string name="hello_world">Hello world!</string>
<string name="action_settings">Settings</string>
<string name="action_debug">Debug</string>
<string name="title_activity_debug">Debug</string>
</resources>