diff --git a/src/A Multimodal Multi-Party Dataset for Emotion Recognition in Conversations.pdf b/src/A Multimodal Multi-Party Dataset for Emotion Recognition in Conversations.pdf
deleted file mode 100644
index 289a99b..0000000
Binary files a/src/A Multimodal Multi-Party Dataset for Emotion Recognition in Conversations.pdf and /dev/null differ
diff --git a/src/AndroidApp/.idea/AndroidApp.iml b/src/AndroidApp/.idea/AndroidApp.iml
new file mode 100644
index 0000000..d6ebd48
--- /dev/null
+++ b/src/AndroidApp/.idea/AndroidApp.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/.idea/misc.xml b/src/AndroidApp/.idea/misc.xml
new file mode 100644
index 0000000..82efee1
--- /dev/null
+++ b/src/AndroidApp/.idea/misc.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+ 1.8
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/.idea/modules.xml b/src/AndroidApp/.idea/modules.xml
new file mode 100644
index 0000000..3e28a09
--- /dev/null
+++ b/src/AndroidApp/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/.idea/workspace.xml b/src/AndroidApp/.idea/workspace.xml
new file mode 100644
index 0000000..c2cd714
--- /dev/null
+++ b/src/AndroidApp/.idea/workspace.xml
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1524884716802
+
+
+ 1524884716802
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/.gitignore b/src/AndroidApp/Tbot/.gitignore
new file mode 100644
index 0000000..39fb081
--- /dev/null
+++ b/src/AndroidApp/Tbot/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/src/AndroidApp/Tbot/.idea/compiler.xml b/src/AndroidApp/Tbot/.idea/compiler.xml
new file mode 100644
index 0000000..96cc43e
--- /dev/null
+++ b/src/AndroidApp/Tbot/.idea/compiler.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/.idea/copyright/profiles_settings.xml b/src/AndroidApp/Tbot/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..e7bedf3
--- /dev/null
+++ b/src/AndroidApp/Tbot/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/.idea/gradle.xml b/src/AndroidApp/Tbot/.idea/gradle.xml
new file mode 100644
index 0000000..3bd0088
--- /dev/null
+++ b/src/AndroidApp/Tbot/.idea/gradle.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/.idea/misc.xml b/src/AndroidApp/Tbot/.idea/misc.xml
new file mode 100644
index 0000000..4c9d020
--- /dev/null
+++ b/src/AndroidApp/Tbot/.idea/misc.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/.idea/modules.xml b/src/AndroidApp/Tbot/.idea/modules.xml
new file mode 100644
index 0000000..1f18dad
--- /dev/null
+++ b/src/AndroidApp/Tbot/.idea/modules.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/.idea/runConfigurations.xml b/src/AndroidApp/Tbot/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /dev/null
+++ b/src/AndroidApp/Tbot/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/app/.gitignore b/src/AndroidApp/Tbot/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/src/AndroidApp/Tbot/app/build.gradle b/src/AndroidApp/Tbot/app/build.gradle
new file mode 100644
index 0000000..2351f11
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/build.gradle
@@ -0,0 +1,33 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 26
+ buildToolsVersion "26.0.2"
+ defaultConfig {
+ applicationId "me.wshuo.tbot"
+ minSdkVersion 18
+ targetSdkVersion 26
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ vectorDrawables.useSupportLibrary = true
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ compile 'com.android.support:appcompat-v7:26.1.0'
+ compile 'com.android.support:design:26.1.0'
+ compile 'com.android.support:support-v4:26.1.0'
+ compile 'com.android.support:support-vector-drawable:26.1.0'
+ testCompile 'junit:junit:4.12'
+}
diff --git a/src/AndroidApp/Tbot/app/proguard-rules.pro b/src/AndroidApp/Tbot/app/proguard-rules.pro
new file mode 100644
index 0000000..5094c45
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:\programs\Android\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/src/AndroidApp/Tbot/app/src/androidTest/java/me/wshuo/tbot/ExampleInstrumentedTest.java b/src/AndroidApp/Tbot/app/src/androidTest/java/me/wshuo/tbot/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..6f31994
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/androidTest/java/me/wshuo/tbot/ExampleInstrumentedTest.java
@@ -0,0 +1,27 @@
+package me.wshuo.tbot;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext(){
+ throw new MyOwnRuntimeException("My Message"); //write by ljw
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("me.wshuo.tbot", appContext.getPackageName());
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/AndroidManifest.xml b/src/AndroidApp/Tbot/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d490081
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/AppCompatPreferenceActivity.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/AppCompatPreferenceActivity.java
new file mode 100644
index 0000000..719acd1
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/AppCompatPreferenceActivity.java
@@ -0,0 +1,109 @@
+package me.wshuo.tbot;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
+ * to be used with AppCompat.
+ */
+public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
+
+ private AppCompatDelegate mDelegate;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ getDelegate().installViewFactory();
+ getDelegate().onCreate(savedInstanceState);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ getDelegate().onPostCreate(savedInstanceState);
+ }
+
+ public ActionBar getSupportActionBar() {
+ return getDelegate().getSupportActionBar();
+ }
+
+ public void setSupportActionBar(@Nullable Toolbar toolbar) {
+ getDelegate().setSupportActionBar(toolbar);
+ }
+
+ @Override
+ public MenuInflater getMenuInflater() {
+ return getDelegate().getMenuInflater();
+ }
+
+ @Override
+ public void setContentView(@LayoutRes int layoutResID) {
+ getDelegate().setContentView(layoutResID);
+ }
+
+ @Override
+ public void setContentView(View view) {
+ getDelegate().setContentView(view);
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().setContentView(view, params);
+ }
+
+ @Override
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().addContentView(view, params);
+ }
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ getDelegate().onPostResume();
+ }
+
+ @Override
+ protected void onTitleChanged(CharSequence title, int color) {
+ super.onTitleChanged(title, color);
+ getDelegate().setTitle(title);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ getDelegate().onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getDelegate().onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ getDelegate().onDestroy();
+ }
+
+ public void invalidateOptionsMenu() {
+ getDelegate().invalidateOptionsMenu();
+ }
+
+ private AppCompatDelegate getDelegate() {
+ if (mDelegate == null) {
+ mDelegate = AppCompatDelegate.create(this, null);
+ }
+ return mDelegate;
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/AudioCall.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/AudioCall.java
new file mode 100644
index 0000000..b6ef982
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/AudioCall.java
@@ -0,0 +1,36 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+public class AudioCall extends AppCompatActivity {
+
+ public Button btnAudioReturn;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_audio_call);
+ btnAudioReturn = (Button) findViewById(R.id.btnAudioReturn);
+ btnAudioReturn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent_Audio_to_Menu = new Intent(AudioCall.this, Menu.class);
+ startActivity(intent_Audio_to_Menu);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onBackPressed() {//当点击后退键时执行该函数
+
+ super.onBackPressed();
+ Intent intent_Audio_to_Menu = new Intent(AudioCall.this, Menu.class);
+ startActivity(intent_Audio_to_Menu);
+ finish();
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Falldown.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Falldown.java
new file mode 100644
index 0000000..a6ec384
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Falldown.java
@@ -0,0 +1,35 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+public class Falldown extends AppCompatActivity {
+
+ public Button btnFallReturn;
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_falldown);
+ btnFallReturn = (Button) findViewById(R.id.btnFallReturn);
+ btnFallReturn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent_Fall_to_Menu = new Intent(Falldown.this, Menu.class);
+ startActivity(intent_Fall_to_Menu);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onBackPressed() {//当点击后退键时执行该函数
+
+ super.onBackPressed();
+ Intent intent_Fall_to_Menu = new Intent(Falldown.this, Menu.class);
+ startActivity(intent_Fall_to_Menu);
+ finish();
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/IPConnect.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/IPConnect.java
new file mode 100644
index 0000000..dfbd4c4
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/IPConnect.java
@@ -0,0 +1,36 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+public class IPConnect extends AppCompatActivity {
+
+ public Button btnIPConnect;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_ipconnect);
+ btnIPConnect = (Button) findViewById(R.id.btnIPConnect);
+ btnIPConnect.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent_IP_to_Menu = new Intent(IPConnect.this, Menu.class);
+ startActivity(intent_IP_to_Menu);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onBackPressed() {//当点击后退键时执行该函数
+
+ super.onBackPressed();
+ Intent intent_IP_to_Menu = new Intent(IPConnect.this, Menu.class);
+ startActivity(intent_IP_to_Menu);
+ finish();
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Login.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Login.java
new file mode 100644
index 0000000..bc1b73f
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Login.java
@@ -0,0 +1,185 @@
+package me.wshuo.tbot;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class Login extends Activity { //登录界面活动
+
+ public int pwdresetFlag=0;
+ private EditText mAccount; //用户名编辑
+ private EditText mPwd; //密码编辑
+ private Button mRegisterButton; //注册按钮
+ private Button mLoginButton; //登录按钮
+ private Button mCancleButton; //注销按钮
+ private CheckBox mRememberCheck;
+
+ private SharedPreferences login_sp;
+ private String userNameValue,passwordValue;
+
+ private View loginView; //登录
+ private View loginSuccessView;
+ private TextView loginSuccessShow;
+ private TextView mChangepwdText;
+ private UserDataManager mUserDataManager; //用户数据管理类
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.login);
+ //通过id找到相应的控件
+ mAccount = (EditText) findViewById(R.id.login_edit_account);
+ mPwd = (EditText) findViewById(R.id.login_edit_pwd);
+ mRegisterButton = (Button) findViewById(R.id.login_btn_register);
+ mLoginButton = (Button) findViewById(R.id.login_btn_login);
+ mCancleButton = (Button) findViewById(R.id.login_btn_cancle);
+ loginView=findViewById(R.id.login_view);
+ loginSuccessView=findViewById(R.id.login_success_view);
+ loginSuccessShow=(TextView) findViewById(R.id.login_success_show);
+
+ mChangepwdText = (TextView) findViewById(R.id.login_text_change_pwd);
+
+ mRememberCheck = (CheckBox) findViewById(R.id.Login_Remember);
+
+ login_sp = getSharedPreferences("userInfo", 0);
+ //String name=login_sp.getString("USER_NAME", "");
+ String pwd = getEncryptedPass();//write by ljw
+ String pwd =login_sp.getString("PASSWORD", "");
+
+ boolean choseRemember =login_sp.getBoolean("mRememberCheck", false);
+ boolean choseAutoLogin =login_sp.getBoolean("mAutologinCheck", false);
+ //如果上次选了记住密码,那进入登录页面也自动勾选记住密码,并填上用户名和密码
+ if(choseRemember){
+ mAccount.setText(name);
+ mPwd.setText(pwd);
+ mRememberCheck.setChecked(true);
+ }
+
+ mRegisterButton.setOnClickListener(mListener); //采用OnClickListener方法设置不同按钮按下之后的监听事件
+ mLoginButton.setOnClickListener(mListener);
+ mCancleButton.setOnClickListener(mListener);
+ mChangepwdText.setOnClickListener(mListener);
+
+ ImageView image = (ImageView) findViewById(R.id.logo); //使用ImageView显示logo
+ image.setImageResource(R.drawable.logo);
+
+ if (mUserDataManager == null) {
+ mUserDataManager = new UserDataManager(this);
+ mUserDataManager.openDataBase(); //建立本地数据库
+ }
+ }
+ OnClickListener mListener = new OnClickListener() { //不同按钮按下的监听事件选择
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.login_btn_register: //登录界面的注册按钮
+ Intent intent_Login_to_Register = new Intent(Login.this,Register.class) ; //切换Login Activity至User Activity
+ startActivity(intent_Login_to_Register);
+ finish();
+ break;
+ case R.id.login_btn_login: //登录界面的登录按钮
+ login();
+ break;
+ case R.id.login_btn_cancle: //登录界面的注销按钮
+ cancel();
+ break;
+ case R.id.login_text_change_pwd: //登录界面的密码重置按钮
+ Intent intent_Login_to_reset = new Intent(Login.this,Resetpwd.class) ; //切换Login Activity至User Activity
+ startActivity(intent_Login_to_reset);
+ finish();
+ break;
+ }
+ }
+ };
+
+ public void login() {//登录按钮监听事件
+ if (isUserNameAndPwdValid()) {
+ String userName = mAccount.getText().toString().trim();//获取当前输入的用户名和密码信息
+ String userPwd = mPwd.getText().toString().trim();
+ SharedPreferences.Editor editor =login_sp.edit();
+ int result=mUserDataManager.findUserByNameAndPwd(userName, userPwd);
+ if(result==1){//返回1说明用户名和密码均正确
+ //保存用户名和密码
+ editor.putString("USER_NAME", userName);
+ editor.putString("PASSWORD", userPwd);
+ //是否记住密码
+ if(mRememberCheck.isChecked()){
+ editor.putBoolean("mRememberCheck", true);
+ }else{
+ editor.putBoolean("mRememberCheck", false);
+ }
+ editor.apply();
+ Intent intent = new Intent(Login.this,Welcome.class) ;//切换Login Activity至User Activity
+ startActivity(intent);
+ finish();
+ Toast.makeText(this, getString(R.string.login_success),Toast.LENGTH_SHORT).show();//登录成功提示
+ }else if(result==0){
+ Toast.makeText(this, getString(R.string.login_fail),Toast.LENGTH_SHORT).show(); //登录失败提示
+ }
+ }
+ }
+ public void cancel() { //注销
+ if (isUserNameAndPwdValid()) {
+ String userName = mAccount.getText().toString().trim(); //获取当前输入的用户名和密码信息
+ String userPwd = mPwd.getText().toString().trim();
+ int result=mUserDataManager.findUserByNameAndPwd(userName, userPwd);
+ if(result==1){ //返回1说明用户名和密码均正确
+// Intent intent = new Intent(Login.this,User.class) ; //切换Login Activity至User Activity
+// startActivity(intent);
+ Toast.makeText(this, getString(R.string.cancel_success),Toast.LENGTH_SHORT).show();//登录成功提示
+ mPwd.setText("");
+ mAccount.setText("");
+ mUserDataManager.deleteUserDatabyname(userName);
+ }else if(result==0){
+ Toast.makeText(this, getString(R.string.cancel_fail),Toast.LENGTH_SHORT).show(); //登录失败提示
+ }
+ }
+
+ }
+
+ public boolean isUserNameAndPwdValid() {
+ if (mAccount.getText().toString().trim().equals("")) {
+ Toast.makeText(this, getString(R.string.account_empty),
+ Toast.LENGTH_SHORT).show();
+ return false;
+ } else if (mPwd.getText().toString().trim().equals("")) {
+ Toast.makeText(this, getString(R.string.pwd_empty),
+ Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onResume() {
+ if (mUserDataManager == null) {
+ mUserDataManager = new UserDataManager(this);
+ mUserDataManager.openDataBase();
+ }
+ super.onResume();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onPause() {
+ if (mUserDataManager != null) {
+ mUserDataManager.closeDataBase();
+ mUserDataManager = null;
+ }
+ super.onPause();
+ }
+
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/MedicineAlert.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/MedicineAlert.java
new file mode 100644
index 0000000..2153510
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/MedicineAlert.java
@@ -0,0 +1,34 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+public class MedicineAlert extends AppCompatActivity {
+
+ public Button btnAlertReturn;
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_medicine_alert);
+ btnAlertReturn = (Button) findViewById(R.id.btnAlertReturn);
+ btnAlertReturn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent_Alert_to_Menu = new Intent(MedicineAlert.this, Menu.class);
+ startActivity(intent_Alert_to_Menu);
+ finish();
+ }
+ });
+ }
+ @Override
+ public void onBackPressed() {//当点击后退键时执行该函数
+
+ super.onBackPressed();
+ Intent intent_Alert_to_Menu = new Intent(MedicineAlert.this, Menu.class);
+ startActivity(intent_Alert_to_Menu);
+ finish();
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Menu.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Menu.java
new file mode 100644
index 0000000..06f1b90
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Menu.java
@@ -0,0 +1,107 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+public class Menu extends AppCompatActivity {
+
+ //write by ljw 没必要改
+ public Button btnMenuReturn;
+ public Button getBtnMenuReturn(){return btnMenuReturn;}
+ public void setBtnMenuReturn(Button btnMenuReturn){this.btnMenuReturn=btnMenuReturn;}
+
+ public Button btnFall;
+ public Button getBtnFall(){return btnFall;}
+ public void setBtnFall(Button btnFall){this.btnFall=btnFall;}
+
+ public Button btnAlert;
+ public Button getBtnAlert(){return btnAlert;}
+ public void setBtnAlert(Button btnAlert){this.btnAlert=btnAlert;}
+
+ public Button btnAudio;
+ public Button getBtnAudio(){return btnAudio;}
+ public void setBtnAudio(Button btnAudio){this.btnAudio=btnAudio;}
+
+ public Button btnVideo;
+ public Button getBtnVideo(){return btnVideo;}
+ public void setBtnVideo(Button btnVideo){this.btnVideo=btnVideo;}
+
+ public Button btnSetting;
+ public Button getBtnSetting(){return btnSetting;}
+ public void setBtnSetting(Button btnSetting){this.btnSetting=btnSetting;}
+
+ public Button btnControl;
+ public Button getBtnControl(){return btnControl;}
+ public void setBtnControl(Button btnControl){this.btnControl=btnControl;}
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_menu);
+ btnMenuReturn = (Button) findViewById(R.id.btnMenuReturn);
+ btnFall = (Button)findViewById(R.id.btnFall);
+ btnAlert = (Button)findViewById(R.id.btnAlert);
+ btnAudio = (Button)findViewById(R.id.btnAudio);
+ btnVideo = (Button)findViewById(R.id.btnVideo);
+ btnSetting = (Button)findViewById(R.id.btnSetting);
+ btnControl = (Button)findViewById(R.id.btnControl);
+
+ btnMenuReturn.setOnClickListener(mListener);
+ btnFall.setOnClickListener(mListener);
+ btnAlert.setOnClickListener(mListener);
+ btnAudio.setOnClickListener(mListener);
+ btnVideo.setOnClickListener(mListener);
+ btnSetting.setOnClickListener(mListener);
+ btnControl.setOnClickListener(mListener);
+ }
+
+ View.OnClickListener mListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.btnMenuReturn:
+ Intent intent_Return = new Intent(Menu.this, IPConnect.class);
+ startActivity(intent_Return);
+ finish();
+ break;
+ case R.id.btnFall:
+ Intent intent_Falldown = new Intent(Menu.this, Falldown.class);
+ startActivity(intent_Falldown);
+ finish();
+ break;
+ case R.id.btnAlert:
+ Intent intent_Alert = new Intent(Menu.this, MedicineAlert.class);
+ startActivity(intent_Alert);
+ finish();
+ break;
+ case R.id.btnAudio:
+ Intent intent_Audio = new Intent(Menu.this, AudioCall.class);
+ startActivity(intent_Audio);
+ finish();
+ break;
+ case R.id.btnVideo:
+ Intent intent_Video = new Intent(Menu.this, VideoObserve.class);
+ startActivity(intent_Video);
+ finish();
+ break;
+ case R.id.btnSetting:
+ Intent intent_Setting = new Intent(Menu.this, Settings.class);
+ startActivity(intent_Setting);
+ finish();
+ break;
+ case R.id.btnControl:
+ Intent intent_Control = new Intent(Menu.this, MotionControl.class);
+ startActivity(intent_Control);
+ finish();
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ };
+
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/MotionControl.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/MotionControl.java
new file mode 100644
index 0000000..9a7e8e5
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/MotionControl.java
@@ -0,0 +1,34 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+public class MotionControl extends AppCompatActivity {
+
+ public Button btnMotionReturn;
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_motion_control);
+ btnMotionReturn = (Button) findViewById(R.id.btnMotionReturn);
+ btnMotionReturn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent_Motion_to_Menu = new Intent(MotionControl.this, Menu.class);
+ startActivity(intent_Motion_to_Menu);
+ finish();
+ }
+ });
+ }
+ @Override
+ public void onBackPressed() {//当点击后退键时执行该函数
+
+ super.onBackPressed();
+ Intent intent_Motion_to_Menu = new Intent(MotionControl.this, Menu.class);
+ startActivity(intent_Motion_to_Menu);
+ finish();
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/PreferenceSettings.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/PreferenceSettings.java
new file mode 100644
index 0000000..7a3d378
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/PreferenceSettings.java
@@ -0,0 +1,13 @@
+package me.wshuo.tbot;
+
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+
+public class PreferenceSettings extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_preference_settings);
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Register.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Register.java
new file mode 100644
index 0000000..105e9a5
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Register.java
@@ -0,0 +1,102 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+public class Register extends AppCompatActivity {
+ private EditText mAccount; //用户名编辑
+ private EditText mPwd; //密码编辑
+ private EditText mPwdCheck; //密码编辑
+ private Button mSureButton; //确定按钮
+ private Button mCancelButton; //取消按钮
+ private UserDataManager mUserDataManager; //用户数据管理类
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.register);
+ mAccount = (EditText) findViewById(R.id.resetpwd_edit_name);
+ mPwd = (EditText) findViewById(R.id.resetpwd_edit_pwd_old);
+ mPwdCheck = (EditText) findViewById(R.id.resetpwd_edit_pwd_new);
+
+ mSureButton = (Button) findViewById(R.id.register_btn_sure);
+ mCancelButton = (Button) findViewById(R.id.register_btn_cancel);
+
+ mSureButton.setOnClickListener(m_register_Listener); //注册界面两个按钮的监听事件
+ mCancelButton.setOnClickListener(m_register_Listener);
+
+ if (mUserDataManager == null) {
+ mUserDataManager = new UserDataManager(this);
+ mUserDataManager.openDataBase(); //建立本地数据库
+ }
+
+ }
+ View.OnClickListener m_register_Listener = new View.OnClickListener() { //不同按钮按下的监听事件选择
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.register_btn_sure: //确认按钮的监听事件
+ register_check();
+ break;
+ case R.id.register_btn_cancel: //取消按钮的监听事件,由注册界面返回登录界面
+ Intent intent_Register_to_Login = new Intent(Register.this,Login.class) ; //切换User Activity至Login Activity
+ startActivity(intent_Register_to_Login);
+ finish();
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ public void register_check() { //确认按钮的监听事件
+ if (isUserNameAndPwdValid()) {
+ String userName = mAccount.getText().toString().trim();
+ String userPwd = mPwd.getText().toString().trim();
+ String userPwdCheck = mPwdCheck.getText().toString().trim();
+ //检查用户是否存在
+ int count=mUserDataManager.findUserByName(userName);
+ //用户已经存在时返回,给出提示文字
+ if(count>0){
+ Toast.makeText(this, getString(R.string.name_already_exist, userName),Toast.LENGTH_SHORT).show();
+ return ;
+ }
+ if(userPwd.equals(userPwdCheck)==false){ //两次密码输入不一样
+ Toast.makeText(this, getString(R.string.pwd_not_the_same),Toast.LENGTH_SHORT).show();
+ return ;
+ } else {
+ UserData mUser = new UserData(userName, userPwd);
+ mUserDataManager.openDataBase();
+ long flag = mUserDataManager.insertUserData(mUser); //新建用户信息
+ if (flag == -1) {
+ Toast.makeText(this, getString(R.string.register_fail),Toast.LENGTH_SHORT).show();
+ }else{
+ Toast.makeText(this, getString(R.string.register_success),Toast.LENGTH_SHORT).show();
+ Intent intent_Register_to_Login = new Intent(Register.this,Login.class) ; //切换User Activity至Login Activity
+ startActivity(intent_Register_to_Login);
+ finish();
+ }
+ }
+ }
+ }
+ public boolean isUserNameAndPwdValid() {
+ //write by ljw
+ String myString = null;
+ if (mAccount.getText().toString().trim().equals(myString)) {
+ Toast.makeText(this, getString(R.string.account_empty),
+ Toast.LENGTH_SHORT).show();
+ return false;
+ } else if (mPwd.getText().toString().trim().equals(myString)) {
+ Toast.makeText(this, getString(R.string.pwd_empty),
+ Toast.LENGTH_SHORT).show();
+ return false;
+ }else if(mPwdCheck.getText().toString().trim().equals(myString)) {
+ Toast.makeText(this, getString(R.string.pwd_check_empty),
+ Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Resetpwd.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Resetpwd.java
new file mode 100644
index 0000000..230ff33
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Resetpwd.java
@@ -0,0 +1,122 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+public class Resetpwd extends AppCompatActivity {
+ private EditText mAccount; //用户名编辑
+ private EditText mPwd_old; //密码编辑
+ private EditText mPwd_new; //密码编辑
+ private EditText mPwdCheck; //密码编辑
+ private Button mSureButton; //确定按钮
+ private Button mCancelButton; //取消按钮
+ private UserDataManager mUserDataManager; //用户数据管理类
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.resetpwd);
+// layout.setOrientation(RelativeLayout.VERTICAL).
+ mAccount = (EditText) findViewById(R.id.resetpwd_edit_name);
+ mPwd_old = (EditText) findViewById(R.id.resetpwd_edit_pwd_old);
+ mPwd_new = (EditText) findViewById(R.id.resetpwd_edit_pwd_new);
+ mPwdCheck = (EditText) findViewById(R.id.resetpwd_edit_pwd_check);
+
+ mSureButton = (Button) findViewById(R.id.resetpwd_btn_sure);
+ mCancelButton = (Button) findViewById(R.id.resetpwd_btn_cancel);
+
+ mSureButton.setOnClickListener(m_resetpwd_Listener); //注册界面两个按钮的监听事件
+ mCancelButton.setOnClickListener(m_resetpwd_Listener);
+ //mCancelButton.setOnClickListener(m_resetpwd_Listener);
+
+ if (mUserDataManager == null) {
+ mUserDataManager = new UserDataManager(this);
+ mUserDataManager.openDataBase(); //建立本地数据库
+ }
+
+ }
+ View.OnClickListener m_resetpwd_Listener = new View.OnClickListener() { //不同按钮按下的监听事件选择
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.resetpwd_btn_sure: //确认按钮的监听事件
+ resetpwd_check();
+ break;
+ case R.id.resetpwd_btn_cancel: //取消按钮的监听事件,由注册界面返回登录界面
+ Intent intent_Resetpwd_to_Login = new Intent(Resetpwd.this,Login.class) ; //切换Resetpwd Activity至Login Activity
+ startActivity(intent_Resetpwd_to_Login);
+ finish();
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ public void resetpwd_check() { //确认按钮的监听事件
+ if (isUserNameAndPwdValid()) {
+ String userName = mAccount.getText().toString().trim();
+ String userPwd_old = mPwd_old.getText().toString().trim();
+ String userPwd_new = mPwd_new.getText().toString().trim();
+ String userPwdCheck = mPwdCheck.getText().toString().trim();
+ int result=mUserDataManager.findUserByNameAndPwd(userName, userPwd_old);
+ if(result==1){ //返回1说明用户名和密码均正确,继续后续操作
+ if(userPwd_new.equals(userPwdCheck)==false){ //两次密码输入不一样
+ Toast.makeText(this, getString(R.string.pwd_not_the_same),Toast.LENGTH_SHORT).show();
+ return ;
+ } else {
+ UserData mUser = new UserData(userName, userPwd_new);
+ mUserDataManager.openDataBase();
+ boolean flag = mUserDataManager.updateUserData(mUser);
+ if (flag == false) {
+ Toast.makeText(this, getString(R.string.resetpwd_fail),Toast.LENGTH_SHORT).show();
+ }else{
+
+ Toast.makeText(this, getString(R.string.resetpwd_success),Toast.LENGTH_SHORT).show();
+
+ mUser.pwdresetFlag=1;
+ Intent intent_Register_to_Login = new Intent(Resetpwd.this,Login.class) ; //切换User Activity至Login Activity
+ startActivity(intent_Register_to_Login);
+ finish();
+ }
+ }
+ }else if(result==0){ //返回0说明用户名和密码不匹配,重新输入
+ Toast.makeText(this, getString(R.string.pwd_not_fit_user),Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+
+
+
+ }
+ }
+ public boolean isUserNameAndPwdValid() {
+ String myString = null;
+ String userName = mAccount.getText().toString().trim();
+ //检查用户是否存在
+ int count=mUserDataManager.findUserByName(userName);
+ //用户不存在时返回,给出提示文字
+ if(count<=0){
+ Toast.makeText(this, getString(R.string.name_not_exist, userName),Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ if (mAccount.getText().toString().trim().equals(myString)) {
+ Toast.makeText(this, getString(R.string.account_empty),Toast.LENGTH_SHORT).show();
+ return false;
+ } else if (mPwd_old.getText().toString().trim().equals(myString)) {
+ Toast.makeText(this, getString(R.string.pwd_empty),Toast.LENGTH_SHORT).show();
+ return false;
+ } else if (mPwd_new.getText().toString().trim().equals(myString)) {
+ Toast.makeText(this, getString(R.string.pwd_new_empty),Toast.LENGTH_SHORT).show();
+ return false;
+ }else if(mPwdCheck.getText().toString().trim().equals(myString)) {
+ Toast.makeText(this, getString(R.string.pwd_check_empty),Toast.LENGTH_SHORT).show();
+ return false;
+ }
+ return true;
+ }
+
+}
+
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Settings.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Settings.java
new file mode 100644
index 0000000..a525905
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Settings.java
@@ -0,0 +1,283 @@
+package me.wshuo.tbot;
+
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.support.v7.app.ActionBar;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.preference.RingtonePreference;
+import android.text.TextUtils;
+import android.view.MenuItem;
+
+import java.util.List;
+
+/**
+ * A {@link PreferenceActivity} that presents a set of application settings. On
+ * handset devices, settings are presented as a single list. On tablets,
+ * settings are split by category, with category headers shown to the left of
+ * the list of settings.
+ *
+ * See
+ * Android Design: Settings for design guidelines and the Settings
+ * API Guide for more information on developing a Settings UI.
+ */
+public class Settings extends AppCompatPreferenceActivity {
+ /**
+ * A preference value change listener that updates the preference's summary
+ * to reflect its new value.
+ */
+ private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object value) {
+ String stringValue = value.toString();
+
+ if (preference instanceof ListPreference) {
+ // For list preferences, look up the correct display value in
+ // the preference's 'entries' list.
+ ListPreference listPreference = (ListPreference) preference;
+ int index = listPreference.findIndexOfValue(stringValue);
+
+ // Set the summary to reflect the new value.
+ preference.setSummary(
+ index >= 0
+ ? listPreference.getEntries()[index]
+ : null);
+
+ } else if (preference instanceof RingtonePreference) {
+ // For ringtone preferences, look up the correct display value
+ // using RingtoneManager.
+ if (TextUtils.isEmpty(stringValue)) {
+ // Empty values correspond to 'silent' (no ringtone).
+ preference.setSummary(R.string.pref_ringtone_silent);
+
+ } else {
+ Ringtone ringtone = RingtoneManager.getRingtone(
+ preference.getContext(), Uri.parse(stringValue));
+
+ if (ringtone == null) {
+ // Clear the summary if there was a lookup error.
+ preference.setSummary(null);
+ } else {
+ // Set the summary to reflect the new ringtone display
+ // name.
+ String name = ringtone.getTitle(preference.getContext());
+ preference.setSummary(name);
+ }
+ }
+
+ } else {
+ // For all other preferences, set the summary to the value's
+ // simple string representation.
+ preference.setSummary(stringValue);
+ }
+ return true;
+ }
+ };
+
+ /**
+ * Helper method to determine if the device has an extra-large screen. For
+ * example, 10" tablets are extra-large.
+ */
+ private static boolean isXLargeTablet(Context context) {
+ return (context.getResources().getConfiguration().screenLayout
+ & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
+ }
+
+ /**
+ * Binds a preference's summary to its value. More specifically, when the
+ * preference's value is changed, its summary (line of text below the
+ * preference title) is updated to reflect the value. The summary is also
+ * immediately updated upon calling this method. The exact display format is
+ * dependent on the type of preference.
+ *
+ * @see #sBindPreferenceSummaryToValueListener
+ */
+ private static void bindPreferenceSummaryToValue(Preference preference) {
+ // Set the listener to watch for value changes.
+ preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
+
+ // Trigger the listener immediately with the preference's
+ // current value.
+ sBindPreferenceSummaryToValueListener.onPreferenceChange(preference,
+ PreferenceManager
+ .getDefaultSharedPreferences(preference.getContext())
+ .getString(preference.getKey(), ""));
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setupActionBar();
+ }
+
+ /**
+ * Set up the {@link android.app.ActionBar}, if the API is available.
+ */
+ private void setupActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ // Show the Up button in the action bar.
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onIsMultiPane() {
+ return isXLargeTablet(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public void onBuildHeaders(List target) {
+ loadHeadersFromResource(R.xml.pref_headers, target);
+ }
+
+ /**
+ * This method stops fragment injection in malicious applications.
+ * Make sure to deny any unknown fragments here.
+ */
+ protected boolean isValidFragment(String fragmentName) {
+ //write by ljw
+ if (PreferenceFragment.equals(fragmentName.class().getName()))
+ return true;
+ if (GeneralPreferenceFragment.equals(fragmentName.class().getName()))
+ return true;
+ if (DataSyncPreferenceFragment.equals(fragmentName.class().getName()))
+ return true;
+ if (NotificationPreferenceFragment.equals(fragmentName.class().getName()))
+ return true;
+ /*
+ return PreferenceFragment.class.getName().equals(fragmentName)
+ || GeneralPreferenceFragment.class.getName().equals(fragmentName)
+ || DataSyncPreferenceFragment.class.getName().equals(fragmentName)
+ || NotificationPreferenceFragment.class.getName().equals(fragmentName);
+ */
+ }
+/* if (PreferenceFragment.equals(fragmentName.class().getName()))
+ return true;
+ if (GeneralPreferenceFragment.equals(fragmentName.class().getName()))
+ return true;
+ if (DataSyncPreferenceFragment.equals(fragmentName.class().getName()))
+ return true;
+ if (NotificationPreferenceFragment.equals(fragmentName.class().getName()))
+ return true;
+ write by ljw
+*/
+ /**
+ * This fragment shows general preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class GeneralPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_general);
+ setHasOptionsMenu(true);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+ bindPreferenceSummaryToValue(findPreference("example_text"));
+ bindPreferenceSummaryToValue(findPreference("example_list"));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ startActivity(new Intent(getActivity(), Settings.class));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * This fragment shows notification preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class NotificationPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_notification);
+ setHasOptionsMenu(true);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+ bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ startActivity(new Intent(getActivity(), Settings.class));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * This fragment shows data and sync preferences only. It is used when the
+ * activity is showing a two-pane settings UI.
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ public static class DataSyncPreferenceFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.pref_data_sync);
+ setHasOptionsMenu(true);
+
+ // Bind the summaries of EditText/List/Dialog/Ringtone preferences
+ // to their values. When their values change, their summaries are
+ // updated to reflect the new value, per the Android Design
+ // guidelines.
+ bindPreferenceSummaryToValue(findPreference("sync_frequency"));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ startActivity(new Intent(getActivity(), Settings.class));
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {//当点击后退键时执行该函数
+
+ super.onBackPressed();
+ Intent intent_Setting_to_Menu = new Intent(Settings.this, Menu.class);
+ startActivity(intent_Setting_to_Menu);
+ finish();
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/User.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/User.java
new file mode 100644
index 0000000..089644f
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/User.java
@@ -0,0 +1,26 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Button;
+
+public class User extends AppCompatActivity {
+ private Button mReturnButton; //没有使用到
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.user);
+ mReturnButton = (Button)findViewById(R.id.returnback);
+
+ }
+ public void back_to_login(View view) {
+ //setContentView(R.layout.login);
+ Intent intent3 = new Intent(User.this,Login.class) ;
+ startActivity(intent3);
+ finish();
+
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/UserData.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/UserData.java
new file mode 100644
index 0000000..b06d047
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/UserData.java
@@ -0,0 +1,49 @@
+package me.wshuo.tbot;
+/**
+ * Created by FoolishFan on 2016/7/14.
+ */
+
+public class UserData {
+ private String userName; //用户名
+ private String userPwd; //用户密码
+ private int userId; //用户ID号
+ public int pwdresetFlag=0;
+ //获取用户名
+ public String getUserName() { //获取用户名
+ return userName;
+ }
+ //设置用户名
+ public void setUserName(String userName) { //输入用户名
+ this.userName = userName;
+ }
+ //获取用户密码
+ public String getUserPwd() { //获取用户密码
+ return userPwd;
+ }
+ //设置用户密码
+ public void setUserPwd(String userPwd) { //输入用户密码
+ this.userPwd = userPwd;
+ }
+ //获取用户id
+ public int getUserId() { //获取用户ID号
+ return userId;
+ }
+ //设置用户id
+ public void setUserId(int userId) { //设置用户ID号
+ this.userId = userId;
+ }
+
+ /* public UserData(String userName, String userPwd, int userId) { //用户信息
+ super();
+ this.userName = userName;
+ this.userPwd = userPwd;
+ this.userId = userId;
+ }*/
+
+ public UserData(String userName, String userPwd) { //这里只采用用户名和密码
+ super();
+ this.userName = userName;
+ this.userPwd = userPwd;
+ }
+
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/UserDataManager.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/UserDataManager.java
new file mode 100644
index 0000000..535a8f5
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/UserDataManager.java
@@ -0,0 +1,157 @@
+package me.wshuo.tbot;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+public class UserDataManager { //用户数据管理类
+ //一些宏定义和声明
+ private static final String TAG = "UserDataManager";
+ private static final String DB_NAME = "user_data";
+ private static final String TABLE_NAME = "users";
+ public static final String ID = "_id";
+ public static final String USER_NAME = "user_name";
+ //public static final String USER_PWD = "user_pwd";
+ public static final String USER_PWD = getEncryptedPass();//write by ljw
+// public static final String SILENT = "silent";
+// public static final String VIBRATE = "vibrate";
+ private static final int DB_VERSION = 2;
+ private Context mContext = null;
+
+ //创建用户book表
+ private static final String DB_CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ + ID + " integer primary key," + USER_NAME + " varchar,"
+ + USER_PWD + " varchar" + ");";
+
+ private SQLiteDatabase mSQLiteDatabase = null;
+ private DataBaseManagementHelper mDatabaseHelper = null;
+
+ //DataBaseManagementHelper继承自SQLiteOpenHelper
+ private static class DataBaseManagementHelper extends SQLiteOpenHelper {
+
+ DataBaseManagementHelper(Context context) {
+ super(context, DB_NAME, null, DB_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Log.i(TAG,"db.getVersion()="+db.getVersion());
+ db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME + ";");
+ db.execSQL(DB_CREATE);
+ Log.i(TAG, "db.execSQL(DB_CREATE)");
+ Log.e(TAG, DB_CREATE);
+ }
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.i(TAG, "DataBaseManagementHelper onUpgrade");
+ onCreate(db);
+ }
+ }
+
+ public UserDataManager(Context context) {
+ mContext = context;
+ Log.i(TAG, "UserDataManager construction!");
+ }
+ //打开数据库
+ public void openDataBase() throws SQLException {
+ mDatabaseHelper = new DataBaseManagementHelper(mContext);
+ mSQLiteDatabase = mDatabaseHelper.getWritableDatabase();
+ }
+ //关闭数据库
+ public void closeDataBase() throws SQLException {
+ mDatabaseHelper.close();
+ }
+ //添加新用户,即注册
+ public long insertUserData(UserData userData) {
+ String userName=userData.getUserName();
+ String userPwd=userData.getUserPwd();
+ ContentValues values = new ContentValues();
+ values.put(USER_NAME, userName);
+ values.put(USER_PWD, userPwd);
+ return mSQLiteDatabase.insert(TABLE_NAME, ID, values);
+ }
+ //更新用户信息,如修改密码
+ public boolean updateUserData(UserData userData) {
+ //int id = userData.getUserId();
+ String userName = userData.getUserName();
+ String userPwd = userData.getUserPwd();
+ ContentValues values = new ContentValues();
+ values.put(USER_NAME, userName);
+ values.put(USER_PWD, userPwd);
+ return mSQLiteDatabase.update(TABLE_NAME, values,null, null) > 0;
+ //return mSQLiteDatabase.update(TABLE_NAME, values, ID + "=" + id, null) > 0;
+ }
+ //
+ public Cursor fetchUserData(int id) throws SQLException {
+ Cursor mCursor = mSQLiteDatabase.query(false, TABLE_NAME, null, ID
+ + "=" + id, null, null, null, null, null);
+ if (mCursor != null) {
+ mCursor.moveToFirst(); //找到了用户数据所在的条目,游标移动到该条数据开始处
+ }
+ return mCursor;
+ }
+ //
+ public Cursor fetchAllUserDatas() {
+ return mSQLiteDatabase.query(TABLE_NAME, null, null, null, null, null,
+ null);
+ }
+ //根据id删除用户
+ public boolean deleteUserData(int id) {
+ return mSQLiteDatabase.delete(TABLE_NAME, ID + "=" + id, null) > 0;
+ }
+ //根据用户名注销
+ public boolean deleteUserDatabyname(String name) {
+ return mSQLiteDatabase.delete(TABLE_NAME, USER_NAME + "=" + name, null) > 0;
+ }
+ //删除所有用户
+ public boolean deleteAllUserDatas() {
+ return mSQLiteDatabase.delete(TABLE_NAME, null, null) > 0;
+ }
+
+ //????
+ public String getStringByColumnName(String columnName, int id) {
+ Cursor mCursor = fetchUserData(id);
+ int columnIndex = mCursor.getColumnIndex(columnName);
+ String columnValue = mCursor.getString(columnIndex);
+ mCursor.close();
+ return columnValue;
+ }
+ //
+
+ public boolean updateUserDataById(String columnName, int id,
+ String columnValue) {
+ ContentValues values = new ContentValues();
+ values.put(columnName, columnValue);
+ return mSQLiteDatabase.update(TABLE_NAME, values, ID + "=" + id, null) > 0;
+ }
+ //根据用户名找用户,可以判断注册时用户名是否已经存在
+ public int findUserByName(String userName){
+ Log.i(TAG,"findUserByName , userName="+userName);
+ int result=0;
+ Cursor mCursor=mSQLiteDatabase.query(TABLE_NAME, null, USER_NAME+"="+userName, null, null, null, null);
+ if(mCursor!=null){
+ result=mCursor.getCount();
+ mCursor.close();
+ Log.i(TAG,"findUserByName , result="+result);
+ }
+ return result;
+ }
+ //根据用户名和密码找用户,用于登录
+ public int findUserByNameAndPwd(String userName,String pwd){
+ Log.i(TAG,"findUserByNameAndPwd");
+ int result=0;
+ Cursor mCursor=mSQLiteDatabase.query(TABLE_NAME, null, USER_NAME+"="+userName+" and "+USER_PWD+"="+pwd,
+ null, null, null, null);
+ if(mCursor!=null){
+ result=mCursor.getCount();
+ mCursor.close();
+ Log.i(TAG,"findUserByNameAndPwd , result="+result);
+ }
+ return result;
+ }
+
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/VideoObserve.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/VideoObserve.java
new file mode 100644
index 0000000..d33bf51
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/VideoObserve.java
@@ -0,0 +1,35 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+public class VideoObserve extends AppCompatActivity {
+
+ public Button btnVideoReturn;
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_video_observe);
+ btnVideoReturn = (Button)findViewById(R.id.btnVideoReturn);
+ btnVideoReturn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent_Video_to_Menu = new Intent(VideoObserve.this, Menu.class);
+ startActivity(intent_Video_to_Menu);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onBackPressed() {//当点击后退键时执行该函数
+
+ super.onBackPressed();
+ Intent intent_Video_to_Menu = new Intent(VideoObserve.this, Menu.class);
+ startActivity(intent_Video_to_Menu);
+ finish();
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Welcome.java b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Welcome.java
new file mode 100644
index 0000000..7d09903
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/java/me/wshuo/tbot/Welcome.java
@@ -0,0 +1,26 @@
+package me.wshuo.tbot;
+
+import android.content.Intent;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+
+public class Welcome extends AppCompatActivity {
+
+ public Button btnWelcome;
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_welcome);
+ btnWelcome = (Button)findViewById(R.id.btnWelcome);
+ btnWelcome.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent_Welcome_to_IPConnect = new Intent(Welcome.this, IPConnect.class);
+ startActivity(intent_Welcome_to_IPConnect);
+ finish();
+ }
+ });
+ }
+}
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/button_style.xml b/src/AndroidApp/Tbot/app/src/main/res/drawable/button_style.xml
new file mode 100644
index 0000000..82c1e3f
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/drawable/button_style.xml
@@ -0,0 +1,40 @@
+
+
+ -
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/ic_info_black_24dp.xml b/src/AndroidApp/Tbot/app/src/main/res/drawable/ic_info_black_24dp.xml
new file mode 100644
index 0000000..34b8202
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/drawable/ic_info_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/src/AndroidApp/Tbot/app/src/main/res/drawable/ic_notifications_black_24dp.xml
new file mode 100644
index 0000000..e3400cf
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/drawable/ic_notifications_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/ic_sync_black_24dp.xml b/src/AndroidApp/Tbot/app/src/main/res/drawable/ic_sync_black_24dp.xml
new file mode 100644
index 0000000..5a283aa
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/drawable/ic_sync_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/ip.png b/src/AndroidApp/Tbot/app/src/main/res/drawable/ip.png
new file mode 100644
index 0000000..1829fe2
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/drawable/ip.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/logo.jpg b/src/AndroidApp/Tbot/app/src/main/res/drawable/logo.jpg
new file mode 100644
index 0000000..49936c4
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/drawable/logo.jpg differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/w1.png b/src/AndroidApp/Tbot/app/src/main/res/drawable/w1.png
new file mode 100644
index 0000000..d48d35b
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/drawable/w1.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/w2.png b/src/AndroidApp/Tbot/app/src/main/res/drawable/w2.png
new file mode 100644
index 0000000..57050e3
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/drawable/w2.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/w3.png b/src/AndroidApp/Tbot/app/src/main/res/drawable/w3.png
new file mode 100644
index 0000000..cdbf764
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/drawable/w3.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/drawable/w4.png b/src/AndroidApp/Tbot/app/src/main/res/drawable/w4.png
new file mode 100644
index 0000000..84b9984
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/drawable/w4.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/activity_audio_call.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_audio_call.xml
new file mode 100644
index 0000000..934f95f
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_audio_call.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/activity_falldown.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_falldown.xml
new file mode 100644
index 0000000..2d193ff
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_falldown.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/activity_ipconnect.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_ipconnect.xml
new file mode 100644
index 0000000..09d94ad
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_ipconnect.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/activity_medicine_alert.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_medicine_alert.xml
new file mode 100644
index 0000000..47d7b60
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_medicine_alert.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/activity_menu.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_menu.xml
new file mode 100644
index 0000000..d84f544
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_menu.xml
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/activity_motion_control.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_motion_control.xml
new file mode 100644
index 0000000..56bf310
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_motion_control.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/activity_preference_settings.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_preference_settings.xml
new file mode 100644
index 0000000..3b6718d
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_preference_settings.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/activity_video_observe.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_video_observe.xml
new file mode 100644
index 0000000..4d5c54c
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_video_observe.xml
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/activity_welcome.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_welcome.xml
new file mode 100644
index 0000000..6f7585c
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/activity_welcome.xml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/login.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/login.xml
new file mode 100644
index 0000000..cd5659f
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/login.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/register.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/register.xml
new file mode 100644
index 0000000..a89c5bb
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/register.xml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/resetpwd.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/resetpwd.xml
new file mode 100644
index 0000000..dd2f9f4
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/resetpwd.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/layout/user.xml b/src/AndroidApp/Tbot/app/src/main/res/layout/user.xml
new file mode 100644
index 0000000..869db9b
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/layout/user.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/mipmap-hdpi/ic_launcher.png b/src/AndroidApp/Tbot/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/mipmap-mdpi/ic_launcher.png b/src/AndroidApp/Tbot/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c133a0c
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/src/AndroidApp/Tbot/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..bfa42f0
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src/AndroidApp/Tbot/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..324e72c
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src/AndroidApp/Tbot/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..aee44e1
Binary files /dev/null and b/src/AndroidApp/Tbot/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/AndroidApp/Tbot/app/src/main/res/values-w820dp/dimens.xml b/src/AndroidApp/Tbot/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/values/colors.xml b/src/AndroidApp/Tbot/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/values/dimens.xml b/src/AndroidApp/Tbot/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 16dp
+ 16dp
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/values/strings.xml b/src/AndroidApp/Tbot/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..fe0805a
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/values/strings.xml
@@ -0,0 +1,103 @@
+
+ 你好
+ Hello World, Date!
+ Tbot
+ 输入帐号
+ 帐号
+ 密码
+ 登 录
+ 注 册
+ 取 消
+ 用户名为空,请重新输入!
+ 密码为空,请重新输入!
+ 密码不正确,请重新输入!
+ 新密码为空,请重新输入!
+ 密码确认为空,请重新输入!
+ 密码确认不正确,请重新输入密码!
+ 注册用户失败,请重新尝试!
+ 注册成功!
+ 密码修改失败,请重新尝试!
+ 密码修改成功!
+ 登陆成功!
+ 注销成功!
+ 登录失败!请输入正确的用户名与密码!
+ 注销失败!请输入正确的用户名与密码!
+ 用户名【%1$s】已存在,请重新输入!
+ 用户名【%1$s】不存在,请重新输入!
+ 用户:%1$s登录,欢迎光临!
+ 欢迎!
+ 空巢老人看护小助手将会帮助您:
+ 设置
+
+
+
+
+ General
+
+ Enable social recommendations
+ Recommendations for people to contact
+ based on your message history
+
+
+ Display name
+ John Smith
+
+ Add friends to messages
+
+ - Always
+ - When possible
+ - Never
+
+
+ - 1
+ - 0
+ - -1
+
+
+
+ Data & sync
+
+ Sync frequency
+
+ - 15 minutes
+ - 30 minutes
+ - 1 hour
+ - 3 hours
+ - 6 hours
+ - Never
+
+
+ - 15
+ - 30
+ - 60
+ - 180
+ - 360
+ - -1
+
+
+
+ - Entry 1
+ - Entry 2
+ - Entry 3
+
+
+
+ - 1
+ - 2
+ - 3
+
+
+
+
+ System sync settings
+
+
+ Notifications
+
+ New message notifications
+
+ Ringtone
+ Silent
+
+ Vibrate
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/values/styles.xml b/src/AndroidApp/Tbot/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/xml/pref_data_sync.xml b/src/AndroidApp/Tbot/app/src/main/res/xml/pref_data_sync.xml
new file mode 100644
index 0000000..6bd9192
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/xml/pref_data_sync.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/xml/pref_general.xml b/src/AndroidApp/Tbot/app/src/main/res/xml/pref_general.xml
new file mode 100644
index 0000000..36569d6
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/xml/pref_general.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/xml/pref_headers.xml b/src/AndroidApp/Tbot/app/src/main/res/xml/pref_headers.xml
new file mode 100644
index 0000000..7237f6c
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/xml/pref_headers.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/main/res/xml/pref_notification.xml b/src/AndroidApp/Tbot/app/src/main/res/xml/pref_notification.xml
new file mode 100644
index 0000000..e5a319e
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/main/res/xml/pref_notification.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/Tbot/app/src/test/java/me/wshuo/tbot/ExampleUnitTest.java b/src/AndroidApp/Tbot/app/src/test/java/me/wshuo/tbot/ExampleUnitTest.java
new file mode 100644
index 0000000..0e6b40e
--- /dev/null
+++ b/src/AndroidApp/Tbot/app/src/test/java/me/wshuo/tbot/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package me.wshuo.tbot;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/src/AndroidApp/Tbot/build.gradle b/src/AndroidApp/Tbot/build.gradle
new file mode 100644
index 0000000..a47fa4b
--- /dev/null
+++ b/src/AndroidApp/Tbot/build.gradle
@@ -0,0 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.0.1'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/src/AndroidApp/Tbot/gradle.properties b/src/AndroidApp/Tbot/gradle.properties
new file mode 100644
index 0000000..aac7c9b
--- /dev/null
+++ b/src/AndroidApp/Tbot/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/src/AndroidApp/Tbot/gradle/wrapper/gradle-wrapper.jar b/src/AndroidApp/Tbot/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/src/AndroidApp/Tbot/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/src/AndroidApp/Tbot/gradle/wrapper/gradle-wrapper.properties b/src/AndroidApp/Tbot/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..fc9b829
--- /dev/null
+++ b/src/AndroidApp/Tbot/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Dec 28 10:00:20 PST 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/src/AndroidApp/Tbot/gradlew b/src/AndroidApp/Tbot/gradlew
new file mode 100644
index 0000000..9d82f78
--- /dev/null
+++ b/src/AndroidApp/Tbot/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/src/AndroidApp/Tbot/gradlew.bat b/src/AndroidApp/Tbot/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/src/AndroidApp/Tbot/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/AndroidApp/Tbot/settings.gradle b/src/AndroidApp/Tbot/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/src/AndroidApp/Tbot/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/src/AndroidApp/android_remocons/.gitignore b/src/AndroidApp/android_remocons/.gitignore
new file mode 100644
index 0000000..e4d43d7
--- /dev/null
+++ b/src/AndroidApp/android_remocons/.gitignore
@@ -0,0 +1,14 @@
+bin
+gen
+libs
+build
+build.xml
+local.properties
+proguard-project.txt
+.gradle
+
+# These are Android Studio files, might be worth including these later.
+.idea
+*.iml
+*.log
+build-log.xml
diff --git a/src/AndroidApp/android_remocons/README.md b/src/AndroidApp/android_remocons/README.md
new file mode 100644
index 0000000..8d95808
--- /dev/null
+++ b/src/AndroidApp/android_remocons/README.md
@@ -0,0 +1,74 @@
+### Documentation
+
+See [rosjava_core](https://github.com/rosjava/rosjava_core) readme.
+
+### Master Branch
+
+The master branch is currently dependant on stable hydro branches of the other rosjava and android
+core repositories (too much movement there to depend on master). Also dependant on the master branch of
+android_apps.
+
+### Rosinstaller
+
+
+* Master: https://raw.github.com/rosjava/rosjava/master/android_apps.rosinstall
+* Hydro: https://raw.github.com/rosjava/rosjava/hydro/android_apps.rosinstall
+=======
+Android Remocons
+================
+
+Remocons for pairing and concert modes as well as some simple apps for testing.
+
+Installation
+============
+
+This is primarily to get the rosjava build tools. If you are using precise, you don't need this step (it will get it from debs).
+
+```
+> yujin_init_workspace -j5 ~/rosjava rosjava
+> cd ~/rosjava
+> yujin_init_build .
+> yujin_make --install-rosdeps
+> yujin_make
+```
+
+This will use the maven repos for the core rosjava and android repos.
+
+```
+> yujin_init_workspace -j5 ~/rocon_rosjava rocon-rosjava
+> cd ~/rocon_rosjava
+> yujin_init_build --underlays=~/rosjava/devel .
+> yujin_make --install-rosdeps
+> yujin_make
+```
+
+Android core (only necessary on indigo):
+
+```
+> yujin_init_workspace -j5 ~/android_core android-core
+> cd ~/android_core
+> yujin_init_build --underlays=~/rosjava/devel .
+> yujin_make
+```
+
+The android interactions::
+
+```
+> yujin_init_workspace -j5 ~/android_interactions
+> cd ~/android_interactions
+> yujin_init_build --underlays="~/android_core/devel;~/rocon_rosjava/devel;~/rosjava/devel" .
+> cd ~/android_interactions/src
+# to compile the rocon android apps as well
+> wstool set rocon_android_apps --git https://github.com/robotics-in-concert/rocon_android_apps.git --version=headless_launcher_update
+> wstool update -j5
+> yujin_make
+> . .bashrc
+```
+
+Launch the android studio, compile the ```rocon_nfc_writer``` and ```headless_launcher```
+
+Other README's
+==============
+
+* rocon_nfc_writer/README.md
+* headless_launcher/README.md
diff --git a/src/AndroidApp/android_remocons/build.gradle b/src/AndroidApp/android_remocons/build.gradle
new file mode 100644
index 0000000..64cc8d0
--- /dev/null
+++ b/src/AndroidApp/android_remocons/build.gradle
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2013 Daniel Stonier
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+task wrapper(type: Wrapper) {
+ gradleVersion = '4.1'
+}
+
+buildscript {
+ apply from: project.file("./buildscript2.gradle")
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.0.1'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+// classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
+ }
+ repositories {
+ google()
+ }
+
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+subprojects {
+ /*
+ * The android plugin configures a few things:
+ *
+ * - local deployment repository : where it dumps the jars and packaged artifacts)
+ * - local maven repositories : where it finds your locally installed/built artifacts)
+ * - external maven repositories : where it goes looking if it can't find dependencies locally
+ * - android build tools version : which version we use across the board
+ *
+ * To modify, or add repos to the default external maven repositories list, pull request against this code:
+ *
+ * https://github.com/rosjava/rosjava_bootstrap/blob/indigo/gradle_plugins/src/main/groovy/org/ros/gradle_plugins/RosPlugin.groovy#L31
+ *
+ * To modify the build tools version, pull request against this code:
+ *
+ * https://github.com/rosjava/rosjava_bootstrap/blob/indigo/gradle_plugins/src/main/groovy/org/ros/gradle_plugins/RosAndroid.groovy#L14
+ */
+ apply plugin: 'ros-android'
+
+ afterEvaluate { project ->
+ android {
+ // Exclude a few files that are duplicated across our dependencies and
+ // prevent packaging Android applications.
+ packagingOptions {
+ exclude "META-INF/LICENSE.txt"
+ exclude "META-INF/NOTICE.txt"
+ }
+ }
+
+ }
+}
+
+defaultTasks 'assembleRelease', 'uploadArchives'
+
+
+repositories {
+ google()
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/buildscript.gradle b/src/AndroidApp/android_remocons/buildscript.gradle
new file mode 100644
index 0000000..9931938
--- /dev/null
+++ b/src/AndroidApp/android_remocons/buildscript.gradle
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+rootProject.buildscript {
+ apply from: project.file("./buildscript2.gradle")
+ dependencies {
+ classpath "com.android.tools.build:gradle:3.0.1"
+ }
+}
diff --git a/src/AndroidApp/android_remocons/buildscript2.gradle b/src/AndroidApp/android_remocons/buildscript2.gradle
new file mode 100644
index 0000000..53f41ef
--- /dev/null
+++ b/src/AndroidApp/android_remocons/buildscript2.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+rootProject.buildscript {
+ String rosMavenPath = System.getenv("ROS_MAVEN_PATH")
+ String rosMavenRepository = System.getenv("ROS_MAVEN_REPOSITORY")
+ repositories {
+ if (rosMavenPath != null) {
+ rosMavenPath.tokenize(":").each { path ->
+ maven {
+ // We can't use uri() here because we aren't running inside something
+ // that implements the Script interface.
+ url "file:${path}"
+ }
+ }
+ }
+ maven {
+ url "http://repository.springsource.com/maven/bundles/release"
+ }
+ maven {
+ url "http://repository.springsource.com/maven/bundles/external"
+ }
+ if (rosMavenRepository != null) {
+ maven {
+ url rosMavenRepository
+ }
+ }
+ maven {
+ url "https://github.com/rosjava/rosjava_mvn_repo/raw/master"
+ }
+ mavenCentral()
+ }
+ dependencies {
+ classpath "org.ros.rosjava_bootstrap:gradle_plugins:0.3.0"
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/build.gradle b/src/AndroidApp/android_remocons/common_tools/build.gradle
new file mode 100644
index 0000000..32d72b4
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/build.gradle
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2013 Jorge Santos.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+//noinspection GroovyAssignabilityCheck
+dependencies {
+ compile 'org.ros.android_core:android_10:0.2.1'
+ compile 'org.ros.rosjava_core:rosjava:0.2.2'
+ compile 'org.ros.rosjava_bootstrap:message_generation:0.3.0'
+ compile 'org.ros.rosjava_messages:diagnostic_msgs:1.11.9'
+ compile 'org.ros.rosjava_messages:rocon_interaction_msgs:0.7.12'
+ compile 'org.ros.rosjava_messages:rocon_std_msgs:0.7.12'
+ compile 'org.ros.rosjava_messages:rocon_app_manager_msgs:0.7.12'
+ compile 'com.github.rosjava.android_extras:gingerbread:0.2.0'
+ compile 'com.github.rosjava.android_extras:zxing:0.2.0'
+ compile 'com.github.rosjava.zeroconf_jmdns_suite:jmdns:0.2.1'
+ compile 'com.github.robotics_in_concert.rocon_rosjava_core:rosjava_utils:0.2.0'
+ compile 'com.github.robotics_in_concert.rocon_rosjava_core:master_info:0.2.0'
+ compile 'com.github.robotics_in_concert.rocon_rosjava_core:rocon_interactions:0.2.0'
+ compile fileTree(include: 'snakeyaml*.jar', dir: '../external_libraries')
+}
+
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 26
+ defaultConfig {
+ minSdkVersion 18
+ targetSdkVersion 26
+ versionCode 1
+ versionName "1.0.1"
+ }
+ buildToolsVersion '26.0.2'
+}
+
+defaultTasks 'assembleRelease'
diff --git a/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/AppLauncher.java b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/AppLauncher.java
new file mode 100644
index 0000000..8d37d6e
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/AppLauncher.java
@@ -0,0 +1,221 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * Copyright (c) 2013, OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.util.Log;
+import android.util.Patterns;
+
+import com.github.rosjava.android_apps.application_management.AppManager;
+import com.github.rosjava.android_apps.application_management.ConcertDescription;
+import com.github.rosjava.android_apps.application_management.RosAppActivity;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+
+/**
+ * A rewrite of robot_remocon/AppLauncher that...
+ * - works with concerts
+ * - can start web apps
+ * - headless; only reports error codes
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class AppLauncher {
+
+ public enum Result {
+ SUCCESS,
+ NOT_INSTALLED,
+ CANNOT_CONNECT,
+ MALFORMED_URI,
+ CONNECT_TIMEOUT,
+ OTHER_ERROR;
+
+ public String message;
+
+ Result withMsg(String message) {
+ this.message = message;
+ return this;
+ }
+ }
+
+ /**
+ * Launch a client app for the given concert app.
+ */
+ static public Result launch(final Activity parent, final ConcertDescription concert,
+ final concert_msgs.RemoconApp app) {
+
+ Log.i("AppLaunch", "launching concert app " + app.getDisplayName() + " on service " + app.getServiceName());
+
+ // On android apps, app name will be an intent action, while for web apps it will be its URL
+ if (Patterns.WEB_URL.matcher(app.getName()).matches() == true) {
+ return launchWebApp(parent, concert, app);
+ }
+ else {
+ return launchAndroidApp(parent, concert, app);
+ }
+ }
+
+
+ /**
+ * Launch a client android app for the given concert app.
+ */
+ static private Result launchAndroidApp(final Activity parent, final ConcertDescription concert,
+ final concert_msgs.RemoconApp app) {
+
+ // Create the Intent from rapp's name, pass it parameters and remaps and start it
+ String appName = app.getName();
+ Intent intent = new Intent(appName);
+
+ // Copy all app data to "extra" data in the intent.
+ intent.putExtra(AppManager.PACKAGE + "." + RosAppActivity.AppMode.CONCERT + "_app_name", appName);
+ intent.putExtra(ConcertDescription.UNIQUE_KEY, concert);
+ intent.putExtra("RemoconActivity", "com.github.rosjava.android_remocons.concert_remocon.ConcertRemocon"); // TODO must be a RoconConstant!
+ intent.putExtra("Parameters", app.getParameters()); // YAML-formatted string
+
+ // Remappings come as a messages list that make YAML parser crash, so we must digest if for him
+ if ((app.getRemappings() != null) && (app.getRemappings().size() > 0)) {
+ String remaps = "{";
+ for (rocon_std_msgs.Remapping remap: app.getRemappings())
+ remaps += remap.getRemapFrom() + ": " + remap.getRemapTo() + ", ";
+ remaps = remaps.substring(0, remaps.length() - 2) + "}";
+ intent.putExtra("Remappings", remaps);
+ }
+
+// intent.putExtra("runningNodes", runningNodes);
+// intent.putExtra("PairedManagerActivity", "com.github.concertics_in_concert.rocon_android.concert_remocon.ConcertRemocon");
+
+ try {
+ Log.i("AppLaunch", "trying to start activity (action: " + appName + " )");
+ parent.startActivity(intent);
+ return Result.SUCCESS;
+ } catch (ActivityNotFoundException e) {
+ Log.i("AppLaunch", "activity not found for action: " + appName);
+ }
+ return Result.NOT_INSTALLED.withMsg("Android app not installed");
+ }
+
+
+ /**
+ * Launch a client web app for the given concert app.
+ */
+ static private Result launchWebApp(final Activity parent, final ConcertDescription concert,
+ final concert_msgs.RemoconApp app) {
+ try
+ {
+ // Validate the URL before starting anything
+ URL appURL = new URL(app.getName());
+
+ AsyncTask asyncTask = new AsyncTask() {
+ @Override
+ protected String doInBackground(URL... urls) {
+ try {
+ HttpURLConnection urlConnection = (HttpURLConnection)urls[0].openConnection();
+ int responseCode = urlConnection.getResponseCode();
+ urlConnection.disconnect();
+ return urlConnection.getResponseMessage();
+ }
+ catch (IOException e) {
+ return e.getMessage();
+ }
+ }
+ }.execute(appURL);
+ String result = asyncTask.get(5, TimeUnit.SECONDS);
+ if (result == null || (result.startsWith("OK") == false && result.startsWith("ok") == false)) {
+ return Result.CANNOT_CONNECT.withMsg(result);
+ }
+
+ // We pass concert URL, parameters and remaps as URL parameters
+ String appUriStr = app.getName();
+ appUriStr += "?" + "MasterURI=" + concert.getMasterUri();
+ if ((app.getParameters() != null) && (app.getParameters().length() > 0)) {
+ appUriStr += "&" + "params=" + URLEncoder.encode(app.getParameters());
+ }
+
+ // Remappings come as a messages list that make YAML parser crash, so we must digest if for him
+ // TODO Single quotes seem to be necessary, but I didn't confirm yet
+ if ((app.getRemappings() != null) && (app.getRemappings().size() > 0)) {
+ String remaps = "{";
+ for (rocon_std_msgs.Remapping remap: app.getRemappings())
+ remaps += "\'" + remap.getRemapFrom() + "\':\'" + remap.getRemapTo() + "\',";
+ remaps = remaps.substring(0, remaps.length() - 1) + "}";
+ appUriStr += "&" + "remaps=" + URLEncoder.encode(remaps);
+ }
+
+ appURL = new URL(appUriStr);
+ appURL.toURI(); // throws URISyntaxException if fails; probably a redundant check
+ Uri appURI = Uri.parse(appUriStr);
+
+ // Create an action view intent and pass rapp's name + extra information as URI
+ Intent intent = new Intent(Intent.ACTION_VIEW, appURI);
+
+ Log.i("AppLaunch", "trying to start web app (URI: " + appUriStr + ")");
+ parent.startActivity(intent);
+ return Result.SUCCESS;
+ }
+ catch (URISyntaxException e) {
+ return Result.MALFORMED_URI.withMsg("Cannot convert URL into URI. " + e.getMessage());
+ }
+ catch (MalformedURLException e)
+ {
+ return Result.MALFORMED_URI.withMsg("App URL is not valid. " + e.getMessage());
+ }
+ catch (ActivityNotFoundException e) {
+ // This cannot happen for a web site, right? must mean that I have no web browser!
+ return Result.NOT_INSTALLED.withMsg("Activity not found for view action??? muoia???");
+ }
+ catch (TimeoutException e)
+ {
+ return Result.CONNECT_TIMEOUT.withMsg("Timeout waiting for app");
+ }
+ catch (Exception e)
+ {
+ return Result.OTHER_ERROR.withMsg(e.getMessage());
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/AppsManager.java b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/AppsManager.java
new file mode 100644
index 0000000..eba4f65
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/AppsManager.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2013, OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.github.rosjava.android_remocons.common_tools;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.github.rosjava.android_apps.application_management.MasterId;
+
+import org.ros.address.InetAddressFactory;
+import org.ros.android.NodeMainExecutorService;
+import org.ros.exception.RosRuntimeException;
+import org.ros.exception.ServiceNotFoundException;
+import org.ros.internal.node.client.ParameterClient;
+import org.ros.internal.node.server.NodeIdentifier;
+import org.ros.namespace.GraphName;
+import org.ros.node.AbstractNodeMain;
+import org.ros.node.ConnectedNode;
+import org.ros.node.Node;
+import org.ros.node.NodeConfiguration;
+import org.ros.node.service.ServiceClient;
+import org.ros.node.service.ServiceResponseListener;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import rocon_interaction_msgs.GetApp;
+import rocon_interaction_msgs.GetAppRequest;
+import rocon_interaction_msgs.GetAppResponse;
+import rocon_interaction_msgs.GetRolesAndApps;
+import rocon_interaction_msgs.GetRolesAndAppsRequest;
+import rocon_interaction_msgs.GetRolesAndAppsResponse;
+import rocon_interaction_msgs.RequestInteraction;
+import rocon_interaction_msgs.RequestInteractionRequest;
+import rocon_interaction_msgs.RequestInteractionResponse;
+
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.ANDROID_PLATFORM_INFO;
+
+/**
+ * This class has been derived from RobotAppsManager in android_apps/application_management.
+ * The original is quite messy, and this is not much better, so maybe needs extra refactoring.
+ * Also RobotAppsManager claims that it can be executed once, but I modified this to execute
+ * arbitrary times. It looks to work, but mechanism is a bit brittle.
+ * Apps manager implements the services and topics required to interact with the concert roles
+ * manager. Typically to use this class its a three step process:
+ *
+ * 1) instantiate and provide a general failure handler
+ * 2) provide a callback via one of the setupXXXservice methods
+ * 3) call the service you want; there's a public method per service
+ * 4) wait for service response.
+ *
+ * Essentially you are creating a node when creating an instance, and rosjava isolates each
+ * service/topic to each 'node'.
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class AppsManager extends AbstractNodeMain {
+ public interface FailureHandler {
+ /**
+ * Called on failure with a short description of why it failed, like "exception" or "timeout".
+ */
+ void handleFailure(String reason);
+ }
+
+ // unique identifier to key string variables between activities.
+ // TODO I make it compatible current apps; not needed if we rewrite as concert apps
+ public static final String PACKAGE = com.github.rosjava.android_apps.application_management.AppManager.PACKAGE;
+
+ public enum Action {
+ NONE, GET_APPS_FOR_ROLE, GET_APP_INFO, REQUEST_APP_USE
+ };
+
+ private int app_hash;
+ private String role;
+ private Action action = Action.NONE;
+ private rocon_interaction_msgs.RemoconApp app;
+ private ConnectNodeThread connectThread;
+ private ConnectedNode connectedNode;
+ private NodeMainExecutorService nodeMainExecutorService;
+ private FailureHandler failureCallback;
+ private ServiceResponseListener requestServiceResponseListener;
+ private ServiceResponseListener getAppsServiceResponseListener;
+ private ServiceResponseListener appInfoServiceResponseListener;
+
+
+ public AppsManager(FailureHandler failureCallback) {
+ this.failureCallback = failureCallback;
+ }
+
+ public void setupRequestService(ServiceResponseListener serviceResponseListener) {
+ this.requestServiceResponseListener = serviceResponseListener;
+ }
+
+ public void setupGetAppsService(ServiceResponseListener serviceResponseListener) {
+ this.getAppsServiceResponseListener = serviceResponseListener;
+ }
+
+ public void setupAppInfoService(ServiceResponseListener serviceResponseListener) {
+ this.appInfoServiceResponseListener = serviceResponseListener;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ shutdown(); // TODO not warrantied to be called, so user should call shutdown; finalize works at all in Android??? I think no....
+ }
+
+ public void shutdown() {
+ if (nodeMainExecutorService != null)
+ nodeMainExecutorService.shutdownNodeMain(this);
+ else
+ Log.w("AppsMng", "Shutting down an uninitialized apps manager");
+ }
+
+ public void getAppsForRole(final MasterId masterId, final String role) {
+ this.action = Action.GET_APPS_FOR_ROLE;
+ this.role = role;
+
+ // If this is the first action requested, we need a connected node, what must be done in a different thread
+ // The requested action will be executed once we have a connected node (this object itself) in onStart method
+ if (this.connectedNode == null) {
+ Log.d("AppsMng", "First action requested (" + this.action + "). Starting node...");
+ new ConnectNodeThread(masterId).start();
+ }
+ else {
+ // But we don't need all this if we already executed an action, and so have a connected node. Anyway,
+ // we must execute any action asynchronously to avoid the android.os.NetworkOnMainThreadException
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ getAppsForRole();
+ return null;
+ }
+ }.execute(); // TODO: can we use this to incorporate a timeout to service calls?
+ }
+ }
+
+ public void requestAppUse(final MasterId masterId, final String role, final rocon_interaction_msgs.RemoconApp app) {
+ this.action = Action.REQUEST_APP_USE;
+ this.role = role;
+ this.app = app;
+
+ // If this is the first action requested, we need a connected node, what must be done in a different thread
+ // The requested action will be executed once we have a connected node (this object itself) in onStart method
+ if (this.connectedNode == null) {
+ Log.d("AppsMng", "First action requested (" + this.action + "). Starting node...");
+ new ConnectNodeThread(masterId).start();
+ }
+ else {
+ // But we don't need all this if we already executed an action, and so have a connected node. Anyway,
+ // we must execute any action asynchronously to avoid the android.os.NetworkOnMainThreadException
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ requestAppUse();
+ return null;
+ }
+ }.execute();
+ }
+ }
+
+ public void getAppInfo(final MasterId masterId, final int hash) {
+ this.action = Action.GET_APP_INFO;
+ this.app_hash = hash;
+
+ // If this is the first action requested, we need a connected node, what must be done in a different thread
+ // The requested action will be executed once we have a connected node (this object itself) in onStart method
+ if (this.connectedNode == null) {
+ Log.d("AppsMng", "First action requested (" + this.action + "). Starting node...");
+ new ConnectNodeThread(masterId).start();
+ }
+ else {
+ // But we don't need all this if we already executed an action, and so have a connected node. Anyway,
+ // we must execute any action asynchronously to avoid the android.os.NetworkOnMainThreadException
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ getAppInfo();
+ return null;
+ }
+ }.execute(); // TODO: can we use this to incorporate a timeout to service calls?
+ }
+ }
+
+ private void getAppsForRole() {
+ // call get_roles_and_apps concert service
+ ServiceClient srvClient;
+ try {
+ Log.d("AppsMng", "List apps service client created [" + GET_ROLES_AND_APPS_SRV + "]");
+ srvClient = connectedNode.newServiceClient(GET_ROLES_AND_APPS_SRV, GetRolesAndApps._TYPE);
+ } catch (ServiceNotFoundException e) {
+ Log.w("AppsMng", "List apps service not found [" + GET_ROLES_AND_APPS_SRV + "]");
+ throw new RosRuntimeException(e); // TODO we should recover from this calling onFailure on listener
+ }
+ final GetRolesAndAppsRequest request = srvClient.newMessage();
+
+ request.getRoles().add(role);
+ request.setPlatformInfo(ANDROID_PLATFORM_INFO);
+
+ srvClient.call(request, getAppsServiceResponseListener);
+ Log.d("AppsMng", "List apps service call done [" + GET_ROLES_AND_APPS_SRV + "]");
+ }
+
+ private void requestAppUse() {
+ // call request_interaction concert service
+ ServiceClient srvClient;
+ try {
+ Log.d("AppsMng", "Request app service client created [" + REQUEST_INTERACTION_SRV + "]");
+ srvClient = connectedNode.newServiceClient(REQUEST_INTERACTION_SRV, RequestInteraction._TYPE);
+ } catch (ServiceNotFoundException e) {
+ Log.w("AppsMng", "Request app service not found [" + REQUEST_INTERACTION_SRV + "]");
+ throw new RosRuntimeException(e); // TODO we should recover from this calling onFailure on listener
+ }
+ final RequestInteractionRequest request = srvClient.newMessage();
+
+ request.setRole(role);
+ request.setApplication(app.getName());
+ request.setServiceName(app.getServiceName());
+ request.setPlatformInfo(ANDROID_PLATFORM_INFO);
+
+ srvClient.call(request, requestServiceResponseListener);
+ Log.d("AppsMng", "Request app service call done [" + REQUEST_INTERACTION_SRV + "]");
+ }
+
+ private void getAppInfo() {
+ // call get_app concert service
+ ServiceClient srvClient;
+ try {
+ Log.d("AppsMng", "Get app info service client created [" + GET_APP_INFO_SRV + "]");
+ srvClient = connectedNode.newServiceClient(GET_APP_INFO_SRV, GetApp._TYPE);
+ } catch (ServiceNotFoundException e) {
+ Log.w("AppsMng", "Get app info not found [" + GET_APP_INFO_SRV + "]");
+ throw new RosRuntimeException(e); // TODO we should recover from this calling onFailure on listener
+ }
+ final GetAppRequest request = srvClient.newMessage();
+
+ request.setHash(app_hash);
+ request.setPlatformInfo(ANDROID_PLATFORM_INFO);
+
+ srvClient.call(request, appInfoServiceResponseListener);
+ Log.d("AppsMng", "Get app info service call done [" + GET_APP_INFO_SRV + "]");
+ }
+
+ /**
+ * Start a thread to get a node connected with the given masterId. Returns immediately
+ * TODO: what happens if the thread is already running??? kill it first and then start a new one?
+ */
+ private class ConnectNodeThread extends Thread {
+ private MasterId masterId;
+
+ public ConnectNodeThread(MasterId masterId) {
+ this.masterId = masterId;
+ setDaemon(true);
+ setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ failureCallback.handleFailure("exception: " + ex.getMessage());
+ }
+ });
+ }
+
+ @Override
+ public void run() {
+ try {
+ URI concertUri = new URI(masterId.getMasterUri());
+
+ // Check if the concert exists by looking for concert name parameter
+ // getParam throws when it can't find the parameter.
+ ParameterClient paramClient = new ParameterClient(
+ NodeIdentifier.forNameAndUri("/concert_checker", concertUri.toString()), concertUri);
+ String name = (String) paramClient.getParam(GraphName.of(CONCERT_NAME_PARAM)).getResult();
+ Log.i("ConcertRemocon", "Concert " + name + " found; retrieve additional information");
+
+ nodeMainExecutorService = new NodeMainExecutorService();
+ NodeConfiguration nodeConfiguration = NodeConfiguration.newPublic(
+ InetAddressFactory.newNonLoopback().getHostAddress(), concertUri);
+ nodeMainExecutorService.execute(AppsManager.this, nodeConfiguration.setNodeName("apps_manager_node"));
+
+ } catch (URISyntaxException e) {
+ Log.w("AppsMng", "invalid concert URI [" + masterId.getMasterUri() + "][" + e.toString() + "]");
+ failureCallback.handleFailure("invalid concert URI");
+ } catch (RuntimeException e) {
+ // thrown if concert could not be found in the getParam call (from java.net.ConnectException)
+ Log.w("AppsMng", "could not find concert [" + masterId.getMasterUri() + "][" + e.toString() + "]");
+ failureCallback.handleFailure(e.toString());
+ } catch (Throwable e) {
+ Log.w("AppsMng", "exception while creating node in concert checker for URI " + masterId.getMasterUri(), e);
+ failureCallback.handleFailure(e.toString());
+ }
+ }
+ }
+
+ @Override
+ public GraphName getDefaultNodeName() {
+ return null;
+ }
+
+ /**
+ * Node started by NodeMainExecutor.execute(). Execute the requested action.
+ *
+ * @param connectedNode
+ */
+ @Override
+ public void onStart(final ConnectedNode connectedNode) {
+ if (this.connectedNode != null) {
+ Log.e("AppsMng", "App manager re-started before previous shutdown; ignoring...");
+ return;
+ }
+
+ this.connectedNode = connectedNode;
+
+ Log.d("AppsMng", "onStart() - " + action);
+
+ switch (action) {
+ case NONE:
+ Log.w("AppsMng", "Node started without specifying an action");
+ break;
+ case REQUEST_APP_USE:
+ requestAppUse();
+ break;
+ case GET_APPS_FOR_ROLE:
+ getAppsForRole();
+ break;
+ case GET_APP_INFO:
+ getAppInfo();
+ break;
+ default:
+ Log.w("AppsMng", "Unrecogniced action requested: " + action);
+ }
+
+ Log.d("AppsMng", "Done");
+ }
+
+ @Override
+ public void onShutdown(Node node) {
+ Log.d("AppsMng", "Shutdown connected node...");
+ super.onShutdown(node);
+
+ // Required so we get reconnected the next time
+ this.connectedNode = null;
+ Log.d("AppsMng", "Done; shutdown apps manager node main");
+ }
+
+ @Override
+ public void onShutdownComplete(Node node) {
+ super.onShutdownComplete(node);
+ }
+
+ @Override
+ public void onError(Node node, Throwable throwable) {
+ super.onError(node, throwable);
+
+ Log.e("AppsMng", node.getName().toString() + " node error: " + throwable.getMessage());
+ failureCallback.handleFailure(node.getName().toString() + " node error: " + throwable.toString());
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/ConcertChecker.java b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/ConcertChecker.java
new file mode 100644
index 0000000..8a9cb44
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/ConcertChecker.java
@@ -0,0 +1,198 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * Copyright (c) 2013, OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools;
+
+import android.util.Log;
+
+import com.github.rosjava.android_apps.application_management.ConcertDescription;
+import com.github.rosjava.android_apps.application_management.MasterId;
+
+import org.ros.address.InetAddressFactory;
+import org.ros.android.NodeMainExecutorService;
+import org.ros.internal.node.client.ParameterClient;
+import org.ros.internal.node.server.NodeIdentifier;
+import org.ros.namespace.GraphName;
+import org.ros.node.NodeConfiguration;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Date;
+
+import static com.github.rosjava.android_remocons.common_tools.RoconConstants.*;
+
+/**
+ * Threaded ROS-concert checker. Runs a thread which checks for a valid ROS
+ * concert and sends back a {@link ConcertDescription} (with concert name and type)
+ * on success or a failure reason on failure.
+ *
+ * @author hersh@willowgarage.com
+ * @author murase@jsk.imi.i.u-tokyo.ac.jp (Kazuto Murase)
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class ConcertChecker {
+ public interface ConcertDescriptionReceiver {
+ /**
+ * Called on success with a description of the concert that got checked.
+ */
+ void receive(ConcertDescription concertDescription);
+ }
+
+ public interface FailureHandler {
+ /**
+ * Called on failure with a short description of why it failed, like
+ * "exception" or "timeout".
+ */
+ void handleFailure(String reason);
+ }
+
+ private CheckerThread checkerThread;
+ private ConcertDescriptionReceiver foundConcertCallback;
+ private FailureHandler failureCallback;
+
+ /**
+ * Constructor. Should not take any time.
+ */
+ public ConcertChecker(ConcertDescriptionReceiver foundConcertCallback, FailureHandler failureCallback) {
+ this.foundConcertCallback = foundConcertCallback;
+ this.failureCallback = failureCallback;
+ }
+
+ /**
+ * Start the checker thread with the given masterId. If the thread is
+ * already running, kill it first and then start anew. Returns immediately.
+ */
+ public void beginChecking(MasterId masterId) {
+ stopChecking();
+ if (masterId.getMasterUri() == null) {
+ failureCallback.handleFailure("empty concert URI");
+ return;
+ }
+ URI uri;
+ try {
+ uri = new URI(masterId.getMasterUri());
+ } catch (URISyntaxException e) {
+ failureCallback.handleFailure("invalid concert URI");
+ return;
+ }
+ checkerThread = new CheckerThread(masterId, uri);
+ checkerThread.start();
+ }
+
+ /**
+ * Stop the checker thread.
+ */
+ public void stopChecking() {
+ if (checkerThread != null && checkerThread.isAlive()) {
+ checkerThread.interrupt();
+ }
+ }
+
+ private class CheckerThread extends Thread {
+ private URI concertUri;
+ private MasterId masterId;
+
+ public CheckerThread(MasterId masterId, URI concertUri) {
+ this.concertUri = concertUri;
+ this.masterId = masterId;
+ setDaemon(true);
+ // don't require callers to explicitly kill all the old checker threads.
+ setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ failureCallback.handleFailure("exception: " + ex.getMessage());
+ }
+ });
+ }
+
+ @Override
+ public void run() {
+ try {
+ // Check if the concert exists by looking for concert name parameter
+ // getParam throws when it can't find the parameter.
+ ParameterClient paramClient = new ParameterClient(
+ NodeIdentifier.forNameAndUri("/concert_checker", concertUri.toString()), concertUri);
+ String name = (String) paramClient.getParam(GraphName.of(CONCERT_NAME_PARAM)).getResult();
+ Log.i("ConcertRemocon", "Concert " + name + " found; retrieve additional information");
+
+ NodeMainExecutorService nodeMainExecutorService = new NodeMainExecutorService();
+ NodeConfiguration nodeConfiguration = NodeConfiguration.newPublic(
+ InetAddressFactory.newNonLoopback().getHostAddress(), concertUri);
+
+ // Check for the concert information topic
+ ListenerNode readInfoTopic =
+ new ListenerNode(CONCERT_INFO_TOPIC, concert_msgs.ConcertInfo._TYPE);
+ nodeMainExecutorService.execute(readInfoTopic, nodeConfiguration.setNodeName("read_info_node"));
+ readInfoTopic.waitForResponse();
+
+
+ concert_msgs.ConcertInfo concertInfo = readInfoTopic.getLastMessage();
+
+ String concertName = concertInfo.getName();
+ String concertDesc = concertInfo.getDescription();
+ rocon_std_msgs.Icon concertIcon = concertInfo.getIcon();
+
+ if (name.equals(concertName) == false)
+ Log.w("ConcertRemocon", "Concert names from parameter and topic differs; we use the later");
+
+ // Check for the concert roles topic
+ ListenerNode readRolesTopic =
+ new ListenerNode(CONCERT_ROLES_TOPIC, concert_msgs.Roles._TYPE);
+ nodeMainExecutorService.execute(readRolesTopic, nodeConfiguration.setNodeName("concert_roles_node"));
+ readRolesTopic.waitForResponse();
+
+ nodeMainExecutorService.shutdownNodeMain(readInfoTopic);
+ nodeMainExecutorService.shutdownNodeMain(readRolesTopic);
+
+ // configure concert description
+ Date timeLastSeen = new Date();
+ ConcertDescription description = new ConcertDescription(masterId, concertName, concertDesc, concertIcon, timeLastSeen);
+ Log.i("ConcertRemocon", "Concert is available");
+ description.setConnectionStatus(ConcertDescription.OK);
+ description.setUserRoles(readRolesTopic.getLastMessage());
+ foundConcertCallback.receive(description);
+ return;
+ } catch (RuntimeException e) {
+ // thrown if concert could not be found in the getParam call (from java.net.ConnectException)
+ Log.w("ConcertRemocon", "could not find concert [" + concertUri + "][" + e.toString() + "]");
+ failureCallback.handleFailure(e.toString());
+ } catch (Throwable e) {
+ Log.w("ConcertRemocon", "exception while creating node in concert checker for URI " + concertUri, e);
+ failureCallback.handleFailure(e.toString());
+ }
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/ListenerNode.java b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/ListenerNode.java
new file mode 100644
index 0000000..6288494
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/ListenerNode.java
@@ -0,0 +1,87 @@
+package com.github.rosjava.android_remocons.common_tools;
+
+import org.ros.android.MessageCallable;
+import org.ros.exception.RosException;
+import org.ros.exception.RosRuntimeException;
+import org.ros.message.MessageListener;
+import org.ros.namespace.GraphName;
+import org.ros.node.AbstractNodeMain;
+import org.ros.node.ConnectedNode;
+import org.ros.node.topic.Subscriber;
+
+
+/**
+ * Created by jorge on 10/30/13.
+ */
+public class ListenerNode extends AbstractNodeMain
+{
+ private String topicName;
+ private String messageType;
+ private T lastMessage;
+ private MessageCallable callable;
+
+ public ListenerNode(String topic, String type)
+ {
+ topicName = topic;
+ messageType = type;
+ }
+
+ public T getLastMessage()
+ {
+ return lastMessage;
+ }
+
+ public void setTopicName(String topicName)
+ {
+ this.topicName = topicName;
+ }
+
+ public void setMessageType(String messageType)
+ {
+ this.messageType = messageType;
+ }
+
+ public void setMessageToStringCallable(MessageCallable callable)
+ {
+ this.callable = callable;
+ }
+
+ @Override
+ public GraphName getDefaultNodeName() {
+ return GraphName.of("get_" + topicName + "_node");
+ }
+
+ @Override
+ public void onStart(ConnectedNode connectedNode) {
+ Subscriber subscriber = connectedNode.newSubscriber(topicName, messageType);
+ subscriber.addMessageListener(new MessageListener() {
+ @Override
+ public void onNewMessage(final T message) {
+ lastMessage = message;
+ if (callable != null) {
+ callable.call(message);
+ }
+ }
+ });
+ }
+
+ /**
+ * Utility function to block until subscriber receives the first message.
+ *
+ * @throws org.ros.exception.RosException : when it times out waiting for the service.
+ */
+ public void waitForResponse() throws RosException {
+ int count = 0;
+ while ( lastMessage == null ) {
+ try {
+ Thread.sleep(200);
+ } catch (Exception e) {
+ throw new RosRuntimeException(e);
+ }
+ if ( count == 20 ) { // timeout.
+ throw new RosException("timed out waiting for topic messages");
+ }
+ count = count + 1;
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/NfcManager.java b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/NfcManager.java
new file mode 100644
index 0000000..85be3f4
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/graveyard/common_tools/NfcManager.java
@@ -0,0 +1,356 @@
+package com.github.rosjava.android_remocons.common_tools;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentFilter.MalformedMimeTypeException;
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.TagLostException;
+import android.nfc.tech.IsoDep;
+import android.nfc.tech.MifareClassic;
+import android.nfc.tech.MifareUltralight;
+import android.nfc.tech.Ndef;
+import android.nfc.tech.NdefFormatable;
+import android.nfc.tech.NfcA;
+import android.nfc.tech.NfcB;
+import android.nfc.tech.NfcF;
+import android.nfc.tech.NfcV;
+import android.os.Parcelable;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.charset.Charset;
+import java.util.Locale;
+
+
+public class NfcManager {
+
+ private Context mContext = null;
+ private PendingIntent mPendingIntent = null;
+ private IntentFilter[] mFilters;
+ private String[][] mTechList;
+ private Intent mPassedIntent = null;
+ private NfcAdapter mNfcAdapter = null;
+ private String mCurrentNdefString = "";
+
+ public NfcManager(Context context) {
+ mContext = context;
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(mContext);
+
+ Intent targetIntent = new Intent(mContext, mContext.getClass());
+ targetIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ mPendingIntent = PendingIntent.getActivity(mContext, 0, targetIntent, 0);
+
+ IntentFilter filter_1 = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
+ IntentFilter filter_2 = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
+ IntentFilter filter_3 = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
+
+ try {
+ filter_1.addDataType("*/*");
+ filter_2.addDataType("*/*");
+ filter_3.addDataType("*/*");
+ } catch (MalformedMimeTypeException e) {
+ throw new RuntimeException("fail", e);
+ }
+
+ mFilters = new IntentFilter[]{filter_1, filter_2, filter_3};
+ mTechList = new String[][]{new String[]{NfcF.class.getName()},
+ new String[]{MifareClassic.class.getName()},
+ new String[]{NfcA.class.getName()},
+ new String[]{NfcB.class.getName()},
+ new String[]{NfcV.class.getName()},
+ new String[]{Ndef.class.getName()},
+ new String[]{NdefFormatable.class.getName()},
+ new String[]{MifareUltralight.class.getName()},
+ new String[]{IsoDep.class.getName()}};
+ }
+
+ public boolean checkNfcStatus() {
+ return mNfcAdapter.isEnabled();
+ }
+
+ public boolean changeNfcStatus(boolean enable) {
+
+ if (mNfcAdapter == null) return false;
+
+ boolean success = false;
+ Class> nfcManagerClass = null;
+ Method setNfcEnabled = null, setNfcDisabled = null;
+
+ if (enable) {
+ try {
+ nfcManagerClass = Class.forName(mNfcAdapter.getClass().getName());
+ setNfcEnabled = nfcManagerClass.getDeclaredMethod("enable");
+ setNfcEnabled.setAccessible(true);
+ success = (Boolean) setNfcEnabled.invoke(mNfcAdapter);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ System.out.println(e.toString());
+ }
+
+ } else {
+ try {
+ nfcManagerClass = Class.forName(mNfcAdapter.getClass().getName());
+ setNfcDisabled = nfcManagerClass.getDeclaredMethod("disable");
+ setNfcDisabled.setAccessible(true);
+ success = (Boolean) setNfcDisabled.invoke(mNfcAdapter);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return success;
+ }
+
+ public boolean enableForegroundDispatch() {
+ if (mNfcAdapter != null) {
+ mNfcAdapter.enableForegroundDispatch((Activity) mContext, mPendingIntent, mFilters, mTechList);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean disableForegroundDispatch() {
+
+ if (mNfcAdapter != null) {
+ mNfcAdapter.disableForegroundDispatch((Activity) mContext);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean onNewIntent(Intent intent) {
+
+ mPassedIntent = intent;
+ String action = mPassedIntent.getAction();
+
+ Toast.makeText(mContext, action, Toast.LENGTH_SHORT).show();
+
+ if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action) ||
+ NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) ||
+ NfcAdapter.ACTION_NDEF_DISCOVERED.equalsIgnoreCase(action))
+ return true;
+ else
+ return false;
+ }
+
+ public String processTag() {
+
+ if (mPassedIntent == null) return "NFC Tag is not discovered.";
+
+ Parcelable[] rawMsgs = mPassedIntent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+
+ if (rawMsgs == null) {
+ return "NDEF Message is null";
+ }
+
+ mCurrentNdefString = "";
+
+ NdefMessage[] msgs = new NdefMessage[rawMsgs.length];
+
+ for (int i = 0; i < rawMsgs.length; i++) {
+ msgs[i] = (NdefMessage) rawMsgs[i];
+ mCurrentNdefString += ndefMessageToString(msgs[i]);
+ }
+
+ return mCurrentNdefString;
+ }
+
+ public String ndefMessageToString(NdefMessage message) {
+
+ String ndefString = "";
+ NdefRecord[] ndef_records = message.getRecords();
+
+ ndefString += "**Num of NdefRecord : " + ndef_records.length + "\n";
+
+ for (int i = 0; i < ndef_records.length; i++) {
+ String temp = "**Record No. " + i + "\n";
+ byte[] type = ndef_records[i].getType();
+ byte[] id = ndef_records[i].getId();
+ byte[] pl = ndef_records[i].getPayload();
+ byte[] arr = ndef_records[i].toByteArray();
+
+ temp = temp + "- TNF=" + ndef_records[i].getTnf() +
+ "\n - TYPE=" + Util.getHexString(type, type.length) + " " + new String(type) +
+ "\n - ID=" + Util.getHexString(id, id.length) + " " + new String(id) +
+ "\n - PayLoad=" + Util.getHexString(pl, pl.length) + " " + new String(pl) +
+ "\n - ByteArray=" + Util.getHexString(arr, arr.length) + " " + new String(arr) + "\n";
+
+ ndefString += temp;
+ }
+
+ return ndefString;
+ }
+
+ public byte[] getPayload() {
+
+ if (mPassedIntent == null)
+ return null;
+
+ Parcelable[] rawMsgs = mPassedIntent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+
+ if (rawMsgs == null) {
+ return null;
+ }
+
+ NdefMessage[] msgs = new NdefMessage[rawMsgs.length];
+
+ for (int i = 0; i < rawMsgs.length; i++) {
+ msgs[i] = (NdefMessage) rawMsgs[i];
+ }
+
+ NdefRecord[] records = msgs[0].getRecords();
+ if (records.length > 0)
+ return records[0].getPayload();
+
+ return null;
+ }
+
+ private NdefRecord createTextRecord(String text, Locale locale, boolean encodeInUtf8) {
+
+ final byte[] langBytes = locale.getLanguage().getBytes(Charset.forName("US-ASCII"));
+ final Charset utfEncoding = encodeInUtf8 ? Charset.forName("UTF-8") : Charset.forName("UTF-16");
+ final byte[] textBytes = text.getBytes(utfEncoding);
+ final int utfBit = encodeInUtf8 ? 0 : (1 << 7);
+ final char status = (char) (utfBit + langBytes.length);
+ final byte[] data = Util.concat(new byte[]{(byte) status}, langBytes, textBytes);
+
+ return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data);
+ }
+
+ public boolean writeTextNdefMessage(String payload, boolean isAAR) {
+ NdefRecord record = createTextRecord(payload, Locale.KOREAN, true);
+ NdefMessage msg = null;
+ if (isAAR)
+ msg = new NdefMessage(new NdefRecord[]{record, NdefRecord.createApplicationRecord(mContext.getPackageName())});
+ else
+ msg = new NdefMessage(new NdefRecord[]{record});
+
+ return writeNdefMessage(msg);
+ }
+
+ public boolean writeTextNdefMessage(byte[] payload, String AAR) {
+ final byte[] langBytes = Locale.KOREAN.getLanguage().getBytes(Charset.forName("US-ASCII"));
+ final Charset utfEncoding = Charset.forName("UTF-8");
+ final int utfBit = 0;
+ final char status = (char) (utfBit + langBytes.length);
+ final byte[] data = Util.concat(new byte[]{(byte) status}, langBytes, payload);
+
+ NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data);
+ NdefMessage msg = null;
+ if (AAR != null)
+ msg = new NdefMessage(new NdefRecord[]{record, NdefRecord.createApplicationRecord(AAR)});
+ else
+ msg = new NdefMessage(new NdefRecord[]{record});
+
+ return writeNdefMessage(msg);
+ }
+
+ public boolean writeUriNdefMessage(String payload, String AAR) {
+
+ NdefRecord record = new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI,
+ NdefRecord.RTD_URI, new byte[0], payload.getBytes());
+ NdefMessage msg = null;
+ if (AAR != null)
+ msg = new NdefMessage(new NdefRecord[]{record, NdefRecord.createApplicationRecord(AAR)});
+ else
+ msg = new NdefMessage(new NdefRecord[]{record});
+
+ return writeNdefMessage(msg);
+ }
+
+ public boolean writeMimeNdefMessage(String payload, String AAR) {
+
+ NdefRecord record = new NdefRecord(NdefRecord.TNF_MIME_MEDIA,
+ ("application/" + mContext.getPackageName()).getBytes(), new byte[0], payload.getBytes());
+ NdefMessage msg = null;
+ if (AAR != null)
+ msg = new NdefMessage(new NdefRecord[]{record, NdefRecord.createApplicationRecord(AAR)});
+ else
+ msg = new NdefMessage(new NdefRecord[]{record});
+
+ return writeNdefMessage(msg);
+ }
+
+// This needs API 16 and we will not use it by now, so commented
+// public boolean writeCustomNdefMessage(String payload, String AAR) {
+//
+// NdefRecord record = NdefRecord.createExternal("Yujin Robot", "Cafe demo", payload.getBytes());
+// NdefMessage msg = null ;
+// if (AAR != null)
+// msg = new NdefMessage(new NdefRecord[] {record, NdefRecord.createApplicationRecord(AAR)});
+// else
+// msg = new NdefMessage(new NdefRecord[] {record});
+//
+// return writeNdefMessage(msg);
+// }
+
+ public boolean writeNdefMessage(NdefMessage message) {
+
+ if (mPassedIntent == null) return false;
+
+ Tag tag = mPassedIntent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+ Ndef ndefTag = Ndef.get(tag);
+
+ try {
+ ndefTag.connect();
+ } catch (IOException e) {
+ Log.e("NfcWriter", "Connect to tag failed. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+
+ try {
+ ndefTag.writeNdefMessage(message);
+ } catch (TagLostException e) {
+ Log.e("NfcWriter", "The tag left the field. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ } catch (IOException e) {
+ Log.e("NfcWriter", "Message writing failure. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ } catch (FormatException e) {
+ Log.e("NfcWriter", "Malformed NDEF message. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+
+ try {
+ ndefTag.close();
+ } catch (IOException e) {
+ Log.e("NfcWriter", "Close tag failed. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/AndroidManifest.xml b/src/AndroidApp/android_remocons/common_tools/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5b6924e
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/AppParameters.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/AppParameters.java
new file mode 100644
index 0000000..1a7960c
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/AppParameters.java
@@ -0,0 +1,15 @@
+package com.github.rosjava.android_remocons.common_tools.apps;
+
+import java.util.LinkedHashMap;
+
+/**
+ * Just to provide a get method with default value to LinkedHashMap
+ * Created by jorge on 11/26/13.
+ */
+public class AppParameters extends LinkedHashMap {
+ public AppParameters() { super(); } // Required by snake yaml
+
+ public Object get(String key, Object def) {
+ return super.containsKey(key) ? super.get(key) : def;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/AppRemappings.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/AppRemappings.java
new file mode 100644
index 0000000..12d7d75
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/AppRemappings.java
@@ -0,0 +1,17 @@
+package com.github.rosjava.android_remocons.common_tools.apps;
+
+import java.util.LinkedHashMap;
+
+/**
+ * Just to provide a get method with default value to LinkedHashMap
+ * Created by jorge on 11/26/13.
+ */
+public class AppRemappings extends LinkedHashMap {
+ public String get(String from) {
+ return super.containsKey(from) ? super.get(from) : from;
+ }
+
+ public String get(String from, String to) {
+ return super.containsKey(from) ? super.get(from) : to;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/MasterNameResolver.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/MasterNameResolver.java
new file mode 100644
index 0000000..8393ce7
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/MasterNameResolver.java
@@ -0,0 +1,115 @@
+package com.github.rosjava.android_remocons.common_tools.apps;
+
+import android.util.Log;
+
+import com.github.rosjava.android_remocons.common_tools.master.MasterDescription;
+
+import org.ros.master.client.MasterStateClient;
+import org.ros.master.client.SystemState;
+import org.ros.master.client.TopicSystemState;
+import org.ros.namespace.GraphName;
+import org.ros.namespace.NameResolver;
+import org.ros.node.AbstractNodeMain;
+import org.ros.node.ConnectedNode;
+
+public class MasterNameResolver extends AbstractNodeMain {
+
+ private MasterDescription currentMaster;
+ private NameResolver masterNameResolver;
+ private GraphName masterName;
+ private ConnectedNode connectedNode;
+ private boolean resolved = false;
+
+ public MasterNameResolver() {
+ }
+
+ public void setMaster(MasterDescription currentMaster) {
+ this.currentMaster = currentMaster;
+ }
+
+ @Override
+ public GraphName getDefaultNodeName() {
+ return null;
+ }
+
+ public void setMasterName(String name) {
+ this.masterName = GraphName.of(name);
+ }
+
+ /**
+ * Return the master name as is that will be resolved when actually
+ * connected to a node.
+ *
+ * @return the name, e.g. 'turtlebot'.
+ */
+ public String getMasterName() {
+ return this.masterName.toString();
+ }
+
+ public void resetMasterName(String name) {
+ masterNameResolver = connectedNode.getResolver().newChild(name);
+ }
+
+ /**
+ * The resolved master namespace (after connecting to a master).
+ *
+ * Warning: Do not call this before actually starting the resolver,
+ * or else it will return a null object.
+ *
+ * todo : get this to throw an exception if null
+ *
+ * @return the master name resolver
+ */
+ public NameResolver getMasterNameSpace() {
+ return masterNameResolver;
+ }
+
+ /**
+ * Call this to block until the resolver finishes its job.
+ * i.e. after an execute is called to run the onStart method
+ * below.
+ *
+ * Note - BLOCKING call!
+ */
+ public void waitForResolver() {
+ while (!resolved) {
+ try {
+ Thread.sleep(100);
+ } catch (Exception e) {
+ Log.w("MasterRemocon", "Master name waitForResolver caught an arbitrary exception");
+ }
+ }
+ }
+
+ @Override
+ /**
+ * Resolves the namespace under which master apps can be started
+ * and stopped. Sometimes this will already have been provided
+ * via setMaster() by managed applications (e.g. remocons) which
+ * use the MasterChecker.
+ *
+ * In other cases, such as standalone application we do a simple
+ * parameter lookup, falling back to a default if provided.
+ */
+ public void onStart(final ConnectedNode connectedNode) {
+ this.connectedNode = connectedNode;
+ if (currentMaster != null) {
+ masterName = GraphName.of(currentMaster.getAppsNameSpace());
+ } else {
+ // This is duplicated in PlatformInfoServiceClient and could be better stored somewhere, but it's not much.
+ MasterStateClient masterClient = new MasterStateClient(this.connectedNode, this.connectedNode.getMasterUri());
+ SystemState systemState = masterClient.getSystemState();
+ for (TopicSystemState topic : systemState.getTopics()) {
+ String topicName = topic.getTopicName();
+ GraphName graph_name = GraphName.of(topicName);
+ if ( graph_name.getBasename().toString().equals("app_list") ) {
+ masterName = graph_name.getParent().toRelative();
+ Log.i("ApplicationManagement", "Configuring master namespace resolver [" + masterName + "]");
+ break;
+ }
+ }
+ }
+ masterNameResolver = connectedNode.getResolver().newChild(masterName);
+ resolved = true;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/RosAppActivity.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/RosAppActivity.java
new file mode 100644
index 0000000..40d665c
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/apps/RosAppActivity.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2013 OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.apps;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+import com.github.robotics_in_concert.rocon_rosjava_core.rocon_interactions.InteractionMode;
+import com.github.rosjava.android_remocons.common_tools.dashboards.Dashboard;
+import com.github.rosjava.android_remocons.common_tools.master.MasterDescription;
+import com.github.rosjava.android_remocons.common_tools.rocon.Constants;
+
+import org.ros.address.InetAddressFactory;
+import org.ros.android.RosActivity;
+import org.ros.exception.RosRuntimeException;
+import org.ros.namespace.NameResolver;
+import org.ros.node.NodeConfiguration;
+import org.ros.node.NodeMainExecutor;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.LinkedHashMap;
+
+/**
+ * @author murase@jsk.imi.i.u-tokyo.ac.jp (Kazuto Murase)
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ *
+ * Modified to work in standalone, paired (robot) and concert modes.
+ * Also now handles parameters and remappings.
+ */
+public abstract class RosAppActivity extends RosActivity {
+
+ /*
+ By default we assume the ros app activity is launched independently. The following attribute is
+ used to identify when it has instead been launched by a controlling application (e.g. remocons)
+ in paired, one-to-one, or concert mode.
+ */
+ private InteractionMode appMode = InteractionMode.STANDALONE;
+ private String masterAppName = null;
+ private String defaultMasterAppName = null;
+ private String defaultMasterName = "";
+ private String androidApplicationName; // descriptive helper only
+ private String remoconActivity = null; // The remocon activity to start when finishing this app
+ // e.g. com.github.rosjava.android_remocons.robot_remocon.RobotRemocon
+ private Serializable remoconExtraData = null; // Extra data for remocon (something inheriting from MasterDescription)
+
+ private int dashboardResourceId = 0;
+ private int mainWindowId = 0;
+ private Dashboard dashboard = null;
+ private NodeConfiguration nodeConfiguration;
+ private NodeMainExecutor nodeMainExecutor;
+ protected MasterNameResolver masterNameResolver;
+ protected MasterDescription masterDescription;
+
+ // By now params and remaps are only available for concert apps; that is, appMode must be CONCERT
+ protected AppParameters params = new AppParameters();
+ protected AppRemappings remaps = new AppRemappings();
+
+ protected void setDashboardResource(int resource) {
+ dashboardResourceId = resource;
+ }
+
+ protected void setMainWindowResource(int resource) {
+ mainWindowId = resource;
+ }
+
+ protected void setDefaultMasterName(String name) {
+ defaultMasterName = name;
+ }
+
+ protected void setDefaultAppName(String name) {
+ defaultMasterAppName = name;
+ }
+
+ protected void setCustomDashboardPath(String path) {
+ dashboard.setCustomDashboardPath(path);
+ }
+
+ protected RosAppActivity(String notificationTicker, String notificationTitle) {
+ super(notificationTicker, notificationTitle);
+ this.androidApplicationName = notificationTitle;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (mainWindowId == 0) {
+ Log.e("RosApp",
+ "You must set the dashboard resource ID in your RosAppActivity");
+ return;
+ }
+ if (dashboardResourceId == 0) {
+ Log.e("RosApp",
+ "You must set the dashboard resource ID in your RosAppActivity");
+ return;
+ }
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ setContentView(mainWindowId);
+
+ masterNameResolver = new MasterNameResolver();
+
+ if (defaultMasterName != null) {
+ masterNameResolver.setMasterName(defaultMasterName);
+ }
+
+// FAKE concert remocon invocation
+// MasterId mid = new MasterId("http://192.168.10.129:11311", "http://192.168.10.129:11311", "DesertStorm3", "WEP2", "yujin0610");
+// MasterDescription md = MasterDescription.createUnknown(mid);
+// getIntent().putExtra(MasterDescription.UNIQUE_KEY, md);
+// getIntent().putExtra(AppManager.PACKAGE + ".concert_app_name", "KKKK");
+// getIntent().putExtra("PairedManagerActivity", "com.github.rosjava.android_remocons.rocon_remocon.Remocon");
+// getIntent().putExtra("ChooserURI", "http://192.168.10.129:11311");
+// getIntent().putExtra("Parameters", "{pickup_point: pickup}");
+// getIntent().putExtra("Remappings", "{ 'cmd_vel':'/robot_teleop/cmd_vel', 'image_color':'/robot_teleop/image_color/compressed_throttle' }");
+
+// FAKE robot remocon invocation
+// MasterId mid = new MasterId("http://192.168.10.211:11311", "http://192.168.10.167:11311", "DesertStorm3", "WEP2", "yujin0610");
+// MasterDescription md = MasterDescription.createUnknown(mid);
+// md.setMasterName("grieg");
+// md.setMasterType("turtlebot");
+// getIntent().putExtra(MasterDescription.UNIQUE_KEY, md);
+// getIntent().putExtra(AppManager.PACKAGE + ".paired_app_name", "KKKK");
+// getIntent().putExtra("PairedManagerActivity", "com.github.rosjava.android_remocons.robot_remocon.RobotRemocon");
+//// getIntent().putExtra("RemoconURI", "http://192.168.10.129:11311");
+// getIntent().putExtra("Parameters", "{pickup_point: pickup}");
+// getIntent().putExtra("Remappings", "{ 'cmd_vel':'/robot_teleop/cmd_vel', 'image_color':'/robot_teleop/image_color/compressed_throttle' }");
+
+
+ for (InteractionMode mode : InteractionMode.values()) {
+ // The remocon specifies its type in the app name extra content string, useful information for the app
+ masterAppName = getIntent().getStringExtra(Constants.ACTIVITY_SWITCHER_ID + "." + mode + "_app_name");
+ if (masterAppName != null) {
+ appMode = mode;
+ break;
+ }
+ }
+
+ if (masterAppName == null) {
+ // App name extra content key not present on intent; no remocon started the app, so we are standalone app
+ Log.e("RosApp", "We are running as standalone :(");
+ masterAppName = defaultMasterAppName;
+ appMode = InteractionMode.STANDALONE;
+ }
+ else
+ {
+ // Managed app; take from the intent all the fancy stuff remocon put there for us
+
+ // Extract parameters and remappings from a YAML-formatted strings; translate into hash maps
+ // We create empty maps if the strings are missing to avoid continuous if ! null checks
+ Yaml yaml = new Yaml();
+
+ String paramsStr = getIntent().getStringExtra("Parameters");
+ String remapsStr = getIntent().getStringExtra("Remappings");
+
+ Log.d("RosApp", "Parameters: " + paramsStr);
+ Log.d("RosApp", "Remappings: " + remapsStr);
+
+ try {
+ if ((paramsStr != null) && (! paramsStr.isEmpty())) {
+ LinkedHashMap paramsList = (LinkedHashMap)yaml.load(paramsStr);
+ if (paramsList != null) {
+ params.putAll(paramsList);
+ Log.d("RosApp", "Parameters: " + paramsStr);
+ }
+ }
+ } catch (ClassCastException e) {
+ Log.e("RosApp", "Cannot cast parameters yaml string to a hash map (" + paramsStr + ")");
+ throw new RosRuntimeException("Cannot cast parameters yaml string to a hash map (" + paramsStr + ")");
+ }
+
+ try {
+ if ((remapsStr != null) && (! remapsStr.isEmpty())) {
+ LinkedHashMap remapsList = (LinkedHashMap)yaml.load(remapsStr);
+ if (remapsList != null) {
+ remaps.putAll(remapsList);
+ Log.d("RosApp", "Remappings: " + remapsStr);
+ }
+ }
+ } catch (ClassCastException e) {
+ Log.e("RosApp", "Cannot cast parameters yaml string to a hash map (" + remapsStr + ")");
+ throw new RosRuntimeException("Cannot cast parameters yaml string to a hash map (" + remapsStr + ")");
+ }
+
+ remoconActivity = getIntent().getStringExtra("RemoconActivity");
+
+ // Master description is mandatory on managed apps, as it contains master URI
+ if (getIntent().hasExtra(MasterDescription.UNIQUE_KEY)) {
+ // Keep a non-casted copy of the master description, so we don't lose the inheriting object
+ // when switching back to the remocon. Not fully sure why this works and not if casting
+ remoconExtraData = getIntent().getSerializableExtra(MasterDescription.UNIQUE_KEY);
+
+ try {
+ masterDescription =
+ (MasterDescription) getIntent().getSerializableExtra(MasterDescription.UNIQUE_KEY);
+ } catch (ClassCastException e) {
+ Log.e("RosApp", "Master description expected on intent on " + appMode + " mode");
+ throw new RosRuntimeException("Master description expected on intent on " + appMode + " mode");
+ }
+ }
+ else {
+ // TODO how should I handle these things? try to go back to remocon? Show a message?
+ Log.e("RosApp", "Master description missing on intent on " + appMode + " mode");
+ throw new RosRuntimeException("Master description missing on intent on " + appMode + " mode");
+ }
+ }
+
+ if (dashboard == null) {
+ dashboard = new Dashboard(this);
+ dashboard.setView((LinearLayout) findViewById(dashboardResourceId),
+ new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT));
+ }
+ }
+
+ @Override
+ protected void init(NodeMainExecutor nodeMainExecutor) {
+ this.nodeMainExecutor = nodeMainExecutor;
+ nodeConfiguration = NodeConfiguration.newPublic(InetAddressFactory
+ .newNonLoopback().getHostAddress(), getMasterUri());
+
+ if (appMode == InteractionMode.STANDALONE) {
+ dashboard.setRobotName(masterNameResolver.getMasterName());
+ }
+ else {
+ masterNameResolver.setMaster(masterDescription);
+ dashboard.setRobotName(masterDescription.getMasterName()); // TODO dashboard not working for concerted apps (Issue #32)
+
+ if (appMode == InteractionMode.PAIRED) {
+ dashboard.setRobotName(masterDescription.getMasterType());
+ }
+ }
+
+ // Run master namespace resolver
+ nodeMainExecutor.execute(masterNameResolver, nodeConfiguration.setNodeName("masterNameResolver"));
+ masterNameResolver.waitForResolver();
+
+ nodeMainExecutor.execute(dashboard, nodeConfiguration.setNodeName("dashboard"));
+ }
+
+ protected NameResolver getMasterNameSpace() {
+ return masterNameResolver.getMasterNameSpace();
+ }
+
+ protected void onAppTerminate() {
+ RosAppActivity.this.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ new AlertDialog.Builder(RosAppActivity.this)
+ .setTitle("App Termination")
+ .setMessage(
+ "The application has terminated on the server, so the client is exiting.")
+ .setCancelable(false)
+ .setNeutralButton("Exit",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ RosAppActivity.this.finish();
+ }
+ }).create().show();
+ }
+ });
+ }
+
+ @Override
+ public void startMasterChooser() {
+ if (appMode == InteractionMode.STANDALONE) {
+ super.startMasterChooser();
+ } else {
+ try {
+ nodeMainExecutorService.setMasterUri(new URI(masterDescription.getMasterUri()));
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ RosAppActivity.this.init(nodeMainExecutorService);
+ return null;
+ }
+ }.execute();
+ } catch (URISyntaxException e) {
+ // Remocon cannot be such a bastard to send as a wrong URI...
+ throw new RosRuntimeException(e);
+ }
+ }
+ }
+
+ protected void releaseMasterNameResolver() {
+ nodeMainExecutor.shutdownNodeMain(masterNameResolver);
+ }
+
+ protected void releaseDashboardNode() {
+ nodeMainExecutor.shutdownNodeMain(dashboard);
+ }
+
+ /**
+ * Whether this ros app activity should be responsible for
+ * starting and stopping a paired master application.
+ *
+ * This responsibility is relinquished if the application
+ * is controlled from a remocon, but required if the
+ * android application is connecting and running directly.
+ *
+ * @return boolean : true if it needs to be managed.
+ */
+ private boolean managePairedRobotApplication() {
+ return ((appMode == InteractionMode.STANDALONE) && (masterAppName != null));
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (appMode != InteractionMode.STANDALONE) { // i.e. it's a managed app
+ Log.i("RosApp", "app terminating and returning control to the remocon.");
+ // Restart the remocon, supply it with the necessary information and stop this activity
+ Intent intent = new Intent();
+ intent.putExtra(Constants.ACTIVITY_SWITCHER_ID + "." + appMode + "_app_name", "AppChooser");
+ intent.putExtra(MasterDescription.UNIQUE_KEY, remoconExtraData);
+ intent.setAction(remoconActivity);
+ intent.addCategory("android.intent.category.DEFAULT");
+ startActivity(intent);
+ finish();
+ } else {
+ Log.i("RosApp", "backpress processing for RosAppActivity");
+ }
+ super.onBackPressed();
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/dashboards/Dashboard.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/dashboards/Dashboard.java
new file mode 100644
index 0000000..c47aa0f
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/dashboards/Dashboard.java
@@ -0,0 +1,201 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.dashboards;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.ros.namespace.GraphName;
+import org.ros.node.ConnectedNode;
+import org.ros.node.Node;
+import org.ros.node.NodeMain;
+
+public class Dashboard implements NodeMain {
+ public interface DashboardInterface {
+ /**
+ * Set the ROS Node to use to get status data and connect it up.
+ * Disconnects the previous node if there was one.
+ */
+ public void onStart(ConnectedNode connectedNode);
+
+ public void onShutdown(Node node);
+ }
+
+ private static final String defaultDashboardPath = "com.github.rosjava.android_remocons.common_tools.dashboards.DefaultDashboard";
+ private static final String turtlebotDashboardPath = "com.github.turtlebot.turtlebot_android.turtlebot_core.dashboards.TurtlebotDashboard";
+ private static final String pr2DashboardPath = "com.ros.pr2.apps.core_components.Pr2Dashboard";
+
+ private DashboardInterface dashboard;
+ private Activity activity;
+ private ViewGroup view;
+ private ViewGroup.LayoutParams lparams;
+ private static String robotName;
+ private static String customDashboardPath;
+
+ public Dashboard(Activity activity) {
+ dashboard = null;
+ this.activity = activity;
+ this.view = null;
+ this.lparams = null;
+ }
+
+ public void setView(ViewGroup view, ViewGroup.LayoutParams lparams) {
+ if (view == null) {
+ Log.e("Dashboard", "Null view for dashboard");
+ }
+ this.view = view;
+ this.lparams = lparams;
+ }
+
+ public void setRobotName(String name) {
+ robotName = name;
+ }
+
+ public void setCustomDashboardPath(String path) {
+ this.customDashboardPath = path;
+ }
+
+ private static DashboardInterface createDashboard(Class dashClass,
+ Context context) {
+ ClassLoader classLoader = Dashboard.class.getClassLoader();
+ Object[] args = new Object[1];
+ DashboardInterface result = null;
+ args[0] = context;
+ try {
+ Class contextClass = Class.forName("android.content.Context");
+ result = (DashboardInterface) dashClass.getConstructor(contextClass).newInstance(args);
+ } catch (Exception ex) {
+ Log.e("Dashboard", "Error during dashboard instantiation:", ex);
+ result = null;
+ }
+ return result;
+ }
+
+ private static DashboardInterface createDashboard(String className,
+ Context context) {
+ Class dashClass = null;
+ try {
+ dashClass = Class.forName(className);
+ } catch (Exception ex) {
+ Log.e("Dashboard", "Error during dashboard class loading:", ex);
+ return null;
+ }
+ return createDashboard(dashClass, context);
+
+ }
+
+ /**
+ * Dynamically locate and create a dashboard.
+ */
+ // TODO: deal with the custom robot dashboard
+ private static DashboardInterface createDashboard(Context context) {
+ if (customDashboardPath != null) {
+ return createDashboard(customDashboardPath, context);
+// } else if (robotName.equals("turtlebot")) {
+// return createDashboard(turtlebotDashboardPath, context);
+// } else if (robotName.equals("pr2")) {
+// return createDashboard(pr2DashboardPath, context);
+ } else {
+ return createDashboard(defaultDashboardPath, context);
+ }
+ }
+
+ @Override
+ public void onError(Node arg0, Throwable arg1) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onShutdown(final Node node) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ DashboardInterface dash = dashboard;
+ if (dash != null) {
+ dash.onShutdown(node);
+ view.removeView((View) dash);
+ }
+ dashboard = null;
+ }
+ });
+ }
+
+ @Override
+ public void onShutdownComplete(Node node) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onStart(ConnectedNode connectedNode) {
+ if (dashboard != null) {
+ // FIXME: should we re-start the dashboard? I think this is really
+ // an error.
+ return;
+ }
+ dashboard = Dashboard.createDashboard(activity);
+ if (dashboard != null) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ DashboardInterface dash = dashboard;
+ ViewGroup localView = view;
+ if (dash != null && localView != null) {
+ localView.addView((View) dash, lparams);
+ } else if (dash == null) {
+ Log.e("Dashboard",
+ "Dashboard could not start: no dashboard");
+ } else if (view == null) {
+ Log.e("Dashboard", "Dashboard could not start: no view");
+ } else {
+ Log.e("Dashboard",
+ "Dashboard could not start: no view or dashboard");
+ }
+ }
+ });
+ dashboard.onStart(connectedNode);
+ }
+ }
+
+ @Override
+ public GraphName getDefaultNodeName() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/dashboards/DefaultDashboard.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/dashboards/DefaultDashboard.java
new file mode 100644
index 0000000..1cbe0b1
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/dashboards/DefaultDashboard.java
@@ -0,0 +1,173 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.dashboards;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+
+import com.github.rosjava.android_extras.gingerbread.view.BatteryLevelView;
+import com.github.rosjava.android_remocons.common_tools.R;
+
+import org.ros.exception.RosException;
+import org.ros.message.MessageListener;
+import org.ros.namespace.GraphName;
+import org.ros.namespace.NameResolver;
+import org.ros.node.ConnectedNode;
+import org.ros.node.Node;
+import org.ros.node.topic.Subscriber;
+
+import java.util.HashMap;
+import java.util.List;
+
+import diagnostic_msgs.DiagnosticArray;
+import diagnostic_msgs.DiagnosticStatus;
+import diagnostic_msgs.KeyValue;
+
+public class DefaultDashboard extends LinearLayout implements Dashboard.DashboardInterface {
+ private BatteryLevelView robotBattery;
+ private BatteryLevelView laptopBattery;
+ private ConnectedNode connectedNode;
+ private Subscriber diagnosticSubscriber;
+ private boolean powerOn = false;
+
+ public DefaultDashboard(Context context) {
+ super(context);
+ inflateSelf(context);
+ }
+ public DefaultDashboard(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ inflateSelf(context);
+ }
+ private void inflateSelf(Context context) {
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.default_dashboard, this);
+ robotBattery = (BatteryLevelView) findViewById(R.id.robot_battery);
+ laptopBattery = (BatteryLevelView) findViewById(R.id.laptop_battery);
+ }
+ /**
+ * Set the ROS Node to use to get status data and connect it up. Disconnects
+ * the previous node if there was one.
+ *
+ * @throws org.ros.exception.RosException
+ */
+ /**
+ * Populate view with new diagnostic data. This must be called in the UI
+ * thread.
+ */
+ private void handleDiagnosticArray(DiagnosticArray msg) {
+ for(DiagnosticStatus status : msg.getStatus()) {
+ if(status.getName().equals("/Power System/Battery")) {
+ populateBatteryFromStatus(robotBattery, status);
+ }
+ if(status.getName().equals("/Power System/Laptop Battery")) {
+ populateBatteryFromStatus(laptopBattery, status);
+ }
+ }
+ }
+
+
+ private void populateBatteryFromStatus(BatteryLevelView view, DiagnosticStatus status) {
+ HashMap values = keyValueArrayToMap(status.getValues());
+ try {
+ float percent = 100 * Float.parseFloat(values.get("Charge (Ah)")) / Float.parseFloat(values.get("Capacity (Ah)"));
+ view.setBatteryPercent((int) percent);
+ // TODO: set color red/yellow/green based on level (maybe with
+ // level-set
+ // in XML)
+ } catch(NumberFormatException ex) {
+ // TODO: make battery level gray
+ } catch(ArithmeticException ex) {
+ // TODO: make battery level gray
+ } catch(NullPointerException ex) {
+ // Do nothing: data wasn't there.
+ }
+ try {
+ view.setPluggedIn(Float.parseFloat(values.get("Current (A)")) > 0);
+ } catch(NumberFormatException ex) {
+ } catch(ArithmeticException ex) {
+ } catch(NullPointerException ex) {
+ }
+ }
+ private HashMap keyValueArrayToMap(List list) {
+ HashMap map = new HashMap();
+ for(KeyValue kv : list) {
+ map.put(kv.getKey(), kv.getValue());
+ }
+ return map;
+ }
+
+ @Override
+ public void onShutdown(Node node) {
+ if(diagnosticSubscriber != null) {
+ diagnosticSubscriber.shutdown();
+ }
+ diagnosticSubscriber = null;
+ connectedNode = null;
+
+ }
+
+ @Override
+ public void onStart(ConnectedNode connectedNode) {
+
+ this.connectedNode = connectedNode;
+ try {
+ if(diagnosticSubscriber == null){
+
+ }
+ diagnosticSubscriber = connectedNode.newSubscriber("diagnostics_agg", "diagnostic_msgs/DiagnosticArray");
+ diagnosticSubscriber.addMessageListener(new MessageListener() {
+ @Override
+ public void onNewMessage(final DiagnosticArray message) {
+ DefaultDashboard.this.post(new Runnable() {
+ @Override
+ public void run() {
+ DefaultDashboard.this.handleDiagnosticArray(message);
+ }
+ });
+ }
+ });
+ NameResolver resolver = connectedNode.getResolver().newChild(GraphName.of("/turtlebot_node"));
+ } catch(Exception ex) {
+ this.connectedNode = null;
+ try {
+ throw (new RosException(ex));
+ } catch (RosException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/layouts/CheckableLinearLayout.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/layouts/CheckableLinearLayout.java
new file mode 100644
index 0000000..1c29260
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/layouts/CheckableLinearLayout.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * Neither the name of the Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.layouts;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.LinearLayout;
+/**
+ * Simple extension of LinearLayout which trivially implements
+ * Checkable interface and adds state_checked to drawable states when
+ * appropriate.
+ */
+public class CheckableLinearLayout extends LinearLayout implements Checkable {
+ private boolean checked = false;
+ private static final int[] CHECKED_STATE_SET = {
+ android.R.attr.state_checked
+ };
+ public CheckableLinearLayout(Context ctx) {
+ super(ctx);
+ }
+ public CheckableLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+ @Override
+ public boolean isChecked() {
+ return checked;
+ }
+ @Override
+ public void setChecked( boolean checked ) {
+ if( this.checked != checked ) {
+ this.checked = checked;
+ refreshDrawableState();
+ }
+ }
+
+ @Override
+ public void toggle() {
+ setChecked( !checked );
+ }
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if( isChecked() ) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/ConcertChecker.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/ConcertChecker.java
new file mode 100644
index 0000000..c61d118
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/ConcertChecker.java
@@ -0,0 +1,213 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * Copyright (c) 2013, OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.master;
+
+import android.util.Log;
+
+import com.github.robotics_in_concert.rocon_rosjava_core.master_info.MasterInfo;
+import com.github.robotics_in_concert.rocon_rosjava_core.master_info.MasterInfoException;
+import com.github.robotics_in_concert.rocon_rosjava_core.rocon_interactions.InteractionsException;
+import com.github.robotics_in_concert.rocon_rosjava_core.rocon_interactions.RoconInteractions;
+import com.github.rosjava.android_remocons.common_tools.rocon.Constants;
+
+import org.ros.address.InetAddressFactory;
+import org.ros.android.NodeMainExecutorService;
+import org.ros.internal.node.client.ParameterClient;
+import org.ros.internal.node.server.NodeIdentifier;
+import org.ros.internal.node.xmlrpc.XmlRpcTimeoutException;
+import org.ros.namespace.GraphName;
+import org.ros.node.NodeConfiguration;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Date;
+
+/**
+ * Threaded ROS-concert checker. Runs a thread which checks for a valid ROS
+ * concert and sends back a {@link RoconDescription} (with concert name and type)
+ * on success or a failure reason on failure.
+ *
+ * @author hersh@willowgarage.com
+ * @author murase@jsk.imi.i.u-tokyo.ac.jp (Kazuto Murase)
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class ConcertChecker {
+ public interface ConcertDescriptionReceiver {
+ /**
+ * Called on success with a description of the concert that got checked.
+ */
+ void receive(RoconDescription roconDescription);
+ }
+
+ public interface FailureHandler {
+ /**
+ * Called on failure with a short description of why it failed, like
+ * "exception" or "timeout".
+ */
+ void handleFailure(String reason);
+ }
+
+ private CheckerThread checkerThread;
+ private ConcertDescriptionReceiver foundConcertCallback;
+ private FailureHandler failureCallback;
+
+ /**
+ * Constructor. Should not take any time.
+ */
+ public ConcertChecker(ConcertDescriptionReceiver foundConcertCallback, FailureHandler failureCallback) {
+ this.foundConcertCallback = foundConcertCallback;
+ this.failureCallback = failureCallback;
+ }
+
+ /**
+ * Start the checker thread with the given masterId. If the thread is
+ * already running, kill it first and then start anew. Returns immediately.
+ */
+ public void beginChecking(MasterId masterId) {
+ stopChecking();
+ if (masterId.getMasterUri() == null) {
+ failureCallback.handleFailure("empty concert URI");
+ return;
+ }
+ URI uri;
+ try {
+ uri = new URI(masterId.getMasterUri());
+ } catch (URISyntaxException e) {
+ failureCallback.handleFailure("invalid concert URI");
+ return;
+ }
+ checkerThread = new CheckerThread(masterId, uri);
+ checkerThread.start();
+ }
+
+ /**
+ * Stop the checker thread.
+ */
+ public void stopChecking() {
+ if (checkerThread != null && checkerThread.isAlive()) {
+ checkerThread.interrupt();
+ }
+ }
+
+ private class CheckerThread extends Thread {
+ private URI concertUri;
+ private MasterId masterId;
+
+ public CheckerThread(MasterId masterId, URI concertUri) {
+ this.concertUri = concertUri;
+ this.masterId = masterId;
+ setDaemon(true);
+ // don't require callers to explicitly kill all the old checker threads.
+ setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ failureCallback.handleFailure("exception: " + ex.getMessage());
+ }
+ });
+ }
+
+ @Override
+ public void run() {
+ try {
+ // Check if the master exists by looking for the rosversion parameter.
+ // getParam throws when it can't find the parameter (DJS: what does it throw?).
+ // Could get it to look for a hardcoded rocon parameter for extra guarantees
+ // (e.g. /rocon/version) however we'd still have to do some checking below
+ // when the info is there but interactions not.
+ ParameterClient paramClient = new ParameterClient(
+ NodeIdentifier.forNameAndUri("/concert_checker", concertUri.toString()), concertUri);
+ String version = (String) paramClient.getParam(GraphName.of("/rosversion")).getResult();
+ Log.i("Remocon", "r ros master found [" + version + "]");
+
+ NodeMainExecutorService nodeMainExecutorService = new NodeMainExecutorService();
+ NodeConfiguration nodeConfiguration = NodeConfiguration.newPublic(
+ InetAddressFactory.newNonLoopback().getHostAddress(), concertUri);
+
+ // Check for the concert information topic
+ MasterInfo masterInfo = new MasterInfo();
+ RoconInteractions roconInteractions = new RoconInteractions(Constants.ANDROID_PLATFORM_INFO.getUri());
+
+ nodeMainExecutorService.execute(
+ masterInfo,
+ nodeConfiguration.setNodeName("master_info_node")
+ );
+ masterInfo.waitForResponse(); // MasterInfoExc. on timeout, listener or ros runtime errors
+ Log.i("Remocon", "master info found");
+ nodeMainExecutorService.execute(
+ roconInteractions,
+ nodeConfiguration.setNodeName("rocon_interactions_node")
+ );
+ roconInteractions.waitForResponse(); // InteractionsExc. on timeout, service or ros runtime errors
+ Log.i("Remocon", "rocon interactions found");
+
+ // configure concert description
+ Date timeLastSeen = new Date();
+ RoconDescription description = new RoconDescription(
+ masterId,
+ masterInfo.getName(),
+ masterInfo.getDescription(),
+ masterInfo.getIcon(),
+ roconInteractions.getNamespace(),
+ timeLastSeen);
+
+ description.setConnectionStatus(RoconDescription.OK);
+ description.setUserRoles(roconInteractions.getRoles());
+ foundConcertCallback.receive(description);
+
+ nodeMainExecutorService.shutdownNodeMain(masterInfo);
+ nodeMainExecutorService.shutdownNodeMain(roconInteractions);
+ return;
+ } catch (XmlRpcTimeoutException e) {
+ Log.w("Remocon", "timed out trying to connect to the master [" + concertUri + "][" + e.toString() + "]");
+ failureCallback.handleFailure("Timed out trying to connect to the master. Is your network interface up?");
+ } catch (RuntimeException e) {
+ // thrown if there is no master at that url (from java.net.ConnectException)
+ Log.w("Remocon", "connection refused. Is the master running? [" + concertUri + "][" + e.toString() + "]");
+ failureCallback.handleFailure("Connection refused. Is the master running?");
+ } catch (InteractionsException e) {
+ Log.w("Remocon", "rocon interactions unavailable [" + concertUri + "][" + e.toString() + "]");
+ failureCallback.handleFailure("Rocon interactions unavailable [" + e.toString() + "]");
+ } catch (MasterInfoException e) {
+ Log.w("Remocon", "master info unavailable [" + concertUri + "][" + e.toString() + "]");
+ failureCallback.handleFailure("Rocon master info unavailable. Is your ROS_IP set? Is the rocon_master_info node running?");
+ } catch (Throwable e) {
+ Log.w("Remocon", "exception while creating node in concert checker for URI " + concertUri, e);
+ failureCallback.handleFailure("unknown exception in the rocon checker [" + e.toString() + "]");
+ }
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/MasterDescription.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/MasterDescription.java
new file mode 100644
index 0000000..41eee83
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/MasterDescription.java
@@ -0,0 +1,274 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.master;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+import org.jboss.netty.buffer.ChannelBuffers;
+
+import java.util.Date;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import rocon_std_msgs.Icon;
+
+/**
+ * Mostly a clone of RobotDescription but generic enough to work also for concert apps.
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class MasterDescription implements java.io.Serializable {
+ // Unique identifier for keys passed between android apps.
+ public static final String UNIQUE_KEY = "com.github.rosjava.android_remocons.master.MasterDescription";
+ private static final long serialVersionUID = 1L;
+
+ public static final String CONNECTING = "connecting...";
+ public static final String OK = "ok";
+ public static final String ERROR = "exception";
+ public static final String WIFI = "invalid wifi";
+ public static final String UNAVAILABLE = "unavailable"; // when the master app manager is busy serving another remote controller
+ public static final String CONTROL = "not started";
+ public static final String NAME_UNKNOWN = "Unknown";
+ public static final String TYPE_UNKNOWN = "Unknown";
+
+ private MasterId masterId;
+ private String masterName;
+ private String masterType; // TODO this contains robot type, but in concerts don't make a lot of sense; move to RobotDescription?
+ private String appsNameSpace;
+
+ /** Icon stored piecemeal because msg arrays (stored as jboss ChannelBuffers) can't be
+ * dumped and reloaded by the snakeyaml library. */
+ private String masterIconFormat;
+ private byte[] masterIconData;
+ private int masterIconDataOffset;
+ private int masterIconDataLength;
+
+ private String connectionStatus;
+ private Date timeLastSeen;
+
+ // TODO(kwc): add in canonicalization of masterName
+ public MasterDescription() {
+ }
+
+ public MasterDescription(MasterId masterId, String masterName, String masterType,
+ Icon masterIcon, String appsNameSpace, Date timeLastSeen) {
+ setMasterName(masterName);
+ setMasterId(masterId);
+ this.masterName = masterName;
+ this.masterType = masterType;
+ this.appsNameSpace = appsNameSpace;
+ if (masterIcon != null) {
+ this.masterIconFormat = masterIcon.getFormat();
+ this.masterIconData = masterIcon.getData().array();
+ this.masterIconDataOffset = masterIcon.getData().arrayOffset();
+ this.masterIconDataLength = masterIcon.getData().readableBytes();
+ }
+ this.timeLastSeen = timeLastSeen;
+ }
+
+ public void copyFrom(MasterDescription other) {
+ masterId = other.masterId;
+ masterName = other.masterName;
+ masterType = other.masterType;
+ appsNameSpace = other.appsNameSpace;
+ masterIconFormat = other.masterIconFormat;
+ masterIconData = other.masterIconData;
+ masterIconDataOffset = other.masterIconDataOffset;
+ masterIconDataLength = other.masterIconDataLength;
+ connectionStatus = other.connectionStatus;
+ timeLastSeen = other.timeLastSeen;
+ }
+
+ public MasterId getMasterId() {
+ return masterId;
+ }
+
+ public String getAppsNameSpace() {
+ return appsNameSpace;
+ }
+
+ /**
+ * Convenience accessor to dig into the master uri for this master.
+ *
+ * @return String : the ros master uri for this master.
+ */
+ public String getMasterUri() {
+ return masterId.getMasterUri();
+ }
+
+ public void setMasterId(MasterId masterId) {
+ // TODO: ensure the master id is sane.
+// if(false) {
+// throw new InvalidMasterDescriptionException("Empty Master URI");
+// }
+ // TODO: validate
+ this.masterId = masterId;
+ }
+
+ public String getMasterName() {
+ return masterName;
+ }
+
+ /**
+ * Provide a human-friendly interpretation of the master name
+ *
+ * Often we use master names with a uuid suffix to ensure the master name in a
+ * multi-master group is unique. This checks for the suffix and if found, strips it.
+ *
+ * It also converts '_'s to spaces and first character of each word to uppercase.
+ *
+ * @return human friendly string name
+ */
+ public String getMasterFriendlyName() {
+ String friendlyName = masterName;
+ // The uuid is a 16 byte hash in hex format = 32 characters
+ if (masterName.length() > 32) {
+ String possibleUuidPart = masterName.substring(masterName.length() - 32);
+ Pattern p = Pattern.compile("[^a-f0-9]");
+ Matcher m = p.matcher(possibleUuidPart);
+ if (!m.find()) {
+ friendlyName = masterName.substring(0, masterName.length() - 32);
+ }
+ }
+ friendlyName = friendlyName.replace('_', ' ');
+ final StringBuilder result = new StringBuilder(friendlyName.length());
+ String[] words = friendlyName.split("\\s");
+ for (int i = 0, l = words.length; i < l; ++i) {
+ if (i > 0) result.append(" ");
+ result.append(Character.toUpperCase(words[i].charAt(0)))
+ .append(words[i].substring(1));
+ }
+ return result.toString();
+ }
+
+ public void setMasterName(String masterName) {
+ // TODO: GraphName validation was removed. What replaced it?
+ // if (!GraphName.validate(masterName)) {
+ // throw new InvalidMasterDescriptionException("Bad master name: " +
+ // masterName);
+ // }
+ this.masterName = masterName;
+ }
+
+ public String getMasterType() {
+ return masterType;
+ }
+
+ public void setMasterType(String masterType) {
+ this.masterType = masterType;
+ }
+
+ public String getMasterIconFormat() {
+ return masterIconFormat;
+ }
+
+ public ChannelBuffer getMasterIconData() {
+ if (masterIconData == null) {
+ return null;
+ } else {
+ ChannelBuffer channelBuffer = ChannelBuffers.copiedBuffer(masterIconData, masterIconDataOffset, masterIconDataLength);
+ return channelBuffer;
+ }
+ }
+
+ public void setMasterIconFormat(String iconFormat) {
+ this.masterIconFormat = iconFormat;
+ }
+
+ public void setMasterIconData(ChannelBuffer iconData) {
+ this.masterIconData = iconData.array();
+ }
+
+ public void setMasterIcon(Icon masterIcon) {
+ this.masterIconFormat = masterIcon.getFormat();
+ this.masterIconData = masterIcon.getData().array();
+ }
+
+ public String getConnectionStatus() {
+ return connectionStatus;
+ }
+
+ public void setConnectionStatus(String connectionStatus) {
+ this.connectionStatus = connectionStatus;
+ }
+
+ public Date getTimeLastSeen() {
+ return timeLastSeen;
+ }
+
+ public void setTimeLastSeen(Date timeLastSeen) {
+ this.timeLastSeen = timeLastSeen;
+ }
+
+ public boolean isUnknown() {
+ return this.masterName.equals(NAME_UNKNOWN);
+ }
+
+// public static MasterDescription createUnknown(MasterId masterId) {
+// return new MasterDescription(masterId, NAME_UNKNOWN, TYPE_UNKNOWN, null, NAME_UNKNOWN, new Date());
+// }
+
+ @Override
+ public boolean equals(Object o) {
+ // Return true if the objects are identical.
+ // (This is just an optimization, not required for correctness.)
+ if (this == o) {
+ return true;
+ }
+ // Return false if the other object has the wrong type.
+ // This type may be an interface depending on the interface's
+ // specification.
+ if (!(o instanceof MasterDescription)) {
+ return false;
+ }
+ // Cast to the appropriate type.
+ // This will succeed because of the instanceof, and lets us access
+ // private fields.
+ MasterDescription lhs = (MasterDescription) o;
+ // Check each field. Primitive fields, reference fields, and nullable
+ // reference
+ // fields are all treated differently.
+ return (masterId == null ? lhs.masterId == null : masterId.equals(lhs.masterId));
+ }
+
+ // I need to override equals() so I'm also overriding hashCode() to match.
+ @Override
+ public int hashCode() {
+ // Start with a non-zero constant.
+ int result = 17;
+ // Include a hash for each field checked by equals().
+ result = 31 * result + (masterId == null ? 0 : masterId.hashCode());
+ return result;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/MasterId.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/MasterId.java
new file mode 100644
index 0000000..141af54
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/MasterId.java
@@ -0,0 +1,151 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.master;
+
+import java.util.Map;
+
+/**
+ * Mostly a clone of RobotId but generic enough to work also for concert apps.
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class MasterId implements java.io.Serializable {
+ private static final long serialVersionUID = -1185642483404745956L;
+
+ private String masterUri;
+ private String wifi;
+ private String wifiEncryption;
+ private String wifiPassword;
+
+ public MasterId() {
+ }
+
+ public MasterId(String masterUri, String wifi, String wifiEncryption, String wifiPassword) {
+ this.masterUri = masterUri;
+ this.wifi = wifi;
+ this.wifiEncryption = wifiEncryption;
+ this.wifiPassword = wifiPassword;
+ }
+
+ public MasterId(Map map) {
+ if (map.containsKey("URL")) {
+ this.masterUri = map.get("URL").toString();
+ }
+ if (map.containsKey("WIFI")) {
+ this.wifi = map.get("WIFI").toString();
+ }
+ if (map.containsKey("WIFIENC")) {
+ this.wifiEncryption = map.get("WIFIENC").toString();
+ }
+ if (map.containsKey("WIFIPW")) {
+ this.wifiPassword = map.get("WIFIPW").toString();
+ }
+ }
+
+ public MasterId(String masterUri) {
+ this.masterUri = masterUri;
+ }
+
+ public String getMasterUri() {
+ return masterUri;
+ }
+
+ public String getWifi() {
+ return wifi;
+ }
+
+ public String getWifiEncryption() {
+ return wifiEncryption;
+ }
+
+ public String getWifiPassword() {
+ return wifiPassword;
+ }
+
+ @Override
+ public String toString() {
+ String str = getMasterUri() == null ? "" : getMasterUri();
+ if (getWifi() != null) {
+ str = str + " On Wifi: " + getWifi();
+ }
+ return str;
+ }
+
+ //TODO: not needed?
+ private boolean nullSafeEquals(Object a, Object b) {
+ if (a == b) { //Handles case where both are null.
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ //Non-are null
+ return a.equals(b);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+
+ // Return true if the objects are identical.
+ // (This is just an optimization, not required for correctness.)
+ if (this == o) {
+ return true;
+ }
+ // Return false if the other object has the wrong type.
+ // This type may be an interface depending on the interface's specification.
+ if (!(o instanceof MasterId)) {
+ return false;
+ }
+ // Cast to the appropriate type.
+ // This will succeed because of the instanceof, and lets us access private fields.
+ MasterId lhs = (MasterId) o;
+ return nullSafeEquals(this.masterUri, lhs.masterUri)
+ && nullSafeEquals(this.wifi, lhs.wifi)
+ && nullSafeEquals(this.wifiEncryption, lhs.wifiEncryption)
+ && nullSafeEquals(this.wifiPassword, lhs.wifiPassword);
+ }
+
+ @Override
+ public int hashCode() {
+ // Start with a non-zero constant.
+ int result = 17;
+ // Include a hash for each field checked by equals().
+ result = 31 * result + (masterUri == null ? 0 : masterUri.hashCode());
+ result = 31 * result + (wifi == null ? 0 : wifi.hashCode());
+ result = 31 * result + (wifiEncryption == null ? 0 : wifiEncryption.hashCode());
+ result = 31 * result + (wifiPassword == null ? 0 : wifiPassword.hashCode());
+ return result;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/RoconDescription.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/RoconDescription.java
new file mode 100644
index 0000000..d206221
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/master/RoconDescription.java
@@ -0,0 +1,114 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.master;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Extends MasterDescription with concert specific attributes.
+ * On concerts, apps namespace must be empty.
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class RoconDescription extends MasterDescription implements java.io.Serializable {
+ private static final long serialVersionUID = -4705526306056241179L;
+
+ private String description;
+ private String[] userRoles;
+ private int currentRole = -1;
+ private String interactionsNamespace;
+
+ public static RoconDescription create(MasterDescription master) {
+ RoconDescription cd = new RoconDescription(master.getMasterId(), master.getMasterName(),
+ null, null, null, new Date());
+ cd.setMasterIconFormat(master.getMasterIconFormat());
+ cd.setMasterIconData(master.getMasterIconData());
+ return cd;
+ }
+
+ public static RoconDescription createUnknown(MasterId masterId) {
+ return new RoconDescription(masterId, NAME_UNKNOWN, null, null, null, new Date());
+ }
+
+ /**
+ * Empty constructor required by snake yaml parsing
+ */
+ public RoconDescription() {
+ }
+
+ public RoconDescription(MasterId masterId, String concertName, String description,
+ rocon_std_msgs.Icon concertIcon, String interactionsNamespace,
+ Date timeLastSeen) {
+ super(masterId, concertName, "Rocon concert", concertIcon, "", timeLastSeen);
+
+ this.description = description;
+ this.interactionsNamespace = interactionsNamespace;
+ }
+
+ public void copyFrom(RoconDescription other) {
+ super.copyFrom(other);
+
+ this.userRoles = other.userRoles.clone();
+ this.description = other.description;
+ this.interactionsNamespace = other.interactionsNamespace;
+ }
+
+ public String getInteractionsNamespace() { return this.interactionsNamespace; }
+
+ public String[] getUserRoles() {
+ return userRoles;
+ }
+
+ public String getCurrentRole() {
+ if (userRoles != null && currentRole >= 0 && currentRole < userRoles.length)
+ return userRoles[currentRole];
+ else
+ return null;
+ }
+
+ public void setInteractionsNamespace(String namespace) {
+ this.interactionsNamespace = namespace;
+ }
+
+ public void setUserRoles(List roles)
+ {
+ userRoles = new String[roles.size()];
+ roles.toArray(userRoles);
+ }
+ public void setCurrentRole(int role) {
+ currentRole = role;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/nfc/NfcManager.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/nfc/NfcManager.java
new file mode 100644
index 0000000..80e0d57
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/nfc/NfcManager.java
@@ -0,0 +1,357 @@
+package com.github.rosjava.android_remocons.common_tools.nfc;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentFilter.MalformedMimeTypeException;
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.TagLostException;
+import android.nfc.tech.IsoDep;
+import android.nfc.tech.MifareClassic;
+import android.nfc.tech.MifareUltralight;
+import android.nfc.tech.Ndef;
+import android.nfc.tech.NdefFormatable;
+import android.nfc.tech.NfcA;
+import android.nfc.tech.NfcB;
+import android.nfc.tech.NfcF;
+import android.nfc.tech.NfcV;
+import android.os.Parcelable;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.github.robotics_in_concert.rocon_rosjava_core.rosjava_utils.ByteArrays;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.charset.Charset;
+import java.util.Locale;
+
+public class NfcManager {
+
+ private Context mContext = null;
+ private PendingIntent mPendingIntent = null;
+ private IntentFilter[] mFilters;
+ private String[][] mTechList;
+ private Intent mPassedIntent = null;
+ private NfcAdapter mNfcAdapter = null;
+ private String mCurrentNdefString = "";
+
+ public NfcManager(Context context) {
+ mContext = context;
+ mNfcAdapter = NfcAdapter.getDefaultAdapter(mContext);
+
+ Intent targetIntent = new Intent(mContext, mContext.getClass());
+ targetIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ mPendingIntent = PendingIntent.getActivity(mContext, 0, targetIntent, 0);
+
+ IntentFilter filter_1 = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
+ IntentFilter filter_2 = new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED);
+ IntentFilter filter_3 = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
+
+ try {
+ filter_1.addDataType("*/*");
+ filter_2.addDataType("*/*");
+ filter_3.addDataType("*/*");
+ } catch (MalformedMimeTypeException e) {
+ throw new RuntimeException("fail", e);
+ }
+
+ mFilters = new IntentFilter[]{filter_1, filter_2, filter_3};
+ mTechList = new String[][]{new String[]{NfcF.class.getName()},
+ new String[]{MifareClassic.class.getName()},
+ new String[]{NfcA.class.getName()},
+ new String[]{NfcB.class.getName()},
+ new String[]{NfcV.class.getName()},
+ new String[]{Ndef.class.getName()},
+ new String[]{NdefFormatable.class.getName()},
+ new String[]{MifareUltralight.class.getName()},
+ new String[]{IsoDep.class.getName()}};
+ }
+
+ public boolean checkNfcStatus() {
+ return mNfcAdapter.isEnabled();
+ }
+
+ public boolean changeNfcStatus(boolean enable) {
+
+ if (mNfcAdapter == null) return false;
+
+ boolean success = false;
+ Class> nfcManagerClass = null;
+ Method setNfcEnabled = null, setNfcDisabled = null;
+
+ if (enable) {
+ try {
+ nfcManagerClass = Class.forName(mNfcAdapter.getClass().getName());
+ setNfcEnabled = nfcManagerClass.getDeclaredMethod("enable");
+ setNfcEnabled.setAccessible(true);
+ success = (Boolean) setNfcEnabled.invoke(mNfcAdapter);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ System.out.println(e.toString());
+ }
+
+ } else {
+ try {
+ nfcManagerClass = Class.forName(mNfcAdapter.getClass().getName());
+ setNfcDisabled = nfcManagerClass.getDeclaredMethod("disable");
+ setNfcDisabled.setAccessible(true);
+ success = (Boolean) setNfcDisabled.invoke(mNfcAdapter);
+ } catch (ClassNotFoundException e) {
+ e.printStackTrace();
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return success;
+ }
+
+ public boolean enableForegroundDispatch() {
+ if (mNfcAdapter != null) {
+ mNfcAdapter.enableForegroundDispatch((Activity) mContext, mPendingIntent, mFilters, mTechList);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean disableForegroundDispatch() {
+
+ if (mNfcAdapter != null) {
+ mNfcAdapter.disableForegroundDispatch((Activity) mContext);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean onNewIntent(Intent intent) {
+
+ mPassedIntent = intent;
+ String action = mPassedIntent.getAction();
+
+ Toast.makeText(mContext, action, Toast.LENGTH_SHORT).show();
+
+ if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action) ||
+ NfcAdapter.ACTION_TECH_DISCOVERED.equals(action) ||
+ NfcAdapter.ACTION_NDEF_DISCOVERED.equalsIgnoreCase(action))
+ return true;
+ else
+ return false;
+ }
+
+ public String processTag() {
+
+ if (mPassedIntent == null) return "NFC Tag is not discovered.";
+
+ Parcelable[] rawMsgs = mPassedIntent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+
+ if (rawMsgs == null) {
+ return "NDEF Message is null";
+ }
+
+ mCurrentNdefString = "";
+
+ NdefMessage[] msgs = new NdefMessage[rawMsgs.length];
+
+ for (int i = 0; i < rawMsgs.length; i++) {
+ msgs[i] = (NdefMessage) rawMsgs[i];
+ mCurrentNdefString += ndefMessageToString(msgs[i]);
+ }
+
+ return mCurrentNdefString;
+ }
+
+ public String ndefMessageToString(NdefMessage message) {
+
+ String ndefString = "";
+ NdefRecord[] ndef_records = message.getRecords();
+
+ ndefString += "**Num of NdefRecord : " + ndef_records.length + "\n";
+
+ for (int i = 0; i < ndef_records.length; i++) {
+ String temp = "**Record No. " + i + "\n";
+ byte[] type = ndef_records[i].getType();
+ byte[] id = ndef_records[i].getId();
+ byte[] pl = ndef_records[i].getPayload();
+ byte[] arr = ndef_records[i].toByteArray();
+
+ temp = temp + "- TNF=" + ndef_records[i].getTnf() +
+ "\n - TYPE=" + ByteArrays.getHexString(type, type.length) + " " + new String(type) +
+ "\n - ID=" + ByteArrays.getHexString(id, id.length) + " " + new String(id) +
+ "\n - PayLoad=" + ByteArrays.getHexString(pl, pl.length) + " " + new String(pl) +
+ "\n - ByteArray=" + ByteArrays.getHexString(arr, arr.length) + " " + new String(arr) + "\n";
+
+ ndefString += temp;
+ }
+
+ return ndefString;
+ }
+
+ public byte[] getPayload() {
+
+ if (mPassedIntent == null)
+ return null;
+
+ Parcelable[] rawMsgs = mPassedIntent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+
+ if (rawMsgs == null) {
+ return null;
+ }
+
+ NdefMessage[] msgs = new NdefMessage[rawMsgs.length];
+
+ for (int i = 0; i < rawMsgs.length; i++) {
+ msgs[i] = (NdefMessage) rawMsgs[i];
+ }
+
+ NdefRecord[] records = msgs[0].getRecords();
+ if (records.length > 0)
+ return records[0].getPayload();
+
+ return null;
+ }
+
+ private NdefRecord createTextRecord(String text, Locale locale, boolean encodeInUtf8) {
+
+ final byte[] langBytes = locale.getLanguage().getBytes(Charset.forName("US-ASCII"));
+ final Charset utfEncoding = encodeInUtf8 ? Charset.forName("UTF-8") : Charset.forName("UTF-16");
+ final byte[] textBytes = text.getBytes(utfEncoding);
+ final int utfBit = encodeInUtf8 ? 0 : (1 << 7);
+ final char status = (char) (utfBit + langBytes.length);
+ final byte[] data = ByteArrays.concat(new byte[]{(byte) status}, langBytes, textBytes);
+
+ return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data);
+ }
+
+ public boolean writeTextNdefMessage(String payload, boolean isAAR) {
+ NdefRecord record = createTextRecord(payload, Locale.KOREAN, true);
+ NdefMessage msg = null;
+ if (isAAR)
+ msg = new NdefMessage(new NdefRecord[]{record, NdefRecord.createApplicationRecord(mContext.getPackageName())});
+ else
+ msg = new NdefMessage(new NdefRecord[]{record});
+
+ return writeNdefMessage(msg);
+ }
+
+ public boolean writeTextNdefMessage(byte[] payload, String AAR) {
+ final byte[] langBytes = Locale.KOREAN.getLanguage().getBytes(Charset.forName("US-ASCII"));
+ final Charset utfEncoding = Charset.forName("UTF-8");
+ final int utfBit = 0;
+ final char status = (char) (utfBit + langBytes.length);
+ final byte[] data = ByteArrays.concat(new byte[]{(byte) status}, langBytes, payload);
+
+ NdefRecord record = new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data);
+ NdefMessage msg = null;
+ if (AAR != null)
+ msg = new NdefMessage(new NdefRecord[]{record, NdefRecord.createApplicationRecord(AAR)});
+ else
+ msg = new NdefMessage(new NdefRecord[]{record});
+
+ return writeNdefMessage(msg);
+ }
+
+ public boolean writeUriNdefMessage(String payload, String AAR) {
+
+ NdefRecord record = new NdefRecord(NdefRecord.TNF_ABSOLUTE_URI,
+ NdefRecord.RTD_URI, new byte[0], payload.getBytes());
+ NdefMessage msg = null;
+ if (AAR != null)
+ msg = new NdefMessage(new NdefRecord[]{record, NdefRecord.createApplicationRecord(AAR)});
+ else
+ msg = new NdefMessage(new NdefRecord[]{record});
+
+ return writeNdefMessage(msg);
+ }
+
+ public boolean writeMimeNdefMessage(String payload, String AAR) {
+
+ NdefRecord record = new NdefRecord(NdefRecord.TNF_MIME_MEDIA,
+ ("application/" + mContext.getPackageName()).getBytes(), new byte[0], payload.getBytes());
+ NdefMessage msg = null;
+ if (AAR != null)
+ msg = new NdefMessage(new NdefRecord[]{record, NdefRecord.createApplicationRecord(AAR)});
+ else
+ msg = new NdefMessage(new NdefRecord[]{record});
+
+ return writeNdefMessage(msg);
+ }
+
+// This needs API 16 and we will not use it by now, so commented
+// public boolean writeCustomNdefMessage(String payload, String AAR) {
+//
+// NdefRecord record = NdefRecord.createExternal("Yujin Robot", "Cafe demo", payload.getBytes());
+// NdefMessage msg = null ;
+// if (AAR != null)
+// msg = new NdefMessage(new NdefRecord[] {record, NdefRecord.createApplicationRecord(AAR)});
+// else
+// msg = new NdefMessage(new NdefRecord[] {record});
+//
+// return writeNdefMessage(msg);
+// }
+
+ public boolean writeNdefMessage(NdefMessage message) {
+
+ if (mPassedIntent == null) return false;
+
+ Tag tag = mPassedIntent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+ Ndef ndefTag = Ndef.get(tag);
+
+ try {
+ ndefTag.connect();
+ } catch (IOException e) {
+ Log.e("NfcWriter", "Connect to tag failed. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+
+ try {
+ ndefTag.writeNdefMessage(message);
+ } catch (TagLostException e) {
+ Log.e("NfcWriter", "The tag left the field. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ } catch (IOException e) {
+ Log.e("NfcWriter", "Message writing failure. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ } catch (FormatException e) {
+ Log.e("NfcWriter", "Malformed NDEF message. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+
+ try {
+ ndefTag.close();
+ } catch (IOException e) {
+ Log.e("NfcWriter", "Close tag failed. " + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/nfc/NfcReaderActivity.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/nfc/NfcReaderActivity.java
new file mode 100644
index 0000000..56137e5
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/nfc/NfcReaderActivity.java
@@ -0,0 +1,139 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.nfc;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.TextView;
+
+import com.github.robotics_in_concert.rocon_rosjava_core.rosjava_utils.ByteArrays;
+import com.github.rosjava.android_remocons.common_tools.R;
+
+import java.util.HashMap;
+
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.NFC_MASTER_HOST_FIELD_LENGTH;
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.NFC_PASSWORD_FIELD_LENGTH;
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.NFC_PAYLOAD_LENGTH;
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.NFC_SSID_FIELD_LENGTH;
+
+/**
+ * Read NfcF tags and return the resulting hash map to the invoker action.
+ */
+public class NfcReaderActivity extends Activity {
+ public static boolean enabled = true;
+
+ private NfcManager nfcManager;
+ private TextView textView;
+
+ private HashMap data;
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+
+ try{
+ setContentView(R.layout.nfc_tag_scan);
+ textView = (TextView) findViewById(R.id.text);
+ textView.setText("Scan a NFC tag");
+ nfcManager = new NfcManager(this);
+ }
+ catch (Exception e) {
+ Log.e("NfcReader", e.getMessage());
+ finish();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (nfcManager != null)
+ nfcManager.enableForegroundDispatch();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ if ((nfcManager != null) && (nfcManager.onNewIntent(intent))) {
+ Log.i("NfcReader", "NFC tag read");
+ byte[] payload = nfcManager.getPayload();
+ if (payload.length != NFC_PAYLOAD_LENGTH + 3) // 1 byte for status and 2 lang bytes
+ {
+ Log.e("NfcReader", "Payload doesn't match expected length: " + payload.length +" != " + NFC_PAYLOAD_LENGTH);
+ return;
+ }
+
+ data = new HashMap();
+
+ int offset = 3; // skip 1 byte for status and 2 lang bytes
+ data.put("WIFI", ByteArrays.toString(payload, offset, NFC_SSID_FIELD_LENGTH).trim());
+ offset += NFC_SSID_FIELD_LENGTH;
+ data.put("WIFIPW", ByteArrays.toString(payload, offset, NFC_PASSWORD_FIELD_LENGTH).trim());
+ data.put("WIFIENC", "WPA2");
+ offset += NFC_PASSWORD_FIELD_LENGTH;
+ String host = ByteArrays.toString(payload, offset, NFC_MASTER_HOST_FIELD_LENGTH).trim();
+ offset += NFC_MASTER_HOST_FIELD_LENGTH;
+ short port = ByteArrays.toShort(payload, offset);
+ data.put("URL", "http://" + host + ":" + port);
+
+ finish();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (nfcManager != null)
+ nfcManager.disableForegroundDispatch();
+ }
+
+ @Override
+ public void finish() {
+ // Prepare data in tent
+ Intent returnIntent = new Intent();
+ if (data != null) {
+ // Activity finished ok, return the data
+ returnIntent.putExtra("tag_data", data);
+ setResult(RESULT_OK, returnIntent);
+ }
+ else {
+ setResult(RESULT_CANCELED, returnIntent);
+ }
+
+ super.finish();
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/AppLauncher.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/AppLauncher.java
new file mode 100644
index 0000000..c964cc9
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/AppLauncher.java
@@ -0,0 +1,490 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * Copyright (c) 2013, OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.rocon;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.util.Log;
+import android.util.Patterns;
+
+import com.github.robotics_in_concert.rocon_rosjava_core.rocon_interactions.InteractionMode;
+import com.github.rosjava.android_remocons.common_tools.master.RoconDescription;
+
+import org.yaml.snakeyaml.Yaml;
+
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A rewrite of robot_remocon/AppLauncher that...
+ * - works with concerts
+ * - can start web apps
+ * - headless; only reports error codes
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class AppLauncher {
+
+ public enum Result {
+ SUCCESS,
+ NOT_INSTALLED,
+ NOTHING,
+ CANNOT_CONNECT,
+ MALFORMED_URI,
+ CONNECT_TIMEOUT,
+ OTHER_ERROR;
+
+ public String message;
+
+ Result withMsg(String message) {
+ this.message = message;
+ return this;
+ }
+ }
+
+ public enum AppType {
+ NATIVE,
+ URL,
+ WEB_URL,
+ WEB_APP,
+ NOTHING;
+ }
+ /**
+ * Launch a client app for the given concert app.
+ */
+ static public Result launch(final Activity parent, final RoconDescription concert,
+ final rocon_interaction_msgs.Interaction app){
+ Log.i("AppLaunch", "launching concert app " + app.getDisplayName() + " on service " + app.getNamespace());
+ // On android apps, app name will be an intent action, while for web apps it will be its URL
+ AppType app_type = checkAppType(app.getName());
+ if(app_type == AppType.URL){
+ return launchUrl(parent, concert, app);
+ }
+ else if(app_type == AppType.WEB_URL){
+ return launchWebUrl(parent, concert, app);
+ }
+ else if(app_type == AppType.WEB_APP){
+ return launchWebApp(parent, concert, app);
+ }
+ else if(app_type == AppType.NATIVE){
+ return launchAndroidApp(parent, concert, app);
+ }
+ else if(app_type == AppType.NOTHING){
+ return Result.NOTHING;
+ }
+ else{
+ return Result.NOTHING;
+ }
+ }
+ /**
+ * Check the application name whether web_url(*) or web_app(*)
+ */
+ static public AppType checkAppType(String app_name){
+ String web_url_desc = "web_url(";
+ String web_app_desc = "web_app(";
+ if (Patterns.WEB_URL.matcher(app_name).matches() == true) {
+ return AppType.URL;
+ }
+ else if(app_name.length() == 0){
+ return AppType.NOTHING;
+ }
+ else if(app_name.contains(web_app_desc)){
+ return AppType.WEB_APP;
+ }
+ else if(app_name.contains(web_url_desc)){
+ return AppType.WEB_URL;
+ }
+ else{
+ return AppType.NATIVE;
+ }
+ }
+
+ /**
+ * Launch a client android app for the given concert app.
+ */
+ static private Result launchAndroidApp(final Activity parent, final RoconDescription concert,
+ final rocon_interaction_msgs.Interaction app) {
+ // Create the Intent from rapp's name, pass it parameters and remaps and start it
+ Result result = Result.OTHER_ERROR;
+ String appName = app.getName();
+ Intent intent = new Intent(appName);
+ // Copy all app data to "extra" data in the intent.
+ intent.putExtra(Constants.ACTIVITY_SWITCHER_ID + "." + InteractionMode.CONCERT + "_app_name", appName);
+ intent.putExtra(RoconDescription.UNIQUE_KEY, concert);
+ intent.putExtra("RemoconActivity", Constants.ACTIVITY_ROCON_REMOCON);
+ intent.putExtra("Parameters", app.getParameters()); // YAML-formatted string
+
+ // Remappings come as a messages list that make YAML parser crash, so we must digest if for him
+ if ((app.getRemappings() != null) && (app.getRemappings().size() > 0)) {
+ String remaps = "{";
+ for (rocon_std_msgs.Remapping remap: app.getRemappings())
+ remaps += remap.getRemapFrom() + ": " + remap.getRemapTo() + ", ";
+ remaps = remaps.substring(0, remaps.length() - 2) + "}";
+ intent.putExtra("Remappings", remaps);
+ }
+ try {
+ Log.i("AppLaunch", "launchAndroidApp trying to start activity (action: " + appName + " )");
+ parent.startActivity(intent);
+ result = Result.SUCCESS;
+ } catch (ActivityNotFoundException e) {
+ Log.i("AppLaunch", "launchAndroidApp activity not found for action, find package name: " + appName);
+ result = launchAndroidAppWithPkgName(parent, concert, app);
+ }
+ return result;
+ }
+
+ static private Result launchAndroidAppWithPkgName(final Activity parent, final RoconDescription concert,
+ final rocon_interaction_msgs.Interaction app) {
+ // Create the Intent from rapp's name, pass it parameters and remaps and start it
+ Result result = Result.OTHER_ERROR;
+ String appName = app.getName();
+ PackageManager manager = parent.getPackageManager();
+ Intent intent = manager.getLaunchIntentForPackage(appName);
+ if(intent != null) {
+ // Copy all app data to "extra" data in the intent.
+ intent.putExtra(Constants.ACTIVITY_SWITCHER_ID + "." + InteractionMode.CONCERT + "_app_name", appName);
+ intent.putExtra(RoconDescription.UNIQUE_KEY, concert);
+ intent.putExtra("RemoconActivity", Constants.ACTIVITY_ROCON_REMOCON);
+ intent.putExtra("Parameters", app.getParameters()); // YAML-formatted string
+
+ // Remappings come as a messages list that make YAML parser crash, so we must digest if for him
+ if ((app.getRemappings() != null) && (app.getRemappings().size() > 0)) {
+ String remaps = "{";
+ for (rocon_std_msgs.Remapping remap : app.getRemappings())
+ remaps += remap.getRemapFrom() + ": " + remap.getRemapTo() + ", ";
+ remaps = remaps.substring(0, remaps.length() - 2) + "}";
+ intent.putExtra("Remappings", remaps);
+ }
+ try {
+ Log.i("AppLaunch", "launchAndroidAppWithPkgName trying to start activity (action: " + appName + " )");
+ parent.startActivity(intent);
+ return Result.SUCCESS;
+ } catch (ActivityNotFoundException e) {
+ Log.i("AppLaunch", "launchAndroidAppWithPkgName activity not found for action: " + appName);
+ result = launchAndroidAppWithAppId(parent, concert, app);
+ }
+ }
+ else{
+ result = launchAndroidAppWithAppId(parent, concert, app);
+ }
+ return result;
+ }
+
+ static private Result launchAndroidAppWithAppId(final Activity parent, final RoconDescription concert,
+ final rocon_interaction_msgs.Interaction app) {
+ // Create the Intent from rapp's name, pass it parameters and remaps and start it
+ Result result = Result.OTHER_ERROR;
+ String launchablePkgName = "";
+ PackageManager manager = parent.getPackageManager();
+ List applicationInfo = manager.getInstalledApplications(manager.GET_META_DATA);
+ for (int i = 0; i < applicationInfo.size(); i++){
+ ApplicationInfo appInfo = applicationInfo.get(i);
+ if (app.getName().contains(appInfo.processName)){
+ launchablePkgName = appInfo.packageName;
+ break;
+ }
+ }
+ if(launchablePkgName.length()!=0) {
+ Intent intent = manager.getLaunchIntentForPackage(launchablePkgName);
+ if(intent != null) {
+ // Copy all app data to "extra" data in the intent.
+ intent.putExtra(Constants.ACTIVITY_SWITCHER_ID + "." + InteractionMode.CONCERT + "_app_name", launchablePkgName);
+ intent.putExtra(RoconDescription.UNIQUE_KEY, concert);
+ intent.putExtra("RemoconActivity", Constants.ACTIVITY_ROCON_REMOCON);
+ intent.putExtra("Parameters", app.getParameters()); // YAML-formatted string
+ // Remappings come as a messages list that make YAML parser crash, so we must digest if for him
+ if ((app.getRemappings() != null) && (app.getRemappings().size() > 0)) {
+ String remaps = "{";
+ for (rocon_std_msgs.Remapping remap : app.getRemappings())
+ remaps += remap.getRemapFrom() + ": " + remap.getRemapTo() + ", ";
+ remaps = remaps.substring(0, remaps.length() - 2) + "}";
+ intent.putExtra("Remappings", remaps);
+ }
+ try {
+ Log.i("AppLaunch", "launchAndroidAppWithoutRosVer trying to start activity (action: " + launchablePkgName + " )");
+ parent.startActivity(intent);
+ result = Result.SUCCESS;
+ } catch (ActivityNotFoundException e) {
+ Log.i("AppLaunch", "launchAndroidAppWithoutRosVer activity not found for action, find package name: " + launchablePkgName);
+ result = Result.NOT_INSTALLED;
+ }
+ }
+ }
+ {
+ result = Result.NOT_INSTALLED;
+ }
+ return result;
+ }
+
+ /**
+ * Launch a client url for the given concert app.
+ */
+ static private Result launchUrl (final Activity parent, final RoconDescription concert,
+ final rocon_interaction_msgs.Interaction app) {
+ try
+ {
+ // Validate the URL before starting anything
+ String app_name = "";
+ app_name = app.getName();
+ URL appURL = new URL(app_name);
+ //2014.12.03 comment by dwlee
+ //reason of blocking, Not necessary in web app launcher.
+ /*
+ AsyncTask asyncTask = new AsyncTask() {
+ @Override
+ protected String doInBackground(URL... urls) {
+ try {
+ HttpURLConnection urlConnection = (HttpURLConnection)urls[0].openConnection();
+ int unused_responseCode = urlConnection.getResponseCode();
+ urlConnection.disconnect();
+ return urlConnection.getResponseMessage();
+ }
+ catch (IOException e) {
+ return e.getMessage();
+ }
+ }
+ }.execute(appURL);
+ String result = asyncTask.get(5, TimeUnit.SECONDS);
+ if (result == null || (result.startsWith("OK") == false && result.startsWith("ok") == false)) {
+ return Result.CANNOT_CONNECT.withMsg(result);
+ }
+ */
+
+ // We pass concert URL, parameters and remaps as URL parameters
+ String appUriStr = app_name;
+ Uri appURI = Uri.parse(appUriStr);
+
+ // Create an action view intent and pass rapp's name + extra information as URI
+ Intent intent = new Intent(Intent.ACTION_VIEW, appURI);
+ intent.putExtra(Constants.ACTIVITY_SWITCHER_ID + "." + InteractionMode.CONCERT + "_app_name",app_name);
+
+ Log.i("AppLaunch", "trying to start web app (URI: " + appUriStr + ")");
+ parent.startActivity(intent);
+ return Result.SUCCESS;
+ }
+ catch (MalformedURLException e)
+ {
+ return Result.MALFORMED_URI.withMsg("App URL is not valid. " + e.getMessage());
+ }
+ catch (ActivityNotFoundException e) {
+ // This cannot happen for a web site, right? must mean that I have no web browser!
+ return Result.NOT_INSTALLED.withMsg("Activity not found for view action??? muoia???");
+ } catch (Exception e)
+ {
+ return Result.OTHER_ERROR.withMsg(e.getMessage());
+ }
+ }
+
+ /**
+ * Launch a client web url for the given concert app.
+ */
+ static private Result launchWebUrl(final Activity parent, final RoconDescription concert,
+ final rocon_interaction_msgs.Interaction app) {
+ try
+ {
+ // Validate the URL before starting anything
+ String app_name = "";
+ String app_type = "web_url";
+ app_name = app.getName().substring(app_type.length()+1,app.getName().length()-1);
+
+ URL appURL = new URL(app_name);
+
+ //2014.12.03 comment by dwlee
+ //reason of blocking, Not necessary in web app launcher.
+ /*
+ AsyncTask asyncTask = new AsyncTask() {
+ @Override
+ protected String doInBackground(URL... urls) {
+ try {
+ HttpURLConnection urlConnection = (HttpURLConnection)urls[0].openConnection();
+ int unused_responseCode = urlConnection.getResponseCode();
+ urlConnection.disconnect();
+ return urlConnection.getResponseMessage();
+ }
+ catch (IOException e) {
+ return e.getMessage();
+ }
+ }
+ }.execute(appURL);
+ String result = asyncTask.get(5, TimeUnit.SECONDS);
+ if (result == null || (result.startsWith("OK") == false && result.startsWith("ok") == false)) {
+ return Result.CANNOT_CONNECT.withMsg(result);
+ }
+ */
+
+ // We pass concert URL, parameters and remaps as URL parameters
+ String appUriStr = app_name;
+ Uri appURI = Uri.parse(appUriStr);
+
+ // Create an action view intent and pass rapp's name + extra information as URI
+ Intent intent = new Intent(Intent.ACTION_VIEW, appURI);
+ intent.putExtra(Constants.ACTIVITY_SWITCHER_ID + "." + InteractionMode.CONCERT + "_app_name",app_name);
+
+ Log.i("AppLaunch", "trying to start web url (URI: " + appUriStr + ")");
+ parent.startActivity(intent);
+ return Result.SUCCESS;
+ }
+ catch (MalformedURLException e)
+ {
+ return Result.MALFORMED_URI.withMsg("App URL is not valid. " + e.getMessage());
+ }
+ catch (ActivityNotFoundException e) {
+ // This cannot happen for a web site, right? must mean that I have no web browser!
+ return Result.NOT_INSTALLED.withMsg("Activity not found for view action??? muoia???");
+ }
+ catch (Exception e)
+ {
+ return Result.OTHER_ERROR.withMsg(e.getMessage());
+ }
+ }
+ /**
+ * Launch a client web app for the given concert app.
+ */
+ static private Result launchWebApp(final Activity parent, final RoconDescription concert,
+ final rocon_interaction_msgs.Interaction app) {
+ try
+ {
+ // Validate the URL before starting anything
+ String app_name = "";
+ String app_type = "";
+ app_type = "web_app";
+ app_name = app.getName().substring(app_type.length()+1,app.getName().length()-1);
+ URL appURL = new URL(app_name);
+ //2014.05.27 comment by dwlee
+ //reason of blocking, Not necessary in web app launcher.
+ /*
+ Log.i("AppLaunch", "Connection test (URI: " + app_name + ")");
+ AsyncTask asyncTask = new AsyncTask() {
+ @Override
+ protected String doInBackground(URL... urls) {
+ try {
+ HttpURLConnection urlConnection = (HttpURLConnection)urls[0].openConnection();
+ int unused_responseCode = urlConnection.getResponseCode();
+ urlConnection.disconnect();
+ return urlConnection.getResponseMessage();
+ }
+ catch (IOException e) {
+ return e.getMessage();
+ }
+ }
+ }.execute(appURL);
+
+ String result = asyncTask.get(15, TimeUnit.SECONDS);
+
+ if (result == null || (result.startsWith("OK") == false && result.startsWith("ok") == false)) {
+ Log.i("AppLaunch", "Connection test Fail (URI: " + app_name + ")");
+ return Result.CANNOT_CONNECT.withMsg(result);
+ }
+ Log.i("AppLaunch", "Connection test Success (URI: " + app_name + ")");
+ */
+
+ // We pass concert URL, parameters and remaps as URL parameters
+ String appUriStr = app_name;
+ String interaction_data = "{";
+ //add remap
+ String remaps = "\"remappings\": {";
+ if ((app.getRemappings() != null) && (app.getRemappings().size() > 0)) {
+ for (rocon_std_msgs.Remapping remap: app.getRemappings())
+ remaps += "\"" + remap.getRemapFrom() + "\":\"" + remap.getRemapTo() + "\",";
+ remaps = remaps.substring(0, remaps.length() - 1) + "}";
+ }
+ else{
+ remaps+="}";
+ }
+ remaps += ",";
+ interaction_data += remaps;
+
+ //add displayname
+ String displayname = "\"display_name\":";
+ if ((app.getDisplayName() != null) && (app.getDisplayName().length() > 0)) {
+ displayname += "\"" + app.getDisplayName() +"\"";
+ }
+ displayname +=",";
+ interaction_data += displayname;
+
+ //add parameters
+ String parameters = "\"parameters\": {";
+ if ((app.getParameters() != null) && (app.getParameters().length() > 2)) {
+ Yaml yaml = new Yaml();
+ Map params = (Map) yaml.load(app.getParameters());
+ for( String key : params.keySet() ) {
+ parameters += "\"" + key + "\":\"" + String.valueOf(params.get(key))+"" + "\",";
+ }
+ parameters = parameters.substring(0, parameters.length() - 1);
+ }
+
+ parameters +="}";
+ interaction_data += parameters;
+ interaction_data +="}";
+
+ appUriStr = appUriStr + "?" + "interaction_data=" + URLEncoder.encode(interaction_data);
+ appURL.toURI(); // throws URISyntaxException if fails; probably a redundant check
+ Uri appURI = Uri.parse(appUriStr);
+
+ // Create an action view intent and pass rapp's name + extra information as URI
+ Intent intent = new Intent(Intent.ACTION_VIEW, appURI);
+ intent.putExtra(Constants.ACTIVITY_SWITCHER_ID + "." + InteractionMode.CONCERT + "_app_name",app_name);
+
+ Log.i("AppLaunch", "trying to start web app (URI: " + appUriStr + ")");
+ parent.startActivity(intent);
+ return Result.SUCCESS;
+ }
+ catch (URISyntaxException e) {
+ return Result.MALFORMED_URI.withMsg("Cannot convert URL into URI. " + e.getMessage());
+ }
+ catch (MalformedURLException e)
+ {
+ return Result.MALFORMED_URI.withMsg("App URL is not valid. " + e.getMessage());
+ }
+ catch (ActivityNotFoundException e) {
+ // This cannot happen for a web site, right? must mean that I have no web browser!
+ return Result.NOT_INSTALLED.withMsg("Activity not found for view action??? muoia???");
+ }
+ catch (Exception e)
+ {
+ return Result.OTHER_ERROR.withMsg(e.getMessage());
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/AppsManager.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/AppsManager.java
new file mode 100644
index 0000000..f333f07
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/AppsManager.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright (C) 2013, OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.rocon;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.github.rosjava.android_remocons.common_tools.master.MasterId;
+
+import org.ros.address.InetAddressFactory;
+import org.ros.android.NodeMainExecutorService;
+import org.ros.exception.RosRuntimeException;
+import org.ros.exception.ServiceNotFoundException;
+import org.ros.internal.node.client.ParameterClient;
+import org.ros.internal.node.server.NodeIdentifier;
+import org.ros.namespace.GraphName;
+import org.ros.node.AbstractNodeMain;
+import org.ros.node.ConnectedNode;
+import org.ros.node.Node;
+import org.ros.node.NodeConfiguration;
+import org.ros.node.service.ServiceClient;
+import org.ros.node.service.ServiceResponseListener;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import rocon_interaction_msgs.GetInteraction;
+import rocon_interaction_msgs.GetInteractionRequest;
+import rocon_interaction_msgs.GetInteractionResponse;
+import rocon_interaction_msgs.GetInteractions;
+import rocon_interaction_msgs.GetInteractionsRequest;
+import rocon_interaction_msgs.GetInteractionsResponse;
+import rocon_interaction_msgs.RequestInteraction;
+import rocon_interaction_msgs.RequestInteractionRequest;
+import rocon_interaction_msgs.RequestInteractionResponse;
+
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.ANDROID_PLATFORM_INFO;
+
+/**
+ * This class has been derived from the original turtlebot robot app manager but now
+ * tuned for interactions.
+ *
+ * The original is quite messy, and this is not much better, so maybe needs extra refactoring.
+ * Also RobotAppsManager claims that it can be executed once, but I modified this to execute
+ * arbitrary times. It looks to work, but mechanism is a bit brittle.
+ * Apps manager implements the services and topics required to interact with the concert roles
+ * manager. Typically to use this class its a three step process:
+ *
+ * 1) instantiate and provide a general failure handler
+ * 2) provide a callback via one of the setupXXXservice methods
+ * 3) call the service you want; there's a public method per service
+ * 4) wait for service response.
+ *
+ * Essentially you are creating a node when creating an instance, and rosjava isolates each
+ * service/topic to each 'node'.
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class AppsManager extends AbstractNodeMain {
+ public interface FailureHandler {
+ /**
+ * Called on failure with a short description of why it failed, like "exception" or "timeout".
+ */
+ void handleFailure(String reason);
+ }
+
+ public enum Action {
+ NONE, GET_INTERACTIONS_FOR_ROLE, GET_INTERACTION_INFO, REQUEST_INTERACTION_USE
+ };
+
+ private String role;
+ private String interactionsNamespace;
+ private Action action = Action.NONE;
+ private rocon_interaction_msgs.Interaction app;
+ private int app_hash; // need this separately since headless starts don't have any app info.
+ private ConnectNodeThread connectThread;
+ private ConnectedNode connectedNode;
+ private NodeMainExecutorService nodeMainExecutorService;
+ private FailureHandler failureCallback;
+ private ServiceResponseListener requestServiceResponseListener;
+ private ServiceResponseListener getAppsServiceResponseListener;
+ private ServiceResponseListener appInfoServiceResponseListener;
+
+
+ public AppsManager(FailureHandler failureCallback) {
+
+ this.failureCallback = failureCallback;
+ }
+
+ /**
+ * This is done outside of the constructor to save it from having to be continually
+ * reinstantiated in the rocon remocons. That does mean it requires some black magic,
+ * i.e. construction -> init & setupXYZ -> execute
+ *
+ * @param interactionsNamespace
+ */
+ public void init(String interactionsNamespace) {
+ this.interactionsNamespace = interactionsNamespace;
+ }
+
+ public void setupRequestService(ServiceResponseListener serviceResponseListener) {
+ this.requestServiceResponseListener = serviceResponseListener;
+ }
+
+ public void setupGetInteractionsService(ServiceResponseListener serviceResponseListener) {
+ this.getAppsServiceResponseListener = serviceResponseListener;
+ }
+
+ public void setupAppInfoService(ServiceResponseListener serviceResponseListener) {
+ this.appInfoServiceResponseListener = serviceResponseListener;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ shutdown(); // TODO not warrantied to be called, so user should call shutdown; finalize works at all in Android??? I think no....
+ }
+
+ public void shutdown() {
+ if (nodeMainExecutorService != null)
+ nodeMainExecutorService.shutdownNodeMain(this);
+ else
+ Log.w("AppsMng", "Shutting down an uninitialized apps manager");
+ }
+
+ public void getAppsForRole(final MasterId masterId, final String role) {
+ this.action = Action.GET_INTERACTIONS_FOR_ROLE;
+ this.role = role;
+
+ // If this is the first action requested, we need a connected node, what must be done in a different thread
+ // The requested action will be executed once we have a connected node (this object itself) in onStart method
+ if (this.connectedNode == null) {
+ Log.d("AppsMng", "First action requested (" + this.action + "). Starting node...");
+ new ConnectNodeThread(masterId).start();
+ }
+ else {
+ // But we don't need all this if we already executed an action, and so have a connected node. Anyway,
+ // we must execute any action asynchronously to avoid the android.os.NetworkOnMainThreadException
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ getAppsForRole();
+ return null;
+ }
+ }.execute(); // TODO: can we use this to incorporate a timeout to service calls?
+ }
+ }
+
+ public void requestAppUse(final MasterId masterId, final String role, final rocon_interaction_msgs.Interaction app) {
+ this.action = Action.REQUEST_INTERACTION_USE;
+ this.role = role;
+ this.app = app;
+ this.app_hash = app.getHash();
+
+ // If this is the first action requested, we need a connected node, what must be done in a different thread
+ // The requested action will be executed once we have a connected node (this object itself) in onStart method
+ if (this.connectedNode == null) {
+ Log.d("AppsMng", "First action requested (" + this.action + "). Starting node...");
+ new ConnectNodeThread(masterId).start();
+ }
+ else {
+ // But we don't need all this if we already executed an action, and so have a connected node. Anyway,
+ // we must execute any action asynchronously to avoid the android.os.NetworkOnMainThreadException
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ requestAppUse();
+ return null;
+ }
+ }.execute();
+ }
+ }
+
+ /**
+ * This is called from the headless launcher where the only thing it knows is the
+ * master id (for connection purposes and the hash stored in an nfc tag (or other).
+ * We need to save this hash for later service calls.
+ *
+ * @param masterId
+ * @param hash
+ */
+ public void getAppInfo(final MasterId masterId, final int hash) {
+ this.action = Action.GET_INTERACTION_INFO;
+ this.app_hash = hash;
+
+ // If this is the first action requested, we need a connected node, what must be done in a different thread
+ // The requested action will be executed once we have a connected node (this object itself) in onStart method
+ if (this.connectedNode == null) {
+ Log.d("AppsMng", "First action requested (" + this.action + "). Starting node...");
+ new ConnectNodeThread(masterId).start();
+ }
+ else {
+ // But we don't need all this if we already executed an action, and so have a connected node. Anyway,
+ // we must execute any action asynchronously to avoid the android.os.NetworkOnMainThreadException
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ getAppInfo();
+ return null;
+ }
+ }.execute(); // TODO: can we use this to incorporate a timeout to service calls?
+ }
+ }
+
+ private void getAppsForRole() {
+ // call get_roles_and_apps concert service
+ ServiceClient srvClient;
+ String serviceName = this.interactionsNamespace + "/get_interactions";
+
+ try {
+ Log.d("AppsMng", "List apps service client created [" + serviceName + "]");
+ srvClient = connectedNode.newServiceClient(serviceName, GetInteractions._TYPE);
+ } catch (ServiceNotFoundException e) {
+ Log.w("AppsMng", "List apps service not found [" + serviceName + "]");
+ throw new RosRuntimeException(e); // TODO we should recover from this calling onFailure on listener
+ }
+ final GetInteractionsRequest request = srvClient.newMessage();
+
+ request.getRoles().add(role);
+ request.setUri(ANDROID_PLATFORM_INFO.getUri());
+
+ srvClient.call(request, getAppsServiceResponseListener);
+ Log.d("AppsMng", "List apps service call done [" + serviceName + "]");
+ }
+
+ private void requestAppUse() {
+ // call request_interaction concert service
+ ServiceClient srvClient;
+ String serviceName = this.interactionsNamespace + "/request_interaction";
+ try {
+ Log.d("AppsMng", "Request app service client created [" + serviceName + "]");
+ srvClient = connectedNode.newServiceClient(serviceName, RequestInteraction._TYPE);
+ } catch (ServiceNotFoundException e) {
+ Log.w("AppsMng", "Request app service not found [" + serviceName + "]");
+ throw new RosRuntimeException(e); // TODO we should recover from this calling onFailure on listener
+ }
+ final RequestInteractionRequest request = srvClient.newMessage();
+
+ request.setHash(this.app.getHash());
+
+ srvClient.call(request, requestServiceResponseListener);
+ Log.d("AppsMng", "Request app service call done [" + serviceName + "]");
+ }
+
+ private void getAppInfo() {
+ // call get_app concert service
+ ServiceClient srvClient;
+ String serviceName = this.interactionsNamespace + "/get_interaction";
+ try {
+ Log.d("AppsMng", "Get app info service client created [" + serviceName + "]");
+ srvClient = connectedNode.newServiceClient(serviceName, GetInteraction._TYPE);
+ } catch (ServiceNotFoundException e) {
+ Log.w("AppsMng", "Get app info not found [" + serviceName + "]");
+ throw new RosRuntimeException(e); // TODO we should recover from this calling onFailure on listener
+ }
+ final GetInteractionRequest request = srvClient.newMessage();
+ request.setHash(this.app_hash);
+
+ srvClient.call(request, appInfoServiceResponseListener);
+ Log.d("AppsMng", "Get app info service call done [" + serviceName + "]");
+ }
+
+ /**
+ * Start a thread to get a node connected with the given masterId. Returns immediately
+ * TODO: what happens if the thread is already running??? kill it first and then start a new one?
+ */
+ private class ConnectNodeThread extends Thread {
+ private MasterId masterId;
+
+ public ConnectNodeThread(MasterId masterId) {
+ this.masterId = masterId;
+ setDaemon(true);
+ setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ failureCallback.handleFailure("exception: " + ex.getMessage());
+ }
+ });
+ }
+
+ @Override
+ public void run() {
+ try {
+ URI concertUri = new URI(masterId.getMasterUri());
+
+ // Check if the master exists by looking for a master parameter
+ // getParam throws when it can't find the parameter (DJS: what does it throw?).
+ // Could get it to look for a hardcoded rocon parameter for extra guarantees
+ // (e.g. /rocon/version) however we'd still have to do some checking below
+ // when the info is there but interactions not.
+ ParameterClient paramClient = new ParameterClient(
+ NodeIdentifier.forNameAndUri("/concert_checker", concertUri.toString()), concertUri);
+ String name = (String) paramClient.getParam(GraphName.of("/rosversion")).getResult();
+ Log.i("Remocon", "Concert " + name + " found; retrieve additional information");
+
+ nodeMainExecutorService = new NodeMainExecutorService();
+ NodeConfiguration nodeConfiguration = NodeConfiguration.newPublic(
+ InetAddressFactory.newNonLoopback().getHostAddress(), concertUri);
+ nodeMainExecutorService.execute(AppsManager.this, nodeConfiguration.setNodeName("apps_manager_node"));
+
+ } catch (URISyntaxException e) {
+ Log.w("AppsMng", "invalid concert URI [" + masterId.getMasterUri() + "][" + e.toString() + "]");
+ failureCallback.handleFailure("invalid concert URI");
+ } catch (RuntimeException e) {
+ // thrown if concert could not be found in the getParam call (from java.net.ConnectException)
+ Log.w("AppsMng", "could not find concert [" + masterId.getMasterUri() + "][" + e.toString() + "]");
+ failureCallback.handleFailure(e.toString());
+ } catch (Throwable e) {
+ Log.w("AppsMng", "exception while creating node in concert checker for URI " + masterId.getMasterUri(), e);
+ failureCallback.handleFailure(e.toString());
+ }
+ }
+ }
+
+ @Override
+ public GraphName getDefaultNodeName() {
+ return null;
+ }
+
+ /**
+ * Node started by NodeMainExecutor.execute(). Execute the requested action.
+ *
+ * @param connectedNode
+ */
+ @Override
+ public void onStart(final ConnectedNode connectedNode) {
+ if (this.connectedNode != null) {
+ Log.e("AppsMng", "App manager re-started before previous shutdown; ignoring...");
+ return;
+ }
+
+ this.connectedNode = connectedNode;
+
+ Log.d("AppsMng", "onStart() - " + action);
+
+ switch (action) {
+ case NONE:
+ Log.w("AppsMng", "Node started without specifying an action");
+ break;
+ case REQUEST_INTERACTION_USE:
+ requestAppUse();
+ break;
+ case GET_INTERACTIONS_FOR_ROLE:
+ getAppsForRole();
+ break;
+ case GET_INTERACTION_INFO:
+ getAppInfo();
+ break;
+ default:
+ Log.w("AppsMng", "Unrecogniced action requested: " + action);
+ }
+
+ Log.d("AppsMng", "Done");
+ }
+
+ @Override
+ public void onShutdown(Node node) {
+ Log.d("AppsMng", "Shutdown connected node...");
+ super.onShutdown(node);
+
+ // Required so we get reconnected the next time
+ this.connectedNode = null;
+ Log.d("AppsMng", "Done; shutdown apps manager node main");
+ }
+
+ @Override
+ public void onShutdownComplete(Node node) {
+ super.onShutdownComplete(node);
+ }
+
+ @Override
+ public void onError(Node node, Throwable throwable) {
+ super.onError(node, throwable);
+
+ Log.e("AppsMng", node.getName().toString() + " node error: " + throwable.getMessage());
+ failureCallback.handleFailure(node.getName().toString() + " node error: " + throwable.toString());
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/Constants.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/Constants.java
new file mode 100644
index 0000000..b85d868
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/Constants.java
@@ -0,0 +1,55 @@
+package com.github.rosjava.android_remocons.common_tools.rocon;
+
+import org.ros.internal.message.DefaultMessageFactory;
+import org.ros.internal.message.definition.MessageDefinitionReflectionProvider;
+
+import rocon_std_msgs.PlatformInfo;
+import rocon_std_msgs.Strings;
+
+/**
+ * General rocon android app constants and topic/parameter/service names
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class Constants {
+ public static final int NFC_SSID_FIELD_LENGTH = 16;
+ public static final int NFC_PASSWORD_FIELD_LENGTH = 16;
+ public static final int NFC_MASTER_HOST_FIELD_LENGTH = 16;
+ public static final int NFC_MASTER_PORT_FIELD_LENGTH = 2;
+ public static final int NFC_APP_HASH_FIELD_LENGTH = 4;
+ public static final int NFC_EXTRA_DATA_FIELD_LENGTH = 2;
+ public static final int NFC_APP_RECORD_FIELD_LENGTH = 56;
+ public static final int NFC_PAYLOAD_LENGTH = 56; // 16 + 16 + 16 + 2 + 4 + 2
+ public static final int NFC_ULTRALIGHT_C_MAX_LENGTH = 137;
+
+ // unique identifier to key string variables between activities.
+ static public final String ACTIVITY_SWITCHER_ID = "com.github.rosjava.android_remocons.common_tools.rocon.Constants";
+ static public final String ACTIVITY_ROCON_REMOCON = "com.github.rosjava.android_remocons.rocon_remocon.Remocon";
+
+ public static final rocon_std_msgs.PlatformInfo ANDROID_PLATFORM_INFO = makePlatformInfo();
+
+ /**
+ * Generate platform information, most specifically, the rocon uri string that is needed.
+ *
+ * @todo : this doesn't introspect the android phone...
+ *
+ * @return rocon_std_msgs.PlatformInfo : filled out platform information
+ */
+ private static PlatformInfo makePlatformInfo() {
+ MessageDefinitionReflectionProvider messageDefinitionProvider = new MessageDefinitionReflectionProvider();
+ DefaultMessageFactory messageFactory = new DefaultMessageFactory(messageDefinitionProvider);
+ PlatformInfo platformInfo = messageFactory.newFromType(PlatformInfo._TYPE);
+ // rocon:/hw/name/app_framework/os"
+ platformInfo.setUri("rocon:/"
+ + Strings.URI_WILDCARD + "/" + Strings.URI_WILDCARD + "/"
+ + Strings.APPLICATION_FRAMEWORK_INDIGO + "/"
+ + Strings.OS_ICE_CREAM_SANDWICH + "|" + Strings.OS_JELLYBEAN + "|"
+ + Strings.OS_CHROME
+ );
+ platformInfo.setVersion(Strings.ROCON_VERSION);
+ /* Not yet implemented */
+ /* platformInfo.setIcon() */
+
+ return platformInfo;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/InteractionsManager.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/InteractionsManager.java
new file mode 100644
index 0000000..6774177
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/rocon/InteractionsManager.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright (C) 2013, OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.rocon;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+import com.github.rosjava.android_remocons.common_tools.master.MasterId;
+
+import org.ros.address.InetAddressFactory;
+import org.ros.android.NodeMainExecutorService;
+import org.ros.exception.RosRuntimeException;
+import org.ros.exception.ServiceNotFoundException;
+import org.ros.internal.node.client.ParameterClient;
+import org.ros.internal.node.server.NodeIdentifier;
+import org.ros.namespace.GraphName;
+import org.ros.node.AbstractNodeMain;
+import org.ros.node.ConnectedNode;
+import org.ros.node.Node;
+import org.ros.node.NodeConfiguration;
+import org.ros.node.service.ServiceClient;
+import org.ros.node.service.ServiceResponseListener;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import rocon_interaction_msgs.GetInteraction;
+import rocon_interaction_msgs.GetInteractionRequest;
+import rocon_interaction_msgs.GetInteractionResponse;
+import rocon_interaction_msgs.GetInteractions;
+import rocon_interaction_msgs.GetInteractionsRequest;
+import rocon_interaction_msgs.GetInteractionsResponse;
+import rocon_interaction_msgs.RequestInteraction;
+import rocon_interaction_msgs.RequestInteractionRequest;
+import rocon_interaction_msgs.RequestInteractionResponse;
+
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.ANDROID_PLATFORM_INFO;
+
+/**
+ * This class has been derived from the original turtlebot robot app manager but now
+ * tuned for interactions.
+ *
+ * The original is quite messy, and this is not much better, so maybe needs extra refactoring.
+ * Also RobotAppsManager claims that it can be executed once, but I modified this to execute
+ * arbitrary times. It looks to work, but mechanism is a bit brittle.
+ * Apps manager implements the services and topics required to interact with the concert roles
+ * manager. Typically to use this class its a three step process:
+ *
+ * 1) instantiate and provide a general failure handler
+ * 2) provide a callback via one of the setupXXXservice methods
+ * 3) call the service you want; there's a public method per service
+ * 4) wait for service response.
+ *
+ * Essentially you are creating a node when creating an instance, and rosjava isolates each
+ * service/topic to each 'node'.
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class InteractionsManager extends AbstractNodeMain {
+ public interface FailureHandler {
+ /**
+ * Called on failure with a short description of why it failed, like "exception" or "timeout".
+ */
+ void handleFailure(String reason);
+ }
+
+ public enum Action {
+ NONE, GET_INTERACTIONS_FOR_ROLE, GET_INTERACTION_INFO, REQUEST_INTERACTION_USE
+ };
+
+ private String role;
+ private String interactionsNamespace;
+ private Action action = Action.NONE;
+ private rocon_interaction_msgs.Interaction app;
+ private int app_hash; // need this separately since headless starts don't have any app info.
+ private String remocon_name; // need this separately since headless starts don't have any app info.
+ private ConnectNodeThread connectThread;
+ private ConnectedNode connectedNode;
+ private NodeMainExecutorService nodeMainExecutorService;
+ private FailureHandler failureCallback;
+ private ServiceResponseListener requestServiceResponseListener;
+ private ServiceResponseListener getAppsServiceResponseListener;
+ private ServiceResponseListener appInfoServiceResponseListener;
+
+
+ public InteractionsManager(FailureHandler failureCallback) {
+
+ this.failureCallback = failureCallback;
+ }
+
+ /**
+ * This is done outside of the constructor to save it from having to be continually
+ * reinstantiated in the rocon remocons. That does mean it requires some black magic,
+ * i.e. construction -> init & setupXYZ -> execute
+ *
+ * @param interactionsNamespace
+ */
+ public void init(String interactionsNamespace) {
+ this.interactionsNamespace = interactionsNamespace;
+ }
+
+ public void setRemoconName(String remocon_name) {
+ this.remocon_name = remocon_name;
+ }
+
+
+ public void setupRequestService(ServiceResponseListener serviceResponseListener) {
+ this.requestServiceResponseListener = serviceResponseListener;
+ }
+
+ public void setupGetInteractionsService(ServiceResponseListener serviceResponseListener) {
+ this.getAppsServiceResponseListener = serviceResponseListener;
+ }
+
+ public void setupAppInfoService(ServiceResponseListener serviceResponseListener) {
+ this.appInfoServiceResponseListener = serviceResponseListener;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ shutdown(); // TODO not warrantied to be called, so user should call shutdown; finalize works at all in Android??? I think no....
+ }
+
+ public void shutdown() {
+ if (nodeMainExecutorService != null)
+ nodeMainExecutorService.shutdownNodeMain(this);
+ else
+ Log.i("InteractionsMng", "Shutting down an uninitialized apps manager");
+ }
+
+ public void getAppsForRole(final MasterId masterId, final String role) {
+ this.action = Action.GET_INTERACTIONS_FOR_ROLE;
+ this.role = role;
+
+ // If this is the first action requested, we need a connected node, what must be done in a different thread
+ // The requested action will be executed once we have a connected node (this object itself) in onStart method
+ if (this.connectedNode == null) {
+ Log.i("InteractionsMng", "First action requested (" + this.action + "). Starting node...");
+ new ConnectNodeThread(masterId).start();
+ }
+ else {
+ // But we don't need all this if we already executed an action, and so have a connected node. Anyway,
+ // we must execute any action asynchronously to avoid the android.os.NetworkOnMainThreadException
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ getAppsForRole();
+ return null;
+ }
+ }.execute(); // TODO: can we use this to incorporate a timeout to service calls?
+ }
+ }
+
+ public void requestAppUse(final MasterId masterId, final String role, final rocon_interaction_msgs.Interaction app) {
+ this.action = Action.REQUEST_INTERACTION_USE;
+ this.role = role;
+ this.app = app;
+ this.app_hash = app.getHash();
+
+ // If this is the first action requested, we need a connected node, what must be done in a different thread
+ // The requested action will be executed once we have a connected node (this object itself) in onStart method
+ if (this.connectedNode == null) {
+ Log.i("InteractionsMng", "First action requested (" + this.action + "). Starting node...");
+ new ConnectNodeThread(masterId).start();
+ }
+ else {
+ // But we don't need all this if we already executed an action, and so have a connected node. Anyway,
+ // we must execute any action asynchronously to avoid the android.os.NetworkOnMainThreadException
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ requestAppUse();
+ return null;
+ }
+ }.execute();
+ }
+ }
+
+ /**
+ * This is called from the headless launcher where the only thing it knows is the
+ * master id (for connection purposes and the hash stored in an nfc tag (or other).
+ * We need to save this hash for later service calls.
+ *
+ * @param masterId
+ * @param hash
+ */
+ public void getAppInfo(final MasterId masterId, final int hash) {
+ this.action = Action.GET_INTERACTION_INFO;
+ this.app_hash = hash;
+
+ // If this is the first action requested, we need a connected node, what must be done in a different thread
+ // The requested action will be executed once we have a connected node (this object itself) in onStart method
+ if (this.connectedNode == null) {
+ Log.i("InteractionsMng", "First action requested (" + this.action + "). Starting node...");
+ new ConnectNodeThread(masterId).start();
+ }
+ else {
+ // But we don't need all this if we already executed an action, and so have a connected node. Anyway,
+ // we must execute any action asynchronously to avoid the android.os.NetworkOnMainThreadException
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ getAppInfo();
+ return null;
+ }
+ }.execute(); // TODO: can we use this to incorporate a timeout to service calls?
+ }
+ }
+
+ private void getAppsForRole() {
+ // call get_roles_and_apps concert service
+ ServiceClient srvClient;
+ String serviceName = this.interactionsNamespace + "/get_interactions";
+
+ try {
+ Log.i("InteractionsMng", "List interactions service client created [" + serviceName + "]");
+ srvClient = connectedNode.newServiceClient(serviceName, GetInteractions._TYPE);
+ } catch (ServiceNotFoundException e) {
+ Log.i("InteractionsMng", "List interactions service not found [" + serviceName + "]");
+ throw new RosRuntimeException(e); // TODO we should recover from this calling onFailure on listener
+ }
+ final GetInteractionsRequest request = srvClient.newMessage();
+
+ request.getRoles().add(role);
+ request.setUri(ANDROID_PLATFORM_INFO.getUri());
+
+ srvClient.call(request, getAppsServiceResponseListener);
+ Log.i("InteractionsMng", "List interactions service call done [" + serviceName + "]");
+ }
+
+ private void requestAppUse() {
+ // call request_interaction concert service
+ ServiceClient srvClient;
+ String serviceName = this.interactionsNamespace + "/request_interaction";
+ try {
+ Log.i("InteractionsMng", "Request app service client created [" + serviceName + "]");
+ srvClient = connectedNode.newServiceClient(serviceName, RequestInteraction._TYPE);
+ } catch (ServiceNotFoundException e) {
+ Log.i("InteractionsMng", "Request app service not found [" + serviceName + "]");
+ throw new RosRuntimeException(e); // TODO we should recover from this calling onFailure on listener
+ }
+ final RequestInteractionRequest request = srvClient.newMessage();
+
+ request.setRemocon(this.remocon_name);
+ request.setHash(this.app.getHash());
+
+ srvClient.call(request, requestServiceResponseListener);
+ Log.i("InteractionsMng", "Request app service call done [" + serviceName + "]");
+ }
+
+ private void getAppInfo() {
+ // call get_app concert service
+ ServiceClient srvClient;
+ String serviceName = this.interactionsNamespace + "/get_interaction";
+ try {
+ Log.i("InteractionsMng", "Get app info service client created [" + serviceName + "]");
+ srvClient = connectedNode.newServiceClient(serviceName, GetInteraction._TYPE);
+ } catch (ServiceNotFoundException e) {
+ Log.i("InteractionsMng", "Get app info not found [" + serviceName + "]");
+ throw new RosRuntimeException(e); // TODO we should recover from this calling onFailure on listener
+ }
+ final GetInteractionRequest request = srvClient.newMessage();
+ request.setHash(this.app_hash);
+
+ srvClient.call(request, appInfoServiceResponseListener);
+ Log.i("InteractionsMng", "Get app info service call done [" + serviceName + "]");
+ }
+
+ /**
+ * Start a thread to get a node connected with the given masterId. Returns immediately
+ * TODO: what happens if the thread is already running??? kill it first and then start a new one?
+ */
+ private class ConnectNodeThread extends Thread {
+ private MasterId masterId;
+
+ public ConnectNodeThread(MasterId masterId) {
+ this.masterId = masterId;
+ setDaemon(true);
+ setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ failureCallback.handleFailure("exception: " + ex.getMessage());
+ }
+ });
+ }
+
+ @Override
+ public void run() {
+ try {
+ URI concertUri = new URI(masterId.getMasterUri());
+
+ // Check if the master exists by looking for a master parameter
+ // getParam throws when it can't find the parameter (DJS: what does it throw?).
+ // Could get it to look for a hardcoded rocon parameter for extra guarantees
+ // (e.g. /rocon/version) however we'd still have to do some checking below
+ // when the info is there but interactions not.
+ ParameterClient paramClient = new ParameterClient(
+ NodeIdentifier.forNameAndUri("/concert_checker", concertUri.toString()), concertUri);
+ String name = (String) paramClient.getParam(GraphName.of("/rosversion")).getResult();
+ Log.i("Remocon", "Concert " + name + " found; retrieve additional information");
+
+ nodeMainExecutorService = new NodeMainExecutorService();
+ NodeConfiguration nodeConfiguration = NodeConfiguration.newPublic(
+ InetAddressFactory.newNonLoopback().getHostAddress(), concertUri);
+ nodeMainExecutorService.execute(InteractionsManager.this, nodeConfiguration.setNodeName("apps_manager_node"));
+
+ } catch (URISyntaxException e) {
+ Log.i("InteractionsMng", "invalid concert URI [" + masterId.getMasterUri() + "][" + e.toString() + "]");
+ failureCallback.handleFailure("invalid concert URI");
+ } catch (RuntimeException e) {
+ // thrown if concert could not be found in the getParam call (from java.net.ConnectException)
+ Log.i("InteractionsMng", "could not find concert [" + masterId.getMasterUri() + "][" + e.toString() + "]");
+ failureCallback.handleFailure(e.toString());
+ } catch (Throwable e) {
+ Log.i("InteractionsMng", "exception while creating node in concert checker for URI " + masterId.getMasterUri(), e);
+ failureCallback.handleFailure(e.toString());
+ }
+ }
+ }
+
+ @Override
+ public GraphName getDefaultNodeName() {
+ return null;
+ }
+
+ /**
+ * Node started by NodeMainExecutor.execute(). Execute the requested action.
+ *
+ * @param connectedNode
+ */
+ @Override
+ public void onStart(final ConnectedNode connectedNode) {
+ if (this.connectedNode != null) {
+ Log.e("InteractionsMng", "App manager re-started before previous shutdown; ignoring...");
+ return;
+ }
+
+ this.connectedNode = connectedNode;
+
+ Log.i("InteractionsMng", "onStart() - " + action);
+
+ switch (action) {
+ case NONE:
+ Log.i("InteractionsMng", "Node started without specifying an action");
+ break;
+ case REQUEST_INTERACTION_USE:
+ requestAppUse();
+ break;
+ case GET_INTERACTIONS_FOR_ROLE:
+ getAppsForRole();
+ break;
+ case GET_INTERACTION_INFO:
+ getAppInfo();
+ break;
+ default:
+ Log.i("InteractionsMng", "Unrecognised action requested: " + action);
+ }
+
+ Log.i("InteractionsMng", "Done");
+ }
+
+ @Override
+ public void onShutdown(Node node) {
+ Log.i("InteractionsMng", "Shutdown connected node...");
+ super.onShutdown(node);
+
+ // Required so we get reconnected the next time
+ this.connectedNode = null;
+ Log.i("InteractionsMng", "Done; shutdown apps manager node main");
+ }
+
+ @Override
+ public void onShutdownComplete(Node node) {
+ super.onShutdownComplete(node);
+ }
+
+ @Override
+ public void onError(Node node, Throwable throwable) {
+ super.onError(node, throwable);
+
+ Log.e("InteractionsMng", node.getName().toString() + " node error: " + throwable.getMessage());
+ failureCallback.handleFailure(node.getName().toString() + " node error: " + throwable.toString());
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/system/Util.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/system/Util.java
new file mode 100644
index 0000000..c3a9eb1
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/system/Util.java
@@ -0,0 +1,114 @@
+package com.github.rosjava.android_remocons.common_tools.system;
+
+import java.security.InvalidParameterException;
+
+public class Util {
+
+ // Hex help
+ private static final byte[] HEX_CHAR_TABLE = { (byte) '0', (byte) '1',
+ (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6',
+ (byte) '7', (byte) '8', (byte) '9', (byte) 'A', (byte) 'B',
+ (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F' };
+
+ public Util() {
+ // TODO Auto-generated constructor stub
+ }
+
+ public static String getHexString(byte[] raw, int len) {
+ byte[] hex = new byte[3 * len];
+ int index = 0;
+ int pos = 0;
+
+ for (byte b : raw) {
+ if (pos >= len)
+ break;
+
+ pos++;
+ int v = b & 0xFF;
+ hex[index++] = HEX_CHAR_TABLE[v >>> 4];
+ hex[index++] = HEX_CHAR_TABLE[v & 0xF];
+ hex[index++] = ' ';
+ }
+
+ return new String(hex);
+ }
+
+ public static byte[] concat(byte[]... arrays) {
+ int length = 0;
+ for (byte[] array : arrays) {
+ length += array.length;
+ }
+
+ byte[] result = new byte[length];
+ int pos = 0;
+ for (byte[] array : arrays) {
+ System.arraycopy(array, 0, result, pos, array.length);
+ pos += array.length;
+ }
+
+ return result;
+ }
+
+ public static String toString(byte[] input, int offset, int count) {
+ if ((offset + count) > input.length)
+ throw new ArrayIndexOutOfBoundsException("Requested chunk exceeds byte array limits");
+
+ byte[] result = new byte[count];
+ for (int i = 0; i < count; i++)
+ result[i] = input[offset + i];
+
+ return new String(result);
+ }
+
+ public static short toShort(byte[] input, int offset) {
+ if ((offset + 2) > input.length)
+ throw new ArrayIndexOutOfBoundsException("Requested chunk exceeds byte array limits");
+
+ return (short) (input[offset + 1] & 0xFF |
+ (input[offset + 0] & 0xFF) << 8);
+ }
+
+
+ public static int toInteger(byte[] input, int offset) {
+ if ((offset + 4) > input.length)
+ throw new ArrayIndexOutOfBoundsException("Requested chunk exceeds byte array limits");
+
+ return input[offset + 3] & 0xFF |
+ (input[offset + 2] & 0xFF) << 8 |
+ (input[offset + 1] & 0xFF) << 16 |
+ (input[offset + 0] & 0xFF) << 24;
+ }
+
+ public static byte[] toFixSizeBytes(String input, int length, byte padding) {
+ if (input.length() > length)
+ throw new InvalidParameterException(length + "exceeds limit in "
+ + (input.length() - length) + " chars");
+
+ byte[] result = new byte[length];
+ byte[] source = input.getBytes();
+ for (int i = 0; i < length; i++)
+ result[i] = i < source.length ? source[i] : padding;
+
+ return result;
+ }
+
+ public static byte[] toBytes(int input)
+ {
+ byte[] result = new byte[4];
+ result[0] = (byte) (input >> 24);
+ result[1] = (byte) (input >> 16);
+ result[2] = (byte) (input >> 8);
+ result[3] = (byte) (input);
+
+ return result;
+ }
+
+ public static byte[] toBytes(short input)
+ {
+ byte[] result = new byte[2];
+ result[0] = (byte) (input >> 8);
+ result[1] = (byte) (input);
+
+ return result;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/system/WifiChecker.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/system/WifiChecker.java
new file mode 100644
index 0000000..bf76f11
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/system/WifiChecker.java
@@ -0,0 +1,295 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.system;
+
+import android.net.wifi.ScanResult;
+import android.net.wifi.SupplicantState;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+
+import com.github.rosjava.android_remocons.common_tools.master.MasterId;
+
+import java.util.List;
+
+/**
+ * Threaded WiFi checker. Checks and tests if the WiFi is configured properly and if not, connects to the correct network.
+ *
+ * @author pratkanis@willowgarage.com
+ */
+public class WifiChecker {
+ public interface SuccessHandler {
+ /**
+ * Called on success with a description of the master that got checked.
+ */
+ void handleSuccess();
+ }
+
+ public interface FailureHandler {
+ /**
+ * Called on failure with a short description of why it failed, like
+ * "exception" or "timeout".
+ */
+ void handleFailure(String reason);
+ }
+
+ public interface ReconnectionHandler {
+ /**
+ * Called to prompt the user to connect to a different network
+ */
+ boolean doReconnection(String from, String to);
+ }
+
+ private CheckerThread checkerThread;
+ private SuccessHandler foundWiFiCallback;
+ private FailureHandler failureCallback;
+ private ReconnectionHandler reconnectionCallback;
+
+ /**
+ * Constructor. Should not take any time.
+ */
+ public WifiChecker(SuccessHandler foundWiFiCallback, FailureHandler failureCallback, ReconnectionHandler reconnectionCallback) {
+ this.foundWiFiCallback = foundWiFiCallback;
+ this.failureCallback = failureCallback;
+ this.reconnectionCallback = reconnectionCallback;
+ }
+
+ /**
+ * Start the checker thread with the given masterId. If the thread is
+ * already running, kill it first and then start anew. Returns immediately.
+ */
+ public void beginChecking(MasterId masterId, WifiManager manager) {
+ stopChecking();
+ //If there's no wifi tag in the master id, skip this step
+ if (masterId.getWifi() == null) {
+ foundWiFiCallback.handleSuccess();
+ return;
+ }
+ checkerThread = new CheckerThread(masterId, manager);
+ checkerThread.start();
+ }
+
+ /**
+ * Stop the checker thread.
+ */
+ public void stopChecking() {
+ if (checkerThread != null && checkerThread.isAlive()) {
+ checkerThread.interrupt();
+ }
+ }
+
+ public static boolean wifiValid(MasterId masterId, WifiManager wifiManager) {
+ WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+ if (masterId.getWifi() == null) { //Does not matter what wifi network, always valid.
+ return true;
+ }
+ if (wifiManager.isWifiEnabled()) {
+ if (wifiInfo != null) {
+ Log.d("WiFiChecker", "WiFi Info: " + wifiInfo.toString() + " IP " + wifiInfo.getIpAddress());
+ if (wifiInfo.getSSID() != null && wifiInfo.getIpAddress() != 0
+ && wifiInfo.getSupplicantState() == SupplicantState.COMPLETED) {
+ String master_SSID = "\"" + masterId.getWifi() + "\"";
+ if (wifiInfo.getSSID().equals(master_SSID)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public String getScanResultSecurity(ScanResult scanResult) {
+ final String cap = scanResult.capabilities;
+ final String[] securityModes = { "WEP", "PSK", "EAP" };
+ for (int i = securityModes.length - 1; i >= 0; i--) {
+ if (cap.contains(securityModes[i])) {
+ return securityModes[i];
+ }
+ }
+ return "OPEN";
+ }
+
+ private class CheckerThread extends Thread {
+ private MasterId masterId;
+ private WifiManager wifiManager;
+
+ public CheckerThread(MasterId masterId, WifiManager wifi) {
+ this.masterId = masterId;
+ this.wifiManager = wifi;
+ setDaemon(true);
+ // don't require callers to explicitly kill all the old checker threads.
+ setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
+ @Override
+ public void uncaughtException(Thread thread, Throwable ex) {
+ failureCallback.handleFailure("exception: " + ex.getMessage());
+ }
+ });
+ }
+
+ private boolean wifiValid() {
+ return WifiChecker.wifiValid(masterId, wifiManager);
+ }
+
+ @Override
+ public void run() {
+ try {
+ if (wifiValid()) {
+ foundWiFiCallback.handleSuccess();
+ } else if (reconnectionCallback.doReconnection(wifiManager.getConnectionInfo().getSSID(), masterId.getWifi())) {
+ Log.d("WiFiChecker", "Wait for networking");
+ wifiManager.setWifiEnabled(true);
+ int i = 0;
+ while (i < 30 && !wifiManager.isWifiEnabled()) {
+ Log.d("WiFiChecker", "Waiting for WiFi enable");
+ Thread.sleep(1000L);
+ i++;
+ }
+ if (!wifiManager.isWifiEnabled()) {
+ failureCallback.handleFailure("Un-able to enable to WiFi");
+ return;
+ }
+ int n = -1;
+ int priority = -1;
+ WifiConfiguration wc = null;
+ String SSID = "\"" + masterId.getWifi() + "\"";
+ for (WifiConfiguration test : wifiManager.getConfiguredNetworks()) {
+ Log.d("WiFiChecker", "WIFI " + test.toString());
+ if (test.priority > priority) {
+ priority = test.priority;
+ }
+ if (test.SSID.equals(SSID)) {
+ n = test.networkId;
+ wc = test;
+ }
+ }
+ if (wc != null) {
+ if (wc.priority != priority) {
+ wc.priority = priority + 1;
+ }
+ wc.status = WifiConfiguration.Status.DISABLED;
+ wifiManager.updateNetwork(wc);
+ }
+
+ //Add new network.
+ if (n == -1) {
+ Log.d("WiFiChecker", "WIFI Unknown");
+
+ List scanResultList = null;
+ Log.d("WiFiChecker", "WIFI Scan Start");
+ if(wifiManager.startScan()){
+ Log.d("WiFiChecker", "WIFI Scan Success");
+ }
+ else{
+ Log.d("WiFiChecker", "WIFI Scan Failure");
+ failureCallback.handleFailure("wifi scan fail");
+ }
+
+ scanResultList = wifiManager.getScanResults();
+ i = 0;
+ while (i < 30 && scanResultList.size()==0) {
+ scanResultList = wifiManager.getScanResults();
+ Log.d("WiFiChecker", "Waiting for getting wifi list");
+ Thread.sleep(1000L);
+ i++;
+ }
+ wc = new WifiConfiguration();
+
+ for (ScanResult result : scanResultList) {
+
+ if (result.SSID.equals(masterId.getWifi())) {
+ String securityMode = getScanResultSecurity(result);
+ Log.d("WiFiChecker", "WIFI mode: " + securityMode);
+
+ wc.SSID = "\"" + masterId.getWifi() + "\"";
+ if (securityMode.equalsIgnoreCase("OPEN")) {
+ wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ } else if (securityMode.equalsIgnoreCase("WEP")) {
+ wc.wepKeys[0] = "\"" + masterId.getWifiPassword() + "\"";
+ wc.wepTxKeyIndex = 0;
+ wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
+ } else {
+ wc.preSharedKey = "\"" + masterId.getWifiPassword() + "\"";
+ wc.hiddenSSID = true;
+ wc.status = WifiConfiguration.Status.ENABLED;
+ wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
+ wc.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
+ wc.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+ wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
+ wc.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
+ wc.allowedProtocols.set(WifiConfiguration.Protocol.RSN);
+ wc.allowedProtocols.set(WifiConfiguration.Protocol.WPA);
+ }
+ n = wifiManager.addNetwork(wc);
+ break;
+
+ }
+ }
+ }
+ Log.d("WiFiChecker", "add Network returned " + n);
+ if (n == -1) {
+ failureCallback.handleFailure("Failed to add the WiFi configure");
+ }
+
+ //Connect to the network
+ boolean b = wifiManager.enableNetwork(n, true);
+ Log.d("WiFiChecker", "enableNetwork returned " + b);
+ if (b) {
+ wifiManager.reconnect();
+ Log.d("WiFiChecker", "Wait for wifi network");
+ i = 0;
+ while (i < 3 && !wifiValid()) {
+ Log.d("WiFiChecker", "Waiting for network: " + i + " " + wifiManager.getWifiState());
+ Thread.sleep(3000L);
+ i++;
+ }
+ if (wifiValid()) {
+ foundWiFiCallback.handleSuccess();
+ } else {
+ failureCallback.handleFailure("WiFi connection timed out");
+ }
+ }
+ } else {
+ failureCallback.handleFailure("Wrong WiFi network");
+ }
+ } catch (Throwable ex) {
+ Log.e("RosAndroid", "Exception while searching for WiFi for "
+ + masterId.getWifi(), ex);
+ failureCallback.handleFailure(ex.toString());
+ }
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/DiscoveryAdapter.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/DiscoveryAdapter.java
new file mode 100644
index 0000000..122bf6b
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/DiscoveryAdapter.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2013 Yujin Robot.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.zeroconf;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Checkable;
+import android.widget.CheckedTextView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.github.rosjava.android_remocons.common_tools.R;
+import com.github.rosjava.zeroconf_jmdns_suite.jmdns.DiscoveredService;
+
+import java.util.ArrayList;
+
+
+public class DiscoveryAdapter extends ArrayAdapter {
+
+ /**
+ * This class is necessary to work a checkbox well
+ */
+
+ public class CustomCheckBox extends LinearLayout implements Checkable {
+ private CheckedTextView checkbox;
+
+ public CustomCheckBox(Context context) {
+ super(context);
+
+ View view = ((LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
+ .inflate(R.layout.zeroconf_master_item, this, false);
+ checkbox = (CheckedTextView) view.findViewById(R.id.service_detail);
+ addView(view);
+ }
+
+
+ @Override
+ public void setChecked(boolean checked) {
+ checkbox.setChecked(checked);
+
+ }
+
+ @Override
+ public boolean isChecked() {
+ return checkbox.isChecked();
+ }
+
+ @Override
+ public void toggle() {
+ setChecked(!isChecked());
+
+ }
+ }
+
+
+ private final Context context;
+ private ArrayList discoveredServices;
+ private String targetServiceName;
+ private int targetServiceDrawable;
+ private int otherServicesDrawable;
+
+ public DiscoveryAdapter(Context context, ArrayList discoveredServices,
+ String targetServiceName, int targetServiceDrawable, int otherServicesDrawable) {
+ super(context, R.layout.zeroconf_master_item, discoveredServices); // pass the list to the super
+ this.context = context;
+ this.discoveredServices = discoveredServices; // keep a pointer locally so we can play with it
+ this.targetServiceName = targetServiceName;
+ this.targetServiceDrawable = targetServiceDrawable;
+ this.otherServicesDrawable = otherServicesDrawable;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+ if (v == null) {
+ v = new CustomCheckBox(getContext());
+ }
+
+ DiscoveredService discovered_service = discoveredServices.get(position);
+ if (discovered_service != null) {
+ TextView tt = (TextView) v.findViewById(R.id.service_name);
+ TextView bt = (TextView) v.findViewById(R.id.service_detail);
+ if (tt != null) {
+ tt.setText(discovered_service.name);
+ }
+ if (bt != null) {
+ String result = "";
+ for (String ipv4_address : discovered_service.ipv4_addresses) {
+ if (result.equals("")) {
+ result += ipv4_address + ":" + discovered_service.port;
+ } else {
+ result += "\n" + ipv4_address + ":" + discovered_service.port;
+ }
+ }
+ for (String ipv6_address : discovered_service.ipv6_addresses) {
+ if (result.equals("")) {
+ result += ipv6_address + ":" + discovered_service.port;
+ } else {
+ result += "\n" + ipv6_address + ":" + discovered_service.port;
+ }
+ }
+ bt.setText(result);
+ }
+ ImageView im = (ImageView) v.findViewById(R.id.icon);
+ if (im != null) {
+ if (discovered_service.type.indexOf("_" + targetServiceName + "._tcp") != -1 ||
+ discovered_service.type.indexOf("_" + targetServiceName + "._udp") != -1) {
+ im.setImageDrawable(context.getResources().getDrawable(targetServiceDrawable));
+ } else {
+ im.setImageDrawable(context.getResources().getDrawable(otherServicesDrawable));
+ }
+ }
+ }
+ return v;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/DiscoveryHandler.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/DiscoveryHandler.java
new file mode 100644
index 0000000..d7bb014
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/DiscoveryHandler.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2013 Yujin Robot.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.zeroconf;
+
+import android.annotation.SuppressLint;
+import android.os.AsyncTask;
+
+import com.github.rosjava.zeroconf_jmdns_suite.jmdns.DiscoveredService;
+import com.github.rosjava.zeroconf_jmdns_suite.jmdns.ZeroconfDiscoveryHandler;
+
+import java.util.ArrayList;
+
+/**
+ * This class is the callback handler for services being listened for
+ * by the jmdns zeroconf class.
+ *
+ * Usually we should do a bit of checking to make sure that any
+ * service isn't getting repeated on another interface, but for
+ * now we can assume your android has only the one interface so that
+ * we handle each added/resolved/removed as a unique entry.
+ */
+public class DiscoveryHandler implements ZeroconfDiscoveryHandler {
+
+ /**
+ * ******************
+ * Tasks
+ * ******************
+ */
+ @SuppressLint("NewApi")
+ private class ServiceAddedTask extends AsyncTask {
+
+ @SuppressLint("NewApi")
+ protected Void doInBackground(DiscoveredService... services) {
+ if (services.length == 1) {
+ DiscoveredService service = services[0];
+ String result = "[+] Service added: " + service.name + "." + service.type + "." + service.domain + ".";
+ publishProgress(result);
+ } else {
+ publishProgress("Error - ServiceAddedTask::doInBackground received #services != 1");
+ }
+ return null;
+ }
+
+ }
+
+ @SuppressLint("NewApi")
+ private class ServiceResolvedTask extends AsyncTask {
+
+ @SuppressLint("NewApi")
+ protected DiscoveredService doInBackground(DiscoveredService... services) {
+ if (services.length == 1) {
+ DiscoveredService discovered_service = services[0];
+ String result = "[=] Service resolved: " + discovered_service.name + "." + discovered_service.type + "." + discovered_service.domain + ".\n";
+ result += " Port: " + discovered_service.port;
+ for (String address : discovered_service.ipv4_addresses) {
+ result += "\n Address: " + address;
+ }
+ for (String address : discovered_service.ipv6_addresses) {
+ result += "\n Address: " + address;
+ }
+ publishProgress(result);
+ return discovered_service;
+ } else {
+ publishProgress("Error - ServiceAddedTask::doInBackground received #services != 1");
+ }
+ return null;
+ }
+
+ @SuppressLint("NewApi")
+ protected void onPostExecute(DiscoveredService discovered_service) {
+ // add to the content and notify the list view if its a new service
+ if (discovered_service != null) {
+ int index = 0;
+ for (DiscoveredService s : discovered_services) {
+ if (s.name.equals(discovered_service.name)) {
+ break;
+ } else {
+ ++index;
+ }
+ }
+ if (index == discovered_services.size()) {
+ discovered_services.add(discovered_service);
+ discovery_adapter.notifyDataSetChanged();
+ } else {
+ android.util.Log.i("zeroconf", "Tried to add an existing service (fix this)");
+ }
+ }
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private class ServiceRemovedTask extends AsyncTask {
+
+ @SuppressLint("NewApi")
+ protected DiscoveredService doInBackground(DiscoveredService... services) {
+ if (services.length == 1) {
+ DiscoveredService discovered_service = services[0];
+ String result = "[-] Service removed: " + discovered_service.name + "." + discovered_service.type + "." + discovered_service.domain + ".\n";
+ result += " Port: " + discovered_service.port;
+ publishProgress(result);
+ return discovered_service;
+ } else {
+ publishProgress("Error - ServiceAddedTask::doInBackground received #services != 1");
+ }
+ return null;
+ }
+
+
+ protected void onPostExecute(DiscoveredService discovered_service) {
+ // remove service from storage and notify list view
+ if (discovered_service != null) {
+ int index = 0;
+ for (DiscoveredService s : discovered_services) {
+ if (s.name.equals(discovered_service.name)) {
+ break;
+ } else {
+ ++index;
+ }
+ }
+ if (index != discovered_services.size()) {
+ discovered_services.remove(index);
+ discovery_adapter.notifyDataSetChanged();
+ } else {
+ android.util.Log.i("zeroconf", "Tried to remove a non-existant service");
+ }
+ }
+ }
+ }
+
+ /**
+ * ******************
+ * Variables
+ * ******************
+ */
+ private ArrayList discovered_services;
+ private DiscoveryAdapter discovery_adapter;
+
+ /**
+ * ******************
+ * Constructors
+ * ******************
+ */
+ public DiscoveryHandler(DiscoveryAdapter discovery_adapter, ArrayList discovered_services) {
+ this.discovery_adapter = discovery_adapter;
+ this.discovered_services = discovered_services;
+ }
+
+ /**
+ * ******************
+ * Callbacks
+ * ******************
+ */
+ public void serviceAdded(DiscoveredService service) {
+ new ServiceAddedTask().execute(service);
+ }
+
+ public void serviceRemoved(DiscoveredService service) {
+ new ServiceRemovedTask().execute(service);
+ }
+
+ public void serviceResolved(DiscoveredService service) {
+ new ServiceResolvedTask().execute(service);
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/DiscoverySetup.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/DiscoverySetup.java
new file mode 100644
index 0000000..7f90503
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/DiscoverySetup.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 Yujin Robot.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.zeroconf;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.os.AsyncTask;
+
+import com.github.rosjava.zeroconf_jmdns_suite.jmdns.Zeroconf;
+
+
+/**
+ * Configures the zeroconf class for discovery of services.
+ */
+
+public class DiscoverySetup extends AsyncTask {
+
+ private ProgressDialog commencing_dialog;
+ private final Context context;
+
+ public DiscoverySetup(Context context) {
+ this.context = context;
+ }
+
+ protected Void doInBackground(Zeroconf... zeroconfs) {
+ if (zeroconfs.length == 1) {
+ Zeroconf zconf = zeroconfs[0];
+ android.util.Log.i("zeroconf", "*********** Discovery Commencing **************");
+
+ zconf.addListener("_ros-master._tcp", "local");
+ zconf.addListener("_ros-master._udp", "local");
+ zconf.addListener("_concert-master._tcp", "local");
+ zconf.addListener("_concert-master._udp", "local");
+
+ } else {
+ android.util.Log.i("zeroconf", "Error - DiscoveryTask::doInBackground received #zeroconfs != 1");
+ }
+ return null;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/Logger.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/Logger.java
new file mode 100644
index 0000000..315738a
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/Logger.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2013 Yujin Robot.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.zeroconf;
+
+import com.github.rosjava.zeroconf_jmdns_suite.jmdns.ZeroconfLogger;
+
+public class Logger implements ZeroconfLogger {
+
+ public void println(String msg) {
+ android.util.Log.i("zeroconf", msg);
+ }
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/MasterSearcher.java b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/MasterSearcher.java
new file mode 100644
index 0000000..c679f08
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/java/com/github/rosjava/android_remocons/common_tools/zeroconf/MasterSearcher.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2013 Yujin Robot.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.github.rosjava.android_remocons.common_tools.zeroconf;
+
+import android.content.Context;
+import android.widget.ListView;
+
+import com.github.rosjava.zeroconf_jmdns_suite.jmdns.DiscoveredService;
+import com.github.rosjava.zeroconf_jmdns_suite.jmdns.Zeroconf;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+
+public class MasterSearcher {
+
+ private Zeroconf zeroconf;
+ private ArrayList discoveredMasters;
+ private DiscoveryAdapter discoveryAdapter;
+ private DiscoveryHandler discoveryHandler;
+ private Logger logger;
+
+ public MasterSearcher(Context context, final ListView listView,
+ String targetServiceName, int targetServiceDrawable, int otherServicesDrawable) {
+
+ discoveredMasters = new ArrayList();
+
+ discoveryAdapter = new DiscoveryAdapter(context, discoveredMasters,
+ targetServiceName, targetServiceDrawable, otherServicesDrawable);
+ listView.setAdapter(discoveryAdapter);
+ listView.setItemsCanFocus(false);
+ listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+
+ logger = new Logger();
+ zeroconf = new Zeroconf(logger);
+ discoveryHandler = new DiscoveryHandler(discoveryAdapter, discoveredMasters);
+ zeroconf.setDefaultDiscoveryCallback(discoveryHandler);
+
+ new DiscoverySetup(context).execute(zeroconf);
+ }
+
+ public void shutdown() {
+ try {
+ zeroconf.shutdown();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/res/drawable/ros_master.png b/src/AndroidApp/android_remocons/common_tools/src/main/res/drawable/ros_master.png
new file mode 100644
index 0000000..f0c065c
Binary files /dev/null and b/src/AndroidApp/android_remocons/common_tools/src/main/res/drawable/ros_master.png differ
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/default_dashboard.xml b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/default_dashboard.xml
new file mode 100644
index 0000000..71a06c9
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/default_dashboard.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/main.xml b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/main.xml
new file mode 100644
index 0000000..f5e367c
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/main.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/nfc_tag_scan.xml b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/nfc_tag_scan.xml
new file mode 100644
index 0000000..7c0fade
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/nfc_tag_scan.xml
@@ -0,0 +1,23 @@
+
+
+
+
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/zeroconf_master_item.xml b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/zeroconf_master_item.xml
new file mode 100644
index 0000000..2f38919
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/zeroconf_master_item.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/zeroconf_master_list.xml b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/zeroconf_master_list.xml
new file mode 100644
index 0000000..f988ddf
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/res/layout/zeroconf_master_list.xml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/common_tools/src/main/res/values/strings.xml b/src/AndroidApp/android_remocons/common_tools/src/main/res/values/strings.xml
new file mode 100644
index 0000000..045e125
--- /dev/null
+++ b/src/AndroidApp/android_remocons/common_tools/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/AndroidApp/android_remocons/external_libraries/Readme.rst b/src/AndroidApp/android_remocons/external_libraries/Readme.rst
new file mode 100644
index 0000000..c3d4d5a
--- /dev/null
+++ b/src/AndroidApp/android_remocons/external_libraries/Readme.rst
@@ -0,0 +1,10 @@
+External Jars
+=============
+
+What libraries we have here are usually patched libraries for android. You can't find them
+in maven central (e.g. vanilla snake_yaml is on maven central, doesn't run on android because
+it uses introspection methods that the dalvik virtual machine does not support).
+
+It would be great to get these out to a maven repository of our own.
+
+
diff --git a/src/AndroidApp/android_remocons/external_libraries/snakeyaml-1.10-android.jar b/src/AndroidApp/android_remocons/external_libraries/snakeyaml-1.10-android.jar
new file mode 100644
index 0000000..ec6b967
Binary files /dev/null and b/src/AndroidApp/android_remocons/external_libraries/snakeyaml-1.10-android.jar differ
diff --git a/src/AndroidApp/android_remocons/gradle.properties b/src/AndroidApp/android_remocons/gradle.properties
new file mode 100644
index 0000000..ffea3dc
--- /dev/null
+++ b/src/AndroidApp/android_remocons/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.daemon=true
+org.gradle.parallel=true
+android.enableBuildCache=false
+
diff --git a/src/AndroidApp/android_remocons/gradle/wrapper/gradle-wrapper.jar b/src/AndroidApp/android_remocons/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..c97a8bd
Binary files /dev/null and b/src/AndroidApp/android_remocons/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/src/AndroidApp/android_remocons/gradle/wrapper/gradle-wrapper.properties b/src/AndroidApp/android_remocons/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a14ec8c
--- /dev/null
+++ b/src/AndroidApp/android_remocons/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Mar 15 19:31:11 CST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/src/AndroidApp/android_remocons/gradlew b/src/AndroidApp/android_remocons/gradlew
new file mode 100644
index 0000000..91a7e26
--- /dev/null
+++ b/src/AndroidApp/android_remocons/gradlew
@@ -0,0 +1,164 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched.
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+fi
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >&-
+APP_HOME="`pwd -P`"
+cd "$SAVED" >&-
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/src/AndroidApp/android_remocons/gradlew.bat b/src/AndroidApp/android_remocons/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/src/AndroidApp/android_remocons/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/AndroidApp/android_remocons/library/.gitignore b/src/AndroidApp/android_remocons/library/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/src/AndroidApp/android_remocons/library/build.gradle b/src/AndroidApp/android_remocons/library/build.gradle
new file mode 100644
index 0000000..d9a31d7
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/build.gradle
@@ -0,0 +1,45 @@
+apply plugin: 'com.android.library'
+group='com.yalantis:sidemenu'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion "26.0.2"
+
+ defaultConfig {
+ minSdkVersion 15
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+dependencies {
+ compile 'com.android.support:appcompat-v7:23.1.1'
+}
+
+task androidJavadocs(type: Javadoc) {
+ source = android.sourceSets.main.java.sourceFiles
+}
+
+task androidJavadocsJar(type: Jar) {
+ classifier = 'javadoc'
+ //basename = artifact_id
+ from androidJavadocs.destinationDir
+}
+
+task androidSourcesJar(type: Jar) {
+ classifier = 'sources'
+ //basename = artifact_id
+ from android.sourceSets.main.java.sourceFiles
+}
+
+artifacts {
+ //archives packageReleaseJar
+ archives androidSourcesJar
+ archives androidJavadocsJar
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/library/gradle.properties b/src/AndroidApp/android_remocons/library/gradle.properties
new file mode 100644
index 0000000..989904b
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/gradle.properties
@@ -0,0 +1,3 @@
+POM_NAME=Side Menu
+POM_ARTIFACT_ID=sidemenu
+POM_PACKAGING=aar
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/library/proguard-rules.pro b/src/AndroidApp/android_remocons/library/proguard-rules.pro
new file mode 100644
index 0000000..e952e52
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:\soft\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/src/AndroidApp/android_remocons/library/src/androidTest/java/yalantis/com/sidemenu/ApplicationTest.java b/src/AndroidApp/android_remocons/library/src/androidTest/java/yalantis/com/sidemenu/ApplicationTest.java
new file mode 100644
index 0000000..ad4803b
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/androidTest/java/yalantis/com/sidemenu/ApplicationTest.java
@@ -0,0 +1,13 @@
+package yalantis.com.sidemenu;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * Testing Fundamentals
+ */
+public class ApplicationTest extends ApplicationTestCase {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/library/src/main/AndroidManifest.xml b/src/AndroidApp/android_remocons/library/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ecacbdd
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/animation/FlipAnimation.java b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/animation/FlipAnimation.java
new file mode 100644
index 0000000..b790daf
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/animation/FlipAnimation.java
@@ -0,0 +1,55 @@
+package yalantis.com.sidemenu.animation;
+
+import android.graphics.Camera;
+import android.graphics.Matrix;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+
+/**
+ * Created by Konstantin on 22.12.2014.
+ */
+public class FlipAnimation extends Animation {
+ private final float mFromDegrees;
+ private final float mToDegrees;
+ private final float mCenterX;
+ private final float mCenterY;
+ private Camera mCamera;
+
+ public FlipAnimation(float fromDegrees, float toDegrees,
+ float centerX, float centerY) {
+ mFromDegrees = fromDegrees;
+ mToDegrees = toDegrees;
+ mCenterX = centerX;
+ mCenterY = centerY;
+ }
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ mCamera = new Camera();
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ final float fromDegrees = mFromDegrees;
+ float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
+
+ final float centerX = mCenterX;
+ final float centerY = mCenterY;
+ final Camera camera = mCamera;
+
+ final Matrix matrix = t.getMatrix();
+
+ camera.save();
+
+ camera.rotateY(degrees);
+
+ camera.getMatrix(matrix);
+ camera.restore();
+
+ matrix.preTranslate(-centerX, -centerY);
+ matrix.postTranslate(centerX, centerY);
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/interfaces/Resourceble.java b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/interfaces/Resourceble.java
new file mode 100644
index 0000000..d5d5e1e
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/interfaces/Resourceble.java
@@ -0,0 +1,10 @@
+package yalantis.com.sidemenu.interfaces;
+
+/**
+ * Created by Konstantin on 12.01.2015.
+ */
+public interface Resourceble {
+ public int getImageRes();
+
+ public String getName();
+}
diff --git a/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/interfaces/ScreenShotable.java b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/interfaces/ScreenShotable.java
new file mode 100644
index 0000000..3b5d65d
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/interfaces/ScreenShotable.java
@@ -0,0 +1,12 @@
+package yalantis.com.sidemenu.interfaces;
+
+import android.graphics.Bitmap;
+
+/**
+ * Created by Konstantin on 12.01.2015.
+ */
+public interface ScreenShotable {
+ public void takeScreenShot();
+
+ public Bitmap getBitmap();
+}
diff --git a/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/model/SlideMenuItem.java b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/model/SlideMenuItem.java
new file mode 100644
index 0000000..e4b6ba3
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/model/SlideMenuItem.java
@@ -0,0 +1,32 @@
+package yalantis.com.sidemenu.model;
+
+import yalantis.com.sidemenu.interfaces.Resourceble;
+
+/**
+ * Created by Konstantin on 23.12.2014.
+ */
+public class SlideMenuItem implements Resourceble {
+ private String name;
+ private int imageRes;
+
+ public SlideMenuItem(String name, int imageRes) {
+ this.name = name;
+ this.imageRes = imageRes;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getImageRes() {
+ return imageRes;
+ }
+
+ public void setImageRes(int imageRes) {
+ this.imageRes = imageRes;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/util/ViewAnimator.java b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/util/ViewAnimator.java
new file mode 100644
index 0000000..3b4be28
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/java/yalantis/com/sidemenu/util/ViewAnimator.java
@@ -0,0 +1,188 @@
+package yalantis.com.sidemenu.util;
+
+import android.os.Handler;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.widget.ImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import yalantis.com.sidemenu.R;
+import yalantis.com.sidemenu.animation.FlipAnimation;
+import yalantis.com.sidemenu.interfaces.Resourceble;
+import yalantis.com.sidemenu.interfaces.ScreenShotable;
+
+/**
+ * Created by Konstantin on 12.01.2015.
+ */
+public class ViewAnimator {
+ private final int ANIMATION_DURATION = 175;
+ public static final int CIRCULAR_REVEAL_ANIMATION_DURATION = 500;
+
+ private AppCompatActivity appCompatActivity;
+
+ private List list;
+
+ private List viewList = new ArrayList<>();
+ private ScreenShotable screenShotable;
+ private DrawerLayout drawerLayout;
+ private ViewAnimatorListener animatorListener;
+
+
+ public ViewAnimator(AppCompatActivity activity,
+ List items,
+ ScreenShotable screenShotable,
+ final DrawerLayout drawerLayout,
+ ViewAnimatorListener animatorListener) {
+ this.appCompatActivity = activity;
+
+ this.list = items;
+ this.screenShotable = screenShotable;
+ this.drawerLayout = drawerLayout;
+ this.animatorListener = animatorListener;
+ }
+
+
+ public void showMenuContent() {
+ setViewsClickable(false);
+ viewList.clear();
+ double size = list.size();
+ for (int i = 0; i < size; i++) {
+ View viewMenu = appCompatActivity.getLayoutInflater().inflate(R.layout.menu_list_item, null);
+
+ final int finalI = i;
+ viewMenu.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int[] location = {0, 0};
+ v.getLocationOnScreen(location);
+ switchItem(list.get(finalI), location[1] + v.getHeight() / 2);
+ }
+ });
+ ((ImageView) viewMenu.findViewById(R.id.menu_item_image)).setImageResource(list.get(i).getImageRes());
+ viewMenu.setVisibility(View.GONE);
+ viewMenu.setEnabled(false);
+ viewList.add(viewMenu);
+ animatorListener.addViewToContainer(viewMenu);
+ final double position = i;
+ final double delay = 3 * ANIMATION_DURATION * (position / size);
+ new Handler().postDelayed(new Runnable() {
+ public void run() {
+ if (position < viewList.size()) {
+ animateView((int) position);
+ }
+ if (position == viewList.size() - 1) {
+ screenShotable.takeScreenShot();
+ setViewsClickable(true);
+ }
+ }
+ }, (long) delay);
+ }
+
+ }
+
+ private void hideMenuContent() {
+ setViewsClickable(false);
+ double size = list.size();
+ for (int i = list.size(); i >= 0; i--) {
+ final double position = i;
+ final double delay = 3 * ANIMATION_DURATION * (position / size);
+ new Handler().postDelayed(new Runnable() {
+ public void run() {
+ if (position < viewList.size()) {
+ animateHideView((int) position);
+ }
+ }
+ }, (long) delay);
+ }
+
+ }
+
+ private void setViewsClickable(boolean clickable) {
+ animatorListener.disableHomeButton();
+ for (View view : viewList) {
+ view.setEnabled(clickable);
+ }
+ }
+
+ private void animateView(int position) {
+ final View view = viewList.get(position);
+ view.setVisibility(View.VISIBLE);
+ FlipAnimation rotation =
+ new FlipAnimation(90, 0, 0.0f, view.getHeight() / 2.0f);
+ rotation.setDuration(ANIMATION_DURATION);
+ rotation.setFillAfter(true);
+ rotation.setInterpolator(new AccelerateInterpolator());
+ rotation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ view.clearAnimation();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+
+ }
+ });
+
+ view.startAnimation(rotation);
+ }
+
+ private void animateHideView(final int position) {
+ final View view = viewList.get(position);
+ FlipAnimation rotation =
+ new FlipAnimation(0, 90, 0.0f, view.getHeight() / 2.0f);
+ rotation.setDuration(ANIMATION_DURATION);
+ rotation.setFillAfter(true);
+ rotation.setInterpolator(new AccelerateInterpolator());
+ rotation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ view.clearAnimation();
+ view.setVisibility(View.INVISIBLE);
+ if (position == viewList.size() - 1) {
+ animatorListener.enableHomeButton();
+ drawerLayout.closeDrawers();
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+
+ }
+ });
+
+ view.startAnimation(rotation);
+ }
+
+ private void switchItem(Resourceble slideMenuItem, int topPosition) {
+ this.screenShotable = animatorListener.onSwitch(slideMenuItem, screenShotable, topPosition);
+ hideMenuContent();
+ }
+
+ public interface ViewAnimatorListener {
+
+ public ScreenShotable onSwitch(Resourceble slideMenuItem, ScreenShotable screenShotable, int position);
+
+ public void disableHomeButton();
+
+ public void enableHomeButton();
+
+ public void addViewToContainer(View view);
+
+ }
+}
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-hdpi/item_down.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-hdpi/item_down.png
new file mode 100644
index 0000000..71676a9
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-hdpi/item_down.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-hdpi/item_up.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-hdpi/item_up.png
new file mode 100644
index 0000000..b4ee97c
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-hdpi/item_up.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-mdpi/item_down.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-mdpi/item_down.png
new file mode 100644
index 0000000..303fbf5
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-mdpi/item_down.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-mdpi/item_up.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-mdpi/item_up.png
new file mode 100644
index 0000000..9ba7f95
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-mdpi/item_up.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-xhdpi/item_down.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xhdpi/item_down.png
new file mode 100644
index 0000000..99b2dfa
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xhdpi/item_down.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-xhdpi/item_up.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xhdpi/item_up.png
new file mode 100644
index 0000000..86da9bc
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xhdpi/item_up.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxhdpi/item_down.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxhdpi/item_down.png
new file mode 100644
index 0000000..cb73dc1
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxhdpi/item_down.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxhdpi/item_up.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxhdpi/item_up.png
new file mode 100644
index 0000000..8a3034f
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxhdpi/item_up.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxxhdpi/item_down.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxxhdpi/item_down.png
new file mode 100644
index 0000000..891817f
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxxhdpi/item_down.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxxhdpi/item_up.png b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxxhdpi/item_up.png
new file mode 100644
index 0000000..8f27c9a
Binary files /dev/null and b/src/AndroidApp/android_remocons/library/src/main/res/drawable-xxxhdpi/item_up.png differ
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/drawable/menu_item_selector.xml b/src/AndroidApp/android_remocons/library/src/main/res/drawable/menu_item_selector.xml
new file mode 100644
index 0000000..6c21169
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/res/drawable/menu_item_selector.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/layout/menu_list_item.xml b/src/AndroidApp/android_remocons/library/src/main/res/layout/menu_list_item.xml
new file mode 100644
index 0000000..f6a623d
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/res/layout/menu_list_item.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/values-w820dp/dimens.xml b/src/AndroidApp/android_remocons/library/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/values/dimens.xml b/src/AndroidApp/android_remocons/library/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..47c8224
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 16dp
+ 16dp
+
diff --git a/src/AndroidApp/android_remocons/library/src/main/res/values/styles.xml b/src/AndroidApp/android_remocons/library/src/main/res/values/styles.xml
new file mode 100644
index 0000000..6d28178
--- /dev/null
+++ b/src/AndroidApp/android_remocons/library/src/main/res/values/styles.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/.gitignore b/src/AndroidApp/android_remocons/rocon_remocon/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/build.gradle b/src/AndroidApp/android_remocons/rocon_remocon/build.gradle
new file mode 100644
index 0000000..6942c47
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/build.gradle
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 Daniel Stonier.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/*
+ * maven dependencies:
+ *
+ * - snakeyaml
+ *
+ * can't use this one from maven central - it uses methods that davlik doesn't
+ * like (generates Verify Errors on adb logcat).
+*/
+repositories {
+ mavenCentral()
+
+ mavenLocal()
+
+ flatDir {
+ dirs 'libs'
+ }
+ maven { url "https://jitpack.io" }
+}
+
+//noinspection GroovyAssignabilityCheck
+dependencies {
+ compile fileTree(include: ['*.jar'], dir: 'lib')
+ compile 'com.android.support.constraint:constraint-layout:1.0.2'
+ compile 'junit:junit:4.12'
+ compile 'com.android.support:support-v13:26.1.0'
+ compile 'com.android.support:appcompat-v7:26.1.0'
+ compile 'com.android.support:cardview-v7:26.1.0'
+ compile 'com.android.support:recyclerview-v7:26.1.0'
+
+ compile 'com.google.code.gson:gson:2.8.0'
+ compile 'org.apache.commons:commons-lang3:3.3.2'
+ compile 'com.google.guava:guava:23.3-android'
+ compile 'com.github.amlcurran.showcaseview:library:5.4.1'
+ compile 'org.ros.rosjava_core:rosjava_geometry:0.2.2'
+ compile 'com.github.rosjava.android_extras:gingerbread:0.2.0'
+ compile 'org.ros.rosjava_messages:tf2_msgs:0.5.15'
+ compile 'org.ros.rosjava_messages:map_store:0.3.1'
+ compile 'org.ros.rosjava_messages:world_canvas_msgs:0.1.0'
+ compile 'org.ros.rosjava_messages:turtlebot_msgs:2.2.+'
+ compile project(':common_tools')
+ compile 'org.ros.rosjava_bootstrap:message_generation:0.3.0'
+ compile 'org.ros.rosjava_messages:rocon_interaction_msgs:0.7.12'
+ compile 'org.ros.rosjava_messages:rocon_std_msgs:0.7.12'
+ compile 'org.ros.rosjava_messages:rocon_app_manager_msgs:0.7.12'
+ compile 'org.ros.android_core:android_10:0.2.1'
+ compile 'org.ros.android_core:android_15:0.2.1'
+ compile 'com.github.rosjava.android_extras:zxing:0.2.0'
+ compile 'com.github.rosjava.zeroconf_jmdns_suite:jmdns:0.2.1'
+ compile 'com.github.robotics_in_concert.rocon_rosjava_core:master_info:0.2.0'
+ compile 'com.github.robotics_in_concert.rocon_rosjava_core:rocon_interactions:0.2.0'
+ compile 'com.github.robotics_in_concert.rocon_rosjava_core:rosjava_utils:0.2.0'
+ compile ('com.github.ozodrukh:CircularReveal:1.1.1@aar') {
+ transitive = true;
+ }
+ //compile fileTree(dir: '../external_libraries', include:'snakeyaml*.jar')
+ compile fileTree(include: 'snakeyaml*.jar', dir: '../external_libraries')
+ compile project(':library')
+}
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 26
+ defaultConfig {
+ minSdkVersion 18
+ targetSdkVersion 26
+ versionCode 1
+ versionName "1.0"
+ multiDexEnabled true
+ }
+buildToolsVersion '26.0.2'
+}
+
+defaultTasks 'assembleRelease'
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/gradle/wrapper/gradle-wrapper.jar b/src/AndroidApp/android_remocons/rocon_remocon/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/src/AndroidApp/android_remocons/rocon_remocon/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/gradle/wrapper/gradle-wrapper.properties b/src/AndroidApp/android_remocons/rocon_remocon/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..5a22a9b
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Mar 15 20:02:17 CST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/gradlew b/src/AndroidApp/android_remocons/rocon_remocon/gradlew
new file mode 100644
index 0000000..9d82f78
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/gradlew.bat b/src/AndroidApp/android_remocons/rocon_remocon/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/lib/commons-compress-1.11.jar b/src/AndroidApp/android_remocons/rocon_remocon/lib/commons-compress-1.11.jar
new file mode 100644
index 0000000..71c92ce
Binary files /dev/null and b/src/AndroidApp/android_remocons/rocon_remocon/lib/commons-compress-1.11.jar differ
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/lib/gcm.jar b/src/AndroidApp/android_remocons/rocon_remocon/lib/gcm.jar
new file mode 100644
index 0000000..ac109a8
Binary files /dev/null and b/src/AndroidApp/android_remocons/rocon_remocon/lib/gcm.jar differ
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/lib/liblinphone.jar b/src/AndroidApp/android_remocons/rocon_remocon/lib/liblinphone.jar
new file mode 100644
index 0000000..44af482
Binary files /dev/null and b/src/AndroidApp/android_remocons/rocon_remocon/lib/liblinphone.jar differ
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/AndroidManifest.xml b/src/AndroidApp/android_remocons/rocon_remocon/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ea24fd4
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/AndroidManifest.xml
@@ -0,0 +1,520 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/src/AndroidApp/android_remocons/rocon_remocon/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl
new file mode 100644
index 0000000..2a492f7
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.vending.billing;
+
+import android.os.Bundle;
+
+/**
+ * InAppBillingService is the service that provides in-app billing version 3 and beyond.
+ * This service provides the following features:
+ * 1. Provides a new API to get details of in-app items published for the app including
+ * price, type, title and description.
+ * 2. The purchase flow is synchronous and purchase information is available immediately
+ * after it completes.
+ * 3. Purchase information of in-app purchases is maintained within the Google Play system
+ * till the purchase is consumed.
+ * 4. An API to consume a purchase of an inapp item. All purchases of one-time
+ * in-app items are consumable and thereafter can be purchased again.
+ * 5. An API to get current purchases of the user immediately. This will not contain any
+ * consumed purchases.
+ *
+ * All calls will give a response code with the following possible values
+ * RESULT_OK = 0 - success
+ * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
+ * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
+ * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
+ * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
+ * RESULT_ERROR = 6 - Fatal error during the API action
+ * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
+ * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
+ */
+interface IInAppBillingService {
+ /**
+ * Checks support for the requested billing API version, package and in-app type.
+ * Minimum API version supported by this interface is 3.
+ * @param apiVersion the billing version which the app is using
+ * @param packageName the package name of the calling app
+ * @param type type of the in-app item being purchased "inapp" for one-time purchases
+ * and "subs" for subscription.
+ * @return RESULT_OK(0) on success, corresponding result code on failures
+ */
+ int isBillingSupported(int apiVersion, String packageName, String type);
+
+ /**
+ * Provides details of a list of SKUs
+ * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
+ * with a list JSON strings containing the productId, price, title and description.
+ * This API can be called with a maximum of 20 SKUs.
+ * @param apiVersion billing API version that the Third-party is using
+ * @param packageName the package name of the calling app
+ * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "DETAILS_LIST" with a StringArrayList containing purchase information
+ * in JSON format similar to:
+ * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
+ * "title : "Example Title", "description" : "This is an example description" }'
+ */
+ Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
+
+ /**
+ * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
+ * the type, a unique purchase token and an optional developer payload.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param sku the SKU of the in-app item as published in the developer console
+ * @param type the type of the in-app item ("inapp" for one-time purchases
+ * and "subs" for subscription).
+ * @param developerPayload optional argument to be sent back with the purchase information
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "BUY_INTENT" - PendingIntent to start the purchase flow
+ *
+ * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
+ * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
+ * If the purchase is successful, the result data will contain the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "INAPP_PURCHASE_DATA" - String in JSON format similar to
+ * '{"orderId":"12999763169054705758.1371079406387615",
+ * "packageName":"com.example.app",
+ * "productId":"exampleSku",
+ * "purchaseTime":1345678900000,
+ * "purchaseToken" : "122333444455555",
+ * "developerPayload":"example developer payload" }'
+ * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
+ * was signed with the private key of the developer
+ * TODO: change this to app-specific keys.
+ */
+ Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
+ String developerPayload);
+
+ /**
+ * Returns the current SKUs owned by the user of the type and package name specified along with
+ * purchase information and a signature of the data to be validated.
+ * This will return all SKUs that have been purchased in V3 and managed items purchased using
+ * V1 and V2 that have not been consumed.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param type the type of the in-app items being requested
+ * ("inapp" for one-time purchases and "subs" for subscription).
+ * @param continuationToken to be set as null for the first call, if the number of owned
+ * skus are too many, a continuationToken is returned in the response bundle.
+ * This method can be called again with the continuation token to get the next set of
+ * owned skus.
+ * @return Bundle containing the following key-value pairs
+ * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+ * failure as listed above.
+ * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
+ * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
+ * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
+ * of the purchase information
+ * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
+ * next set of in-app purchases. Only set if the
+ * user has more owned skus than the current list.
+ */
+ Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
+
+ /**
+ * Consume the last purchase of the given SKU. This will result in this item being removed
+ * from all subsequent responses to getPurchases() and allow re-purchase of this item.
+ * @param apiVersion billing API version that the app is using
+ * @param packageName package name of the calling app
+ * @param purchaseToken token in the purchase information JSON that identifies the purchase
+ * to be consumed
+ * @return 0 if consumption succeeded. Appropriate error values for failures.
+ */
+ int consumePurchase(int apiVersion, String packageName, String purchaseToken);
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/AppAdapter.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/AppAdapter.java
new file mode 100644
index 0000000..35fb572
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/AppAdapter.java
@@ -0,0 +1,103 @@
+/*
+* Software License Agreement (BSD License)
+*
+* Copyright (c) 2011, Willow Garage, Inc.
+* All rights reserved.
+* Redistribution and use in source and binary forms, with or without
+* modification, are permitted provided that the following conditions
+* are met:
+*
+* * Redistributions of source code must retain the above copyright
+* notice, this list of conditions and the following disclaimer.
+* * Redistributions in binary form must reproduce the above
+* copyright notice, this list of conditions and the following
+* disclaimer in the documentation and/or other materials provided
+* with the distribution.
+* * Neither the name of Willow Garage, Inc. nor the names of its
+* contributors may be used to endorse or promote products derived
+* from this software without specific prior written permission.
+*
+* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+* POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+
+import java.util.ArrayList;
+
+import rocon_interaction_msgs.Interaction;
+
+public class AppAdapter extends BaseAdapter {
+ private Context context;
+ private ArrayList interactions;
+
+ public AppAdapter(Context c, ArrayList interactions) {
+ context = c;
+ this.interactions = interactions;
+ }
+
+ @Override
+ public int getCount() {
+ if (interactions == null) {
+ return 0;
+ }
+ return interactions.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return null;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ /**
+ * Create a new View for each item referenced by the Adapter.
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ View view = inflater.inflate(R.layout.app_item, null);
+ Interaction interaction = interactions.get(position);
+ if( interaction.getIcon().getData().array().length > 0 && interaction.getIcon().getFormat() != null &&
+ (interaction.getIcon().getFormat().equals("jpeg") || interaction.getIcon().getFormat().equals("png")) ) {
+ ChannelBuffer buffer = interaction.getIcon().getData();
+ Bitmap iconBitmap = BitmapFactory.decodeByteArray( interaction.getIcon().getData().array(), buffer.arrayOffset(), buffer.readableBytes());
+
+ if( iconBitmap != null ) {
+ ImageView iv = (ImageView) view.findViewById(R.id.icon);
+ iv.setImageBitmap(iconBitmap);
+ }
+ }
+ TextView tv = (TextView) view.findViewById(R.id.name);
+ tv.setText(interaction.getDisplayName());
+
+ return view;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/Database.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/Database.java
new file mode 100644
index 0000000..b10cc27
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/Database.java
@@ -0,0 +1,154 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.util.Log;
+
+public class Database extends ContentProvider {
+ DatabaseHelper dbh;
+
+ private class DatabaseHelper extends SQLiteOpenHelper {
+ DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ /**
+ * Called by the database helper.
+ */
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ Log.i("MasterChooser", "Creating database...");
+ db.execSQL(TABLE_CREATE);
+ }
+
+ /**
+ * On upgrade of the database, do nothing
+ */
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.i("MasterChooser", "The database is being upgraded from " + oldVersion + " to " + newVersion);
+ }
+ }
+
+ public static final Uri CONTENT_URI = Uri.parse("content://com.github.rosjava.android_remocons.rocon_remocon");
+ public static final int DATABASE_VERSION = 2;
+ public static final String DATABASE_NAME = "concertlist_table";
+ public static final String TABLE_NAME = "concertlist";
+ public static final String TABLE_COLUMN = "concerts";
+ private static final String TABLE_CREATE =
+ "CREATE TABLE " + TABLE_NAME + " (" + TABLE_COLUMN + " TEXT);";
+
+ @Override
+ public int delete(Uri arg0, String arg1, String[] arg2) {
+ Log.e("ConcertContentProvider", "Invalid method: delete");
+ return 0;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ Log.e("ConcertContentProvider", "Invalid method: getType");
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ SQLiteDatabase db = dbh.getWritableDatabase();
+ if (db == null) {
+ Log.e("ConcertContentProvider", "Could not get the writable database.");
+ return null;
+ }
+ db.beginTransaction();
+ if (values.get(TABLE_COLUMN) != null) {
+ Log.i("ConcertContentProvider", "Saving concert...");
+
+ Cursor c = db.query(TABLE_NAME, new String[]{TABLE_COLUMN,},
+ null, new String[]{}, null, null, null);
+ if (c.getCount() > 0) {
+ Log.i("ConcertContentProvider", "Update currently existing row");
+ db.update(TABLE_NAME, values, null, new String[]{});
+ } else {
+ Log.i("ConcertContentProvider", "Insert new row");
+ if (db.insert(TABLE_NAME, null, values) < 0) {
+ Log.e("ConcertContentProvider", "Error inserting row!");
+ }
+ }
+ } else {
+ Log.i("ConcertContentProvider", "Deleting saved concert...");
+ db.delete(TABLE_NAME, null, new String[]{});
+ }
+
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ db.close();
+ Log.i("ConcertContentProvider", "Done saving current concert");
+ return CONTENT_URI;
+ }
+
+ @Override
+ public boolean onCreate() {
+ dbh = new DatabaseHelper(this.getContext());
+ if (dbh == null) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ SQLiteDatabase db = dbh.getReadableDatabase();
+ Cursor res = db.query(TABLE_NAME, new String[]{TABLE_COLUMN,},
+ null, new String[]{}, null, null, null);
+ //db.close();
+ return res;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+ Log.e("ConcertContentProvider", "Invalid method: update");
+ // TODO Auto-generated method stub
+ return 0;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/FunctionAdapter.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/FunctionAdapter.java
new file mode 100644
index 0000000..74f4f44
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/FunctionAdapter.java
@@ -0,0 +1,113 @@
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.github.rosjava.android_remocons.rocon_remocon.medicine_alert.DeskClockMainActivity;
+import com.github.rosjava.android_remocons.rocon_remocon.motion_control.MotionControl;
+
+import java.util.List;
+
+import static android.support.v4.content.ContextCompat.startActivity;
+
+/**
+ * Created by turtlebot on 18-3-24.
+ */
+public class FunctionAdapter extends RecyclerView.Adapter {
+ private List mFunctionList;
+
+
+
+ public FunctionAdapter(List mFunctionList) {
+ this.mFunctionList = mFunctionList;
+ }
+
+ //定义了内部类,构造函数里要传入View参数,它通常就是RecyclerView子项最外层的布局
+ static class ViewHolder extends RecyclerView.ViewHolder {
+ View functionView;
+ ImageView functionImage;
+ TextView functionName;
+
+ public ViewHolder(View view) {
+ super(view);
+ functionView = view;
+ functionImage = (ImageView) view.findViewById(R.id.function_image);
+ functionName = (TextView) view.findViewById(R.id.function_name);
+ }
+ }
+
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.function_item, parent, false);
+ final ViewHolder holder = new ViewHolder(view);
+
+
+ holder.functionView.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View view) {
+ int position = holder.getAdapterPosition();
+ FunctionItem functionItem = mFunctionList.get(position);
+
+ Toast.makeText(view.getContext(), "you clicked view "+functionItem.getName(), Toast.LENGTH_SHORT).show();
+ Context context = view.getContext();
+ Intent intent;
+ switch (position) {
+ case 0:
+ intent = new Intent(context, MotionControl.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ break;
+ case 1:
+ intent = new Intent(context, DeskClockMainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ break;
+ default:
+ break;
+ }
+ //Intent intent = new Intent(context, TestActivity.class);
+ //intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ //context.startActivity(intent);
+
+ }
+ });
+ holder.functionImage.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ int position = holder.getAdapterPosition();
+ FunctionItem functionItem = mFunctionList.get(position);
+
+ Toast.makeText(view.getContext(), "you clicked image "+functionItem.getName(), Toast.LENGTH_SHORT).show();
+ Context context = view.getContext();
+ Intent intent = new Intent(context, TestActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intent);
+ }
+ });
+
+ return holder;
+ }
+
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ FunctionItem functionItem = mFunctionList.get(position);
+ holder.functionImage.setImageResource(functionItem.getImageId());
+ holder.functionName.setText(functionItem.getName());
+ }
+
+ @Override
+ public int getItemCount() {
+ return mFunctionList.size();
+ }
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/FunctionItem.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/FunctionItem.java
new file mode 100644
index 0000000..d874785
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/FunctionItem.java
@@ -0,0 +1,23 @@
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+/**
+ * Created by turtlebot on 18-3-24.
+ */
+
+public class FunctionItem {
+ private String name;
+ private int imageId;
+
+ public FunctionItem(String name, int imageId) {
+ this.name = name;
+ this.imageId = imageId;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public int getImageId() {
+ return imageId;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MainActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MainActivity.java
new file mode 100644
index 0000000..280290b
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MainActivity.java
@@ -0,0 +1,53 @@
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+
+import org.ros.node.NodeConfiguration;
+import org.ros.node.NodeMainExecutor;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class MainActivity extends AppCompatActivity{
+
+ /** Notification ticker for the App */
+ public static final String NOTIFICATION_TICKER = "ROS Control";
+ /** Notification title for the App */
+ public static final String NOTIFICATION_TITLE = "ROS Control";
+
+ private List functionItemList = new ArrayList<>();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ initFunctions(); //初始化功能数据
+ RecyclerView recyclerView = (RecyclerView) findViewById(R.id.function_recycler_view);
+ LinearLayoutManager layoutManager = new LinearLayoutManager(this);
+ recyclerView.setLayoutManager(layoutManager);
+ FunctionAdapter adapter = new FunctionAdapter(functionItemList);
+ recyclerView.setAdapter(adapter);
+
+ }
+
+ //initialize all function data
+ private void initFunctions() {
+ FunctionItem motionControl = new FunctionItem("移动控制", R.drawable.directional_arrow);
+ functionItemList.add(motionControl);
+ FunctionItem medicineAlert = new FunctionItem("服药提醒", R.drawable.directional_arrow);
+ functionItemList.add(medicineAlert);
+
+ }
+
+
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MasterAdapter.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MasterAdapter.java
new file mode 100644
index 0000000..68823cd
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MasterAdapter.java
@@ -0,0 +1,81 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+
+import com.github.rosjava.android_remocons.common_tools.master.RoconDescription;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author hersh@willowgarage.com
+ */
+public class MasterAdapter extends BaseAdapter {
+ private Context context;
+ private List masterItems;
+ public MasterAdapter(MasterChooser rmc, List concerts) {
+ context = rmc;
+ masterItems = new ArrayList();
+ if (concerts != null) {
+ for (int i = 0; i < concerts.size(); i++) {
+ masterItems.add(new MasterItem(concerts.get(i), rmc));
+ }
+ }
+ }
+ @Override
+ public int getCount() {
+ if (masterItems == null) {
+ return 0;
+ }
+ return masterItems.size();
+ }
+ @Override
+ public Object getItem(int position) {
+ return null;
+ }
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+ // create a new View for each item referenced by the Adapter
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ return masterItems.get(position).getView(context, convertView, parent);
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MasterChooser.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MasterChooser.java
new file mode 100644
index 0000000..083fc5b
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MasterChooser.java
@@ -0,0 +1,645 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * Copyright (c) 2013, OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Spannable;
+import android.text.Spannable.Factory;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
+import android.util.Log;
+import android.util.SparseBooleanArray;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.github.rosjava.android_remocons.common_tools.master.MasterId;
+import com.github.rosjava.android_remocons.common_tools.master.RoconDescription;
+import com.github.rosjava.android_remocons.common_tools.nfc.NfcReaderActivity;
+import com.github.rosjava.android_remocons.common_tools.zeroconf.MasterSearcher;
+import com.github.rosjava.android_remocons.rocon_remocon.send_inst.SendInst;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.LinphoneLauncherActivity;
+import com.github.rosjava.zeroconf_jmdns_suite.jmdns.DiscoveredService;
+import com.google.zxing.IntentIntegrator;
+import com.google.zxing.IntentResult;
+
+import org.yaml.snakeyaml.Yaml;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * A rewrite of ye olde RobotMasterChooser to work with rocon masters (i.e.
+ * those that have rocon master info and an interactions manager present).
+ */
+public class MasterChooser extends Activity {
+
+ private static final int ADD_URI_DIALOG_ID = 0;
+ private static final int ADD_DELETION_DIALOG_ID = 1;
+ private static final int ADD_SEARCH_CONCERT_DIALOG_ID = 2;
+
+ private static final int QR_CODE_SCAN_REQUEST_CODE = 101;
+ private static final int NFC_TAG_SCAN_REQUEST_CODE = 102;
+
+ private List masters;
+ private boolean[] selections;
+ private MasterSearcher masterSearcher;
+ private ListView listView;
+ private Yaml yaml = new Yaml();
+
+
+ public MasterChooser() {
+ masters = new ArrayList();
+ }
+
+ private void readMasterList() {
+ String str = null;
+ Cursor c = getContentResolver().query(
+ Database.CONTENT_URI, null, null, null, null);
+ if (c == null) {
+ masters = new ArrayList();
+ Log.e("Remocon", "master chooser provider failed!!!");
+ return;
+ }
+ if (c.getCount() > 0) {
+ c.moveToFirst();
+ str = c.getString(c.getColumnIndex(Database.TABLE_COLUMN));
+ Log.i("Remocon", "master chooser found a rocon master: " + str);
+ }
+ if (str != null) {
+ masters = (List) yaml.load(str);
+ } else {
+ masters = new ArrayList();
+ }
+ }
+
+ public void writeMasterList() {
+ Log.i("Remocon", "master chooser saving rocon master details...");
+ String str = null;
+ final List tmp = masters; // Avoid race conditions
+ if (tmp != null) {
+ str = yaml.dump(tmp);
+ }
+ ContentValues cv = new ContentValues();
+ cv.put(Database.TABLE_COLUMN, str);
+ Uri newEmp = getContentResolver().insert(Database.CONTENT_URI, cv);
+ if (newEmp != Database.CONTENT_URI) {
+ Log.e("Remocon", "master chooser could not save concert, non-equal URI's");
+ }
+ }
+
+ private void refresh() {
+ readMasterList();
+ updateListView();
+ }
+
+ private void updateListView() {
+ setContentView(R.layout.master_chooser_new);
+ ListView listview = (ListView) findViewById(R.id.master_list);
+ listview.setAdapter(new MasterAdapter(this, masters));
+ registerForContextMenu(listview);
+
+ listview.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View v,
+ int position, long id) {
+ choose(position);
+ }
+ });
+ }
+
+ /**
+ * Called when the user clicks on one of the listed masters in master chooser
+ * view. Should probably check the connection status before
+ * proceeding here, but perhaps we can just rely on the user clicking
+ * refresh so this process stays without any lag delay.
+ *
+ * @param position
+ */
+ private void choose(int position) {
+ RoconDescription concert = masters.get(position);
+ if (concert == null || concert.getConnectionStatus() == null
+ || concert.getConnectionStatus().equals(RoconDescription.ERROR)) {
+ AlertDialog d = new AlertDialog.Builder(MasterChooser.this)
+ .setTitle("Error!")
+ .setCancelable(false)
+ .setMessage("Failed: Cannot contact concert")
+ .setNeutralButton("OK",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ }
+ }).create();
+ d.show();
+ } else if ( concert.getConnectionStatus().equals(RoconDescription.UNAVAILABLE) ) {
+ AlertDialog d = new AlertDialog.Builder(MasterChooser.this)
+ .setTitle("Master Unavailable!")
+ .setCancelable(false)
+ .setMessage("Currently busy serving another.")
+ .setNeutralButton("OK",
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ }
+ }).create();
+ d.show();
+ } else {
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra(RoconDescription.UNIQUE_KEY, concert);
+ setResult(RESULT_OK, resultIntent);
+ finish();
+ }
+ }
+
+ private void addMaster(MasterId masterId) {
+ addMaster(masterId, false);
+ }
+
+ private void addMaster(MasterId masterId, boolean connectToDuplicates) {
+ Log.i("MasterChooserActivity", "adding master to the concert master chooser [" + masterId.toString() + "]");
+ if (masterId == null || masterId.getMasterUri() == null) {
+ } else {
+ for (int i = 0; i < masters.toArray().length; i++) {
+ RoconDescription concert = masters.get(i);
+ if (concert.getMasterId().equals(masterId)) {
+ if (connectToDuplicates) {
+ choose(i);
+ return;
+ } else {
+ Toast.makeText(this, "That concert is already listed.",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+ }
+ }
+ Log.i("MasterChooserActivity", "creating concert description: "
+ + masterId.toString());
+ masters.add(RoconDescription.createUnknown(masterId));
+ Log.i("MasterChooserActivity", "description created");
+ onMastersChanged();
+ }
+ }
+
+ private void onMastersChanged() {
+ writeMasterList();
+ updateListView();
+ }
+
+ private void deleteAllMasters() {
+ masters.clear();
+ onMastersChanged();
+ }
+
+ private void deleteSelectedMasters(boolean[] array) {
+ int j = 0;
+ for (int i = 0; i < array.length; i++) {
+ if (array[i]) {
+ masters.remove(j);
+ } else {
+ j++;
+ }
+ }
+ onMastersChanged();
+ }
+
+ private void deleteUnresponsiveMasters() {
+ Iterator iter = masters.iterator();
+ while (iter.hasNext()) {
+ RoconDescription concert = iter.next();
+ if (concert == null || concert.getConnectionStatus() == null
+ || concert.getConnectionStatus().equals(RoconDescription.ERROR)) {
+ Log.i("Remocon", "concert master chooser removing concert with connection status '"
+ + concert.getConnectionStatus() + "'");
+ iter.remove();
+ }
+ }
+ onMastersChanged();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ readMasterList();
+ updateListView();
+
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ // Sub-activity to gather concert connection data completed: can be QR code or NFC tag scan
+ // TODO: cannot unify both calls?
+
+ if (resultCode == RESULT_CANCELED) {
+ Toast.makeText(this, "Cancelled", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ Map data = null;
+ if (requestCode == QR_CODE_SCAN_REQUEST_CODE) {
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(
+ requestCode, resultCode, intent);
+ if (scanResult != null && scanResult.getContents() != null) {
+ Yaml yaml = new Yaml();
+ String scanned_data = scanResult.getContents().toString();
+ data = (Map) yaml.load(scanned_data);
+ }
+ }
+ else if (requestCode == NFC_TAG_SCAN_REQUEST_CODE && resultCode == RESULT_OK) {
+ if (intent.hasExtra("tag_data")) {
+ data = (Map) intent.getExtras().getSerializable("tag_data");
+ }
+ }
+ else {
+ Log.w("Remocon", "Unknown activity request code: " + requestCode);
+ return;
+ }
+
+ if (data == null) {
+ Toast.makeText(this, "Scan failed", Toast.LENGTH_SHORT).show();
+ }
+ else {
+ try {
+ Log.d("Remocon", "master chooser OBJECT: " + data.toString());
+ addMaster(new MasterId(data), false);
+ } catch (Exception e) {
+ Toast.makeText(this, "invalid rocon master description: " + e.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ readMasterList();
+ final Dialog dialog;
+ Button button;
+ AlertDialog.Builder builder;
+ switch (id) {
+ case ADD_URI_DIALOG_ID:
+ dialog = new Dialog(this);
+ dialog.setContentView(R.layout.add_uri_dialog);
+ dialog.setTitle("Add a Master");
+ dialog.setOnKeyListener(new DialogKeyListener());
+ EditText uriField = (EditText) dialog.findViewById(R.id.uri_editor);
+ uriField.setText("http://localhost:11311/",
+ TextView.BufferType.EDITABLE);
+ button = (Button) dialog.findViewById(R.id.enter_button);
+ button.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ enterMasterInfo(dialog);
+ removeDialog(ADD_URI_DIALOG_ID);
+ }
+ });
+ button = (Button) dialog.findViewById(R.id.qr_code_button);
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ scanQRCodeClicked(v);
+ }
+ });
+ button = (Button) dialog.findViewById(R.id.nfc_tag_button);
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ scanNFCTagClicked(v);
+ }
+ });
+ button = (Button) dialog.findViewById(R.id.search_master_button);
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ searchMasterClicked(v);
+ }
+ });
+
+ button = (Button) dialog.findViewById(R.id.cancel_button);
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ removeDialog(ADD_URI_DIALOG_ID);
+ }
+ });
+
+ /*
+ button = (Button) dialog.findViewById(R.id.go_to_phone);//NO1
+ button.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ gotoPhone();
+ }
+ });
+ */
+
+ break;
+ case ADD_DELETION_DIALOG_ID:
+ builder = new AlertDialog.Builder(this);
+ String newline = System.getProperty("line.separator");
+ if (masters.size() > 0) {
+ selections = new boolean[masters.size()];
+ Spannable[] concert_names = new Spannable[masters.size()];
+ Spannable name;
+ for (int i = 0; i < masters.size(); i++) {
+ name = Factory.getInstance().newSpannable(
+ masters.get(i).getMasterName() + newline + masters.get(i).getMasterId());
+ name.setSpan(new ForegroundColorSpan(0xff888888),
+ masters.get(i).getMasterName().length(), name.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ name.setSpan(new RelativeSizeSpan(0.8f),
+ masters.get(i).getMasterName().length(), name.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ concert_names[i] = name;
+ }
+ builder.setTitle("Delete a concert");
+ builder.setMultiChoiceItems(concert_names, selections,
+ new DialogSelectionClickHandler());
+ builder.setPositiveButton("Delete Selections",
+ new DeletionDialogButtonClickHandler());
+ builder.setNegativeButton("Cancel",
+ new DeletionDialogButtonClickHandler());
+ dialog = builder.create();
+ } else {
+ builder.setTitle("No masters to delete.");
+ dialog = builder.create();
+ final Timer t = new Timer();
+ t.schedule(new TimerTask() {
+ public void run() {
+ removeDialog(ADD_DELETION_DIALOG_ID);
+ }
+ }, 2 * 1000);
+ }
+ break;
+ case ADD_SEARCH_CONCERT_DIALOG_ID:
+ builder = new AlertDialog.Builder(this);
+ builder.setTitle("Scanning on the local network...");
+ LayoutInflater layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ listView = (ListView) layoutInflater.inflate(R.layout.zeroconf_master_list, null);
+ masterSearcher = new MasterSearcher(this, listView, "concert-master", R.drawable.conductor, R.drawable.turtle);
+ builder.setView(listView);
+ builder.setPositiveButton("Select", new SearchMasterDialogButtonClickHandler());
+ builder.setNegativeButton("Cancel", new SearchMasterDialogButtonClickHandler());
+ dialog = builder.create();
+ dialog.setOnKeyListener(new DialogKeyListener());
+ break;
+ default:
+ dialog = null;
+ }
+ return dialog;
+ }
+
+ public class DialogSelectionClickHandler implements
+ DialogInterface.OnMultiChoiceClickListener {
+ public void onClick(DialogInterface dialog, int clicked,
+ boolean selected) {
+ return;
+ }
+ }
+
+ public class DeletionDialogButtonClickHandler implements
+ DialogInterface.OnClickListener {
+ public void onClick(DialogInterface dialog, int clicked) {
+ switch (clicked) {
+ case DialogInterface.BUTTON_POSITIVE:
+ deleteSelectedMasters(selections);
+ removeDialog(ADD_DELETION_DIALOG_ID);
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ removeDialog(ADD_DELETION_DIALOG_ID);
+ break;
+ }
+ }
+ }
+
+ public class SearchMasterDialogButtonClickHandler implements
+ DialogInterface.OnClickListener {
+ public void onClick(DialogInterface dialog, int clicked) {
+ switch (clicked) {
+ case DialogInterface.BUTTON_POSITIVE:
+ SparseBooleanArray positions = listView
+ .getCheckedItemPositions();
+
+ for (int i = 0; i < positions.size(); i++) {
+ if (positions.valueAt(i)) {
+ enterMasterInfo((DiscoveredService) listView.getAdapter()
+ .getItem(positions.keyAt(i)));
+ }
+ }
+ removeDialog(ADD_DELETION_DIALOG_ID);
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ removeDialog(ADD_DELETION_DIALOG_ID);
+ break;
+ }
+ }
+ }
+
+ public void enterMasterInfo(DiscoveredService discovered_service) {
+ /*
+ This could be better - it should actually contact and check off each
+ resolvable zeroconf address looking for the master. Instead, we just grab
+ the first ipv4 address and totally ignore the possibility of an ipv6 master.
+ */
+ String newMasterUri = null;
+ if ( discovered_service.ipv4_addresses.size() != 0 ) {
+ newMasterUri = "http://" + discovered_service.ipv4_addresses.get(0) + ":"
+ + discovered_service.port + "/";
+ }
+ if (newMasterUri != null && newMasterUri.length() > 0) {
+ android.util.Log.i("Remocon", newMasterUri);
+ Map data = new HashMap();
+ data.put("URL", newMasterUri);
+ try {
+ addMaster(new MasterId(data));
+ } catch (Exception e) {
+ Toast.makeText(MasterChooser.this, "Invalid Parameters.",
+ Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ Toast.makeText(MasterChooser.this, "No valid resolvable master URI.",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ public void enterMasterInfo(Dialog dialog) {
+ EditText uriField = (EditText) dialog.findViewById(R.id.uri_editor);
+ String newMasterUri = uriField.getText().toString();
+ EditText wifiNameField = (EditText) dialog
+ .findViewById(R.id.wifi_name_editor);
+ String newWifiName = wifiNameField.getText().toString();
+ EditText wifiPasswordField = (EditText) dialog
+ .findViewById(R.id.wifi_password_editor);
+ String newWifiPassword = wifiPasswordField.getText().toString();
+ if (newMasterUri != null && newMasterUri.length() > 0) {
+ Map data = new HashMap();
+ data.put("URL", newMasterUri);
+ if (newWifiName != null && newWifiName.length() > 0) {
+ data.put("WIFI", newWifiName);
+ }
+ if (newWifiPassword != null && newWifiPassword.length() > 0) {
+ data.put("WIFIPW", newWifiPassword);
+ }
+ try {
+ addMaster(new MasterId(data));
+ } catch (Exception e) {
+ Toast.makeText(MasterChooser.this, "Invalid Parameters.",
+ Toast.LENGTH_SHORT).show();
+ }
+ } else {
+ Toast.makeText(MasterChooser.this, "Must specify Master URI.",
+ Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ public class DialogKeyListener implements DialogInterface.OnKeyListener {
+ @Override
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN
+ && keyCode == KeyEvent.KEYCODE_ENTER) {
+ Dialog dlg = (Dialog) dialog;
+ enterMasterInfo(dlg);
+ removeDialog(ADD_URI_DIALOG_ID);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ public void addMasterClicked(View view) {
+ showDialog(ADD_URI_DIALOG_ID);
+ }
+
+ public void refreshClicked(View view) {
+ refresh();
+ }
+
+ //点击send跳转到SendInst活动
+ public void send(){
+ //String cmd = "ls -l";
+ //SendInst sendActivity = new SendInst();
+
+ //sendActivity.roscmd = cmd;
+ Intent intent = new Intent(MasterChooser.this, SendInst.class);
+ startActivity(intent);
+ }
+ public void sendClicked(View view){
+ send();
+ }
+
+ public void scanQRCodeClicked(View view) {
+ dismissDialog(ADD_URI_DIALOG_ID);
+ IntentIntegrator.initiateScan(this, IntentIntegrator.DEFAULT_TITLE,
+ IntentIntegrator.DEFAULT_MESSAGE, IntentIntegrator.DEFAULT_YES,
+ IntentIntegrator.DEFAULT_NO, IntentIntegrator.QR_CODE_TYPES);
+ }
+
+ public void scanNFCTagClicked(View view) {
+ dismissDialog(ADD_URI_DIALOG_ID);
+ Intent i = new Intent(this, NfcReaderActivity.class);
+ // Set the request code so we can identify the callback via this code
+ startActivityForResult(i, NFC_TAG_SCAN_REQUEST_CODE);
+ }
+
+ public void searchMasterClicked(View view) {
+ removeDialog(ADD_URI_DIALOG_ID);
+ showDialog(ADD_SEARCH_CONCERT_DIALOG_ID);
+
+ }
+
+ public void gotoPhone() //NO2
+ {
+ Intent intent = new Intent(MasterChooser.this, LinphoneLauncherActivity.class);
+ startActivity(intent);
+
+ //Toast.makeText(MasterChooser.this, "想开电话,想得美!", Toast.LENGTH_LONG).show();
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.master_chooser_option_menu, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == R.id.add_concert) {
+ showDialog(ADD_URI_DIALOG_ID);
+ return true;
+ } else if (id == R.id.delete_selected) {
+ showDialog(ADD_DELETION_DIALOG_ID);
+ return true;
+ } else if (id == R.id.delete_unresponsive) {
+ deleteUnresponsiveMasters();
+ return true;
+ } else if (id == R.id.delete_all) {
+ deleteAllMasters();
+ return true;
+ } else if (id == R.id.kill) {
+ Intent intent = new Intent();
+ setResult(RESULT_CANCELED, intent);
+ finish();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MasterItem.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MasterItem.java
new file mode 100644
index 0000000..2c184a2
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/MasterItem.java
@@ -0,0 +1,176 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.github.rosjava.android_remocons.common_tools.master.ConcertChecker;
+import com.github.rosjava.android_remocons.common_tools.master.RoconDescription;
+import com.github.rosjava.android_remocons.common_tools.system.WifiChecker;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+
+/**
+ * Data class behind view of one item in the list of ROS Masters. Gets created with a master URI and a
+ * local host name, then starts a {@link com.github.rosjava.android_remocons.common_tools.master.ConcertChecker}
+ * to look up concert name and type.
+ *
+ * @author hersh@willowgarage.com
+ */
+public class MasterItem implements ConcertChecker.ConcertDescriptionReceiver,
+ ConcertChecker.FailureHandler {
+ private ConcertChecker checker;
+ private View view;
+ private RoconDescription description;
+ private MasterChooser parentMca;
+ private String errorReason;
+
+ public MasterItem(RoconDescription roconDescription, MasterChooser parentMca) {
+ errorReason = "";
+ this.parentMca = parentMca;
+ this.description = roconDescription;
+ this.description.setConnectionStatus(RoconDescription.CONNECTING);
+ if (WifiChecker.wifiValid(this.description.getMasterId(),
+ (WifiManager) parentMca.getApplicationContext().getSystemService(parentMca.WIFI_SERVICE))) {
+ checker = new ConcertChecker(this, this);
+ checker.beginChecking(this.description.getMasterId());
+ } else {
+ errorReason = "Wrong WiFi Network";
+ description.setConnectionStatus(RoconDescription.WIFI);
+ safePopulateView();
+ }
+ }
+
+ public boolean isOk() {
+ return this.description.getConnectionStatus().equals(RoconDescription.OK);
+ }
+
+ public void handleSuccess() {
+ checker.beginChecking(this.description.getMasterId());
+ }
+
+ @Override
+ public void receive(RoconDescription roconDescription) {
+ description.copyFrom(roconDescription);
+ safePopulateView();
+ }
+
+ @Override
+ public void handleFailure(String reason) {
+ errorReason = reason;
+ description.setConnectionStatus(RoconDescription.ERROR);
+ safePopulateView();
+ }
+
+ public View getView(Context context, View convert_view, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ // Using convert_view here seems to cause the wrong view to show
+ // up sometimes, so I'm always making new ones.
+ view = inflater.inflate(R.layout.master_item, null);
+ populateView();
+ return view;
+ }
+
+ private void safePopulateView() {
+ if (view != null) {
+ final MasterChooser mca = parentMca;
+ view.post(new Runnable() {
+ @Override
+ public void run() {
+ populateView();
+ mca.writeMasterList();
+ }
+ });
+ }
+ }
+
+ private void populateView() {
+ Log.i("MasterItem", "connection status = " + description.getConnectionStatus());
+ boolean isOk = description.getConnectionStatus().equals(RoconDescription.OK);
+ boolean isUnavailable = description.getConnectionStatus().equals(RoconDescription.UNAVAILABLE);
+ boolean isWifi = description.getConnectionStatus().equals(RoconDescription.WIFI);
+ boolean isError = description.getConnectionStatus().equals(RoconDescription.ERROR);
+ boolean isConnecting = description.getConnectionStatus().equals(RoconDescription.CONNECTING);
+ ProgressBar progress = (ProgressBar) view.findViewById(R.id.progress_circle);
+ progress.setIndeterminate(true);
+ progress.setVisibility(isConnecting ? View.VISIBLE : View.GONE);
+ ImageView errorImage = (ImageView) view.findViewById(R.id.error_icon);
+ errorImage.setVisibility(isError ? View.VISIBLE : View.GONE);
+ ImageView iv = (ImageView) view.findViewById(R.id.concert_icon);
+ iv.setVisibility((isOk || isWifi || isUnavailable) ? View.VISIBLE : View.GONE);
+ if (isWifi) {
+ iv.setImageResource(R.drawable.wifi_question_mark);
+ } else if (description.getMasterIconData() == null) {
+ iv.setImageResource(R.drawable.question_mark);
+ } else if (description.getMasterIconData().array().length > 0 && description.getMasterIconFormat() != null &&
+ (description.getMasterIconFormat().equals("jpeg") || description.getMasterIconFormat().equals("png"))) {
+ ChannelBuffer buffer = description.getMasterIconData();
+ Bitmap iconBitmap = BitmapFactory.decodeByteArray(buffer.array(), buffer.arrayOffset(), buffer.readableBytes());
+ if (iconBitmap != null) {
+ iv.setImageBitmap(iconBitmap);
+ } else {
+ iv.setImageResource(R.drawable.question_mark);
+ }
+ } else {
+ iv.setImageResource(R.drawable.question_mark);
+ }
+ if (isUnavailable) {
+ // Be nice to do alpha here, but that is api 16 and we are targeting 10.
+ ColorMatrix matrix = new ColorMatrix();
+ matrix.setSaturation(0); //0 means grayscale
+ ColorMatrixColorFilter cf = new ColorMatrixColorFilter(matrix);
+ iv.setColorFilter(cf);
+ }
+ TextView tv;
+ tv = (TextView) view.findViewById(R.id.uri);
+ tv.setText(description.getMasterId().toString());
+ tv = (TextView) view.findViewById(R.id.name);
+ tv.setText(description.getMasterFriendlyName());
+ tv = (TextView) view.findViewById(R.id.status);
+ tv.setText(errorReason);
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/NfcLauncherActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/NfcLauncherActivity.java
new file mode 100644
index 0000000..e5ae811
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/NfcLauncherActivity.java
@@ -0,0 +1,254 @@
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager;
+import android.nfc.NfcAdapter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Vibrator;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.github.robotics_in_concert.rocon_rosjava_core.rocon_interactions.InteractionMode;
+import com.github.robotics_in_concert.rocon_rosjava_core.rosjava_utils.ByteArrays;
+import com.github.rosjava.android_remocons.common_tools.master.ConcertChecker;
+import com.github.rosjava.android_remocons.common_tools.master.MasterId;
+import com.github.rosjava.android_remocons.common_tools.master.RoconDescription;
+import com.github.rosjava.android_remocons.common_tools.nfc.NfcManager;
+import com.github.rosjava.android_remocons.common_tools.nfc.NfcReaderActivity;
+import com.github.rosjava.android_remocons.common_tools.rocon.Constants;
+import com.github.rosjava.android_remocons.common_tools.system.WifiChecker;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.NFC_MASTER_HOST_FIELD_LENGTH;
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.NFC_PASSWORD_FIELD_LENGTH;
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.NFC_PAYLOAD_LENGTH;
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.NFC_SSID_FIELD_LENGTH;
+
+/**
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class NfcLauncherActivity extends NfcReaderActivity {
+
+ private enum Step {
+ STARTUP,
+ CONNECT_SSID,
+ CHECK_CONCERT,
+ START_CONCERT,
+ ABORT_LAUNCH;
+
+ public Step next() {
+ return this.ordinal() < Step.values().length - 1
+ ? Step.values()[this.ordinal() + 1]
+ : null;
+ }
+ }
+
+ private Step launchStep = Step.STARTUP;
+ private Toast lastToast;
+ private Vibrator vibrator;
+ private NfcManager nfcManager;
+
+ private String ssid;
+ private String password;
+ private String masterHost;
+ private short masterPort;
+ private MasterId masterId;
+ private RoconDescription concert;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ try {
+ vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
+ vibrator.vibrate(500);
+ toast(getString(R.string.app_name) + " started", Toast.LENGTH_SHORT);
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+ Log.d("NfcLaunch", action + " action started");
+
+ nfcManager = new NfcManager(this);
+ nfcManager.onNewIntent(intent);
+
+ if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action) == false)
+ {
+ // This should not append unless we are debugging
+ throw new Exception("Not started by NDEF_DISCOVERED action; this activity is only intended to run that way");
+ }
+
+ //** Step 1. Parsing NFC Data
+ parseNFCData();
+
+ Log.i("NfcLaunch", "NFC tag read");
+ toast("NFC tag read", Toast.LENGTH_SHORT);
+
+ //** Step 2. Connect to SSID
+ connectToSSID();
+
+ Log.i("NfcLaunch", "Connected to " + ssid);
+ toast("Connected to " + ssid, Toast.LENGTH_SHORT);
+
+ //** Step 3. Validate the concert: check for specific topics on masterUri
+ checkConcert();
+
+ Log.i("NfcLaunch", "Concert " + masterId.getMasterUri() + " up and running");
+ toast("Concert " + masterId.getMasterUri() + " up and running", Toast.LENGTH_SHORT);
+
+ //** Step 4. Start the concert!
+ startConcert();
+
+ //** Terminate this app
+ finish();
+ }
+ catch (Exception e) {
+ // TODO make and "error sound"
+ Log.e("NfcLaunch", e.getMessage());
+ toast(e.getMessage(), Toast.LENGTH_LONG);
+ finish();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ }
+
+ private void parseNFCData() throws Exception {
+ byte[] payload = nfcManager.getPayload();
+ if (payload.length != NFC_PAYLOAD_LENGTH + 3) // 1 byte for status and 2 lang bytes
+ {
+ throw new Exception("Payload doesn't match expected length: "
+ + payload.length +" != " + NFC_PAYLOAD_LENGTH);
+ }
+
+ int offset = 3; // skip 1 byte for status and 2 lang bytes
+ ssid = ByteArrays.toString(payload, offset, NFC_SSID_FIELD_LENGTH).trim();
+ offset += NFC_SSID_FIELD_LENGTH;
+ password = ByteArrays.toString(payload, offset, NFC_PASSWORD_FIELD_LENGTH).trim();
+ offset += NFC_PASSWORD_FIELD_LENGTH;
+ masterHost = ByteArrays.toString(payload, offset, NFC_MASTER_HOST_FIELD_LENGTH).trim();
+ offset += NFC_MASTER_HOST_FIELD_LENGTH;
+ masterPort = ByteArrays.toShort(payload, offset);
+
+ launchStep = launchStep.next();
+ }
+
+ private void connectToSSID() throws Exception {
+ String masterUri = "http://" + masterHost + ":" + masterPort;
+ String encryption = "WPA2"; // not needed
+ masterId = new MasterId(masterUri, ssid, encryption, password);
+
+ final WifiChecker wc = new WifiChecker(
+ new WifiChecker.SuccessHandler() {
+ public void handleSuccess() {
+ launchStep = launchStep.next();
+ }
+ },
+ new WifiChecker.FailureHandler() {
+ public void handleFailure(String reason) {
+ launchStep = Step.ABORT_LAUNCH;
+ }
+ },
+ new WifiChecker.ReconnectionHandler() {
+ public boolean doReconnection(String from, String to) {
+ // TODO should I ask for permit? maybe it's a bit rude to switch network without asking!
+ Log.i("NfcLaunch", "Switching from " + from + " to " + to);
+ toast("Switching from " + from + " to " + to, Toast.LENGTH_SHORT);
+ return true;
+ }
+ }
+ );
+ toast("Connecting to " + ssid + "...", Toast.LENGTH_SHORT);
+ wc.beginChecking(masterId, (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE));
+
+ if (waitFor(Step.CHECK_CONCERT, 15) == false) {
+ throw new Exception("Cannot connect to " + ssid + ". Aborting app launch");
+ }
+ }
+
+ private void checkConcert() throws Exception {
+ final ConcertChecker cc = new ConcertChecker(
+ new ConcertChecker.ConcertDescriptionReceiver() {
+ public void receive(RoconDescription concertDescription) {
+ concert = concertDescription;
+ if ( concert.getConnectionStatus() == RoconDescription.UNAVAILABLE ) {
+ // Check that it's not busy
+ Log.e("NfcLaunch", "Concert is unavailable: busy serving another remote controller");
+ launchStep = Step.ABORT_LAUNCH;
+ } else {
+ launchStep = launchStep.next();
+ }
+ }
+ },
+ new ConcertChecker.FailureHandler() {
+ public void handleFailure(String reason) {
+ Log.e("NfcLaunch", "Cannot contact ROS master: " + reason);
+ launchStep = Step.ABORT_LAUNCH;
+ }
+ }
+ );
+ toast("Validating " + masterId.getMasterUri() + "...", Toast.LENGTH_SHORT);
+ cc.beginChecking(masterId);
+
+ if (waitFor(Step.START_CONCERT, 10) == false) {
+ throw new Exception("Cannot connect to " + masterId.getMasterUri() + ". Aborting app launch");
+ }
+ }
+
+ private void startConcert() throws Exception {
+ Intent intent = new Intent("Remocon");
+ intent.putExtra(RoconDescription.UNIQUE_KEY, concert);
+ intent.putExtra(Constants.ACTIVITY_SWITCHER_ID + "." + InteractionMode.CONCERT + "_app_name", "NfcLauncher");
+ startActivity(intent);
+ }
+
+ private boolean waitFor(final Step step, final int timeout) {
+ AsyncTask asyncTask = new AsyncTask() {
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ while ((launchStep != step) && (launchStep != Step.ABORT_LAUNCH)) {
+ try { Thread.sleep(200); }
+ catch (InterruptedException e) { return false; }
+ }
+ return (launchStep == step); // returns false on ABORT_LAUNCH or InterruptedException
+ }
+ }.execute();
+ try {
+ return asyncTask.get(timeout, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Log.e("NfcLaunch", "Async task interrupted. " + e.getMessage());
+ return false;
+ } catch (ExecutionException e) {
+ Log.e("NfcLaunch", "Async task execution error. " + e.getMessage());
+ return false;
+ } catch (TimeoutException e) {
+ Log.e("NfcLaunch", "Async task timeout (" + timeout + " s). " + e.getMessage());
+ return false;
+ }
+ }
+
+ private void toast(final String message, final int length) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // We overwrite only short duration toast, as the long ones are normally important
+ if ((lastToast != null) && (lastToast.getDuration() == Toast.LENGTH_SHORT))
+ lastToast.cancel();
+ lastToast = Toast.makeText(getBaseContext(), message, length);
+ lastToast.show();
+ }
+ });
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/PairSubscriber.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/PairSubscriber.java
new file mode 100644
index 0000000..b803627
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/PairSubscriber.java
@@ -0,0 +1,97 @@
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.util.Log;
+
+import com.google.common.base.Preconditions;
+
+import org.ros.message.MessageListener;
+import org.ros.namespace.GraphName;
+import org.ros.node.ConnectedNode;
+import org.ros.node.Node;
+import org.ros.node.NodeMain;
+import org.ros.node.topic.Subscriber;
+
+import rocon_interaction_msgs.Pair;
+
+/**
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ *
+ * Publishes the remocon platform info and current role/app being run (if selected) in a latched topic.
+ * Singleton class, intended to survive along the whole remocon session, including go and back to apps.
+ */
+public class PairSubscriber implements NodeMain {
+ public static final String NODE_NAME = "remocon_pair_subscriber_node";
+
+ private static PairSubscriber instance = null;
+
+ private Subscriber pair_subsciber;
+
+ private boolean initialized = false;
+ private int paired_app_hash;
+ StatusPublisher statusPublisher = StatusPublisher.getInstance();
+
+
+ private PairSubscriber() {}
+
+ public static PairSubscriber getInstance() {
+ if (instance == null) {
+ instance = new PairSubscriber();
+ Log.i("Remocon", "Remocon pair subscriber created");
+ }
+
+ return instance;
+ }
+
+ public boolean isInitialized() {
+ return initialized;
+ }
+ public void setAppHash(int app_hash){
+ paired_app_hash = app_hash;
+ }
+
+ @Override
+ public GraphName getDefaultNodeName() {
+ return GraphName.of(NODE_NAME);
+ }
+
+ @Override
+ public void onStart(ConnectedNode connectedNode) {
+ Preconditions.checkArgument(! initialized, "Remocon pair subscriber already initialized");
+
+ paired_app_hash = 0;
+ pair_subsciber =
+ connectedNode.newSubscriber("interactions/pairing",Pair._TYPE);
+
+ pair_subsciber.addMessageListener(new MessageListener() {
+ @Override
+ public void onNewMessage(Pair pair) {
+ if (paired_app_hash != 0){
+ if(pair.getRemocon().equals(statusPublisher.REMOCON_FULL_NAME) && pair.getRapp().equals("")){
+ paired_app_hash = 0;
+ statusPublisher.update(false,paired_app_hash, null);
+ }
+ }
+ }
+ });
+
+ initialized = true;
+ Log.i("Remocon", "Remocon pair subscriber initialized");
+ }
+
+ @Override
+ public void onShutdown(Node node) {
+ Preconditions.checkArgument(initialized, "Remocon pair subscriber not initialized");
+ pair_subsciber.shutdown();
+ initialized = false;
+ Log.i("Remocon", "Remocon pair subscriber shutdown");
+ }
+
+ @Override
+ public void onShutdownComplete(Node node) {
+ }
+
+ @Override
+ public void onError(Node node, Throwable throwable) {
+ Log.e("Remocon", "Remocon status publisher error: " + throwable.getMessage());
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/Remocon.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/Remocon.java
new file mode 100644
index 0000000..8b173a1
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/Remocon.java
@@ -0,0 +1,657 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2011, Willow Garage, Inc.
+ * Copyright (c) 2013, OSRF.
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Button;
+import android.widget.GridView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.github.robotics_in_concert.rocon_rosjava_core.rocon_interactions.InteractionMode;
+import com.github.rosjava.android_remocons.common_tools.master.ConcertChecker;
+import com.github.rosjava.android_remocons.common_tools.master.MasterId;
+import com.github.rosjava.android_remocons.common_tools.master.RoconDescription;
+import com.github.rosjava.android_remocons.common_tools.rocon.AppLauncher;
+import com.github.rosjava.android_remocons.common_tools.rocon.Constants;
+import com.github.rosjava.android_remocons.common_tools.rocon.InteractionsManager;
+import com.github.rosjava.android_remocons.common_tools.system.WifiChecker;
+import com.github.rosjava.android_remocons.rocon_remocon.dialogs.AlertDialogWrapper;
+import com.github.rosjava.android_remocons.rocon_remocon.dialogs.LaunchInteractionDialog;
+import com.github.rosjava.android_remocons.rocon_remocon.dialogs.ProgressDialogWrapper;
+import com.google.common.base.Preconditions;
+
+import org.ros.android.RosActivity;
+import org.ros.exception.RemoteException;
+import org.ros.exception.RosRuntimeException;
+import org.ros.node.NodeConfiguration;
+import org.ros.node.NodeMainExecutor;
+import org.ros.node.service.ServiceResponseListener;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+import rocon_interaction_msgs.GetInteractionsResponse;
+import rocon_interaction_msgs.Interaction;
+
+/**
+ * An almost complete rewrite of RobotRemocon to work with rocon interactions.
+ *
+ * References to concert below should be refactored to interactions so as not to
+ * be confusing (it is now concert agnostic) (Daniel Stonier).
+ *
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ */
+public class Remocon extends RosActivity {
+
+ /* startActivityForResult Request Codes */
+ private static final int CONCERT_MASTER_CHOOSER_REQUEST_CODE = 1;
+
+ private Interaction selectedInteraction;
+ private String concertAppName = null;
+ private String defaultConcertAppName = null;
+ private RoconDescription roconDescription;
+ private NodeConfiguration nodeConfiguration;
+ private ArrayList availableAppsCache;
+ private TextView concertNameView;
+ private Button leaveConcertButton;
+ private LaunchInteractionDialog launchInteractionDialog;
+ private ProgressDialogWrapper progressDialog;
+ private AlertDialogWrapper wifiDialog;
+ private AlertDialogWrapper evictDialog;
+ private AlertDialogWrapper errorDialog;
+ private InteractionsManager interactionsManager;
+ private StatusPublisher statusPublisher;
+ private PairSubscriber pairSubscriber;
+ private boolean alreadyClicked = false;
+ private boolean validatedConcert;
+ private long availableAppsCacheTime;
+
+ /*
+ By default we assume the remocon has just launched independently, however
+ it can be launched upon the closure of one of its children applications.
+ */
+ private boolean fromApplication = false; // true if it is a remocon activity getting control from a closing app
+ private boolean fromNfcLauncher = false; // true if it is a remocon activity started by NfcLauncherActivity
+
+ public Remocon() {
+ super("Remocon", "Remocon");
+ availableAppsCacheTime = 0;
+ availableAppsCache = new ArrayList();
+ statusPublisher = StatusPublisher.getInstance();
+ pairSubscriber= PairSubscriber.getInstance();
+ pairSubscriber.setAppHash(0);
+ }
+
+ @Override
+ protected void init() {
+ Log.d("Remocon", "init()");
+ if (!fromApplication && !fromNfcLauncher) {
+ super.init();
+ } else {
+ Log.i("Remocon", "init() - returned from closing interaction, or started by Nfc launcher");
+ // In both cases we expect a concert description in the intent
+ if (getIntent().hasExtra(RoconDescription.UNIQUE_KEY)) {
+ Log.w("Remocon", "init() - successfully retrieved concert description key and moving to init(Intent)");
+ init(getIntent());
+ Log.i("Remocon", "Successfully retrieved concert description from the intent");
+ } else {
+ Log.e("Remocon", "Closing interaction didn't return the concert description");
+ // We are fucked-up in this case... TODO: recover or close all
+ }
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onResume();
+ if ( getIntent().getExtras() != null ) {
+ Log.i("Remocon", "onStart: " + Constants.ACTIVITY_SWITCHER_ID + "." + InteractionMode.CONCERT + "_app_name");
+ concertAppName = getIntent().getStringExtra(Constants.ACTIVITY_SWITCHER_ID + "." + InteractionMode.CONCERT + "_app_name");
+ if (concertAppName == null){
+ fromApplication = false;
+ fromApplication = false;
+ }
+ else{
+ if (concertAppName.equals("AppChooser")) { // TODO ugly legacy identifier, it's misleading so change it sometime
+ Log.i("Remocon", "got intent from a closing remocon application");
+ statusPublisher.update(false, 0, null);
+ fromApplication = true;
+ }
+ else if (concertAppName.equals("NfcLauncher")) {
+ Log.i("Remocon", "got intent from an Nfc launched application");
+ fromNfcLauncher = true;
+ }
+ }
+ }
+ super.onStart();
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.i("[Remocon]", "Oncreate");
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ setContentView(R.layout.rocon_remocon);
+ concertNameView = (TextView) findViewById(R.id.concert_name_view);
+ // Prepare the app manager; we do here instead of on init to keep using the same instance when switching roles
+ interactionsManager = new InteractionsManager(
+ new InteractionsManager.FailureHandler() {
+ public void handleFailure(String reason) {
+ Log.e("Remocon", "Failure on interactions manager: " + reason);
+ }
+ }
+ );
+ interactionsManager.setupGetInteractionsService(new ServiceResponseListener() {
+ @Override
+ public void onSuccess(rocon_interaction_msgs.GetInteractionsResponse response) {
+ List apps = response.getInteractions();
+ if (apps.size() > 0) {
+ availableAppsCache = (ArrayList) apps;
+ Log.i("Remocon", "Interaction Publication: " + availableAppsCache.size() + " apps");
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ updateAppList(availableAppsCache, roconDescription.getMasterName(), roconDescription.getCurrentRole());
+ progressDialog.dismiss();
+ }
+ });
+ } else {
+ // TODO: maybe I should notify the user... he will think something is wrong!
+ Log.w("Remocon", "No interactions available for the '" + roconDescription.getCurrentRole() + "' role.");
+ }
+ availableAppsCacheTime = System.currentTimeMillis();
+ }
+
+ @Override
+ public void onFailure(RemoteException e) {
+ progressDialog.dismiss();
+ Log.e("Remocon", "retrieve interactions for the role '"
+ + roconDescription.getCurrentRole() + "' failed: " + e.getMessage());
+ }
+ });
+ interactionsManager.setupRequestService(new ServiceResponseListener() {
+ @Override
+ public void onSuccess(rocon_interaction_msgs.RequestInteractionResponse response) {
+ Preconditions.checkNotNull(selectedInteraction);
+
+ final boolean allowed = response.getResult();
+ final String reason = response.getMessage();
+
+ boolean ret_launcher_dialog = false;
+ progressDialog.dismiss();
+
+ if(AppLauncher.checkAppType(selectedInteraction.getName()) == AppLauncher.AppType.NOTHING){
+ pairSubscriber.setAppHash(selectedInteraction.getHash());
+ ret_launcher_dialog = true;
+ }
+ else{
+ launchInteractionDialog.setup(selectedInteraction, allowed, reason);
+ if(allowed){
+ pairSubscriber.setAppHash(selectedInteraction.getHash());
+ }
+ else{
+ pairSubscriber.setAppHash(0);
+ }
+ ret_launcher_dialog = launchInteractionDialog.show();
+ }
+
+ if (ret_launcher_dialog) {
+ Log.i("Remocon", "Selected Launch button");
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+
+ AppLauncher.Result result =
+ AppLauncher.launch(Remocon.this, roconDescription, selectedInteraction);//启动App的返回结果
+ if (result == AppLauncher.Result.SUCCESS) {
+ // App successfully launched! Notify the concert and finish this activity
+ //statusPublisher.update(true, selectedInteraction.getHash(), selectedInteraction.getName());
+ // TODO try to no finish so statusPublisher remains while on app; risky, but seems to work! finish();
+ }
+ else if (result == AppLauncher.Result.NOTHING){
+ //statusPublisher.update(false, selectedInteraction.getHash(), selectedInteraction.getName());
+ }
+ else if (result == AppLauncher.Result.NOT_INSTALLED) {
+ // App not installed; ask for going to play store to download the missing app
+ statusPublisher.update(false, 0, null);
+ Log.i("Remocon", "Showing not-installed dialog.");
+ final String installPackage = selectedInteraction.getName().substring(0, selectedInteraction.getName().lastIndexOf("."));
+ selectedInteraction = null;
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(Remocon.this);
+ dialog.setIcon(R.drawable.playstore_icon_small);
+ dialog.setTitle("Android app not installed.");
+ dialog.setMessage("This interaction requires an android application to be installed.\n"
+ + "Would you like to install '" + installPackage + "' from the market place?");
+ dialog.setPositiveButton("Yes", new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface dlog, int i) {
+ Uri uri = Uri.parse("market://details?id=" + installPackage);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ Remocon.this.startActivity(intent);
+ }
+ });
+ dialog.setNegativeButton("No", new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface dlog, int i) {
+ dlog.dismiss();
+ }
+ });
+ dialog.show();
+ }
+ else {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(Remocon.this);
+ dialog.setIcon(R.drawable.failure_small);
+ dialog.setTitle("Cannot start app");
+ dialog.setMessage(result.message);
+ dialog.setPositiveButton("Accept", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dlog, int i) {
+ // nothing todo?
+ }
+ });
+ dialog.show();
+ }
+
+ };
+ });
+ }
+ else {
+ Log.i("[Remocon]","User select cancel");
+ statusPublisher.update(false, 0, null);
+ }
+ }
+
+ @Override
+ public void onFailure(RemoteException e) {
+ progressDialog.dismiss();
+ Log.e("Remocon", "Retrieve rapps for role "
+ + roconDescription.getCurrentRole() + " failed: " + e.getMessage());
+ }
+ });
+
+ pairSubscriber.setAppHash(0);
+ }
+
+ /**
+ * This gets processed as soon as the application returns it
+ * with a uri - this is either as a result of the master chooser
+ * or as in the case when it has been relaunched by an application,
+ * from intents set by the application.
+ *
+ * Here we configure the remocon environment for a particular concert,
+ * listing apps and providing the required triggers for interacting
+ * with that concert.
+ *
+ * @param nodeMainExecutor
+ */
+ @Override
+ protected void init(final NodeMainExecutor nodeMainExecutor) {
+ try {
+ java.net.Socket socket = new java.net.Socket(getMasterUri().getHost(), getMasterUri().getPort());
+ java.net.InetAddress local_network_address = socket.getLocalAddress();
+ socket.close();
+ NodeConfiguration nodeConfiguration =
+ NodeConfiguration.newPublic(local_network_address.getHostAddress(), getMasterUri());
+ interactionsManager.init(roconDescription.getInteractionsNamespace());
+ interactionsManager.getAppsForRole(roconDescription.getMasterId(), roconDescription.getCurrentRole());
+ interactionsManager.setRemoconName(statusPublisher.REMOCON_FULL_NAME);
+ progressDialog.show("Getting apps...",
+ "Retrieving interactions for the '" + roconDescription.getCurrentRole() + "' role");
+ //execution of publisher
+ if (! statusPublisher.isInitialized()) {
+ // If we come back from an app, it should be already initialized, so call execute again would crash
+ nodeMainExecutorService.execute(statusPublisher, nodeConfiguration.setNodeName(StatusPublisher.NODE_NAME));
+ }
+ //execution of subscriber
+ pairSubscriber.setAppHash(0);
+
+ if (! pairSubscriber.isInitialized()) {
+ // If we come back from an app, it should be already initialized, so call execute again would crash
+ nodeMainExecutorService.execute(pairSubscriber, nodeConfiguration.setNodeName(pairSubscriber.NODE_NAME));
+ }
+ } catch (IOException e) {
+ // Socket problem
+ }
+ }
+
+ /**
+ * Initialise from an intent triggered by either the returning concert
+ * chooser or a concert app.
+ *
+ * Validation will occur slightly differently in both cases - when
+ * returning from the app, it will assume its already
+ * in control and skip the invitation step (see validateConcert).
+ *
+ * This eventually passes control back to the generic init function
+ * (TODO: can probably incorporate this into that function).
+ *
+ * @param intent
+ */
+ void init(Intent intent) {
+ URI uri;
+ try {
+ roconDescription = (RoconDescription) intent.getSerializableExtra(RoconDescription.UNIQUE_KEY);
+ validatedConcert = false;
+ validateConcert(roconDescription.getMasterId());
+
+ uri = new URI(roconDescription.getMasterId().getMasterUri());
+ Log.i("Remocon", "init(Intent) - master uri is " + uri.toString());
+ } catch (ClassCastException e) {
+ Log.e("Remocon", "Cannot get concert description from intent. " + e.getMessage());
+ throw new RosRuntimeException(e);
+ } catch (URISyntaxException e) {
+ throw new RosRuntimeException(e);
+ }
+ nodeMainExecutorService.setMasterUri(uri);
+ // Run init() in a new thread as a convenience since it often
+ // requires network access. This would be more robust if it
+ // had a failure handler for uncontactable errors (override
+ // onPostExecute) that occurred when calling init. In reality
+ // this shouldn't happen often - only when the connection
+ // is unavailable inbetween validating and init'ing.
+ if (roconDescription.getCurrentRole() == null) {
+ chooseRole();
+ }
+ else {
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ while (!validatedConcert) {
+ // should use a sleep here to avoid burnout
+ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
+ }
+ Log.i("Remocon", "init(Intent) passing control back to init(nodeMainExecutorService)");
+ Remocon.this.init(nodeMainExecutorService);
+ return null;
+ }
+ }.execute();
+ }
+ }
+
+ private void chooseRole() {
+ Log.i("Remocon", "concert chosen; show choose user role dialog");
+ roconDescription.setCurrentRole(-1);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(Remocon.this);
+ builder.setTitle("Choose your role");
+ builder.setSingleChoiceItems(roconDescription.getUserRoles(), -1,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int selectedRole) {
+ roconDescription.setCurrentRole(selectedRole);
+ String role = roconDescription.getCurrentRole();
+ Toast.makeText(Remocon.this, role + " selected", Toast.LENGTH_SHORT).show();
+ dialog.dismiss();
+
+ new AsyncTask() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ while (!validatedConcert) {
+ // should use a sleep here to avoid burnout
+ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
+ }
+ Remocon.this.init(nodeMainExecutorService);
+ return null;
+ }
+ }.execute();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ /**
+ * The main result gathered here is that from the concert master chooser
+ * which is started on top of the initial Remocon Activity.
+ * This proceeds to then set the uri and trigger the init() calls.
+ *
+ * @param requestCode
+ * @param resultCode
+ * @param data
+ */
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ Log.i("Remocon", "onActivityResult [" + requestCode + "][" + resultCode + "]");
+ if (resultCode == RESULT_CANCELED) {
+ Log.i("Remocon", "activityResult...RESULT_CANCELLED");
+ finish();
+ } else if (resultCode == RESULT_OK) {
+ Log.i("Remocon", "activityResult...RESULT_OK");
+ if (requestCode == CONCERT_MASTER_CHOOSER_REQUEST_CODE) {
+ init(data);
+ } else {
+ // Without a master URI configured, we are in an unusable state.
+ nodeMainExecutorService.shutdown();
+ finish();
+ }
+ } else {
+ Log.w("Remocon", "activityResult...??? [" + resultCode + "]");
+ }
+ }
+
+ /**
+ * This is an override which diverts the usual startup once a node is
+ * connected. Typically this would go to the master chooser, however
+ * here we are sometimes returning from one of its child apps (in which
+ * case it doesn't have to go choosing a concert). In that case, send
+ * it directly to the concert validation and initialisation steps.
+ */
+ @Override
+ public void startMasterChooser() {
+ if (!fromApplication && !fromNfcLauncher) {
+ super.startActivityForResult(new Intent(this,
+ MasterChooser.class),
+ CONCERT_MASTER_CHOOSER_REQUEST_CODE);
+ }
+ }
+
+ public void validateConcert(final MasterId id) {
+ // TODO: why built here? and why recreate a builder, if wrapper already has?
+ launchInteractionDialog = new LaunchInteractionDialog(this);
+ wifiDialog = new AlertDialogWrapper(this, new AlertDialog.Builder(this)
+ .setTitle("Change Wifi?").setCancelable(false), "Yes", "No");
+ evictDialog = new AlertDialogWrapper(this,
+ new AlertDialog.Builder(this).setTitle("Evict User?")
+ .setCancelable(false), "Yes", "No");
+ errorDialog = new AlertDialogWrapper(this,
+ new AlertDialog.Builder(this).setTitle("Could Not Connect")
+ .setCancelable(false), "Ok");
+ progressDialog = new ProgressDialogWrapper(this);
+ final AlertDialogWrapper wifiDialog = new AlertDialogWrapper(this,
+ new AlertDialog.Builder(this).setTitle("Change Wifi?")
+ .setCancelable(false), "Yes", "No");
+
+ // Run a set of checkers in series. The last step must ensure the master is up.
+ final ConcertChecker cc = new ConcertChecker(
+ new ConcertChecker.ConcertDescriptionReceiver() {
+ public void receive(RoconDescription concertDescription) {
+ progressDialog.dismiss();
+ if(!fromApplication) {
+ // Check that it's not busy
+ if ( concertDescription.getConnectionStatus() == RoconDescription.UNAVAILABLE ) {
+ errorDialog.show("Concert is unavailable : busy serving another remote controller.");
+ errorDialog.dismiss();
+ startMasterChooser();
+ } else {
+ validatedConcert = true; // for us this is enough check!
+ }
+ } else { // fromApplication
+ // Working on the lovely assumption that we're already controlling the rapp manager
+ // since we come from a running app. Note that this code is run after platform info
+ // checks have been made (see MasterChecker).
+ validatedConcert = true;
+ }
+ }
+ }, new ConcertChecker.FailureHandler() {
+ public void handleFailure(String reason) {
+ final String reason2 = reason;
+ // Kill the connecting to ros master dialog.
+ progressDialog.dismiss();
+ errorDialog.show("Cannot contact ROS master: " + reason2);
+ errorDialog.dismiss();
+ // TODO : gracefully abort back to the concert master chooser instead.
+ finish();
+ }
+ });
+
+ // Ensure that the correct WiFi network is selected.
+ final WifiChecker wc = new WifiChecker(
+ new WifiChecker.SuccessHandler() {
+ public void handleSuccess() {
+ progressDialog.show("Checking...", "Starting connection process");
+ cc.beginChecking(id);
+ }
+ }, new WifiChecker.FailureHandler() {
+ public void handleFailure(String reason) {
+ final String reason2 = reason;
+ progressDialog.dismiss();
+ errorDialog.show("Cannot connect to concert WiFi: " + reason2);
+ errorDialog.dismiss();
+ finish();
+ }
+ }, new WifiChecker.ReconnectionHandler() {
+ public boolean doReconnection(String from, String to) {
+ progressDialog.dismiss();
+ if (from == null) {
+ wifiDialog.setMessage("To interact with this master, you must connect to " + to
+ + "\nDo you want to connect to " + to + "?");
+ } else {
+ wifiDialog.setMessage("To interact with this master, you must switch wifi networks"
+ + "\nDo you want to switch from " + from + " to " + to + "?");
+ }
+
+ progressDialog.show("Checking...", "Switching wifi networks");
+ return wifiDialog.show();
+ }
+ });
+ progressDialog.show("Connecting...", "Checking wifi connection");
+ wc.beginChecking(id, (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE));
+ }
+
+ protected void updateAppList(final ArrayList apps, final String master_name, final String role) {
+ Log.d("Remocon", "updating app list gridview");
+ selectedInteraction = null;
+ concertNameView.setText(master_name + " - " + role);
+ GridView gridview = (GridView) findViewById(R.id.gridview);
+ AppAdapter appAdapter = new AppAdapter(Remocon.this, apps);
+ gridview.setAdapter(appAdapter);
+ registerForContextMenu(gridview);
+ gridview.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View v, int position, long id) {
+ selectedInteraction = apps.get(position);
+ progressDialog.show("Requesting app...", "Requesting permission to use "
+ + selectedInteraction.getDisplayName());
+ interactionsManager.requestAppUse(roconDescription.getMasterId(), role, selectedInteraction);
+ statusPublisher.update(true, selectedInteraction.getHash(), selectedInteraction.getName());
+
+ }
+ });
+ Log.d("Remocon", "app list gridview updated");
+ }
+
+ /**
+ * Choose a new role and get the apps associated to the new roll.
+ */
+ public void changeRoleClicked(View view) {
+ chooseRole();
+ }
+
+ /**
+ * This returns the activity to the concert master chooser
+ * activity. It will get triggered via either a backpress
+ * or the button provided in the Remocon activity.
+ */
+ public void leaveConcertClicked(View view) {
+ availableAppsCache.clear();
+ startActivityForResult(new Intent(this, MasterChooser.class),
+ CONCERT_MASTER_CHOOSER_REQUEST_CODE);
+
+ nodeMainExecutorService.shutdownNodeMain(statusPublisher);
+ nodeMainExecutorService.shutdownNodeMain(pairSubscriber);
+
+ interactionsManager.shutdown();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(0, 0, 0, R.string.exit);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ super.onOptionsItemSelected(item);
+ switch (item.getItemId()) {
+ case 0:
+ finish();
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public void onBackPressed() {
+ Log.i("Remocon", "Press Back Button");
+ leaveConcertClicked(null);
+ }
+
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/StatusPublisher.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/StatusPublisher.java
new file mode 100644
index 0000000..bf890db
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/StatusPublisher.java
@@ -0,0 +1,130 @@
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.util.Log;
+
+import com.google.common.base.Preconditions;
+
+import org.ros.namespace.GraphName;
+import org.ros.node.ConnectedNode;
+import org.ros.node.Node;
+import org.ros.node.NodeMain;
+import org.ros.node.topic.Publisher;
+
+import java.util.UUID;
+
+import rocon_interaction_msgs.RemoconStatus;
+import rocon_std_msgs.Strings;
+
+import static com.github.rosjava.android_remocons.common_tools.rocon.Constants.ANDROID_PLATFORM_INFO;
+
+/**
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ *
+ * Publishes the remocon platform info and current role/app being run (if selected) in a latched topic.
+ * Singleton class, intended to survive along the whole remocon session, including go and back to apps.
+ */
+public class StatusPublisher implements NodeMain {
+ public static final String NODE_NAME = "remocon_status_pub_node";
+
+ private static final String REMOCON_NAME = "android_remocon";
+ private static final String REMOCON_UUID = UUID.randomUUID().toString().replaceAll("-", "");
+ public static final String REMOCON_FULL_NAME = REMOCON_NAME+"_"+REMOCON_UUID;
+ public static final String ROCON_VERSION = Strings.ROCON_VERSION;
+
+ private static StatusPublisher instance;
+
+ private Publisher publisher;
+
+ private boolean initialized = false;
+ private RemoconStatus status;
+
+ private StatusPublisher() {}
+
+ public static StatusPublisher getInstance() {
+ if (instance == null) {
+ instance = new StatusPublisher();
+ Log.d("RoconRemocon", "Remocon status publisher created");
+ }
+
+ return instance;
+ }
+
+ public boolean isInitialized() {
+ return initialized;
+ }
+
+ @Override
+ public GraphName getDefaultNodeName() {
+ return GraphName.of(NODE_NAME);
+ }
+
+ @Override
+ public void onStart(ConnectedNode connectedNode) {
+ Preconditions.checkArgument(! initialized, "Remocon status publisher already initialized");
+
+ // Prepare latched publisher
+ publisher = connectedNode.newPublisher("/remocons/" + REMOCON_NAME + "_" + REMOCON_UUID,
+ rocon_interaction_msgs.RemoconStatus._TYPE);
+ publisher.setLatchMode(true);
+ int[] running_interactions = new int[0];
+
+ // Prepare and publish default status; platform info and uuid remain for the whole session
+ status = publisher.newMessage();
+ status.setPlatformInfo(ANDROID_PLATFORM_INFO);
+
+// status.getPlatformInfo().setName(REMOCON_NAME);
+// status.getPlatformInfo().setOs(PlatformInfo.OS_ANDROID);
+// status.getPlatformInfo().setVersion(PlatformInfo.VERSION_ANDROID_JELLYBEAN);
+// status.getPlatformInfo().setPlatform(PlatformInfo.PLATFORM_TABLET);
+// status.getPlatformInfo().setSystem(PlatformInfo..SYSTEM_ROSJAVA);
+// status.getPlatformInfo().setName(REMOCON_NAME);
+// status.getUuid().getUuid().setByte(0, 0);//REMOCON_UUID.getBytes());
+
+ // TODO hack! uuid is a byte[16] array but like this it fails msg delivery! must be cause the weird rosjava version of byte[] reserves 255 bytes buffer
+ status.setVersion(ROCON_VERSION);
+ status.setUuid(REMOCON_UUID);
+ status.setRunningInteractions(running_interactions);
+ //status.setRunningApp(false);
+ //status.setAppName(""); // not yet implemented
+
+ publisher.publish(status);
+
+ initialized = true;
+ Log.i("Remocon", "Remocon status publisher initialized");
+ }
+
+ @Override
+ public void onShutdown(Node node) {
+ Preconditions.checkArgument(initialized, "Remocon status publisher not initialized");
+
+ publisher.shutdown();
+ initialized = false;
+ Log.i("Remocon", "Remocon status publisher shutdown");
+ }
+
+ @Override
+ public void onShutdownComplete(Node node) {
+ }
+
+ @Override
+ public void onError(Node node, Throwable throwable) {
+ Log.e("Remocon", "Remocon status publisher error: " + throwable.getMessage());
+ }
+
+ public void update(boolean is_runnging, int runningApp_hash, String appName) {
+
+ int[] running_interactions = null;
+ if (is_runnging) {
+ Log.i("Remocon", "Remocon status publisher updated. Running "+appName+": " + runningApp_hash);
+ running_interactions = new int[1];
+ running_interactions[0] = runningApp_hash;
+ status.setRunningInteractions(running_interactions);
+ }
+ else{
+ Log.i("Remocon", "Remocon status publisher updated. Fail running "+appName+": " + runningApp_hash);
+ running_interactions = new int[0];
+ status.setRunningInteractions(running_interactions);
+ }
+ publisher.publish(status);
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/TestActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/TestActivity.java
new file mode 100644
index 0000000..62d3173
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/TestActivity.java
@@ -0,0 +1,15 @@
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+
+public class TestActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_test);
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/WelcomActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/WelcomActivity.java
new file mode 100644
index 0000000..caf250c
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/WelcomActivity.java
@@ -0,0 +1,54 @@
+package com.github.rosjava.android_remocons.rocon_remocon;
+
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.widget.TextView;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * Created by wyj on 2018/5/30.
+ */
+
+public class WelcomActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_welcome);
+ //设置此界面为
+ // 竖屏
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ init();
+ }
+
+ private void init() {
+ TextView copyright = findViewById(R.id.copyright);
+ try {
+ PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(),0);
+ copyright.setText("Copyright © 2018 GSQ Team All rights reserved. ");
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ //tv_version.setText("version");
+ }
+ //利用timer让此界面延迟3秒后跳转,timer有一个线程,该线程不断执行task
+ Timer timer = new Timer();
+ TimerTask timerTask = new TimerTask() {
+ @Override
+ public void run() {
+ //发送intent实现页面跳转,第一个参数为当前页面的context,第二个参数为要跳转的主页
+ Intent intent = new Intent(WelcomActivity.this,Remocon.class);
+ startActivity(intent);
+ //跳转后关闭当前欢迎页面
+ WelcomActivity.this.finish();
+ }
+ };
+ //调度执行timerTask,第二个参数传入延迟时间(毫秒)
+ timer.schedule(timerTask,3000);
+
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/dialogs/AlertDialogWrapper.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/dialogs/AlertDialogWrapper.java
new file mode 100644
index 0000000..d21745c
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/dialogs/AlertDialogWrapper.java
@@ -0,0 +1,123 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2013, Yujin Robot.
+ *
+ * All rights reserved.
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of Willow Garage, Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.github.rosjava.android_remocons.rocon_remocon.dialogs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+
+/**
+ * @author jorge@yujinrobot.com (Jorge Santos Simon)
+ *
+ * Wraps the alert dialog so it can be used as a yes/no function
+ */
+public class AlertDialogWrapper {
+ protected int state;
+ protected AlertDialog dialog;
+ protected Activity context;
+ protected boolean enablePositive = true;
+
+ public AlertDialogWrapper(Activity context,
+ AlertDialog.Builder builder, String yesButton, String noButton) {
+ state = 0;
+ this.context = context;
+ dialog = builder
+ .setPositiveButton(yesButton,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ state = 1;
+ }
+ })
+ .setNegativeButton(noButton,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ state = 2;
+ }
+ }).create();
+ }
+
+ public AlertDialogWrapper(Activity context,
+ AlertDialog.Builder builder, String okButton) {
+ state = 0;
+ this.context = context;
+ dialog = builder.setNeutralButton(okButton,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ state = 1;
+ }
+ }).create();
+ }
+
+ public void setTitle(String m) {
+ dialog.setTitle(m);
+ }
+
+ public void setMessage(String m) {
+ dialog.setMessage(m);
+ }
+
+ public boolean show(String m) {
+ setMessage(m);
+ return show();
+ }
+
+ public boolean show() {
+ state = 0;
+ context.runOnUiThread(new Runnable() {
+ public void run() {
+ dialog.show();
+ dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(enablePositive);
+ }
+ });
+ // Kind of a hack. Do we know a better way?
+ while (state == 0) {
+ try {
+ Thread.sleep(1L);
+ } catch (Exception e) {
+ break;
+ }
+ }
+ dismiss();
+ return state == 1;
+ }
+
+ public void dismiss() {
+ if (dialog != null) {
+ dialog.dismiss();
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/dialogs/LaunchInteractionDialog.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/dialogs/LaunchInteractionDialog.java
new file mode 100644
index 0000000..6bd4a0c
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/dialogs/LaunchInteractionDialog.java
@@ -0,0 +1,59 @@
+package com.github.rosjava.android_remocons.rocon_remocon.dialogs;
+
+/**
+ * Created by jorge on 11/7/13.
+ */
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.Log;
+
+import org.jboss.netty.buffer.ChannelBuffer;
+
+/**
+ * Custom alert dialog to prompt launching an app
+ */
+public class LaunchInteractionDialog extends AlertDialogWrapper {
+ public LaunchInteractionDialog(Activity context) {
+ super(context, new AlertDialog.Builder(context), "Launch", "Cancel");
+ }
+
+ public void setup(rocon_interaction_msgs.Interaction app, boolean allowed, String reason) {
+
+ boolean hasValidIcon = false;
+ String iconFormat = app.getIcon().getFormat();
+ ChannelBuffer buffer = app.getIcon().getData();
+
+ if (buffer.array().length > 0 && iconFormat != null &&
+ (iconFormat.equals("jpeg") || iconFormat.equals("png"))) {
+ Bitmap bitmap =
+ BitmapFactory.decodeByteArray(buffer.array(), buffer.arrayOffset(), buffer.readableBytes());
+ if (bitmap != null) {
+ Bitmap iconBitmap = Bitmap.createScaledBitmap(bitmap, 180, 180, false);
+ dialog.setIcon(new BitmapDrawable(context.getResources(), iconBitmap));
+ hasValidIcon = true;
+ }
+ }
+
+ if (! hasValidIcon) {
+ // dialog.setIcon(context.getResources().getDrawable(R.drawable.question_mark));
+ // TODO default image? concert icon?
+ }
+
+ dialog.setTitle(app.getDisplayName());
+
+ if (allowed) {
+ Log.i("Remocon", "This interaction is permitted [" + reason + "]");
+ dialog.setMessage(app.getDescription() + "\n\nLovely, you are allowed to launch this interaction.");
+ enablePositive = true;
+ }
+ else {
+ Log.i("Remocon", "This interaction is not permitted [" + reason + "]");
+ dialog.setMessage(app.getDescription() + "\n\nDude, this is not allowed - what did you do?\n" + reason);
+ enablePositive = false;
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/dialogs/ProgressDialogWrapper.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/dialogs/ProgressDialogWrapper.java
new file mode 100644
index 0000000..da8efff
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/dialogs/ProgressDialogWrapper.java
@@ -0,0 +1,49 @@
+package com.github.rosjava.android_remocons.rocon_remocon.dialogs;
+
+/**
+ * Created by jorge on 11/7/13.
+ */
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.util.Log;
+
+/**
+ * Wraps the progress dialog so it can be used to show/vanish easily
+ */
+public class ProgressDialogWrapper {
+ private ProgressDialog progressDialog;
+ private Activity activity;
+
+ public ProgressDialogWrapper(Activity activity) {
+ this.activity = activity;
+ progressDialog = null;
+ }
+
+ public void dismiss() {
+ Log.d("Remocon", "Stopping the spinner");
+ activity.runOnUiThread(new Runnable() {
+ public void run() {
+ if (progressDialog != null) {
+ progressDialog.dismiss();
+ progressDialog = null;
+ }
+ }
+ });
+ }
+
+ public void show(final String title, final String text) {
+ activity.runOnUiThread(new Runnable() {
+ public void run() {
+ if (progressDialog != null) {
+ Log.d("Remocon", "Restarting the spinner with a new message");
+ progressDialog.dismiss();
+ }
+
+ progressDialog = ProgressDialog.show(activity, title, text, true, true);
+ progressDialog.setCancelable(false);
+ progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ }
+ });
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/FallDetectedActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/FallDetectedActivity.java
new file mode 100644
index 0000000..5067960
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/FallDetectedActivity.java
@@ -0,0 +1,244 @@
+ package com.github.rosjava.android_remocons.rocon_remocon.fall_dectect;
+
+/**
+ * Created by wyj on 2018/5/16.
+ */
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.os.Bundle;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.animation.AccelerateInterpolator;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import io.codetail.animation.SupportAnimator;
+import io.codetail.animation.ViewAnimationUtils;
+import yalantis.com.sidemenu.interfaces.Resourceble;
+import yalantis.com.sidemenu.interfaces.ScreenShotable;
+import yalantis.com.sidemenu.model.SlideMenuItem;
+import com.github.rosjava.android_remocons.rocon_remocon.fall_dectect.fragment.ContentFragment;
+import yalantis.com.sidemenu.util.ViewAnimator;
+
+public class FallDetectedActivity extends AppCompatActivity implements ViewAnimator.ViewAnimatorListener {
+ private DrawerLayout drawerLayout; //抽屉布局
+ private ActionBarDrawerToggle drawerToggle;
+ private List list = new ArrayList<>();
+ private ContentFragment contentFragment;
+ private ViewAnimator viewAnimator;
+ private int res = R.drawable.fall_pic2;//设置图片
+ private LinearLayout linearLayout;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.fall_detect);//启动摔倒检测的界面
+ contentFragment = ContentFragment.newInstance(R.drawable.fall_pic2);
+ getSupportFragmentManager().beginTransaction()
+ .replace(R.id.content_frame, contentFragment)
+ .commit();
+ drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ drawerLayout.setScrimColor(Color.TRANSPARENT);
+ linearLayout = (LinearLayout) findViewById(R.id.left_drawer);
+ linearLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ drawerLayout.closeDrawers();
+ }
+ });
+ setActionBar();
+ createMenuList();
+ viewAnimator = new ViewAnimator<>(this, list, contentFragment, drawerLayout, this);
+
+ Button buttonStartDetect=(Button)findViewById(R.id.StartFallDetect);
+ buttonStartDetect.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ AlertDialog.Builder alertdialogbuilder=new AlertDialog.Builder(FallDetectedActivity.this);
+ alertdialogbuilder.setMessage("您确定开启摔倒检测吗?");
+ alertdialogbuilder.setPositiveButton("我确定", click1);
+ alertdialogbuilder.setNegativeButton("再考虑一下", click2);
+ AlertDialog alertdialog1=alertdialogbuilder.create();
+ alertdialog1.show();
+ }
+ });
+
+ Button buttonStopDetect=(Button)findViewById(R.id.CloseFallDetect);
+ buttonStopDetect.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ AlertDialog.Builder alertdialogbuilder=new AlertDialog.Builder(FallDetectedActivity.this);
+ alertdialogbuilder.setMessage("您确定关闭摔倒检测吗?");
+ alertdialogbuilder.setPositiveButton("我确定", click2);
+ alertdialogbuilder.setNegativeButton("再考虑一下", click2);
+ AlertDialog alertdialog1=alertdialogbuilder.create();
+ alertdialog1.show();
+ }
+ });
+
+ }
+ private DialogInterface.OnClickListener click1=new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface arg0,int arg1)
+ {
+ Intent intent = new Intent(FallDetectedActivity.this, NewListener.class);
+ startActivity(intent);
+ }
+ };
+ private DialogInterface.OnClickListener click2=new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface arg0,int arg1)
+ {
+ arg0.cancel();
+ }
+ };
+ private void createMenuList() {//设置侧边菜单栏的布局和图片
+ SlideMenuItem menuItem0 = new SlideMenuItem(ContentFragment.CLOSE, R.drawable.icn_close);
+ list.add(menuItem0);
+ SlideMenuItem menuItem = new SlideMenuItem(ContentFragment.BUILDING, R.drawable.icn_1);
+ list.add(menuItem);
+ SlideMenuItem menuItem2 = new SlideMenuItem(ContentFragment.BOOK, R.drawable.icn_2);
+ list.add(menuItem2);
+ SlideMenuItem menuItem3 = new SlideMenuItem(ContentFragment.PAINT, R.drawable.icn_3);
+ list.add(menuItem3);
+ SlideMenuItem menuItem4 = new SlideMenuItem(ContentFragment.CASE, R.drawable.icn_4);
+ list.add(menuItem4);
+ SlideMenuItem menuItem5 = new SlideMenuItem(ContentFragment.SHOP, R.drawable.icn_5);
+ list.add(menuItem5);
+ SlideMenuItem menuItem6 = new SlideMenuItem(ContentFragment.PARTY, R.drawable.icn_6);
+ list.add(menuItem6);
+ SlideMenuItem menuItem7 = new SlideMenuItem(ContentFragment.MOVIE, R.drawable.icn_7);
+ list.add(menuItem7);
+ }
+
+
+ private void setActionBar() {//设置活动Bar
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setHomeButtonEnabled(true);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ drawerToggle = new ActionBarDrawerToggle(
+ this, /* host Activity */
+ drawerLayout, /* DrawerLayout object */
+ toolbar, /* nav drawer icon to replace 'Up' caret */
+ R.string.drawer_open, /* "open drawer" description */
+ R.string.drawer_close /* "close drawer" description */
+ ) {
+
+ /** Called when a drawer has settled in a completely closed state. */
+ public void onDrawerClosed(View view) {
+ super.onDrawerClosed(view);
+ linearLayout.removeAllViews();
+ linearLayout.invalidate();
+ }
+
+ @Override
+ public void onDrawerSlide(View drawerView, float slideOffset) {
+ super.onDrawerSlide(drawerView, slideOffset);
+ if (slideOffset > 0.6 && linearLayout.getChildCount() == 0)
+ viewAnimator.showMenuContent();
+ }
+
+ /** Called when a drawer has settled in a completely open state. */
+ public void onDrawerOpened(View drawerView) {
+ super.onDrawerOpened(drawerView);
+ }
+ };
+ drawerLayout.setDrawerListener(drawerToggle);
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ drawerToggle.syncState();
+ }
+
+ //配置改变的响应
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ drawerToggle.onConfigurationChanged(newConfig);
+ }
+
+ //选择设置的菜单
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_main, menu);
+ return true;
+ }
+
+ //菜单选择操作
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (drawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ }
+ switch (item.getItemId()) {
+ case R.id.action_settings:
+ return true;
+ // wyj
+ case android.R.id.home:
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private ScreenShotable replaceFragment(ScreenShotable screenShotable, int topPosition) {
+ this.res = this.res == R.drawable.fall_pic2 ? R.drawable.fall_pic: R.drawable.fall_pic2;
+ View view = findViewById(R.id.content_frame);
+ int finalRadius = Math.max(view.getWidth(), view.getHeight());
+ SupportAnimator animator = ViewAnimationUtils.createCircularReveal(view, 0, topPosition, 0, finalRadius);
+ animator.setInterpolator(new AccelerateInterpolator());
+ animator.setDuration(ViewAnimator.CIRCULAR_REVEAL_ANIMATION_DURATION);
+
+ findViewById(R.id.content_overlay).setBackgroundDrawable(new BitmapDrawable(getResources(), screenShotable.getBitmap()));
+ animator.start();
+ ContentFragment contentFragment = ContentFragment.newInstance(this.res);
+ getSupportFragmentManager().beginTransaction().replace(R.id.content_frame, contentFragment).commit();
+ return contentFragment;
+ }
+
+ @Override
+ public ScreenShotable onSwitch(Resourceble slideMenuItem, ScreenShotable screenShotable, int position) {
+ switch (slideMenuItem.getName()) {
+ case ContentFragment.CLOSE:
+ return screenShotable;
+ default:
+ return replaceFragment(screenShotable, position);
+ }
+ }
+
+ @Override
+ public void disableHomeButton() {
+ getSupportActionBar().setHomeButtonEnabled(true);
+
+ }
+
+ @Override
+ public void enableHomeButton() {
+ getSupportActionBar().setHomeButtonEnabled(false);
+ drawerLayout.closeDrawers();
+
+ }
+
+ @Override
+ public void addViewToContainer(View view) {
+ linearLayout.addView(view);
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/FallDialog.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/FallDialog.java
new file mode 100644
index 0000000..27e7ebf
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/FallDialog.java
@@ -0,0 +1,61 @@
+package com.github.rosjava.android_remocons.rocon_remocon.fall_dectect;
+
+/**
+ * Created by wyj on 2018/5/7.
+ */
+
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.os.Bundle;
+import android.view.View;
+
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+import com.github.rosjava.android_remocons.rocon_remocon.Remocon;
+import com.github.rosjava.android_remocons.rocon_remocon.WelcomActivity;
+
+public class FallDialog extends AppCompatActivity {
+ /*本类实现摔倒后弹出摔倒提示对话框的功能*/
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ //生成一个对话框
+ AlertDialog.Builder alertdialogbuilder=new AlertDialog.Builder(this);
+ alertdialogbuilder.setMessage("老人摔倒了");
+ alertdialogbuilder.setPositiveButton("我知道了", click1);
+ alertdialogbuilder.setNegativeButton("继续监测摔倒情况", click2);
+ AlertDialog alertdialog1=alertdialogbuilder.create();
+ alertdialog1.show();
+ //setContentView(R.layout.fall_alert);
+ }
+
+ public void showdialog(View view)
+ {
+ //Toast.makeText(this,"clickme",Toast.LENGTH_LONG).show();
+
+ }
+
+ private DialogInterface.OnClickListener click1=new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface arg0,int arg1)
+ {
+ //响应点击返回摔倒检测的主界面
+ Intent intent = new Intent(FallDialog.this,FallDetectedActivity.class);
+ startActivity(intent);
+ //android.os.Process.killProcess(android.os.Process.myPid());
+ }
+ };
+ private DialogInterface.OnClickListener click2=new DialogInterface.OnClickListener()
+ {
+ //响应点击返回继续监听的界面
+ @Override
+ public void onClick(DialogInterface arg0,int arg1)
+ {
+ Intent intent = new Intent(FallDialog.this,NewListener.class);
+ startActivity(intent);
+ arg0.cancel();
+ }
+ };
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/NewListener.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/NewListener.java
new file mode 100644
index 0000000..97fb8ee
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/NewListener.java
@@ -0,0 +1,116 @@
+package com.github.rosjava.android_remocons.rocon_remocon.fall_dectect;
+
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.github.rosjava.android_remocons.common_tools.apps.RosAppActivity;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+import com.github.rosjava.android_remocons.rocon_remocon.Remocon;
+import com.github.rosjava.android_remocons.rocon_remocon.WelcomActivity;
+
+import org.ros.android.RosActivity;
+import org.ros.node.NodeConfiguration;
+import org.ros.node.NodeMainExecutor;
+
+import java.io.IOException;
+
+/*本类实现了检测摔倒事件的发生*/
+public class NewListener extends RosAppActivity
+{
+ private subscriber sc;
+ boolean alertConfirm;
+
+ public NewListener()
+ {
+ super("listener", "listener");
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ setDefaultMasterName(getString(R.string.default_robot));
+ setDashboardResource(R.id.top_bar);
+ setMainWindowResource(R.layout.listener);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void init(NodeMainExecutor nodeMainExecutor)
+ {
+ //super.init(nodeMainExecutor);
+ try {
+ //建立socket连接将机器人端与安卓端联系在一起,实现信息的沟通
+ Thread.sleep(1000);
+ java.net.Socket socket = new java.net.Socket(getMasterUri().getHost(), getMasterUri().getPort());
+ java.net.InetAddress local_network_address = socket.getLocalAddress();
+ socket.close();
+
+ //声明一个新的subscriber对象
+ sc = new subscriber();
+ //配置网络
+ NodeConfiguration nodeConfiguration =
+ NodeConfiguration.newPublic(local_network_address.getHostAddress(), getMasterUri());
+ //处理sc的内容
+ nodeMainExecutor.execute(sc, nodeConfiguration);
+ Log.i("listener", "I heard msg from ubuntu123456 : \"" + sc.getAlertText() + "\"");
+ alertConfirm = true;
+ while(alertConfirm) {
+ //sc.getAlertText();
+ Log.i("listener", "I heard msg from ubuntu : \"" + sc.getAlertText() + "\"");
+ if (sc.getAlertText() != null) {
+ Log.e("listener", "收到了");
+ Intent intent = new Intent(NewListener.this,FallDialog.class);
+ startActivity(intent);
+ alertConfirm = false;
+ }
+ }
+ } catch(InterruptedException e) {
+ // Thread interruption
+ } catch (IOException e) {
+ // Socket problem
+ }
+ }
+ private DialogInterface.OnClickListener click1=new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface arg0,int arg1)
+ {
+ android.os.Process.killProcess(android.os.Process.myPid());
+ }
+ };
+ private DialogInterface.OnClickListener click2=new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface arg0,int arg1)
+ {
+ arg0.cancel();
+ }
+ };
+
+ //重构父类函数
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ menu.add(0,0,0,R.string.stop_app);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ //重构父类函数
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ super.onOptionsItemSelected(item);
+ switch (item.getItemId()){
+ case 0:
+ finish();
+ break;
+ }
+ return true;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/fragment/ContentFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/fragment/ContentFragment.java
new file mode 100644
index 0000000..a5b75ff
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/fragment/ContentFragment.java
@@ -0,0 +1,88 @@
+package com.github.rosjava.android_remocons.rocon_remocon.fall_dectect.fragment;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import yalantis.com.sidemenu.interfaces.ScreenShotable;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+
+/**
+ * Created by Konstantin on 22.12.2014.
+ */
+public class ContentFragment extends Fragment implements ScreenShotable {
+ public static final String CLOSE = "Close";
+ public static final String BUILDING = "Building";
+ public static final String BOOK = "Book";
+ public static final String PAINT = "Paint";
+ public static final String CASE = "Case";
+ public static final String SHOP = "Shop";
+ public static final String PARTY = "Party";
+ public static final String MOVIE = "Movie";
+
+ private View containerView;
+ protected ImageView mImageView;
+ protected int res;
+ private Bitmap bitmap;
+
+ public static ContentFragment newInstance(int resId) {
+ ContentFragment contentFragment = new ContentFragment();
+ Bundle bundle = new Bundle();
+ bundle.putInt(Integer.class.getName(), resId);
+ contentFragment.setArguments(bundle);
+ return contentFragment;
+ }
+
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ this.containerView = view.findViewById(R.id.container);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ res = getArguments().getInt(Integer.class.getName());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_main, container, false);
+ mImageView = (ImageView) rootView.findViewById(R.id.image_content);
+ mImageView.setClickable(true);
+ mImageView.setFocusable(true);
+ mImageView.setImageResource(res);
+ return rootView;
+ }
+
+ @Override
+ public void takeScreenShot() {
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ Bitmap bitmap = Bitmap.createBitmap(containerView.getWidth(),
+ containerView.getHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ containerView.draw(canvas);
+ ContentFragment.this.bitmap = bitmap;
+ }
+ };
+
+ thread.start();
+
+ }
+
+ @Override
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+}
+
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/listener.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/listener.java
new file mode 100644
index 0000000..7f85c9b
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/listener.java
@@ -0,0 +1,138 @@
+package com.github.rosjava.android_remocons .rocon_remocon.fall_dectect;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.EditText;
+import android.widget.Toast;
+import android.support.v7.app.AlertDialog;
+
+import com.github.rosjava.android_remocons.common_tools.apps.RosAppActivity;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+
+import org.ros.android.view.RosTextView;
+import org.ros.node.ConnectedNode;
+import org.ros.node.NodeConfiguration;
+import org.ros.node.NodeMainExecutor;
+
+import java.io.IOException;
+
+import std_msgs.String;
+
+public class listener extends RosAppActivity
+{
+ private Toast lastToast;
+ private ConnectedNode node;
+ private RosTextView rosTextView;
+ //private subscriber sb;
+
+ public listener()
+ {
+ super("listener", "listener");
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ setDefaultMasterName(getString(R.string.default_robot));
+ setDashboardResource(R.id.top_bar);
+ setMainWindowResource(R.layout.listener);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void init(NodeMainExecutor nodeMainExecutor)
+ {
+ //String chatterTopic = remaps.get(getString(R.string.chatter_topic));
+ super.init(nodeMainExecutor);
+
+ /*rosTextView = (RosTextView) findViewById(R.id.text);
+ rosTextView.setTopicName(getMasterNameSpace().resolve(chatterTopic).toString());
+ rosTextView.setMessageType(std_msgs.String._TYPE);
+ rosTextView.setMessageToStringCallable(new MessageCallable() {
+ @Override
+ public java.lang.String call(std_msgs.String message) {
+ Log.e("Listener", "received a message [" + message.getData() + "]");
+ return message.getData();
+ }
+ });*/
+ try {
+ // Really horrible hack till I work out exactly the root cause and fix for
+ // https://github.com/rosjava/android_remocons/issues/47
+ Thread.sleep(1000);
+ java.net.Socket socket = new java.net.Socket(getMasterUri().getHost(), getMasterUri().getPort());
+ java.net.InetAddress local_network_address = socket.getLocalAddress();
+ socket.close();
+ Log.i("123", "I heard msg from ubuntu : \" \"");
+ // sb = new subscriber();
+
+ NodeConfiguration nodeConfiguration =
+ NodeConfiguration.newPublic(local_network_address.getHostAddress(), getMasterUri());
+ //nodeMainExecutor.execute(sb, nodeConfiguration);
+
+ /*boolean flag = true;
+ while(flag){
+ if(sb.getAlertText()==null) {
+ /*new AlertDialog.Builder(listener.this)
+ .setTitle("确认")
+ .setMessage("确定吗?")
+ .setPositiveButton("是", null)
+ .setNegativeButton("否", null)
+ .show();
+ flag = false;
+ Intent intent = new Intent(listener.this, NewListener.class);
+ startActivity(intent);
+ }
+ }*/
+
+
+ } catch(InterruptedException e) {
+ // Thread interruption
+ } catch (IOException e) {
+ // Socket problem
+ }
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ menu.add(0,0,0,R.string.stop_app);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ super.onOptionsItemSelected(item);
+ switch (item.getItemId()){
+ case 0:
+ finish();
+ break;
+ }
+ return true;
+ }
+
+// /**
+// * Call Toast on UI thread.
+// * @param message Message to show on toast.
+// */
+// private void showToast(final String message)
+// {
+// runOnUiThread(new Runnable()
+// {
+// @Override
+// public void run() {
+// if (lastToast != null)
+// lastToast.cancel();
+//
+// lastToast = Toast.makeText(getBaseContext(), message, Toast.LENGTH_LONG);
+// lastToast.show();
+// }
+// });
+// }
+
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/subscriber.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/subscriber.java
new file mode 100644
index 0000000..c5715cc
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/fall_dectect/subscriber.java
@@ -0,0 +1,71 @@
+package com.github.rosjava.android_remocons.rocon_remocon.fall_dectect;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.widget.TextView;
+
+import org.ros.concurrent.CancellableLoop;
+import org.ros.message.MessageListener;
+import org.ros.namespace.GraphName;
+import org.ros.node.AbstractNodeMain;
+import org.ros.node.ConnectedNode;
+import org.ros.node.topic.Subscriber;
+
+public class subscriber extends AbstractNodeMain {
+ /*本类实现了订阅摔倒检测信息的功能*/
+ private static String TAG = "subscriber";
+
+ public String text = null;
+
+ public subscriber() {
+ //判断该类是否执行
+ Log.i(TAG, " is running!");
+
+ }
+ //将提示的文本调出
+ public String getAlertText(){
+ return this.text;
+ }
+
+ @Override
+ public GraphName getDefaultNodeName() {
+ return GraphName.of("rosjava_tutorial_pubsub/listener");
+ }
+
+ @Override
+ public void onStart(final ConnectedNode connectedNode) {
+ //create subscriber
+ final Subscriber subscriber =
+ connectedNode.newSubscriber("chatter", std_msgs.String._TYPE);
+ subscriber.addMessageListener(new MessageListener() {
+ @Override
+ public void onNewMessage(std_msgs.String message) {
+ text = message.getData();
+ Log.i(TAG, "I heard : \"" + text + "\"");
+ }
+ });
+
+ //循环监听节点的信息
+ connectedNode.executeCancellableLoop(new CancellableLoop() {
+ @Override
+ protected void setup() {
+ /*Looper.prepare();
+ Looper.loop();*/
+ };
+
+ @Override
+ protected void loop() throws InterruptedException {
+ long time = System.currentTimeMillis();
+ if (time % 1000 == 0) {
+ //用于测试该节点运行了多长时间
+ Log.i(TAG, "ros_node run again after 1s");
+ }
+ Log.i(TAG, "I heard msg from ubuntu : \"" + text+ "\"");
+ }
+ });
+
+ }
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/follower/FollowerActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/follower/FollowerActivity.java
new file mode 100644
index 0000000..5774a49
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/follower/FollowerActivity.java
@@ -0,0 +1,246 @@
+package com.github.rosjava.android_remocons.rocon_remocon.follower;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.Toast;
+
+import com.github.rosjava.android_remocons.common_tools.apps.RosAppActivity;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+
+import org.ros.address.InetAddressFactory;
+import org.ros.android.BitmapFromCompressedImage;
+import org.ros.android.view.RosImageView;
+import org.ros.exception.RemoteException;
+import org.ros.exception.ServiceNotFoundException;
+import org.ros.namespace.GraphName;
+import org.ros.namespace.NameResolver;
+import org.ros.node.AbstractNodeMain;
+import org.ros.node.ConnectedNode;
+import org.ros.node.NodeConfiguration;
+import org.ros.node.NodeMainExecutor;
+import org.ros.node.service.ServiceClient;
+import org.ros.node.service.ServiceResponseListener;
+
+import sensor_msgs.CompressedImage;
+
+public class FollowerActivity extends RosAppActivity
+{
+ private Toast lastToast;
+ private ConnectedNode node;
+ private RosImageView cameraView;
+ private static final String cameraTopic = "camera/rgb/image_rect_color/compressed";
+
+
+
+ public FollowerActivity()
+ {
+ super("FollowerActivity", "FollowerActivity");
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ setDefaultMasterName(getString(R.string.default_robot));
+ setDefaultAppName(getString(R.string.default_app_follower));
+ setDashboardResource(R.id.top_bar);
+ setMainWindowResource(R.layout.activity_follower);
+
+ super.onCreate(savedInstanceState);
+ cameraView = (RosImageView)findViewById(R.id.follower_image);
+ cameraView.setMessageType(CompressedImage._TYPE);
+ cameraView.setMessageToBitmapCallable(new BitmapFromCompressedImage());
+
+ buildView(cameraView, false);
+
+ // TODO Tricky solution to the StrictMode; the recommended way is by using AsyncTask
+ if (android.os.Build.VERSION.SDK_INT > 9) {
+ StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
+ StrictMode.setThreadPolicy(policy);
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig)
+ {
+ // TODO this is not called now, so we cannot flip the screen
+ Log.e("FollowerActivity", "onConfigurationChanged");
+ super.onConfigurationChanged(newConfig);
+ setContentView(R.layout.main);
+
+ buildView(cameraView, true);
+ }
+
+ private void buildView(RosImageView prevCamImage, boolean rebuild)
+ {
+ if(rebuild == true)
+ {
+ cameraView = prevCamImage;
+ // Register input control callbacks
+ Button backButton = (Button) findViewById(R.id.back_button);
+ backButton.setOnClickListener(backButtonListener);
+
+ ImageButton startButton = (ImageButton)findViewById(R.id.button_start);
+ startButton.setOnClickListener(startButtonListener);
+
+ ImageButton stopButton = (ImageButton)findViewById(R.id.button_stop);
+ stopButton.setOnClickListener(stopButtonListener);
+ }
+ else
+ {
+ // Register input control callbacks
+ Button backButton = (Button) findViewById(R.id.back_button);
+ backButton.setOnClickListener(backButtonListener);
+
+ ImageButton startButton = (ImageButton)findViewById(R.id.button_start);
+ startButton.setOnClickListener(startButtonListener);
+
+ ImageButton stopButton = (ImageButton)findViewById(R.id.button_stop);
+ stopButton.setOnClickListener(stopButtonListener);
+ }
+ }
+
+ @Override
+ protected void init(NodeMainExecutor nodeMainExecutor)
+ {
+ super.init(nodeMainExecutor);
+
+ NodeConfiguration nodeConfiguration =
+ NodeConfiguration.newPublic(InetAddressFactory.newNonLoopback().getHostAddress(), getMasterUri());
+
+ // Execute camera view node
+ cameraView.setTopicName(getMasterNameSpace().resolve(cameraTopic).toString());
+ nodeMainExecutor.execute(cameraView, nodeConfiguration.setNodeName("android/camera_view"));
+
+ // Execute another node just to allow calling services; I suppose there will be a shortcut for this in the future
+ nodeMainExecutor.execute(new AbstractNodeMain()
+ {
+ @Override
+ public GraphName getDefaultNodeName()
+ {
+ return GraphName.of("android/follower");
+ }
+
+ @Override
+ public void onStart(final ConnectedNode connectedNode)
+ {
+ node = connectedNode;
+ }
+ }, nodeConfiguration.setNodeName("android/follower"));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ menu.add(0,0,0,R.string.stop_app);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item)
+ {
+ super.onOptionsItemSelected(item);
+ switch (item.getItemId()){
+ case 0:
+ onDestroy();
+ break;
+ }
+ return true;
+ }
+
+ private void callService(byte newState)
+ {
+ if (node == null)
+ {
+ Log.e("FollowerActivity", "Still doesn't have a connected node");
+ return;
+ }
+
+ ServiceClient serviceClient;
+ try
+ {
+ NameResolver appNameSpace = getMasterNameSpace();
+ String srvTopic = appNameSpace.resolve("turtlebot_follower/change_state").toString();
+ serviceClient = node.newServiceClient(srvTopic, turtlebot_msgs.SetFollowState._TYPE);
+ }
+ catch (ServiceNotFoundException e)
+ {
+ Log.e("FollowerActivity", "Service not found: " + e.getMessage());
+ Toast.makeText(getBaseContext(), "Change follower state service not found", Toast.LENGTH_LONG).show();
+ return;
+ }
+ final turtlebot_msgs.SetFollowStateRequest request = serviceClient.newMessage();
+ request.setState(newState);
+
+ serviceClient.call(request, new ServiceResponseListener() {
+ @Override
+ public void onSuccess(turtlebot_msgs.SetFollowStateResponse response) {
+ Log.i("FollowerActivity", "Service result " + response.getResult());
+ if (request.getState() == turtlebot_msgs.SetFollowStateRequest.STOPPED)
+ showToast("Follower stopped");
+ else
+ showToast("Follower started");
+ }
+
+ @Override
+ public void onFailure(RemoteException e) {
+ Log.e("FollowerActivity", "Service result: failure (" + e.getMessage() + ")");
+ showToast("Change follower state failed");
+ }
+ });
+ }
+
+ /**
+ * Call Toast on UI thread.
+ * @param message Message to show on toast.
+ */
+ private void showToast(final String message)
+ {
+ runOnUiThread(new Runnable()
+ {
+ @Override
+ public void run() {
+ if (lastToast != null)
+ lastToast.cancel();
+
+ lastToast = Toast.makeText(getBaseContext(), message, Toast.LENGTH_LONG);
+ lastToast.show();
+ }
+ });
+ }
+
+ private final OnClickListener backButtonListener = new OnClickListener()
+ {
+ @Override
+ public void onClick(View v)
+ {
+ onBackPressed();
+ }
+ };
+
+ private final OnClickListener startButtonListener = new OnClickListener()
+ {
+ @Override
+ public void onClick(View v)
+ {
+ callService(turtlebot_msgs.SetFollowStateRequest.FOLLOW);
+ }
+ };
+
+ private final OnClickListener stopButtonListener = new OnClickListener()
+ {
+ @Override
+ public void onClick(View v)
+ {
+ callService(turtlebot_msgs.SetFollowStateRequest.STOPPED);
+ }
+ };
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/AboutFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/AboutFragment.java
new file mode 100644
index 0000000..cdbd70b
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/AboutFragment.java
@@ -0,0 +1,178 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+AboutFragment.java
+Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCore.LogCollectionUploadState;
+import org.linphone.core.LinphoneCoreListenerBase;
+import org.linphone.mediastream.Log;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+import android.app.Fragment;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+
+/**
+ * @author Sylvain Berfini
+ */
+public class AboutFragment extends Fragment implements OnClickListener {
+ View sendLogButton = null;
+ View resetLogButton = null;
+ ImageView cancel;
+ LinphoneCoreListenerBase mListener;
+ private ProgressDialog progress;
+ private boolean uploadInProgress;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.about, container, false);
+
+ TextView aboutVersion = (TextView) view.findViewById(R.id.about_android_version);
+ TextView aboutLiblinphoneVersion = (TextView) view.findViewById(R.id.about_liblinphone_version);
+ aboutLiblinphoneVersion.setText(String.format(getString(R.string.about_liblinphone_version), LinphoneManager.getLc().getVersion()));
+ try {
+ aboutVersion.setText(String.format(getString(R.string.about_version), getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0).versionName));
+ } catch (NameNotFoundException e) {
+ Log.e(e, "cannot get version name");
+ }
+
+ cancel = (ImageView) view.findViewById(R.id.cancel);
+ cancel.setOnClickListener(this);
+
+ sendLogButton = view.findViewById(R.id.send_log);
+ sendLogButton.setOnClickListener(this);
+ sendLogButton.setVisibility(LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE);
+
+ resetLogButton = view.findViewById(R.id.reset_log);
+ resetLogButton.setOnClickListener(this);
+ resetLogButton.setVisibility(LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE);
+
+ mListener = new LinphoneCoreListenerBase() {
+ @Override
+ public void uploadProgressIndication(LinphoneCore lc, int offset, int total) {
+ }
+
+ @Override
+ public void uploadStateChanged(LinphoneCore lc, LogCollectionUploadState state, String info) {
+ if (state == LogCollectionUploadState.LogCollectionUploadStateInProgress) {
+ displayUploadLogsInProgress();
+ } else if (state == LogCollectionUploadState.LogCollectionUploadStateDelivered || state == LogCollectionUploadState.LogCollectionUploadStateNotDelivered) {
+ uploadInProgress = false;
+ if (progress != null) progress.dismiss();
+ if (state == LogCollectionUploadState.LogCollectionUploadStateDelivered) {
+ sendLogs(LinphoneService.instance().getApplicationContext(), info);
+ }
+ }
+ }
+ };
+
+ return view;
+ }
+
+ private void displayUploadLogsInProgress() {
+ if (uploadInProgress) {
+ return;
+ }
+ uploadInProgress = true;
+
+ progress = ProgressDialog.show(LinphoneActivity.instance(), null, null);
+ Drawable d = new ColorDrawable(ContextCompat.getColor(getActivity(), R.color.colorE));
+ d.setAlpha(200);
+ progress.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
+ progress.getWindow().setBackgroundDrawable(d);
+ progress.setContentView(R.layout.progress_dialog);
+ progress.show();
+ }
+
+ private void sendLogs(Context context, String info){
+ final String appName = context.getString(R.string.app_name);
+
+ Intent i = new Intent(Intent.ACTION_SEND);
+ i.putExtra(Intent.EXTRA_EMAIL, new String[]{ context.getString(R.string.about_bugreport_email) });
+ i.putExtra(Intent.EXTRA_SUBJECT, appName + " Logs");
+ i.putExtra(Intent.EXTRA_TEXT, info);
+ i.setType("application/zip");
+
+ try {
+ startActivity(Intent.createChooser(i, "Send mail..."));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Log.e(ex);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.removeListener(mListener);
+ }
+
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.addListener(mListener);
+ }
+
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().selectMenu(FragmentsAvailable.ABOUT);
+ }
+
+ super.onResume();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (v == sendLogButton) {
+ if (lc != null) {
+ lc.uploadLogCollection();
+ }
+ } else if (v == resetLogButton) {
+ if (lc != null) {
+ lc.resetLogCollection();
+ }
+ } else if (v == cancel) {
+ LinphoneActivity.instance().goToDialerFragment();
+ }
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/AccountPreferencesFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/AccountPreferencesFragment.java
new file mode 100644
index 0000000..c9afe01
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/AccountPreferencesFragment.java
@@ -0,0 +1,617 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+AccountPreferencesFragment.java
+Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.LinphonePreferences.AccountBuilder;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.assistant.AssistantActivity;
+import org.linphone.core.LinphoneAccountCreator;
+import org.linphone.core.LinphoneCoreException;
+import org.linphone.core.LinphoneCoreFactory;
+import org.linphone.mediastream.Log;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.ui.PreferencesListFragment;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceCategory;
+import android.preference.PreferenceScreen;
+import android.support.v4.content.ContextCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class AccountPreferencesFragment extends PreferencesListFragment implements LinphoneAccountCreator.LinphoneAccountCreatorListener {
+ private int n;
+ private boolean isNewAccount=false;
+ private LinphonePreferences mPrefs;
+ private EditTextPreference mProxyPreference;
+ private ListPreference mTransportPreference;
+ private AccountBuilder builder;
+ private LinphoneAccountCreator accountCreator;
+ private ProgressDialog progress;
+
+ public AccountPreferencesFragment() {
+ super(R.xml.account_preferences);
+ mPrefs = LinphonePreferences.instance();
+ }
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ PreferenceScreen screen = getPreferenceScreen();
+ n = getArguments().getInt("Account", 0);
+ if(n == mPrefs.getAccountCount()) {
+ isNewAccount = true;
+ builder = new AccountBuilder(LinphoneManager.getLc());
+ }
+ initAccountPreferencesFields(screen);
+
+ // Force hide keyboard
+ getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+ }
+
+ public static boolean isEditTextEmpty(String s) {
+ return s.equals(""); // really empty.
+ }
+
+ OnPreferenceChangeListener usernameChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if(isEditTextEmpty(newValue.toString())) return false;
+ if (isNewAccount) {
+ builder.setUsername(newValue.toString());
+ } else {
+ mPrefs.setAccountUsername(n, newValue.toString());
+ }
+ preference.setSummary(newValue.toString());
+ return true;
+ }
+ };
+ OnPreferenceChangeListener useridChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (isNewAccount) {
+ builder.setUserId(newValue.toString());
+ } else {
+ mPrefs.setAccountUserId(n, newValue.toString());
+ }
+ preference.setSummary(newValue.toString());
+ return true;
+ }
+ };
+ OnPreferenceChangeListener passwordChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if(isEditTextEmpty(newValue.toString())) return false;
+ if (isNewAccount) {
+ builder.setPassword(newValue.toString());
+ } else {
+ mPrefs.setAccountPassword(n, newValue.toString());
+ }
+ return true;
+ }
+ };
+ OnPreferenceChangeListener domainChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if(isEditTextEmpty(newValue.toString())) return false;
+ if (isNewAccount) {
+ builder.setDomain(newValue.toString());
+ } else {
+ mPrefs.setAccountDomain(n, newValue.toString());
+ }
+ preference.setSummary(newValue.toString());
+ return true;
+ }
+ };
+ OnPreferenceChangeListener displayNameChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (isNewAccount) {
+ builder.setDisplayName(newValue.toString());
+ } else {
+ mPrefs.setAccountDisplayName(n, newValue.toString());
+ }
+ preference.setSummary(newValue.toString());
+ return true;
+ }
+ };
+ OnPreferenceChangeListener proxyChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String value = newValue.toString();
+ if (isNewAccount) {
+ builder.setProxy(newValue.toString());
+ preference.setSummary(newValue.toString());
+ } else {
+ mPrefs.setAccountProxy(n, value);
+ preference.setSummary(mPrefs.getAccountProxy(n));
+
+ if (mTransportPreference != null) {
+ mTransportPreference.setSummary(mPrefs.getAccountTransportString(n));
+ mTransportPreference.setValue(mPrefs.getAccountTransportKey(n));
+ }
+ }
+ return true;
+ }
+ };
+ OnPreferenceChangeListener outboundProxyChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (isNewAccount) {
+ builder.setOutboundProxyEnabled((Boolean) newValue);
+ } else {
+ mPrefs.setAccountOutboundProxyEnabled(n, (Boolean) newValue);
+ }
+ return true;
+ }
+ };
+ OnPreferenceChangeListener expiresChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (isNewAccount) {
+ builder.setExpires(newValue.toString());
+ } else {
+ mPrefs.setExpires(n, newValue.toString());
+ }
+ preference.setSummary(newValue.toString());
+ return true;
+ }
+ };
+ OnPreferenceChangeListener prefixChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String value = newValue.toString();
+ preference.setSummary(value);
+ if (isNewAccount) {
+ builder.setPrefix(value);
+ } else {
+ mPrefs.setPrefix(n, value);
+ }
+ return true;
+ }
+ };
+ OnPreferenceChangeListener avpfChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ boolean value = (Boolean) newValue;
+ if (isNewAccount) {
+ builder.setAvpfEnabled(value);
+ } else {
+ mPrefs.enableAvpf(n, value);
+ }
+ return true;
+ }
+ };
+ OnPreferenceChangeListener avpfRRIntervalChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String value = newValue.toString();
+ try {
+ int intValue = Integer.parseInt(value);
+ if ((intValue < 1) || (intValue > 5)) {
+ return false;
+ }
+ } catch (NumberFormatException nfe) { }
+ if (isNewAccount) {
+ //TODO
+ } else {
+ mPrefs.setAvpfRRInterval(n, value);
+ }
+ preference.setSummary(value);
+ return true;
+ }
+ };
+ OnPreferenceChangeListener escapeChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ boolean value = (Boolean) newValue;
+ if (isNewAccount) {
+ //TODO
+ } else {
+ mPrefs.setReplacePlusByZeroZero(n, value);
+ }
+ return true;
+ }
+ };
+ OnPreferenceChangeListener friendlistSubscribeListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ boolean value = (Boolean) newValue;
+ mPrefs.enabledFriendlistSubscription(value);
+ LinphoneManager.getInstance().subscribeFriendList(value);
+ return true;
+ }
+ };
+ OnPreferenceClickListener linkAccountListener = new OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Intent assistant = new Intent();
+ assistant.setClass(LinphoneActivity.instance(), AssistantActivity.class);
+ assistant.putExtra("LinkPhoneNumber", true);
+ assistant.putExtra("FromPref", true);
+ startActivity(assistant);
+ return true;
+ }
+ };
+ OnPreferenceChangeListener disableChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ boolean value = (Boolean) newValue;
+ if (isNewAccount) {
+ builder.setEnabled(!value);
+ } else {
+ mPrefs.setAccountEnabled(n, !value);
+ }
+ return true;
+ }
+ };
+ OnPreferenceChangeListener transportChangedListener = new OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String key = newValue.toString();
+ if (isNewAccount) {
+ //TODO
+ //builder.setTransport(transport);
+ } else {
+ mPrefs.setAccountTransport(n, key);
+ preference.setSummary(mPrefs.getAccountTransportString(n));
+ preference.setDefaultValue(mPrefs.getAccountTransportKey(n));
+ if (mProxyPreference != null) {
+ String newProxy = mPrefs.getAccountProxy(n);
+ mProxyPreference.setSummary(newProxy);
+ mProxyPreference.setText(newProxy);
+ }
+ }
+ return true;
+ }
+ };
+
+ private void initAccountPreferencesFields(PreferenceScreen parent) {
+ boolean isDefaultAccount = mPrefs.getDefaultAccountIndex() == n;
+
+ accountCreator = LinphoneCoreFactory.instance().createAccountCreator(LinphoneManager.getLc()
+ , LinphonePreferences.instance().getXmlrpcUrl());
+ accountCreator.setListener(this);
+
+ PreferenceCategory account = (PreferenceCategory) getPreferenceScreen().findPreference(getString(R.string.pref_sipaccount_key));
+ EditTextPreference username = (EditTextPreference) account.getPreference(0);
+ username.setOnPreferenceChangeListener(usernameChangedListener);
+ if (!isNewAccount){
+ username.setText(mPrefs.getAccountUsername(n));
+ username.setSummary(username.getText());
+ }
+
+ EditTextPreference userid = (EditTextPreference) account.getPreference(1);
+ userid.setOnPreferenceChangeListener(useridChangedListener);
+ if (!isNewAccount){
+ userid.setText(mPrefs.getAccountUserId(n));
+ userid.setSummary(userid.getText());
+ }
+
+ EditTextPreference password = (EditTextPreference) account.getPreference(2);
+ password.setOnPreferenceChangeListener(passwordChangedListener);
+ if(!isNewAccount){
+ password.setText(mPrefs.getAccountPassword(n));
+ }
+
+ EditTextPreference domain = (EditTextPreference) account.getPreference(3);
+ domain.setOnPreferenceChangeListener(domainChangedListener);
+ if (!isNewAccount){
+ domain.setText(mPrefs.getAccountDomain(n));
+ domain.setSummary(domain.getText());
+ }
+
+ EditTextPreference displayName = (EditTextPreference) account.getPreference(4);
+ displayName.setOnPreferenceChangeListener(displayNameChangedListener);
+ if (!isNewAccount){
+ displayName.setText(mPrefs.getAccountDisplayName(n));
+ displayName.setSummary(displayName.getText());
+ }
+
+ PreferenceCategory advanced = (PreferenceCategory) getPreferenceScreen().findPreference(getString(R.string.pref_advanced_key));
+ mTransportPreference = (ListPreference) advanced.getPreference(0);
+ initializeTransportPreference(mTransportPreference);
+ mTransportPreference.setOnPreferenceChangeListener(transportChangedListener);
+ if(!isNewAccount){
+ mTransportPreference.setSummary(mPrefs.getAccountTransportString(n));
+ }
+
+ mProxyPreference = (EditTextPreference) advanced.getPreference(1);
+ mProxyPreference.setOnPreferenceChangeListener(proxyChangedListener);
+ if (!isNewAccount){
+ mProxyPreference.setText(mPrefs.getAccountProxy(n));
+ mProxyPreference.setSummary("".equals(mProxyPreference.getText()) || (mProxyPreference.getText() == null) ? getString(R.string.pref_help_proxy) : mProxyPreference.getText());
+ }
+
+ CheckBoxPreference outboundProxy = (CheckBoxPreference) advanced.getPreference(2);
+ outboundProxy.setOnPreferenceChangeListener(outboundProxyChangedListener);
+ if (!isNewAccount){
+ outboundProxy.setChecked(mPrefs.isAccountOutboundProxySet(n));
+ }
+
+ EditTextPreference expires = (EditTextPreference) advanced.getPreference(3);
+ expires.setOnPreferenceChangeListener(expiresChangedListener);
+ if(!isNewAccount){
+ expires.setText(mPrefs.getExpires(n));
+ expires.setSummary(mPrefs.getExpires(n));
+ }
+
+ EditTextPreference prefix = (EditTextPreference) advanced.getPreference(4);
+ prefix.setOnPreferenceChangeListener(prefixChangedListener);
+ if(!isNewAccount){
+ String prefixValue = mPrefs.getPrefix(n);
+ prefix.setText(prefixValue);
+ prefix.setSummary(prefixValue);
+ }
+
+ CheckBoxPreference avpf = (CheckBoxPreference) advanced.getPreference(5);
+ avpf.setOnPreferenceChangeListener(avpfChangedListener);
+ if (!isNewAccount){
+ avpf.setChecked(mPrefs.avpfEnabled(n));
+ }
+
+ EditTextPreference avpfRRInterval = (EditTextPreference) advanced.getPreference(6);
+ avpfRRInterval.setOnPreferenceChangeListener(avpfRRIntervalChangedListener);
+ if (!isNewAccount){
+ avpfRRInterval.setText(mPrefs.getAvpfRRInterval(n));
+ avpfRRInterval.setSummary(mPrefs.getAvpfRRInterval(n));
+ }
+
+ CheckBoxPreference escape = (CheckBoxPreference) advanced.getPreference(7);
+ escape.setOnPreferenceChangeListener(escapeChangedListener);
+ if(!isNewAccount){
+ escape.setChecked(mPrefs.getReplacePlusByZeroZero(n));
+ }
+
+ CheckBoxPreference friendlistSubscribe = (CheckBoxPreference) advanced.getPreference(8);
+ friendlistSubscribe.setOnPreferenceChangeListener(friendlistSubscribeListener);
+ if(!isNewAccount){
+ friendlistSubscribe.setChecked(mPrefs.isFriendlistsubscriptionEnabled());
+ }
+
+ Preference linkAccount = advanced.getPreference(9);
+ linkAccount.setOnPreferenceClickListener(linkAccountListener);
+
+ PreferenceCategory manage = (PreferenceCategory) getPreferenceScreen().findPreference(getString(R.string.pref_manage_key));
+ final CheckBoxPreference disable = (CheckBoxPreference) manage.getPreference(0);
+ disable.setEnabled(true);
+ disable.setOnPreferenceChangeListener(disableChangedListener);
+ if(!isNewAccount){
+ disable.setChecked(!mPrefs.isAccountEnabled(n));
+ }
+
+ CheckBoxPreference mainAccount = (CheckBoxPreference) manage.getPreference(1);
+ mainAccount.setChecked(isDefaultAccount);
+ mainAccount.setEnabled(!mainAccount.isChecked());
+ mainAccount.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ mPrefs.setDefaultAccount(n);
+ disable.setEnabled(false);
+ disable.setChecked(false);
+ preference.setEnabled(false);
+ return true;
+ }
+ });
+ if(!isNewAccount){
+ mainAccount.setEnabled(!mainAccount.isChecked());
+ }
+
+ final Preference changePassword = manage.getPreference(2);
+ if (mPrefs.getAccountDomain(n).compareTo(getString(R.string.default_domain)) == 0) {
+ changePassword.setEnabled(!isNewAccount);
+ changePassword.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ final AlertDialog.Builder alert = new AlertDialog.Builder(LinphoneActivity.instance());
+ LayoutInflater inflater = LinphoneActivity.instance().getLayoutInflater();
+ View layout = inflater.inflate(R.layout.new_password, null);
+ final EditText pass1 = (EditText) layout.findViewById(R.id.password1);
+ final EditText pass2 = (EditText) layout.findViewById(R.id.password2);
+ alert.setNeutralButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ LinphoneAccountCreator.Status status = accountCreator.setPassword(pass1.getText().toString());
+ if (status.equals(LinphoneAccountCreator.Status.Ok)) {
+ if (pass1.getText().toString().compareTo(pass2.getText().toString()) == 0) {
+ accountCreator.setUsername(mPrefs.getAccountUsername(n));
+ accountCreator.setHa1(mPrefs.getAccountHa1(n));
+ status = accountCreator.updatePassword(pass1.getText().toString());
+ if (!status.equals(LinphoneAccountCreator.Status.Ok)) {
+ LinphoneUtils.displayErrorAlert(LinphoneUtils.errorForStatus(status)
+ , LinphoneActivity.instance());
+ } else {
+ progress = ProgressDialog.show(LinphoneActivity.instance(), null, null);
+ Drawable d = new ColorDrawable(ContextCompat.getColor(LinphoneActivity.instance(), R.color.colorE));
+ d.setAlpha(200);
+ progress.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
+ progress.getWindow().setBackgroundDrawable(d);
+ progress.setContentView(R.layout.progress_dialog);
+ progress.show();
+ }
+ } else {
+ LinphoneUtils.displayErrorAlert(getString(R.string.wizard_passwords_unmatched)
+ , LinphoneActivity.instance());
+ }
+ return;
+ }
+ LinphoneUtils.displayErrorAlert(LinphoneUtils.errorForStatus(status), LinphoneActivity.instance());
+ }
+ });
+
+ alert.setView(layout);
+ alert.show();
+ return true;
+ }
+ });
+ } else {
+ changePassword.setEnabled(false);
+ }
+
+ final Preference delete = manage.getPreference(3);
+ delete.setEnabled(!isNewAccount);
+ delete.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ mPrefs.deleteAccount(n);
+ LinphoneActivity.instance().refreshAccounts();
+ LinphoneActivity.instance().displaySettings();
+ return true;
+ }
+ });
+ }
+
+ private void initializeTransportPreference(ListPreference pref) {
+ List entries = new ArrayList();
+ List values = new ArrayList();
+ entries.add(getString(R.string.pref_transport_udp));
+ values.add(getString(R.string.pref_transport_udp_key));
+ entries.add(getString(R.string.pref_transport_tcp));
+ values.add(getString(R.string.pref_transport_tcp_key));
+
+ if (!getResources().getBoolean(R.bool.disable_all_security_features_for_markets)) {
+ entries.add(getString(R.string.pref_transport_tls));
+ values.add(getString(R.string.pref_transport_tls_key));
+ }
+ setListPreferenceValues(pref, entries, values);
+
+ if (! isNewAccount) {
+ pref.setSummary(mPrefs.getAccountTransportString(n));
+ pref.setDefaultValue(mPrefs.getAccountTransportKey(n));
+ pref.setValueIndex(entries.indexOf(mPrefs.getAccountTransportString(n)));
+ } else {
+
+ pref.setSummary(getString(R.string.pref_transport_udp));
+ pref.setDefaultValue(getString(R.string.pref_transport_udp));
+ pref.setValueIndex(entries.indexOf(getString(R.string.pref_transport_udp)));
+ }
+ }
+
+ private static void setListPreferenceValues(ListPreference pref, List entries, List values) {
+ CharSequence[] contents = new CharSequence[entries.size()];
+ entries.toArray(contents);
+ pref.setEntries(contents);
+ contents = new CharSequence[values.size()];
+ values.toArray(contents);
+ pref.setEntryValues(contents);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().selectMenu(FragmentsAvailable.SETTINGS);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (LinphoneActivity.isInstanciated()) {
+ try {
+ if(isNewAccount){
+ builder.saveNewAccount();
+ }
+ } catch (LinphoneCoreException e) {
+ Log.e(e);
+ }
+ LinphoneActivity.instance().isNewProxyConfig();
+ LinphoneManager.getLc().refreshRegisters();
+ LinphoneActivity.instance().hideTopBar();
+ }
+ }
+
+ @Override
+ public void onAccountCreatorIsAccountUsed(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+ }
+
+ @Override
+ public void onAccountCreatorAccountCreated(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+
+ }
+
+ @Override
+ public void onAccountCreatorAccountActivated(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+
+ }
+
+ @Override
+ public void onAccountCreatorAccountLinkedWithPhoneNumber(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+
+ }
+
+ @Override
+ public void onAccountCreatorPhoneNumberLinkActivated(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+
+ }
+
+ @Override
+ public void onAccountCreatorIsAccountActivated(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+
+ }
+
+ @Override
+ public void onAccountCreatorPhoneAccountRecovered(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+
+ }
+
+ @Override
+ public void onAccountCreatorIsAccountLinked(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+
+ }
+
+ @Override
+ public void onAccountCreatorIsPhoneNumberUsed(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+ }
+
+ @Override
+ public void onAccountCreatorPasswordUpdated(LinphoneAccountCreator accountCreator, LinphoneAccountCreator.Status status) {
+ progress.dismiss();
+ if (status.equals(LinphoneAccountCreator.Status.Ok)) {
+ mPrefs.setAccountPassword(n, accountCreator.getPassword());
+ PreferenceCategory account = (PreferenceCategory) getPreferenceScreen().findPreference(getString(R.string.pref_sipaccount_key));
+ ((EditTextPreference) account.getPreference(2)).setText(mPrefs.getAccountPassword(n));
+ LinphoneUtils.displayErrorAlert(getString(R.string.pref_password_changed), LinphoneActivity.instance());
+ } else {
+ LinphoneUtils.displayErrorAlert(LinphoneUtils.errorForStatus(status), LinphoneActivity.instance());
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/BandwidthManager.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/BandwidthManager.java
new file mode 100644
index 0000000..669273e
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/BandwidthManager.java
@@ -0,0 +1,65 @@
+/*
+BandwithManager.java
+Copyright (C) 2010 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+import org.linphone.core.LinphoneCallParams;
+import org.linphone.core.LinphoneCore;
+
+public class BandwidthManager {
+
+ public static final int HIGH_RESOLUTION = 0;
+ public static final int LOW_RESOLUTION = 1;
+ public static final int LOW_BANDWIDTH = 2;
+
+ private static BandwidthManager instance;
+
+ private int currentProfile = HIGH_RESOLUTION;
+ public int getCurrentProfile() {return currentProfile;}
+
+ public static final synchronized BandwidthManager getInstance() {
+ if (instance == null) instance = new BandwidthManager();
+ return instance;
+ }
+
+
+ private BandwidthManager() {
+ // FIXME register a listener on NetworkManager to get notified of network state
+ // FIXME register a listener on Preference to get notified of change in video enable value
+
+ // FIXME initially get those values
+ }
+
+
+ public void updateWithProfileSettings(LinphoneCore lc, LinphoneCallParams callParams) {
+ if (callParams != null) { // in call
+ // Update video parm if
+ if (!isVideoPossible()) { // NO VIDEO
+ callParams.setVideoEnabled(false);
+ callParams.setAudioBandwidth(40);
+ } else {
+ callParams.setVideoEnabled(true);
+ callParams.setAudioBandwidth(0); // disable limitation
+ }
+ }
+ }
+
+ public boolean isVideoPossible() {
+ return currentProfile != LOW_BANDWIDTH;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/BluetoothManager.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/BluetoothManager.java
new file mode 100644
index 0000000..119b14b
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/BluetoothManager.java
@@ -0,0 +1,320 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+BluetoothManager.java
+Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+import java.util.List;
+
+import org.linphone.mediastream.Log;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAssignedNumbers;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.os.Build;
+
+/**
+ * @author Sylvain Berfini
+ */
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class BluetoothManager extends BroadcastReceiver {
+ public int PLANTRONICS_BUTTON_PRESS = 1;
+ public int PLANTRONICS_BUTTON_LONG_PRESS = 2;
+ public int PLANTRONICS_BUTTON_DOUBLE_PRESS = 5;
+
+ public int PLANTRONICS_BUTTON_CALL = 2;
+ public int PLANTRONICS_BUTTON_MUTE = 3;
+
+ private static BluetoothManager instance;
+
+ private Context mContext;
+ private AudioManager mAudioManager;
+ private BluetoothAdapter mBluetoothAdapter;
+ private BluetoothHeadset mBluetoothHeadset;
+ private BluetoothDevice mBluetoothDevice;
+ private BluetoothProfile.ServiceListener mProfileListener;
+ private boolean isBluetoothConnected;
+ private boolean isScoConnected;
+
+ public static BluetoothManager getInstance() {
+ if (instance == null) {
+ instance = new BluetoothManager();
+ }
+ return instance;
+ }
+
+ public BluetoothManager() {
+ isBluetoothConnected = false;
+ if (!ensureInit()) {
+ Log.w("[Bluetooth] Manager tried to init but LinphoneService not ready yet...");
+ }
+ instance = this;
+ }
+
+ public void initBluetooth() {
+ if (!ensureInit()) {
+ Log.w("[Bluetooth] Manager tried to init bluetooth but LinphoneService not ready yet...");
+ return;
+ }
+
+ IntentFilter filter = new IntentFilter();
+ filter.addCategory(BluetoothHeadset.VENDOR_SPECIFIC_HEADSET_EVENT_COMPANY_ID_CATEGORY + "." + BluetoothAssignedNumbers.PLANTRONICS);
+ filter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED);
+ filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
+ filter.addAction(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT);
+ mContext.registerReceiver(this, filter);
+ Log.d("[Bluetooth] Receiver started");
+
+ startBluetooth();
+ }
+
+ private void startBluetooth() {
+ if (isBluetoothConnected) {
+ Log.e("[Bluetooth] Already started, skipping...");
+ return;
+ }
+
+ mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+
+ if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) {
+ if (mProfileListener != null) {
+ Log.w("[Bluetooth] Headset profile was already opened, let's close it");
+ mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
+ }
+
+ mProfileListener = new BluetoothProfile.ServiceListener() {
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ if (profile == BluetoothProfile.HEADSET) {
+ Log.d("[Bluetooth] Headset connected");
+ mBluetoothHeadset = (BluetoothHeadset) proxy;
+ isBluetoothConnected = true;
+ }
+ }
+ public void onServiceDisconnected(int profile) {
+ if (profile == BluetoothProfile.HEADSET) {
+ mBluetoothHeadset = null;
+ isBluetoothConnected = false;
+ Log.d("[Bluetooth] Headset disconnected");
+ LinphoneManager.getInstance().routeAudioToReceiver();
+ }
+ }
+ };
+ boolean success = mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET);
+ if (!success) {
+ Log.e("[Bluetooth] getProfileProxy failed !");
+ }
+ } else {
+ Log.w("[Bluetooth] Interface disabled on device");
+ }
+ }
+
+ private boolean ensureInit() {
+ if (mBluetoothAdapter == null) {
+ mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ }
+ if (mContext == null) {
+ if (LinphoneService.isReady()) {
+ mContext = LinphoneService.instance().getApplicationContext();
+ } else {
+ return false;
+ }
+ }
+ if (mContext != null && mAudioManager == null) {
+ mAudioManager = ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE));
+ }
+ return true;
+ }
+
+ public boolean routeAudioToBluetooth() {
+ ensureInit();
+
+ if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() && mAudioManager != null && mAudioManager.isBluetoothScoAvailableOffCall()) {
+ if (isBluetoothHeadsetAvailable()) {
+ if (mAudioManager != null && !mAudioManager.isBluetoothScoOn()) {
+ Log.d("[Bluetooth] SCO off, let's start it");
+ mAudioManager.setBluetoothScoOn(true);
+ mAudioManager.startBluetoothSco();
+ }
+ } else {
+ return false;
+ }
+
+ // Hack to ensure bluetooth sco is really running
+ boolean ok = isUsingBluetoothAudioRoute();
+ int retries = 0;
+ while (!ok && retries < 5) {
+ retries++;
+
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException e) {}
+
+ if (mAudioManager != null) {
+ mAudioManager.setBluetoothScoOn(true);
+ mAudioManager.startBluetoothSco();
+ }
+
+ ok = isUsingBluetoothAudioRoute();
+ }
+ if (ok) {
+ if (retries > 0) {
+ Log.d("[Bluetooth] Audio route ok after " + retries + " retries");
+ } else {
+ Log.d("[Bluetooth] Audio route ok");
+ }
+ } else {
+ Log.d("[Bluetooth] Audio route still not ok...");
+ }
+
+ return ok;
+ }
+
+ return false;
+ }
+
+ public boolean isUsingBluetoothAudioRoute() {
+ return mBluetoothHeadset != null && mBluetoothHeadset.isAudioConnected(mBluetoothDevice) && isScoConnected;
+ }
+
+ public boolean isBluetoothHeadsetAvailable() {
+ ensureInit();
+ if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() && mAudioManager != null && mAudioManager.isBluetoothScoAvailableOffCall()) {
+ boolean isHeadsetConnected = false;
+ if (mBluetoothHeadset != null) {
+ List devices = mBluetoothHeadset.getConnectedDevices();
+ mBluetoothDevice = null;
+ for (final BluetoothDevice dev : devices) {
+ if (mBluetoothHeadset.getConnectionState(dev) == BluetoothHeadset.STATE_CONNECTED) {
+ mBluetoothDevice = dev;
+ isHeadsetConnected = true;
+ break;
+ }
+ }
+ Log.d(isHeadsetConnected ? "[Bluetooth] Headset found, bluetooth audio route available" : "[Bluetooth] No headset found, bluetooth audio route unavailable");
+ }
+ return isHeadsetConnected;
+ }
+
+ return false;
+ }
+
+ public void disableBluetoothSCO() {
+ if (mAudioManager != null && mAudioManager.isBluetoothScoOn()) {
+ mAudioManager.stopBluetoothSco();
+ mAudioManager.setBluetoothScoOn(false);
+
+ // Hack to ensure bluetooth sco is really stopped
+ int retries = 0;
+ while (isScoConnected && retries < 10) {
+ retries++;
+
+ try {
+ Thread.sleep(200);
+ } catch (InterruptedException e) {}
+
+ mAudioManager.stopBluetoothSco();
+ mAudioManager.setBluetoothScoOn(false);
+ }
+ Log.w("[Bluetooth] SCO disconnected!");
+ }
+ }
+
+ public void stopBluetooth() {
+ Log.w("[Bluetooth] Stopping...");
+ isBluetoothConnected = false;
+
+ disableBluetoothSCO();
+
+ if (mBluetoothAdapter != null && mProfileListener != null && mBluetoothHeadset != null) {
+ mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
+ mProfileListener = null;
+ }
+ mBluetoothDevice = null;
+
+ Log.w("[Bluetooth] Stopped!");
+
+ if (LinphoneManager.isInstanciated()) {
+ LinphoneManager.getInstance().routeAudioToReceiver();
+ }
+ }
+
+ public void destroy() {
+ try {
+ stopBluetooth();
+
+ try {
+ mContext.unregisterReceiver(this);
+ Log.d("[Bluetooth] Receiver stopped");
+ } catch (Exception e) {}
+ } catch (Exception e) {
+ Log.e(e);
+ }
+ }
+
+ public void onReceive(Context context, Intent intent) {
+ if (!LinphoneManager.isInstanciated())
+ return;
+
+ String action = intent.getAction();
+ if (AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED.equals(action)) {
+ int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, 0);
+ if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
+ Log.d("[Bluetooth] SCO state: connected");
+// LinphoneManager.getInstance().audioStateChanged(AudioState.BLUETOOTH);
+ isScoConnected = true;
+ } else if (state == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) {
+ Log.d("[Bluetooth] SCO state: disconnected");
+// LinphoneManager.getInstance().audioStateChanged(AudioState.SPEAKER);
+ isScoConnected = false;
+ } else {
+ Log.d("[Bluetooth] SCO state: " + state);
+ }
+ }
+ else if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(action)) {
+ int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, BluetoothAdapter.STATE_DISCONNECTED);
+ if (state == 0) {
+ Log.d("[Bluetooth] State: disconnected");
+ stopBluetooth();
+ } else if (state == 2) {
+ Log.d("[Bluetooth] State: connected");
+ startBluetooth();
+ } else {
+ Log.d("[Bluetooth] State: " + state);
+ }
+ }
+ else if (intent.getAction().equals(BluetoothHeadset.ACTION_VENDOR_SPECIFIC_HEADSET_EVENT)) {
+ String command = intent.getExtras().getString(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD);
+ //int type = intent.getExtras().getInt(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_CMD_TYPE);
+
+ Object[] args = (Object[]) intent.getExtras().get(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS);
+ String eventName = (String) args[0];
+
+ if (eventName.equals("BUTTON") && args.length >= 3) {
+ Integer buttonID = (Integer) args[1];
+ Integer mode = (Integer) args[2];
+ Log.d("[Bluetooth] Event: " + command + " : " + eventName + ", id = " + buttonID + " (" + mode + ")");
+ }
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/BootReceiver.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/BootReceiver.java
new file mode 100644
index 0000000..d22d16d
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/BootReceiver.java
@@ -0,0 +1,46 @@
+/*
+BootReceiver.java
+Copyright (C) 2010 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+import org.linphone.core.LinphoneCoreFactory;
+import org.linphone.core.LpConfig;
+import org.linphone.mediastream.Log;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+public class BootReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equalsIgnoreCase(Intent.ACTION_SHUTDOWN)) {
+ Log.w("Device is shutting down, destroying LinphoneCore to unregister");
+ LinphoneManager.destroy();
+ } else {
+ String path = context.getFilesDir().getAbsolutePath() + "/.linphonerc";
+ LpConfig lpConfig = LinphoneCoreFactory.instance().createLpConfig(path);
+ if (lpConfig.getBool("app", "auto_start", false)) {
+ Intent lLinphoneServiceIntent = new Intent(Intent.ACTION_MAIN);
+ lLinphoneServiceIntent.setClass(context, LinphoneService.class);
+ context.startService(lLinphoneServiceIntent);
+ }
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallActivity.java
new file mode 100644
index 0000000..9b331eb
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallActivity.java
@@ -0,0 +1,1587 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+CallActivity.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+import java.util.Arrays;
+import java.util.List;
+
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneCall;
+import org.linphone.core.LinphoneCall.State;
+import org.linphone.core.LinphoneCallParams;
+import org.linphone.core.LinphoneChatMessage;
+import org.linphone.core.LinphoneChatRoom;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCoreException;
+import org.linphone.core.LinphoneCoreListenerBase;
+import org.linphone.core.LinphonePlayer;
+import org.linphone.mediastream.Log;
+import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.ui.Numpad;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.Chronometer;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class CallActivity extends Activity implements OnClickListener, SensorEventListener, ActivityCompat.OnRequestPermissionsResultCallback {
+ private final static int SECONDS_BEFORE_HIDING_CONTROLS = 4000;
+ private final static int SECONDS_BEFORE_DENYING_CALL_UPDATE = 30000;
+ private static final int PERMISSIONS_REQUEST_CAMERA = 202;
+ private static final int PERMISSIONS_ENABLED_CAMERA = 203;
+ private static final int PERMISSIONS_ENABLED_MIC = 204;
+
+ private static CallActivity instance;
+
+ private Handler mControlsHandler = new Handler();
+ private Runnable mControls;
+ private ImageView switchCamera;
+ private TextView missedChats;
+ private RelativeLayout mActiveCallHeader, sideMenuContent, avatar_layout;
+ private ImageView pause, hangUp, dialer, video, micro, speaker, options, addCall, transfer, conference, conferenceStatus, contactPicture;
+ private ImageView audioRoute, routeSpeaker, routeEarpiece, routeBluetooth, menu, chat;
+ private LinearLayout mNoCurrentCall, callInfo, mCallPaused;
+ private ProgressBar videoProgress;
+ private StatusFragment status;
+ private CallAudioFragment audioCallFragment;
+ private CallVideoFragment videoCallFragment;
+ private boolean isSpeakerEnabled = false, isMicMuted = false, isTransferAllowed;
+ private LinearLayout mControlsLayout;
+ private Numpad numpad;
+ private int cameraNumber;
+ private CountDownTimer timer;
+ private boolean isVideoCallPaused = false;
+
+ private static PowerManager powerManager;
+ private static PowerManager.WakeLock wakeLock;
+ private static int field = 0x00000020;
+ private SensorManager mSensorManager;
+ private Sensor mProximity;
+
+ private LinearLayout callsList, conferenceList;
+ private LayoutInflater inflater;
+ private ViewGroup container;
+ private boolean isConferenceRunning = false;
+ private LinphoneCoreListenerBase mListener;
+ private DrawerLayout sideMenu;
+ private boolean mProximitySensingEnabled;
+
+ public static CallActivity instance() {
+ return instance;
+ }
+
+ public static boolean isInstanciated() {
+ return instance != null;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ instance = this;
+
+ if (getResources().getBoolean(R.bool.orientation_portrait_only)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ setContentView(R.layout.call);
+
+ isTransferAllowed = getApplicationContext().getResources().getBoolean(R.bool.allow_transfers);
+
+ if(!BluetoothManager.getInstance().isBluetoothHeadsetAvailable()) {
+ BluetoothManager.getInstance().initBluetooth();
+ }
+
+ cameraNumber = AndroidCameraConfiguration.retrieveCameras().length;
+
+ try {
+ // Yeah, this is hidden field.
+ field = PowerManager.class.getClass().getField("PROXIMITY_SCREEN_OFF_WAKE_LOCK").getInt(null);
+ } catch (Throwable ignored) {
+ }
+
+ powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ wakeLock = powerManager.newWakeLock(field, getLocalClassName());
+
+ mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
+ mProximity = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
+
+ mListener = new LinphoneCoreListenerBase() {
+ @Override
+ public void messageReceived(LinphoneCore lc, LinphoneChatRoom cr, LinphoneChatMessage message) {
+ displayMissedChats();
+ }
+
+ @Override
+ public void callState(LinphoneCore lc, final LinphoneCall call, LinphoneCall.State state, String message) {
+ if (LinphoneManager.getLc().getCallsNb() == 0) {
+ finish();
+ return;
+ }
+
+ if (state == State.IncomingReceived) {
+ startIncomingCallActivity();
+ return;
+ } else if (state == State.Paused || state == State.PausedByRemote || state == State.Pausing) {
+ if(LinphoneManager.getLc().getCurrentCall() != null) {
+ enabledVideoButton(false);
+ }
+ if(isVideoEnabled(call)){
+ showAudioView();
+ }
+ } else if (state == State.Resuming) {
+ if(LinphonePreferences.instance().isVideoEnabled()){
+ status.refreshStatusItems(call, isVideoEnabled(call));
+ if(call.getCurrentParamsCopy().getVideoEnabled()){
+ showVideoView();
+ }
+ }
+ if(LinphoneManager.getLc().getCurrentCall() != null) {
+ enabledVideoButton(true);
+ }
+ } else if (state == State.StreamsRunning) {
+ switchVideo(isVideoEnabled(call));
+ enableAndRefreshInCallActions();
+
+ if (status != null) {
+ videoProgress.setVisibility(View.GONE);
+ status.refreshStatusItems(call, isVideoEnabled(call));
+ }
+ } else if (state == State.CallUpdatedByRemote) {
+ // If the correspondent proposes video while audio call
+ boolean videoEnabled = LinphonePreferences.instance().isVideoEnabled();
+ if (!videoEnabled) {
+ acceptCallUpdate(false);
+ }
+
+ boolean remoteVideo = call.getRemoteParams().getVideoEnabled();
+ boolean localVideo = call.getCurrentParamsCopy().getVideoEnabled();
+ boolean autoAcceptCameraPolicy = LinphonePreferences.instance().shouldAutomaticallyAcceptVideoRequests();
+ if (remoteVideo && !localVideo && !autoAcceptCameraPolicy && !LinphoneManager.getLc().isInConference()) {
+ showAcceptCallUpdateDialog();
+ timer = new CountDownTimer(SECONDS_BEFORE_DENYING_CALL_UPDATE, 1000) {
+ public void onTick(long millisUntilFinished) { }
+ public void onFinish() {
+ //TODO dismiss dialog
+ acceptCallUpdate(false);
+ }
+ }.start();
+
+ /*showAcceptCallUpdateDialog();
+
+ timer = new CountDownTimer(SECONDS_BEFORE_DENYING_CALL_UPDATE, 1000) {
+ public void onTick(long millisUntilFinished) { }
+ public void onFinish() {
+ //TODO dismiss dialog
+
+ }
+ }.start();*/
+
+ }
+// else if (remoteVideo && !LinphoneManager.getLc().isInConference() && autoAcceptCameraPolicy) {
+// mHandler.post(new Runnable() {
+// @Override
+// public void run() {
+// acceptCallUpdate(true);
+// }
+// });
+// }
+ }
+
+ refreshIncallUi();
+ transfer.setEnabled(LinphoneManager.getLc().getCurrentCall() != null);
+ }
+
+ @Override
+ public void callEncryptionChanged(LinphoneCore lc, final LinphoneCall call, boolean encrypted, String authenticationToken) {
+ if (status != null) {
+ if(call.getCurrentParamsCopy().getMediaEncryption().equals(LinphoneCore.MediaEncryption.ZRTP) && !call.isAuthenticationTokenVerified()){
+ status.showZRTPDialog(call);
+ }
+ status.refreshStatusItems(call, call.getCurrentParamsCopy().getVideoEnabled());
+ }
+ }
+
+ };
+
+ if (findViewById(R.id.fragmentContainer) != null) {
+ initUI();
+
+ if (LinphoneManager.getLc().getCallsNb() > 0) {
+ LinphoneCall call = LinphoneManager.getLc().getCalls()[0];
+
+ if (LinphoneUtils.isCallEstablished(call)) {
+ enableAndRefreshInCallActions();
+ }
+ }
+
+ if (savedInstanceState != null) {
+ // Fragment already created, no need to create it again (else it will generate a memory leak with duplicated fragments)
+ isSpeakerEnabled = savedInstanceState.getBoolean("Speaker");
+ isMicMuted = savedInstanceState.getBoolean("Mic");
+ isVideoCallPaused = savedInstanceState.getBoolean("VideoCallPaused");
+ refreshInCallActions();
+ return;
+ } else {
+ isSpeakerEnabled = LinphoneManager.getLc().isSpeakerEnabled();
+ isMicMuted = LinphoneManager.getLc().isMicMuted();
+ }
+
+ Fragment callFragment;
+ if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) {
+ callFragment = new CallVideoFragment();
+ videoCallFragment = (CallVideoFragment) callFragment;
+ displayVideoCall(false);
+ LinphoneManager.getInstance().routeAudioToSpeaker();
+ isSpeakerEnabled = true;
+ } else {
+ callFragment = new CallAudioFragment();
+ audioCallFragment = (CallAudioFragment) callFragment;
+ }
+
+ if(BluetoothManager.getInstance().isBluetoothHeadsetAvailable()){
+ BluetoothManager.getInstance().routeAudioToBluetooth();
+ }
+
+ callFragment.setArguments(getIntent().getExtras());
+ getFragmentManager().beginTransaction().add(R.id.fragmentContainer, callFragment).commitAllowingStateLoss();
+ }
+ }
+
+ private boolean isVideoEnabled(LinphoneCall call) {
+ if(call != null){
+ return call.getCurrentParamsCopy().getVideoEnabled();
+ }
+ return false;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putBoolean("Speaker", LinphoneManager.getLc().isSpeakerEnabled());
+ outState.putBoolean("Mic", LinphoneManager.getLc().isMicMuted());
+ outState.putBoolean("VideoCallPaused", isVideoCallPaused);
+
+ super.onSaveInstanceState(outState);
+ }
+
+ private boolean isTablet() {
+ return getResources().getBoolean(R.bool.isTablet);
+ }
+
+ private void initUI() {
+ inflater = LayoutInflater.from(this);
+ container = (ViewGroup) findViewById(R.id.topLayout);
+ callsList = (LinearLayout) findViewById(R.id.calls_list);
+ conferenceList = (LinearLayout) findViewById(R.id.conference_list);
+
+ //TopBar
+ video = (ImageView) findViewById(R.id.video);
+ video.setOnClickListener(this);
+ enabledVideoButton(false);
+
+ videoProgress = (ProgressBar) findViewById(R.id.video_in_progress);
+ videoProgress.setVisibility(View.GONE);
+
+ micro = (ImageView) findViewById(R.id.micro);
+ micro.setOnClickListener(this);
+
+ speaker = (ImageView) findViewById(R.id.speaker);
+ speaker.setOnClickListener(this);
+
+ options = (ImageView) findViewById(R.id.options);
+ options.setOnClickListener(this);
+ options.setEnabled(false);
+
+ //BottonBar
+ hangUp = (ImageView) findViewById(R.id.hang_up);
+ hangUp.setOnClickListener(this);
+
+ dialer = (ImageView) findViewById(R.id.dialer);
+ dialer.setOnClickListener(this);
+
+ numpad = (Numpad) findViewById(R.id.numpad);
+ numpad.getBackground().setAlpha(240);
+
+ chat = (ImageView) findViewById(R.id.chat);
+ chat.setOnClickListener(this);
+ missedChats = (TextView) findViewById(R.id.missed_chats);
+
+ //Others
+
+ //Active Call
+ callInfo = (LinearLayout) findViewById(R.id.active_call_info);
+
+ pause = (ImageView) findViewById(R.id.pause);
+ pause.setOnClickListener(this);
+ enabledPauseButton(false);
+
+ mActiveCallHeader = (RelativeLayout) findViewById(R.id.active_call);
+ mNoCurrentCall = (LinearLayout) findViewById(R.id.no_current_call);
+ mCallPaused = (LinearLayout) findViewById(R.id.remote_pause);
+
+ contactPicture = (ImageView) findViewById(R.id.contact_picture);
+ avatar_layout = (RelativeLayout) findViewById(R.id.avatar_layout);
+
+ /*if(isTablet()){
+ speaker.setEnabled(false);
+ }
+ speaker.setEnabled(false);*/
+
+ //Options
+ addCall = (ImageView) findViewById(R.id.add_call);
+ addCall.setOnClickListener(this);
+ addCall.setEnabled(false);
+
+ transfer = (ImageView) findViewById(R.id.transfer);
+ transfer.setOnClickListener(this);
+ transfer.setEnabled(false);
+
+ conference = (ImageView) findViewById(R.id.conference);
+ conference.setEnabled(false);
+ conference.setOnClickListener(this);
+
+ try {
+ audioRoute = (ImageView) findViewById(R.id.audio_route);
+ audioRoute.setOnClickListener(this);
+ routeSpeaker = (ImageView) findViewById(R.id.route_speaker);
+ routeSpeaker.setOnClickListener(this);
+ routeEarpiece = (ImageView) findViewById(R.id.route_earpiece);
+ routeEarpiece.setOnClickListener(this);
+ routeBluetooth = (ImageView) findViewById(R.id.route_bluetooth);
+ routeBluetooth.setOnClickListener(this);
+ } catch (NullPointerException npe) {
+ Log.e("Bluetooth: Audio routes menu disabled on tablets for now (1)");
+ }
+
+ switchCamera = (ImageView) findViewById(R.id.switchCamera);
+ switchCamera.setOnClickListener(this);
+
+ mControlsLayout = (LinearLayout) findViewById(R.id.menu);
+
+ if (!isTransferAllowed) {
+ addCall.setBackgroundResource(R.drawable.options_add_call);
+ }
+
+ if (BluetoothManager.getInstance().isBluetoothHeadsetAvailable()) {
+ try {
+ audioRoute.setVisibility(View.VISIBLE);
+ speaker.setVisibility(View.GONE);
+ } catch (NullPointerException npe) { Log.e("Bluetooth: Audio routes menu disabled on tablets for now (2)"); }
+ } else {
+ try {
+ audioRoute.setVisibility(View.GONE);
+ speaker.setVisibility(View.VISIBLE);
+ } catch (NullPointerException npe) { Log.e("Bluetooth: Audio routes menu disabled on tablets for now (3)"); }
+ }
+
+ createInCallStats();
+ LinphoneManager.getInstance().changeStatusToOnThePhone();
+ }
+
+ public void checkAndRequestPermission(String permission, int result) {
+ int permissionGranted = getPackageManager().checkPermission(permission, getPackageName());
+ Log.i("[Permission] " + permission + " is " + (permissionGranted == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ if (permissionGranted != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(permission) || ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
+ Log.i("[Permission] Asking for " + permission);
+ ActivityCompat.requestPermissions(this, new String[] { permission }, result);
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, final int[] grantResults) {
+ for (int i = 0; i < permissions.length; i++) {
+ Log.i("[Permission] " + permissions[i] + " is " + (grantResults[i] == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+ }
+
+ switch (requestCode) {
+ case PERMISSIONS_REQUEST_CAMERA:
+ UIThreadDispatcher.dispatch(new Runnable() {
+ @Override
+ public void run() {
+ acceptCallUpdate(grantResults[0] == PackageManager.PERMISSION_GRANTED);
+ }
+ });
+ break;
+ case PERMISSIONS_ENABLED_CAMERA:
+ UIThreadDispatcher.dispatch(new Runnable() {
+ @Override
+ public void run() {
+ disableVideo(grantResults[0] != PackageManager.PERMISSION_GRANTED);
+ }
+ });
+ break;
+ case PERMISSIONS_ENABLED_MIC:
+ UIThreadDispatcher.dispatch(new Runnable() {
+ @Override
+ public void run() {
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ toggleMicro();
+ }
+ }
+ });
+ break;
+ }
+ }
+
+ public void createInCallStats() {
+ sideMenu = (DrawerLayout) findViewById(R.id.side_menu);
+ menu = (ImageView) findViewById(R.id.call_quality);
+
+ sideMenuContent = (RelativeLayout) findViewById(R.id.side_menu_content);
+
+ menu.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (sideMenu.isDrawerVisible(Gravity.LEFT)) {
+ sideMenu.closeDrawer(sideMenuContent);
+ } else {
+ sideMenu.openDrawer(sideMenuContent);
+ }
+ }
+ });
+
+ status.initCallStatsRefresher(LinphoneManager.getLc().getCurrentCall(), findViewById(R.id.incall_stats));
+
+ }
+
+ private void refreshIncallUi(){
+ refreshInCallActions();
+ refreshCallList(getResources());
+ enableAndRefreshInCallActions();
+ displayMissedChats();
+ }
+
+ private void refreshInCallActions() {
+ if (!LinphonePreferences.instance().isVideoEnabled() || isConferenceRunning) {
+ enabledVideoButton(false);
+ } else {
+ if(video.isEnabled()) {
+ if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) {
+ video.setImageResource(R.drawable.camera_selected);
+ videoProgress.setVisibility(View.INVISIBLE);
+ } else {
+ video.setImageResource(R.drawable.camera_button);
+ }
+ } else {
+ video.setImageResource(R.drawable.camera_button);
+ }
+ }
+ if (getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName()) != PackageManager.PERMISSION_GRANTED) {
+ video.setImageResource(R.drawable.camera_button);
+ }
+
+ if (isSpeakerEnabled) {
+ speaker.setImageResource(R.drawable.speaker_selected);
+ } else {
+ speaker.setImageResource(R.drawable.speaker_default);
+ }
+
+ if (getPackageManager().checkPermission(Manifest.permission.RECORD_AUDIO, getPackageName()) != PackageManager.PERMISSION_GRANTED) {
+ isMicMuted = true;
+ }
+ if (isMicMuted) {
+ micro.setImageResource(R.drawable.micro_selected);
+ } else {
+ micro.setImageResource(R.drawable.micro_default);
+ }
+
+ try {
+ if (isSpeakerEnabled) {
+ routeSpeaker.setImageResource(R.drawable.route_speaker_selected);
+ routeEarpiece.setImageResource(R.drawable.route_earpiece);
+ routeBluetooth.setImageResource(R.drawable.route_bluetooth);
+ }
+
+ routeSpeaker.setImageResource(R.drawable.route_speaker);
+ if (BluetoothManager.getInstance().isUsingBluetoothAudioRoute()) {
+ routeEarpiece.setImageResource(R.drawable.route_earpiece);
+ routeBluetooth.setImageResource(R.drawable.route_bluetooth_selected);
+ } else {
+ routeEarpiece.setImageResource(R.drawable.route_earpiece_selected);
+ routeBluetooth.setImageResource(R.drawable.route_bluetooth);
+ }
+ } catch (NullPointerException npe) {
+ Log.e("Bluetooth: Audio routes menu disabled on tablets for now (4)");
+ }
+ }
+
+ private void enableAndRefreshInCallActions() {
+ int confsize = 0;
+
+ if(LinphoneManager.getLc().isInConference()) {
+ confsize = LinphoneManager.getLc().getConferenceSize() - (LinphoneManager.getLc().isInConference() ? 1 : 0);
+ }
+
+ //Enabled transfer button
+ if(isTransferAllowed && !LinphoneManager.getLc().soundResourcesLocked())
+ enabledTransferButton(true);
+
+ //Enable conference button
+ if(LinphoneManager.getLc().getCallsNb() > 1 && LinphoneManager.getLc().getCallsNb() > confsize && !LinphoneManager.getLc().soundResourcesLocked()) {
+ enabledConferenceButton(true);
+ } else {
+ enabledConferenceButton(false);
+ }
+
+ addCall.setEnabled(LinphoneManager.getLc().getCallsNb() < LinphoneManager.getLc().getMaxCalls() && !LinphoneManager.getLc().soundResourcesLocked());
+ options.setEnabled(!getResources().getBoolean(R.bool.disable_options_in_call) && (addCall.isEnabled() || transfer.isEnabled()));
+
+ if(LinphoneManager.getLc().getCurrentCall() != null && LinphonePreferences.instance().isVideoEnabled() && !LinphoneManager.getLc().getCurrentCall().mediaInProgress()) {
+ enabledVideoButton(true);
+ } else {
+ enabledVideoButton(false);
+ }
+ if(LinphoneManager.getLc().getCurrentCall() != null && !LinphoneManager.getLc().getCurrentCall().mediaInProgress()){
+ enabledPauseButton(true);
+ } else {
+ enabledPauseButton(false);
+ }
+ micro.setEnabled(true);
+ if(!isTablet()){
+ speaker.setEnabled(true);
+ }
+ transfer.setEnabled(true);
+ pause.setEnabled(true);
+ dialer.setEnabled(true);
+ }
+
+ public void updateStatusFragment(StatusFragment statusFragment) {
+ status = statusFragment;
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+
+ if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) {
+ //displayVideoCallControlsIfHidden();
+ }
+
+ if (id == R.id.video) {
+ int camera = getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName());
+ Log.i("[Permission] Camera permission is " + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ if (camera == PackageManager.PERMISSION_GRANTED) {
+ disableVideo(isVideoEnabled(LinphoneManager.getLc().getCurrentCall()));
+ } else {
+ checkAndRequestPermission(Manifest.permission.CAMERA, PERMISSIONS_ENABLED_CAMERA);
+
+ }
+ }
+ else if (id == R.id.micro) {
+ int recordAudio = getPackageManager().checkPermission(Manifest.permission.RECORD_AUDIO, getPackageName());
+ Log.i("[Permission] Record audio permission is " + (recordAudio == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ if (recordAudio == PackageManager.PERMISSION_GRANTED) {
+ toggleMicro();
+ } else {
+ checkAndRequestPermission(Manifest.permission.RECORD_AUDIO, PERMISSIONS_ENABLED_MIC);
+ }
+ }
+ else if (id == R.id.speaker) {
+ toggleSpeaker();
+ }
+ else if (id == R.id.add_call) {
+ goBackToDialer();
+ }
+ else if (id == R.id.pause) {
+ pauseOrResumeCall(LinphoneManager.getLc().getCurrentCall());
+ }
+ else if (id == R.id.hang_up) {
+ hangUp();
+ }
+ else if (id == R.id.dialer) {
+ hideOrDisplayNumpad();
+ }
+ else if (id == R.id.chat) {
+ goToChatList();
+ }
+ else if (id == R.id.conference) {
+ enterConference();
+ hideOrDisplayCallOptions();
+ }
+ else if (id == R.id.switchCamera) {
+ if (videoCallFragment != null) {
+ videoCallFragment.switchCamera();
+ }
+ }
+ else if (id == R.id.transfer) {
+ goBackToDialerAndDisplayTransferButton();
+ }
+ else if (id == R.id.options) {
+ hideOrDisplayCallOptions();
+ }
+ else if (id == R.id.audio_route) {
+ hideOrDisplayAudioRoutes();
+ }
+ else if (id == R.id.route_bluetooth) {
+ if (BluetoothManager.getInstance().routeAudioToBluetooth()) {
+ isSpeakerEnabled = false;
+ routeBluetooth.setImageResource(R.drawable.route_bluetooth_selected);
+ routeSpeaker.setImageResource(R.drawable.route_speaker);
+ routeEarpiece.setImageResource(R.drawable.route_earpiece);
+ }
+ hideOrDisplayAudioRoutes();
+ }
+ else if (id == R.id.route_earpiece) {
+ LinphoneManager.getInstance().routeAudioToReceiver();
+ isSpeakerEnabled = false;
+ routeBluetooth.setImageResource(R.drawable.route_bluetooth);
+ routeSpeaker.setImageResource(R.drawable.route_speaker);
+ routeEarpiece.setImageResource(R.drawable.route_earpiece_selected);
+ hideOrDisplayAudioRoutes();
+ }
+ else if (id == R.id.route_speaker) {
+ LinphoneManager.getInstance().routeAudioToSpeaker();
+ isSpeakerEnabled = true;
+ routeBluetooth.setImageResource(R.drawable.route_bluetooth);
+ routeSpeaker.setImageResource(R.drawable.route_speaker_selected);
+ routeEarpiece.setImageResource(R.drawable.route_earpiece);
+ hideOrDisplayAudioRoutes();
+ }
+
+ else if (id == R.id.call_pause) {
+ LinphoneCall call = (LinphoneCall) v.getTag();
+ pauseOrResumeCall(call);
+ }
+ else if (id == R.id.conference_pause) {
+ pauseOrResumeConference();
+ }
+ }
+
+ private void enabledVideoButton(boolean enabled){
+ if(enabled) {
+ video.setEnabled(true);
+ } else {
+ video.setEnabled(false);
+ }
+ }
+
+ private void enabledPauseButton(boolean enabled){
+ if(enabled) {
+ pause.setEnabled(true);
+ pause.setImageResource(R.drawable.pause_big_default);
+ } else {
+ pause.setEnabled(false);
+ pause.setImageResource(R.drawable.pause_big_disabled);
+ }
+ }
+
+ private void enabledTransferButton(boolean enabled){
+ if(enabled) {
+ transfer.setEnabled(true);
+ } else {
+ transfer.setEnabled(false);
+ }
+ }
+
+ private void enabledConferenceButton(boolean enabled){
+ if (enabled) {
+ conference.setEnabled(true);
+ } else {
+ conference.setEnabled(false);
+ }
+ }
+
+ private void disableVideo(final boolean videoDisabled) {
+ final LinphoneCall call = LinphoneManager.getLc().getCurrentCall();
+ if (call == null) {
+ return;
+ }
+
+ if (videoDisabled) {
+ LinphoneCallParams params = LinphoneManager.getLc().createCallParams(call);
+ params.setVideoEnabled(false);
+ LinphoneManager.getLc().updateCall(call, params);
+ } else {
+ videoProgress.setVisibility(View.VISIBLE);
+ if (!call.getRemoteParams().isLowBandwidthEnabled()) {
+ LinphoneManager.getInstance().addVideo();
+ } else {
+ displayCustomToast(getString(R.string.error_low_bandwidth), Toast.LENGTH_LONG);
+ }
+ }
+ }
+
+ public void displayCustomToast(final String message, final int duration) {
+ LayoutInflater inflater = getLayoutInflater();
+ View layout = inflater.inflate(R.layout.toast, (ViewGroup) findViewById(R.id.toastRoot));
+
+ TextView toastText = (TextView) layout.findViewById(R.id.toastMessage);
+ toastText.setText(message);
+
+ final Toast toast = new Toast(getApplicationContext());
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.setDuration(duration);
+ toast.setView(layout);
+ toast.show();
+ }
+
+ private void switchVideo(final boolean displayVideo) {
+ final LinphoneCall call = LinphoneManager.getLc().getCurrentCall();
+ if (call == null) {
+ return;
+ }
+
+ //Check if the call is not terminated
+ if(call.getState() == State.CallEnd || call.getState() == State.CallReleased) return;
+
+ if (!displayVideo) {
+ showAudioView();
+ } else {
+ if (!call.getRemoteParams().isLowBandwidthEnabled()) {
+ LinphoneManager.getInstance().addVideo();
+ if (videoCallFragment == null || !videoCallFragment.isVisible())
+ showVideoView();
+ } else {
+ displayCustomToast(getString(R.string.error_low_bandwidth), Toast.LENGTH_LONG);
+ }
+ }
+ }
+
+ private void enableProximitySensing(boolean enable){
+ if (enable){
+ if (!mProximitySensingEnabled){
+ mSensorManager.registerListener(this, mProximity, SensorManager.SENSOR_DELAY_NORMAL);
+ mProximitySensingEnabled = true;
+ }
+ }else{
+ if (mProximitySensingEnabled){
+ mSensorManager.unregisterListener(this);
+ mProximitySensingEnabled = false;
+ }
+ }
+ }
+
+ private void showAudioView() {
+ enableProximitySensing(true);
+ replaceFragmentVideoByAudio();
+ displayAudioCall();
+ showStatusBar();
+ removeCallbacks();
+ }
+
+ private void showVideoView() {
+ if (!BluetoothManager.getInstance().isBluetoothHeadsetAvailable()) {
+ Log.w("Bluetooth not available, using speaker");
+ LinphoneManager.getInstance().routeAudioToSpeaker();
+ isSpeakerEnabled = true;
+ }
+ refreshInCallActions();
+
+ enableProximitySensing(false);
+ replaceFragmentAudioByVideo();
+ hideStatusBar();
+ }
+
+ private void displayNoCurrentCall(boolean display){
+ if(!display) {
+ mActiveCallHeader.setVisibility(View.VISIBLE);
+ mNoCurrentCall.setVisibility(View.GONE);
+ } else {
+ mActiveCallHeader.setVisibility(View.GONE);
+ mNoCurrentCall.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void displayCallPaused(boolean display){
+ if(display){
+ mCallPaused.setVisibility(View.VISIBLE);
+ } else {
+ mCallPaused.setVisibility(View.GONE);
+ }
+ }
+
+ private void displayAudioCall(){
+ mControlsLayout.setVisibility(View.VISIBLE);
+ mActiveCallHeader.setVisibility(View.VISIBLE);
+ callInfo.setVisibility(View.VISIBLE);
+ avatar_layout.setVisibility(View.VISIBLE);
+ switchCamera.setVisibility(View.GONE);
+ }
+
+ private void replaceFragmentVideoByAudio() {
+ audioCallFragment = new CallAudioFragment();
+ FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ transaction.replace(R.id.fragmentContainer, audioCallFragment);
+ try {
+ transaction.commitAllowingStateLoss();
+ } catch (Exception e) {
+ }
+ }
+
+ private void replaceFragmentAudioByVideo() {
+// Hiding controls to let displayVideoCallControlsIfHidden add them plus the callback
+ videoCallFragment = new CallVideoFragment();
+
+ FragmentTransaction transaction = getFragmentManager().beginTransaction();
+ transaction.replace(R.id.fragmentContainer, videoCallFragment);
+ try {
+ transaction.commitAllowingStateLoss();
+ } catch (Exception e) {
+ }
+ }
+
+ private void toggleMicro() {
+ LinphoneCore lc = LinphoneManager.getLc();
+ isMicMuted = !isMicMuted;
+ lc.muteMic(isMicMuted);
+ if (isMicMuted) {
+ micro.setImageResource(R.drawable.micro_selected);
+ } else {
+ micro.setImageResource(R.drawable.micro_default);
+ }
+ }
+
+ private void toggleSpeaker() {
+ isSpeakerEnabled = !isSpeakerEnabled;
+ if (isSpeakerEnabled) {
+ LinphoneManager.getInstance().routeAudioToSpeaker();
+ speaker.setImageResource(R.drawable.speaker_selected);
+ LinphoneManager.getLc().enableSpeaker(isSpeakerEnabled);
+ } else {
+ Log.d("Toggle speaker off, routing back to earpiece");
+ LinphoneManager.getInstance().routeAudioToReceiver();
+ speaker.setImageResource(R.drawable.speaker_default);
+ }
+ }
+
+ public void pauseOrResumeCall(LinphoneCall call) {
+ LinphoneCore lc = LinphoneManager.getLc();
+ if (call != null && LinphoneManager.getLc().getCurrentCall() == call) {
+ lc.pauseCall(call);
+ if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) {
+ isVideoCallPaused = true;
+ }
+ pause.setImageResource(R.drawable.pause_big_over_selected);
+ } else if (call != null) {
+ if (call.getState() == State.Paused) {
+ lc.resumeCall(call);
+ if (isVideoCallPaused) {
+ isVideoCallPaused = false;
+ }
+ pause.setImageResource(R.drawable.pause_big_default);
+ }
+ }
+ }
+
+ private void hangUp() {
+ LinphoneCore lc = LinphoneManager.getLc();
+ LinphoneCall currentCall = lc.getCurrentCall();
+
+ if (currentCall != null) {
+ lc.terminateCall(currentCall);
+ } else if (lc.isInConference()) {
+ lc.terminateConference();
+ } else {
+ lc.terminateAllCalls();
+ }
+ }
+
+ public void displayVideoCall(boolean display){
+ if(display) {
+ showStatusBar();
+ mControlsLayout.setVisibility(View.VISIBLE);
+ mActiveCallHeader.setVisibility(View.VISIBLE);
+ callInfo.setVisibility(View.VISIBLE);
+ avatar_layout.setVisibility(View.GONE);
+ callsList.setVisibility(View.VISIBLE);
+ if (cameraNumber > 1) {
+ switchCamera.setVisibility(View.VISIBLE);
+ }
+ } else {
+ hideStatusBar();
+ mControlsLayout.setVisibility(View.GONE);
+ mActiveCallHeader.setVisibility(View.GONE);
+ switchCamera.setVisibility(View.GONE);
+ callsList.setVisibility(View.GONE);
+ }
+ }
+
+
+ public void displayVideoCallControlsIfHidden() {
+ if (mControlsLayout != null) {
+ if (mControlsLayout.getVisibility() != View.VISIBLE) {
+ displayVideoCall(true);
+ }
+ resetControlsHidingCallBack();
+ }
+ }
+
+ public void resetControlsHidingCallBack() {
+ if (mControlsHandler != null && mControls != null) {
+ mControlsHandler.removeCallbacks(mControls);
+ }
+ mControls = null;
+
+ if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall()) && mControlsHandler != null) {
+ mControlsHandler.postDelayed(mControls = new Runnable() {
+ public void run() {
+ hideNumpad();
+ video.setEnabled(true);
+ transfer.setVisibility(View.INVISIBLE);
+ addCall.setVisibility(View.INVISIBLE);
+ conference.setVisibility(View.INVISIBLE);
+ displayVideoCall(false);
+ numpad.setVisibility(View.GONE);
+ options.setImageResource(R.drawable.options_default);
+ }
+ }, SECONDS_BEFORE_HIDING_CONTROLS);
+ }
+ }
+
+ public void removeCallbacks() {
+ if (mControlsHandler != null && mControls != null) {
+ mControlsHandler.removeCallbacks(mControls);
+ }
+ mControls = null;
+ }
+
+ private void hideNumpad() {
+ if (numpad == null || numpad.getVisibility() != View.VISIBLE) {
+ return;
+ }
+
+ dialer.setImageResource(R.drawable.footer_dialer);
+ numpad.setVisibility(View.GONE);
+ }
+
+ private void hideOrDisplayNumpad() {
+ if (numpad == null) {
+ return;
+ }
+
+ if (numpad.getVisibility() == View.VISIBLE) {
+ hideNumpad();
+ } else {
+ dialer.setImageResource(R.drawable.dialer_alt_back);
+ numpad.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void hideOrDisplayAudioRoutes()
+ {
+ if (routeSpeaker.getVisibility() == View.VISIBLE) {
+ routeSpeaker.setVisibility(View.INVISIBLE);
+ routeBluetooth.setVisibility(View.INVISIBLE);
+ routeEarpiece.setVisibility(View.INVISIBLE);
+ } else {
+ routeSpeaker.setVisibility(View.VISIBLE);
+ routeBluetooth.setVisibility(View.VISIBLE);
+ routeEarpiece.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void hideOrDisplayCallOptions() {
+ //Hide options
+ if (addCall.getVisibility() == View.VISIBLE) {
+ options.setImageResource(R.drawable.options_default);
+ if (isTransferAllowed) {
+ transfer.setVisibility(View.INVISIBLE);
+ }
+ addCall.setVisibility(View.INVISIBLE);
+ conference.setVisibility(View.INVISIBLE);
+ } else { //Display options
+ if (isTransferAllowed) {
+ transfer.setVisibility(View.VISIBLE);
+ }
+ addCall.setVisibility(View.VISIBLE);
+ conference.setVisibility(View.VISIBLE);
+ options.setImageResource(R.drawable.options_selected);
+ transfer.setEnabled(LinphoneManager.getLc().getCurrentCall() != null);
+ }
+ }
+
+ public void goBackToDialer() {
+ Intent intent = new Intent();
+ intent.putExtra("Transfer", false);
+ setResult(Activity.RESULT_FIRST_USER, intent);
+ finish();
+ }
+
+ private void goBackToDialerAndDisplayTransferButton() {
+ Intent intent = new Intent();
+ intent.putExtra("Transfer", true);
+ setResult(Activity.RESULT_FIRST_USER, intent);
+ finish();
+ }
+
+ private void goToChatList() {
+ Intent intent = new Intent();
+ intent.putExtra("chat", true);
+ setResult(Activity.RESULT_FIRST_USER, intent);
+ finish();
+ }
+
+ public void acceptCallUpdate(boolean accept) {
+ if (timer != null) {
+ timer.cancel();
+ }
+
+ LinphoneCall call = LinphoneManager.getLc().getCurrentCall();
+ if (call == null) {
+ return;
+ }
+
+ LinphoneCallParams params = LinphoneManager.getLc().createCallParams(call);
+ if (accept) {
+ params.setVideoEnabled(true);
+ LinphoneManager.getLc().enableVideo(true, true);
+ }
+
+ try {
+ LinphoneManager.getLc().acceptCallUpdate(call, params);
+ } catch (LinphoneCoreException e) {
+ Log.e(e);
+ }
+ }
+
+ public void startIncomingCallActivity() {
+ startActivity(new Intent(this, CallIncomingActivity.class));
+ }
+
+ public void hideStatusBar() {
+ if (isTablet()) {
+ return;
+ }
+
+ findViewById(R.id.status).setVisibility(View.GONE);
+ findViewById(R.id.fragmentContainer).setPadding(0, 0, 0, 0);
+ }
+
+ public void showStatusBar() {
+ if (isTablet()) {
+ return;
+ }
+
+ if (status != null && !status.isVisible()) {
+ // Hack to ensure statusFragment is visible after coming back to
+ // dialer from chat
+ status.getView().setVisibility(View.VISIBLE);
+ }
+ findViewById(R.id.status).setVisibility(View.VISIBLE);
+ //findViewById(R.id.fragmentContainer).setPadding(0, LinphoneUtils.pixelsToDpi(getResources(), 40), 0, 0);
+ }
+
+
+ private void showAcceptCallUpdateDialog() {
+ final Dialog dialog = new Dialog(this);
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.colorC));
+ d.setAlpha(200);
+ dialog.setContentView(R.layout.dialog);
+ dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT,WindowManager.LayoutParams.MATCH_PARENT);
+ dialog.getWindow().setBackgroundDrawable(d);
+
+ TextView customText = (TextView) dialog.findViewById(R.id.customText);
+ customText.setText(getResources().getString(R.string.add_video_dialog));
+ Button delete = (Button) dialog.findViewById(R.id.delete_button);
+ delete.setText(R.string.accept);
+ Button cancel = (Button) dialog.findViewById(R.id.cancel);
+ cancel.setText(R.string.decline);
+
+ delete.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ int camera = getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName());
+ Log.i("[Permission] Camera permission is " + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ if (camera == PackageManager.PERMISSION_GRANTED) {
+ CallActivity.instance().acceptCallUpdate(true);
+ } else {
+ checkAndRequestPermission(Manifest.permission.CAMERA, PERMISSIONS_REQUEST_CAMERA);
+ }
+
+ dialog.dismiss();
+ }
+ });
+
+ cancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick (View view){
+ if (CallActivity.isInstanciated()) {
+ CallActivity.instance().acceptCallUpdate(false);
+ }
+ dialog.dismiss();
+ }
+ });
+ dialog.show();
+ }
+
+ @Override
+ protected void onResume() {
+ instance = this;
+ super.onResume();
+
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.addListener(mListener);
+ }
+
+ refreshIncallUi();
+ handleViewIntent();
+
+ if (!isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) {
+ enableProximitySensing(true);
+ removeCallbacks();
+ }
+ }
+
+ private void handleViewIntent() {
+ Intent intent = getIntent();
+ if(intent != null && intent.getAction() == "android.intent.action.VIEW") {
+ LinphoneCall call = LinphoneManager.getLc().getCurrentCall();
+ if(call != null && isVideoEnabled(call)) {
+ LinphonePlayer player = call.getPlayer();
+ String path = intent.getData().getPath();
+ Log.i("Openning " + path);
+ int openRes = player.open(path, new LinphonePlayer.Listener() {
+
+ @Override
+ public void endOfFile(LinphonePlayer player) {
+ player.close();
+ }
+ });
+ if(openRes == -1) {
+ String message = "Could not open " + path;
+ Log.e(message);
+ Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
+ return;
+ }
+ Log.i("Start playing");
+ if(player.start() == -1) {
+ player.close();
+ String message = "Could not start playing " + path;
+ Log.e(message);
+ Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.removeListener(mListener);
+ }
+
+ super.onPause();
+
+ if (mControlsHandler != null && mControls != null) {
+ mControlsHandler.removeCallbacks(mControls);
+ }
+ mControls = null;
+ enableProximitySensing(false);
+ }
+
+ @Override
+ protected void onDestroy() {
+ LinphoneManager.getInstance().changeStatusToOnline();
+
+ if (mControlsHandler != null && mControls != null) {
+ mControlsHandler.removeCallbacks(mControls);
+ }
+ mControls = null;
+ mControlsHandler = null;
+
+ enableProximitySensing(false);
+
+ unbindDrawables(findViewById(R.id.topLayout));
+ instance = null;
+ super.onDestroy();
+ System.gc();
+ }
+
+ private void unbindDrawables(View view) {
+ if (view.getBackground() != null) {
+ view.getBackground().setCallback(null);
+ }
+ if (view instanceof ImageView) {
+ view.setOnClickListener(null);
+ }
+ if (view instanceof ViewGroup && !(view instanceof AdapterView)) {
+ for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
+ unbindDrawables(((ViewGroup) view).getChildAt(i));
+ }
+ ((ViewGroup) view).removeAllViews();
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (LinphoneUtils.onKeyVolumeAdjust(keyCode)) return true;
+ if (LinphoneUtils.onKeyBackGoHome(this, keyCode, event)) return true;
+ return super.onKeyDown(keyCode, event);
+ }
+
+ public void bindAudioFragment(CallAudioFragment fragment) {
+ audioCallFragment = fragment;
+ }
+
+ public void bindVideoFragment(CallVideoFragment fragment) {
+ videoCallFragment = fragment;
+ }
+
+
+ //CALL INFORMATION
+ private void displayCurrentCall(LinphoneCall call){
+ LinphoneAddress lAddress = call.getRemoteAddress();
+ TextView contactName = (TextView) findViewById(R.id.current_contact_name);
+ setContactInformation(contactName, contactPicture, lAddress);
+ registerCallDurationTimer(null, call);
+ }
+
+ private void displayPausedCalls(Resources resources, final LinphoneCall call, int index) {
+ // Control Row
+ LinearLayout callView;
+
+ if(call == null) {
+ callView = (LinearLayout) inflater.inflate(R.layout.conference_paused_row, container, false);
+ callView.setId(index + 1);
+ callView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ pauseOrResumeConference();
+ }
+ });
+ } else {
+ callView = (LinearLayout) inflater.inflate(R.layout.call_inactive_row, container, false);
+ callView.setId(index+1);
+
+ TextView contactName = (TextView) callView.findViewById(R.id.contact_name);
+ ImageView contactImage = (ImageView) callView.findViewById(R.id.contact_picture);
+
+ LinphoneAddress lAddress = call.getRemoteAddress();
+ setContactInformation(contactName, contactImage, lAddress);
+ displayCallStatusIconAndReturnCallPaused(callView, call);
+ registerCallDurationTimer(callView, call);
+ }
+ callsList.addView(callView);
+ }
+
+ private void setContactInformation(TextView contactName, ImageView contactPicture, LinphoneAddress lAddress) {
+ LinphoneContact lContact = ContactsManager.getInstance().findContactFromAddress(lAddress);
+ if (lContact == null) {
+ contactName.setText(LinphoneUtils.getAddressDisplayName(lAddress));
+ contactPicture.setImageResource(R.drawable.avatar);
+ } else {
+ contactName.setText(lContact.getFullName());
+ LinphoneUtils.setImagePictureFromUri(contactPicture.getContext(), contactPicture, lContact.getPhotoUri(), lContact.getThumbnailUri());
+ }
+ }
+
+ private boolean displayCallStatusIconAndReturnCallPaused(LinearLayout callView, LinphoneCall call) {
+ boolean isCallPaused, isInConference;
+ ImageView callState = (ImageView) callView.findViewById(R.id.call_pause);
+ callState.setTag(call);
+ callState.setOnClickListener(this);
+
+ if (call.getState() == State.Paused || call.getState() == State.PausedByRemote || call.getState() == State.Pausing) {
+ callState.setImageResource(R.drawable.pause);
+ isCallPaused = true;
+ isInConference = false;
+ } else if (call.getState() == State.OutgoingInit || call.getState() == State.OutgoingProgress || call.getState() == State.OutgoingRinging) {
+ isCallPaused = false;
+ isInConference = false;
+ } else {
+ isInConference = isConferenceRunning && call.isInConference();
+ isCallPaused = false;
+ }
+
+ return isCallPaused || isInConference;
+ }
+
+ private void registerCallDurationTimer(View v, LinphoneCall call) {
+ int callDuration = call.getDuration();
+ if (callDuration == 0 && call.getState() != State.StreamsRunning) {
+ return;
+ }
+
+ Chronometer timer;
+ if(v == null){
+ timer = (Chronometer) findViewById(R.id.current_call_timer);
+ } else {
+ timer = (Chronometer) v.findViewById(R.id.call_timer);
+ }
+
+ if (timer == null) {
+ throw new IllegalArgumentException("no callee_duration view found");
+ }
+
+ timer.setBase(SystemClock.elapsedRealtime() - 1000 * callDuration);
+ timer.start();
+ }
+
+ public void refreshCallList(Resources resources) {
+ isConferenceRunning = LinphoneManager.getLc().isInConference();
+ List pausedCalls = LinphoneUtils.getCallsInState(LinphoneManager.getLc(), Arrays.asList(State.PausedByRemote));
+
+ //MultiCalls
+ if(LinphoneManager.getLc().getCallsNb() > 1){
+ callsList.setVisibility(View.VISIBLE);
+ }
+
+ //Active call
+ if(LinphoneManager.getLc().getCurrentCall() != null) {
+ displayNoCurrentCall(false);
+ if(isVideoEnabled(LinphoneManager.getLc().getCurrentCall()) && !isConferenceRunning && pausedCalls.size() == 0) {
+ displayVideoCall(false);
+ } else {
+ displayAudioCall();
+ }
+ } else {
+ showAudioView();
+ displayNoCurrentCall(true);
+ if(LinphoneManager.getLc().getCallsNb() == 1) {
+ callsList.setVisibility(View.VISIBLE);
+ }
+ }
+
+ //Conference
+ if (isConferenceRunning) {
+ displayConference(true);
+ } else {
+ displayConference(false);
+ }
+
+ if(callsList != null) {
+ callsList.removeAllViews();
+ int index = 0;
+
+ if (LinphoneManager.getLc().getCallsNb() == 0) {
+ goBackToDialer();
+ return;
+ }
+
+ boolean isConfPaused = false;
+ for (LinphoneCall call : LinphoneManager.getLc().getCalls()) {
+ if (call.isInConference() && !isConferenceRunning) {
+ isConfPaused = true;
+ index++;
+ } else {
+ if (call != LinphoneManager.getLc().getCurrentCall() && !call.isInConference()) {
+ displayPausedCalls(resources, call, index);
+ index++;
+ } else {
+ displayCurrentCall(call);
+ }
+ }
+ }
+
+ if (!isConferenceRunning) {
+ if (isConfPaused) {
+ callsList.setVisibility(View.VISIBLE);
+ displayPausedCalls(resources, null, index);
+ }
+
+ }
+
+ }
+
+ //Paused by remote
+ if (pausedCalls.size() == 1) {
+ displayCallPaused(true);
+ } else {
+ displayCallPaused(false);
+ }
+ }
+
+ //Conference
+ private void exitConference(final LinphoneCall call){
+ LinphoneCore lc = LinphoneManager.getLc();
+
+ if (call.isInConference()) {
+ lc.removeFromConference(call);
+ if (lc.getConferenceSize() <= 1) {
+ lc.leaveConference();
+ }
+ }
+ refreshIncallUi();
+ }
+
+ private void enterConference() {
+ LinphoneManager.getLc().addAllToConference();
+ }
+
+ public void pauseOrResumeConference() {
+ LinphoneCore lc = LinphoneManager.getLc();
+ conferenceStatus = (ImageView) findViewById(R.id.conference_pause);
+ if(conferenceStatus != null) {
+ if (lc.isInConference()) {
+ conferenceStatus.setImageResource(R.drawable.pause_big_over_selected);
+ lc.leaveConference();
+ } else {
+ conferenceStatus.setImageResource(R.drawable.pause_big_default);
+ lc.enterConference();
+ }
+ }
+ refreshCallList(getResources());
+ }
+
+ private void displayConferenceParticipant(int index, final LinphoneCall call){
+ LinearLayout confView = (LinearLayout) inflater.inflate(R.layout.conf_call_control_row, container, false);
+ conferenceList.setId(index + 1);
+ TextView contact = (TextView) confView.findViewById(R.id.contactNameOrNumber);
+
+ LinphoneContact lContact = ContactsManager.getInstance().findContactFromAddress(call.getRemoteAddress());
+ if (lContact == null) {
+ contact.setText(call.getRemoteAddress().getUserName());
+ } else {
+ contact.setText(lContact.getFullName());
+ }
+
+ registerCallDurationTimer(confView, call);
+
+ ImageView quitConference = (ImageView) confView.findViewById(R.id.quitConference);
+ quitConference.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ exitConference(call);
+ }
+ });
+ conferenceList.addView(confView);
+
+ }
+
+ private void displayConferenceHeader(){
+ conferenceList.setVisibility(View.VISIBLE);
+ RelativeLayout headerConference = (RelativeLayout) inflater.inflate(R.layout.conference_header, container, false);
+ conferenceStatus = (ImageView) headerConference.findViewById(R.id.conference_pause);
+ conferenceStatus.setOnClickListener(this);
+ conferenceList.addView(headerConference);
+
+ }
+
+ private void displayConference(boolean isInConf){
+ if(isInConf) {
+ mControlsLayout.setVisibility(View.VISIBLE);
+ enableProximitySensing(true);
+ mActiveCallHeader.setVisibility(View.GONE);
+ mNoCurrentCall.setVisibility(View.GONE);
+ conferenceList.removeAllViews();
+
+ //Conference Header
+ displayConferenceHeader();
+
+ //Conference participant
+ int index = 1;
+ for (LinphoneCall call : LinphoneManager.getLc().getCalls()) {
+ if (call.isInConference()) {
+ displayConferenceParticipant(index, call);
+ index++;
+ }
+ }
+ conferenceList.setVisibility(View.VISIBLE);
+ } else {
+ conferenceList.setVisibility(View.GONE);
+ }
+ }
+
+ public static Boolean isProximitySensorNearby(final SensorEvent event) {
+ float threshold = 4.001f; // <= 4 cm is near
+
+ final float distanceInCm = event.values[0];
+ final float maxDistance = event.sensor.getMaximumRange();
+ Log.d("Proximity sensor report [",distanceInCm,"] , for max range [",maxDistance,"]");
+
+ if (maxDistance <= threshold) {
+ // Case binary 0/1 and short sensors
+ threshold = maxDistance;
+ }
+ return distanceInCm < threshold;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (event.timestamp == 0) return;
+ if(isProximitySensorNearby(event)){
+ if(!wakeLock.isHeld()) {
+ wakeLock.acquire();
+ }
+ } else {
+ if(wakeLock.isHeld()) {
+ wakeLock.release();
+ }
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+
+ }
+
+ private void displayMissedChats() {
+ int count = 0;
+ LinphoneChatRoom[] chats = LinphoneManager.getLc().getChatRooms();
+ for (LinphoneChatRoom chatroom : chats) {
+ count += chatroom.getUnreadMessagesCount();
+ }
+
+ if (count > 0) {
+ missedChats.setText(count + "");
+ missedChats.setVisibility(View.VISIBLE);
+ } else {
+ missedChats.clearAnimation();
+ missedChats.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallAudioFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallAudioFragment.java
new file mode 100644
index 0000000..a4b3b18
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallAudioFragment.java
@@ -0,0 +1,99 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+CallAudioFragment.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class CallAudioFragment extends Fragment {
+ private CallActivity incallActvityInstance;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.audio, container, false);
+ return view;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ incallActvityInstance = (CallActivity) getActivity();
+
+ if (incallActvityInstance != null) {
+ incallActvityInstance.bindAudioFragment(this);
+ }
+
+ // Just to be sure we have incall controls
+ if (incallActvityInstance != null) {
+ incallActvityInstance.removeCallbacks();
+ }
+ }
+
+ class SwipeGestureDetector implements OnTouchListener {
+ static final int MIN_DISTANCE = 100;
+ private float downX, upX;
+ private boolean lock;
+
+ private SwipeListener listener;
+
+ public SwipeGestureDetector(SwipeListener swipeListener) {
+ super();
+ listener = swipeListener;
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ switch(event.getAction()){
+ case MotionEvent.ACTION_DOWN:
+ lock = false;
+ downX = event.getX();
+ return true;
+
+ case MotionEvent.ACTION_MOVE:
+ if (lock) {
+ return false;
+ }
+ upX = event.getX();
+
+ float deltaX = downX - upX;
+
+ if (Math.abs(deltaX) > MIN_DISTANCE) {
+ lock = true;
+ if (deltaX < 0) { listener.onLeftToRightSwipe(); return true; }
+ if (deltaX > 0) { listener.onRightToLeftSwipe(); return true; }
+ }
+ break;
+ }
+ return false;
+ }
+ }
+
+ interface SwipeListener {
+ void onRightToLeftSwipe();
+ void onLeftToRightSwipe();
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallIncomingActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallIncomingActivity.java
new file mode 100644
index 0000000..c119b3d
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallIncomingActivity.java
@@ -0,0 +1,360 @@
+/*
+CallIncomingActivity.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.compatibility.Compatibility;
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneCall;
+import org.linphone.core.LinphoneCall.State;
+import org.linphone.core.LinphoneCallParams;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCoreListenerBase;
+import org.linphone.mediastream.Log;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.ui.LinphoneSliders.LinphoneSliderTriggered;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.support.v4.app.ActivityCompat;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+public class CallIncomingActivity extends Activity implements LinphoneSliderTriggered {
+ private static CallIncomingActivity instance;
+
+ private TextView name, number;
+ private ImageView contactPicture, accept, decline;
+ private LinphoneCall mCall;
+ private LinphoneCoreListenerBase mListener;
+ private LinearLayout acceptUnlock;
+ private LinearLayout declineUnlock;
+ private boolean isScreenActive, alreadyAcceptedOrDeniedCall;
+ private float answerX;
+ private float declineX;
+
+ public static CallIncomingActivity instance() {
+ return instance;
+ }
+
+ public static boolean isInstanciated() {
+ return instance != null;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (getResources().getBoolean(R.bool.orientation_portrait_only)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.call_incoming);
+
+ name = (TextView) findViewById(R.id.contact_name);
+ number = (TextView) findViewById(R.id.contact_number);
+ contactPicture = (ImageView) findViewById(R.id.contact_picture);
+
+ // set this flag so this activity will stay in front of the keyguard
+ int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+ getWindow().addFlags(flags);
+
+ PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ isScreenActive = Compatibility.isScreenOn(pm);
+
+ final int screenWidth = getResources().getDisplayMetrics().widthPixels;
+
+ acceptUnlock = (LinearLayout) findViewById(R.id.acceptUnlock);
+ declineUnlock = (LinearLayout) findViewById(R.id.declineUnlock);
+
+ accept = (ImageView) findViewById(R.id.accept);
+ decline = (ImageView) findViewById(R.id.decline);
+ accept.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if(isScreenActive) {
+ answer();
+ } else {
+ decline.setVisibility(View.GONE);
+ acceptUnlock.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+
+ if(!isScreenActive) {
+ accept.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ float curX;
+ switch (motionEvent.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ acceptUnlock.setVisibility(View.VISIBLE);
+ decline.setVisibility(View.GONE);
+ answerX = motionEvent.getX();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ curX = motionEvent.getX();
+ if((answerX - curX) >= 0)
+ view.scrollBy((int) (answerX - curX), view.getScrollY());
+ answerX = curX;
+ if (curX < screenWidth/4) {
+ answer();
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ view.scrollTo(0, view.getScrollY());
+ decline.setVisibility(View.VISIBLE);
+ acceptUnlock.setVisibility(View.GONE);
+ break;
+ }
+ return true;
+ }
+ });
+
+ decline.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ float curX;
+ switch (motionEvent.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ declineUnlock.setVisibility(View.VISIBLE);
+ accept.setVisibility(View.GONE);
+ declineX = motionEvent.getX();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ curX = motionEvent.getX();
+ view.scrollBy((int) (declineX - curX), view.getScrollY());
+ declineX = curX;
+ Log.w(curX);
+ if (curX > (screenWidth/2)){
+ decline();
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ view.scrollTo(0, view.getScrollY());
+ accept.setVisibility(View.VISIBLE);
+ declineUnlock.setVisibility(View.GONE);
+ break;
+
+ }
+ return true;
+ }
+ });
+ }
+
+ decline.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if(isScreenActive) {
+ decline();
+ } else {
+ accept.setVisibility(View.GONE);
+ acceptUnlock.setVisibility(View.VISIBLE);
+ }
+ }
+ });
+
+ mListener = new LinphoneCoreListenerBase(){
+ @Override
+ public void callState(LinphoneCore lc, LinphoneCall call, State state, String message) {
+ if (call == mCall && State.CallEnd == state) {
+ finish();
+ }
+ if (state == State.StreamsRunning) {
+ // The following should not be needed except some devices need it (e.g. Galaxy S).
+ LinphoneManager.getLc().enableSpeaker(LinphoneManager.getLc().isSpeakerEnabled());
+ }
+ }
+ };
+
+ super.onCreate(savedInstanceState);
+ instance = this;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ instance = this;
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.addListener(mListener);
+ }
+
+ alreadyAcceptedOrDeniedCall = false;
+ mCall = null;
+
+ // Only one call ringing at a time is allowed
+ if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null) {
+ List calls = LinphoneUtils.getLinphoneCalls(LinphoneManager.getLc());
+ for (LinphoneCall call : calls) {
+ if (State.IncomingReceived == call.getState()) {
+ mCall = call;
+ break;
+ }
+ }
+ }
+ if (mCall == null) {
+ //The incoming call no longer exists.
+ Log.d("Couldn't find incoming call");
+ finish();
+ return;
+ }
+
+
+ LinphoneAddress address = mCall.getRemoteAddress();
+ LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address);
+ if (contact != null) {
+ LinphoneUtils.setImagePictureFromUri(this, contactPicture, contact.getPhotoUri(), contact.getThumbnailUri());
+ name.setText(contact.getFullName());
+ } else {
+ name.setText(LinphoneUtils.getAddressDisplayName(address));
+ }
+ number.setText(address.asStringUriOnly());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ checkAndRequestCallPermissions();
+ }
+
+ @Override
+ protected void onPause() {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.removeListener(mListener);
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ instance = null;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (LinphoneManager.isInstanciated() && (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME)) {
+ LinphoneManager.getLc().terminateCall(mCall);
+ finish();
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void decline() {
+ if (alreadyAcceptedOrDeniedCall) {
+ return;
+ }
+ alreadyAcceptedOrDeniedCall = true;
+
+ LinphoneManager.getLc().terminateCall(mCall);
+ finish();
+ }
+
+ private void answer() {
+ if (alreadyAcceptedOrDeniedCall) {
+ return;
+ }
+ alreadyAcceptedOrDeniedCall = true;
+
+ LinphoneCallParams params = LinphoneManager.getLc().createCallParams(mCall);
+
+ boolean isLowBandwidthConnection = !LinphoneUtils.isHighBandwidthConnection(LinphoneService.instance().getApplicationContext());
+
+ if (params != null) {
+ params.enableLowBandwidth(isLowBandwidthConnection);
+ }else {
+ Log.e("Could not create call params for call");
+ }
+
+ if (params == null || !LinphoneManager.getInstance().acceptCallWithParams(mCall, params)) {
+ // the above method takes care of Samsung Galaxy S
+ Toast.makeText(this, R.string.couldnt_accept_call, Toast.LENGTH_LONG).show();
+ } else {
+ if (!LinphoneActivity.isInstanciated()) {
+ return;
+ }
+ LinphoneManager.getInstance().routeAudioToReceiver();
+ LinphoneActivity.instance().startIncallActivity(mCall);
+ }
+ }
+
+ @Override
+ public void onLeftHandleTriggered() {
+
+ }
+
+ @Override
+ public void onRightHandleTriggered() {
+
+ }
+
+ private void checkAndRequestCallPermissions() {
+ ArrayList permissionsList = new ArrayList();
+
+ int recordAudio = getPackageManager().checkPermission(Manifest.permission.RECORD_AUDIO, getPackageName());
+ Log.i("[Permission] Record audio permission is " + (recordAudio == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+ int camera = getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName());
+ Log.i("[Permission] Camera permission is " + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ if (recordAudio != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(Manifest.permission.RECORD_AUDIO) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
+ Log.i("[Permission] Asking for record audio");
+ permissionsList.add(Manifest.permission.RECORD_AUDIO);
+ }
+ }
+ if (LinphonePreferences.instance().shouldInitiateVideoCall() || LinphonePreferences.instance().shouldAutomaticallyAcceptVideoRequests()) {
+ if (camera != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(Manifest.permission.CAMERA) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
+ Log.i("[Permission] Asking for camera");
+ permissionsList.add(Manifest.permission.CAMERA);
+ }
+ }
+ }
+
+ if (permissionsList.size() > 0) {
+ String[] permissions = new String[permissionsList.size()];
+ permissions = permissionsList.toArray(permissions);
+ ActivityCompat.requestPermissions(this, permissions, 0);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ for (int i = 0; i < permissions.length; i++) {
+ Log.i("[Permission] " + permissions[i] + " is " + (grantResults[i] == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallManager.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallManager.java
new file mode 100644
index 0000000..c59c67c
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallManager.java
@@ -0,0 +1,140 @@
+/*
+CallManager.java
+Copyright (C) 2010 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneCall;
+import org.linphone.core.LinphoneCallParams;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCoreException;
+import org.linphone.mediastream.Log;
+
+
+/**
+ * Handle call updating, reinvites.
+ *
+ * @author Guillaume Beraudo
+ *
+ */
+public class CallManager {
+
+ private static CallManager instance;
+
+ private CallManager() {}
+ public static final synchronized CallManager getInstance() {
+ if (instance == null) instance = new CallManager();
+ return instance;
+ }
+
+ private BandwidthManager bm() {
+ return BandwidthManager.getInstance();
+ }
+
+
+
+
+ public void inviteAddress(LinphoneAddress lAddress, boolean videoEnabled, boolean lowBandwidth) throws LinphoneCoreException {
+ LinphoneCore lc = LinphoneManager.getLc();
+
+ LinphoneCallParams params = lc.createCallParams(null);
+ bm().updateWithProfileSettings(lc, params);
+
+ if (videoEnabled && params.getVideoEnabled()) {
+ params.setVideoEnabled(true);
+ } else {
+ params.setVideoEnabled(false);
+ }
+
+ if (lowBandwidth) {
+ params.enableLowBandwidth(true);
+ Log.d("Low bandwidth enabled in call params");
+ }
+
+ lc.inviteAddressWithParams(lAddress, params);
+ }
+
+
+
+
+ /**
+ * Add video to a currently running voice only call.
+ * No re-invite is sent if the current call is already video
+ * or if the bandwidth settings are too low.
+ * @return if updateCall called
+ */
+ boolean reinviteWithVideo() {
+ LinphoneCore lc = LinphoneManager.getLc();
+ LinphoneCall lCall = lc.getCurrentCall();
+ if (lCall == null) {
+ Log.e("Trying to reinviteWithVideo while not in call: doing nothing");
+ return false;
+ }
+ LinphoneCallParams params = lCall.getCurrentParamsCopy();
+
+ if (params.getVideoEnabled()) return false;
+
+
+ // Check if video possible regarding bandwidth limitations
+ bm().updateWithProfileSettings(lc, params);
+
+ // Abort if not enough bandwidth...
+ if (!params.getVideoEnabled()) {
+ return false;
+ }
+
+ // Not yet in video call: try to re-invite with video
+ lc.updateCall(lCall, params);
+ return true;
+ }
+
+
+
+ /**
+ * Re-invite with parameters updated from profile.
+ */
+ void reinvite() {
+ LinphoneCore lc = LinphoneManager.getLc();
+ LinphoneCall lCall = lc.getCurrentCall();
+ if (lCall == null) {
+ Log.e("Trying to reinvite while not in call: doing nothing");
+ return;
+ }
+ LinphoneCallParams params = lCall.getCurrentParamsCopy();
+ bm().updateWithProfileSettings(lc, params);
+ lc.updateCall(lCall, params);
+ }
+
+ /**
+ * Change the preferred video size used by linphone core. (impact landscape/portrait buffer).
+ * Update current call, without reinvite.
+ * The camera will be restarted when mediastreamer chain is recreated and setParameters is called.
+ */
+ public void updateCall() {
+ LinphoneCore lc = LinphoneManager.getLc();
+ LinphoneCall lCall = lc.getCurrentCall();
+ if (lCall == null) {
+ Log.e("Trying to updateCall while not in call: doing nothing");
+ return;
+ }
+ LinphoneCallParams params = lCall.getCurrentParamsCopy();
+ bm().updateWithProfileSettings(lc, params);
+ lc.updateCall(lCall, null);
+ }
+
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallOutgoingActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallOutgoingActivity.java
new file mode 100644
index 0000000..111d12e
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallOutgoingActivity.java
@@ -0,0 +1,288 @@
+/*
+CallOutgoingActivity.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneCall;
+import org.linphone.core.LinphoneCall.State;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCoreListenerBase;
+import org.linphone.core.Reason;
+import org.linphone.mediastream.Log;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+public class CallOutgoingActivity extends Activity implements OnClickListener{
+ private static CallOutgoingActivity instance;
+
+ private TextView name, number;
+ private ImageView contactPicture, micro, speaker, hangUp;
+ private LinphoneCall mCall;
+ private LinphoneCoreListenerBase mListener;
+ private boolean isMicMuted, isSpeakerEnabled;
+
+ public static CallOutgoingActivity instance() {
+ return instance;
+ }
+
+ public static boolean isInstanciated() {
+ return instance != null;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (getResources().getBoolean(R.bool.orientation_portrait_only)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.call_outgoing);
+
+ name = (TextView) findViewById(R.id.contact_name);
+ number = (TextView) findViewById(R.id.contact_number);
+ contactPicture = (ImageView) findViewById(R.id.contact_picture);
+
+ isMicMuted = false;
+ isSpeakerEnabled = false;
+
+ micro = (ImageView) findViewById(R.id.micro);
+ micro.setOnClickListener(this);
+ speaker = (ImageView) findViewById(R.id.speaker);
+ speaker.setOnClickListener(this);
+
+ // set this flag so this activity will stay in front of the keyguard
+ int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+ getWindow().addFlags(flags);
+
+ hangUp = (ImageView) findViewById(R.id.outgoing_hang_up);
+ hangUp.setOnClickListener(this);
+
+ mListener = new LinphoneCoreListenerBase(){
+ @Override
+ public void callState(LinphoneCore lc, LinphoneCall call, LinphoneCall.State state, String message) {
+ if (call == mCall && State.Connected == state) {
+ if (!LinphoneActivity.isInstanciated()) {
+ return;
+ }
+ LinphoneActivity.instance().startIncallActivity(mCall);
+ finish();
+ return;
+ } else if (state == State.Error) {
+ // Convert LinphoneCore message for internalization
+ if (message != null && call.getErrorInfo().getReason() == Reason.Declined) {
+ displayCustomToast(getString(R.string.error_call_declined), Toast.LENGTH_SHORT);
+ } else if (message != null && call.getErrorInfo().getReason() == Reason.NotFound) {
+ displayCustomToast(getString(R.string.error_user_not_found), Toast.LENGTH_SHORT);
+ } else if (message != null && call.getErrorInfo().getReason() == Reason.Media) {
+ displayCustomToast(getString(R.string.error_incompatible_media), Toast.LENGTH_SHORT);
+ } else if (message != null && call.getErrorInfo().getReason() == Reason.Busy) {
+ displayCustomToast(getString(R.string.error_user_busy), Toast.LENGTH_SHORT);
+ } else if (message != null) {
+ displayCustomToast(getString(R.string.error_unknown) + " - " + message, Toast.LENGTH_SHORT);
+ }
+ }
+
+ if (LinphoneManager.getLc().getCallsNb() == 0) {
+ finish();
+ return;
+ }
+ }
+ };
+ instance = this;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ instance = this;
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.addListener(mListener);
+ }
+
+ // Only one call ringing at a time is allowed
+ if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null) {
+ List calls = LinphoneUtils.getLinphoneCalls(LinphoneManager.getLc());
+ for (LinphoneCall call : calls) {
+ State cstate = call.getState();
+ if (State.OutgoingInit == cstate || State.OutgoingProgress == cstate
+ || State.OutgoingRinging == cstate || State.OutgoingEarlyMedia == cstate) {
+ mCall = call;
+ break;
+ }
+ if (State.StreamsRunning == cstate) {
+ if (!LinphoneActivity.isInstanciated()) {
+ return;
+ }
+ LinphoneActivity.instance().startIncallActivity(mCall);
+ finish();
+ }
+ }
+ }
+ if (mCall == null) {
+ Log.e("Couldn't find outgoing call");
+ LinphoneActivity.instance().goToDialerFragment();
+ finish();
+ return;
+ }
+
+ LinphoneAddress address = mCall.getRemoteAddress();
+ LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address);
+ if (contact != null) {
+ LinphoneUtils.setImagePictureFromUri(this, contactPicture, contact.getPhotoUri(), contact.getThumbnailUri());
+ name.setText(contact.getFullName());
+ } else {
+ name.setText(LinphoneUtils.getAddressDisplayName(address));
+ }
+ number.setText(address.asStringUriOnly());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ checkAndRequestCallPermissions();
+ }
+
+ @Override
+ protected void onPause() {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.removeListener(mListener);
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ instance = null;
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+
+ if (id == R.id.micro) {
+ isMicMuted = !isMicMuted;
+ if(isMicMuted) {
+ micro.setImageResource(R.drawable.micro_selected);
+ } else {
+ micro.setImageResource(R.drawable.micro_default);
+ }
+ LinphoneManager.getLc().muteMic(isMicMuted);
+ }
+ if (id == R.id.speaker) {
+ isSpeakerEnabled = !isSpeakerEnabled;
+ if(isSpeakerEnabled) {
+ speaker.setImageResource(R.drawable.speaker_selected);
+ } else {
+ speaker.setImageResource(R.drawable.speaker_default);
+ }
+ LinphoneManager.getLc().enableSpeaker(isSpeakerEnabled);
+ }
+ if (id == R.id.outgoing_hang_up) {
+ decline();
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (LinphoneManager.isInstanciated() && (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_HOME)) {
+ LinphoneManager.getLc().terminateCall(mCall);
+ finish();
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ public void displayCustomToast(final String message, final int duration) {
+ LayoutInflater inflater = getLayoutInflater();
+ View layout = inflater.inflate(R.layout.toast, (ViewGroup) findViewById(R.id.toastRoot));
+
+ TextView toastText = (TextView) layout.findViewById(R.id.toastMessage);
+ toastText.setText(message);
+
+ final Toast toast = new Toast(getApplicationContext());
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.setDuration(duration);
+ toast.setView(layout);
+ toast.show();
+ }
+
+ private void decline() {
+ LinphoneManager.getLc().terminateCall(mCall);
+ finish();
+ }
+
+ private void checkAndRequestCallPermissions() {
+ ArrayList permissionsList = new ArrayList();
+
+ int recordAudio = getPackageManager().checkPermission(Manifest.permission.RECORD_AUDIO, getPackageName());
+ Log.i("[Permission] Record audio permission is " + (recordAudio == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+ int camera = getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName());
+ Log.i("[Permission] Camera permission is " + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ if (recordAudio != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(Manifest.permission.RECORD_AUDIO) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
+ Log.i("[Permission] Asking for record audio");
+ permissionsList.add(Manifest.permission.RECORD_AUDIO);
+ }
+ }
+ if (LinphonePreferences.instance().shouldInitiateVideoCall() || LinphonePreferences.instance().shouldAutomaticallyAcceptVideoRequests()) {
+ if (camera != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(Manifest.permission.CAMERA) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
+ Log.i("[Permission] Asking for camera");
+ permissionsList.add(Manifest.permission.CAMERA);
+ }
+ }
+ }
+
+ if (permissionsList.size() > 0) {
+ String[] permissions = new String[permissionsList.size()];
+ permissions = permissionsList.toArray(permissions);
+ ActivityCompat.requestPermissions(this, permissions, 0);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ for (int i = 0; i < permissions.length; i++) {
+ Log.i("[Permission] " + permissions[i] + " is " + (grantResults[i] == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallVideoFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallVideoFragment.java
new file mode 100644
index 0000000..a22bb0f
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/CallVideoFragment.java
@@ -0,0 +1,339 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+CallVideoFragment.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.compatibility.Compatibility;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.compatibility.CompatibilityScaleGestureDetector;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.compatibility.CompatibilityScaleGestureListener;
+import org.linphone.core.LinphoneCall;
+import org.linphone.mediastream.Log;
+import org.linphone.mediastream.video.AndroidVideoWindowImpl;
+import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration;
+
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnDoubleTapListener;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class CallVideoFragment extends Fragment implements OnGestureListener, OnDoubleTapListener, CompatibilityScaleGestureListener {
+ private SurfaceView mVideoView;
+ private SurfaceView mCaptureView;
+ private AndroidVideoWindowImpl androidVideoWindowImpl;
+ private GestureDetector mGestureDetector;
+ private float mZoomFactor = 1.f;
+ private float mZoomCenterX, mZoomCenterY;
+ private CompatibilityScaleGestureDetector mScaleDetector;
+ private CallActivity inCallActivity;
+ private int previewX, previewY;
+
+ @SuppressWarnings("deprecation") // Warning useless because value is ignored and automatically set by new APIs.
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view;
+ if (LinphoneManager.getLc().hasCrappyOpenGL()) {
+ view = inflater.inflate(R.layout.video_no_opengl, container, false);
+ } else {
+ view = inflater.inflate(R.layout.video, container, false);
+ }
+
+ mVideoView = (SurfaceView) view.findViewById(R.id.videoSurface);
+ mCaptureView = (SurfaceView) view.findViewById(R.id.videoCaptureSurface);
+ mCaptureView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); // Warning useless because value is ignored and automatically set by new APIs.
+
+ fixZOrder(mVideoView, mCaptureView);
+
+ androidVideoWindowImpl = new AndroidVideoWindowImpl(mVideoView, mCaptureView, new AndroidVideoWindowImpl.VideoWindowListener() {
+ public void onVideoRenderingSurfaceReady(AndroidVideoWindowImpl vw, SurfaceView surface) {
+ mVideoView = surface;
+ LinphoneManager.getLc().setVideoWindow(vw);
+ }
+
+ public void onVideoRenderingSurfaceDestroyed(AndroidVideoWindowImpl vw) {
+
+ }
+
+ public void onVideoPreviewSurfaceReady(AndroidVideoWindowImpl vw, SurfaceView surface) {
+ mCaptureView = surface;
+ LinphoneManager.getLc().setPreviewWindow(mCaptureView);
+ }
+
+ public void onVideoPreviewSurfaceDestroyed(AndroidVideoWindowImpl vw) {
+
+ }
+ });
+
+ mVideoView.setOnTouchListener(new OnTouchListener() {
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mScaleDetector != null) {
+ mScaleDetector.onTouchEvent(event);
+ }
+
+ mGestureDetector.onTouchEvent(event);
+ if (inCallActivity != null) {
+ inCallActivity.displayVideoCallControlsIfHidden();
+ }
+ return true;
+ }
+ });
+
+ mCaptureView.setOnTouchListener(new OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ switch (motionEvent.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ previewX = (int) motionEvent.getX();
+ previewY = (int) motionEvent.getY();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ int x = (int) motionEvent.getX();
+ int y = (int) motionEvent.getY();
+ RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)mCaptureView.getLayoutParams();
+ lp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 0); // Clears the rule, as there is no removeRule until API 17.
+ lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, 0);
+ int left = lp.leftMargin + (x - previewX);
+ int top = lp.topMargin + (y - previewY);
+ lp.leftMargin = left;
+ lp.topMargin = top;
+ view.setLayoutParams(lp);
+ break;
+ }
+ return true;
+ }
+ });
+ return view;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ inCallActivity = (CallActivity) getActivity();
+ if (inCallActivity != null) {
+ inCallActivity.bindVideoFragment(this);
+ }
+ }
+
+ private void fixZOrder(SurfaceView video, SurfaceView preview) {
+ video.setZOrderOnTop(false);
+ preview.setZOrderOnTop(true);
+ preview.setZOrderMediaOverlay(true); // Needed to be able to display control layout over
+ }
+
+ public void switchCamera() {
+ try {
+ int videoDeviceId = LinphoneManager.getLc().getVideoDevice();
+ videoDeviceId = (videoDeviceId + 1) % AndroidCameraConfiguration.retrieveCameras().length;
+ LinphoneManager.getLc().setVideoDevice(videoDeviceId);
+ CallManager.getInstance().updateCall();
+
+ // previous call will cause graph reconstruction -> regive preview
+ // window
+ if (mCaptureView != null) {
+ LinphoneManager.getLc().setPreviewWindow(mCaptureView);
+ }
+ } catch (ArithmeticException ae) {
+ Log.e("Cannot swtich camera : no camera");
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (LinphonePreferences.instance().isOverlayEnabled()) {
+ LinphoneService.instance().destroyOverlay();
+ }
+ if (androidVideoWindowImpl != null) {
+ synchronized (androidVideoWindowImpl) {
+ LinphoneManager.getLc().setVideoWindow(androidVideoWindowImpl);
+ }
+ }
+
+ mGestureDetector = new GestureDetector(inCallActivity, this);
+ mScaleDetector = Compatibility.getScaleGestureDetector(inCallActivity, this);
+ }
+
+ @Override
+ public void onPause() {
+ if (androidVideoWindowImpl != null) {
+ synchronized (androidVideoWindowImpl) {
+ /*
+ * this call will destroy native opengl renderer which is used by
+ * androidVideoWindowImpl
+ */
+ LinphoneManager.getLc().setVideoWindow(null);
+ }
+ }
+ if (LinphonePreferences.instance().isOverlayEnabled()) {
+ LinphoneService.instance().createOverlay();
+ }
+
+ super.onPause();
+ }
+
+ public boolean onScale(CompatibilityScaleGestureDetector detector) {
+ mZoomFactor *= detector.getScaleFactor();
+ // Don't let the object get too small or too large.
+ // Zoom to make the video fill the screen vertically
+ float portraitZoomFactor = ((float) mVideoView.getHeight()) / (float) ((3 * mVideoView.getWidth()) / 4);
+ // Zoom to make the video fill the screen horizontally
+ float landscapeZoomFactor = ((float) mVideoView.getWidth()) / (float) ((3 * mVideoView.getHeight()) / 4);
+ mZoomFactor = Math.max(0.1f, Math.min(mZoomFactor, Math.max(portraitZoomFactor, landscapeZoomFactor)));
+
+ LinphoneCall currentCall = LinphoneManager.getLc().getCurrentCall();
+ if (currentCall != null) {
+ currentCall.zoomVideo(mZoomFactor, mZoomCenterX, mZoomCenterY);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (LinphoneUtils.isCallEstablished(LinphoneManager.getLc().getCurrentCall())) {
+ if (mZoomFactor > 1) {
+ // Video is zoomed, slide is used to change center of zoom
+ if (distanceX > 0 && mZoomCenterX < 1) {
+ mZoomCenterX += 0.01;
+ } else if(distanceX < 0 && mZoomCenterX > 0) {
+ mZoomCenterX -= 0.01;
+ }
+ if (distanceY < 0 && mZoomCenterY < 1) {
+ mZoomCenterY += 0.01;
+ } else if(distanceY > 0 && mZoomCenterY > 0) {
+ mZoomCenterY -= 0.01;
+ }
+
+ if (mZoomCenterX > 1)
+ mZoomCenterX = 1;
+ if (mZoomCenterX < 0)
+ mZoomCenterX = 0;
+ if (mZoomCenterY > 1)
+ mZoomCenterY = 1;
+ if (mZoomCenterY < 0)
+ mZoomCenterY = 0;
+
+ LinphoneManager.getLc().getCurrentCall().zoomVideo(mZoomFactor, mZoomCenterX, mZoomCenterY);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ if (LinphoneUtils.isCallEstablished(LinphoneManager.getLc().getCurrentCall())) {
+ if (mZoomFactor == 1.f) {
+ // Zoom to make the video fill the screen vertically
+ float portraitZoomFactor = ((float) mVideoView.getHeight()) / (float) ((3 * mVideoView.getWidth()) / 4);
+ // Zoom to make the video fill the screen horizontally
+ float landscapeZoomFactor = ((float) mVideoView.getWidth()) / (float) ((3 * mVideoView.getHeight()) / 4);
+
+ mZoomFactor = Math.max(portraitZoomFactor, landscapeZoomFactor);
+ }
+ else {
+ resetZoom();
+ }
+
+ LinphoneManager.getLc().getCurrentCall().zoomVideo(mZoomFactor, mZoomCenterX, mZoomCenterY);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void resetZoom() {
+ mZoomFactor = 1.f;
+ mZoomCenterX = mZoomCenterY = 0.5f;
+ }
+
+ @Override
+ public void onDestroy() {
+ inCallActivity = null;
+
+ mCaptureView = null;
+ if (mVideoView != null) {
+ mVideoView.setOnTouchListener(null);
+ mVideoView = null;
+ }
+ if (androidVideoWindowImpl != null) {
+ // Prevent linphone from crashing if correspondent hang up while you are rotating
+ androidVideoWindowImpl.release();
+ androidVideoWindowImpl = null;
+ }
+ if (mGestureDetector != null) {
+ mGestureDetector.setOnDoubleTapListener(null);
+ mGestureDetector = null;
+ }
+ if (mScaleDetector != null) {
+ mScaleDetector.destroy();
+ mScaleDetector = null;
+ }
+
+ super.onDestroy();
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true; // Needed to make the GestureDetector working
+ }
+
+ @Override
+ public boolean onDoubleTapEvent(MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+ return false;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ return false;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ChatFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ChatFragment.java
new file mode 100644
index 0000000..ae2e5cd
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ChatFragment.java
@@ -0,0 +1,1559 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+ChatFragment.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Locale;
+
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.compatibility.Compatibility;
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneBuffer;
+import org.linphone.core.LinphoneChatMessage;
+import org.linphone.core.LinphoneChatMessage.State;
+import org.linphone.core.LinphoneChatRoom;
+import org.linphone.core.LinphoneContent;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCoreFactory;
+import org.linphone.core.LinphoneCoreListenerBase;
+import org.linphone.mediastream.Log;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.Fragment;
+import android.app.ProgressDialog;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.ExifInterface;
+import android.media.ThumbnailUtils;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+import android.support.v4.content.CursorLoader;
+import android.text.Editable;
+import android.text.Spanned;
+import android.text.TextWatcher;
+import android.text.method.LinkMovementMethod;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+
+public class ChatFragment extends Fragment implements OnClickListener, LinphoneChatMessage.LinphoneChatMessageListener {
+ private static final int ADD_PHOTO = 1337;
+ private static final int MENU_DELETE_MESSAGE = 0;
+ private static final int MENU_PICTURE_SMALL = 2;
+ private static final int MENU_PICTURE_MEDIUM = 3;
+ private static final int MENU_PICTURE_LARGE = 4;
+ private static final int MENU_PICTURE_REAL = 5;
+ private static final int MENU_COPY_TEXT = 6;
+ private static final int MENU_RESEND_MESSAGE = 7;
+ private static final int SIZE_SMALL = 500;
+ private static final int SIZE_MEDIUM = 1000;
+ private static final int SIZE_LARGE = 1500;
+ private static final int SIZE_MAX = 2048;
+
+ private LinphoneChatRoom chatRoom;
+ private String sipUri;
+ private EditText message;
+ private ImageView edit, selectAll, deselectAll, startCall, delete, sendImage, sendMessage, cancel;
+ private TextView contactName, remoteComposing;
+ private ImageView back, backToCall;
+ private EditText searchContactField;
+ private LinearLayout topBar, editList;
+ private SearchContactsListAdapter searchAdapter;
+ private ListView messagesList, resultContactsSearch;
+ private LayoutInflater inflater;
+ private Bitmap defaultBitmap;
+
+ private boolean isEditMode = false;
+ private LinphoneContact contact;
+ private Uri imageToUploadUri;
+ private String filePathToUpload;
+ private TextWatcher textWatcher;
+ private ViewTreeObserver.OnGlobalLayoutListener keyboardListener;
+ private ChatMessageAdapter adapter;
+
+ private LinphoneCoreListenerBase mListener;
+ private ByteArrayInputStream mUploadingImageStream;
+ private boolean newChatConversation = false;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ final View view = inflater.inflate(R.layout.chat, container, false);
+
+ LinphoneManager.addListener(this);
+ // Retain the fragment across configuration changes
+ setRetainInstance(true);
+
+ this.inflater = inflater;
+
+ if(getArguments() == null || getArguments().getString("SipUri") == null) {
+ newChatConversation = true;
+ } else {
+ //Retrieve parameter from intent
+ sipUri = getArguments().getString("SipUri");
+ }
+
+ //Initialize UI
+ defaultBitmap = BitmapFactory.decodeResource(getActivity().getResources(), R.drawable.chat_picture_over);
+
+ contactName = (TextView) view.findViewById(R.id.contact_name);
+ messagesList = (ListView) view.findViewById(R.id.chat_message_list);
+ searchContactField = (EditText) view.findViewById(R.id.search_contact_field);
+ resultContactsSearch = (ListView) view.findViewById(R.id.result_contacts);
+
+ editList = (LinearLayout) view.findViewById(R.id.edit_list);
+ topBar = (LinearLayout) view.findViewById(R.id.top_bar);
+
+ sendMessage = (ImageView) view.findViewById(R.id.send_message);
+ sendMessage.setOnClickListener(this);
+
+ remoteComposing = (TextView) view.findViewById(R.id.remote_composing);
+ remoteComposing.setVisibility(View.GONE);
+
+ cancel = (ImageView) view.findViewById(R.id.cancel);
+ cancel.setOnClickListener(this);
+
+ edit = (ImageView) view.findViewById(R.id.edit);
+ edit.setOnClickListener(this);
+
+ startCall = (ImageView) view.findViewById(R.id.start_call);
+ startCall.setOnClickListener(this);
+
+ backToCall = (ImageView) view.findViewById(R.id.back_to_call);
+ backToCall.setOnClickListener(this);
+
+ selectAll = (ImageView) view.findViewById(R.id.select_all);
+ selectAll.setOnClickListener(this);
+
+ deselectAll = (ImageView) view.findViewById(R.id.deselect_all);
+ deselectAll.setOnClickListener(this);
+
+ delete = (ImageView) view.findViewById(R.id.delete);
+ delete.setOnClickListener(this);
+
+ if (newChatConversation) {
+ initNewChatConversation();
+ }
+
+ message = (EditText) view.findViewById(R.id.message);
+
+ sendImage = (ImageView) view.findViewById(R.id.send_picture);
+ if (!getResources().getBoolean(R.bool.disable_chat_send_file)) {
+ sendImage.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ pickImage();
+ LinphoneActivity.instance().checkAndRequestPermissionsToSendImage();
+ }
+ });
+ //registerForContextMenu(sendImage);
+ } else {
+ sendImage.setEnabled(false);
+ }
+
+ back = (ImageView) view.findViewById(R.id.back);
+ if(getResources().getBoolean(R.bool.isTablet)){
+ back.setVisibility(View.INVISIBLE);
+ } else {
+ back.setOnClickListener(this);
+ }
+
+ mListener = new LinphoneCoreListenerBase(){
+ @Override
+ public void messageReceived(LinphoneCore lc, LinphoneChatRoom cr, LinphoneChatMessage message) {
+ LinphoneAddress from = cr.getPeerAddress();
+ if (from.asStringUriOnly().equals(sipUri)) {
+ LinphoneService.instance().removeMessageNotification();
+ cr.markAsRead();
+ LinphoneActivity.instance().updateMissedChatCount();
+ adapter.addMessage(cr.getHistory(1)[0]);
+
+ String externalBodyUrl = message.getExternalBodyUrl();
+ LinphoneContent fileTransferContent = message.getFileTransferInformation();
+ if (externalBodyUrl != null || fileTransferContent != null) {
+ LinphoneActivity.instance().checkAndRequestExternalStoragePermission();
+ }
+ }
+ }
+
+ @Override
+ public void isComposingReceived(LinphoneCore lc, LinphoneChatRoom room) {
+ if (chatRoom != null && room != null && chatRoom.getPeerAddress().asStringUriOnly().equals(room.getPeerAddress().asStringUriOnly())) {
+ remoteComposing.setVisibility(chatRoom.isRemoteComposing() ? View.VISIBLE : View.GONE);
+ }
+ }
+ };
+
+ textWatcher = new TextWatcher() {
+ public void afterTextChanged(Editable arg0) {}
+
+ public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {}
+
+ public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
+ if (message.getText().toString().equals("")) {
+ sendMessage.setEnabled(false);
+ } else {
+ if (chatRoom != null)
+ chatRoom.compose();
+ sendMessage.setEnabled(true);
+ }
+ }
+ };
+
+ return view;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (message != null) {
+ outState.putString("messageDraft", message.getText().toString());
+ }
+ if (contact != null) {
+ outState.putSerializable("contactDraft",contact);
+ outState.putString("sipUriDraft",sipUri);
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ private void addVirtualKeyboardVisiblityListener() {
+ keyboardListener = new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ Rect visibleArea = new Rect();
+ getActivity().getWindow().getDecorView().getWindowVisibleDisplayFrame(visibleArea);
+
+ int heightDiff = getActivity().getWindow().getDecorView().getRootView().getHeight() - (visibleArea.bottom - visibleArea.top);
+ if (heightDiff > 200) {
+ showKeyboardVisibleMode();
+ } else {
+ hideKeyboardVisibleMode();
+ }
+ }
+ };
+ getActivity().getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(keyboardListener);
+ }
+
+ private void removeVirtualKeyboardVisiblityListener() {
+ Compatibility.removeGlobalLayoutListener(getActivity().getWindow().getDecorView().getViewTreeObserver(), keyboardListener);
+ }
+
+ public void showKeyboardVisibleMode() {
+ boolean isOrientationLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ if (isOrientationLandscape && topBar != null) {
+ //topBar.setVisibility(View.GONE);
+ }
+ LinphoneActivity.instance().hideTabBar(true);
+ //contactPicture.setVisibility(View.GONE);
+ }
+
+ public void hideKeyboardVisibleMode() {
+ boolean isOrientationLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ //contactPicture.setVisibility(View.VISIBLE);
+ if (isOrientationLandscape && topBar != null) {
+ //topBar.setVisibility(View.VISIBLE);
+ }
+ LinphoneActivity.instance().hideTabBar(false);
+ }
+
+ public int getNbItemsChecked(){
+ int size = messagesList.getAdapter().getCount();
+ int nb = 0;
+ for(int i=0; i 0 && isNetworkReachable) {
+ LinphoneChatMessage message = chatRoom.createLinphoneChatMessage(messageToSend);
+ chatRoom.sendChatMessage(message);
+ lAddress = chatRoom.getPeerAddress();
+
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().onMessageSent(sipUri, messageToSend);
+ }
+
+ message.setListener(LinphoneManager.getInstance());
+ if (newChatConversation) {
+ exitNewConversationMode(lAddress.asStringUriOnly());
+ } else {
+ adapter.addMessage(message);
+ }
+
+ Log.i("Sent message current status: " + message.getStatus());
+ } else if (!isNetworkReachable && LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().displayCustomToast(getString(R.string.error_network_unreachable), Toast.LENGTH_LONG);
+ }
+ }
+
+ private void sendImageMessage(String path, int imageSize) {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ boolean isNetworkReachable = lc == null ? false : lc.isNetworkReachable();
+
+ if(newChatConversation && chatRoom == null) {
+ String address = searchContactField.getText().toString();
+ if (address != null && !address.equals("")) {
+ initChatRoom(address);
+ }
+ }
+
+ if (chatRoom != null && path != null && path.length() > 0 && isNetworkReachable) {
+ try {
+ Bitmap bm = BitmapFactory.decodeFile(path);
+ if (bm != null) {
+ FileUploadPrepareTask task = new FileUploadPrepareTask(getActivity(), path, imageSize);
+ task.execute(bm);
+ } else {
+ Log.e("Error, bitmap factory can't read " + path);
+ }
+ } catch (RuntimeException e) {
+ Log.e("Error, not enough memory to create the bitmap");
+ }
+ } else if (!isNetworkReachable && LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().displayCustomToast(getString(R.string.error_network_unreachable), Toast.LENGTH_LONG);
+ }
+ }
+
+ private LinphoneChatMessage getMessageForId(int id) {
+ if (adapter == null) return null;
+ for (int i = 0; i < adapter.getCount(); i++) {
+ LinphoneChatMessage message = adapter.getItem(i);
+ if (message.getStorageId() == id) {
+ return message;
+ }
+ }
+ return null;
+ }
+
+ private void invalidate() {
+ adapter.refreshHistory();
+ chatRoom.markAsRead();
+ }
+
+ private void resendMessage(int id) {
+ LinphoneChatMessage message = getMessageForId(id);
+ if (message == null)
+ return;
+
+ chatRoom.deleteMessage(getMessageForId(id));
+ invalidate();
+
+ if (message.getText() != null && message.getText().length() > 0) {
+ sendTextMessage(message.getText());
+ } else {
+ sendImageMessage(message.getAppData(), 0);
+ }
+ }
+
+ private void copyTextMessageToClipboard(int id) {
+ LinphoneChatMessage message = null;
+ for (int i = 0; i < adapter.getCount(); i++) {
+ LinphoneChatMessage msg = adapter.getItem(i);
+ if (msg.getStorageId() == id) {
+ message = msg;
+ break;
+ }
+ }
+
+ String txt = null;
+ if (message != null) {
+ txt = message.getText();
+ }
+ if (txt != null) {
+ ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("Message", txt);
+ clipboard.setPrimaryClip(clip);
+ LinphoneActivity.instance().displayCustomToast(getString(R.string.text_copied_to_clipboard), Toast.LENGTH_SHORT);
+ }
+ }
+
+ //File transfer
+ private void pickImage() {
+ List cameraIntents = new ArrayList();
+ Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ File file = new File(Environment.getExternalStorageDirectory(), getString(R.string.temp_photo_name_with_date).replace("%s", String.valueOf(System.currentTimeMillis())));
+ imageToUploadUri = Uri.fromFile(file);
+ captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageToUploadUri);
+ cameraIntents.add(captureIntent);
+
+ Intent galleryIntent = new Intent();
+ galleryIntent.setType("image/*");
+ galleryIntent.setAction(Intent.ACTION_PICK);
+
+ Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.image_picker_title));
+ chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(new Parcelable[]{}));
+
+ startActivityForResult(chooserIntent, ADD_PHOTO);
+ }
+
+ public String getRealPathFromURI(Uri contentUri) {
+ String[] proj = {MediaStore.Images.Media.DATA};
+ CursorLoader loader = new CursorLoader(getActivity(), contentUri, proj, null, null, null);
+ Cursor cursor = loader.loadInBackground();
+ if (cursor != null && cursor.moveToFirst()) {
+ int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
+ String result = cursor.getString(column_index);
+ cursor.close();
+ return result;
+ }
+ return null;
+ }
+
+ class FileUploadPrepareTask extends AsyncTask {
+ private String path;
+ private ProgressDialog progressDialog;
+
+ public FileUploadPrepareTask(Context context, String fileToUploadPath, int size) {
+ path = fileToUploadPath;
+
+ progressDialog = new ProgressDialog(context);
+ progressDialog.setIndeterminate(true);
+ progressDialog.setMessage(getString(R.string.processing_image));
+ progressDialog.show();
+ }
+
+ @Override
+ protected byte[] doInBackground(Bitmap... params) {
+ Bitmap bm = params[0];
+
+ if (bm.getWidth() >= bm.getHeight() && bm.getWidth() > SIZE_MAX) {
+ bm = Bitmap.createScaledBitmap(bm, SIZE_MAX, (SIZE_MAX * bm.getHeight()) / bm.getWidth(), false);
+ } else if (bm.getHeight() >= bm.getWidth() && bm.getHeight() > SIZE_MAX) {
+ bm = Bitmap.createScaledBitmap(bm, (SIZE_MAX * bm.getWidth()) / bm.getHeight(), SIZE_MAX, false);
+ }
+
+ // Rotate the bitmap if possible/needed, using EXIF data
+ try {
+ if (path != null) {
+ ExifInterface exif = new ExifInterface(path);
+ int pictureOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0);
+ Matrix matrix = new Matrix();
+ if (pictureOrientation == 6) {
+ matrix.postRotate(90);
+ } else if (pictureOrientation == 3) {
+ matrix.postRotate(180);
+ } else if (pictureOrientation == 8) {
+ matrix.postRotate(270);
+ }
+ bm = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
+ }
+ } catch (Exception e) {
+ Log.e(e);
+ }
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ String extension = LinphoneUtils.getExtensionFromFileName(path);
+ if (extension != null && extension.toLowerCase(Locale.getDefault()).equals("png")) {
+ bm.compress(Bitmap.CompressFormat.PNG, 100, stream);
+ } else {
+ bm.compress(Bitmap.CompressFormat.JPEG, 100, stream);
+ }
+ byte[] byteArray = stream.toByteArray();
+ return byteArray;
+ }
+
+ @Override
+ protected void onPostExecute(byte[] result) {
+ if (progressDialog != null && progressDialog.isShowing()) {
+ progressDialog.dismiss();
+ }
+ mUploadingImageStream = new ByteArrayInputStream(result);
+
+ String fileName = path.substring(path.lastIndexOf("/") + 1);
+ String extension = LinphoneUtils.getExtensionFromFileName(fileName);
+ LinphoneContent content = LinphoneCoreFactory.instance().createLinphoneContent("image", extension, result, null);
+ content.setName(fileName);
+
+ LinphoneChatMessage message = chatRoom.createFileTransferMessage(content);
+ message.setListener(LinphoneManager.getInstance());
+ message.setAppData(path);
+
+ LinphoneManager.getInstance().setUploadPendingFileMessage(message);
+ LinphoneManager.getInstance().setUploadingImageStream(mUploadingImageStream);
+
+ chatRoom.sendChatMessage(message);
+ adapter.addMessage(message);
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == ADD_PHOTO && resultCode == Activity.RESULT_OK) {
+ String fileToUploadPath = null;
+
+ if (data != null && data.getData() != null) {
+ fileToUploadPath = getRealPathFromURI(data.getData());
+ } else if (imageToUploadUri != null) {
+ fileToUploadPath = imageToUploadUri.getPath();
+ }
+
+ if (fileToUploadPath != null) {
+ //showPopupMenuAskingImageSize(fileToUploadPath);
+ sendImageMessage(fileToUploadPath,0);
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ //New conversation
+ private void exitNewConversationMode(String address) {
+ sipUri = address;
+ searchContactField.setVisibility(View.GONE);
+ resultContactsSearch.setVisibility(View.GONE);
+ messagesList.setVisibility(View.VISIBLE);
+ contactName.setVisibility(View.VISIBLE);
+ edit.setVisibility(View.VISIBLE);
+ startCall.setVisibility(View.VISIBLE);
+
+ if(getResources().getBoolean(R.bool.isTablet)){
+ back.setVisibility(View.INVISIBLE);
+ } else {
+ back.setOnClickListener(this);
+ }
+
+ newChatConversation = false;
+ initChatRoom(sipUri);
+ }
+
+ private void initNewChatConversation(){
+ messagesList.setVisibility(View.GONE);
+ edit.setVisibility(View.INVISIBLE);
+ startCall.setVisibility(View.INVISIBLE);
+ contactName.setVisibility(View.INVISIBLE);
+ resultContactsSearch.setVisibility(View.VISIBLE);
+ searchAdapter = new SearchContactsListAdapter(null);
+ resultContactsSearch.setAdapter(searchAdapter);
+ searchContactField.setVisibility(View.VISIBLE);
+ searchContactField.requestFocus();
+ searchContactField.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {}
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ searchContacts(searchContactField.getText().toString());
+ }
+ });
+ }
+
+ private class ContactAddress {
+ LinphoneContact contact;
+ String address;
+
+ private ContactAddress(LinphoneContact c, String a){
+ this.contact = c;
+ this.address = a;
+ }
+ }
+
+ private void searchContacts(String search) {
+ if (search == null || search.length() == 0) {
+ resultContactsSearch.setAdapter(new SearchContactsListAdapter(null));
+ return;
+ }
+
+ List result = new ArrayList();
+ if(search != null) {
+ for (ContactAddress c : searchAdapter.contacts) {
+ String address = c.address;
+ if (address.startsWith("sip:")) address = address.substring(4);
+ if (c.contact.getFullName() != null && c.contact.getFullName().toLowerCase(Locale.getDefault()).startsWith(search.toLowerCase(Locale.getDefault()))
+ || address.toLowerCase(Locale.getDefault()).startsWith(search.toLowerCase(Locale.getDefault()))) {
+ result.add(c);
+ }
+ }
+ }
+
+ resultContactsSearch.setAdapter(new SearchContactsListAdapter(result));
+ searchAdapter.notifyDataSetChanged();
+ }
+
+ class ChatMessageAdapter extends BaseAdapter {
+ private class ViewHolder implements LinphoneChatMessage.LinphoneChatMessageListener {
+ public int id;
+ public RelativeLayout bubbleLayout;
+ public CheckBox delete;
+ public LinearLayout background;
+ public ImageView contactPicture;
+ public TextView contactName;
+ public TextView messageText;
+ public ImageView messageImage;
+ public RelativeLayout fileTransferLayout;
+ public ProgressBar fileTransferProgressBar;
+ public Button fileTransferAction;
+ public ImageView messageStatus;
+ public ProgressBar messageSendingInProgress;
+ public ImageView contactPictureMask;
+
+ public ViewHolder(View view) {
+ id = view.getId();
+ bubbleLayout = (RelativeLayout) view.findViewById(R.id.bubble);
+ delete = (CheckBox) view.findViewById(R.id.delete_message);
+ background = (LinearLayout) view.findViewById(R.id.background);
+ contactPicture = (ImageView) view.findViewById(R.id.contact_picture);
+ contactName = (TextView) view.findViewById(R.id.contact_header);
+ messageText = (TextView) view.findViewById(R.id.message);
+ messageImage = (ImageView) view.findViewById(R.id.image);
+ fileTransferLayout = (RelativeLayout) view.findViewById(R.id.file_transfer_layout);
+ fileTransferProgressBar = (ProgressBar) view.findViewById(R.id.progress_bar);
+ fileTransferAction = (Button) view.findViewById(R.id.file_transfer_action);
+ messageStatus = (ImageView) view.findViewById(R.id.status);
+ messageSendingInProgress = (ProgressBar) view.findViewById(R.id.inprogress);
+ contactPictureMask = (ImageView) view.findViewById(R.id.mask);
+ }
+
+ @Override
+ public void onLinphoneChatMessageStateChanged(LinphoneChatMessage msg, State state) {
+
+ }
+
+ @Override
+ public void onLinphoneChatMessageFileTransferReceived(LinphoneChatMessage msg, LinphoneContent content, LinphoneBuffer buffer) {
+
+ }
+
+ @Override
+ public void onLinphoneChatMessageFileTransferSent(LinphoneChatMessage msg, LinphoneContent content, int offset, int size, LinphoneBuffer bufferToFill) {
+
+ }
+
+ @Override
+ public void onLinphoneChatMessageFileTransferProgressChanged(LinphoneChatMessage msg, LinphoneContent content, int offset, int total) {
+ if (msg.getStorageId() == id) fileTransferProgressBar.setProgress(offset * 100 / total);
+ }
+ }
+
+ ArrayList history;
+ Context context;
+
+ public ChatMessageAdapter(Context c) {
+ context = c;
+ history = new ArrayList();
+ refreshHistory();
+ }
+
+ public void destroy() {
+ if (history != null) {
+ history.clear();
+ }
+ }
+
+ public void refreshHistory() {
+ history.clear();
+ LinphoneChatMessage[] messages = chatRoom.getHistory();
+ history.addAll(Arrays.asList(messages));
+ notifyDataSetChanged();
+ }
+
+ public void addMessage(LinphoneChatMessage message) {
+ history.add(message);
+ notifyDataSetChanged();
+ messagesList.setSelection(getCount() - 1);
+ }
+
+ @Override
+ public int getCount() {
+ return history.size();
+ }
+
+ @Override
+ public LinphoneChatMessage getItem(int position) {
+ return history.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return history.get(position).getStorageId();
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ final LinphoneChatMessage message = history.get(position);
+ View view = null;
+ final ViewHolder holder;
+ boolean sameMessage = false;
+
+ if (convertView != null) {
+ view = convertView;
+ holder = (ViewHolder) view.getTag();
+ LinphoneManager.removeListener(holder);
+ } else {
+ view = LayoutInflater.from(context).inflate(R.layout.chat_bubble, null);
+ holder = new ViewHolder(view);
+ view.setTag(holder);
+ }
+
+ if (holder.id == message.getStorageId()) {
+ // Horrible workaround to not reload image on edit chat list
+ if (holder.messageImage.getTag() != null
+ && (holder.messageImage.getTag().equals(message.getAppData())
+ || ((String) holder.messageImage.getTag()).substring(7).equals(message.getAppData()))
+ ){
+ sameMessage = true;
+ }
+ } else {
+ holder.id = message.getStorageId();
+ }
+ view.setId(holder.id);
+ registerForContextMenu(view);
+
+ LinphoneChatMessage.State status = message.getStatus();
+ String externalBodyUrl = message.getExternalBodyUrl();
+ LinphoneContent fileTransferContent = message.getFileTransferInformation();
+
+ holder.delete.setVisibility(View.GONE);
+ holder.messageText.setVisibility(View.GONE);
+ holder.messageImage.setVisibility(View.GONE);
+ holder.fileTransferLayout.setVisibility(View.GONE);
+ holder.fileTransferProgressBar.setProgress(0);
+ holder.fileTransferAction.setEnabled(true);
+ holder.messageStatus.setVisibility(View.INVISIBLE);
+ holder.messageSendingInProgress.setVisibility(View.GONE);
+
+ String displayName = message.getFrom().getDisplayName();
+ if (displayName == null) {
+ displayName = message.getFrom().getUserName();
+ }
+ if (!message.isOutgoing()) {
+ if (contact != null) {
+ if (contact != null && contact.getFullName() != null) {
+ displayName = contact.getFullName();
+ }
+ if (contact.hasPhoto()) {
+ Bitmap photo = contact.getPhoto();
+ if (photo != null) {
+ holder.contactPicture.setImageBitmap(photo);
+ } else {
+ LinphoneUtils.setImagePictureFromUri(getActivity(), holder.contactPicture, contact.getPhotoUri(), contact.getThumbnailUri());
+ }
+ } else {
+ holder.contactPicture.setImageResource(R.drawable.avatar);
+ }
+ } else {
+ holder.contactPicture.setImageResource(R.drawable.avatar);
+ }
+ } else {
+ holder.contactPicture.setImageResource(R.drawable.avatar);
+ }
+ holder.contactName.setText(timestampToHumanDate(context, message.getTime()) + " - " + displayName);
+
+ if (status == LinphoneChatMessage.State.NotDelivered) {
+ holder.messageStatus.setVisibility(View.VISIBLE);
+ holder.messageStatus.setImageResource(R.drawable.chat_message_not_delivered);
+ } else if (status == LinphoneChatMessage.State.InProgress) {
+ holder.messageSendingInProgress.setVisibility(View.VISIBLE);
+ }
+
+ if (externalBodyUrl != null || fileTransferContent != null) {
+ String appData = message.getAppData();
+
+ if (message.isOutgoing() && appData != null) {
+ holder.messageImage.setVisibility(View.VISIBLE);
+ if (!sameMessage) {
+ loadBitmap(message.getAppData(), holder.messageImage);
+ holder.messageImage.setTag(message.getAppData());
+ }
+
+ if (LinphoneManager.getInstance().getMessageUploadPending() != null && LinphoneManager.getInstance().getMessageUploadPending().getStorageId() == message.getStorageId()) {
+ holder.messageSendingInProgress.setVisibility(View.GONE);
+ holder.fileTransferLayout.setVisibility(View.VISIBLE);
+ LinphoneManager.addListener(holder);
+ }
+ } else {
+ if (appData != null && !LinphoneManager.getInstance().isMessagePending(message) && appData.contains(context.getString(R.string.temp_photo_name_with_date).split("%s")[0])) {
+ appData = null;
+ }
+
+ if (appData == null) {
+ LinphoneManager.addListener(holder);
+ holder.fileTransferLayout.setVisibility(View.VISIBLE);
+ } else {
+ if (LinphoneManager.getInstance().isMessagePending(message)) {
+ LinphoneManager.addListener(holder);
+ holder.fileTransferAction.setEnabled(false);
+ holder.fileTransferLayout.setVisibility(View.VISIBLE);
+ } else {
+ LinphoneManager.removeListener(holder);
+ holder.fileTransferLayout.setVisibility(View.GONE);
+ holder.messageImage.setVisibility(View.VISIBLE);
+ if (!sameMessage) {
+ loadBitmap(appData, holder.messageImage);
+ holder.messageImage.setTag(message.getAppData());
+ }
+ }
+ }
+ }
+ } else {
+ Spanned text = null;
+ String msg = message.getText();
+ if (msg != null) {
+ text = getTextWithHttpLinks(msg);
+ holder.messageText.setText(text);
+ holder.messageText.setMovementMethod(LinkMovementMethod.getInstance());
+ holder.messageText.setVisibility(View.VISIBLE);
+ }
+ }
+
+ if (message.isOutgoing()) {
+ holder.fileTransferAction.setText(getString(R.string.cancel));
+ holder.fileTransferAction.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (LinphoneManager.getInstance().getMessageUploadPending() != null) {
+ holder.fileTransferProgressBar.setVisibility(View.GONE);
+ holder.fileTransferProgressBar.setProgress(0);
+ message.cancelFileTransfer();
+ LinphoneManager.getInstance().setUploadPendingFileMessage(null);
+ }
+ }
+ });
+ } else {
+ holder.fileTransferAction.setText(getString(R.string.accept));
+ holder.fileTransferAction.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (context.getPackageManager().checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, context.getPackageName()) == PackageManager.PERMISSION_GRANTED) {
+ v.setEnabled(false);
+ String extension = message.getFileTransferInformation().getSubtype();
+ String filename = context.getString(R.string.temp_photo_name_with_date).replace("%s", String.valueOf(System.currentTimeMillis())) + "." + extension;
+ File file = new File(Environment.getExternalStorageDirectory(), filename);
+ message.setAppData(filename);
+ LinphoneManager.getInstance().addDownloadMessagePending(message);
+ message.setListener(LinphoneManager.getInstance());
+ message.setFileTransferFilepath(file.getPath());
+ message.downloadFile();
+ } else {
+ Log.w("WRITE_EXTERNAL_STORAGE permission not granted, won't be able to store the downloaded file");
+ LinphoneActivity.instance().checkAndRequestExternalStoragePermission();
+ }
+ }
+ });
+ }
+
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
+ if (message.isOutgoing()) {
+ layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+ layoutParams.setMargins(100, 10, 10, 10);
+ holder.background.setBackgroundResource(R.drawable.resizable_chat_bubble_outgoing);
+ Compatibility.setTextAppearance(holder.contactName, getActivity(), R.style.font3);
+ Compatibility.setTextAppearance(holder.fileTransferAction, getActivity(), R.style.font15);
+ holder.fileTransferAction.setBackgroundResource(R.drawable.resizable_confirm_delete_button);
+ holder.contactPictureMask.setImageResource(R.drawable.avatar_chat_mask_outgoing);
+ } else {
+ if (isEditMode) {
+ layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+ layoutParams.setMargins(100, 10, 10, 10);
+ } else {
+ layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
+ layoutParams.setMargins(10, 10, 100, 10);
+ }
+ holder.background.setBackgroundResource(R.drawable.resizable_chat_bubble_incoming);
+ Compatibility.setTextAppearance(holder.contactName, getActivity(), R.style.font9);
+ Compatibility.setTextAppearance(holder.fileTransferAction, getActivity(), R.style.font8);
+ holder.fileTransferAction.setBackgroundResource(R.drawable.resizable_assistant_button);
+ holder.contactPictureMask.setImageResource(R.drawable.avatar_chat_mask);
+ }
+ holder.bubbleLayout.setLayoutParams(layoutParams);
+
+ if (isEditMode) {
+ holder.delete.setVisibility(View.VISIBLE);
+ holder.delete.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ messagesList.setItemChecked(position, b);
+ if (getNbItemsChecked() == getCount()) {
+ deselectAll.setVisibility(View.VISIBLE);
+ selectAll.setVisibility(View.GONE);
+ enabledDeleteButton(true);
+ } else {
+ if (getNbItemsChecked() == 0) {
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(false);
+ } else {
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(true);
+ }
+ }
+ }
+ });
+
+ if (messagesList.isItemChecked(position)) {
+ holder.delete.setChecked(true);
+ } else {
+ holder.delete.setChecked(false);
+ }
+ }
+
+ return view;
+ }
+
+ private String timestampToHumanDate(Context context, long timestamp) {
+ try {
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(timestamp);
+
+ SimpleDateFormat dateFormat;
+ if (isToday(cal)) {
+ dateFormat = new SimpleDateFormat(context.getResources().getString(R.string.today_date_format));
+ } else {
+ dateFormat = new SimpleDateFormat(context.getResources().getString(R.string.messages_date_format));
+ }
+
+ return dateFormat.format(cal.getTime());
+ } catch (NumberFormatException nfe) {
+ return String.valueOf(timestamp);
+ }
+ }
+
+ private boolean isToday(Calendar cal) {
+ return isSameDay(cal, Calendar.getInstance());
+ }
+
+ private boolean isSameDay(Calendar cal1, Calendar cal2) {
+ if (cal1 == null || cal2 == null) {
+ return false;
+ }
+
+ return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
+ cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
+ cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR));
+ }
+
+ private Spanned getTextWithHttpLinks(String text) {
+ if (text.contains("<")) {
+ text = text.replace("<", "<");
+ }
+ if (text.contains(">")) {
+ text = text.replace(">", ">");
+ }
+ if (text.contains("http://")) {
+ int indexHttp = text.indexOf("http://");
+ int indexFinHttp = text.indexOf(" ", indexHttp) == -1 ? text.length() : text.indexOf(" ", indexHttp);
+ String link = text.substring(indexHttp, indexFinHttp);
+ String linkWithoutScheme = link.replace("http://", "");
+ text = text.replaceFirst(link, "" + linkWithoutScheme + "");
+ }
+ if (text.contains("https://")) {
+ int indexHttp = text.indexOf("https://");
+ int indexFinHttp = text.indexOf(" ", indexHttp) == -1 ? text.length() : text.indexOf(" ", indexHttp);
+ String link = text.substring(indexHttp, indexFinHttp);
+ String linkWithoutScheme = link.replace("https://", "");
+ text = text.replaceFirst(link, "" + linkWithoutScheme + "");
+ }
+
+ return Compatibility.fromHtml(text);
+ }
+
+ public void loadBitmap(String path, ImageView imageView) {
+ if (cancelPotentialWork(path, imageView)) {
+ BitmapWorkerTask task = new BitmapWorkerTask(imageView);
+ final AsyncBitmap asyncBitmap = new AsyncBitmap(context.getResources(), defaultBitmap, task);
+ imageView.setImageDrawable(asyncBitmap);
+ task.execute(path);
+ }
+ }
+
+ private class BitmapWorkerTask extends AsyncTask {
+ private final WeakReference imageViewReference;
+ public String path;
+
+ public BitmapWorkerTask(ImageView imageView) {
+ path = null;
+ // Use a WeakReference to ensure the ImageView can be garbage collected
+ imageViewReference = new WeakReference(imageView);
+ }
+
+ // Decode image in background.
+ @Override
+ protected Bitmap doInBackground(String... params) {
+ path = params[0];
+ Bitmap bm = null;
+
+ if (path.startsWith("content")) {
+ try {
+ bm = MediaStore.Images.Media.getBitmap(context.getContentResolver(), Uri.parse(path));
+ } catch (FileNotFoundException e) {
+ Log.e(e);
+ } catch (IOException e) {
+ Log.e(e);
+ }
+ } else {
+ bm = BitmapFactory.decodeFile(path);
+ path = "file://" + path;
+ }
+
+ if (bm != null) {
+ bm = ThumbnailUtils.extractThumbnail(bm, SIZE_MAX, SIZE_MAX);
+ }
+ return bm;
+ }
+
+ // Once complete, see if ImageView is still around and set bitmap.
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ if (isCancelled()) {
+ bitmap = null;
+ }
+
+ if (imageViewReference != null && bitmap != null) {
+ final ImageView imageView = imageViewReference.get();
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+ if (this == bitmapWorkerTask && imageView != null) {
+ imageView.setImageBitmap(bitmap);
+ imageView.setTag(path);
+ imageView.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.parse((String)v.getTag()), "image/*");
+ context.startActivity(intent);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ class AsyncBitmap extends BitmapDrawable {
+ private final WeakReference bitmapWorkerTaskReference;
+
+ public AsyncBitmap(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
+ super(res, bitmap);
+ bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask);
+ }
+
+ public BitmapWorkerTask getBitmapWorkerTask() {
+ return bitmapWorkerTaskReference.get();
+ }
+ }
+
+ private boolean cancelPotentialWork(String path, ImageView imageView) {
+ final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
+
+ if (bitmapWorkerTask != null) {
+ final String bitmapData = bitmapWorkerTask.path;
+ // If bitmapData is not yet set or it differs from the new data
+ if (bitmapData == null || bitmapData != path) {
+ // Cancel previous task
+ bitmapWorkerTask.cancel(true);
+ } else {
+ // The same work is already in progress
+ return false;
+ }
+ }
+ // No task associated with the ImageView, or an existing task was cancelled
+ return true;
+ }
+
+ private BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
+ if (imageView != null) {
+ final Drawable drawable = imageView.getDrawable();
+ if (drawable instanceof AsyncBitmap) {
+ final AsyncBitmap asyncDrawable = (AsyncBitmap) drawable;
+ return asyncDrawable.getBitmapWorkerTask();
+ }
+ }
+ return null;
+ }
+ }
+
+ class SearchContactsListAdapter extends BaseAdapter {
+ private class ViewHolder {
+ public TextView name;
+ public TextView address;
+
+ public ViewHolder(View view) {
+ name = (TextView) view.findViewById(R.id.contact_name);
+ address = (TextView) view.findViewById(R.id.contact_address);
+ }
+ }
+
+ private List contacts;
+ private LayoutInflater mInflater;
+
+ SearchContactsListAdapter(List contactsList) {
+ mInflater = inflater;
+ if (contactsList == null) {
+ contacts = getContactsList();
+ } else {
+ contacts = contactsList;
+ }
+ }
+
+ public List getContactsList() {
+ List list = new ArrayList();
+ if(ContactsManager.getInstance().hasContacts()) {
+ for (LinphoneContact con : ContactsManager.getInstance().getContacts()) {
+ for (LinphoneNumberOrAddress noa : con.getNumbersOrAddresses()) {
+ String value = noa.getValue();
+ // Fix for sip:username compatibility issue
+ if (value.startsWith("sip:") && !value.contains("@")) {
+ value = value.substring(4);
+ value = LinphoneUtils.getFullAddressFromUsername(value);
+ }
+ list.add(new ContactAddress(con, value));
+ }
+ }
+ }
+ return list;
+ }
+
+ public int getCount() {
+ return contacts.size();
+ }
+
+ public ContactAddress getItem(int position) {
+ if (contacts == null || position >= contacts.size()) {
+ contacts = getContactsList();
+ return contacts.get(position);
+ } else {
+ return contacts.get(position);
+ }
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = null;
+ ContactAddress contact;
+ ViewHolder holder = null;
+
+ do {
+ contact = getItem(position);
+ } while (contact == null);
+
+ if (convertView != null) {
+ view = convertView;
+ holder = (ViewHolder) view.getTag();
+ } else {
+ view = mInflater.inflate(R.layout.search_contact_cell, parent, false);
+ holder = new ViewHolder(view);
+ view.setTag(holder);
+ }
+
+ final String a = contact.address;
+ LinphoneContact c = contact.contact;
+
+ holder.name.setText(c.getFullName());
+ holder.address.setText(a);
+
+ view.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ exitNewConversationMode(a);
+ }
+ });
+
+ return view;
+ }
+ }
+
+ //LinphoneChatMessage Listener
+ @Override
+ public void onLinphoneChatMessageStateChanged(LinphoneChatMessage msg, State state) {
+ redrawMessageList();
+ }
+
+ @Override
+ public void onLinphoneChatMessageFileTransferReceived(LinphoneChatMessage msg, LinphoneContent content, LinphoneBuffer buffer) {}
+
+ @Override
+ public void onLinphoneChatMessageFileTransferSent(LinphoneChatMessage msg, LinphoneContent content, int offset, int size, LinphoneBuffer bufferToFill) {}
+
+ @Override
+ public void onLinphoneChatMessageFileTransferProgressChanged(LinphoneChatMessage msg, LinphoneContent content, int offset, int total) {}
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ChatListFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ChatListFragment.java
new file mode 100644
index 0000000..7011932
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ChatListFragment.java
@@ -0,0 +1,486 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+ChatListFragment.java
+Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+import java.util.List;
+
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneChatMessage;
+import org.linphone.core.LinphoneChatRoom;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCoreException;
+import org.linphone.core.LinphoneCoreFactory;
+import org.linphone.core.LinphoneCoreListenerBase;
+import org.linphone.mediastream.Log;
+
+import android.app.Dialog;
+import android.app.Fragment;
+import android.graphics.Bitmap;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class ChatListFragment extends Fragment implements OnClickListener, OnItemClickListener, ContactsUpdatedListener {
+ private LayoutInflater mInflater;
+ private List mConversations;
+ private ListView chatList;
+ private TextView noChatHistory;
+ private ImageView edit, selectAll, deselectAll, delete, newDiscussion, cancel, backInCall;
+ private LinearLayout editList, topbar;
+ private boolean isEditMode = false;
+ private LinphoneCoreListenerBase mListener;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mInflater = inflater;
+
+ View view = inflater.inflate(R.layout.chatlist, container, false);
+ chatList = (ListView) view.findViewById(R.id.chatList);
+ chatList.setOnItemClickListener(this);
+ registerForContextMenu(chatList);
+
+ noChatHistory = (TextView) view.findViewById(R.id.noChatHistory);
+
+ editList = (LinearLayout) view.findViewById(R.id.edit_list);
+ topbar = (LinearLayout) view.findViewById(R.id.top_bar);
+
+ cancel = (ImageView) view.findViewById(R.id.cancel);
+ cancel.setOnClickListener(this);
+
+ edit = (ImageView) view.findViewById(R.id.edit);
+ edit.setOnClickListener(this);
+
+ newDiscussion = (ImageView) view.findViewById(R.id.new_discussion);
+ newDiscussion.setOnClickListener(this);
+
+ selectAll = (ImageView) view.findViewById(R.id.select_all);
+ selectAll.setOnClickListener(this);
+
+ deselectAll = (ImageView) view.findViewById(R.id.deselect_all);
+ deselectAll.setOnClickListener(this);
+
+ backInCall = (ImageView) view.findViewById(R.id.back_in_call);
+ backInCall.setOnClickListener(this);
+
+ delete = (ImageView) view.findViewById(R.id.delete);
+ delete.setOnClickListener(this);
+
+ mListener = new LinphoneCoreListenerBase() {
+ @Override
+ public void messageReceived(LinphoneCore lc, LinphoneChatRoom cr, LinphoneChatMessage message) {
+ refresh();
+ }
+ };
+ return view;
+ }
+
+ private void selectAllList(boolean isSelectAll){
+ int size = chatList.getAdapter().getCount();
+ for(int i=0; i 0) {
+ LinphoneActivity.instance().displayChat(mConversations.get(0));
+ } else {
+ LinphoneActivity.instance().displayEmptyFragment();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ContactsManager.addContactsListener(this);
+
+ if (LinphoneManager.getLc().getCallsNb() > 0) {
+ backInCall.setVisibility(View.VISIBLE);
+ } else {
+ backInCall.setVisibility(View.INVISIBLE);
+ }
+
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().selectMenu(FragmentsAvailable.CHAT_LIST);
+ LinphoneActivity.instance().hideTabBar(false);
+ }
+
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.addListener(mListener);
+ }
+
+ refresh();
+ }
+
+ @Override
+ public void onPause() {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.removeListener(mListener);
+ }
+ ContactsManager.removeContactsListener(this);
+ super.onPause();
+ }
+
+ @Override
+ public void onContactsUpdated() {
+ hideAndDisplayMessageIfNoChat();
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ menu.add(0, v.getId(), 0, getString(R.string.delete));
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
+ if (info == null || info.targetView == null) {
+ return false;
+ }
+ String sipUri = chatList.getAdapter().getItem(info.position).toString();
+
+ LinphoneActivity.instance().removeFromChatList(sipUri);
+ mConversations = LinphoneActivity.instance().getChatList();
+ hideAndDisplayMessageIfNoChat();
+ return true;
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+
+ if (id == R.id.back_in_call) {
+ LinphoneActivity.instance().resetClassicMenuLayoutAndGoBackToCallIfStillRunning();
+ return;
+ }
+
+ if (id == R.id.select_all) {
+ deselectAll.setVisibility(View.VISIBLE);
+ selectAll.setVisibility(View.GONE);
+ enabledDeleteButton(true);
+ selectAllList(true);
+ return;
+ }
+ if (id == R.id.deselect_all) {
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(false);
+ selectAllList(false);
+ return;
+ }
+
+ if (id == R.id.cancel) {
+ quitEditMode();
+ return;
+ }
+
+ if (id == R.id.delete) {
+ final Dialog dialog = LinphoneActivity.instance().displayDialog(getString(R.string.delete_text));
+ Button delete = (Button) dialog.findViewById(R.id.delete_button);
+ Button cancel = (Button) dialog.findViewById(R.id.cancel);
+
+ delete.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ removeChatsConversation();
+ dialog.dismiss();
+ quitEditMode();
+ }
+ });
+
+ cancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dialog.dismiss();
+ quitEditMode();
+ }
+ });
+ dialog.show();
+ return;
+ }
+ else if (id == R.id.edit) {
+ topbar.setVisibility(View.GONE);
+ editList.setVisibility(View.VISIBLE);
+ isEditMode = true;
+ hideAndDisplayMessageIfNoChat();
+ enabledDeleteButton(false);
+ }
+ else if (id == R.id.new_discussion) {
+ LinphoneActivity.instance().displayChat(null);
+ /*String sipUri = fastNewChat.getText().toString();
+ if (sipUri.equals("")) {
+ LinphoneActivity.instance().displayContacts(true);
+ } else {
+ if (!LinphoneUtils.isSipAddress(sipUri)) {
+ if (LinphoneManager.getLc().getDefaultProxyConfig() == null) {
+ return;
+ }
+ sipUri = sipUri + "@" + LinphoneManager.getLc().getDefaultProxyConfig().getDomain();
+ }
+ if (!LinphoneUtils.isStrictSipAddress(sipUri)) {
+ sipUri = "sip:" + sipUri;
+ }
+
+ }*/
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView> adapter, View view, int position, long id) {
+ String sipUri = chatList.getAdapter().getItem(position).toString();
+
+ if (LinphoneActivity.isInstanciated() && !isEditMode) {
+ LinphoneActivity.instance().displayChat(sipUri);
+ }
+ }
+
+ class ChatListAdapter extends BaseAdapter {
+ private class ViewHolder {
+ public TextView lastMessageView;
+ public TextView date;
+ public TextView displayName;
+ public TextView unreadMessages;
+ public CheckBox select;
+ public ImageView contactPicture;
+
+ public ViewHolder(View view) {
+ lastMessageView = (TextView) view.findViewById(R.id.lastMessage);
+ date = (TextView) view.findViewById(R.id.date);
+ displayName = (TextView) view.findViewById(R.id.sipUri);
+ unreadMessages = (TextView) view.findViewById(R.id.unreadMessages);
+ select = (CheckBox) view.findViewById(R.id.delete_chatroom);
+ contactPicture = (ImageView) view.findViewById(R.id.contact_picture);
+ }
+ }
+
+ ChatListAdapter() {}
+
+ public int getCount() {
+ return mConversations.size();
+ }
+
+ public Object getItem(int position) {
+ return mConversations.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View view = null;
+ ViewHolder holder = null;
+ String sipUri = mConversations.get(position);
+
+ if (convertView != null) {
+ view = convertView;
+ holder = (ViewHolder) view.getTag();
+ } else {
+ view = mInflater.inflate(R.layout.chatlist_cell, parent, false);
+ holder = new ViewHolder(view);
+ view.setTag(holder);
+ }
+
+ LinphoneAddress address;
+ try {
+ address = LinphoneCoreFactory.instance().createLinphoneAddress(sipUri);
+ } catch (LinphoneCoreException e) {
+ Log.e("Chat view cannot parse address", e);
+ return view;
+ }
+
+ LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address);
+ String message = "";
+ Long time;
+
+ LinphoneChatRoom chatRoom = LinphoneManager.getLc().getChatRoom(address);
+ int unreadMessagesCount = chatRoom.getUnreadMessagesCount();
+ LinphoneChatMessage[] history = chatRoom.getHistory(1);
+ LinphoneChatMessage msg = history[0];
+
+ if(msg.getFileTransferInformation() != null || msg.getExternalBodyUrl() != null || msg.getAppData() != null ){
+ holder.lastMessageView.setBackgroundResource(R.drawable.chat_file_message);
+ time = msg.getTime();
+ holder.date.setText(LinphoneUtils.timestampToHumanDate(getActivity(),time,getString(R.string.messages_list_date_format)));
+ holder.lastMessageView.setText("");
+ } else if (msg.getText() != null && msg.getText().length() > 0 ){
+ message = msg.getText();
+ holder.lastMessageView.setBackgroundResource(0);
+ time = msg.getTime();
+ holder.date.setText(LinphoneUtils.timestampToHumanDate(getActivity(),time,getString(R.string.messages_list_date_format)));
+ holder.lastMessageView.setText(message);
+ }
+
+ holder.displayName.setSelected(true); // For animation
+ holder.displayName.setText(contact == null ? LinphoneUtils.getAddressDisplayName(address) : contact.getFullName());
+
+
+ if (contact != null) {
+ Bitmap photo = contact.getPhoto();
+ if (photo != null) {
+ holder.contactPicture.setImageBitmap(photo);
+ } else {
+ LinphoneUtils.setImagePictureFromUri(getActivity(), holder.contactPicture, contact.getPhotoUri(), contact.getThumbnailUri());
+ }
+ } else {
+ holder.contactPicture.setImageResource(R.drawable.avatar);
+ }
+
+ if (unreadMessagesCount > 0) {
+ holder.unreadMessages.setVisibility(View.VISIBLE);
+ holder.unreadMessages.setText(String.valueOf(unreadMessagesCount));
+ if (unreadMessagesCount > 99) {
+ holder.unreadMessages.setTextSize(12);
+ }
+ holder.displayName.setTypeface(null, Typeface.BOLD);
+ } else {
+ holder.unreadMessages.setVisibility(View.GONE);
+ holder.displayName.setTypeface(null, Typeface.NORMAL);
+ }
+
+ if (isEditMode) {
+ holder.unreadMessages.setVisibility(View.GONE);
+ holder.select.setVisibility(View.VISIBLE);
+ holder.select.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ chatList.setItemChecked(position, b);
+ if (getNbItemsChecked() == getCount()) {
+ deselectAll.setVisibility(View.VISIBLE);
+ selectAll.setVisibility(View.GONE);
+ enabledDeleteButton(true);
+ } else {
+ if (getNbItemsChecked() == 0) {
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(false);
+ } else {
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(true);
+ }
+ }
+ }
+ });
+ if(chatList.isItemChecked(position)) {
+ holder.select.setChecked(true);
+ } else {
+ holder.select.setChecked(false);
+ }
+ } else {
+ if (unreadMessagesCount > 0) {
+ holder.unreadMessages.setVisibility(View.VISIBLE);
+ }
+ }
+ return view;
+ }
+ }
+}
+
+
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactDetailsFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactDetailsFragment.java
new file mode 100644
index 0000000..c0d9de8
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactDetailsFragment.java
@@ -0,0 +1,224 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+ContactDetailsFragment.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+import org.linphone.core.LinphoneProxyConfig;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TableLayout;
+import android.widget.TextView;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class ContactDetailsFragment extends Fragment implements OnClickListener {
+ private LinphoneContact contact;
+ private ImageView editContact, deleteContact, back;
+ private TextView organization;
+ private LayoutInflater inflater;
+ private View view;
+ private boolean displayChatAddressOnly = false;
+
+ private OnClickListener dialListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().setAddresGoToDialerAndCall(v.getTag().toString(), contact.getFullName(), contact.getPhotoUri());
+ }
+ }
+ };
+
+ private OnClickListener chatListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().displayChat(v.getTag().toString());
+ }
+ }
+ };
+
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ contact = (LinphoneContact) getArguments().getSerializable("Contact");
+
+ this.inflater = inflater;
+ view = inflater.inflate(R.layout.contact, container, false);
+
+ if (getArguments() != null) {
+ displayChatAddressOnly = getArguments().getBoolean("ChatAddressOnly");
+ }
+
+ editContact = (ImageView) view.findViewById(R.id.editContact);
+ editContact.setOnClickListener(this);
+
+ deleteContact = (ImageView) view.findViewById(R.id.deleteContact);
+ deleteContact.setOnClickListener(this);
+
+ organization = (TextView) view.findViewById(R.id.contactOrganization);
+ boolean isOrgVisible = getResources().getBoolean(R.bool.display_contact_organization);
+ String org = contact.getOrganization();
+ if (org != null && !org.isEmpty() && isOrgVisible) {
+ organization.setText(org);
+ } else {
+ organization.setVisibility(View.GONE);
+ }
+
+ back = (ImageView) view.findViewById(R.id.back);
+ if(getResources().getBoolean(R.bool.isTablet)){
+ back.setVisibility(View.INVISIBLE);
+ } else {
+ back.setOnClickListener(this);
+ }
+
+ return view;
+ }
+
+ public void changeDisplayedContact(LinphoneContact newContact) {
+ contact = newContact;
+ displayContact(inflater, view);
+ }
+
+ @SuppressLint("InflateParams")
+ private void displayContact(LayoutInflater inflater, View view) {
+ ImageView contactPicture = (ImageView) view.findViewById(R.id.contact_picture);
+ if (contact.hasPhoto()) {
+ LinphoneUtils.setImagePictureFromUri(getActivity(), contactPicture, contact.getPhotoUri(), contact.getThumbnailUri());
+ } else {
+ contactPicture.setImageResource(R.drawable.avatar);
+ }
+
+ TextView contactName = (TextView) view.findViewById(R.id.contact_name);
+ contactName.setText(contact.getFullName());
+
+ TableLayout controls = (TableLayout) view.findViewById(R.id.controls);
+ controls.removeAllViews();
+ for (LinphoneNumberOrAddress noa : contact.getNumbersOrAddresses()) {
+ boolean skip = false;
+ View v = inflater.inflate(R.layout.contact_control_row, null);
+
+ String value = noa.getValue();
+ String displayednumberOrAddress = LinphoneUtils.getDisplayableUsernameFromAddress(value);
+
+ TextView label = (TextView) v.findViewById(R.id.address_label);
+ if (noa.isSIPAddress()) {
+ label.setText(R.string.sip_address);
+ skip |= getResources().getBoolean(R.bool.hide_contact_sip_addresses);
+ } else {
+ label.setText(R.string.phone_number);
+ skip |= getResources().getBoolean(R.bool.hide_contact_phone_numbers);
+ }
+
+ TextView tv = (TextView) v.findViewById(R.id.numeroOrAddress);
+ tv.setText(displayednumberOrAddress);
+ tv.setSelected(true);
+
+
+ LinphoneProxyConfig lpc = LinphoneManager.getLc().getDefaultProxyConfig();
+ if (lpc != null) {
+ String username = lpc.normalizePhoneNumber(displayednumberOrAddress);
+ value = LinphoneUtils.getFullAddressFromUsername(username);
+ }
+
+ String contactAddress = contact.getPresenceModelForUri(noa.getValue());
+ if (contactAddress != null) {
+ v.findViewById(R.id.friendLinphone).setVisibility(View.VISIBLE);
+ }
+
+ if (!displayChatAddressOnly) {
+ v.findViewById(R.id.contact_call).setOnClickListener(dialListener);
+ if (contactAddress != null) {
+ v.findViewById(R.id.contact_call).setTag(contactAddress);
+ } else {
+ v.findViewById(R.id.contact_call).setTag(value);
+ }
+ } else {
+ v.findViewById(R.id.contact_call).setVisibility(View.GONE);
+ }
+
+ v.findViewById(R.id.contact_chat).setOnClickListener(chatListener);
+ if (contactAddress != null) {
+ v.findViewById(R.id.contact_chat).setTag(contactAddress);
+ } else {
+ v.findViewById(R.id.contact_chat).setTag(value);
+ }
+
+ if (getResources().getBoolean(R.bool.disable_chat)) {
+ v.findViewById(R.id.contact_chat).setVisibility(View.GONE);
+ }
+
+ if (!skip) {
+ controls.addView(v);
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().selectMenu(FragmentsAvailable.CONTACT_DETAIL);
+ LinphoneActivity.instance().hideTabBar(false);
+ }
+ contact.refresh();
+ displayContact(inflater, view);
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+
+ if (id == R.id.editContact) {
+ LinphoneActivity.instance().editContact(contact);
+ }
+ if (id == R.id.deleteContact) {
+ final Dialog dialog = LinphoneActivity.instance().displayDialog(getString(R.string.delete_text));
+ Button delete = (Button) dialog.findViewById(R.id.delete_button);
+ Button cancel = (Button) dialog.findViewById(R.id.cancel);
+
+ delete.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ contact.delete();
+ LinphoneActivity.instance().displayContacts(false);
+ dialog.dismiss();
+ }
+ });
+
+ cancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dialog.dismiss();
+
+ }
+ });
+ dialog.show();
+ }
+ if (id == R.id.back) {
+ getFragmentManager().popBackStackImmediate();
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactEditorFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactEditorFragment.java
new file mode 100644
index 0000000..d86f434
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactEditorFragment.java
@@ -0,0 +1,596 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+ ContactEditorFragment.java
+ Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.linphone.mediastream.Log;
+import org.linphone.mediastream.Version;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.Fragment;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Parcelable;
+import android.provider.ContactsContract.DisplayPhoto;
+import android.provider.MediaStore;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+public class ContactEditorFragment extends Fragment {
+ private View view;
+ private ImageView cancel, deleteContact, ok;
+ private ImageView addNumber, addSipAddress, contactPicture;
+ private LinearLayout phoneNumbersSection, sipAddressesSection;
+ private EditText firstName, lastName, organization;
+ private LayoutInflater inflater;
+
+ private static final int ADD_PHOTO = 1337;
+ private static final int PHOTO_SIZE = 128;
+
+ private boolean isNewContact;
+ private LinphoneContact contact;
+ private List numbersAndAddresses;
+ private int firstSipAddressIndex = -1;
+ private LinearLayout sipAddresses, numbers;
+ private String newSipOrNumberToAdd;
+ private Uri pickedPhotoForContactUri;
+ private byte[] photoToAdd;
+
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ this.inflater = inflater;
+
+ contact = null;
+ isNewContact = true;
+
+ if (getArguments() != null) {
+ Serializable obj = getArguments().getSerializable("Contact");
+ if (obj != null) {
+ contact = (LinphoneContact) obj;
+ isNewContact = false;
+ if (getArguments().getString("NewSipAdress") != null) {
+ newSipOrNumberToAdd = getArguments().getString("NewSipAdress");
+ }
+ } else if (getArguments().getString("NewSipAdress") != null) {
+ newSipOrNumberToAdd = getArguments().getString("NewSipAdress");
+ }
+ }
+
+ view = inflater.inflate(R.layout.contact_edit, container, false);
+
+ phoneNumbersSection = (LinearLayout) view.findViewById(R.id.phone_numbers);
+ if (getResources().getBoolean(R.bool.hide_phone_numbers_in_editor) || !ContactsManager.getInstance().hasContactsAccess()) {
+ //Currently linphone friends don't support phone numbers, so hide them
+ phoneNumbersSection.setVisibility(View.GONE);
+ }
+
+ sipAddressesSection = (LinearLayout) view.findViewById(R.id.sip_addresses);
+ if (getResources().getBoolean(R.bool.hide_sip_addresses_in_editor)) {
+ sipAddressesSection.setVisibility(View.GONE);
+ }
+
+
+ deleteContact = (ImageView) view.findViewById(R.id.delete_contact);
+
+ cancel = (ImageView) view.findViewById(R.id.cancel);
+ cancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ getFragmentManager().popBackStackImmediate();
+ }
+ });
+
+ ok = (ImageView) view.findViewById(R.id.ok);
+ ok.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (isNewContact) {
+ boolean areAllFielsEmpty = true;
+ for (LinphoneNumberOrAddress nounoa : numbersAndAddresses) {
+ if (nounoa.getValue() != null && !nounoa.getValue().equals("")) {
+ areAllFielsEmpty = false;
+ break;
+ }
+ }
+ if (areAllFielsEmpty) {
+ getFragmentManager().popBackStackImmediate();
+ return;
+ }
+ contact = LinphoneContact.createContact();
+ }
+ contact.setFirstNameAndLastName(firstName.getText().toString(), lastName.getText().toString());
+ if (photoToAdd != null) {
+ contact.setPhoto(photoToAdd);
+ }
+ for (LinphoneNumberOrAddress noa : numbersAndAddresses) {
+ if (noa.isSIPAddress() && noa.getValue() != null) {
+ noa.setValue(LinphoneUtils.getFullAddressFromUsername(noa.getValue()));
+ }
+ contact.addOrUpdateNumberOrAddress(noa);
+ }
+ contact.setOrganization(organization.getText().toString());
+ contact.save();
+ getFragmentManager().popBackStackImmediate();
+ }
+ });
+
+ lastName = (EditText) view.findViewById(R.id.contactLastName);
+ // Hack to display keyboard when touching focused edittext on Nexus One
+ if (Version.sdkStrictlyBelow(Version.API11_HONEYCOMB_30)) {
+ lastName.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ InputMethodManager imm = (InputMethodManager) LinphoneActivity.instance().getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.toggleSoftInput(InputMethodManager.SHOW_FORCED,0);
+ }
+ });
+ }
+ lastName.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (lastName.getText().length() > 0 || firstName.getText().length() > 0) {
+ ok.setEnabled(true);
+ } else {
+ ok.setEnabled(false);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ firstName = (EditText) view.findViewById(R.id.contactFirstName);
+ firstName.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (firstName.getText().length() > 0 || lastName.getText().length() > 0) {
+ ok.setEnabled(true);
+ } else {
+ ok.setEnabled(false);
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+
+ organization = (EditText) view.findViewById(R.id.contactOrganization);
+ boolean isOrgVisible = getResources().getBoolean(R.bool.display_contact_organization);
+ if (!isOrgVisible) {
+ organization.setVisibility(View.GONE);
+ view.findViewById(R.id.contactOrganizationTitle).setVisibility(View.GONE);
+ } else {
+ if (!isNewContact) {
+ organization.setText(contact.getOrganization());
+ }
+ }
+
+ if (!isNewContact) {
+ String fn = contact.getFirstName();
+ String ln = contact.getLastName();
+ if (fn != null || ln != null) {
+ firstName.setText(fn);
+ lastName.setText(ln);
+ } else {
+ lastName.setText(contact.getFullName());
+ firstName.setText("");
+ }
+
+ deleteContact.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final Dialog dialog = LinphoneActivity.instance().displayDialog(getString(R.string.delete_text));
+ Button delete = (Button) dialog.findViewById(R.id.delete_button);
+ Button cancel = (Button) dialog.findViewById(R.id.cancel);
+
+ delete.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ contact.delete();
+ LinphoneActivity.instance().displayContacts(false);
+ dialog.dismiss();
+ }
+ });
+
+ cancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dialog.dismiss();
+
+ }
+ });
+ dialog.show();
+ }
+ });
+ } else {
+ deleteContact.setVisibility(View.INVISIBLE);
+ }
+
+ contactPicture = (ImageView) view.findViewById(R.id.contact_picture);
+ if (contact != null) {
+ LinphoneUtils.setImagePictureFromUri(getActivity(), contactPicture, contact.getPhotoUri(), contact.getThumbnailUri());
+ } else {
+ contactPicture.setImageResource(R.drawable.avatar);
+ }
+
+ contactPicture.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ pickImage();
+ LinphoneActivity.instance().checkAndRequestCameraPermission();
+ }
+ });
+
+ numbersAndAddresses = new ArrayList();
+ sipAddresses = initSipAddressFields(contact);
+ numbers = initNumbersFields(contact);
+
+ addSipAddress = (ImageView) view.findViewById(R.id.add_address_field);
+ if (getResources().getBoolean(R.bool.allow_only_one_sip_address)) {
+ addSipAddress.setVisibility(View.GONE);
+ }
+ addSipAddress.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ addEmptyRowToAllowNewNumberOrAddress(sipAddresses,true);
+ }
+ });
+
+ addNumber = (ImageView) view.findViewById(R.id.add_number_field);
+ if (getResources().getBoolean(R.bool.allow_only_one_phone_number)) {
+ addNumber.setVisibility(View.GONE);
+ }
+ addNumber.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ addEmptyRowToAllowNewNumberOrAddress(numbers,false);
+ }
+ });
+
+ lastName.requestFocus();
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if(LinphoneActivity.isInstanciated()){
+ LinphoneActivity.instance().hideTabBar(false);
+ }
+
+ // Force hide keyboard
+ getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
+ }
+
+ @Override
+ public void onPause() {
+ // Force hide keyboard
+ InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ View view = getActivity().getCurrentFocus();
+ if (imm != null && view != null) {
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ super.onPause();
+ }
+
+ private void pickImage() {
+ pickedPhotoForContactUri = null;
+ final List cameraIntents = new ArrayList();
+ final Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ File file = new File(Environment.getExternalStorageDirectory(), getString(R.string.temp_photo_name));
+ pickedPhotoForContactUri = Uri.fromFile(file);
+ captureIntent.putExtra("outputX", PHOTO_SIZE);
+ captureIntent.putExtra("outputY", PHOTO_SIZE);
+ captureIntent.putExtra("aspectX", 0);
+ captureIntent.putExtra("aspectY", 0);
+ captureIntent.putExtra("scale", true);
+ captureIntent.putExtra("return-data", false);
+ captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, pickedPhotoForContactUri);
+ cameraIntents.add(captureIntent);
+
+ final Intent galleryIntent = new Intent();
+ galleryIntent.setType("image/*");
+ galleryIntent.setAction(Intent.ACTION_GET_CONTENT);
+
+ final Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.image_picker_title));
+ chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(new Parcelable[]{}));
+
+ startActivityForResult(chooserIntent, ADD_PHOTO);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == ADD_PHOTO && resultCode == Activity.RESULT_OK) {
+ if (data != null && data.getExtras() != null && data.getExtras().get("data") != null) {
+ Bitmap bm = (Bitmap) data.getExtras().get("data");
+ editContactPicture(null, bm);
+ }
+ else if (data != null && data.getData() != null) {
+ Uri selectedImageUri = data.getData();
+ try {
+ Bitmap selectedImage = MediaStore.Images.Media.getBitmap(LinphoneManager.getInstance().getContext().getContentResolver(), selectedImageUri);
+ selectedImage = Bitmap.createScaledBitmap(selectedImage, PHOTO_SIZE, PHOTO_SIZE, false);
+ editContactPicture(null, selectedImage);
+ } catch (IOException e) { Log.e(e); }
+ }
+ else if (pickedPhotoForContactUri != null) {
+ String filePath = pickedPhotoForContactUri.getPath();
+ editContactPicture(filePath, null);
+ }
+ else {
+ File file = new File(Environment.getExternalStorageDirectory(), getString(R.string.temp_photo_name));
+ if (file.exists()) {
+ pickedPhotoForContactUri = Uri.fromFile(file);
+ String filePath = pickedPhotoForContactUri.getPath();
+ editContactPicture(filePath, null);
+ }
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ private void editContactPicture(String filePath, Bitmap image) {
+ if (image == null) {
+ image = BitmapFactory.decodeFile(filePath);
+ }
+
+ Bitmap scaledPhoto;
+ int size = getThumbnailSize();
+ if (size > 0) {
+ scaledPhoto = Bitmap.createScaledBitmap(image, size, size, false);
+ } else {
+ scaledPhoto = Bitmap.createBitmap(image);
+ }
+ image.recycle();
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ scaledPhoto.compress(Bitmap.CompressFormat.PNG , 0, stream);
+ contactPicture.setImageBitmap(scaledPhoto);
+ photoToAdd = stream.toByteArray();
+ }
+
+ private int getThumbnailSize() {
+ int value = -1;
+ Cursor c = LinphoneActivity.instance().getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, new String[] { DisplayPhoto.THUMBNAIL_MAX_DIM }, null, null, null);
+ try {
+ c.moveToFirst();
+ value = c.getInt(0);
+ } catch (Exception e) {
+ Log.e(e);
+ }
+ return value;
+ }
+
+ private LinearLayout initNumbersFields(final LinphoneContact contact) {
+ LinearLayout controls = (LinearLayout) view.findViewById(R.id.controls_numbers);
+ controls.removeAllViews();
+
+ if (contact != null) {
+ for (LinphoneNumberOrAddress numberOrAddress : contact.getNumbersOrAddresses()) {
+ if (!numberOrAddress.isSIPAddress()) {
+ View view = displayNumberOrAddress(controls, numberOrAddress.getValue(), false);
+ if (view != null)
+ controls.addView(view);
+ }
+ }
+ }
+
+ if (newSipOrNumberToAdd != null) {
+ boolean isSip = LinphoneUtils.isStrictSipAddress(newSipOrNumberToAdd) || !LinphoneUtils.isNumberAddress(newSipOrNumberToAdd);
+ if(!isSip) {
+ View view = displayNumberOrAddress(controls, newSipOrNumberToAdd, false);
+ if (view != null)
+ controls.addView(view);
+ }
+ }
+
+ if (controls.getChildCount() == 0) {
+ addEmptyRowToAllowNewNumberOrAddress(controls,false);
+ }
+
+ return controls;
+ }
+
+ private LinearLayout initSipAddressFields(final LinphoneContact contact) {
+ LinearLayout controls = (LinearLayout) view.findViewById(R.id.controls_sip_address);
+ controls.removeAllViews();
+
+ if (contact != null) {
+ for (LinphoneNumberOrAddress numberOrAddress : contact.getNumbersOrAddresses()) {
+ if (numberOrAddress.isSIPAddress()) {
+ View view = displayNumberOrAddress(controls, numberOrAddress.getValue(), true);
+ if (view != null)
+ controls.addView(view);
+ }
+ }
+ }
+
+ if (newSipOrNumberToAdd != null) {
+ boolean isSip = LinphoneUtils.isStrictSipAddress(newSipOrNumberToAdd) || !LinphoneUtils.isNumberAddress(newSipOrNumberToAdd);
+ if (isSip) {
+ View view = displayNumberOrAddress(controls, newSipOrNumberToAdd, true);
+ if (view != null)
+ controls.addView(view);
+ }
+ }
+
+ if (controls.getChildCount() == 0) {
+ addEmptyRowToAllowNewNumberOrAddress(controls,true);
+ }
+
+ return controls;
+ }
+
+ private View displayNumberOrAddress(final LinearLayout controls, String numberOrAddress, boolean isSIP) {
+ return displayNumberOrAddress(controls, numberOrAddress, isSIP, false);
+ }
+
+ @SuppressLint("InflateParams")
+ private View displayNumberOrAddress(final LinearLayout controls, String numberOrAddress, boolean isSIP, boolean forceAddNumber) {
+ String displayNumberOrAddress = numberOrAddress;
+ if (isSIP) {
+ if (firstSipAddressIndex == -1) {
+ firstSipAddressIndex = controls.getChildCount();
+ }
+ displayNumberOrAddress = LinphoneUtils.getDisplayableUsernameFromAddress(numberOrAddress);
+ }
+ if ((getResources().getBoolean(R.bool.hide_phone_numbers_in_editor) && !isSIP) || (getResources().getBoolean(R.bool.hide_sip_addresses_in_editor) && isSIP)) {
+ if (forceAddNumber)
+ isSIP = !isSIP; // If number can't be displayed because we hide a sort of number, change that category
+ else
+ return null;
+ }
+
+ LinphoneNumberOrAddress tempNounoa;
+ if (forceAddNumber) {
+ tempNounoa = new LinphoneNumberOrAddress(null, isSIP);
+ } else {
+ if(isNewContact || newSipOrNumberToAdd != null) {
+ tempNounoa = new LinphoneNumberOrAddress(numberOrAddress, isSIP);
+ } else {
+ tempNounoa = new LinphoneNumberOrAddress(null, isSIP, numberOrAddress);
+ }
+ }
+ final LinphoneNumberOrAddress nounoa = tempNounoa;
+ numbersAndAddresses.add(nounoa);
+
+ final View view = inflater.inflate(R.layout.contact_edit_row, null);
+
+ final EditText noa = (EditText) view.findViewById(R.id.numoraddr);
+ if (!isSIP) {
+ noa.setInputType(InputType.TYPE_CLASS_PHONE);
+ }
+ noa.setText(displayNumberOrAddress);
+ noa.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ nounoa.setValue(noa.getText().toString());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+ if (forceAddNumber) {
+ nounoa.setValue(noa.getText().toString());
+ }
+
+ ImageView delete = (ImageView) view.findViewById(R.id.delete_field);
+ if ((getResources().getBoolean(R.bool.allow_only_one_phone_number) && !isSIP) || (getResources().getBoolean(R.bool.allow_only_one_sip_address) && isSIP)) {
+ delete.setVisibility(View.GONE);
+ }
+ delete.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (contact != null) {
+ contact.removeNumberOrAddress(nounoa);
+ }
+ numbersAndAddresses.remove(nounoa);
+ view.setVisibility(View.GONE);
+
+ }
+ });
+ return view;
+ }
+
+ @SuppressLint("InflateParams")
+ private void addEmptyRowToAllowNewNumberOrAddress(final LinearLayout controls, final boolean isSip) {
+ final View view = inflater.inflate(R.layout.contact_edit_row, null);
+ final LinphoneNumberOrAddress nounoa = new LinphoneNumberOrAddress(null, isSip);
+
+ final EditText noa = (EditText) view.findViewById(R.id.numoraddr);
+ numbersAndAddresses.add(nounoa);
+ noa.setHint(isSip ? getString(R.string.sip_address) : getString(R.string.phone_number));
+ if (!isSip) {
+ noa.setInputType(InputType.TYPE_CLASS_PHONE);
+ }
+ noa.requestFocus();
+ noa.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ nounoa.setValue(noa.getText().toString());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ }
+ });
+
+ final ImageView delete = (ImageView) view.findViewById(R.id.delete_field);
+ if ((getResources().getBoolean(R.bool.allow_only_one_phone_number) && !isSip) || (getResources().getBoolean(R.bool.allow_only_one_sip_address) && isSip)) {
+ delete.setVisibility(View.GONE);
+ }
+ delete.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ numbersAndAddresses.remove(nounoa);
+ view.setVisibility(View.GONE);
+ }
+ });
+
+ controls.addView(view);
+ }
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactsListFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactsListFragment.java
new file mode 100644
index 0000000..fe01acc
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactsListFragment.java
@@ -0,0 +1,621 @@
+/*
+ContactsListFragment.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import android.app.Dialog;
+import android.app.Fragment;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class ContactsListFragment extends Fragment implements OnClickListener, OnItemClickListener, ContactsUpdatedListener {
+ private LayoutInflater mInflater;
+ private ListView contactsList;
+ private TextView noSipContact, noContact;
+ private ImageView allContacts, linphoneContacts, newContact, edit, selectAll, deselectAll, delete, cancel;
+ private boolean onlyDisplayLinphoneContacts, isEditMode, isSearchMode;
+ private View allContactsSelected, linphoneContactsSelected;
+ private LinearLayout editList, topbar;
+ private int lastKnownPosition;
+ private boolean editOnClick = false, editConsumed = false, onlyDisplayChatAddress = false;
+ private String sipAddressToAdd;
+ private ImageView clearSearchField;
+ private EditText searchField;
+ private ProgressBar contactsFetchInProgress;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mInflater = inflater;
+ View view = inflater.inflate(R.layout.contacts_list, container, false);
+
+ if (getArguments() != null) {
+ editOnClick = getArguments().getBoolean("EditOnClick");
+ sipAddressToAdd = getArguments().getString("SipAddress");
+
+ onlyDisplayChatAddress = getArguments().getBoolean("ChatAddressOnly");
+ }
+
+ noSipContact = (TextView) view.findViewById(R.id.noSipContact);
+ noContact = (TextView) view.findViewById(R.id.noContact);
+
+ contactsList = (ListView) view.findViewById(R.id.contactsList);
+ contactsList.setOnItemClickListener(this);
+
+ allContacts = (ImageView) view.findViewById(R.id.all_contacts);
+ allContacts.setOnClickListener(this);
+
+ linphoneContacts = (ImageView) view.findViewById(R.id.linphone_contacts);
+ linphoneContacts.setOnClickListener(this);
+
+ allContactsSelected = view.findViewById(R.id.all_contacts_select);
+ linphoneContactsSelected = view.findViewById(R.id.linphone_contacts_select);
+
+ newContact = (ImageView) view.findViewById(R.id.newContact);
+ newContact.setOnClickListener(this);
+ newContact.setEnabled(LinphoneManager.getLc().getCallsNb() == 0);
+
+ allContacts.setEnabled(onlyDisplayLinphoneContacts);
+ linphoneContacts.setEnabled(!allContacts.isEnabled());
+
+ selectAll = (ImageView) view.findViewById(R.id.select_all);
+ selectAll.setOnClickListener(this);
+
+ deselectAll = (ImageView) view.findViewById(R.id.deselect_all);
+ deselectAll.setOnClickListener(this);
+
+ delete = (ImageView) view.findViewById(R.id.delete);
+ delete.setOnClickListener(this);
+
+ editList = (LinearLayout) view.findViewById(R.id.edit_list);
+ topbar = (LinearLayout) view.findViewById(R.id.top_bar);
+
+ cancel = (ImageView) view.findViewById(R.id.cancel);
+ cancel.setOnClickListener(this);
+
+ edit = (ImageView) view.findViewById(R.id.edit);
+ edit.setOnClickListener(this);
+
+ clearSearchField = (ImageView) view.findViewById(R.id.clearSearchField);
+ clearSearchField.setOnClickListener(this);
+
+ searchField = (EditText) view.findViewById(R.id.searchField);
+ searchField.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ searchContacts(searchField.getText().toString());
+ }
+ });
+
+ contactsFetchInProgress = (ProgressBar) view.findViewById(R.id.contactsFetchInProgress);
+ contactsFetchInProgress.setVisibility(View.VISIBLE);
+
+ return view;
+ }
+
+ public int getNbItemsChecked(){
+ int size = contactsList.getAdapter().getCount();
+ int nb = 0;
+ for(int i=0; i 0) {
+ searchContacts();
+ } else {
+ changeContactsAdapter();
+ }
+
+ if (id == R.id.newContact) {
+ editConsumed = true;
+ LinphoneActivity.instance().addContact(null, sipAddressToAdd);
+ }
+ else if (id == R.id.clearSearchField) {
+ searchField.setText("");
+ }
+ }
+
+ private void selectAllList(boolean isSelectAll){
+ int size = contactsList.getAdapter().getCount();
+ for(int i=0; i ids = new ArrayList();
+ int size = contactsList.getAdapter().getCount();
+
+ for (int i = size - 1; i >= 0; i--) {
+ if (contactsList.isItemChecked(i)) {
+ LinphoneContact contact = (LinphoneContact) contactsList.getAdapter().getItem(i);
+ if (contact.isAndroidContact()) {
+ contact.deleteFriend();
+ ids.add(contact.getAndroidId());
+ } else {
+ contact.delete();
+ }
+ }
+ }
+
+ ContactsManager.getInstance().deleteMultipleContactsAtOnce(ids);
+ }
+
+ public void quitEditMode(){
+ isEditMode = false;
+ editList.setVisibility(View.GONE);
+ topbar.setVisibility(View.VISIBLE);
+ invalidate();
+ if(getResources().getBoolean(R.bool.isTablet)){
+ displayFirstContact();
+ }
+ }
+
+ public void displayFirstContact(){
+ if (contactsList != null && contactsList.getAdapter() != null && contactsList.getAdapter().getCount() > 0) {
+ LinphoneActivity.instance().displayContact((LinphoneContact) contactsList.getAdapter().getItem(0), false);
+ } else {
+ LinphoneActivity.instance().displayEmptyFragment();
+ }
+ }
+
+ private void searchContacts() {
+ searchContacts(searchField.getText().toString());
+ }
+
+ private void searchContacts(String search) {
+ if (search == null || search.length() == 0) {
+ changeContactsAdapter();
+ return;
+ }
+ changeContactsToggle();
+
+ isSearchMode = true;
+
+ if (onlyDisplayLinphoneContacts) {
+ contactsList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
+ contactsList.setAdapter(new ContactsListAdapter(ContactsManager.getInstance().getSIPContacts(search)));
+ } else {
+ contactsList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
+ contactsList.setAdapter(new ContactsListAdapter(ContactsManager.getInstance().getContacts(search)));
+ }
+ }
+
+ private void changeContactsAdapter() {
+ changeContactsToggle();
+
+ isSearchMode = false;
+ noSipContact.setVisibility(View.GONE);
+ noContact.setVisibility(View.GONE);
+ contactsList.setVisibility(View.VISIBLE);
+
+ ContactsListAdapter adapter;
+ if (onlyDisplayLinphoneContacts) {
+ contactsList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
+ adapter = new ContactsListAdapter(ContactsManager.getInstance().getSIPContacts());
+ contactsList.setAdapter(adapter);
+ edit.setEnabled(true);
+ } else {
+ contactsList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
+ adapter = new ContactsListAdapter(ContactsManager.getInstance().getContacts());
+ contactsList.setAdapter(adapter);
+ edit.setEnabled(true);
+ }
+
+ if (adapter.getCount() > 0) {
+ contactsFetchInProgress.setVisibility(View.GONE);
+ }
+ ContactsManager.getInstance().setLinphoneContactsPrefered(onlyDisplayLinphoneContacts);
+ }
+
+ private void changeContactsToggle() {
+ if (onlyDisplayLinphoneContacts) {
+ allContacts.setEnabled(true);
+ allContactsSelected.setVisibility(View.INVISIBLE);
+ linphoneContacts.setEnabled(false);
+ linphoneContactsSelected.setVisibility(View.VISIBLE);
+ } else {
+ allContacts.setEnabled(false);
+ allContactsSelected.setVisibility(View.VISIBLE);
+ linphoneContacts.setEnabled(true);
+ linphoneContactsSelected.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView> adapter, View view, int position, long id) {
+ LinphoneContact contact = (LinphoneContact) adapter.getItemAtPosition(position);
+ if (editOnClick) {
+ editConsumed = true;
+ LinphoneActivity.instance().editContact(contact, sipAddressToAdd);
+ } else {
+ lastKnownPosition = contactsList.getFirstVisiblePosition();
+ LinphoneActivity.instance().displayContact(contact, onlyDisplayChatAddress);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ ContactsManager.addContactsListener(this);
+ super.onResume();
+
+ if (editConsumed) {
+ editOnClick = false;
+ sipAddressToAdd = null;
+ }
+
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().selectMenu(FragmentsAvailable.CONTACTS_LIST);
+ LinphoneActivity.instance().hideTabBar(false);
+ onlyDisplayLinphoneContacts = ContactsManager.getInstance().isLinphoneContactsPrefered();
+ }
+ changeContactsToggle();
+ invalidate();
+ }
+
+ @Override
+ public void onPause() {
+ ContactsManager.removeContactsListener(this);
+ super.onPause();
+ }
+
+ @Override
+ public void onContactsUpdated() {
+ invalidate();
+ }
+
+ public void invalidate() {
+ if (searchField != null && searchField.getText().toString().length() > 0) {
+ searchContacts(searchField.getText().toString());
+ } else {
+ changeContactsAdapter();
+ }
+ contactsList.setSelectionFromTop(lastKnownPosition, 0);
+ }
+
+ class ContactsListAdapter extends BaseAdapter implements SectionIndexer {
+ private class ViewHolder {
+ public CheckBox delete;
+ public ImageView linphoneFriend;
+ public TextView name;
+ public LinearLayout separator;
+ public TextView separatorText;
+ public ImageView contactPicture;
+ public TextView organization;
+ //public ImageView friendStatus;
+
+ public ViewHolder(View view) {
+ delete = (CheckBox) view.findViewById(R.id.delete);
+ linphoneFriend = (ImageView) view.findViewById(R.id.friendLinphone);
+ name = (TextView) view.findViewById(R.id.name);
+ separator = (LinearLayout) view.findViewById(R.id.separator);
+ separatorText = (TextView) view.findViewById(R.id.separator_text);
+ contactPicture = (ImageView) view.findViewById(R.id.contact_picture);
+ organization = (TextView) view.findViewById(R.id.contactOrganization);
+ //friendStatus = (ImageView) view.findViewById(R.id.friendStatus);
+ }
+ }
+
+ private List contacts;
+ String[] sections;
+ ArrayList sectionsList;
+ Mapmap = new LinkedHashMap();
+
+ ContactsListAdapter(List contactsList) {
+ contacts = contactsList;
+
+ map = new LinkedHashMap();
+ String prevLetter = null;
+ for (int i = 0; i < contacts.size(); i++) {
+ LinphoneContact contact = contacts.get(i);
+ String fullName = contact.getFullName();
+ if (fullName == null || fullName.isEmpty()) {
+ continue;
+ }
+ String firstLetter = fullName.substring(0, 1).toUpperCase(Locale.getDefault());
+ if (!firstLetter.equals(prevLetter)) {
+ prevLetter = firstLetter;
+ map.put(firstLetter, i);
+ }
+ }
+ sectionsList = new ArrayList(map.keySet());
+ sections = new String[sectionsList.size()];
+ sectionsList.toArray(sections);
+ }
+
+ public int getCount() {
+ return contacts.size();
+ }
+
+ public Object getItem(int position) {
+ if (position >= getCount()) return null;
+ return contacts.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View view = null;
+ LinphoneContact contact = (LinphoneContact) getItem(position);
+ if (contact == null) return null;
+
+ ViewHolder holder = null;
+ if (convertView != null) {
+ view = convertView;
+ holder = (ViewHolder) view.getTag();
+ } else {
+ view = mInflater.inflate(R.layout.contact_cell, parent, false);
+ holder = new ViewHolder(view);
+ view.setTag(holder);
+ }
+
+ holder.name.setText(contact.getFullName());
+
+ if (!isSearchMode) {
+ if (getPositionForSection(getSectionForPosition(position)) != position) {
+ holder.separator.setVisibility(View.GONE);
+ } else {
+ holder.separator.setVisibility(View.VISIBLE);
+ String fullName = contact.getFullName();
+ if (fullName != null && !fullName.isEmpty()) {
+ holder.separatorText.setText(String.valueOf(fullName.charAt(0)));
+ }
+ }
+ } else {
+ holder.separator.setVisibility(View.GONE);
+ }
+
+ if (contact.isInLinphoneFriendList()) {
+ holder.linphoneFriend.setVisibility(View.VISIBLE);
+ } else {
+ holder.linphoneFriend.setVisibility(View.GONE);
+ }
+
+ if (contact.hasPhoto()) {
+ Bitmap photo = contact.getPhoto();
+ if (photo != null) {
+ holder.contactPicture.setImageBitmap(photo);
+ } else {
+ LinphoneUtils.setImagePictureFromUri(getActivity(), holder.contactPicture, contact.getPhotoUri(), contact.getThumbnailUri());
+ }
+ } else {
+ holder.contactPicture.setImageResource(R.drawable.avatar);
+ }
+
+ boolean isOrgVisible = getResources().getBoolean(R.bool.display_contact_organization);
+ String org = contact.getOrganization();
+ if (org != null && !org.isEmpty() && isOrgVisible) {
+ holder.organization.setText(org);
+ holder.organization.setVisibility(View.VISIBLE);
+ } else {
+ holder.organization.setVisibility(View.GONE);
+ }
+
+ if (isEditMode) {
+ holder.delete.setVisibility(View.VISIBLE);
+ holder.delete.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ contactsList.setItemChecked(position, b);
+ if(getNbItemsChecked() == getCount()){
+ deselectAll.setVisibility(View.VISIBLE);
+ selectAll.setVisibility(View.GONE);
+ enabledDeleteButton(true);
+ } else {
+ if(getNbItemsChecked() == 0){
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(false);
+ } else {
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(true);
+ }
+ }
+ }
+ });
+ if (contactsList.isItemChecked(position)) {
+ holder.delete.setChecked(true);
+ } else {
+ holder.delete.setChecked(false);
+ }
+ } else {
+ holder.delete.setVisibility(View.INVISIBLE);
+ }
+
+ /*LinphoneFriend[] friends = LinphoneManager.getLc().getFriendList();
+ if (!ContactsManager.getInstance().isContactPresenceDisabled() && friends != null) {
+ holder.friendStatus.setVisibility(View.VISIBLE);
+ PresenceActivityType presenceActivity = friends[0].getPresenceModel().getActivity().getType();
+ if (presenceActivity == PresenceActivityType.Online) {
+ holder.friendStatus.setImageResource(R.drawable.led_connected);
+ } else if (presenceActivity == PresenceActivityType.Busy) {
+ holder.friendStatus.setImageResource(R.drawable.led_error);
+ } else if (presenceActivity == PresenceActivityType.Away) {
+ holder.friendStatus.setImageResource(R.drawable.led_inprogress);
+ } else if (presenceActivity == PresenceActivityType.Offline) {
+ holder.friendStatus.setImageResource(R.drawable.led_disconnected);
+ } else {
+ holder.friendStatus.setImageResource(R.drawable.call_quality_indicator_0);
+ }
+ }*/
+
+ return view;
+ }
+
+ @Override
+ public Object[] getSections() {
+ return sections;
+ }
+
+ @Override
+ public int getPositionForSection(int sectionIndex) {
+ if (sectionIndex >= sections.length || sectionIndex < 0) {
+ return 0;
+ }
+ return map.get(sections[sectionIndex]);
+ }
+
+ @Override
+ public int getSectionForPosition(int position) {
+ if (position >= contacts.size() || position < 0) {
+ return 0;
+ }
+ LinphoneContact contact = contacts.get(position);
+ String fullName = contact.getFullName();
+ if (fullName == null || fullName.isEmpty()) {
+ return 0;
+ }
+ String letter = fullName.substring(0, 1).toUpperCase(Locale.getDefault());
+ return sectionsList.indexOf(letter);
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactsManager.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactsManager.java
new file mode 100644
index 0000000..ace5ef5
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/ContactsManager.java
@@ -0,0 +1,442 @@
+/*
+ContactsManager.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneFriend;
+import org.linphone.core.LinphoneProxyConfig;
+import org.linphone.mediastream.Log;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Data;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+interface ContactsUpdatedListener {
+ void onContactsUpdated();
+}
+
+public class ContactsManager extends ContentObserver {
+ private static ContactsManager instance;
+ private List contacts, sipContacts;
+ private boolean preferLinphoneContacts = false, isContactPresenceDisabled = true, hasContactAccess = false;
+ private ContentResolver contentResolver;
+ private Context context;
+ private ContactsFetchTask contactsFetchTask;
+
+ private static ArrayList contactsUpdatedListeners;
+ public static void addContactsListener(ContactsUpdatedListener listener) {
+ contactsUpdatedListeners.add(listener);
+ }
+ public static void removeContactsListener(ContactsUpdatedListener listener) {
+ contactsUpdatedListeners.remove(listener);
+ }
+
+ private static Handler handler = new Handler() {
+ @Override
+ public void handleMessage (Message msg) {
+
+ }
+ };
+
+ private ContactsManager(Handler handler) {
+ super(handler);
+ contactsUpdatedListeners = new ArrayList();
+ contacts = new ArrayList();
+ sipContacts = new ArrayList();
+ }
+
+ public void destroy() {
+ if (contactsFetchTask != null && !contactsFetchTask.isCancelled()) {
+ contactsFetchTask.cancel(true);
+ }
+ instance = null;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ onChange(selfChange, null);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ fetchContactsAsync();
+ }
+
+ public ContentResolver getContentResolver() {
+ return contentResolver;
+ }
+
+ public static final synchronized ContactsManager getInstance() {
+ if (instance == null) instance = new ContactsManager(handler);
+ return instance;
+ }
+
+ public synchronized boolean hasContacts() {
+ return contacts.size() > 0;
+ }
+
+ public synchronized List getContacts() {
+ return contacts;
+ }
+
+ public synchronized List getSIPContacts() {
+ setContacts(contacts);
+ return sipContacts;
+ }
+
+ public synchronized List getContacts(String search) {
+ search = search.toLowerCase(Locale.getDefault());
+ List searchContactsBegin = new ArrayList();
+ List searchContactsContain = new ArrayList();
+ for (LinphoneContact contact : contacts) {
+ if (contact.getFullName() != null) {
+ if (contact.getFullName().toLowerCase(Locale.getDefault()).startsWith(search)) {
+ searchContactsBegin.add(contact);
+ } else if (contact.getFullName().toLowerCase(Locale.getDefault()).contains(search)) {
+ searchContactsContain.add(contact);
+ }
+ }
+ }
+ searchContactsBegin.addAll(searchContactsContain);
+ return searchContactsBegin;
+ }
+
+ public synchronized List getSIPContacts(String search) {
+ search = search.toLowerCase(Locale.getDefault());
+ List searchContactsBegin = new ArrayList();
+ List searchContactsContain = new ArrayList();
+ for (LinphoneContact contact : sipContacts) {
+ if (contact.getFullName() != null) {
+ if (contact.getFullName().toLowerCase(Locale.getDefault()).startsWith(search)) {
+ searchContactsBegin.add(contact);
+ } else if (contact.getFullName().toLowerCase(Locale.getDefault()).contains(search)) {
+ searchContactsContain.add(contact);
+ }
+ }
+ }
+ searchContactsBegin.addAll(searchContactsContain);
+ return searchContactsBegin;
+ }
+
+ public void enableContactsAccess() {
+ hasContactAccess = true;
+ }
+
+ public boolean hasContactsAccess() {
+ if (context == null)
+ return false;
+ int contacts = context.getPackageManager().checkPermission(android.Manifest.permission.READ_CONTACTS, context.getPackageName());
+ return contacts == context.getPackageManager().PERMISSION_GRANTED && !context.getResources().getBoolean(R.bool.force_use_of_linphone_friends);
+ }
+
+ public void setLinphoneContactsPrefered(boolean isPrefered) {
+ preferLinphoneContacts = isPrefered;
+ }
+
+ public boolean isLinphoneContactsPrefered() {
+ return preferLinphoneContacts;
+ }
+
+ public boolean isContactPresenceDisabled() {
+ return isContactPresenceDisabled;
+ }
+
+ public void initializeContactManager(Context context, ContentResolver contentResolver) {
+ this.context = context;
+ this.contentResolver = contentResolver;
+ }
+
+ public void initializeSyncAccount(Context context, ContentResolver contentResolver) {
+ initializeContactManager(context, contentResolver);
+ AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
+
+ Account[] accounts = accountManager.getAccountsByType(context.getPackageName());
+
+ if (accounts != null && accounts.length == 0) {
+ Account newAccount = new Account(context.getString(R.string.sync_account_name), context.getPackageName());
+ try {
+ accountManager.addAccountExplicitly(newAccount, null, null);
+ } catch (Exception e) {
+ Log.e(e);
+ }
+ }
+ initializeContactManager(context, contentResolver);
+ }
+
+ public LinphoneContact findContactFromAddress(LinphoneAddress address) {
+ String sipUri = address.asStringUriOnly();
+ String username = address.getUserName();
+
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ LinphoneProxyConfig lpc = null;
+ if (lc != null) {
+ lpc = lc.getDefaultProxyConfig();
+ }
+
+ for (LinphoneContact c: getContacts()) {
+ for (LinphoneNumberOrAddress noa: c.getNumbersOrAddresses()) {
+ String normalized = null;
+ if (lpc != null) {
+ normalized = lpc.normalizePhoneNumber(noa.getValue());
+ }
+ String alias = c.getPresenceModelForUri(noa.getValue());
+
+ if ((noa.isSIPAddress() && noa.getValue().equals(sipUri)) || (alias != null && alias.equals(sipUri)) || (normalized != null && !noa.isSIPAddress() && normalized.equals(username)) || (!noa.isSIPAddress() && noa.getValue().equals(username))) {
+ return c;
+ }
+ }
+ }
+ return null;
+ }
+
+ public LinphoneContact findContactFromPhoneNumber(String phoneNumber) {
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ LinphoneProxyConfig lpc = null;
+ if (lc != null) {
+ lpc = lc.getDefaultProxyConfig();
+ }
+
+ for (LinphoneContact c: getContacts()) {
+ for (LinphoneNumberOrAddress noa: c.getNumbersOrAddresses()) {
+ String normalized = null;
+ if (lpc != null) {
+ normalized = lpc.normalizePhoneNumber(noa.getValue());
+ }
+ if (noa.getValue().equals(phoneNumber) || (normalized != null && normalized.equals(phoneNumber))) {
+ return c;
+ }
+ }
+ }
+ return null;
+ }
+
+ public synchronized void setContacts(List c) {
+ contacts = c;
+ sipContacts = new ArrayList();
+ for (LinphoneContact contact : contacts) {
+ if (contact.hasAddress() || contact.isInLinphoneFriendList()) {
+ sipContacts.add(contact);
+ }
+ }
+ }
+
+ public synchronized void fetchContactsAsync() {
+ if (contactsFetchTask != null && !contactsFetchTask.isCancelled()) {
+ contactsFetchTask.cancel(true);
+ }
+ contactsFetchTask = new ContactsFetchTask();
+ contactsFetchTask.execute();
+ }
+
+ private class ContactsFetchTask extends AsyncTask, List> {
+ @SuppressWarnings("unchecked")
+ protected List doInBackground(Void... params) {
+ List contacts = new ArrayList();
+
+ if (hasContactsAccess()) {
+ Cursor c = getContactsCursor(contentResolver);
+ if (c != null) {
+ while (c.moveToNext()) {
+ String id = c.getString(c.getColumnIndex(Data.CONTACT_ID));
+ LinphoneContact contact = new LinphoneContact();
+ contact.setAndroidId(id);
+ contacts.add(contact);
+ }
+ c.close();
+ }
+ } else {
+ Log.w("[Permission] Read contacts permission wasn't granted, only fetch LinphoneFriends");
+ }
+
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ for (LinphoneFriend friend : lc.getFriendList()) {
+ String refkey = friend.getRefKey();
+ if (refkey != null) {
+ boolean found = false;
+ for (LinphoneContact contact : contacts) {
+ if (refkey.equals(contact.getAndroidId())) {
+ // Native matching contact found, link the friend to it
+ contact.setFriend(friend);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ if (hasContactAccess) {
+ // If refkey != null and hasContactAccess but there isn't a native contact with this value, then this contact has been deleted. Let's do the same with the LinphoneFriend
+ lc.removeFriend(friend);
+ } else {
+ // Refkey not null but no contact access => can't link it to native contact so display it on is own
+ LinphoneContact contact = new LinphoneContact();
+ contact.setFriend(friend);
+ contacts.add(contact);
+ }
+ }
+ } else {
+ // No refkey so it's a standalone contact
+ LinphoneContact contact = new LinphoneContact();
+ contact.setFriend(friend);
+ contacts.add(contact);
+ }
+ }
+ }
+
+ for (LinphoneContact contact : contacts) {
+ // This will only get name & picture informations to be able to quickly display contacts list
+ contact.minimalRefresh();
+ }
+ Collections.sort(contacts);
+
+ // Public the current list of contacts without all the informations populated
+ publishProgress(contacts);
+
+ for (LinphoneContact contact : contacts) {
+ // This time fetch all informations including phone numbers and SIP addresses
+ contact.refresh();
+ }
+
+ return contacts;
+ }
+
+ protected void onProgressUpdate(List... result) {
+ setContacts(result[0]);
+ for (ContactsUpdatedListener listener : contactsUpdatedListeners) {
+ listener.onContactsUpdated();
+ }
+ }
+
+ protected void onPostExecute(List result) {
+ setContacts(result);
+ for (ContactsUpdatedListener listener : contactsUpdatedListeners) {
+ listener.onContactsUpdated();
+ }
+ }
+ }
+
+ public static String getAddressOrNumberForAndroidContact(ContentResolver resolver, Uri contactUri) {
+ // Phone Numbers
+ String[] projection = new String[] { CommonDataKinds.Phone.NUMBER };
+ Cursor c = resolver.query(contactUri, projection, null, null, null);
+ if (c != null) {
+ while (c.moveToNext()) {
+ int numberIndex = c.getColumnIndex(CommonDataKinds.Phone.NUMBER);
+ String number = c.getString(numberIndex);
+ c.close();
+ return number;
+ }
+ }
+
+ // SIP addresses
+ projection = new String[] { CommonDataKinds.SipAddress.SIP_ADDRESS };
+ c = resolver.query(contactUri, projection, null, null, null);
+ if (c != null) {
+ while (c.moveToNext()) {
+ int numberIndex = c.getColumnIndex(CommonDataKinds.SipAddress.SIP_ADDRESS);
+ String address = c.getString(numberIndex);
+ c.close();
+ return address;
+ }
+ c.close();
+ }
+ return null;
+ }
+
+ public void delete(String id) {
+ ArrayList ids = new ArrayList();
+ ids.add(id);
+ deleteMultipleContactsAtOnce(ids);
+ }
+
+ public void deleteMultipleContactsAtOnce(List ids) {
+ String select = Data.CONTACT_ID + " = ?";
+ ArrayList ops = new ArrayList();
+
+ for (String id : ids) {
+ String[] args = new String[] { id };
+ ops.add(ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI).withSelection(select, args).build());
+ }
+
+ ContentResolver cr = ContactsManager.getInstance().getContentResolver();
+ try {
+ cr.applyBatch(ContactsContract.AUTHORITY, ops);
+ } catch (Exception e) {
+ Log.e(e);
+ }
+ }
+
+ public String getString(int resourceID) {
+ return context.getString(resourceID);
+ }
+
+ private Cursor getContactsCursor(ContentResolver cr) {
+ String req = "(" + Data.MIMETYPE + " = '" + CommonDataKinds.Phone.CONTENT_ITEM_TYPE
+ + "' AND " + CommonDataKinds.Phone.NUMBER + " IS NOT NULL "
+ + " OR (" + Data.MIMETYPE + " = '" + CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE
+ + "' AND " + CommonDataKinds.SipAddress.SIP_ADDRESS + " IS NOT NULL))";
+ String[] projection = new String[] { Data.CONTACT_ID, Data.DISPLAY_NAME };
+ String query = Data.DISPLAY_NAME + " IS NOT NULL AND (" + req + ")";
+
+ Cursor cursor = cr.query(Data.CONTENT_URI, projection, query, null, " lower(" + Data.DISPLAY_NAME + ") COLLATE UNICODE ASC");
+ if (cursor == null) {
+ return cursor;
+ }
+
+ MatrixCursor result = new MatrixCursor(cursor.getColumnNames());
+ Set groupBy = new HashSet();
+ while (cursor.moveToNext()) {
+ String name = cursor.getString(cursor.getColumnIndex(Data.DISPLAY_NAME));
+ if (!groupBy.contains(name)) {
+ groupBy.add(name);
+ Object[] newRow = new Object[cursor.getColumnCount()];
+
+ int contactID = cursor.getColumnIndex(Data.CONTACT_ID);
+ int displayName = cursor.getColumnIndex(Data.DISPLAY_NAME);
+
+ newRow[contactID] = cursor.getString(contactID);
+ newRow[displayName] = cursor.getString(displayName);
+
+ result.addRow(newRow);
+ }
+ }
+ cursor.close();
+ return result;
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/DialerFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/DialerFragment.java
new file mode 100644
index 0000000..9de9c09
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/DialerFragment.java
@@ -0,0 +1,241 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+DialerFragment.java
+Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+import org.linphone.core.LinphoneCore;
+import org.linphone.mediastream.Log;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.ui.AddressAware;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.ui.AddressText;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.ui.CallButton;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.ui.EraseButton;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import android.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class DialerFragment extends Fragment {
+ private static DialerFragment instance;
+ private static boolean isCallTransferOngoing = false;
+
+ private AddressAware numpad;
+ private AddressText mAddress;
+ private CallButton mCall;
+ private ImageView mAddContact;
+ private OnClickListener addContactListener, cancelListener, transferListener;
+ private boolean shouldEmptyAddressField = true;
+
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.dialer, container, false);
+
+ mAddress = (AddressText) view.findViewById(R.id.address);
+ mAddress.setDialerFragment(this);
+
+ EraseButton erase = (EraseButton) view.findViewById(R.id.erase);
+ erase.setAddressWidget(mAddress);
+
+ mCall = (CallButton) view.findViewById(R.id.call);
+ mCall.setAddressWidget(mAddress);
+ if (LinphoneActivity.isInstanciated() && LinphoneManager.getLc().getCallsNb() > 0) {
+ if (isCallTransferOngoing) {
+ mCall.setImageResource(R.drawable.call_transfer);
+ } else {
+ mCall.setImageResource(R.drawable.call_add);
+ }
+ } else {
+ mCall.setImageResource(R.drawable.call_audio_start);
+ }
+
+ numpad = (AddressAware) view.findViewById(R.id.numpad);
+ if (numpad != null) {
+ numpad.setAddressWidget(mAddress);
+ }
+
+ mAddContact = (ImageView) view.findViewById(R.id.add_contact);
+ mAddContact.setEnabled(!(LinphoneActivity.isInstanciated() && LinphoneManager.getLc().getCallsNb() > 0));
+
+ addContactListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinphoneActivity.instance().displayContactsForEdition(mAddress.getText().toString());
+ }
+ };
+ cancelListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinphoneActivity.instance().resetClassicMenuLayoutAndGoBackToCallIfStillRunning();
+ }
+ };
+ transferListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LinphoneCore lc = LinphoneManager.getLc();
+ if (lc.getCurrentCall() == null) {
+ return;
+ }
+ lc.transferCall(lc.getCurrentCall(), mAddress.getText().toString());
+ isCallTransferOngoing = false;
+ LinphoneActivity.instance().resetClassicMenuLayoutAndGoBackToCallIfStillRunning();
+ }
+ };
+
+ resetLayout(isCallTransferOngoing);
+
+ if (getArguments() != null) {
+ shouldEmptyAddressField = false;
+ String number = getArguments().getString("SipUri");
+ String displayName = getArguments().getString("DisplayName");
+ String photo = getArguments().getString("PhotoUri");
+ mAddress.setText(number);
+ if (displayName != null) {
+ mAddress.setDisplayedName(displayName);
+ }
+ if (photo != null) {
+ mAddress.setPictureUri(Uri.parse(photo));
+ }
+ }
+
+ instance = this;
+
+ return view;
+ }
+
+
+
+ /**
+ * @return null if not ready yet
+ */
+ public static DialerFragment instance() {
+ return instance;
+ }
+
+ @Override
+ public void onPause() {
+ instance = null;
+ super.onPause();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ instance = this;
+
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().selectMenu(FragmentsAvailable.DIALER);
+ LinphoneActivity.instance().updateDialerFragment(this);
+ LinphoneActivity.instance().showStatusBar();
+ LinphoneActivity.instance().hideTabBar(false);
+ }
+
+ boolean isOrientationLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ if(isOrientationLandscape && !getResources().getBoolean(R.bool.isTablet)) {
+ ((LinearLayout) numpad).setVisibility(View.GONE);
+ } else {
+ ((LinearLayout) numpad).setVisibility(View.VISIBLE);
+ }
+
+ if (shouldEmptyAddressField) {
+ mAddress.setText("");
+ } else {
+ shouldEmptyAddressField = true;
+ }
+ resetLayout(isCallTransferOngoing);
+ }
+
+ public void resetLayout(boolean callTransfer) {
+ if (!LinphoneActivity.isInstanciated()) {
+ return;
+ }
+ isCallTransferOngoing = LinphoneActivity.instance().isCallTransfer();
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc == null) {
+ return;
+ }
+
+ if (lc.getCallsNb() > 0) {
+ if (isCallTransferOngoing) {
+ mCall.setImageResource(R.drawable.call_transfer);
+ mCall.setExternalClickListener(transferListener);
+ } else {
+ mCall.setImageResource(R.drawable.call_add);
+ mCall.resetClickListener();
+ }
+ mAddContact.setEnabled(true);
+ mAddContact.setImageResource(R.drawable.call_alt_back);
+ mAddContact.setOnClickListener(cancelListener);
+ } else {
+ mCall.setImageResource(R.drawable.call_audio_start);
+ mAddContact.setEnabled(false);
+ mAddContact.setImageResource(R.drawable.contact_add_button);
+ mAddContact.setOnClickListener(addContactListener);
+ enableDisableAddContact();
+ }
+ }
+
+ public void enableDisableAddContact() {
+ mAddContact.setEnabled(LinphoneManager.getLc().getCallsNb() > 0 || !mAddress.getText().toString().equals(""));
+ }
+
+ public void displayTextInAddressBar(String numberOrSipAddress) {
+ shouldEmptyAddressField = false;
+ mAddress.setText(numberOrSipAddress);
+ }
+
+ public void newOutgoingCall(String numberOrSipAddress) {
+ displayTextInAddressBar(numberOrSipAddress);
+ LinphoneManager.getInstance().newOutgoingCall(mAddress);
+ }
+
+ public void newOutgoingCall(Intent intent) {
+ if (intent != null && intent.getData() != null) {
+ String scheme = intent.getData().getScheme();
+ if (scheme.startsWith("imto")) {
+ mAddress.setText("sip:" + intent.getData().getLastPathSegment());
+ } else if (scheme.startsWith("call") || scheme.startsWith("sip")) {
+ mAddress.setText(intent.getData().getSchemeSpecificPart());
+ } else {
+ Uri contactUri = intent.getData();
+ String address = ContactsManager.getAddressOrNumberForAndroidContact(LinphoneService.instance().getContentResolver(), contactUri);
+ if(address != null) {
+ mAddress.setText(address);
+ } else {
+ Log.e("Unknown scheme: ", scheme);
+ mAddress.setText(intent.getData().getSchemeSpecificPart());
+ }
+ }
+
+ mAddress.clearDisplayedName();
+ intent.setData(null);
+
+ LinphoneManager.getInstance().newOutgoingCall(mAddress);
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/EmptyFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/EmptyFragment.java
new file mode 100644
index 0000000..80317c6
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/EmptyFragment.java
@@ -0,0 +1,40 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+AboutFragment.java
+Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+import android.os.Bundle;
+import android.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class EmptyFragment extends Fragment {
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ View view = inflater.inflate(R.layout.empty_fragment, container, false);
+ return view;
+ }
+
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/FragmentsAvailable.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/FragmentsAvailable.java
new file mode 100644
index 0000000..7b041aa
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/FragmentsAvailable.java
@@ -0,0 +1,98 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+FragmentsAvailable.java
+Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+/**
+ * @author Sylvain Berfini
+ */
+public enum FragmentsAvailable {
+ UNKNOW,
+ DIALER,
+ EMPTY,
+ HISTORY_LIST,
+ HISTORY_DETAIL,
+ CONTACTS_LIST,
+ CONTACT_DETAIL,
+ CONTACT_EDITOR,
+ ABOUT,
+ ACCOUNT_SETTINGS,
+ SETTINGS,
+ CHAT_LIST,
+ CHAT;
+
+ public boolean shouldAnimate() {
+ return true;
+ }
+
+ public boolean isRightOf(FragmentsAvailable fragment) {
+ switch (this) {
+ case HISTORY_LIST:
+ return fragment == UNKNOW;
+
+ case HISTORY_DETAIL:
+ return HISTORY_LIST.isRightOf(fragment) || fragment == HISTORY_LIST;
+
+ case CONTACTS_LIST:
+ return HISTORY_DETAIL.isRightOf(fragment) || fragment == HISTORY_DETAIL;
+
+ case CONTACT_DETAIL:
+ return CONTACTS_LIST.isRightOf(fragment) || fragment == CONTACTS_LIST;
+
+ case CONTACT_EDITOR:
+ return CONTACT_DETAIL.isRightOf(fragment) || fragment == CONTACT_DETAIL;
+
+ case DIALER:
+ return CONTACT_EDITOR.isRightOf(fragment) || fragment == CONTACT_EDITOR;
+
+ case CHAT_LIST:
+ return DIALER.isRightOf(fragment) || fragment == DIALER;
+
+ case SETTINGS:
+ return CHAT_LIST.isRightOf(fragment) || fragment == CHAT_LIST;
+
+ case ABOUT:
+ case ACCOUNT_SETTINGS:
+ return SETTINGS.isRightOf(fragment) || fragment == SETTINGS;
+
+ case CHAT:
+ return CHAT_LIST.isRightOf(fragment) || fragment == CHAT_LIST;
+
+ default:
+ return false;
+ }
+ }
+
+ public boolean shouldAddItselfToTheRightOf(FragmentsAvailable fragment) {
+ switch (this) {
+ case HISTORY_DETAIL:
+ return fragment == HISTORY_LIST || fragment == HISTORY_DETAIL;
+
+ case CONTACT_DETAIL:
+ return fragment == CONTACTS_LIST || fragment == CONTACT_EDITOR|| fragment == CONTACT_DETAIL;
+
+ case CONTACT_EDITOR:
+ return fragment == CONTACTS_LIST || fragment == CONTACT_DETAIL || fragment == CONTACT_EDITOR;
+
+ case CHAT:
+ return fragment == CHAT_LIST || fragment == CHAT;
+
+ default:
+ return false;
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/HistoryDetailFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/HistoryDetailFragment.java
new file mode 100644
index 0000000..09c6b28
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/HistoryDetailFragment.java
@@ -0,0 +1,178 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+HistoryDetailFragment.java
+Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneCoreException;
+import org.linphone.core.LinphoneCoreFactory;
+import org.linphone.mediastream.Log;
+
+import android.app.Fragment;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class HistoryDetailFragment extends Fragment implements OnClickListener {
+ private ImageView dialBack, chat, addToContacts, goToContact, back;
+ private View view;
+ private ImageView contactPicture, callDirection;
+ private TextView contactName, contactAddress, time, date;
+ private String sipUri, displayName, pictureUri;
+ private LinphoneContact contact;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ sipUri = getArguments().getString("SipUri");
+ displayName = getArguments().getString("DisplayName");
+ pictureUri = getArguments().getString("PictureUri");
+ String status = getArguments().getString("CallStatus");
+ String callTime = getArguments().getString("CallTime");
+ String callDate = getArguments().getString("CallDate");
+
+ view = inflater.inflate(R.layout.history_detail, container, false);
+
+ dialBack = (ImageView) view.findViewById(R.id.call);
+ dialBack.setOnClickListener(this);
+
+ back = (ImageView) view.findViewById(R.id.back);
+ if(getResources().getBoolean(R.bool.isTablet)){
+ back.setVisibility(View.INVISIBLE);
+ } else {
+ back.setOnClickListener(this);
+ }
+
+ chat = (ImageView) view.findViewById(R.id.chat);
+ chat.setOnClickListener(this);
+ if (getResources().getBoolean(R.bool.disable_chat))
+ view.findViewById(R.id.chat).setVisibility(View.GONE);
+
+ addToContacts = (ImageView) view.findViewById(R.id.add_contact);
+ addToContacts.setOnClickListener(this);
+
+ goToContact = (ImageView) view.findViewById(R.id.goto_contact);
+ goToContact.setOnClickListener(this);
+
+ contactPicture = (ImageView) view.findViewById(R.id.contact_picture);
+
+ contactName = (TextView) view.findViewById(R.id.contact_name);
+ contactAddress = (TextView) view.findViewById(R.id.contact_address);
+
+ callDirection = (ImageView) view.findViewById(R.id.direction);
+
+ time = (TextView) view.findViewById(R.id.time);
+ date = (TextView) view.findViewById(R.id.date);
+
+ displayHistory(status, callTime, callDate);
+
+ return view;
+ }
+
+ private void displayHistory(String status, String callTime, String callDate) {
+ if (status.equals(getResources().getString(R.string.missed))) {
+ callDirection.setImageResource(R.drawable.call_missed);
+ } else if (status.equals(getResources().getString(R.string.incoming))) {
+ callDirection.setImageResource(R.drawable.call_incoming);
+ } else if (status.equals(getResources().getString(R.string.outgoing))) {
+ callDirection.setImageResource(R.drawable.call_outgoing);
+ }
+
+ time.setText(callTime == null ? "" : callTime);
+ Long longDate = Long.parseLong(callDate);
+ date.setText(LinphoneUtils.timestampToHumanDate(getActivity(),longDate,getString(R.string.history_detail_date_format)));
+
+ LinphoneAddress lAddress = null;
+ try {
+ lAddress = LinphoneCoreFactory.instance().createLinphoneAddress(sipUri);
+ } catch (LinphoneCoreException e) {
+ Log.e(e);
+ }
+
+ if (lAddress != null) {
+ contactAddress.setText(lAddress.asStringUriOnly());
+ contact = ContactsManager.getInstance().findContactFromAddress(lAddress);
+ if (contact != null) {
+ contactName.setText(contact.getFullName());
+ LinphoneUtils.setImagePictureFromUri(view.getContext(),contactPicture,contact.getPhotoUri(),contact.getThumbnailUri());
+ addToContacts.setVisibility(View.GONE);
+ goToContact.setVisibility(View.VISIBLE);
+ } else {
+ contactName.setText(displayName == null ? LinphoneUtils.getAddressDisplayName(sipUri) : displayName);
+ contactPicture.setImageResource(R.drawable.avatar);
+ addToContacts.setVisibility(View.VISIBLE);
+ goToContact.setVisibility(View.GONE);
+ }
+ } else {
+ contactAddress.setText(sipUri);
+ contactName.setText(displayName == null ? LinphoneUtils.getAddressDisplayName(sipUri) : displayName);
+ }
+ }
+
+ public void changeDisplayedHistory(String sipUri, String displayName, String pictureUri, String status, String callTime, String callDate) {
+ if (displayName == null ) {
+ displayName = LinphoneUtils.getUsernameFromAddress(sipUri);
+ }
+
+ this.sipUri = sipUri;
+ this.displayName = displayName;
+ this.pictureUri = pictureUri;
+ displayHistory(status, callTime, callDate);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().selectMenu(FragmentsAvailable.HISTORY_DETAIL);
+ LinphoneActivity.instance().hideTabBar(false);
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+
+ if (id == R.id.back) {
+ getFragmentManager().popBackStackImmediate();
+ } if (id == R.id.call) {
+ LinphoneActivity.instance().setAddresGoToDialerAndCall(sipUri, displayName, pictureUri == null ? null : Uri.parse(pictureUri));
+ } else if (id == R.id.chat) {
+ LinphoneActivity.instance().displayChat(sipUri);
+ } else if (id == R.id.add_contact) {
+ String uri = sipUri;
+ try {
+ LinphoneAddress addr = LinphoneCoreFactory.instance().createLinphoneAddress(sipUri);
+ uri = addr.asStringUriOnly();
+ } catch (LinphoneCoreException e) {
+ Log.e(e);
+ }
+ LinphoneActivity.instance().displayContactsForEdition(uri);
+ } else if (id == R.id.goto_contact) {
+ LinphoneActivity.instance().displayContact(contact, false);
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/HistoryListFragment.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/HistoryListFragment.java
new file mode 100644
index 0000000..4245ede
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/HistoryListFragment.java
@@ -0,0 +1,540 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+/*
+HistoryListFragment.java
+Copyright (C) 2015 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.List;
+
+import org.linphone.core.CallDirection;
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneCallLog;
+import org.linphone.core.LinphoneCallLog.CallStatus;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class HistoryListFragment extends Fragment implements OnClickListener, OnItemClickListener, ContactsUpdatedListener {
+ private ListView historyList;
+ private LayoutInflater mInflater;
+ private TextView noCallHistory, noMissedCallHistory;
+ private ImageView missedCalls, allCalls, edit, selectAll, deselectAll, delete, cancel;
+ private View allCallsSelected, missedCallsSelected;
+ private LinearLayout editList, topBar;
+ private boolean onlyDisplayMissedCalls, isEditMode;
+ private List mLogs;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mInflater = inflater;
+ View view = inflater.inflate(R.layout.history, container, false);
+
+ noCallHistory = (TextView) view.findViewById(R.id.no_call_history);
+ noMissedCallHistory = (TextView) view.findViewById(R.id.no_missed_call_history);
+
+ historyList = (ListView) view.findViewById(R.id.history_list);
+ historyList.setOnItemClickListener(this);
+
+ delete = (ImageView) view.findViewById(R.id.delete);
+ delete.setOnClickListener(this);
+
+ editList = (LinearLayout) view.findViewById(R.id.edit_list);
+ topBar = (LinearLayout) view.findViewById(R.id.top_bar);
+
+ cancel = (ImageView) view.findViewById(R.id.cancel);
+ cancel.setOnClickListener(this);
+
+ allCalls = (ImageView) view.findViewById(R.id.all_calls);
+ allCalls.setOnClickListener(this);
+
+ allCallsSelected = view.findViewById(R.id.all_calls_select);
+
+ missedCalls = (ImageView) view.findViewById(R.id.missed_calls);
+ missedCalls.setOnClickListener(this);
+
+ missedCallsSelected = view.findViewById(R.id.missed_calls_select);
+
+ selectAll = (ImageView) view.findViewById(R.id.select_all);
+ selectAll.setOnClickListener(this);
+
+ deselectAll = (ImageView) view.findViewById(R.id.deselect_all);
+ deselectAll.setOnClickListener(this);
+
+ allCalls.setEnabled(false);
+ onlyDisplayMissedCalls = false;
+
+ edit = (ImageView) view.findViewById(R.id.edit);
+ edit.setOnClickListener(this);
+
+ return view;
+ }
+
+ public void refresh() {
+ mLogs = Arrays.asList(LinphoneManager.getLc().getCallLogs());
+ }
+
+ private void selectAllList(boolean isSelectAll){
+ int size = historyList.getAdapter().getCount();
+ for(int i=0; i 0) {
+ LinphoneCallLog log = mLogs.get(0);
+ if (log.getDirection() == CallDirection.Incoming) {
+ LinphoneActivity.instance().displayHistoryDetail(mLogs.get(0).getFrom().toString(), mLogs.get(0));
+ } else {
+ LinphoneActivity.instance().displayHistoryDetail(mLogs.get(0).getTo().toString(), mLogs.get(0));
+ }
+ } else {
+ LinphoneActivity.instance().displayEmptyFragment();
+ }
+ }
+
+ private void removeCallLogs(){
+ int size = historyList.getAdapter().getCount();
+ for(int i=0; i missedCalls = new ArrayList();
+ for (LinphoneCallLog log : mLogs) {
+ if (log.getStatus() == CallStatus.Missed) {
+ missedCalls.add(log);
+ }
+ }
+ mLogs = missedCalls;
+ }
+ }
+
+ private boolean hideHistoryListAndDisplayMessageIfEmpty() {
+ removeNotMissedCallsFromLogs();
+ if (mLogs.isEmpty()) {
+ if (onlyDisplayMissedCalls) {
+ noMissedCallHistory.setVisibility(View.VISIBLE);
+ } else {
+ noCallHistory.setVisibility(View.VISIBLE);
+ }
+ historyList.setVisibility(View.GONE);
+ edit.setEnabled(false);
+ return true;
+ } else {
+ noCallHistory.setVisibility(View.GONE);
+ noMissedCallHistory.setVisibility(View.GONE);
+ historyList.setVisibility(View.VISIBLE);
+ edit.setEnabled(true);
+ return false;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ContactsManager.addContactsListener(this);
+
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().selectMenu(FragmentsAvailable.HISTORY_LIST);
+ LinphoneActivity.instance().hideTabBar(false);
+ LinphoneActivity.instance().displayMissedCalls(0);
+ }
+
+ mLogs = Arrays.asList(LinphoneManager.getLc().getCallLogs());
+ if (!hideHistoryListAndDisplayMessageIfEmpty()) {
+ historyList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
+ historyList.setAdapter(new CallHistoryAdapter(getActivity().getApplicationContext()));
+ }
+ }
+
+ @Override
+ public void onPause() {
+ ContactsManager.removeContactsListener(this);
+ super.onPause();
+ }
+
+ @Override
+ public void onContactsUpdated() {
+ historyList.setAdapter(new CallHistoryAdapter(getActivity().getApplicationContext()));
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+
+ if (id == R.id.select_all) {
+ deselectAll.setVisibility(View.VISIBLE);
+ selectAll.setVisibility(View.GONE);
+ enabledDeleteButton(true);
+ selectAllList(true);
+ return;
+ }
+ if (id == R.id.deselect_all) {
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(false);
+ selectAllList(false);
+ return;
+ }
+
+ if (id == R.id.cancel) {
+ quitEditMode();
+ return;
+ }
+
+ if (id == R.id.delete) {
+ if(historyList.getCheckedItemCount() == 0) {
+ quitEditMode();
+ return;
+ }
+
+ final Dialog dialog = LinphoneActivity.instance().displayDialog(getString(R.string.delete_text));
+ Button delete = (Button) dialog.findViewById(R.id.delete_button);
+ Button cancel = (Button) dialog.findViewById(R.id.cancel);
+
+ delete.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ removeCallLogs();
+ dialog.dismiss();
+ quitEditMode();
+ }
+ });
+
+ cancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dialog.dismiss();
+ quitEditMode();
+ }
+ });
+ dialog.show();
+ return;
+ }
+
+ if (id == R.id.all_calls) {
+ allCalls.setEnabled(false);
+ allCallsSelected.setVisibility(View.VISIBLE);
+ missedCallsSelected.setVisibility(View.INVISIBLE);
+ missedCalls.setEnabled(true);
+ onlyDisplayMissedCalls = false;
+ refresh();
+ }
+ if (id == R.id.missed_calls) {
+ allCalls.setEnabled(true);
+ allCallsSelected.setVisibility(View.INVISIBLE);
+ missedCallsSelected.setVisibility(View.VISIBLE);
+ missedCalls.setEnabled(false);
+ onlyDisplayMissedCalls = true;
+ }
+
+ if (id == R.id.edit) {
+ topBar.setVisibility(View.GONE);
+ editList.setVisibility(View.VISIBLE);
+ enabledDeleteButton(false);
+ isEditMode = true;
+ }
+
+ if (!hideHistoryListAndDisplayMessageIfEmpty()) {
+ historyList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
+ historyList.setAdapter(new CallHistoryAdapter(getActivity().getApplicationContext()));
+ }
+
+ if(isEditMode){
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ }
+
+ }
+
+ @Override
+ public void onItemClick(AdapterView> adapter, View view, int position, long id) {
+ if (isEditMode) {
+ LinphoneCallLog log = mLogs.get(position);
+ LinphoneManager.getLc().removeCallLog(log);
+ mLogs = Arrays.asList(LinphoneManager.getLc().getCallLogs());
+ } else {
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneCallLog log = mLogs.get(position);
+ LinphoneAddress address;
+ if (log.getDirection() == CallDirection.Incoming) {
+ address = log.getFrom();
+ } else {
+ address = log.getTo();
+ }
+ LinphoneActivity.instance().setAddresGoToDialerAndCall(address.asStringUriOnly(), address.getDisplayName(), null);
+ }
+ }
+ }
+
+ public void quitEditMode(){
+ isEditMode = false;
+ editList.setVisibility(View.GONE);
+ topBar.setVisibility(View.VISIBLE);
+
+ refresh();
+ if (!hideHistoryListAndDisplayMessageIfEmpty()) {
+ historyList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
+ historyList.setAdapter(new CallHistoryAdapter(getActivity().getApplicationContext()));
+ }
+ if (getResources().getBoolean(R.bool.isTablet)) {
+ displayFirstLog();
+ }
+ }
+
+ class CallHistoryAdapter extends BaseAdapter {
+ private class ViewHolder {
+ public TextView contact;
+ public ImageView detail;
+ public CheckBox select;
+ public ImageView callDirection;
+ public ImageView contactPicture;
+
+ public ViewHolder(View view) {
+ contact = (TextView) view.findViewById(R.id.sip_uri);
+ detail = (ImageView) view.findViewById(R.id.detail);
+ select = (CheckBox) view.findViewById(R.id.delete);
+ callDirection = (ImageView) view.findViewById(R.id.icon);
+ contactPicture = (ImageView) view.findViewById(R.id.contact_picture);
+ }
+ }
+
+ CallHistoryAdapter(Context aContext) {
+
+ }
+
+ public int getCount() {
+ return mLogs.size();
+ }
+
+ public Object getItem(int position) {
+ return mLogs.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @SuppressLint("SimpleDateFormat")
+ private String timestampToHumanDate(Calendar cal) {
+ SimpleDateFormat dateFormat;
+ if (isToday(cal)) {
+ return getString(R.string.today);
+ } else if (isYesterday(cal)) {
+ return getString(R.string.yesterday);
+ } else {
+ dateFormat = new SimpleDateFormat(getResources().getString(R.string.history_date_format));
+ }
+
+ return dateFormat.format(cal.getTime());
+ }
+
+ private boolean isSameDay(Calendar cal1, Calendar cal2) {
+ if (cal1 == null || cal2 == null) {
+ return false;
+ }
+
+ return (cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) &&
+ cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) &&
+ cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR));
+ }
+
+ private boolean isToday(Calendar cal) {
+ return isSameDay(cal, Calendar.getInstance());
+ }
+
+ private boolean isYesterday(Calendar cal) {
+ Calendar yesterday = Calendar.getInstance();
+ yesterday.roll(Calendar.DAY_OF_MONTH, -1);
+ return isSameDay(cal, yesterday);
+ }
+
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View view = null;
+ ViewHolder holder = null;
+
+ if (convertView != null) {
+ view = convertView;
+ holder = (ViewHolder) view.getTag();
+ } else {
+ view = mInflater.inflate(R.layout.history_cell, parent,false);
+ holder = new ViewHolder(view);
+ view.setTag(holder);
+ }
+
+ final LinphoneCallLog log = mLogs.get(position);
+ long timestamp = log.getTimestamp();
+ LinphoneAddress address;
+
+ holder.contact.setSelected(true); // For automated horizontal scrolling of long texts
+
+ LinearLayout separator = (LinearLayout) view.findViewById(R.id.separator);
+ TextView separatorText = (TextView) view.findViewById(R.id.separator_text);
+ Calendar logTime = Calendar.getInstance();
+ logTime.setTimeInMillis(timestamp);
+ separatorText.setText(timestampToHumanDate(logTime));
+
+ if (position > 0) {
+ LinphoneCallLog previousLog = mLogs.get(position-1);
+ long previousTimestamp = previousLog.getTimestamp();
+ Calendar previousLogTime = Calendar.getInstance();
+ previousLogTime.setTimeInMillis(previousTimestamp);
+
+ if (isSameDay(previousLogTime, logTime)) {
+ separator.setVisibility(View.GONE);
+ } else {
+ separator.setVisibility(View.VISIBLE);
+ }
+ } else {
+ separator.setVisibility(View.VISIBLE);
+ }
+
+ if (log.getDirection() == CallDirection.Incoming) {
+ address = log.getFrom();
+ if (log.getStatus() == CallStatus.Missed) {
+ holder.callDirection.setImageResource(R.drawable.call_status_missed);
+ } else {
+ holder.callDirection.setImageResource(R.drawable.call_status_incoming);
+ }
+ } else {
+ address = log.getTo();
+ holder.callDirection.setImageResource(R.drawable.call_status_outgoing);
+ }
+
+ LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(address);
+ String displayName = null;
+ final String sipUri = address.asString();
+ if (c != null) {
+ displayName = c.getFullName();
+ if (c.hasPhoto()) {
+ Bitmap photo = c.getPhoto();
+ if (photo != null) {
+ holder.contactPicture.setImageBitmap(photo);
+ } else {
+ LinphoneUtils.setImagePictureFromUri(getActivity(), holder.contactPicture, c.getPhotoUri(), c.getThumbnailUri());
+ }
+ } else {
+ LinphoneUtils.setImagePictureFromUri(getActivity(), holder.contactPicture, c.getPhotoUri(), c.getThumbnailUri());
+ }
+ } else {
+ holder.contactPicture.setImageResource(R.drawable.avatar);
+ }
+
+ if (displayName == null) {
+ holder.contact.setText(LinphoneUtils.getAddressDisplayName(sipUri));
+ } else {
+ holder.contact.setText(displayName);
+ }
+
+ if (isEditMode) {
+ holder.select.setVisibility(View.VISIBLE);
+ holder.select.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ historyList.setItemChecked(position, b);
+ if(getNbItemsChecked() == getCount()){
+ deselectAll.setVisibility(View.VISIBLE);
+ selectAll.setVisibility(View.GONE);
+ enabledDeleteButton(true);
+ } else {
+ if(getNbItemsChecked() == 0){
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(false);
+ } else {
+ deselectAll.setVisibility(View.GONE);
+ selectAll.setVisibility(View.VISIBLE);
+ enabledDeleteButton(true);
+ }
+ }
+ }
+ });
+ holder.detail.setVisibility(View.INVISIBLE);
+ if(historyList.isItemChecked(position)) {
+ holder.select.setChecked(true);
+ } else {
+ holder.select.setChecked(false);
+ }
+ } else {
+ holder.select.setVisibility(View.GONE);
+ holder.detail.setVisibility(View.VISIBLE);
+ holder.detail.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (LinphoneActivity.isInstanciated()) {
+ LinphoneActivity.instance().displayHistoryDetail(sipUri, log);
+ }
+ }
+ });
+ }
+ return view;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/KeepAliveReceiver.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/KeepAliveReceiver.java
new file mode 100644
index 0000000..5385b8d
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/KeepAliveReceiver.java
@@ -0,0 +1,75 @@
+/*
+KeepAliveReceiver.java
+Copyright (C) 2010 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.compatibility.Compatibility;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCoreFactory;
+import org.linphone.mediastream.Log;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+
+/*
+ * Purpose of this receiver is to disable keep alives when screen is off
+ * */
+public class KeepAliveReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!LinphoneService.isReady()) {
+ return;
+ } else {
+ boolean isDebugEnabled = LinphonePreferences.instance().isDebugEnabled();
+ LinphoneCoreFactory.instance().enableLogCollection(isDebugEnabled);
+ LinphoneCoreFactory.instance().setDebugMode(isDebugEnabled, context.getString(R.string.app_name));
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc == null) return;
+
+ String action = intent.getAction();
+ if (action == null) {
+ Log.i("[KeepAlive] Refresh registers");
+ lc.refreshRegisters();
+ //make sure iterate will have enough time, device will not sleep until exit from this method
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ Log.e("Cannot sleep for 2s", e);
+ } finally {
+ //make sure the application will at least wakes up every 10 mn
+ Intent newIntent = new Intent(context, KeepAliveReceiver.class);
+ PendingIntent keepAlivePendingIntent = PendingIntent.getBroadcast(context, 0, newIntent, PendingIntent.FLAG_ONE_SHOT);
+
+ AlarmManager alarmManager = ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE));
+ Compatibility.scheduleAlarm(alarmManager, AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 600000, keepAlivePendingIntent);
+ }
+ } else if (action.equalsIgnoreCase(Intent.ACTION_SCREEN_ON)) {
+ Log.i("[KeepAlive] Screen is on, enable");
+ lc.enableKeepAlive(true);
+ } else if (action.equalsIgnoreCase(Intent.ACTION_SCREEN_OFF)) {
+ Log.i("[KeepAlive] Screen is off, disable");
+ lc.enableKeepAlive(false);
+ }
+ }
+ }
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/LinphoneActivity.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/LinphoneActivity.java
new file mode 100644
index 0000000..5d0ee24
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/LinphoneActivity.java
@@ -0,0 +1,1791 @@
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+/*
+ LinphoneActivity.java
+ Copyright (C) 2012 Belledonne Communications, Grenoble, France
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+import com.github.rosjava.android_remocons.rocon_remocon.MasterChooser;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.LinphoneManager.AddressType;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.assistant.AssistantActivity;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.assistant.AssistantFragmentsEnum;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.assistant.RemoteProvisioningLoginActivity;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.compatibility.Compatibility;
+import org.linphone.core.CallDirection;
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneAuthInfo;
+import org.linphone.core.LinphoneCall;
+import org.linphone.core.LinphoneCall.State;
+import org.linphone.core.LinphoneCallLog;
+import org.linphone.core.LinphoneCallLog.CallStatus;
+import org.linphone.core.LinphoneChatMessage;
+import org.linphone.core.LinphoneChatRoom;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCore.RegistrationState;
+import org.linphone.core.LinphoneCoreException;
+import org.linphone.core.LinphoneCoreFactory;
+import org.linphone.core.LinphoneCoreListenerBase;
+import org.linphone.core.LinphoneProxyConfig;
+import org.linphone.core.Reason;
+import org.linphone.mediastream.Log;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.purchase.InAppPurchaseActivity;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.ui.AddressText;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.xmlrpc.XmlRpcHelper;
+import com.github.rosjava.android_remocons.rocon_remocon.linphone.xmlrpc.XmlRpcListenerBase;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+/**
+ * @author Sylvain Berfini
+ */
+public class LinphoneActivity extends Activity implements OnClickListener, ContactPicked, ActivityCompat.OnRequestPermissionsResultCallback {
+ public static final String PREF_FIRST_LAUNCH = "pref_first_launch";
+ private static final int SETTINGS_ACTIVITY = 123;
+ private static final int CALL_ACTIVITY = 19;
+ private static final int PERMISSIONS_REQUEST_OVERLAY = 206;
+ private static final int PERMISSIONS_REQUEST_SYNC = 207;
+ private static final int PERMISSIONS_REQUEST_CONTACTS = 208;
+ private static final int PERMISSIONS_RECORD_AUDIO_ECHO_CANCELLER = 209;
+ private static final int PERMISSIONS_READ_EXTERNAL_STORAGE_DEVICE_RINGTONE = 210;
+ private static final int PERMISSIONS_RECORD_AUDIO_ECHO_TESTER = 211;
+
+ private static LinphoneActivity instance;
+
+ private StatusFragment statusFragment;
+ private TextView missedCalls, missedChats;
+ private RelativeLayout contacts, history, dialer, chat;
+ private View contacts_selected, history_selected, dialer_selected, chat_selected;
+ private RelativeLayout mTopBar;
+ private ImageView cancel;
+ private FragmentsAvailable pendingFragmentTransaction, currentFragment;
+ private Fragment fragment;
+ private List fragmentsHistory;
+ private Fragment.SavedState dialerSavedState;
+ private boolean newProxyConfig;
+ private boolean emptyFragment = false;
+ private boolean isTrialAccount = false;
+ private OrientationEventListener mOrientationHelper;
+ private LinphoneCoreListenerBase mListener;
+ private LinearLayout mTabBar;
+
+ private DrawerLayout sideMenu;
+ private RelativeLayout sideMenuContent, quitLayout, defaultAccount;
+ private ListView accountsList, sideMenuItemList;
+ private ImageView menu;
+ private boolean fetchedContactsOnce = false;
+ private boolean doNotGoToCallActivity = false;
+ private List sideMenuItems;
+ private boolean callTransfer = false;
+
+ static final boolean isInstanciated() {
+ return instance != null;
+ }
+
+ public static final LinphoneActivity instance() {
+ if (instance != null)
+ return instance;
+ throw new RuntimeException("LinphoneActivity not instantiated yet");
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ //This must be done before calling super.onCreate().
+ super.onCreate(savedInstanceState);
+
+ if (getResources().getBoolean(R.bool.orientation_portrait_only)) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ if (!LinphoneManager.isInstanciated()) {
+ finish();
+ startActivity(getIntent().setClass(this, LinphoneLauncherActivity.class));
+ return;
+ }
+
+ boolean useFirstLoginActivity = getResources().getBoolean(R.bool.display_account_assistant_at_first_start);
+ if (LinphonePreferences.instance().isProvisioningLoginViewEnabled()) {
+ Intent wizard = new Intent();
+ wizard.setClass(this, RemoteProvisioningLoginActivity.class);
+ wizard.putExtra("Domain", LinphoneManager.getInstance().wizardLoginViewDomain);
+ startActivity(wizard);
+ finish();
+ return;
+ } else if (savedInstanceState == null && (useFirstLoginActivity && LinphonePreferences.instance().isFirstLaunch())) {
+ if (LinphonePreferences.instance().getAccountCount() > 0) {
+ LinphonePreferences.instance().firstLaunchSuccessful();
+ } else {
+ startActivity(new Intent().setClass(this, AssistantActivity.class));
+ finish();
+ return;
+ }
+ }
+
+ if (getIntent() != null && getIntent().getExtras() != null) {
+ newProxyConfig = getIntent().getExtras().getBoolean("isNewProxyConfig");
+ }
+
+ if (getResources().getBoolean(R.bool.use_linphone_tag)) {
+ if (getPackageManager().checkPermission(Manifest.permission.WRITE_SYNC_SETTINGS, getPackageName()) != PackageManager.PERMISSION_GRANTED) {
+ checkSyncPermission();
+ } else {
+ ContactsManager.getInstance().initializeSyncAccount(getApplicationContext(), getContentResolver());
+ }
+ } else {
+ ContactsManager.getInstance().initializeContactManager(getApplicationContext(), getContentResolver());
+ }
+
+ setContentView(R.layout.main1);
+ instance = this;
+ fragmentsHistory = new ArrayList();
+ pendingFragmentTransaction = FragmentsAvailable.UNKNOW;
+
+ initButtons();
+ initSideMenu();
+
+ currentFragment = FragmentsAvailable.EMPTY;
+ if (savedInstanceState == null) {
+ changeCurrentFragment(FragmentsAvailable.DIALER, getIntent().getExtras());
+ } else {
+ currentFragment = (FragmentsAvailable) savedInstanceState.getSerializable("currentFragment");
+ }
+
+ mListener = new LinphoneCoreListenerBase(){
+ @Override
+ public void messageReceived(LinphoneCore lc, LinphoneChatRoom cr, LinphoneChatMessage message) {
+ displayMissedChats(getUnreadMessageCount());
+ }
+
+ @Override
+ public void registrationState(LinphoneCore lc, LinphoneProxyConfig proxy, LinphoneCore.RegistrationState state, String smessage) {
+ if (state.equals(RegistrationState.RegistrationCleared)) {
+ if (lc != null) {
+ LinphoneAuthInfo authInfo = lc.findAuthInfo(proxy.getIdentity(), proxy.getRealm(), proxy.getDomain());
+ if (authInfo != null)
+ lc.removeAuthInfo(authInfo);
+ }
+ }
+
+ refreshAccounts();
+
+ if(getResources().getBoolean(R.bool.use_phone_number_validation)) {
+ if (state.equals(RegistrationState.RegistrationOk)) {
+ LinphoneManager.getInstance().isAccountWithAlias();
+ }
+ }
+
+ if(state.equals(RegistrationState.RegistrationFailed) && newProxyConfig) {
+ newProxyConfig = false;
+ if (proxy.getError() == Reason.BadCredentials) {
+ //displayCustomToast(getString(R.string.error_bad_credentials), Toast.LENGTH_LONG);
+ }
+ if (proxy.getError() == Reason.Unauthorized) {
+ displayCustomToast(getString(R.string.error_unauthorized), Toast.LENGTH_LONG);
+ }
+ if (proxy.getError() == Reason.IOError) {
+ displayCustomToast(getString(R.string.error_io_error), Toast.LENGTH_LONG);
+ }
+ }
+ }
+
+ @Override
+ public void callState(LinphoneCore lc, LinphoneCall call, LinphoneCall.State state, String message) {
+ if (state == State.IncomingReceived) {
+ startActivity(new Intent(LinphoneActivity.instance(), CallIncomingActivity.class));
+ } else if (state == State.OutgoingInit || state == State.OutgoingProgress) {
+ startActivity(new Intent(LinphoneActivity.instance(), CallOutgoingActivity.class));
+ } else if (state == State.CallEnd || state == State.Error || state == State.CallReleased) {
+ resetClassicMenuLayoutAndGoBackToCallIfStillRunning();
+ }
+
+ int missedCalls = LinphoneManager.getLc().getMissedCallsCount();
+ displayMissedCalls(missedCalls);
+ }
+ };
+
+ int missedCalls = LinphoneManager.getLc().getMissedCallsCount();
+ displayMissedCalls(missedCalls);
+
+ int rotation = getWindowManager().getDefaultDisplay().getRotation();
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ rotation = 0;
+ break;
+ case Surface.ROTATION_90:
+ rotation = 90;
+ break;
+ case Surface.ROTATION_180:
+ rotation = 180;
+ break;
+ case Surface.ROTATION_270:
+ rotation = 270;
+ break;
+ }
+
+ LinphoneManager.getLc().setDeviceRotation(rotation);
+ mAlwaysChangingPhoneAngle = rotation;
+ }
+
+ private void initButtons() {
+ mTabBar = (LinearLayout) findViewById(R.id.footer);
+ mTopBar = (RelativeLayout) findViewById(R.id.top_bar);
+
+ cancel = (ImageView) findViewById(R.id.cancel);
+ cancel.setOnClickListener(this);
+
+ history = (RelativeLayout) findViewById(R.id.history);
+ history.setOnClickListener(this);
+ contacts = (RelativeLayout) findViewById(R.id.contacts);
+ contacts.setOnClickListener(this);
+ dialer = (RelativeLayout) findViewById(R.id.dialer);
+ dialer.setOnClickListener(this);
+ chat = (RelativeLayout) findViewById(R.id.chat);
+ chat.setOnClickListener(this);
+
+ history_selected = findViewById(R.id.history_select);
+ contacts_selected = findViewById(R.id.contacts_select);
+ dialer_selected = findViewById(R.id.dialer_select);
+ chat_selected = findViewById(R.id.chat_select);
+
+ missedCalls = (TextView) findViewById(R.id.missed_calls);
+ missedChats = (TextView) findViewById(R.id.missed_chats);
+ }
+
+ private boolean isTablet() {
+ return getResources().getBoolean(R.bool.isTablet);
+ }
+
+ public void hideStatusBar() {
+ if (isTablet()) {
+ return;
+ }
+
+ findViewById(R.id.status).setVisibility(View.GONE);
+ }
+
+ public void showStatusBar() {
+ if (isTablet()) {
+ return;
+ }
+
+ if (statusFragment != null && !statusFragment.isVisible()) {
+ statusFragment.getView().setVisibility(View.VISIBLE);
+ }
+ findViewById(R.id.status).setVisibility(View.VISIBLE);
+ }
+
+ public void isNewProxyConfig(){
+ newProxyConfig = true;
+ }
+
+ private void changeCurrentFragment(FragmentsAvailable newFragmentType, Bundle extras) {
+ changeCurrentFragment(newFragmentType, extras, false);
+ }
+
+ private void changeCurrentFragment(FragmentsAvailable newFragmentType, Bundle extras, boolean withoutAnimation) {
+ if (newFragmentType == currentFragment && newFragmentType != FragmentsAvailable.CHAT) {
+ return;
+ }
+
+ if (currentFragment == FragmentsAvailable.DIALER) {
+ try {
+ DialerFragment dialerFragment = DialerFragment.instance();
+ dialerSavedState = getFragmentManager().saveFragmentInstanceState(dialerFragment);
+ } catch (Exception e) {
+ }
+ }
+
+ fragment = null;
+
+ switch (newFragmentType) {
+ case HISTORY_LIST:
+ fragment = new HistoryListFragment();
+ break;
+ case HISTORY_DETAIL:
+ fragment = new HistoryDetailFragment();
+ break;
+ case CONTACTS_LIST:
+ checkAndRequestReadContactsPermission();
+ fragment = new ContactsListFragment();
+ break;
+ case CONTACT_DETAIL:
+ fragment = new ContactDetailsFragment();
+ break;
+ case CONTACT_EDITOR:
+ fragment = new ContactEditorFragment();
+ break;
+ case DIALER:
+ fragment = new DialerFragment();
+ if (extras == null) {
+ fragment.setInitialSavedState(dialerSavedState);
+ }
+ break;
+ case SETTINGS:
+ fragment = new SettingsFragment();
+ break;
+ case ACCOUNT_SETTINGS:
+ fragment = new AccountPreferencesFragment();
+ break;
+ case ABOUT:
+ fragment = new AboutFragment();
+ break;
+ case EMPTY:
+ fragment = new EmptyFragment();
+ break;
+ case CHAT_LIST:
+ fragment = new ChatListFragment();
+ break;
+ case CHAT:
+ fragment = new ChatFragment();
+ break;
+ default:
+ break;
+ }
+
+ if (fragment != null) {
+ fragment.setArguments(extras);
+ if (isTablet()) {
+ changeFragmentForTablets(fragment, newFragmentType, withoutAnimation);
+ switch (newFragmentType) {
+ case HISTORY_LIST:
+ ((HistoryListFragment) fragment).displayFirstLog();
+ break;
+ case CONTACTS_LIST:
+ ((ContactsListFragment) fragment).displayFirstContact();
+ break;
+ case CHAT_LIST:
+ ((ChatListFragment) fragment).displayFirstChat();
+ break;
+ }
+ } else {
+ changeFragment(fragment, newFragmentType, withoutAnimation);
+ }
+ }
+ }
+
+ private void changeFragment(Fragment newFragment, FragmentsAvailable newFragmentType, boolean withoutAnimation) {
+ FragmentManager fm = getFragmentManager();
+ FragmentTransaction transaction = fm.beginTransaction();
+
+ /*if (!withoutAnimation && !isAnimationDisabled && currentFragment.shouldAnimate()) {
+ if (newFragmentType.isRightOf(currentFragment)) {
+ transaction.setCustomAnimations(R.anim.slide_in_right_to_left,
+ R.anim.slide_out_right_to_left,
+ R.anim.slide_in_left_to_right,
+ R.anim.slide_out_left_to_right);
+ } else {
+ transaction.setCustomAnimations(R.anim.slide_in_left_to_right,
+ R.anim.slide_out_left_to_right,
+ R.anim.slide_in_right_to_left,
+ R.anim.slide_out_right_to_left);
+ }
+ }*/
+
+ if (newFragmentType != FragmentsAvailable.DIALER
+ && newFragmentType != FragmentsAvailable.CONTACTS_LIST
+ && newFragmentType != FragmentsAvailable.CHAT_LIST
+ && newFragmentType != FragmentsAvailable.HISTORY_LIST) {
+ transaction.addToBackStack(newFragmentType.toString());
+ } else {
+ while (fm.getBackStackEntryCount() > 0) {
+ fm.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ }
+ }
+
+ transaction.replace(R.id.fragmentContainer, newFragment, newFragmentType.toString());
+ transaction.commitAllowingStateLoss();
+ fm.executePendingTransactions();
+
+ currentFragment = newFragmentType;
+ }
+
+ private void changeFragmentForTablets(Fragment newFragment, FragmentsAvailable newFragmentType, boolean withoutAnimation) {
+ if (getResources().getBoolean(R.bool.show_statusbar_only_on_dialer)) {
+ if (newFragmentType == FragmentsAvailable.DIALER) {
+ showStatusBar();
+ } else {
+ hideStatusBar();
+ }
+ }
+ emptyFragment = false;
+ LinearLayout ll = (LinearLayout) findViewById(R.id.fragmentContainer2);
+
+ FragmentTransaction transaction = getFragmentManager().beginTransaction();
+
+ if(newFragmentType == FragmentsAvailable.EMPTY){
+ ll.setVisibility(View.VISIBLE);
+ emptyFragment = true;
+ transaction.replace(R.id.fragmentContainer2, newFragment);
+ transaction.commitAllowingStateLoss();
+ getFragmentManager().executePendingTransactions();
+ } else {
+ if (newFragmentType.shouldAddItselfToTheRightOf(currentFragment)) {
+ ll.setVisibility(View.VISIBLE);
+
+ if (newFragmentType == FragmentsAvailable.CONTACT_EDITOR) {
+ transaction.addToBackStack(newFragmentType.toString());
+ }
+ transaction.replace(R.id.fragmentContainer2, newFragment);
+ } else {
+ if (newFragmentType == FragmentsAvailable.EMPTY) {
+ ll.setVisibility(View.VISIBLE);
+ transaction.replace(R.id.fragmentContainer2, new EmptyFragment());
+ emptyFragment = true;
+ }
+
+ if (newFragmentType == FragmentsAvailable.DIALER
+ || newFragmentType == FragmentsAvailable.ABOUT
+ || newFragmentType == FragmentsAvailable.SETTINGS
+ || newFragmentType == FragmentsAvailable.ACCOUNT_SETTINGS) {
+ ll.setVisibility(View.GONE);
+ } else {
+ ll.setVisibility(View.VISIBLE);
+ transaction.replace(R.id.fragmentContainer2, new EmptyFragment());
+ }
+
+ /*if (!withoutAnimation && !isAnimationDisabled && currentFragment.shouldAnimate()) {
+ if (newFragmentType.isRightOf(currentFragment)) {
+ transaction.setCustomAnimations(R.anim.slide_in_right_to_left, R.anim.slide_out_right_to_left, R.anim.slide_in_left_to_right, R.anim.slide_out_left_to_right);
+ } else {
+ transaction.setCustomAnimations(R.anim.slide_in_left_to_right, R.anim.slide_out_left_to_right, R.anim.slide_in_right_to_left, R.anim.slide_out_right_to_left);
+ }
+ }*/
+ transaction.replace(R.id.fragmentContainer, newFragment);
+ }
+ transaction.commitAllowingStateLoss();
+ getFragmentManager().executePendingTransactions();
+
+ currentFragment = newFragmentType;
+ if (newFragmentType == FragmentsAvailable.DIALER
+ || newFragmentType == FragmentsAvailable.SETTINGS
+ || newFragmentType == FragmentsAvailable.CONTACTS_LIST
+ || newFragmentType == FragmentsAvailable.CHAT_LIST
+ || newFragmentType == FragmentsAvailable.HISTORY_LIST) {
+ try {
+ getFragmentManager().popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
+ } catch (IllegalStateException e) {
+
+ }
+ }
+ fragmentsHistory.add(currentFragment);
+ }
+ }
+
+ public void displayHistoryDetail(String sipUri, LinphoneCallLog log) {
+ LinphoneAddress lAddress;
+ try {
+ lAddress = LinphoneCoreFactory.instance().createLinphoneAddress(sipUri);
+ } catch (LinphoneCoreException e) {
+ Log.e("Cannot display history details",e);
+ //TODO display error message
+ return;
+ }
+ LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(lAddress);
+
+ String displayName = c != null ? c.getFullName() : LinphoneUtils.getAddressDisplayName(sipUri);
+ String pictureUri = c != null && c.getPhotoUri() != null ? c.getPhotoUri().toString() : null;
+
+ String status;
+ if (log.getDirection() == CallDirection.Outgoing) {
+ status = getString(R.string.outgoing);
+ } else {
+ if (log.getStatus() == CallStatus.Missed) {
+ status = getString(R.string.missed);
+ } else {
+ status = getString(R.string.incoming);
+ }
+ }
+
+ String callTime = secondsToDisplayableString(log.getCallDuration());
+ String callDate = String.valueOf(log.getTimestamp());
+
+ Fragment fragment2 = getFragmentManager().findFragmentById(R.id.fragmentContainer2);
+ if (fragment2 != null && fragment2.isVisible() && currentFragment == FragmentsAvailable.HISTORY_DETAIL) {
+ HistoryDetailFragment historyDetailFragment = (HistoryDetailFragment) fragment2;
+ historyDetailFragment.changeDisplayedHistory(lAddress.asStringUriOnly(), displayName, pictureUri, status, callTime, callDate);
+ } else {
+ Bundle extras = new Bundle();
+ extras.putString("SipUri", lAddress.asString());
+ if (displayName != null) {
+ extras.putString("DisplayName", displayName);
+ extras.putString("PictureUri", pictureUri);
+ }
+ extras.putString("CallStatus", status);
+ extras.putString("CallTime", callTime);
+ extras.putString("CallDate", callDate);
+
+ changeCurrentFragment(FragmentsAvailable.HISTORY_DETAIL, extras);
+ }
+ }
+
+ public void displayEmptyFragment(){
+ changeCurrentFragment(FragmentsAvailable.EMPTY, new Bundle());
+ }
+
+ @SuppressLint("SimpleDateFormat")
+ private String secondsToDisplayableString(int secs) {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
+ Calendar cal = Calendar.getInstance();
+ cal.set(0, 0, 0, 0, 0, secs);
+ return dateFormat.format(cal.getTime());
+ }
+
+ public void displayContact(LinphoneContact contact, boolean chatOnly) {
+ Fragment fragment2 = getFragmentManager().findFragmentById(R.id.fragmentContainer2);
+ if (fragment2 != null && fragment2.isVisible() && currentFragment == FragmentsAvailable.CONTACT_DETAIL) {
+ ContactDetailsFragment contactFragment = (ContactDetailsFragment) fragment2;
+ contactFragment.changeDisplayedContact(contact);
+ } else {
+ Bundle extras = new Bundle();
+ extras.putSerializable("Contact", contact);
+ extras.putBoolean("ChatAddressOnly", chatOnly);
+ changeCurrentFragment(FragmentsAvailable.CONTACT_DETAIL, extras);
+ }
+ }
+
+ public void displayContacts(boolean chatOnly) {
+ Bundle extras = new Bundle();
+ extras.putBoolean("ChatAddressOnly", chatOnly);
+ changeCurrentFragment(FragmentsAvailable.CONTACTS_LIST, extras);
+ }
+
+ public void displayContactsForEdition(String sipAddress) {
+ Bundle extras = new Bundle();
+ extras.putBoolean("EditOnClick", true);
+ extras.putString("SipAddress", sipAddress);
+ changeCurrentFragment(FragmentsAvailable.CONTACTS_LIST, extras);
+ }
+
+ public void displayAbout() {
+ changeCurrentFragment(FragmentsAvailable.ABOUT, null);
+ }
+
+ public void displayAssistant() {
+ startActivity(new Intent(LinphoneActivity.this, AssistantActivity.class));
+ }
+
+
+
+ public void displayInapp() {
+ startActivity(new Intent(LinphoneActivity.this, InAppPurchaseActivity.class));
+ }
+
+ public int getUnreadMessageCount() {
+ int count = 0;
+ LinphoneChatRoom[] chats = LinphoneManager.getLc().getChatRooms();
+ for (LinphoneChatRoom chatroom : chats) {
+ count += chatroom.getUnreadMessagesCount();
+ }
+ return count;
+ }
+
+ public void displayChat(String sipUri) {
+ if (getResources().getBoolean(R.bool.disable_chat)) {
+ return;
+ }
+
+ String pictureUri = null;
+ String thumbnailUri = null;
+ String displayName = null;
+
+ LinphoneAddress lAddress = null;
+ if(sipUri != null) {
+ try {
+ lAddress = LinphoneManager.getLc().interpretUrl(sipUri);
+ } catch (LinphoneCoreException e) {
+ //TODO display error message
+ Log.e("Cannot display chat", e);
+ return;
+ }
+ LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(lAddress);
+ displayName = contact != null ? contact.getFullName() : null;
+
+ if (contact != null && contact.getPhotoUri() != null) {
+ pictureUri = contact.getPhotoUri().toString();
+ thumbnailUri = contact.getThumbnailUri().toString();
+ }
+ }
+
+ if (currentFragment == FragmentsAvailable.CHAT_LIST || currentFragment == FragmentsAvailable.CHAT) {
+ Fragment fragment2 = getFragmentManager().findFragmentById(R.id.fragmentContainer2);
+ if (fragment2 != null && fragment2.isVisible() && currentFragment == FragmentsAvailable.CHAT && !emptyFragment) {
+ ChatFragment chatFragment = (ChatFragment) fragment2;
+ chatFragment.changeDisplayedChat(sipUri, displayName, pictureUri);
+ } else {
+ Bundle extras = new Bundle();
+ extras.putString("SipUri", sipUri);
+ if (sipUri != null && lAddress.getDisplayName() != null) {
+ extras.putString("DisplayName", displayName);
+ extras.putString("PictureUri", pictureUri);
+ extras.putString("ThumbnailUri", thumbnailUri);
+ }
+ changeCurrentFragment(FragmentsAvailable.CHAT, extras);
+ }
+ } else {
+ if(isTablet()){
+ changeCurrentFragment(FragmentsAvailable.CHAT_LIST, null);
+ displayChat(sipUri);
+ } else {
+ Bundle extras = new Bundle();
+ extras.putString("SipUri", sipUri);
+ if (sipUri != null && lAddress.getDisplayName() != null) {
+ extras.putString("DisplayName", displayName);
+ extras.putString("PictureUri", pictureUri);
+ extras.putString("ThumbnailUri", thumbnailUri);
+ }
+ changeCurrentFragment(FragmentsAvailable.CHAT, extras);
+ }
+ }
+
+ LinphoneService.instance().resetMessageNotifCount();
+ LinphoneService.instance().removeMessageNotification();
+ displayMissedChats(getUnreadMessageCount());
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ resetSelection();
+
+ if (id == R.id.history) {
+ changeCurrentFragment(FragmentsAvailable.HISTORY_LIST, null);
+ history_selected.setVisibility(View.VISIBLE);
+ LinphoneManager.getLc().resetMissedCallsCount();
+ displayMissedCalls(0);
+ } else if (id == R.id.contacts) {
+ changeCurrentFragment(FragmentsAvailable.CONTACTS_LIST, null);
+ contacts_selected.setVisibility(View.VISIBLE);
+ } else if (id == R.id.dialer) {
+ changeCurrentFragment(FragmentsAvailable.DIALER, null);
+ dialer_selected.setVisibility(View.VISIBLE);
+ } else if (id == R.id.chat) {
+ changeCurrentFragment(FragmentsAvailable.CHAT_LIST, null);
+ chat_selected.setVisibility(View.VISIBLE);
+ } else if (id == R.id.cancel){
+ hideTopBar();
+ displayDialer();
+ }
+ }
+
+ private void resetSelection() {
+ history_selected.setVisibility(View.GONE);
+ contacts_selected.setVisibility(View.GONE);
+ dialer_selected.setVisibility(View.GONE);
+ chat_selected.setVisibility(View.GONE);
+ }
+
+ public void hideTabBar(Boolean hide) {
+ if(hide){
+ mTabBar.setVisibility(View.GONE);
+ } else {
+ mTabBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void hideTopBar() {
+ mTopBar.setVisibility(View.GONE);
+ }
+
+ @SuppressWarnings("incomplete-switch")
+ public void selectMenu(FragmentsAvailable menuToSelect) {
+ currentFragment = menuToSelect;
+ resetSelection();
+
+ switch (menuToSelect) {
+ case HISTORY_LIST:
+ case HISTORY_DETAIL:
+ history_selected.setVisibility(View.VISIBLE);
+ break;
+ case CONTACTS_LIST:
+ case CONTACT_DETAIL:
+ case CONTACT_EDITOR:
+ contacts_selected.setVisibility(View.VISIBLE);
+ break;
+ case DIALER:
+ dialer_selected.setVisibility(View.VISIBLE);
+ break;
+ case SETTINGS:
+ case ACCOUNT_SETTINGS:
+ hideTabBar(true);
+ mTopBar.setVisibility(View.VISIBLE);
+ break;
+ case ABOUT:
+ hideTabBar(true);
+ break;
+ case CHAT_LIST:
+ case CHAT:
+ chat_selected.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+
+ public void updateDialerFragment(DialerFragment fragment) {
+ // Hack to maintain soft input flags
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
+ }
+
+ public void goToDialerFragment() {
+ changeCurrentFragment(FragmentsAvailable.DIALER, null);
+ dialer_selected.setVisibility(View.VISIBLE);
+ }
+
+ public void onMessageSent(String to, String message) {
+ Fragment fragment = getFragmentManager().findFragmentById(R.id.fragmentContainer);
+ if (fragment.getClass() == ChatListFragment.class) {
+ ((ChatListFragment)fragment).refresh();
+ }
+ }
+
+ public void updateStatusFragment(StatusFragment fragment) {
+ statusFragment = fragment;
+ }
+
+ public void displaySettings() {
+ changeCurrentFragment(FragmentsAvailable.SETTINGS, null);
+ }
+
+ public void displayDialer() {
+ changeCurrentFragment(FragmentsAvailable.DIALER, null);
+ }
+
+ public void displayAccountSettings(int accountNumber) {
+ Bundle bundle = new Bundle();
+ bundle.putInt("Account", accountNumber);
+ changeCurrentFragment(FragmentsAvailable.ACCOUNT_SETTINGS, bundle);
+ //settings.setSelected(true);
+ }
+
+ public StatusFragment getStatusFragment() {
+ return statusFragment;
+ }
+
+ public List getChatList() {
+ ArrayList chatList = new ArrayList();
+
+ LinphoneChatRoom[] chats = LinphoneManager.getLc().getChatRooms();
+ List rooms = new ArrayList();
+
+ for (LinphoneChatRoom chatroom : chats) {
+ if (chatroom.getHistorySize() > 0) {
+ rooms.add(chatroom);
+ }
+ }
+
+ if (rooms.size() > 1) {
+ Collections.sort(rooms, new Comparator() {
+ @Override
+ public int compare(LinphoneChatRoom a, LinphoneChatRoom b) {
+ LinphoneChatMessage[] messagesA = a.getHistory(1);
+ LinphoneChatMessage[] messagesB = b.getHistory(1);
+ long atime = messagesA[0].getTime();
+ long btime = messagesB[0].getTime();
+
+ if (atime > btime)
+ return -1;
+ else if (btime > atime)
+ return 1;
+ else
+ return 0;
+ }
+ });
+ }
+
+ for (LinphoneChatRoom chatroom : rooms) {
+ chatList.add(chatroom.getPeerAddress().asStringUriOnly());
+ }
+
+ return chatList;
+ }
+
+ public void removeFromChatList(String sipUri) {
+ LinphoneChatRoom chatroom = LinphoneManager.getLc().getOrCreateChatRoom(sipUri);
+ chatroom.deleteHistory();
+ }
+
+ public void updateMissedChatCount() {
+ displayMissedChats(getUnreadMessageCount());
+ }
+
+ public void displayMissedCalls(final int missedCallsCount) {
+ if (missedCallsCount > 0) {
+ missedCalls.setText(missedCallsCount + "");
+ missedCalls.setVisibility(View.VISIBLE);
+ } else {
+ LinphoneManager.getLc().resetMissedCallsCount();
+ missedCalls.clearAnimation();
+ missedCalls.setVisibility(View.GONE);
+ }
+ }
+
+ private void displayMissedChats(final int missedChatCount) {
+ if (missedChatCount > 0) {
+ missedChats.setText(missedChatCount + "");
+ missedChats.setVisibility(View.VISIBLE);
+ } else {
+ missedChats.clearAnimation();
+ missedChats.setVisibility(View.GONE);
+ }
+ }
+
+ public void displayCustomToast(final String message, final int duration) {
+ LayoutInflater inflater = getLayoutInflater();
+ View layout = inflater.inflate(R.layout.toast, (ViewGroup) findViewById(R.id.toastRoot));
+
+ TextView toastText = (TextView) layout.findViewById(R.id.toastMessage);
+ toastText.setText(message);
+
+ final Toast toast = new Toast(getApplicationContext());
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.setDuration(duration);
+ toast.setView(layout);
+ toast.show();
+ }
+
+ public Dialog displayDialog(String text){
+ Dialog dialog = new Dialog(this);
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.colorC));
+ d.setAlpha(200);
+ dialog.setContentView(R.layout.dialog);
+ dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
+ dialog.getWindow().setBackgroundDrawable(d);
+
+ TextView customText = (TextView) dialog.findViewById(R.id.customText);
+ customText.setText(text);
+ return dialog;
+ }
+
+ public Dialog displayWrongPasswordDialog(final String username, final String realm, final String domain){
+ final Dialog dialog = new Dialog(this);
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.colorC));
+ d.setAlpha(200);
+ dialog.setContentView(R.layout.input_dialog);
+ dialog.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
+ dialog.getWindow().setBackgroundDrawable(d);
+
+ TextView customText = (TextView) dialog.findViewById(R.id.customText);
+ customText.setText(getString(R.string.error_bad_credentials));
+
+ Button retry = (Button) dialog.findViewById(R.id.retry);
+ Button cancel = (Button) dialog.findViewById(R.id.cancel);
+
+ retry.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ String newPassword = ((EditText) dialog.findViewById(R.id.password)).getText().toString();
+ LinphoneAuthInfo authInfo = LinphoneCoreFactory.instance().createAuthInfo(username, null, newPassword, null, realm, domain);
+ LinphoneManager.getLc().addAuthInfo(authInfo);
+ LinphoneManager.getLc().refreshRegisters();
+ dialog.dismiss();
+ }
+ });
+
+ cancel.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ dialog.dismiss();
+ }
+ });
+
+ return dialog;
+ }
+
+ @Override
+ public void setAddresGoToDialerAndCall(String number, String name, Uri photo) {
+// Bundle extras = new Bundle();
+// extras.putString("SipUri", number);
+// extras.putString("DisplayName", name);
+// extras.putString("Photo", photo == null ? null : photo.toString());
+// changeCurrentFragment(FragmentsAvailable.DIALER, extras);
+
+ AddressType address = new AddressText(this, null);
+ address.setDisplayedName(name);
+ address.setText(number);
+ LinphoneManager.getInstance().newOutgoingCall(address);
+ }
+
+ public void startIncallActivity(LinphoneCall currentCall) {
+ Intent intent = new Intent(this, CallActivity.class);
+ startOrientationSensor();
+ startActivityForResult(intent, CALL_ACTIVITY);
+ }
+
+ /**
+ * Register a sensor to track phoneOrientation changes
+ */
+ private synchronized void startOrientationSensor() {
+ if (mOrientationHelper == null) {
+ mOrientationHelper = new LocalOrientationEventListener(this);
+ }
+ mOrientationHelper.enable();
+ }
+
+ private int mAlwaysChangingPhoneAngle = -1;
+
+ private class LocalOrientationEventListener extends OrientationEventListener {
+ public LocalOrientationEventListener(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onOrientationChanged(final int o) {
+ if (o == OrientationEventListener.ORIENTATION_UNKNOWN) {
+ return;
+ }
+
+ int degrees = 270;
+ if (o < 45 || o > 315)
+ degrees = 0;
+ else if (o < 135)
+ degrees = 90;
+ else if (o < 225)
+ degrees = 180;
+
+ if (mAlwaysChangingPhoneAngle == degrees) {
+ return;
+ }
+ mAlwaysChangingPhoneAngle = degrees;
+
+ Log.d("Phone orientation changed to ", degrees);
+ int rotation = (360 - degrees) % 360;
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.setDeviceRotation(rotation);
+ LinphoneCall currentCall = lc.getCurrentCall();
+ if (currentCall != null && currentCall.cameraEnabled() && currentCall.getCurrentParamsCopy().getVideoEnabled()) {
+ lc.updateCall(currentCall, null);
+ }
+ }
+ }
+ }
+
+ public Boolean isCallTransfer(){
+ return callTransfer;
+ }
+
+ private void initInCallMenuLayout(final boolean callTransfer) {
+ selectMenu(FragmentsAvailable.DIALER);
+ DialerFragment dialerFragment = DialerFragment.instance();
+ if (dialerFragment != null) {
+ ((DialerFragment) dialerFragment).resetLayout(callTransfer);
+ }
+ }
+
+ public void resetClassicMenuLayoutAndGoBackToCallIfStillRunning() {
+ DialerFragment dialerFragment = DialerFragment.instance();
+ if (dialerFragment != null) {
+ ((DialerFragment) dialerFragment).resetLayout(true);
+ }
+
+ if (LinphoneManager.isInstanciated() && LinphoneManager.getLc().getCallsNb() > 0) {
+ LinphoneCall call = LinphoneManager.getLc().getCalls()[0];
+ if (call.getState() == LinphoneCall.State.IncomingReceived) {
+ startActivity(new Intent(LinphoneActivity.this, CallIncomingActivity.class));
+ } else {
+ startIncallActivity(call);
+ }
+ }
+ }
+
+ public FragmentsAvailable getCurrentFragment() {
+ return currentFragment;
+ }
+
+ public void addContact(String displayName, String sipUri)
+ {
+ Bundle extras = new Bundle();
+ extras.putSerializable("NewSipAdress", sipUri);
+ changeCurrentFragment(FragmentsAvailable.CONTACT_EDITOR, extras);
+ }
+
+ public void editContact(LinphoneContact contact)
+ {
+ Bundle extras = new Bundle();
+ extras.putSerializable("Contact", contact);
+ changeCurrentFragment(FragmentsAvailable.CONTACT_EDITOR, extras);
+ }
+
+ public void editContact(LinphoneContact contact, String sipAddress)
+ {
+ Bundle extras = new Bundle();
+ extras.putSerializable("Contact", contact);
+ extras.putSerializable("NewSipAdress", sipAddress);
+ changeCurrentFragment(FragmentsAvailable.CONTACT_EDITOR, extras);
+ }
+
+ public void quit() {
+ finish();
+ stopService(new Intent(Intent.ACTION_MAIN).setClass(this, LinphoneService.class));
+ }
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ if (pendingFragmentTransaction != FragmentsAvailable.UNKNOW) {
+ changeCurrentFragment(pendingFragmentTransaction, null, true);
+ selectMenu(pendingFragmentTransaction);
+ pendingFragmentTransaction = FragmentsAvailable.UNKNOW;
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_FIRST_USER && requestCode == SETTINGS_ACTIVITY) {
+ if (data.getExtras().getBoolean("Exit", false)) {
+ quit();
+ } else {
+ pendingFragmentTransaction = (FragmentsAvailable) data.getExtras().getSerializable("FragmentToDisplay");
+ }
+ } else if (resultCode == Activity.RESULT_FIRST_USER && requestCode == CALL_ACTIVITY) {
+ getIntent().putExtra("PreviousActivity", CALL_ACTIVITY);
+ callTransfer = data == null ? false : data.getBooleanExtra("Transfer", false);
+ boolean chat = data == null ? false : data.getBooleanExtra("chat", false);
+ if(chat){
+ pendingFragmentTransaction = FragmentsAvailable.CHAT_LIST;
+ }
+ if (LinphoneManager.getLc().getCallsNb() > 0) {
+ initInCallMenuLayout(callTransfer);
+ } else {
+ resetClassicMenuLayoutAndGoBackToCallIfStillRunning();
+ }
+ } else if (requestCode == PERMISSIONS_REQUEST_OVERLAY) {
+ if (Compatibility.canDrawOverlays(this)) {
+ LinphonePreferences.instance().enableOverlay(true);
+ }
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ getIntent().putExtra("PreviousActivity", 0);
+
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.removeListener(mListener);
+ }
+ callTransfer = false;
+
+ super.onPause();
+ }
+
+ public boolean checkAndRequestOverlayPermission() {
+ Log.i("[Permission] Draw overlays permission is " + (Compatibility.canDrawOverlays(this) ? "granted" : "denied"));
+ if (!Compatibility.canDrawOverlays(this)) {
+ Log.i("[Permission] Asking for overlay");
+ Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
+ startActivityForResult(intent, PERMISSIONS_REQUEST_OVERLAY);
+ return false;
+ }
+ return true;
+ }
+
+ public void checkAndRequestReadPhoneStatePermission() {
+ checkAndRequestPermission(Manifest.permission.READ_PHONE_STATE, 0);
+ }
+
+ public void checkAndRequestReadExternalStoragePermission() {
+ checkAndRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, 0);
+ }
+
+ public void checkAndRequestExternalStoragePermission() {
+ checkAndRequestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, 0);
+ }
+
+ public void checkAndRequestCameraPermission() {
+ checkAndRequestPermission(Manifest.permission.CAMERA, 0);
+ }
+
+ public void checkAndRequestReadContactsPermission() {
+ checkAndRequestPermission(Manifest.permission.READ_CONTACTS, PERMISSIONS_REQUEST_CONTACTS);
+ }
+
+ public void checkAndRequestInappPermission() {
+ checkAndRequestPermission(Manifest.permission.GET_ACCOUNTS, PERMISSIONS_REQUEST_CONTACTS);
+ }
+
+ public void checkAndRequestWriteContactsPermission() {
+ checkAndRequestPermission(Manifest.permission.WRITE_CONTACTS, 0);
+ }
+
+ public void checkAndRequestRecordAudioPermissionForEchoCanceller() {
+ checkAndRequestPermission(Manifest.permission.RECORD_AUDIO, PERMISSIONS_RECORD_AUDIO_ECHO_CANCELLER);
+ }
+
+ public void checkAndRequestRecordAudioPermissionsForEchoTester() {
+ checkAndRequestPermission(Manifest.permission.RECORD_AUDIO, PERMISSIONS_RECORD_AUDIO_ECHO_TESTER);
+ }
+
+ public void checkAndRequestReadExternalStoragePermissionForDeviceRingtone() {
+ checkAndRequestPermission(Manifest.permission.READ_EXTERNAL_STORAGE, PERMISSIONS_READ_EXTERNAL_STORAGE_DEVICE_RINGTONE);
+ }
+
+ public void checkAndRequestPermissionsToSendImage() {
+ ArrayList permissionsList = new ArrayList();
+
+ int readExternalStorage = getPackageManager().checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE, getPackageName());
+ Log.i("[Permission] Read external storage permission is " + (readExternalStorage == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+ int camera = getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName());
+ Log.i("[Permission] Camera permission is " + (camera == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ if (readExternalStorage != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(Manifest.permission.READ_EXTERNAL_STORAGE) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
+ Log.i("[Permission] Asking for read external storage");
+ permissionsList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
+ }
+ }
+ if (camera != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(Manifest.permission.CAMERA) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
+ Log.i("[Permission] Asking for camera");
+ permissionsList.add(Manifest.permission.CAMERA);
+ }
+ }
+ if (permissionsList.size() > 0) {
+ String[] permissions = new String[permissionsList.size()];
+ permissions = permissionsList.toArray(permissions);
+ ActivityCompat.requestPermissions(this, permissions, 0);
+ }
+ }
+
+ private void checkSyncPermission() {
+ checkAndRequestPermission(Manifest.permission.WRITE_SYNC_SETTINGS, PERMISSIONS_REQUEST_SYNC);
+ }
+
+ public void checkAndRequestPermission(String permission, int result) {
+ int permissionGranted = getPackageManager().checkPermission(permission, getPackageName());
+ Log.i("[Permission] " + permission + " is " + (permissionGranted == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ if (permissionGranted != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(permission) || ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
+ Log.i("[Permission] Asking for " + permission);
+ ActivityCompat.requestPermissions(this, new String[] { permission }, result);
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ if (permissions.length <= 0)
+ return;
+
+ int readContactsI = -1;
+ for (int i = 0; i < permissions.length; i++) {
+ Log.i("[Permission] " + permissions[i] + " is " + (grantResults[i] == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+ if (permissions[i] == Manifest.permission.READ_CONTACTS)
+ readContactsI = i;
+ }
+
+ switch (requestCode) {
+ case PERMISSIONS_REQUEST_SYNC:
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ ContactsManager.getInstance().initializeSyncAccount(getApplicationContext(), getContentResolver());
+ } else {
+ ContactsManager.getInstance().initializeContactManager(getApplicationContext(), getContentResolver());
+ }
+ break;
+ case PERMISSIONS_RECORD_AUDIO_ECHO_CANCELLER:
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ ((SettingsFragment) fragment).startEchoCancellerCalibration();
+ } else {
+ ((SettingsFragment) fragment).echoCalibrationFail();
+ }
+ break;
+ case PERMISSIONS_READ_EXTERNAL_STORAGE_DEVICE_RINGTONE:
+ if (readContactsI >= 0 && grantResults[readContactsI] == PackageManager.PERMISSION_GRANTED) {
+ ContactsManager.getInstance().enableContactsAccess();
+ }
+ if (!fetchedContactsOnce) {
+ ContactsManager.getInstance().enableContactsAccess();
+ ContactsManager.getInstance().fetchContactsAsync();
+ fetchedContactsOnce = true;
+ }
+ if (permissions[0].compareTo(Manifest.permission.READ_EXTERNAL_STORAGE) != 0)
+ break;
+ boolean enableRingtone = (grantResults[0] == PackageManager.PERMISSION_GRANTED);
+ LinphonePreferences.instance().enableDeviceRingtone(enableRingtone);
+ LinphoneManager.getInstance().enableDeviceRingtone(enableRingtone);
+ break;
+ case PERMISSIONS_RECORD_AUDIO_ECHO_TESTER:
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
+ ((SettingsFragment) fragment).startEchoTester();
+ break;
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ ArrayList permissionsList = new ArrayList();
+
+ int contacts = getPackageManager().checkPermission(Manifest.permission.READ_CONTACTS, getPackageName());
+ Log.i("[Permission] Contacts permission is " + (contacts == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ int readPhone = getPackageManager().checkPermission(Manifest.permission.READ_PHONE_STATE, getPackageName());
+ Log.i("[Permission] Read phone state permission is " + (readPhone == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ int ringtone = getPackageManager().checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE, getPackageName());
+ Log.i("[Permission] Read external storage for ring tone permission is " + (ringtone == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
+
+ if (ringtone != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(Manifest.permission.READ_EXTERNAL_STORAGE) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) {
+ Log.i("[Permission] Asking for read external storage for ring tone");
+ permissionsList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
+ }
+ }
+ if (readPhone != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(Manifest.permission.READ_PHONE_STATE) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_PHONE_STATE)) {
+ Log.i("[Permission] Asking for read phone state");
+ permissionsList.add(Manifest.permission.READ_PHONE_STATE);
+ }
+ }
+ if (contacts != PackageManager.PERMISSION_GRANTED) {
+ if (LinphonePreferences.instance().firstTimeAskingForPermission(Manifest.permission.READ_CONTACTS) || ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_CONTACTS)) {
+ Log.i("[Permission] Asking for contacts");
+ permissionsList.add(Manifest.permission.READ_CONTACTS);
+ }
+ } else {
+ if (!fetchedContactsOnce) {
+ ContactsManager.getInstance().enableContactsAccess();
+ ContactsManager.getInstance().fetchContactsAsync();
+ fetchedContactsOnce = true;
+ }
+ }
+
+ if (permissionsList.size() > 0) {
+ String[] permissions = new String[permissionsList.size()];
+ permissions = permissionsList.toArray(permissions);
+ ActivityCompat.requestPermissions(this, permissions, PERMISSIONS_READ_EXTERNAL_STORAGE_DEVICE_RINGTONE);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ outState.putSerializable("currentFragment", currentFragment);
+ outState.putBoolean("fetchedContactsOnce", fetchedContactsOnce);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ fetchedContactsOnce = savedInstanceState.getBoolean("fetchedContactsOnce");
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (!LinphoneService.isReady()) {
+ startService(new Intent(Intent.ACTION_MAIN).setClass(this, LinphoneService.class));
+ }
+
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc != null) {
+ lc.addListener(mListener);
+ }
+
+ if (isTablet()) {
+ // Prevent fragmentContainer2 to be visible when rotating the device
+ LinearLayout ll = (LinearLayout) findViewById(R.id.fragmentContainer2);
+ if (currentFragment == FragmentsAvailable.DIALER
+ || currentFragment == FragmentsAvailable.ABOUT
+ || currentFragment == FragmentsAvailable.SETTINGS
+ || currentFragment == FragmentsAvailable.ACCOUNT_SETTINGS) {
+ ll.setVisibility(View.GONE);
+ }
+ }
+
+ refreshAccounts();
+
+ if(getResources().getBoolean(R.bool.enable_in_app_purchase)){
+ isTrialAccount();
+ }
+
+ updateMissedChatCount();
+ if(LinphonePreferences.instance().isFriendlistsubscriptionEnabled() && LinphoneManager.getLc().getDefaultProxyConfig() != null){
+ LinphoneManager.getInstance().subscribeFriendList(true);
+ } else {
+ LinphoneManager.getInstance().subscribeFriendList(false);
+ }
+
+ displayMissedCalls(LinphoneManager.getLc().getMissedCallsCount());
+
+ LinphoneManager.getInstance().changeStatusToOnline();
+
+ if (getIntent().getIntExtra("PreviousActivity", 0) != CALL_ACTIVITY && !doNotGoToCallActivity) {
+ if (LinphoneManager.getLc().getCalls().length > 0) {
+ LinphoneCall call = LinphoneManager.getLc().getCalls()[0];
+ LinphoneCall.State callState = call.getState();
+
+ if (callState == State.IncomingReceived) {
+ startActivity(new Intent(this, CallIncomingActivity.class));
+ } else if (callState == State.OutgoingInit || callState == State.OutgoingProgress || callState == State.OutgoingRinging) {
+ startActivity(new Intent(this, CallOutgoingActivity.class));
+ } else {
+ startIncallActivity(call);
+ }
+ }
+ }
+ doNotGoToCallActivity = false;
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mOrientationHelper != null) {
+ mOrientationHelper.disable();
+ mOrientationHelper = null;
+ }
+
+ instance = null;
+ super.onDestroy();
+
+ unbindDrawables(findViewById(R.id.topLayout));
+ System.gc();
+ }
+
+ private void unbindDrawables(View view) {
+ if (view != null && view.getBackground() != null) {
+ view.getBackground().setCallback(null);
+ }
+ if (view instanceof ViewGroup && !(view instanceof AdapterView)) {
+ for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
+ unbindDrawables(((ViewGroup) view).getChildAt(i));
+ }
+ ((ViewGroup) view).removeAllViews();
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+
+ Bundle extras = intent.getExtras();
+ if (extras != null && extras.getBoolean("GoToChat", false)) {
+ LinphoneService.instance().removeMessageNotification();
+ String sipUri = extras.getString("ChatContactSipUri");
+ doNotGoToCallActivity = true;
+ displayChat(sipUri);
+ } else if (extras != null && extras.getBoolean("GoToHistory", false)) {
+ doNotGoToCallActivity = true;
+ changeCurrentFragment(FragmentsAvailable.HISTORY_LIST, null);
+ } else if (extras != null && extras.getBoolean("GoToInapp", false)) {
+ LinphoneService.instance().removeMessageNotification();
+ doNotGoToCallActivity = true;
+ displayInapp();
+ } else if (extras != null && extras.getBoolean("Notification", false)) {
+ if (LinphoneManager.getLc().getCallsNb() > 0) {
+ LinphoneCall call = LinphoneManager.getLc().getCalls()[0];
+ startIncallActivity(call);
+ }
+ } else {
+ DialerFragment dialerFragment = DialerFragment.instance();
+ if (dialerFragment != null) {
+ if (extras != null && extras.containsKey("SipUriOrNumber")) {
+ if (getResources().getBoolean(R.bool.automatically_start_intercepted_outgoing_gsm_call)) {
+ ((DialerFragment) dialerFragment).newOutgoingCall(extras.getString("SipUriOrNumber"));
+ } else {
+ ((DialerFragment) dialerFragment).displayTextInAddressBar(extras.getString("SipUriOrNumber"));
+ }
+ } else {
+ ((DialerFragment) dialerFragment).newOutgoingCall(intent);
+ }
+ }
+ if (LinphoneManager.getLc().getCalls().length > 0) {
+ // If a call is ringing, start incomingcallactivity
+ Collection incoming = new ArrayList();
+ incoming.add(LinphoneCall.State.IncomingReceived);
+ if (LinphoneUtils.getCallsInState(LinphoneManager.getLc(), incoming).size() > 0) {
+ if (CallActivity.isInstanciated()) {
+ CallActivity.instance().startIncomingCallActivity();
+ } else {
+ startActivity(new Intent(this, CallIncomingActivity.class));
+ }
+ }
+ }
+ }
+ }
+
+ public boolean onKeyDown(int keyCode, KeyEvent event) { //NO1
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (currentFragment == FragmentsAvailable.DIALER
+ || currentFragment == FragmentsAvailable.CONTACTS_LIST
+ || currentFragment == FragmentsAvailable.HISTORY_LIST
+ || currentFragment == FragmentsAvailable.CHAT_LIST) {
+ boolean isBackgroundModeActive = LinphonePreferences.instance().isBackgroundModeEnabled();
+ if (!isBackgroundModeActive) {
+ stopService(new Intent(Intent.ACTION_MAIN).setClass(this, LinphoneService.class));
+ finish();
+ Intent intent = new Intent(LinphoneActivity.this, MasterChooser.class);
+ startActivity(intent);
+ } else if (LinphoneUtils.onKeyBackGoHome(this, keyCode, event)) {
+ Intent intent = new Intent(LinphoneActivity.this, MasterChooser.class);
+ startActivity(intent);
+ return true;
+ }
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+
+ //SIDE MENU
+ public void openOrCloseSideMenu(boolean open) {
+ if(open) {
+ sideMenu.openDrawer(sideMenuContent);
+ } else {
+ sideMenu.closeDrawer(sideMenuContent);
+ }
+ }
+
+ public void initSideMenu() {
+ sideMenu = (DrawerLayout) findViewById(R.id.side_menu);
+ sideMenuItems = new ArrayList();
+ sideMenuItems.add(getResources().getString(R.string.menu_assistant));
+ sideMenuItems.add(getResources().getString(R.string.menu_settings));
+ if(getResources().getBoolean(R.bool.enable_in_app_purchase)){
+ sideMenuItems.add(getResources().getString(R.string.inapp));
+ }
+ sideMenuItems.add(getResources().getString(R.string.menu_about));
+ sideMenuContent = (RelativeLayout) findViewById(R.id.side_menu_content);
+ sideMenuItemList = (ListView)findViewById(R.id.item_list);
+ menu = (ImageView) findViewById(R.id.side_menu_button);
+
+ sideMenuItemList.setAdapter(new ArrayAdapter(this, R.layout.side_menu_item_cell, sideMenuItems));
+ sideMenuItemList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
+ if (sideMenuItemList.getAdapter().getItem(i).toString().equals(getString(R.string.menu_settings))) {
+ LinphoneActivity.instance().displaySettings();
+ }
+ if (sideMenuItemList.getAdapter().getItem(i).toString().equals(getString(R.string.menu_about))) {
+ LinphoneActivity.instance().displayAbout();
+ }
+ if (sideMenuItemList.getAdapter().getItem(i).toString().equals(getString(R.string.menu_assistant))) {
+ LinphoneActivity.instance().displayAssistant();
+ }
+ if(getResources().getBoolean(R.bool.enable_in_app_purchase)){
+ if (sideMenuItemList.getAdapter().getItem(i).toString().equals(getString(R.string.inapp))) {
+ LinphoneActivity.instance().displayInapp();
+ }
+ }
+ openOrCloseSideMenu(false);
+ }
+ });
+
+ initAccounts();
+
+ menu.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if(sideMenu.isDrawerVisible(Gravity.LEFT)){
+ sideMenu.closeDrawer(sideMenuContent);
+ } else {
+ sideMenu.openDrawer(sideMenuContent);
+ }
+ }
+ });
+
+ quitLayout = (RelativeLayout) findViewById(R.id.side_menu_quit);
+ quitLayout.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LinphoneActivity.instance().quit();
+ }
+ });
+ }
+
+ private int getStatusIconResource(LinphoneCore.RegistrationState state) {
+ try {
+ if (state == RegistrationState.RegistrationOk) {
+ return R.drawable.led_connected;
+ } else if (state == RegistrationState.RegistrationProgress) {
+ return R.drawable.led_inprogress;
+ } else if (state == RegistrationState.RegistrationFailed) {
+ return R.drawable.led_error;
+ } else {
+ return R.drawable.led_disconnected;
+ }
+ } catch (Exception e) {
+ Log.e(e);
+ }
+
+ return R.drawable.led_disconnected;
+ }
+
+ private void displayMainAccount(){
+ defaultAccount.setVisibility(View.VISIBLE);
+ ImageView status = (ImageView) defaultAccount.findViewById(R.id.main_account_status);
+ TextView address = (TextView) defaultAccount.findViewById(R.id.main_account_address);
+ TextView displayName = (TextView) defaultAccount.findViewById(R.id.main_account_display_name);
+
+
+ LinphoneProxyConfig proxy = LinphoneManager.getLc().getDefaultProxyConfig();
+ if(proxy == null) {
+ displayName.setText(getString(R.string.no_account));
+ status.setVisibility(View.GONE);
+ address.setText("");
+ statusFragment.resetAccountStatus();
+ LinphoneManager.getInstance().subscribeFriendList(false);
+
+ defaultAccount.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LinphoneActivity.instance().displayAccountSettings(0);
+ openOrCloseSideMenu(false);
+ }
+ });
+ } else {
+ address.setText(proxy.getAddress().asStringUriOnly());
+ displayName.setText(LinphoneUtils.getAddressDisplayName(proxy.getAddress()));
+ status.setImageResource(getStatusIconResource(proxy.getState()));
+ status.setVisibility(View.VISIBLE);
+
+ defaultAccount.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ LinphoneActivity.instance().displayAccountSettings(LinphonePreferences.instance().getDefaultAccountIndex());
+ openOrCloseSideMenu(false);
+ }
+ });
+ }
+ }
+
+ public void refreshAccounts(){
+ if (LinphoneManager.getLc().getProxyConfigList().length > 1) {
+ accountsList.setVisibility(View.VISIBLE);
+ accountsList.setAdapter(new AccountsListAdapter());
+ accountsList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
+ if(view != null) {
+ int position = Integer.parseInt(view.getTag().toString());
+ LinphoneActivity.instance().displayAccountSettings(position);
+ }
+ openOrCloseSideMenu(false);
+ }
+ });
+ } else {
+ accountsList.setVisibility(View.GONE);
+ }
+ displayMainAccount();
+ }
+
+ private void initAccounts() {
+ accountsList = (ListView) findViewById(R.id.accounts_list);
+ defaultAccount = (RelativeLayout) findViewById(R.id.default_account);
+ }
+
+ class AccountsListAdapter extends BaseAdapter {
+ List proxy_list;
+
+ AccountsListAdapter() {
+ proxy_list = new ArrayList();
+ refresh();
+ }
+
+ public void refresh(){
+ proxy_list = new ArrayList();
+ for(LinphoneProxyConfig proxyConfig : LinphoneManager.getLc().getProxyConfigList()){
+ if(proxyConfig != LinphoneManager.getLc().getDefaultProxyConfig()){
+ proxy_list.add(proxyConfig);
+ }
+ }
+ }
+
+ public int getCount() {
+ if (proxy_list != null) {
+ return proxy_list.size();
+ } else {
+ return 0;
+ }
+ }
+
+ public Object getItem(int position) {
+ return proxy_list.get(position);
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View view = null;
+ LinphoneProxyConfig lpc = (LinphoneProxyConfig) getItem(position);
+ if (convertView != null) {
+ view = convertView;
+ } else {
+ view = getLayoutInflater().inflate(R.layout.side_menu_account_cell, parent, false);
+ }
+
+ ImageView status = (ImageView) view.findViewById(R.id.account_status);
+ TextView address = (TextView) view.findViewById(R.id.account_address);
+ String sipAddress = lpc.getAddress().asStringUriOnly();
+
+ address.setText(sipAddress);
+
+ int nbAccounts = LinphonePreferences.instance().getAccountCount();
+ int accountIndex = 0;
+
+ for (int i = 0; i < nbAccounts; i++) {
+ String username = LinphonePreferences.instance().getAccountUsername(i);
+ String domain = LinphonePreferences.instance().getAccountDomain(i);
+ String id = "sip:" + username + "@" + domain;
+ if (id.equals(sipAddress)) {
+ accountIndex = i;
+ view.setTag(accountIndex);
+ break;
+ }
+ }
+ status.setImageResource(getStatusIconResource(lpc.getState()));
+ return view;
+ }
+ }
+
+ //Inapp Purchase
+ private void isTrialAccount() {
+ if(LinphoneManager.getLc().getDefaultProxyConfig() != null && LinphonePreferences.instance().getInappPopupTime() != null) {
+ XmlRpcHelper helper = new XmlRpcHelper();
+ helper.isTrialAccountAsync(new XmlRpcListenerBase() {
+ @Override
+ public void onTrialAccountFetched(boolean isTrial) {
+ isTrialAccount = isTrial;
+ getExpirationAccount();
+ }
+
+ @Override
+ public void onError(String error) {
+ }
+ }, LinphonePreferences.instance().getAccountUsername(LinphonePreferences.instance().getDefaultAccountIndex()), LinphonePreferences.instance().getAccountHa1(LinphonePreferences.instance().getDefaultAccountIndex()));
+ }
+ }
+
+ private void getExpirationAccount() {
+ if(LinphoneManager.getLc().getDefaultProxyConfig() != null && LinphonePreferences.instance().getInappPopupTime() != null) {
+ XmlRpcHelper helper = new XmlRpcHelper();
+ helper.getAccountExpireAsync(new XmlRpcListenerBase() {
+ @Override
+ public void onAccountExpireFetched(String result) {
+ if (result != null) {
+ long timestamp = Long.parseLong(result);
+
+ Calendar calresult = Calendar.getInstance();
+ calresult.setTimeInMillis(timestamp);
+
+ int diff = getDiffDays(calresult, Calendar.getInstance());
+ if (diff != -1 && diff <= getResources().getInteger(R.integer.days_notification_shown)) {
+ displayInappNotification(timestampToHumanDate(calresult));
+ }
+ }
+ }
+
+ @Override
+ public void onError(String error) {
+ }
+ }, LinphonePreferences.instance().getAccountUsername(LinphonePreferences.instance().getDefaultAccountIndex()), LinphonePreferences.instance().getAccountHa1(LinphonePreferences.instance().getDefaultAccountIndex()));
+ }
+ }
+
+
+
+ public void displayInappNotification(String date) {
+ Timestamp now = new Timestamp(new Date().getTime());
+ if (LinphonePreferences.instance().getInappPopupTime() != null && Long.parseLong(LinphonePreferences.instance().getInappPopupTime()) > now.getTime()) {
+ return;
+ } else {
+ long newDate = now.getTime() + getResources().getInteger(R.integer.time_between_inapp_notification);
+ LinphonePreferences.instance().setInappPopupTime(String.valueOf(newDate));
+ }
+ if(isTrialAccount){
+ LinphoneService.instance().displayInappNotification(String.format(getString(R.string.inapp_notification_trial_expire), date));
+ } else {
+ LinphoneService.instance().displayInappNotification(String.format(getString(R.string.inapp_notification_account_expire), date));
+ }
+
+ }
+
+ private String timestampToHumanDate(Calendar cal) {
+ SimpleDateFormat dateFormat;
+ dateFormat = new SimpleDateFormat(getResources().getString(R.string.inapp_popup_date_format));
+ return dateFormat.format(cal.getTime());
+ }
+
+ private int getDiffDays(Calendar cal1, Calendar cal2) {
+ if (cal1 == null || cal2 == null) {
+ return -1;
+ }
+ if(cal1.get(Calendar.ERA) == cal2.get(Calendar.ERA) && cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)){
+ return cal1.get(Calendar.DAY_OF_YEAR) - cal2.get(Calendar.DAY_OF_YEAR);
+ }
+ return -1;
+ }
+}
+
+
+
+interface ContactPicked {
+ void setAddresGoToDialerAndCall(String number, String name, Uri photo);
+}
diff --git a/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/LinphoneContact.java b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/LinphoneContact.java
new file mode 100644
index 0000000..e9f24b4
--- /dev/null
+++ b/src/AndroidApp/android_remocons/rocon_remocon/src/main/java/com/github/rosjava/android_remocons/rocon_remocon/linphone/LinphoneContact.java
@@ -0,0 +1,841 @@
+/*
+LinphoneContact.java
+Copyright (C) 2016 Belledonne Communications, Grenoble, France
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package com.github.rosjava.android_remocons.rocon_remocon.linphone;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import org.linphone.core.LinphoneAddress;
+import org.linphone.core.LinphoneCore;
+import org.linphone.core.LinphoneCoreException;
+import org.linphone.core.LinphoneCoreFactory;
+import org.linphone.core.LinphoneFriend;
+import org.linphone.core.LinphoneFriend.SubscribePolicy;
+import org.linphone.core.PresenceBasicStatus;
+import org.linphone.core.PresenceModel;
+import org.linphone.mediastream.Log;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.MediaStore;
+import android.provider.ContactsContract.CommonDataKinds;
+import com.github.rosjava.android_remocons.rocon_remocon.R;
+public class LinphoneContact implements Serializable, Comparable {
+ /**
+ *
+ */
+ private static final long serialVersionUID = 9015568163905205244L;
+
+ private transient LinphoneFriend friend;
+ private String fullName, firstName, lastName, androidId, androidRawId, androidTagId, organization;
+ private transient Uri photoUri, thumbnailUri;
+ private List addresses;
+ private transient ArrayList changesToCommit;
+ private transient ArrayList changesToCommit2;
+ private boolean hasSipAddress;
+ private transient Bitmap photoBitmap, thumbnailBitmap;
+
+ public LinphoneContact() {
+ addresses = new ArrayList();
+ androidId = null;
+ thumbnailUri = null;
+ photoUri = null;
+ changesToCommit = new ArrayList();
+ changesToCommit2 = new ArrayList();
+ hasSipAddress = false;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (photoBitmap != null) {
+ photoBitmap.recycle();
+ photoBitmap = null;
+ }
+ if (thumbnailBitmap != null) {
+ thumbnailBitmap.recycle();
+ thumbnailBitmap = null;
+ }
+ super.finalize();
+ }
+
+ @Override
+ public int compareTo(LinphoneContact contact) {
+ String fullName = getFullName() != null ? getFullName() : "";
+ String contactFullName = contact.getFullName() != null ? contact.getFullName() : "";
+ /*String firstLetter = fullName == null || fullName.isEmpty() ? "" : fullName.substring(0, 1).toUpperCase(Locale.getDefault());
+ String contactfirstLetter = contactFullName == null || contactFullName.isEmpty() ? "" : contactFullName.substring(0, 1).toUpperCase(Locale.getDefault());*/
+ return fullName.compareTo(contactFullName);
+ }
+
+ public void setFullName(String name) {
+ fullName = name;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+
+ public void setFirstNameAndLastName(String fn, String ln) {
+ if (fn != null && fn.length() == 0 && ln != null && ln.length() == 0) return;
+
+ if (isAndroidContact()) {
+ if (firstName != null || lastName != null) {
+ String select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + "'";
+ String[] args = new String[]{ getAndroidId() };
+
+ changesToCommit.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(select, args)
+ .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.StructuredName.GIVEN_NAME, fn)
+ .withValue(CommonDataKinds.StructuredName.FAMILY_NAME, ln)
+ .build()
+ );
+ } else {
+ changesToCommit.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.StructuredName.GIVEN_NAME, fn)
+ .withValue(CommonDataKinds.StructuredName.FAMILY_NAME, ln)
+ .build());
+ }
+ }
+
+ firstName = fn;
+ lastName = ln;
+ if (firstName != null && lastName != null && firstName.length() > 0 && lastName.length() > 0) {
+ fullName = firstName + " " + lastName;
+ } else if (firstName != null && firstName.length() > 0) {
+ fullName = firstName;
+ } else if (lastName != null && lastName.length() > 0) {
+ fullName = lastName;
+ }
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public String getOrganization() {
+ return organization;
+ }
+
+ public void setOrganization(String org) {
+ if (isAndroidContact()) {
+ if (androidRawId != null) {
+ String select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.Organization.CONTENT_ITEM_TYPE + "'";
+ String[] args = new String[]{ getAndroidId() };
+
+ if (organization != null) {
+ changesToCommit.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(select, args)
+ .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.Organization.COMPANY, org)
+ .build());
+ } else {
+ changesToCommit.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, androidRawId)
+ .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.Organization.COMPANY, org)
+ .build());
+ }
+ } else {
+ changesToCommit.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.Organization.COMPANY, org)
+ .build());
+ }
+ }
+
+ organization = org;
+ }
+
+ public boolean hasPhoto() {
+ return photoUri != null;
+ }
+
+ public void setPhotoUri(Uri uri) {
+ if (uri.equals(photoUri)) return;
+ photoUri = uri;
+
+ if (photoBitmap != null) {
+ photoBitmap.recycle();
+ }
+ try {
+ photoBitmap = MediaStore.Images.Media.getBitmap(ContactsManager.getInstance().getContentResolver(), photoUri);
+ } catch (FileNotFoundException e) {
+ // Let's not say anything if the picture doesn't exist, it will pollute the logs
+ } catch (IOException e) {
+ Log.e(e);
+ }
+ }
+
+ public Uri getPhotoUri() {
+ return photoUri;
+ }
+
+ public Bitmap getPhotoBitmap() {
+ return photoBitmap;
+ }
+
+ public void setThumbnailUri(Uri uri) {
+ if (uri.equals(thumbnailUri)) return;
+ thumbnailUri = uri;
+
+ if (thumbnailBitmap != null) {
+ thumbnailBitmap.recycle();
+ }
+ try {
+ thumbnailBitmap = MediaStore.Images.Media.getBitmap(ContactsManager.getInstance().getContentResolver(), thumbnailUri);
+ } catch (FileNotFoundException e) {
+ // Let's not say anything if the picture doesn't exist, it will pollute the logs
+ } catch (IOException e) {
+ Log.e(e);
+ }
+ }
+
+ public Uri getThumbnailUri() {
+ return thumbnailUri;
+ }
+
+ public Bitmap getThumbnailBitmap() {
+ return thumbnailBitmap;
+ }
+
+ public Bitmap getPhoto() {
+ if (photoBitmap != null) {
+ return photoBitmap;
+ } else if (thumbnailBitmap != null) {
+ return thumbnailBitmap;
+ }
+ return null;
+ }
+
+ public void setPhoto(byte[] photo) {
+ if (photo != null) {
+ if (isAndroidContact()) {
+ if (androidRawId != null) {
+ changesToCommit.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, androidRawId)
+ .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.Photo.PHOTO, photo)
+ .withValue(ContactsContract.Data.IS_PRIMARY, 1)
+ .withValue(ContactsContract.Data.IS_SUPER_PRIMARY, 1)
+ .build());
+ } else {
+ changesToCommit.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ .withValue(CommonDataKinds.Photo.PHOTO, photo)
+ .build());
+ }
+ }
+ }
+ }
+
+ public void addNumberOrAddress(LinphoneNumberOrAddress noa) {
+ if (noa == null) return;
+ if (noa.isSIPAddress()) {
+ hasSipAddress = true;
+ }
+ addresses.add(noa);
+ }
+
+ public List getNumbersOrAddresses() {
+ return addresses;
+ }
+
+ public boolean hasAddress(String address) {
+ for (LinphoneNumberOrAddress noa : getNumbersOrAddresses()) {
+ if (noa.isSIPAddress()) {
+ String value = noa.getValue();
+ if (value.equals(address) || value.equals("sip:" + address)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public boolean hasAddress() {
+ return hasSipAddress;
+ }
+
+ public void removeNumberOrAddress(LinphoneNumberOrAddress noa) {
+ if (noa != null && noa.getOldValue() != null) {
+ if (isAndroidContact()) {
+ String select;
+ if (noa.isSIPAddress()) {
+ select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + "' AND " + CommonDataKinds.SipAddress.SIP_ADDRESS + "=?";
+ } else {
+ select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "' AND " + CommonDataKinds.Phone.NUMBER + "=?";
+ }
+ String[] args = new String[]{ getAndroidId(), noa.getOldValue() };
+
+ changesToCommit.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(select, args)
+ .build());
+
+ if (androidTagId != null && noa.isSIPAddress()) {
+ select = ContactsContract.Data.RAW_CONTACT_ID + "=? AND " + ContactsContract.Data.DATA1 + "=?";
+ args = new String[] { androidTagId, noa.getOldValue() };
+
+ changesToCommit.add(ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI)
+ .withSelection(select, args)
+ .build());
+ }
+ }
+
+ if (isLinphoneFriend()) {
+ if (noa.isSIPAddress()) {
+ if (!noa.getOldValue().startsWith("sip:")) {
+ noa.setOldValue("sip:" + noa.getOldValue());
+ }
+ }
+ LinphoneNumberOrAddress toRemove = null;
+ for (LinphoneNumberOrAddress address : addresses) {
+ if (noa.getOldValue().equals(address.getValue()) && noa.isSIPAddress() == address.isSIPAddress()) {
+ toRemove = address;
+ break;
+ }
+ }
+ if (toRemove != null) {
+ addresses.remove(toRemove);
+ }
+ }
+ }
+ }
+
+ public void addOrUpdateNumberOrAddress(LinphoneNumberOrAddress noa) {
+ if (noa != null && noa.getValue() != null) {
+ if (isAndroidContact()) {
+ if (noa.getOldValue() == null) {
+ ContentValues values = new ContentValues();
+ if (noa.isSIPAddress()) {
+ values.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE);
+ values.put(CommonDataKinds.SipAddress.DATA, noa.getValue());
+ values.put(CommonDataKinds.SipAddress.TYPE, CommonDataKinds.SipAddress.TYPE_CUSTOM);
+ values.put(CommonDataKinds.SipAddress.LABEL, ContactsManager.getInstance().getString(R.string.addressbook_label));
+ } else {
+ values.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
+ values.put(CommonDataKinds.Phone.NUMBER, noa.getValue());
+ values.put(CommonDataKinds.Phone.TYPE, CommonDataKinds.Phone.TYPE_CUSTOM);
+ values.put(CommonDataKinds.Phone.LABEL, ContactsManager.getInstance().getString(R.string.addressbook_label));
+ }
+ if (androidRawId != null) {
+ changesToCommit.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, androidRawId)
+ .withValues(values)
+ .build());
+ } else {
+ changesToCommit.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValues(values)
+ .build());
+ }
+
+ if (noa.isSIPAddress() && LinphoneManager.getInstance().getContext().getResources().getBoolean(R.bool.use_linphone_tag)) {
+ if (androidTagId != null) {
+ changesToCommit.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValue(ContactsContract.Data.RAW_CONTACT_ID, androidTagId)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsManager.getInstance().getString(R.string.sync_mimetype))
+ .withValue(ContactsContract.Data.DATA1, noa.getValue())
+ .withValue(ContactsContract.Data.DATA2, ContactsManager.getInstance().getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, noa.getValue())
+ .build());
+ } else {
+ changesToCommit2.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsManager.getInstance().getString(R.string.sync_mimetype))
+ .withValue(ContactsContract.Data.DATA1, noa.getValue())
+ .withValue(ContactsContract.Data.DATA2, ContactsManager.getInstance().getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, noa.getValue())
+ .build());
+ }
+ }
+ } else {
+ ContentValues values = new ContentValues();
+ String select;
+ String[] args = new String[] { getAndroidId(), noa.getOldValue() };
+
+ if (noa.isSIPAddress()) {
+ select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE + "' AND " + CommonDataKinds.SipAddress.SIP_ADDRESS + "=?";
+ values.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE);
+ values.put(CommonDataKinds.SipAddress.DATA, noa.getValue());
+ } else {
+ select = ContactsContract.Data.CONTACT_ID + "=? AND " + ContactsContract.Data.MIMETYPE + "='" + CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "' AND " + CommonDataKinds.Phone.NUMBER + "=?";
+ values.put(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
+ values.put(CommonDataKinds.Phone.NUMBER, noa.getValue());
+ }
+ changesToCommit.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(select, args)
+ .withValues(values)
+ .build());
+
+ if (noa.isSIPAddress() && LinphoneManager.getInstance().getContext().getResources().getBoolean(R.bool.use_linphone_tag)) {
+ if (androidTagId != null) {
+ changesToCommit.add(ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.RAW_CONTACT_ID + "=? AND " + ContactsContract.Data.DATA1 + "=? ", new String[] { androidTagId, noa.getOldValue() })
+ .withValue(ContactsContract.Data.DATA1, noa.getValue())
+ .withValue(ContactsContract.Data.DATA2, ContactsManager.getInstance().getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, noa.getValue())
+ .build());
+ } else {
+ changesToCommit2.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsManager.getInstance().getString(R.string.sync_mimetype))
+ .withValue(ContactsContract.Data.DATA1, noa.getValue())
+ .withValue(ContactsContract.Data.DATA2, ContactsManager.getInstance().getString(R.string.app_name))
+ .withValue(ContactsContract.Data.DATA3, noa.getValue())
+ .build());
+ }
+ }
+ }
+ }
+ if (isLinphoneFriend()) {
+ if (noa.isSIPAddress()) {
+ if (!noa.getValue().startsWith("sip:")) {
+ noa.setValue("sip:" + noa.getValue());
+ }
+ }
+ if (noa.getOldValue() != null) {
+ if (noa.isSIPAddress()) {
+ if (!noa.getOldValue().startsWith("sip:")) {
+ noa.setOldValue("sip:" + noa.getOldValue());
+ }
+ }
+ for (LinphoneNumberOrAddress address : addresses) {
+ if (noa.getOldValue().equals(address.getValue()) && noa.isSIPAddress() == address.isSIPAddress()) {
+ address.setValue(noa.getValue());
+ break;
+ }
+ }
+ } else {
+ addresses.add(noa);
+ }
+ }
+ }
+ }
+
+ public void setAndroidId(String id) {
+ androidId = id;
+ }
+
+ public String getAndroidId() {
+ return androidId;
+ }
+
+ private void createOrUpdateFriend() {
+ if (!isLinphoneFriend()) {
+ friend = LinphoneManager.getLc().createFriend();
+ friend.enableSubscribes(false);
+ friend.setIncSubscribePolicy(SubscribePolicy.SPDeny);
+ if (isAndroidContact()) {
+ friend.setRefKey(getAndroidId());
+ }
+ }
+ if (isLinphoneFriend()) {
+ updateFriend();
+ }
+ }
+
+ private void updateFriend() {
+ if (!isLinphoneFriend()) return;
+
+ LinphoneCore lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull();
+ if (lc == null) return;
+
+ friend.edit();
+ friend.setFamilyName(lastName);
+ friend.setGivenName(firstName);
+ friend.setName(fullName);
+
+ for (LinphoneAddress address : friend.getAddresses()) {
+ friend.removeAddress(address);
+ }
+ for (String phone : friend.getPhoneNumbers()) {
+ friend.removePhoneNumber(phone);
+ }
+ if (organization != null && !organization.isEmpty()) {
+ friend.setOrganization(organization);
+ }
+ for (LinphoneNumberOrAddress noa : addresses) {
+ if (noa.isSIPAddress()) {
+ try {
+ LinphoneAddress addr = lc.interpretUrl(noa.getValue());
+ if (addr != null) {
+ friend.addAddress(addr);
+ }
+ } catch (LinphoneCoreException e) {
+ Log.e(e);
+ }
+ } else {
+ friend.addPhoneNumber(noa.getValue());
+ }
+ }
+ friend.done();
+
+ if (!friend.isAlreadyPresentInFriendList()) {
+ try {
+ LinphoneManager.getLcIfManagerNotDestroyedOrNull().addFriend(friend);
+ } catch (LinphoneCoreException e) {
+ Log.e(e);
+ }
+ }
+
+ if (!ContactsManager.getInstance().hasContactsAccess()) {
+ // This refresh is only needed if app has no contacts permission to refresh the list of LinphoneFriends.
+ // Otherwise contacts will be refreshed due to changes in native contact and the handler in ContactsManager
+ ContactsManager.getInstance().fetchContactsAsync();
+ }
+ }
+
+ public void save() {
+ if (isAndroidContact() && ContactsManager.getInstance().hasContactsAccess() && changesToCommit.size() > 0) {
+ try {
+ ContactsManager.getInstance().getContentResolver().applyBatch(ContactsContract.AUTHORITY, changesToCommit);
+ createLinphoneTagIfNeeded();
+ } catch (Exception e) {
+ Log.e(e);
+ } finally {
+ changesToCommit = new ArrayList();
+ changesToCommit2 = new ArrayList