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 000000000..7d2ec10f4 Binary files /dev/null and b/sample/app/src/main/res/drawable/banner.png differ 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 000000000..d0cc8fc84 Binary files /dev/null and b/sample/app/src/main/res/drawable/led_connected.png differ 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 000000000..20c814333 Binary files /dev/null and b/sample/app/src/main/res/drawable/led_disconnected.png differ diff --git a/sample/app/src/main/res/drawable/led_error.png b/sample/app/src/main/res/drawable/led_error.png new file mode 100644 index 000000000..67418cc43 Binary files /dev/null and b/sample/app/src/main/res/drawable/led_error.png differ diff --git a/sample/app/src/main/res/drawable/led_inprogress.png b/sample/app/src/main/res/drawable/led_inprogress.png new file mode 100644 index 000000000..cf7263481 Binary files /dev/null and b/sample/app/src/main/res/drawable/led_inprogress.png differ diff --git a/sample/app/src/main/res/layout/call.xml b/sample/app/src/main/res/layout/call.xml new file mode 100644 index 000000000..1ab7d6c83 --- /dev/null +++ b/sample/app/src/main/res/layout/call.xml @@ -0,0 +1,19 @@ + + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + + + + +