From aa70fe4b0330af01ec606fe222356ce9b55aaf93 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 6 May 2019 11:47:27 +0200 Subject: [PATCH] Feature/sample app --- sample/.gitignore | 13 + sample/app/.gitignore | 1 + sample/app/build.gradle | 36 +++ sample/app/proguard-rules.pro | 21 ++ sample/app/src/main/AndroidManifest.xml | 49 ++++ .../org/linphone/sample/CallActivity.java | 149 +++++++++++ .../sample/ConfigureAccountActivity.java | 113 +++++++++ .../org/linphone/sample/LauncherActivity.java | 70 ++++++ .../org/linphone/sample/LinphoneService.java | 235 ++++++++++++++++++ .../org/linphone/sample/MainActivity.java | 152 +++++++++++ sample/app/src/main/res/drawable/banner.png | Bin 0 -> 12440 bytes .../src/main/res/drawable/led_connected.png | Bin 0 -> 1046 bytes .../main/res/drawable/led_disconnected.png | Bin 0 -> 904 bytes .../app/src/main/res/drawable/led_error.png | Bin 0 -> 840 bytes .../src/main/res/drawable/led_inprogress.png | Bin 0 -> 952 bytes sample/app/src/main/res/layout/call.xml | 19 ++ .../src/main/res/layout/configure_account.xml | 71 ++++++ sample/app/src/main/res/layout/launcher.xml | 21 ++ sample/app/src/main/res/layout/main.xml | 22 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2132 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 2343 bytes .../linphone_launcher_icon_foreground.png | Bin 0 -> 1832 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1417 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1467 bytes .../linphone_launcher_icon_foreground.png | Bin 0 -> 1213 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2880 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 3178 bytes .../linphone_launcher_icon_foreground.png | Bin 0 -> 2532 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4524 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 5416 bytes .../linphone_launcher_icon_foreground.png | Bin 0 -> 3874 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6221 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 8083 bytes .../linphone_launcher_icon_foreground.png | Bin 0 -> 5406 bytes .../app/src/main/res/raw/linphonerc_default | 20 ++ .../app/src/main/res/raw/linphonerc_factory | 34 +++ sample/app/src/main/res/values/color.xml | 5 + sample/app/src/main/res/values/colors.xml | 6 + sample/app/src/main/res/values/strings.xml | 3 + sample/app/src/main/res/values/styles.xml | 6 + sample/build.gradle | 27 ++ sample/gradle.properties | 20 ++ sample/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + sample/gradlew | 172 +++++++++++++ sample/gradlew.bat | 84 +++++++ sample/settings.gradle | 1 + 49 files changed, 1366 insertions(+) create mode 100644 sample/.gitignore create mode 100644 sample/app/.gitignore create mode 100644 sample/app/build.gradle create mode 100644 sample/app/proguard-rules.pro create mode 100644 sample/app/src/main/AndroidManifest.xml create mode 100644 sample/app/src/main/java/org/linphone/sample/CallActivity.java create mode 100644 sample/app/src/main/java/org/linphone/sample/ConfigureAccountActivity.java create mode 100644 sample/app/src/main/java/org/linphone/sample/LauncherActivity.java create mode 100644 sample/app/src/main/java/org/linphone/sample/LinphoneService.java create mode 100644 sample/app/src/main/java/org/linphone/sample/MainActivity.java create mode 100644 sample/app/src/main/res/drawable/banner.png create mode 100644 sample/app/src/main/res/drawable/led_connected.png create mode 100644 sample/app/src/main/res/drawable/led_disconnected.png create mode 100644 sample/app/src/main/res/drawable/led_error.png create mode 100644 sample/app/src/main/res/drawable/led_inprogress.png create mode 100644 sample/app/src/main/res/layout/call.xml create mode 100644 sample/app/src/main/res/layout/configure_account.xml create mode 100644 sample/app/src/main/res/layout/launcher.xml create mode 100644 sample/app/src/main/res/layout/main.xml create mode 100644 sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 sample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 sample/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 sample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 sample/app/src/main/res/mipmap-hdpi/linphone_launcher_icon_foreground.png create mode 100644 sample/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 sample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 sample/app/src/main/res/mipmap-mdpi/linphone_launcher_icon_foreground.png create mode 100644 sample/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 sample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 sample/app/src/main/res/mipmap-xhdpi/linphone_launcher_icon_foreground.png create mode 100644 sample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 sample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 sample/app/src/main/res/mipmap-xxhdpi/linphone_launcher_icon_foreground.png create mode 100644 sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 sample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 sample/app/src/main/res/mipmap-xxxhdpi/linphone_launcher_icon_foreground.png create mode 100644 sample/app/src/main/res/raw/linphonerc_default create mode 100644 sample/app/src/main/res/raw/linphonerc_factory create mode 100644 sample/app/src/main/res/values/color.xml create mode 100644 sample/app/src/main/res/values/colors.xml create mode 100644 sample/app/src/main/res/values/strings.xml create mode 100644 sample/app/src/main/res/values/styles.xml create mode 100644 sample/build.gradle create mode 100644 sample/gradle.properties create mode 100644 sample/gradle/wrapper/gradle-wrapper.jar create mode 100644 sample/gradle/wrapper/gradle-wrapper.properties create mode 100755 sample/gradlew create mode 100644 sample/gradlew.bat create mode 100644 sample/settings.gradle diff --git a/sample/.gitignore b/sample/.gitignore new file mode 100644 index 000000000..2b75303ac --- /dev/null +++ b/sample/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/sample/app/.gitignore b/sample/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/sample/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/sample/app/build.gradle b/sample/app/build.gradle new file mode 100644 index 000000000..1bc54c517 --- /dev/null +++ b/sample/app/build.gradle @@ -0,0 +1,36 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "org.linphone.sample" + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + maven { + // Replace snapshots by releases for releases ! + url "https://linphone.org/snapshots/maven_repository" + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.0.2' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + + implementation "org.linphone:linphone-sdk-android:4.2+" +} diff --git a/sample/app/proguard-rules.pro b/sample/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/sample/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# 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 *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/sample/app/src/main/AndroidManifest.xml b/sample/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9ce1a1c00 --- /dev/null +++ b/sample/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample/app/src/main/java/org/linphone/sample/CallActivity.java b/sample/app/src/main/java/org/linphone/sample/CallActivity.java new file mode 100644 index 000000000..aa54854f9 --- /dev/null +++ b/sample/app/src/main/java/org/linphone/sample/CallActivity.java @@ -0,0 +1,149 @@ +package org.linphone.sample; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.os.Bundle; +import android.util.DisplayMetrics; +import android.view.TextureView; +import android.widget.RelativeLayout; + +import androidx.annotation.Nullable; + +import org.linphone.core.Call; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.VideoDefinition; +import org.linphone.core.tools.Log; +import org.linphone.mediastream.Version; + +public class CallActivity extends Activity { + // We use 2 TextureView, one for remote video and one for local camera preview + private TextureView mVideoView; + private TextureView mCaptureView; + + private CoreListenerStub mCoreListener; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.call); + + mVideoView = findViewById(R.id.videoSurface); + mCaptureView = findViewById(R.id.videoCaptureSurface); + + Core core = LinphoneService.getCore(); + // We need to tell the core in which to display what + core.setNativeVideoWindowId(mVideoView); + core.setNativePreviewWindowId(mCaptureView); + + // Listen for call state changes + mCoreListener = new CoreListenerStub() { + @Override + public void onCallStateChanged(Core lc, Call call, Call.State state, String message) { + if (state == Call.State.End || state == Call.State.Released) { + // Once call is finished (end state), terminate the activity + // We also check for released state (called a few seconds later) just in case + // we missed the first one + finish(); + } + } + }; + } + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + + LinphoneService.getCore().addListener(mCoreListener); + resizePreview(); + } + + @Override + protected void onPause() { + LinphoneService.getCore().removeListener(mCoreListener); + + super.onPause(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + @TargetApi(24) + @Override + public void onUserLeaveHint() { + // If the device supports Picture in Picture let's use it + boolean supportsPip = + getPackageManager() + .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); + Log.i("[Call] Is picture in picture supported: " + supportsPip); + if (supportsPip && Version.sdkAboveOrEqual(24)) { + enterPictureInPictureMode(); + } + } + + @Override + public void onPictureInPictureModeChanged( + boolean isInPictureInPictureMode, Configuration newConfig) { + if (isInPictureInPictureMode) { + // Currently nothing to do has we only display video + // But if we had controls or other UI elements we should hide them + } else { + // If we did hide something, let's make them visible again + } + } + + private void resizePreview() { + Core core = LinphoneService.getCore(); + if (core.getCallsNb() > 0) { + Call call = core.getCurrentCall(); + if (call == null) { + call = core.getCalls()[0]; + } + if (call == null) return; + + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(metrics); + int screenHeight = metrics.heightPixels; + int maxHeight = + screenHeight / 4; // Let's take at most 1/4 of the screen for the camera preview + + VideoDefinition videoSize = + call.getCurrentParams() + .getSentVideoDefinition(); // It already takes care of rotation + if (videoSize.getWidth() == 0 || videoSize.getHeight() == 0) { + Log.w( + "[Video] Couldn't get sent video definition, using default video definition"); + videoSize = core.getPreferredVideoDefinition(); + } + int width = videoSize.getWidth(); + int height = videoSize.getHeight(); + + Log.d("[Video] Video height is " + height + ", width is " + width); + width = width * maxHeight / height; + height = maxHeight; + + if (mCaptureView == null) { + Log.e("[Video] mCaptureView is null !"); + return; + } + + RelativeLayout.LayoutParams newLp = new RelativeLayout.LayoutParams(width, height); + newLp.addRule( + RelativeLayout.ALIGN_PARENT_BOTTOM, + 1); // Clears the rule, as there is no removeRule until API 17. + newLp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, 1); + mCaptureView.setLayoutParams(newLp); + Log.d("[Video] Video preview size set to " + width + "x" + height); + } + } +} diff --git a/sample/app/src/main/java/org/linphone/sample/ConfigureAccountActivity.java b/sample/app/src/main/java/org/linphone/sample/ConfigureAccountActivity.java new file mode 100644 index 000000000..6d4a226b6 --- /dev/null +++ b/sample/app/src/main/java/org/linphone/sample/ConfigureAccountActivity.java @@ -0,0 +1,113 @@ +package org.linphone.sample; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.RadioGroup; +import android.widget.Toast; + +import androidx.annotation.Nullable; + +import org.linphone.core.AccountCreator; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.ProxyConfig; +import org.linphone.core.RegistrationState; +import org.linphone.core.TransportType; + +public class ConfigureAccountActivity extends Activity { + private EditText mUsername, mPassword, mDomain; + private RadioGroup mTransport; + private Button mConnect; + + private AccountCreator mAccountCreator; + private CoreListenerStub mCoreListener; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.configure_account); + + // Account creator can help you create/config accounts, even not sip.linphone.org ones + // As we only want to configure an existing account, no need for server URL to make requests + // to know whether or not account exists, etc... + mAccountCreator = LinphoneService.getCore().createAccountCreator(null); + + mUsername = findViewById(R.id.username); + mPassword = findViewById(R.id.password); + mDomain = findViewById(R.id.domain); + mTransport = findViewById(R.id.assistant_transports); + + mConnect = findViewById(R.id.configure); + mConnect.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + configureAccount(); + } + }); + + mCoreListener = new CoreListenerStub() { + @Override + public void onRegistrationStateChanged(Core core, ProxyConfig cfg, RegistrationState state, String message) { + if (state == RegistrationState.Ok) { + finish(); + } else if (state == RegistrationState.Failed) { + Toast.makeText(ConfigureAccountActivity.this, "Failure: " + message, Toast.LENGTH_LONG).show(); + } + } + }; + } + + @Override + protected void onStart() { + super.onStart(); + } + + @Override + protected void onResume() { + super.onResume(); + + LinphoneService.getCore().addListener(mCoreListener); + } + + @Override + protected void onPause() { + LinphoneService.getCore().removeListener(mCoreListener); + + super.onPause(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + private void configureAccount() { + // At least the 3 below values are required + mAccountCreator.setUsername(mUsername.getText().toString()); + mAccountCreator.setDomain(mDomain.getText().toString()); + mAccountCreator.setPassword(mPassword.getText().toString()); + + // By default it will be UDP if not set, but TLS is strongly recommended + switch (mTransport.getCheckedRadioButtonId()) { + case R.id.transport_udp: + mAccountCreator.setTransport(TransportType.Udp); + break; + case R.id.transport_tcp: + mAccountCreator.setTransport(TransportType.Tcp); + break; + case R.id.transport_tls: + mAccountCreator.setTransport(TransportType.Tls); + break; + } + + // This will automatically create the proxy config and auth info and add them to the Core + ProxyConfig cfg = mAccountCreator.configure(); + // Make sure the newly created one is the default + LinphoneService.getCore().setDefaultProxyConfig(cfg); + } +} diff --git a/sample/app/src/main/java/org/linphone/sample/LauncherActivity.java b/sample/app/src/main/java/org/linphone/sample/LauncherActivity.java new file mode 100644 index 000000000..9321cb140 --- /dev/null +++ b/sample/app/src/main/java/org/linphone/sample/LauncherActivity.java @@ -0,0 +1,70 @@ +package org.linphone.sample; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; + +public class LauncherActivity extends Activity { + private Handler mHandler; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.launcher); + + mHandler = new Handler(); + } + + @Override + protected void onStart() { + super.onStart(); + + // Check whether the Service is already running + if (LinphoneService.isReady()) { + onServiceReady(); + } else { + // If it's not, let's start it + startService( + new Intent().setClass(this, LinphoneService.class)); + // And wait for it to be ready, so we can safely use it afterwards + new ServiceWaitThread().start(); + } + } + + private void onServiceReady() { + // Once the service is ready, we can move on in the application + // We'll forward the intent action, type and extras so it can be handled + // by the next activity if needed, it's not the launcher job to do that + Intent intent = new Intent(); + intent.setClass(LauncherActivity.this, MainActivity.class); + if (getIntent() != null && getIntent().getExtras() != null) { + intent.putExtras(getIntent().getExtras()); + } + intent.setAction(getIntent().getAction()); + intent.setType(getIntent().getType()); + startActivity(intent); + } + + // This thread will periodically check if the Service is ready, and then call onServiceReady + private class ServiceWaitThread extends Thread { + public void run() { + while (!LinphoneService.isReady()) { + try { + sleep(30); + } catch (InterruptedException e) { + throw new RuntimeException("waiting thread sleep() has been interrupted"); + } + } + // As we're in a thread, we can't do UI stuff in it, must post a runnable in UI thread + mHandler.post( + new Runnable() { + @Override + public void run() { + onServiceReady(); + } + }); + } + } +} diff --git a/sample/app/src/main/java/org/linphone/sample/LinphoneService.java b/sample/app/src/main/java/org/linphone/sample/LinphoneService.java new file mode 100644 index 000000000..5b933c9fd --- /dev/null +++ b/sample/app/src/main/java/org/linphone/sample/LinphoneService.java @@ -0,0 +1,235 @@ +package org.linphone.sample; + +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Handler; +import android.os.IBinder; + +import androidx.annotation.Nullable; + +import org.linphone.core.Call; +import org.linphone.core.CallParams; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.Factory; +import org.linphone.core.LogCollectionState; +import org.linphone.core.MediaDirection; +import org.linphone.core.tools.Log; +import org.linphone.mediastream.Version; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Timer; +import java.util.TimerTask; + +public class LinphoneService extends Service { + private static final String START_LINPHONE_LOGS = " ==== Device information dump ===="; + // Keep a static reference to the Service so we can access it from anywhere in the app + private static LinphoneService sInstance; + + private Handler mHandler; + private Timer mTimer; + + private Core mCore; + private CoreListenerStub mCoreListener; + + public static boolean isReady() { + return sInstance != null; + } + + public static LinphoneService getInstance() { + return sInstance; + } + + public static Core getCore() { + return sInstance.mCore; + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + + // The first call to liblinphone SDK MUST BE to a Factory method + // So let's enable the library debug logs & log collection + String basePath = getFilesDir().getAbsolutePath(); + Factory.instance().setLogCollectionPath(basePath); + Factory.instance().enableLogCollection(LogCollectionState.Enabled); + Factory.instance().setDebugMode(true, getString(R.string.app_name)); + + // Dump some useful information about the device we're running on + Log.i(START_LINPHONE_LOGS); + dumpDeviceInformation(); + dumpInstalledLinphoneInformation(); + + mHandler = new Handler(); + // This will be our main Core listener, it will change activities depending on events + mCoreListener = new CoreListenerStub() { + @Override + public void onCallStateChanged(Core lc, Call call, Call.State state, String message) { + if (state == Call.State.IncomingReceived) { + // For this sample we will automatically answer incoming calls + CallParams params = getCore().createCallParams(call); + params.enableVideo(true); + call.acceptWithParams(params); + } else if (state == Call.State.Connected) { + // This stats means the call has been established, let's start the call activity + Intent intent = new Intent(LinphoneService.this, CallActivity.class); + // As it is the Service that is starting the activity, we have to give this flag + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + } + }; + + try { + // Let's copy some RAW resources to the device + // The default config file must only be installed once (the first time) + copyIfNotExist(R.raw.linphonerc_default, basePath + "/.linphonerc"); + // The factory config is used to override any other setting, let's copy it each time + copyFromPackage(R.raw.linphonerc_factory, "linphonerc"); + } catch (IOException ioe) { + Log.e(ioe); + } + + // Create the Core and add our listener + mCore = Factory.instance() + .createCore(basePath + "/.linphonerc", basePath + "/linphonerc", this); + mCore.addListener(mCoreListener); + // Core is ready to be configured + configureCore(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + // If our Service is already running, no need to continue + if (sInstance != null) { + return START_STICKY; + } + + // Our Service has been started, we can keep our reference on it + // From now one the Launcher will be able to call onServiceReady() + sInstance = this; + + // Core must be started after being created and configured + mCore.start(); + // We also MUST call the iterate() method of the Core on a regular basis + TimerTask lTask = + new TimerTask() { + @Override + public void run() { + mHandler.post( + new Runnable() { + @Override + public void run() { + if (mCore != null) { + mCore.iterate(); + } + } + }); + } + }; + mTimer = new Timer("Linphone scheduler"); + mTimer.schedule(lTask, 0, 20); + + return START_STICKY; + } + + @Override + public void onDestroy() { + mCore.removeListener(mCoreListener); + mTimer.cancel(); + mCore.stop(); + // A stopped Core can be started again + // To ensure resources are freed, we must ensure it will be garbage collected + mCore = null; + // Don't forget to free the singleton as well + sInstance = null; + + super.onDestroy(); + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + // For this sample we will kill the Service at the same time we kill the app + stopSelf(); + + super.onTaskRemoved(rootIntent); + } + + private void configureCore() { + // We will create a directory for user signed certificates if needed + String basePath = getFilesDir().getAbsolutePath(); + String userCerts = basePath + "/user-certs"; + File f = new File(userCerts); + if (!f.exists()) { + if (!f.mkdir()) { + Log.e(userCerts + " can't be created."); + } + } + mCore.setUserCertificatesPath(userCerts); + } + + private void dumpDeviceInformation() { + StringBuilder sb = new StringBuilder(); + sb.append("DEVICE=").append(Build.DEVICE).append("\n"); + sb.append("MODEL=").append(Build.MODEL).append("\n"); + sb.append("MANUFACTURER=").append(Build.MANUFACTURER).append("\n"); + sb.append("SDK=").append(Build.VERSION.SDK_INT).append("\n"); + sb.append("Supported ABIs="); + for (String abi : Version.getCpuAbis()) { + sb.append(abi).append(", "); + } + sb.append("\n"); + Log.i(sb.toString()); + } + + private void dumpInstalledLinphoneInformation() { + PackageInfo info = null; + try { + info = getPackageManager().getPackageInfo(getPackageName(), 0); + } catch (PackageManager.NameNotFoundException nnfe) { + Log.e(nnfe); + } + + if (info != null) { + Log.i( + "[Service] Linphone version is ", + info.versionName + " (" + info.versionCode + ")"); + } else { + Log.i("[Service] Linphone version is unknown"); + } + } + + private void copyIfNotExist(int ressourceId, String target) throws IOException { + File lFileToCopy = new File(target); + if (!lFileToCopy.exists()) { + copyFromPackage(ressourceId, lFileToCopy.getName()); + } + } + + private void copyFromPackage(int ressourceId, String target) throws IOException { + FileOutputStream lOutputStream = openFileOutput(target, 0); + InputStream lInputStream = getResources().openRawResource(ressourceId); + int readByte; + byte[] buff = new byte[8048]; + while ((readByte = lInputStream.read(buff)) != -1) { + lOutputStream.write(buff, 0, readByte); + } + lOutputStream.flush(); + lOutputStream.close(); + lInputStream.close(); + } +} diff --git a/sample/app/src/main/java/org/linphone/sample/MainActivity.java b/sample/app/src/main/java/org/linphone/sample/MainActivity.java new file mode 100644 index 000000000..3d6298c84 --- /dev/null +++ b/sample/app/src/main/java/org/linphone/sample/MainActivity.java @@ -0,0 +1,152 @@ +package org.linphone.sample; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.widget.ImageView; + +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; + +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.ProxyConfig; +import org.linphone.core.RegistrationState; +import org.linphone.core.tools.Log; + +import java.util.ArrayList; + +public class MainActivity extends Activity { + private ImageView mLed; + private CoreListenerStub mCoreListener; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.main); + + mLed = findViewById(R.id.led); + + // Monitors the registration state of our account(s) and update the LED accordingly + mCoreListener = new CoreListenerStub() { + @Override + public void onRegistrationStateChanged(Core core, ProxyConfig cfg, RegistrationState state, String message) { + updateLed(state); + } + }; + } + + @Override + protected void onStart() { + super.onStart(); + + // Ask runtime permissions, such as record audio and camera + // We don't need them here but once the user has granted them we won't have to ask again + checkAndRequestCallPermissions(); + } + + @Override + protected void onResume() { + super.onResume(); + + // The best way to use Core listeners in Activities is to add them in onResume + // and to remove them in onPause + LinphoneService.getCore().addListener(mCoreListener); + + // Manually update the LED registration state, in case it has been registered before + // we add a chance to register the above listener + ProxyConfig proxyConfig = LinphoneService.getCore().getDefaultProxyConfig(); + if (proxyConfig != null) { + updateLed(proxyConfig.getState()); + } else { + // No account configured, we display the configuration activity + startActivity(new Intent(this, ConfigureAccountActivity.class)); + } + } + + @Override + protected void onPause() { + super.onPause(); + + // Like I said above, remove unused Core listeners in onPause + LinphoneService.getCore().removeListener(mCoreListener); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + // Callback for when permissions are asked to the user + for (int i = 0; i < permissions.length; i++) { + Log.i( + "[Permission] " + + permissions[i] + + " is " + + (grantResults[i] == PackageManager.PERMISSION_GRANTED + ? "granted" + : "denied")); + } + } + + private void updateLed(RegistrationState state) { + switch (state) { + case Ok: // This state means you are connected, to can make and receive calls & messages + mLed.setImageResource(R.drawable.led_connected); + break; + case None: // This state is the default state + case Cleared: // This state is when you disconnected + mLed.setImageResource(R.drawable.led_disconnected); + break; + case Failed: // This one means an error happened, for example a bad password + mLed.setImageResource(R.drawable.led_error); + break; + case Progress: // Connection is in progress, next state will be either Ok or Failed + mLed.setImageResource(R.drawable.led_inprogress); + break; + } + } + + private void checkAndRequestCallPermissions() { + ArrayList permissionsList = new ArrayList<>(); + + // Some required permissions needs to be validated manually by the user + // Here we ask for record audio and camera to be able to make video calls with sound + // Once granted we don't have to ask them again, but if denied we can + 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) { + Log.i("[Permission] Asking for record audio"); + permissionsList.add(Manifest.permission.RECORD_AUDIO); + } + + if (camera != PackageManager.PERMISSION_GRANTED) { + 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); + } + } +} diff --git a/sample/app/src/main/res/drawable/banner.png b/sample/app/src/main/res/drawable/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..7d2ec10f4a232f4dfd9cf030d06ab05f6b5d7acd GIT binary patch literal 12440 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 Lm1QcVOvC;Q%HCs% literal 0 HcmV?d00001 diff --git a/sample/app/src/main/res/drawable/led_connected.png b/sample/app/src/main/res/drawable/led_connected.png new file mode 100644 index 0000000000000000000000000000000000000000..d0cc8fc840efab6ae029876a8f1a320424b5d29a GIT binary patch literal 1046 zcmV+x1nK*UP)500006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy00Xv3L_t(o!?l=8Xk1ks#((E#nkF@g zCKHn~BymjChBh^jC|&fS&}f??A_`Jas!%P6-Bc=6QA82yLZt|bLKZ$2x@i?@OB=B% zSS4yHXkw*?JV`Vrahx`<$xLoO7tVzA=HxM*JKye}?|<(7-ShgNb09-W%9B)#4>D8V4~UP;Ceh#E=9mAQVBv@H{m z$0SY2Kg~&cOOlf=$Pr2IYQ8Y*#uQ6L<<7-d3-y_#yj2ZgNr!-UmjWG+4beSzir%qS zqSHOZ5)l9#6QFqA9fX5TRG03dv|!_M|NA-cgfT`-!bo}$I1S`1h8W~x|1qwNe2L_) z*t!+P+u0I6Oj%+5QXwxJV~$#3B!z%ZV9}bDh&SS5|1sjT z<9@E={Lu5J?dLX1s!O%ZcHriP?#yhI--o~OJ*|w4|H4qT-DW+Mg4yGB`Xg<)b8)hv z-m$aG!c=>mfk+$KQh(%ITk&Q|#sNZJ=jwES)?lv121&S+-r^#l#0oPNA0c}Z)5#R# z1#*Bj-cnZyvMGZ#amxV~&ENb$NmfzvbFJ^c<|Ma@*D1Js#?45#w z9bP9KY|0wUZNZ(k;?C66KIL_)O76GO`=aX7W?S*e6wG;GY@w5zQ$T&iv%Z5=7Vltv zQKQW|{{IHe7&k?>Q1unhkiW*ifox0;Tf&EJ)@5T%*TSs*1~9WQVr`&^=1nc+IJv&S zY^!;VTMO^DS+88Tg-9N@TD(!^MGf3{=TRIJ$Oxn^w2!*bKAY7Jd}Z6|l_h6ZnYYGZ z^m|$_KhMl;G%X0n1lU^hDs`c!Y<2?JVT@^CYD3-tdtLmCNydXpDJowTHL^l4g`0y(cNXK`5y~(twW;lalska^4O}YPo^i2d`Qs)%$fN zvRTpzNwG8lTuB!s?aJ0=Q6aD!co?WnUKRy`Kr*Ea16{xw;A>;da60e*4-Zw5pWuAp Q`~Uy|07*qoM6N<$g7ISUUjP6A literal 0 HcmV?d00001 diff --git a/sample/app/src/main/res/drawable/led_disconnected.png b/sample/app/src/main/res/drawable/led_disconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..20c81433372ad2cb93eabf57ad1c73bcf3180e19 GIT binary patch literal 904 zcmV;319$w1P)500006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy00SsVL_t(o!_AmKh#Ykk$3Jgw?)DEi z`{&F>IIuB=K@EXxqgIlrhy>CEVj%&CC2vVOBk6&pp{CO#Nf#wq9uOxa-O=p&OVT^DfLxHY zqx=+0`c{%REyz_#(L^i6v7#ta9LJMF{UE70Q2c)rqO5|2?U_kYSC;q zT_z9sZhYH&Bd@##Ty}Y6b8~a%6w&E)*xK51nbv_%)1LV<9Vc-dv$3&}TjFfD+w}W= zm+9&rjHD&tyvx1GPRUCdUVc;3+lgo11*&PU?Ck91eyj`z1A4ul%k*&q^HJIohT)L| z_ArHEcp%K{6TUD{m{XF>1J+y#ZeAJ=hc2&IfE6c96h+Ka8S_^>V3GfaIvo^+8FO?9ZX6sh^sY0Q^%F4>jAVCli1cA%+5ctVeb{-$?tmEl7dc7Wv zM&n2&MZI3n0@(%LGqZXld_`SZchZYyu3`MQo;BAtVcdGv)hNYDwkcq zY#b_SRR)8>-Z|rW9*c{Md#%a=5CAvL?DI*RxGR$SnvZTs@~7=8*Cjp85uzjM;w=64 znxtDtxP3_LzNFK6U5T8MbXQVu3V=w`pOVha>#`^SJ^(%ePL3{%UIF~kk@5_90{jO2 eY-Z1500006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy00QVqL_t(o!?l=CNK{c2hkx(QSfXTR zV~NnBLJDdq1R*el2(w}gqKz9V2;8`8XNoH~LakJYAV~-U7bb*Ah}gn3Ou~h^3UzD| z<=-^LCeyElisrj-oN39i*-gz~xjx?f}bprwyPHu#*L; z0oD`x#Of-{&qFMhDAWreJyDPcahHll;n5=~E{0GDq)kgpgK!w?>R@DKKcJWUo+IFz z>mi+-gnRcO5ZD2TXQibuG!zf$VLXt0VBR>bqXRNC6NA`owhavpu(4rUbsf0k2J_Si z($oZ&l|0C9tE_Yb`smm$;GA)L+S^kH*|yqRx9zIj3)AKpGCd844|@Y5ZT_hKeiO{t zZjb}O566(|YVSa{t*FRtyUuUJTy=!z=HTE#?_nh9>odXBNRln-rsFhscT2IDd{Jko z$s5^*Ic0?LOI=+iZy2y_N%@Y@__%*C(Wtb%yw{s0DaQ=+^{am{AkEB}B3ZViG>3a_ zO}@%DCT?s=KOOE+$UhLvl5%rRktKVpRQy*LFZu^lP#|Sw?DZ~MmNjon8g)!4FZT~7 z5;1u{xnFvVi;$7w&!smo;Ckr^2mmvVaDBb^AZ2B4$+BQ~etPZ*FDyWAu6H1T01OYu zPn26QCxKs%VS|I-<*1exw}0ifskkzaZfk>JFg1|sYB!Mg##$IS27EV8>FtG_oa7;b zL1=Dv1NjYHidP300xQO;OH0t$2w7Q)K~z=2)Kt8J{B9zx%Kf-a#_TM#wn8KVd3iek z+1XH10*@cV#KeBB%9CW8xGErqKWYMk$##{6z!wi8R)C6B`t5w+*?+t~>{SnN!K){c zF!0KF7rAR4cn6gE^;mRB(sfC500006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY4c7nw4c7reD4Tcy00US_L_t(o!?l=AXjE4ihM(^yF(GLD z3#ldzsG-ItMq#u{sBtBYEn*9CVO6L_kk*x52qJD2q2fYHH&t+)gfs^J|HcU zh83Ptl8#IAas@dkDLJD@jK`(;kd#c!6slEH$&3cDq;G(eSxHCw;MuRxa|Z@{AU-e= z_X?q`8mbzhx(VjjO!dC!ft_ZitT2*30<7&LqV^`E;sAq8wQvl}j$)4<+9gMVFw-+ut~_S167 zCTKk10=fsR^}EYUgVY0i0*ySql|Kl;lk0H*tjmc3yZk@?G&>L&C-DL3_%44CUk`qS z7hNvr;A#oiK_u$AJtfRCCrni8ZFfZ*B$)?P2CU)U=wOEXV0?6PvK=IrV$E^pecDtO1X4d`xUHd2y&nPl$Hf%fsUQswOYxco{e8P+F0#)NV67H{DTXkF(2OXMlt7_g&~|gO}Yf z*8gUV&09LLM!mZMW-pow_mr7^J<~(nUP*C9qwggZ=6b4(NqQP0#E7Kr`JA^aC7pST z+lR@zCTVq8S0W9Pev&kn13*$zo22HbE{o;>n}Mysiu7gCLZC3cr920^fnR`2X7)Un a_WuLz`(2z*w#ruk0000 + + + + + + + \ No newline at end of file diff --git a/sample/app/src/main/res/layout/configure_account.xml b/sample/app/src/main/res/layout/configure_account.xml new file mode 100644 index 000000000..6b9a232e7 --- /dev/null +++ b/sample/app/src/main/res/layout/configure_account.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + +