diff --git a/.gitignore b/.gitignore index 8730854e9..622ae11a7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,55 +6,21 @@ .settings adb.pid bc-android.keystore -bin build -certdata.txt -check_tools.mk -default.properties -doc -gen -liblinphone-junit-report.xml -liblinphone-sdk/android-* -liblinphonetester_*.zip -libs -libs-debug -linphone-android.iml -linphone-junit-report*.xml -linphonetester_*.zip +*.iml lint.xml local.properties -Makefile -obj -proguard-project.txt -project.properties res/.DS_Store res/raw/lpconfig.xsd -submodules/externals/build/cunit/CUnit/ -submodules/externals/build/ffmpeg/arm/ -submodules/externals/build/ffmpeg/x86 -submodules/externals/build/libvpx/arm -submodules/externals/build/libvpx/x86 -submodules/externals/build/openh264/arm -submodules/externals/build/openh264/x86 -submodules/externals/ffmpeg/arm/ -tests/*$py.class -tests/build.xml -tests/project.properties -ant_password.properties -liblinphone_tester/liblinphonetester_* -liblinphone_tester/tests.output -tests/linphonetester_* -tests/tests.output -WORK .d -google-services.json .*clang* **/*.iml -src/linphone-wrapper -liblinphone_tester/res/raw/ **/.classpath **/.project **/*.kdev4 -liblinphone-sdk/res/ **/.vscode res/value-hi_IN +linphone-sdk-android/*.aar +app/release +keystore.properties +app/src/main/res/xml/contacts.xml diff --git a/.gitlab-ci-files/job-android.yml b/.gitlab-ci-files/job-android.yml new file mode 100644 index 000000000..34cfba452 --- /dev/null +++ b/.gitlab-ci-files/job-android.yml @@ -0,0 +1,33 @@ +job-android: + + stage: build + tags: [ "docker-android" ] + image: gitlab.linphone.org:4567/bc/public/linphone-android/bc-dev-android:28 + + before_script: + - if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then eval $(ssh-agent -s); fi + - if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi + + script: + - sdkmanager + - scp -oStrictHostKeyChecking=no $DEPLOY_SERVER:$ANDROID_KEYSTORE_PATH app/ + - scp -oStrictHostKeyChecking=no $DEPLOY_SERVER:$ANDROID_GOOGLE_SERVICES_PATH app/ + - echo storePassword=$ANDROID_KEYSTORE_PASSWORD > keystore.properties + - echo keyPassword=$ANDROID_KEYSTORE_KEY_PASSWORD >> keystore.properties + - echo keyAlias=$ANDROID_KEYSTORE_KEY_ALIAS >> keystore.properties + - echo storeFile=$ANDROID_KEYSTORE_FILE >> keystore.properties + - ./gradlew assembleDebug + - ./gradlew assembleRelease + + artifacts: + paths: + - ./app/build/outputs/apk/debug/linphone-android-debug-*.apk + - ./app/build/outputs/apk/release/linphone-android-release-*.apk + when: always + expire_in: 1 week + + +.scheduled-job-android: + extends: job-android + only: + - schedules \ No newline at end of file diff --git a/.gitlab-ci-files/job-upload.yml b/.gitlab-ci-files/job-upload.yml new file mode 100644 index 000000000..31e1d1a50 --- /dev/null +++ b/.gitlab-ci-files/job-upload.yml @@ -0,0 +1,12 @@ +job-android-upload: + + stage: deploy + tags: [ "deploy" ] + + only: + - schedules + dependencies: + - job-android + + script: + - cd app/build/outputs/apk/ && rsync ./debug/*.apk $DEPLOY_SERVER:$ANDROID_DEPLOY_DIRECTORY \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..0e65baf26 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,19 @@ +################################################# +# Base configuration +################################################# + + + +################################################# +# Platforms to test +################################################# + + +include: + - '.gitlab-ci-files/job-android.yml' + - '.gitlab-ci-files/job-upload.yml' + + +stages: + - build + - deploy diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 379a64301..000000000 --- a/.gitmodules +++ /dev/null @@ -1,122 +0,0 @@ -[submodule "submodules/linphone"] - path = submodules/linphone - url = https://gitlab.linphone.org/BC/public/linphone.git -[submodule "submodules/externals/gsm"] - path = submodules/externals/gsm - url = https://gitlab.linphone.org/BC/public/external/gsm.git -[submodule "submodules/externals/speex"] - path = submodules/externals/speex - url = https://gitlab.linphone.org/BC/public/external/speex.git -[submodule "submodules/externals/ffmpeg"] - path = submodules/externals/ffmpeg - url = https://gitlab.linphone.org/BC/public/external/ffmpeg.git - ignore = dirty -[submodule "submodules/externals/x264"] - path = submodules/externals/x264 - url = https://gitlab.linphone.org/BC/public/external/x264.git - ignore = dirty -[submodule "submodules/msx264"] - path = submodules/msx264 - url = https://gitlab.linphone.org/BC/public/msx264.git -[submodule "submodules/externals/opencore-amr"] - path = submodules/externals/opencore-amr - url = https://gitlab.linphone.org/BC/public/external/opencore-amr.git - ignore = dirty -[submodule "submodules/msamr"] - path = submodules/msamr - url = https://gitlab.linphone.org/BC/public/msamr.git -[submodule "submodules/externals/libvpx"] - path = submodules/externals/libvpx - url = https://gitlab.linphone.org/BC/public/external/libvpx.git - ignore = dirty -[submodule "submodules/bzrtp"] - path = submodules/bzrtp - url = https://gitlab.linphone.org/BC/public/bzrtp.git -[submodule "submodules/externals/srtp"] - path = submodules/externals/srtp - url = https://gitlab.linphone.org/BC/public/external/srtp.git -[submodule "submodules/mssilk"] - path = submodules/mssilk - url = https://gitlab.linphone.org/BC/public/mssilk.git -[submodule "submodules/bcg729"] - path = submodules/bcg729 - url = https://gitlab.linphone.org/BC/public/bcg729.git -[submodule "submodules/belle-sip"] - path = submodules/belle-sip - url = https://gitlab.linphone.org/BC/public/belle-sip.git -[submodule "submodules/externals/libxml2"] - path = submodules/externals/libxml2 - url = https://gitlab.linphone.org/BC/public/external/libxml2.git - ignore = dirty -[submodule "submodules/externals/libupnp"] - path = submodules/externals/libupnp - url = https://gitlab.linphone.org/BC/public/external/libupnp.git -[submodule "submodules/externals/opus"] - path = submodules/externals/opus - url = https://gitlab.linphone.org/BC/public/external/opus.git - ignore = dirty -[submodule "submodules/mswebrtc"] - path = submodules/mswebrtc - url = https://gitlab.linphone.org/BC/public/mswebrtc.git -[submodule "submodules/msopenh264"] - path = submodules/msopenh264 - url = https://gitlab.linphone.org/BC/public/msopenh264.git -[submodule "submodules/externals/openh264"] - path = submodules/externals/openh264 - url = https://gitlab.linphone.org/BC/public/external/openh264.git - ignore = dirty -[submodule "submodules/mscodec2"] - path = submodules/mscodec2 - url = https://gitlab.linphone.org/BC/public/mscodec2.git -[submodule "submodules/bctoolbox"] - path = submodules/bctoolbox - url = https://gitlab.linphone.org/BC/public/bctoolbox.git -[submodule "submodules/externals/mbedtls"] - path = submodules/externals/mbedtls - url = https://gitlab.linphone.org/BC/public/external/mbedtls.git - ignore = dirty -[submodule "submodules/cmake-builder"] - path = submodules/cmake-builder - url = https://gitlab.linphone.org/BC/public/linphone-cmake-builder.git -[submodule "submodules/externals/bv16-floatingpoint"] - path = submodules/externals/bv16-floatingpoint - url = https://gitlab.linphone.org/BC/public/external/bv16-floatingpoint.git -[submodule "submodules/belr"] - path = submodules/belr - url = https://gitlab.linphone.org/BC/public/belr.git -[submodule "submodules/belcard"] - path = submodules/belcard - url = https://gitlab.linphone.org/BC/public/belcard.git -[submodule "submodules/bcunit"] - path = submodules/bcunit - url = https://gitlab.linphone.org/BC/public/bcunit.git -[submodule "submodules/externals/vo-amrwbenc"] - path = submodules/externals/vo-amrwbenc - url = https://gitlab.linphone.org/BC/public/external/vo-amrwbenc.git -[submodule "submodules/externals/codec2"] - path = submodules/externals/codec2 - url = https://gitlab.linphone.org/BC/public/external/codec2.git -[submodule "submodules/externals/libjpeg-turbo"] - path = submodules/externals/libjpeg-turbo - url = https://gitlab.linphone.org/BC/public/external/libjpeg-turbo.git -[submodule "submodules/mediastreamer2"] - path = submodules/mediastreamer2 - url = https://gitlab.linphone.org/BC/public/mediastreamer2.git -[submodule "submodules/oRTP"] - path = submodules/oRTP - url = https://gitlab.linphone.org/BC/public/ortp.git -[submodule "submodules/bcmatroska2"] - path = submodules/bcmatroska2 - url = https://gitlab.linphone.org/BC/public/bcmatroska2.git -[submodule "submodules/externals/xerces-c"] - path = submodules/externals/xerces-c - url = https://gitlab.linphone.org/BC/public/external/xerces-c.git -[submodule "submodules/externals/libxsd"] - path = submodules/externals/libxsd - url = https://gitlab.linphone.org/BC/public/external/libxsd.git -[submodule "submodules/externals/soci"] - path = submodules/externals/soci - url = https://gitlab.linphone.org/BC/public/external/soci.git -[submodule "submodules/externals/zxing-cpp"] - path = submodules/externals/zxing-cpp - url = https://gitlab.linphone.org/BC/public/external/zxing-cpp.git diff --git a/.tx/config b/.tx/config index 1f6efb2af..35511f253 100644 --- a/.tx/config +++ b/.tx/config @@ -5,6 +5,6 @@ minimum_perc = 1 type = ANDROID [linphone-android.stringsxml] -file_filter = res/values-/strings.xml -source_file = res/values/strings.xml +file_filter = app/src/main/res/values-/strings.xml +source_file = app/src/main/res/values/strings.xml source_lang = en diff --git a/AndroidManifestSdk.xml b/AndroidManifestSdk.xml deleted file mode 100755 index 17de7b7ee..000000000 --- a/AndroidManifestSdk.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/CHANGELOG.md b/CHANGELOG.md index c4e42c543..db3a8eb76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,39 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. -## [Incomming] -- feature: support of H265 codec. -- feature: use TextureView instead of GL2JNIView, easier to use and will fix issues +## [4.1] - 2019-xx-xx + +### Improvements +- Improved UI. +- Added adaptive icon for Android 8+. +- Use of binary SDK hosted in Maven repository instead of having to build it manually. +- Asynchronous fetch of native contacts. +- Removed unused graphical resources and improved some existing ones. +- Updated translations. +- Updated sliders used to answer or hangup incoming call so they can be used with accessibility tools. +- Reworked settings, new ones are available, some unused have been removed. +- Video overlay also displays local preview now. +- In About fragment, the license text is a link to the full license on gnu.org. + +### Features +- Added new end to end LIME encryption for instant messaging, both for single chat rooms and group ones. +- Send multiple files with text in the same chat message. +- Allow multiple images to be shared from an external app through linphone at once. +- Support of H265 codec. +- Use TextureView instead of GL2JNIView, easier to use and will fix issues. +- Send SMS to invite your friends in using Linphone. +- Reply to / mark as read chat message in notification. +- Answer or hangup calls in notification. +- Setting to automatically download incoming files, either always, never or depending on their sizes. This adds the feature of having images received by file transfer in notifications on Android 8+. +- Call recording. +- Get remote provisioning URL from QR code scan. +- Allow rich input while composing chat message to easily send images from keyboard. +- Animated GIFs are no longer displayed as static images. +- A dark mode is available. +- List all calls with the same SIP address in history detail view. + +### Fixes +- Fixed basic chat room with same correspondant with multiple local accounts always displaying the same one when clicking on it in chat rooms list view. ## [4.0.1] - 2018-06-26 @@ -34,7 +64,6 @@ Group changes to describe their impact on the project, as follows: ### Fixed - issue with changing push notification token not passed to library, possibly resulting in a loss of incoming calls. - ## [3.3.0] - 2017-10-18 ### Added diff --git a/README.md b/README.md index ad69dc51b..bacadcfde 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,104 @@ -[![pipeline status](https://gitlab.linphone.org/BC/public/linphone-android/badges/master/pipeline.svg)](https://gitlab.linphone.org/BC/public/linphone-android/commits/master) +[![pipeline status](https://gitlab.linphone.org/BC/public/linphone-sdk/badges/master/pipeline.svg)](https://gitlab.linphone.org/BC/public/linphone-android/commits/feature/release-4.1) Linphone is a free VoIP and video softphone based on the SIP protocol. -# COMPILATION INSTRUCTIONS +# What's new -## To build liblinphone for Android, you must: +Now the default way of building linphone-android is to download the AAR SDK in our maven repository. +Compared to previous versions, this project no longer uses submodules developper has to build in order to get a working app. +However, if you wish to use a locally compiled SDK see below how to proceed. -1. Download the latest Android sdk with platform-tools and tools updated to latest revision, then add both 'tools' and 'platform-tools' folders in your path and the android-sdk folder to ANDROID_HOME environment variable. +We offer different flavors for the SDK in our maven repository: org.linphone.no-video (a build without video) and org.linphone.legacy (old java wrapper if you didn't migrate your app code to the new one yet). -2. Download the latest Android NDK from google and add it to your path (no symlink !!!) and ANDROID_NDK environment variable. +The repository structure has also been cleaned and updated, and changing the package name can now be done in a single step. +This allows developpers to keep a stable version as well as a developpment one on the same device easily. -3. Install _yasm_, _nasm_ (For OpenH224 support only), _python_, _pkg_config_, _doxygen_, _graphviz_ and _cmake(>=3.12)_. - * On 64 bits linux systems you'll need the _ia32-libs_ package. - * With the latest Debian (multiarch), you need this: - * `dpkg --add-architecture i386` - * `aptitude update` - * `aptitude install libstdc++6:i386 libgcc1:i386 zlib1g:i386 libncurses5:i386` +# Building the app -4. Run `./prepare.py` in the top level directory. This will configure the build and generate a Makefile in the top level directory. Some options can be passed to choose what you want to include in the build and the platforms for which you want to build. Use `./prepare.py --help` to see what these options are. - -5. Run the Makefile script in the top level directory, `make`. - -6. _(optional)_ To install the generated apk into a plugged device, run `make install`. - -7. _(optional)_ To generate a liblinphone SDK zip containing a full jar and native libraries, run `make liblinphone-android-sdk` - -8. _(optional)_ To generate a libmediastreamer2 SDK zip containing a full jar and native libraries, run `make mediastreamer2-sdk` - -9. _(optional)_ To generate a signed apk to publish on the Google Play, run `make release`. Make sure you filled the gradle.properties values for version.name, store file, store password, key alias and key password to correctly sign the generated apk: - * RELEASE_STORE_FILE="" - * RELEASE_STORE_PASSWORD= - * RELEASE_KEY_ALIAS= - * RELEASE_KEY_PASSWORD= - - If you don't, the passwords will be asked at the signing phase. - -10. _(optional)_ Once you compiled the libraries succesfully with 'make', you can reduce the compilation time using 'make quick': it will only generate a new APK from java files. - -## To create an apk with a different package name - -You need to edit the build.gradle file: - -1. look for the function named "getPackageName()" and change it value accordingly -2. also update the values in the AndroidManifest file where the comment appears -3. change the package name also in the files: res/xml/syncadapter.xml, res/xml/contacts.xml and res/values/non_localizable_custom where appears -4. run again the Makefile script by calling "make" - -## To run the liblinphone test suite on android - -Simply run `make liblinphone_tester`. This will be build everything, generate an apk, and install it on the connected device if any. - -You can speed up the compilation by using ccache (compiler cache, see [ccache.samba.org](https://ccache.samba.org/)). Give the *"-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"* options to the *prepare.py* script. - -# PUSH NOTIFICATION - -## Firebase - -To enable firebase in Linphone, just add your 'google-service.json' in project root, add your key at 'push_sender_id' and add 'firebase' at 'push_type' in 'res/values/non_localizable_custom.xml' -Be sure to have all services for Firebase in your 'AndroidManifest.xml' - -# TROUBLESHOOTING - -If you encounter the following issue: +If you have Android Studio, simply open the project, wait for the gradle synchronization and then build/install the app. +It will download the linphone library from our Maven repository as an AAR file so you don't have to build anything yourself. +If you don't have Android Studio, you can build and install the app using gradle: ``` -E/dalvikvm( 2465): dlopen("/data/app-lib/org.linphone-1/liblinphone-armeabi-v7a.so") failed: -Cannot load library: soinfo_relocate(linker.cpp:975): cannot locate symbol "rand" referenced -by "liblinphone-armeabi-v7a.so" +./gradlew assembleDebug +``` +will compile the APK file (assembleRelease to instead if you want to build a release package), and then +``` +./gradlew installDebug +``` +to install the generated APK in the previous step (use installRelease instead if you built a release package). + +APK files are stored within ```./app/build/outputs/apk/debug/``` and ```./app/build/outputs/apk/release/``` directories. + +## Building a local SDK + +1. Clone the linphone-sdk repository from out gitlab: +``` +git clone https://gitlab.linphone.org/BC/public/linphone-sdk.git --recursive ``` -It's because you have installed the android-21 platform (which is chosen automatically because it's the most recent) and you deployed the apk on a android < 5 device. - -To fix this, in the Makefile, force *ANDROID_MOST_RECENT_TARGET=android-19*. - -If you encounter troubles with the make clean target and you are using the 8e android ndk, the solution can be found [here](https://groups.google.com/forum/?fromgroups=#!topic/android-ndk/3wIbb-h3nDU). - -If you built the app using eclipse, ensure you ran at least once the make command (see above steps 0 to 3) ! Else you'll have this exceptions: +2. Follow the instructions in the linphone-sdk/README file to build the SDK. +3. Edit in the linphone-sdk-android folder of this project the symbolic link (debug and/or release) to the generated AAR. +We recommend to at least create the link for the release AAR that can be used for debug APK flavor because it is smaller and will reduce the time required to install the APK. ``` -FATAL EXCEPTION: main -java.lang.ExceptionInInitializerError -... -Caused by: java.lang.UnsatisfiedLinkError: Couldn't load linphone-armeabi-v7a: findLibrary -returned null +ln -s /linphone-sdk/build/linphone-sdk/bin/outputs/aar/linphone-sdk-android-release.aar linphone-sdk-android/linphone-sdk-android-release.aar +ln -s /linphone-sdk/build/linphone-sdk/bin/outputs/aar/linphone-sdk-android-debug.aar linphone-sdk-android/linphone-sdk-android-debug.aar ``` -# BUILD OPTIONS +4. Rebuild the app in Android Studio. -The build options are to be passed to the *prepare.py* script. For example to enable the x264 encoder give the *"-DENABLE_X264=YES"* to *prepare.py*. +## Native debugging -The available options can be listed with the `./prepare.py --list-features` +1. Install LLDB from SDK Tools in Android-studio. + +2. In Android-studio go to Run->Edit Configurations->Debugger. + +3. Select 'Dual' or 'Native' and add the path to linphone-sdk libraries. + +4. Open native file and put your breakpoint on it. + +5. Make sure you are using the debug AAR in the app/build.gradle script and not the release one (to have faster builds by default the release AAR is used even for debug APK flavor). + +6. Debug app. + +## Create an APK with a different package name + +Before the 4.1 release, there were a lot of files to edit to change the package name. +Now, simply edit the app/build.gradle file and change the value returned by method ```getPackageName()``` +The next build will automatically use this value everywhere thanks to ```manifestPlaceholders``` feature of gradle and Android. + +You may have already noticed that the app installed by Android Studio has ```org.linphone.debug``` package name. +If you build the app as release, the package name will be ```org.linphone```. + +## Firebase push notifications + +Now that Google Cloud Messaging has been deprecated and will be completely removed on April 11th 2019, the only official way of using push notifications is through Firebase. + +However to make Firebase push notifications work, the project needs to have a file named app/google-services.json that contains some confidential informations, so you won't find it (it has been added to the .gitignore file). +This means that if you compile this project, you won't have push notification feature working in the app! + +To enable them, just add your own ```google-services.json``` in the app folder. + +## Translations + +We use transifex so the community can translate the strings of the app in their own language. + +Note for developpers: here's how to push/pull string resources to/from transifex: +``` +tx pull -af +``` +to update local translations with latest transifex changes +``` +tx push -s -f --no-interactive +``` +to push new strings to transifex so they can be translated. # CONTRIBUTIONS In order to submit a patch for inclusion in linphone's source code: -1. First make sure your patch applies to latest git sources before submitting: patches made to old versions can't be merged. -2. Fill out and send us an email with the link of pullrequest and the [Contributor Agreement](http://www.belledonne-communications.com/downloads/Belledonne_communications_CA.pdf) for your patch to be included in the git tree. The goal of this agreement to grant us peaceful exercise of our rights on the linphone source code, while not losing your rights on your contribution. +1. First make sure your patch applies to latest git sources before submitting: patches made to old versions can't and won't be merged. +2. Fill out and send us an email with the link of pullrequest and the [Contributor Agreement](http://www.belledonne-communications.com/downloads/Belledonne_communications_CA.pdf) for your patch to be included in the git tree. +The goal of this agreement to grant us peaceful exercise of our rights on the linphone source code, while not losing your rights on your contribution. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..3bb12fad6 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,198 @@ +apply plugin: 'com.android.application' + +static def getPackageName() { + return "org.linphone" +} + +static def firebaseEnabled() { + File googleFile = new File('app/google-services.json') + return googleFile.exists() +} + +static def isLocalDebugAarAvailable() { + File debugAar = new File('linphone-sdk-android/linphone-sdk-android-debug.aar') + return debugAar.exists() +} + +static def isLocalReleaseAarAvailable() { + File releaseAar = new File('linphone-sdk-android/linphone-sdk-android-release.aar') + return releaseAar.exists() +} + +static def isLocalAarAvailable() { + return isLocalDebugAarAvailable() || isLocalReleaseAarAvailable() +} + +///// Exclude Files ///// + +def excludeFiles = [] +if (!firebaseEnabled()) { + excludeFiles.add('**/Firebase*') + println '[Push Notification] Firebase disabled' +} + +def excludePackage = [] + +excludePackage.add('**/gdb.*') +excludePackage.add('**/libopenh264**') +excludePackage.add('**/**tester**') +excludePackage.add('**/LICENSE.txt') + + +def gitVersion = new ByteArrayOutputStream() + +task getGitVersion { + exec { + commandLine 'git', 'describe', '--always' + standardOutput = gitVersion + } + doLast { + gitVersion = gitVersion.toString().trim() + println("Git version: " + gitVersion) + } +} + +project.tasks['preBuild'].dependsOn 'getGitVersion' + +///////////////////////// + +repositories { + maven { + // Replace snapshots by releases for releases ! + url "https://linphone.org/releases/maven_repository" + } +} + +android { + lintOptions { + abortOnError false + } + + compileSdkVersion 28 + defaultConfig { + minSdkVersion 21 + targetSdkVersion 28 + versionCode 4119 + versionName "4.1" + applicationId getPackageName() + multiDexEnabled true + manifestPlaceholders = [linphone_address_mime_type:"vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"] + } + + applicationVariants.all { variant -> + variant.outputs.all { + outputFileName = "linphone-android-${variant.buildType.name}-${gitVersion.toString().trim()}.apk" + } + } + + def keystorePropertiesFile = rootProject.file("keystore.properties") + def keystoreProperties = new Properties() + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + + signingConfigs { + release { + storeFile file(keystoreProperties['storeFile']) + storePassword keystoreProperties['storePassword'] + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + } + } + + buildTypes { + release { + minifyEnabled true + signingConfig signingConfigs.release + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + + resValue "string", "sync_account_type", getPackageName() + ".sync" + resValue "string", "file_provider", getPackageName() + ".provider" + resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address" + + if (!firebaseEnabled()) { + resValue "string", "gcm_defaultSenderId", "none" + } + } + debug { + applicationIdSuffix ".debug" + debuggable true + jniDebuggable true + versionNameSuffix '-debug' + + resValue "string", "sync_account_type", getPackageName() + ".sync" + resValue "string", "file_provider", getPackageName() + ".provider" + resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address" + + if (!firebaseEnabled()) { + resValue "string", "gcm_defaultSenderId", "none" + } + } + } + + sourceSets { + main { + java.excludes = excludeFiles + + packagingOptions { + excludes = excludePackage + } + } + } + + packagingOptions { + pickFirst 'META-INF/NOTICE' + pickFirst 'META-INF/LICENSE' + exclude 'META-INF/MANIFEST.MF' + } +} + +dependencies { + if (firebaseEnabled()) { + implementation 'com.google.firebase:firebase-messaging:17.5.0' + } + implementation 'com.android.billingclient:billing:1.2' + implementation 'org.apache.commons:commons-compress:1.18' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'com.google.android.material:material:1.1.0-alpha04' + implementation 'com.google.android:flexbox:1.1.0' + implementation 'com.github.bumptech.glide:glide:4.9.0' + + if (isLocalAarAvailable()) { + if (isLocalReleaseAarAvailable()) { + //debug AAR is a lot bigger than release one, and APK install time will be much longer so use release one for day-to-day development + implementation project(path: ":linphone-sdk-android", configuration: 'release') + } else { + releaseImplementation project(path: ":linphone-sdk-android", configuration: 'release') + debugImplementation project(path: ":linphone-sdk-android", configuration: 'debug') + } + } else { + debugImplementation "org.linphone:linphone-sdk-android-debug:4.1+" + releaseImplementation "org.linphone:linphone-sdk-android:4.1+" + } +} +if (firebaseEnabled()) { + apply plugin: 'com.google.gms.google-services' +} + +task generateContactsXml(type: Copy) { + from 'contacts.xml' + into "src/main/res/xml/" + filter { + line -> line + .replaceAll('%%AUTO_GENERATED%%', 'This file has been automatically generated, do not edit or commit !') + .replaceAll('%%PACKAGE_NAME%%', getPackageName()) + + } +} +project.tasks['preBuild'].dependsOn 'generateContactsXml' + +apply plugin: "com.diffplug.gradle.spotless" +spotless { + java { + target '**/*.java' + googleJavaFormat('1.6').aosp() + removeUnusedImports() + } +} +project.tasks['preBuild'].dependsOn 'spotlessApply' diff --git a/app/contacts.xml b/app/contacts.xml new file mode 100644 index 000000000..7836193c0 --- /dev/null +++ b/app/contacts.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 000000000..c26aa7702 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,57 @@ +{ + "project_info": { + "project_number": "929724111839", + "firebase_url": "https://linphone-android-8a563.firebaseio.com", + "project_id": "linphone-android-8a563", + "storage_bucket": "linphone-android-8a563.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:929724111839:android:4662ea9a056188c4", + "android_client_info": { + "package_name": "org.linphone" + } + }, + "oauth_client": [ + { + "client_id": "929724111839-co5kffto4j7dets7oolvfv0056cvpfbl.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "org.linphone", + "certificate_hash": "85463a95603f7b6331899b74b85d53d043dcd500" + } + }, + { + "client_id": "929724111839-v5so1tcd65iil7dd7sde8jgii44h8luf.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCKrwWhkbA7Iy3wpEI8_ZvKOMp5jf6vV6A" + } + ] + }, + { + "client_info": { + "mobilesdk_app_id": "1:929724111839:android:3cf90ee1d2f8fcb6", + "android_client_info": { + "package_name": "org.linphone.debug" + } + }, + "oauth_client": [ + { + "client_id": "929724111839-v5so1tcd65iil7dd7sde8jgii44h8luf.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyCKrwWhkbA7Iy3wpEI8_ZvKOMp5jf6vV6A" + } + ] + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..efab2ccf6 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1 @@ +-dontwarn org.apache.** diff --git a/AndroidManifest.xml b/app/src/main/AndroidManifest.xml similarity index 71% rename from AndroidManifest.xml rename to app/src/main/AndroidManifest.xml index 8a39fc3b7..dee9a8d64 100755 --- a/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,153 +1,163 @@ - + - - - - - - - - - - + + + + + + + + android:required="false" /> - - - + + + + android:required="false" /> - - + + - + - + - - + + - - - - - - + + + + + + - + - + + + android:xlargeScreens="true" /> + android:largeHeap="true" + android:roundIcon="@mipmap/ic_launcher_round"> - + - + + + + + + - - + + - + - - + + - + - + - - + + - - - - - + + + + + - - - - - - + + + + + + + + + + + + + + + - - - - - + android:noHistory="true" + android:showWhenLocked="true" + android:turnScreenOn="true" + android:theme="@style/LinphoneStyleLight"> - + + android:noHistory="true" + android:theme="@style/LinphoneStyleLight"> - + + android:noHistory="true" + android:showWhenLocked="true" + android:theme="@style/LinphoneStyleLight"> - + - + + - - + + + android:label="@string/service_name" /> - + + android:resource="@xml/syncadapter" /> + android:resource="@xml/contacts" /> - + + android:resource="@xml/authenticator" /> - + - + android:enabled="false"/> - + - + - + - - - - + - + - + - - - - - - + + android:resource="@xml/provider_paths" /> - + + + diff --git a/src/android/com/android/vending/billing/IInAppBillingService.aidl b/app/src/main/java/com/android/vending/billing/IInAppBillingService.aidl similarity index 100% rename from src/android/com/android/vending/billing/IInAppBillingService.aidl rename to app/src/main/java/com/android/vending/billing/IInAppBillingService.aidl diff --git a/app/src/main/java/org/linphone/LinphoneActivity.java b/app/src/main/java/org/linphone/LinphoneActivity.java new file mode 100644 index 000000000..edd3ceaed --- /dev/null +++ b/app/src/main/java/org/linphone/LinphoneActivity.java @@ -0,0 +1,2046 @@ +package org.linphone; + +/* +LinphoneActivity.java +Copyright (C) 2017 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.Manifest; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.Dialog; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.app.KeyguardManager; +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.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.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import org.linphone.LinphoneManager.AddressType; +import org.linphone.assistant.AssistantActivity; +import org.linphone.assistant.RemoteProvisioningLoginActivity; +import org.linphone.call.CallActivity; +import org.linphone.call.CallIncomingActivity; +import org.linphone.call.CallOutgoingActivity; +import org.linphone.chat.ChatMessagesFragment; +import org.linphone.chat.ChatRoomCreationFragment; +import org.linphone.chat.ChatRoomsFragment; +import org.linphone.chat.DevicesFragment; +import org.linphone.chat.GroupInfoFragment; +import org.linphone.chat.ImdnFragment; +import org.linphone.chat.ImdnOldFragment; +import org.linphone.compatibility.Compatibility; +import org.linphone.contacts.ContactAddress; +import org.linphone.contacts.ContactDetailsFragment; +import org.linphone.contacts.ContactEditorFragment; +import org.linphone.contacts.ContactsFragment; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.AuthInfo; +import org.linphone.core.Call; +import org.linphone.core.Call.State; +import org.linphone.core.CallLog; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatRoom; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.Factory; +import org.linphone.core.ProxyConfig; +import org.linphone.core.Reason; +import org.linphone.core.RegistrationState; +import org.linphone.core.tools.Log; +import org.linphone.fragments.AboutFragment; +import org.linphone.fragments.DialerFragment; +import org.linphone.fragments.EmptyFragment; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.fragments.StatusFragment; +import org.linphone.history.HistoryDetailFragment; +import org.linphone.history.HistoryFragment; +import org.linphone.purchase.InAppPurchaseActivity; +import org.linphone.recording.RecordingsFragment; +import org.linphone.settings.AccountSettingsFragment; +import org.linphone.settings.AudioSettingsFragment; +import org.linphone.settings.LinphonePreferences; +import org.linphone.settings.SettingsFragment; +import org.linphone.utils.DeviceUtils; +import org.linphone.utils.IntentUtils; +import org.linphone.utils.LinphoneGenericActivity; +import org.linphone.utils.LinphoneUtils; +import org.linphone.utils.PushNotificationUtils; +import org.linphone.views.AddressText; +import org.linphone.xmlrpc.XmlRpcHelper; +import org.linphone.xmlrpc.XmlRpcListenerBase; + +public class LinphoneActivity extends LinphoneGenericActivity + implements OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback { + public static final int ANDROID_APP_SETTINGS_ACTIVITY = 300; + + 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_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 sInstance; + + public String addressWaitingToBeCalled; + + private StatusFragment mStatusFragment; + private TextView mMissedCalls, mMissedChats; + private RelativeLayout mContacts, mHistory, mDialer, mChat; + private View mContactsSelected, mHistorySelected, mDialerSelected, mChatSelected; + private LinearLayout mTopBar; + private TextView mTopBarTitle; + private ImageView mCancel; + private FragmentsAvailable mPendingFragmentTransaction, mCurrentFragment, mLeftFragment; + private Fragment mFragment; + private Fragment.SavedState mDialerSavedState; + private boolean mNewProxyConfig; + private boolean mEmptyFragment = false; + private boolean mIsTrialAccount = false; + private OrientationEventListener mOrientationHelper; + private CoreListenerStub mListener; + private LinearLayout mTabBar; + private DrawerLayout mSideMenu; + private RelativeLayout mSideMenuContent, mQuitLayout, mDefaultAccount; + private ListView mAccountsList, mSideMenuItemList; + private ImageView mMenu; + private List mSideMenuItems; + private boolean mCallTransfer = false; + private boolean mIsOnBackground = false; + private int mAlwaysChangingPhoneAngle = -1; + + public static boolean isInstanciated() { + return sInstance != null; + } + + public static LinphoneActivity instance() { + if (sInstance != null) return sInstance; + throw new RuntimeException("LinphoneActivity not instantiated yet"); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + // This must be done before calling super.onCreate(). + super.onCreate(savedInstanceState); + + LinphoneService.instance().removeForegroundServiceNotificationIfPossible(); + + if (getResources().getBoolean(R.bool.orientation_portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + 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 + && LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null + && LinphonePreferences.instance().isFirstLaunch())) { + if (LinphonePreferences.instance().getAccountCount() > 0) { + LinphonePreferences.instance().firstLaunchSuccessful(); + } else { + startActivity(new Intent().setClass(this, AssistantActivity.class)); + finish(); + return; + } + } + + if (getResources().getBoolean(R.bool.use_linphone_tag)) { + if (getPackageManager() + .checkPermission( + Manifest.permission.WRITE_SYNC_SETTINGS, getPackageName()) + != PackageManager.PERMISSION_GRANTED) { + checkSyncPermission(); + } + } + + setContentView(R.layout.main); + sInstance = this; + mPendingFragmentTransaction = FragmentsAvailable.UNKNOW; + + initButtons(); + initSideMenu(); + + mCurrentFragment = FragmentsAvailable.EMPTY; + if (savedInstanceState == null) { + changeCurrentFragment(FragmentsAvailable.DIALER, getIntent().getExtras()); + } else { + mCurrentFragment = + (FragmentsAvailable) savedInstanceState.getSerializable("mCurrentFragment"); + } + + mListener = + new CoreListenerStub() { + @Override + public void onMessageReceived(Core lc, ChatRoom cr, ChatMessage message) { + displayMissedChats(LinphoneManager.getInstance().getUnreadMessageCount()); + } + + @Override + public void onRegistrationStateChanged( + Core lc, ProxyConfig proxy, RegistrationState state, String smessage) { + AuthInfo authInfo = + lc.findAuthInfo( + proxy.getRealm(), + proxy.getIdentityAddress().getUsername(), + proxy.getDomain()); + + refreshAccounts(); + + if (getResources().getBoolean(R.bool.use_phone_number_validation) + && authInfo != null + && authInfo.getDomain() + .equals(getString(R.string.default_domain))) { + if (state.equals(RegistrationState.Ok)) { + LinphoneManager.getInstance().isAccountWithAlias(); + } + } + + if (state.equals(RegistrationState.Failed) && mNewProxyConfig) { + mNewProxyConfig = false; + 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); + } + } + + if (state == RegistrationState.Ok) { + // For push notifications to work on Huawei device, + // app must be in "protected mode" in battery settings... + // https://stackoverflow.com/questions/31638986/protected-apps-setting-on-huawei-phones-and-how-to-handle-it + DeviceUtils + .displayDialogIfDeviceHasPowerManagerThatCouldPreventPushNotifications( + LinphoneActivity.this); + } + } + + @Override + public void onCallStateChanged( + Core lc, Call call, Call.State state, String message) { + if (state == State.IncomingReceived) { + // This case will be handled by the service listener + } else if (state == State.OutgoingInit || state == State.OutgoingProgress) { + startActivity( + new Intent( + LinphoneActivity.instance(), + CallOutgoingActivity.class)); + } else if (state == State.End + || state == State.Error + || state == State.Released) { + resetClassicMenuLayoutAndGoBackToCallIfStillRunning(); + } + + int missedCalls = LinphoneManager.getLc().getMissedCallsCount(); + displayMissedCalls(missedCalls); + } + }; + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + int missedCalls = lc.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; + } + + mAlwaysChangingPhoneAngle = rotation; + if (LinphoneManager.isInstanciated()) { + LinphoneManager.getLc().setDeviceRotation(rotation); + onNewIntent(getIntent()); + } + } + + @Override + protected void onStart() { + super.onStart(); + + ArrayList permissionsToAskFor = new ArrayList<>(); + String[] permissionsToHave = { + // This one is to allow floating notifications + Manifest.permission.SYSTEM_ALERT_WINDOW, + // Required starting Android 9 to be able to start a foreground service + "android.permission.FOREGROUND_SERVICE", + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.READ_PHONE_STATE, + Manifest.permission.WRITE_CONTACTS, + Manifest.permission.READ_CONTACTS + }; + + for (String permissionToHave : permissionsToHave) { + if (!checkPermission(permissionToHave)) { + permissionsToAskFor.add(permissionToHave); + } + } + + if (permissionsToAskFor.size() > 0) { + for (String permission : permissionsToAskFor) { + Log.i("[Permission] Asking for " + permission + " permission"); + } + String[] permissions = new String[permissionsToAskFor.size()]; + permissions = permissionsToAskFor.toArray(permissions); + + KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + boolean locked = km.inKeyguardRestrictedInputMode(); + if (!locked) { + // This is to workaround an infinite loop of pause/start in LinphoneActivity issue + // if incoming call ends while screen if off and locked + ActivityCompat.requestPermissions( + this, permissions, PERMISSIONS_READ_EXTERNAL_STORAGE_DEVICE_RINGTONE); + } + } else { + if (getResources().getBoolean(R.bool.check_for_update_when_app_starts)) { + checkForUpdate(); + } + } + + if (checkPermission(Manifest.permission.READ_CONTACTS)) { + ContactsManager.getInstance().enableContactsAccess(); + } + ContactsManager.getInstance().initializeContactManager(this); + + if (DeviceUtils.isAppUserRestricted(this)) { + Log.w( + "[Linphone Activity] Device has been restricted by user (Android 9+), push notifications won't work !"); + } + + int bucket = DeviceUtils.getAppStandbyBucket(this); + if (bucket > 0) { + Log.w( + "[Linphone Activity] Device is in bucket " + + Compatibility.getAppStandbyBucketNameFromValue(bucket)); + } + + if (!PushNotificationUtils.isAvailable(this)) { + Log.w("[Linphone Activity] Push notifications won't work !"); + } + + IntentUtils.handleIntent(this, getIntent()); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putSerializable("mCurrentFragment", mCurrentFragment); + super.onSaveInstanceState(outState); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + } + + @Override + protected void onPause() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + } + mCallTransfer = false; + mIsOnBackground = true; + + super.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + if (!LinphoneService.isReady()) { + startService(new Intent(Intent.ACTION_MAIN).setClass(this, LinphoneService.class)); + } + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + } + + if (isTablet()) { + // Prevent fragmentContainer2 to be visible when rotating the device + LinearLayout ll = findViewById(R.id.fragmentContainer2); + if (mCurrentFragment == FragmentsAvailable.DIALER + || mCurrentFragment == FragmentsAvailable.ABOUT + || mCurrentFragment == FragmentsAvailable.SETTINGS + || mCurrentFragment == FragmentsAvailable.SETTINGS_SUBLEVEL + || mCurrentFragment == FragmentsAvailable.ACCOUNT_SETTINGS) { + ll.setVisibility(View.GONE); + } + } + + refreshAccounts(); + + if (getResources().getBoolean(R.bool.enable_in_app_purchase)) { + isTrialAccount(); + } + + displayMissedChats(LinphoneManager.getInstance().getUnreadMessageCount()); + displayMissedCalls(LinphoneManager.getLc().getMissedCallsCount()); + + if (!getIntent().getBooleanExtra("DoNotGoToCallActivity", false)) { + if (LinphoneManager.getLc().getCalls().length > 0) { + Call call = LinphoneManager.getLc().getCalls()[0]; + Call.State onCallStateChanged = call.getState(); + + if (onCallStateChanged == State.IncomingReceived + || onCallStateChanged == State.IncomingEarlyMedia) { + startActivity(new Intent(this, CallIncomingActivity.class)); + } else if (onCallStateChanged == State.OutgoingInit + || onCallStateChanged == State.OutgoingProgress + || onCallStateChanged == State.OutgoingRinging) { + startActivity(new Intent(this, CallOutgoingActivity.class)); + } else { + startIncallActivity(); + } + } + } + } + + @Override + protected void onPostResume() { + super.onPostResume(); + if (mPendingFragmentTransaction != FragmentsAvailable.UNKNOW) { + changeCurrentFragment(mPendingFragmentTransaction, null); + selectMenu(mPendingFragmentTransaction); + mPendingFragmentTransaction = FragmentsAvailable.UNKNOW; + } + } + + @Override + protected void onDestroy() { + if (mOrientationHelper != null) { + mOrientationHelper.disable(); + mOrientationHelper = null; + } + + sInstance = null; + super.onDestroy(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (getIntent() != null && getIntent().getExtras() != null) { + mNewProxyConfig = getIntent().getExtras().getBoolean("isNewProxyConfig"); + } + + if (requestCode == ANDROID_APP_SETTINGS_ACTIVITY) { + LinphoneActivity.instance().goToDialerFragment(); + } else if (resultCode == Activity.RESULT_FIRST_USER && requestCode == SETTINGS_ACTIVITY) { + if (data.getExtras().getBoolean("Exit", false)) { + quit(); + } else { + mPendingFragmentTransaction = + (FragmentsAvailable) data.getExtras().getSerializable("FragmentToDisplay"); + } + } else if (resultCode == Activity.RESULT_FIRST_USER && requestCode == CALL_ACTIVITY) { + getIntent().putExtra("PreviousActivity", CALL_ACTIVITY); + mCallTransfer = data != null && data.getBooleanExtra("Transfer", false); + if (LinphoneManager.getLc().getCallsNb() > 0) { + initInCallMenuLayout(); + } 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 onNewIntent(Intent intent) { + super.onNewIntent(intent); + + /*if (getCurrentFragment() == FragmentsAvailable.SETTINGS) { + if (mFragment instanceof SettingsFragment) { + ((SettingsFragment) mFragment).closePreferenceScreen(); + } + }*/ + + Bundle extras = intent.getExtras(); + if (extras != null) { + if (extras.getBoolean("GoToChat", false)) { + String localSipUri = extras.getString("LocalSipUri"); + String remoteSipUri = extras.getString("ChatContactSipUri"); + Log.i( + "[Linphone Activity] Intent asked to go to chat, local URI " + + localSipUri + + ", remote URI " + + remoteSipUri); + intent.putExtra("DoNotGoToCallActivity", true); + if (remoteSipUri == null) { + goToChatList(); + } else { + goToChat(localSipUri, remoteSipUri, extras); + } + } else if (extras.getBoolean("GoToHistory", false)) { + Log.i("[Linphone Activity] Intent asked to go to call history"); + intent.putExtra("DoNotGoToCallActivity", true); + changeCurrentFragment(FragmentsAvailable.HISTORY_LIST, null); + } else if (extras.getBoolean("GoToInapp", false)) { + Log.i("[Linphone Activity] Intent asked to go to inapp"); + intent.putExtra("DoNotGoToCallActivity", true); + displayInapp(); + } else if (extras.getBoolean("Notification", false)) { + if (LinphoneManager.getLc().getCallsNb() > 0) { + startIncallActivity(); + } + } else if (extras.getBoolean("StartCall", false)) { + addressWaitingToBeCalled = extras.getString("NumberToCall"); + goToDialerFragment(); + } else if (extras.getBoolean("Transfer", false)) { + intent.putExtra("DoNotGoToCallActivity", true); + } else if (extras.getBoolean("AddCall", false)) { + intent.putExtra("DoNotGoToCallActivity", true); + } else if (intent.getStringExtra("msgShared") != null) { + String message = intent.getStringExtra("msgShared"); + Log.i( + "[Linphone Activity] Intent asked to go to chat list to share message " + + message); + extras.putString("messageDraft", message); + changeCurrentFragment(FragmentsAvailable.CHAT_LIST, extras); + intent.removeExtra("msgShared"); + } else if (intent.getStringExtra("fileShared") != null + && !intent.getStringExtra("fileShared").equals("")) { + String file = intent.getStringExtra("fileShared"); + Log.i( + "[Linphone Activity] Intent asked to go to chat list to share file(s) " + + file); + extras.putString("fileSharedUri", file); + changeCurrentFragment(FragmentsAvailable.CHAT_LIST, extras); + intent.removeExtra("fileShared"); + } else { + DialerFragment dialerFragment = DialerFragment.instance(); + if (dialerFragment != null) { + if (extras.containsKey("SipUriOrNumber")) { + if (getResources() + .getBoolean( + R.bool.automatically_start_intercepted_outgoing_gsm_call)) { + dialerFragment.newOutgoingCall(extras.getString("SipUriOrNumber")); + } else { + dialerFragment.displayTextInAddressBar( + extras.getString("SipUriOrNumber")); + } + } + } else { + if (extras.containsKey("SipUriOrNumber")) { + addressWaitingToBeCalled = extras.getString("SipUriOrNumber"); + goToDialerFragment(); + } + } + } + } + setIntent(intent); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + + // If permission was asked we wait here for the results so dialogs won't conflict + if (getResources().getBoolean(R.bool.check_for_update_when_app_starts)) { + checkForUpdate(); + } + + 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].compareTo(Manifest.permission.READ_CONTACTS) == 0 + || permissions[i].compareTo(Manifest.permission.WRITE_CONTACTS) == 0) + readContactsI = i; + } + + if (readContactsI >= 0 + && grantResults[readContactsI] == PackageManager.PERMISSION_GRANTED) { + ContactsManager.getInstance().enableContactsAccess(); + } + switch (requestCode) { + case PERMISSIONS_REQUEST_SYNC: + ContactsManager.getInstance().initializeContactManager(this); + break; + case PERMISSIONS_RECORD_AUDIO_ECHO_CANCELLER: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + ((AudioSettingsFragment) mFragment).startEchoCancellerCalibration(); + } + break; + case PERMISSIONS_READ_EXTERNAL_STORAGE_DEVICE_RINGTONE: + 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) + ((AudioSettingsFragment) mFragment).startEchoTester(); + break; + } + } + + private void checkForUpdate() { + String url = LinphonePreferences.instance().getCheckReleaseUrl(); + if (url != null && !url.isEmpty()) { + int lastTimestamp = LinphonePreferences.instance().getLastCheckReleaseTimestamp(); + int currentTimeStamp = (int) System.currentTimeMillis(); + int interval = getResources().getInteger(R.integer.time_between_update_check); // 24h + if (lastTimestamp == 0 || currentTimeStamp - lastTimestamp >= interval) { + LinphoneManager.getLcIfManagerNotDestroyedOrNull() + .checkForUpdate(BuildConfig.VERSION_NAME); + LinphonePreferences.instance().setLastCheckReleaseTimestamp(currentTimeStamp); + } + } + } + + private void initButtons() { + mTabBar = findViewById(R.id.footer); + mTopBar = findViewById(R.id.top_bar); + mTopBarTitle = findViewById(R.id.top_bar_title); + + mCancel = findViewById(R.id.cancel); + mCancel.setOnClickListener(this); + + mHistory = findViewById(R.id.history); + mHistory.setOnClickListener(this); + mContacts = findViewById(R.id.contacts); + mContacts.setOnClickListener(this); + mDialer = findViewById(R.id.dialer); + mDialer.setOnClickListener(this); + mChat = findViewById(R.id.chat); + mChat.setOnClickListener(this); + if (getResources().getBoolean(R.bool.disable_chat)) { + mChat.setVisibility(View.GONE); + } + + mHistorySelected = findViewById(R.id.history_select); + mContactsSelected = findViewById(R.id.contacts_select); + mDialerSelected = findViewById(R.id.dialer_select); + mChatSelected = findViewById(R.id.chat_select); + + mMissedCalls = findViewById(R.id.missed_calls); + mMissedChats = findViewById(R.id.missed_chats); + } + + public 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 (mStatusFragment != null && !mStatusFragment.isVisible()) { + mStatusFragment.getView().setVisibility(View.VISIBLE); + } + findViewById(R.id.status).setVisibility(View.VISIBLE); + } + + public void popBackStack() { + getFragmentManager().popBackStackImmediate(); + mCurrentFragment = FragmentsAvailable.EMPTY; + } + + private void changeCurrentFragment(FragmentsAvailable newFragmentType, Bundle extras) { + if (newFragmentType == mCurrentFragment + && newFragmentType != FragmentsAvailable.CHAT_LIST + && newFragmentType != FragmentsAvailable.CHAT + && newFragmentType != FragmentsAvailable.GROUP_CHAT) { + return; + } + + if (mCurrentFragment == FragmentsAvailable.DIALER) { + try { + DialerFragment dialerFragment = DialerFragment.instance(); + mDialerSavedState = getFragmentManager().saveFragmentInstanceState(dialerFragment); + } catch (Exception e) { + Log.e(e); + } + } + + mFragment = null; + switch (newFragmentType) { + case HISTORY_LIST: + mFragment = new HistoryFragment(); + break; + case HISTORY_DETAIL: + mFragment = new HistoryDetailFragment(); + break; + case CONTACTS_LIST: + mFragment = new ContactsFragment(); + break; + case CONTACT_DETAIL: + mFragment = new ContactDetailsFragment(); + break; + case CONTACT_EDITOR: + mFragment = new ContactEditorFragment(); + break; + case DIALER: + mFragment = new DialerFragment(); + if (extras == null) { + mFragment.setInitialSavedState(mDialerSavedState); + } + break; + case SETTINGS: + mFragment = new SettingsFragment(); + break; + case ACCOUNT_SETTINGS: + mFragment = new AccountSettingsFragment(); + break; + case ABOUT: + mFragment = new AboutFragment(); + break; + case EMPTY: + mFragment = new EmptyFragment(); + break; + case CHAT_LIST: + mFragment = new ChatRoomsFragment(); + break; + case CREATE_CHAT: + mFragment = new ChatRoomCreationFragment(); + break; + case INFO_GROUP_CHAT: + mFragment = new GroupInfoFragment(); + break; + case GROUP_CHAT: + mFragment = new ChatMessagesFragment(); + break; + case MESSAGE_IMDN: + if (getResources().getBoolean(R.bool.use_new_chat_bubbles_layout)) { + mFragment = new ImdnFragment(); + } else { + mFragment = new ImdnOldFragment(); + } + break; + case CONTACT_DEVICES: + mFragment = new DevicesFragment(); + break; + case RECORDING_LIST: + mFragment = new RecordingsFragment(); + break; + default: + break; + } + + applyFragmentChanges(newFragmentType, extras); + } + + private void changeSettingsFragment(Fragment fragment) { + mFragment = fragment; + applyFragmentChanges(FragmentsAvailable.SETTINGS_SUBLEVEL, null); + } + + private void applyFragmentChanges(FragmentsAvailable newFragmentType, Bundle extras) { + if (mFragment != null) { + mFragment.setArguments(extras); + if (isTablet()) { + changeFragmentForTablets(mFragment, newFragmentType); + switch (newFragmentType) { + case HISTORY_LIST: + ((HistoryFragment) mFragment).displayFirstLog(); + break; + case CONTACTS_LIST: + ((ContactsFragment) mFragment).displayFirstContact(); + break; + case CHAT_LIST: + ((ChatRoomsFragment) mFragment).displayFirstChat(); + break; + } + } else { + changeFragment(mFragment, newFragmentType); + } + LinphoneUtils.hideKeyboard(this); + } + } + + private void changeFragment(Fragment newFragment, FragmentsAvailable newFragmentType) { + FragmentManager fm = getFragmentManager(); + FragmentTransaction transaction = fm.beginTransaction(); + + 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); + } + } + + Compatibility.setFragmentTransactionReorderingAllowed(transaction, false); + transaction.replace(R.id.fragmentContainer, newFragment, newFragmentType.toString()); + transaction.commitAllowingStateLoss(); + fm.executePendingTransactions(); + + mCurrentFragment = newFragmentType; + } + + private void changeFragmentForTablets( + Fragment newFragment, FragmentsAvailable newFragmentType) { + if (getResources().getBoolean(R.bool.show_statusbar_only_on_dialer)) { + if (newFragmentType == FragmentsAvailable.DIALER) { + showStatusBar(); + } else { + hideStatusBar(); + } + } + mEmptyFragment = false; + LinearLayout ll = findViewById(R.id.fragmentContainer2); + + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + + if (newFragmentType == FragmentsAvailable.EMPTY) { + ll.setVisibility(View.VISIBLE); + mEmptyFragment = true; + transaction.replace(R.id.fragmentContainer2, newFragment); + transaction.commitAllowingStateLoss(); + getFragmentManager().executePendingTransactions(); + } else { + if (newFragmentType.shouldAddItselfToTheRightOf(mCurrentFragment) + || newFragmentType.shouldAddItselfToTheRightOf(mLeftFragment)) { + ll.setVisibility(View.VISIBLE); + + if (newFragmentType == FragmentsAvailable.CONTACT_EDITOR) { + transaction.addToBackStack(newFragmentType.toString()); + } + transaction.replace(R.id.fragmentContainer2, newFragment); + mLeftFragment = mCurrentFragment; + + if (newFragmentType == FragmentsAvailable.GROUP_CHAT + && mLeftFragment != FragmentsAvailable.CHAT_LIST) { + mLeftFragment = FragmentsAvailable.CHAT_LIST; + transaction.replace(R.id.fragmentContainer, new ChatRoomsFragment()); + } + } else { + if (newFragmentType == FragmentsAvailable.EMPTY) { + ll.setVisibility(View.VISIBLE); + transaction.replace(R.id.fragmentContainer2, new EmptyFragment()); + mEmptyFragment = true; + } + + if (newFragmentType == FragmentsAvailable.DIALER + || newFragmentType == FragmentsAvailable.ABOUT + || newFragmentType == FragmentsAvailable.SETTINGS + || newFragmentType == FragmentsAvailable.ACCOUNT_SETTINGS + || newFragmentType == FragmentsAvailable.CREATE_CHAT + || newFragmentType == FragmentsAvailable.INFO_GROUP_CHAT + || newFragmentType == FragmentsAvailable.RECORDING_LIST) { + ll.setVisibility(View.GONE); + } else { + ll.setVisibility(View.VISIBLE); + transaction.replace(R.id.fragmentContainer2, new EmptyFragment()); + } + + transaction.replace(R.id.fragmentContainer, newFragment); + } + transaction.commitAllowingStateLoss(); + getFragmentManager().executePendingTransactions(); + + mCurrentFragment = 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 (java.lang.IllegalStateException e) { + Log.e(e); + } + } + } + } + + public void displayHistoryDetail(String sipUri, CallLog log) { + Address lAddress; + LinphoneContact c = null; + + lAddress = Factory.instance().createAddress(sipUri); + if (lAddress != null) { + 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; + + Fragment fragment2 = getFragmentManager().findFragmentById(R.id.fragmentContainer2); + if (fragment2 != null + && fragment2.isVisible() + && mCurrentFragment == FragmentsAvailable.HISTORY_DETAIL) { + HistoryDetailFragment historyDetailFragment = (HistoryDetailFragment) fragment2; + historyDetailFragment.changeDisplayedHistory(sipUri, displayName); + } else { + Bundle extras = new Bundle(); + extras.putString("SipUri", sipUri); + if (displayName != null) { + extras.putString("DisplayName", displayName); + extras.putString("PictureUri", pictureUri); + } + + changeCurrentFragment(FragmentsAvailable.HISTORY_DETAIL, extras); + } + } + + public void displayEmptyFragment() { + changeCurrentFragment(FragmentsAvailable.EMPTY, new Bundle()); + } + + public void displayContact(LinphoneContact contact, boolean chatOnly) { + Fragment fragment2 = getFragmentManager().findFragmentById(R.id.fragmentContainer2); + if (fragment2 != null + && fragment2.isVisible() + && mCurrentFragment == 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); + } + + private void displayAbout() { + changeCurrentFragment(FragmentsAvailable.ABOUT, null); + } + + private void displayRecordings() { + changeCurrentFragment(FragmentsAvailable.RECORDING_LIST, null); + } + + public void displaySubSettings(Fragment fragment) { + changeSettingsFragment(fragment); + } + + public void displayContactsForEdition(String sipAddress, String displayName) { + Bundle extras = new Bundle(); + extras.putBoolean("EditOnClick", true); + extras.putString("SipAddress", sipAddress); + extras.putString("DisplayName", displayName); + changeCurrentFragment(FragmentsAvailable.CONTACTS_LIST, extras); + } + + private void displayAssistant() { + startActivity(new Intent(LinphoneActivity.this, AssistantActivity.class)); + } + + private void displayInapp() { + startActivity(new Intent(LinphoneActivity.this, InAppPurchaseActivity.class)); + } + + public void goToChatCreator( + String address, + ArrayList selectedContacts, + String subject, + boolean isGoBack, + Bundle shareInfos, + boolean createGroupChat, + boolean isChatRoomEncrypted) { + if (mCurrentFragment == FragmentsAvailable.INFO_GROUP_CHAT && isGoBack) { + getFragmentManager().popBackStackImmediate(); + getFragmentManager().popBackStackImmediate(); + } + Bundle extras = new Bundle(); + extras.putSerializable("selectedContacts", selectedContacts); + extras.putString("subject", subject); + extras.putString("groupChatRoomAddress", address); + extras.putBoolean("createGroupChatRoom", createGroupChat); + extras.putBoolean("encrypted", isChatRoomEncrypted); + + if (shareInfos != null) { + if (shareInfos.getString("fileSharedUri") != null) + extras.putString("fileSharedUri", shareInfos.getString("fileSharedUri")); + if (shareInfos.getString("messageDraft") != null) + extras.putString("messageDraft", shareInfos.getString("messageDraft")); + } + + changeCurrentFragment(FragmentsAvailable.CREATE_CHAT, extras); + } + + public void goToChat(String localSipUri, String remoteSipUri, Bundle shareInfos) { + Bundle extras = new Bundle(); + extras.putString("LocalSipUri", localSipUri); + extras.putString("RemoteSipUri", remoteSipUri); + + if (shareInfos != null) { + if (shareInfos.getString("fileSharedUri") != null) + extras.putString("fileSharedUri", shareInfos.getString("fileSharedUri")); + if (shareInfos.getString("messageDraft") != null) + extras.putString("messageDraft", shareInfos.getString("messageDraft")); + } + + if (isTablet()) { + Fragment fragment2 = getFragmentManager().findFragmentById(R.id.fragmentContainer2); + if (fragment2 != null + && fragment2.isVisible() + && mCurrentFragment == FragmentsAvailable.GROUP_CHAT + && !mEmptyFragment) { + ChatMessagesFragment chatFragment = (ChatMessagesFragment) fragment2; + chatFragment.changeDisplayedChat(localSipUri, remoteSipUri); + } else { + changeCurrentFragment(FragmentsAvailable.GROUP_CHAT, extras); + } + } else { + changeCurrentFragment(FragmentsAvailable.GROUP_CHAT, extras); + } + + LinphoneManager.getInstance().updateUnreadCountForChatRoom(localSipUri, remoteSipUri, 0); + displayMissedChats(LinphoneManager.getInstance().getUnreadMessageCount()); + } + + public void goToChatGroupInfos( + String address, + ArrayList contacts, + String subject, + boolean isEditionEnabled, + boolean isGoBack, + Bundle shareInfos, + boolean enableEncryption) { + if (mCurrentFragment == FragmentsAvailable.CREATE_CHAT && isGoBack) { + getFragmentManager().popBackStackImmediate(); + getFragmentManager().popBackStackImmediate(); + } + Bundle extras = new Bundle(); + extras.putString("groupChatRoomAddress", address); + extras.putBoolean("isEditionEnabled", isEditionEnabled); + extras.putSerializable("ContactAddress", contacts); + extras.putString("subject", subject); + extras.putBoolean("encryptionEnabled", enableEncryption); + + if (shareInfos != null) { + if (shareInfos.getString("fileSharedUri") != null) + extras.putString("fileSharedUri", shareInfos.getString("fileSharedUri")); + if (shareInfos.getString("messageDraft") != null) + extras.putString("messageDraft", shareInfos.getString("messageDraft")); + } + + changeCurrentFragment(FragmentsAvailable.INFO_GROUP_CHAT, extras); + } + + public void goToContactDevicesInfos(String localSipUri, String remoteSipUri) { + Bundle extras = new Bundle(); + extras.putSerializable("LocalSipUri", localSipUri); + extras.putSerializable("RemoteSipUri", remoteSipUri); + changeCurrentFragment(FragmentsAvailable.CONTACT_DEVICES, extras); + } + + public void goToChatMessageImdnInfos( + String localSipUri, String remoteSipUri, String messageId) { + Bundle extras = new Bundle(); + extras.putSerializable("LocalSipUri", localSipUri); + extras.putSerializable("RemoteSipUri", remoteSipUri); + extras.putString("MessageId", messageId); + changeCurrentFragment(FragmentsAvailable.MESSAGE_IMDN, extras); + } + + public void goToChatList() { + changeCurrentFragment(FragmentsAvailable.CHAT_LIST, null); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + resetSelection(); + + if (id == R.id.history) { + changeCurrentFragment(FragmentsAvailable.HISTORY_LIST, null); + mHistorySelected.setVisibility(View.VISIBLE); + LinphoneManager.getLc().resetMissedCallsCount(); + displayMissedCalls(0); + } else if (id == R.id.contacts) { + changeCurrentFragment(FragmentsAvailable.CONTACTS_LIST, null); + mContactsSelected.setVisibility(View.VISIBLE); + } else if (id == R.id.dialer) { + changeCurrentFragment(FragmentsAvailable.DIALER, null); + mDialerSelected.setVisibility(View.VISIBLE); + } else if (id == R.id.chat) { + changeCurrentFragment(FragmentsAvailable.CHAT_LIST, null); + mChatSelected.setVisibility(View.VISIBLE); + } else if (id == R.id.cancel) { + if (mCurrentFragment == FragmentsAvailable.SETTINGS_SUBLEVEL && !isTablet()) { + popBackStack(); + } else { + hideTopBar(); + displayDialer(); + } + } + } + + private void resetSelection() { + mHistorySelected.setVisibility(View.GONE); + mContactsSelected.setVisibility(View.GONE); + mDialerSelected.setVisibility(View.GONE); + mChatSelected.setVisibility(View.GONE); + } + + public void hideTabBar(Boolean hide) { + if (hide && !isTablet()) { // do not hide if tablet, otherwise won't be able to navigate... + mTabBar.setVisibility(View.GONE); + } else { + mTabBar.setVisibility(View.VISIBLE); + } + } + + public void hideTopBar() { + mTopBar.setVisibility(View.GONE); + mTopBarTitle.setText(""); + } + + private void showTopBar() { + mTopBar.setVisibility(View.VISIBLE); + } + + private void showTopBarWithTitle(String title) { + showTopBar(); + mTopBarTitle.setText(title); + } + + public void selectMenu(FragmentsAvailable menuToSelect) { + selectMenu(menuToSelect, null); + } + + @SuppressWarnings("incomplete-switch") + public void selectMenu(FragmentsAvailable menuToSelect, String customTitle) { + mCurrentFragment = menuToSelect; + resetSelection(); + hideTopBar(); + boolean hideBottomBar = + getResources().getBoolean(R.bool.hide_bottom_bar_on_second_level_views); + + switch (menuToSelect) { + case HISTORY_LIST: + hideTabBar(false); + mHistorySelected.setVisibility(View.VISIBLE); + break; + case HISTORY_DETAIL: + hideTabBar(hideBottomBar); + mHistorySelected.setVisibility(View.VISIBLE); + break; + case CONTACTS_LIST: + hideTabBar(false); + mContactsSelected.setVisibility(View.VISIBLE); + break; + case CONTACT_DETAIL: + case CONTACT_EDITOR: + hideTabBar(hideBottomBar); + mContactsSelected.setVisibility(View.VISIBLE); + break; + case DIALER: + hideTabBar(false); + mDialerSelected.setVisibility(View.VISIBLE); + break; + case SETTINGS: + case ACCOUNT_SETTINGS: + case SETTINGS_SUBLEVEL: + hideTabBar(hideBottomBar); + if (customTitle == null) { + showTopBarWithTitle(getString(R.string.settings)); + } else { + showTopBarWithTitle(customTitle); + } + break; + case ABOUT: + showTopBarWithTitle(getString(R.string.about)); + hideTabBar(hideBottomBar); + break; + case CHAT_LIST: + hideTabBar(false); + mChatSelected.setVisibility(View.VISIBLE); + break; + case CREATE_CHAT: + case GROUP_CHAT: + case INFO_GROUP_CHAT: + case MESSAGE_IMDN: + case CONTACT_DEVICES: + case CHAT: + hideTabBar(hideBottomBar); + mChatSelected.setVisibility(View.VISIBLE); + break; + case RECORDING_LIST: + hideTabBar(hideBottomBar); + break; + } + } + + public void updateDialerFragment() { + // Hack to maintain soft input flags + getWindow() + .setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN + | WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + } + + private void goToDialerFragment() { + Bundle extras = new Bundle(); + extras.putString("SipUri", ""); + changeCurrentFragment(FragmentsAvailable.DIALER, extras); + mDialerSelected.setVisibility(View.VISIBLE); + } + + public void updateStatusFragment(StatusFragment fragment) { + mStatusFragment = fragment; + } + + public void displaySettings() { + changeCurrentFragment(FragmentsAvailable.SETTINGS, null); + } + + private void displayDialer() { + changeCurrentFragment(FragmentsAvailable.DIALER, null); + } + + public void displayAccountSettings(int accountNumber) { + Bundle bundle = new Bundle(); + bundle.putInt("Account", accountNumber); + changeCurrentFragment(FragmentsAvailable.ACCOUNT_SETTINGS, bundle); + } + + public void refreshMissedChatCountDisplay() { + displayMissedChats(LinphoneManager.getInstance().getUnreadMessageCount()); + } + + public void displayMissedCalls(final int missedCallsCount) { + if (missedCallsCount > 0) { + mMissedCalls.setText(missedCallsCount + ""); + mMissedCalls.setVisibility(View.VISIBLE); + } else { + if (LinphoneManager.isInstanciated()) LinphoneManager.getLc().resetMissedCallsCount(); + mMissedCalls.clearAnimation(); + mMissedCalls.setVisibility(View.GONE); + } + } + + public void displayMissedChats(final int missedChatCount) { + if (missedChatCount > 0) { + mMissedChats.setText(missedChatCount + ""); + mMissedChats.setVisibility(View.VISIBLE); + } else { + mMissedChats.clearAnimation(); + mMissedChats.setVisibility(View.GONE); + } + if (mCurrentFragment == FragmentsAvailable.CHAT_LIST + && mFragment instanceof ChatRoomsFragment) { + ((ChatRoomsFragment) mFragment).invalidate(); + } + } + + 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 = 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 void displayChatRoomError() { + final Dialog dialog = displayDialog(getString(R.string.chat_room_creation_failed)); + dialog.findViewById(R.id.dialog_delete_button).setVisibility(View.GONE); + Button cancel = dialog.findViewById(R.id.dialog_cancel_button); + cancel.setText(getString(R.string.ok)); + cancel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + dialog.dismiss(); + } + }); + + dialog.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.dark_grey_color)); + 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 = dialog.findViewById(R.id.dialog_message); + customText.setText(text); + return dialog; + } + + public void setAddresGoToDialerAndCall(String number, String name) { + AddressType address = new AddressText(this, null); + address.setText(number); + address.setDisplayedName(name); + LinphoneManager.getInstance().newOutgoingCall(address); + } + + public void startIncallActivity() { + 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(); + } + + public Boolean isCallTransfer() { + return mCallTransfer; + } + + private void initInCallMenuLayout() { + selectMenu(FragmentsAvailable.DIALER); + DialerFragment dialerFragment = DialerFragment.instance(); + if (dialerFragment != null) { + (dialerFragment).resetLayout(); + } + } + + public void resetClassicMenuLayoutAndGoBackToCallIfStillRunning() { + DialerFragment dialerFragment = DialerFragment.instance(); + if (dialerFragment != null) { + (dialerFragment).resetLayout(); + } + + if (LinphoneManager.isInstanciated() && LinphoneManager.getLc().getCallsNb() > 0) { + Call call = LinphoneManager.getLc().getCalls()[0]; + if (call.getState() == Call.State.IncomingReceived + || call.getState() == State.IncomingEarlyMedia) { + startActivity(new Intent(LinphoneActivity.this, CallIncomingActivity.class)); + } else { + startIncallActivity(); + } + } + } + + public FragmentsAvailable getCurrentFragment() { + return mCurrentFragment; + } + + public void addContact(String displayName, String sipUri) { + Bundle extras = new Bundle(); + extras.putSerializable("NewSipAdress", sipUri); + extras.putSerializable("NewDisplayName", displayName); + changeCurrentFragment(FragmentsAvailable.CONTACT_EDITOR, extras); + } + + public void editContact(LinphoneContact contact, String sipUri) { + Bundle extras = new Bundle(); + extras.putSerializable("Contact", contact); + extras.putString("NewSipAdress", sipUri); + changeCurrentFragment(FragmentsAvailable.CONTACT_EDITOR, extras); + } + + private void quit() { + finish(); + stopService(new Intent(Intent.ACTION_MAIN).setClass(this, LinphoneService.class)); + ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + am.killBackgroundProcesses(getString(R.string.sync_account_type)); + android.os.Process.killProcess(android.os.Process.myPid()); + } + + 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 checkAndRequestExternalStoragePermission() { + checkAndRequestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, 0); + } + + public void checkAndRequestReadContactsPermission() { + checkAndRequestPermission(Manifest.permission.READ_CONTACTS, PERMISSIONS_REQUEST_SYNC); + } + + public void checkAndRequestCameraPermission() { + checkAndRequestPermission(Manifest.permission.CAMERA, 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 permissionsToAskFor = new ArrayList<>(); + String[] permissionsToHave = { + Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA + }; + + for (String permissionToHave : permissionsToHave) { + if (!checkPermission(permissionToHave)) { + permissionsToAskFor.add(permissionToHave); + } + } + + if (permissionsToAskFor.size() > 0) { + String[] permissions = new String[permissionsToAskFor.size()]; + permissions = permissionsToAskFor.toArray(permissions); + ActivityCompat.requestPermissions(this, permissions, 0); + } + } + + private void checkSyncPermission() { + checkAndRequestPermission( + Manifest.permission.WRITE_SYNC_SETTINGS, PERMISSIONS_REQUEST_SYNC); + } + + private boolean checkPermission(String permission) { + int granted = getPackageManager().checkPermission(permission, getPackageName()); + Log.i( + "[Permission] " + + permission + + " permission is " + + (granted == PackageManager.PERMISSION_GRANTED ? "granted" : "denied")); + return granted == PackageManager.PERMISSION_GRANTED; + } + + private void checkAndRequestPermission(String permission, int result) { + int permissionGranted = getPackageManager().checkPermission(permission, getPackageName()); + Log.i( + "[Permission] " + + permission + + " is " + + (permissionGranted == PackageManager.PERMISSION_GRANTED + ? "granted" + : "denied")); + + if (!checkPermission(permission)) { + Log.i("[Permission] Asking for " + permission); + ActivityCompat.requestPermissions(this, new String[] {permission}, result); + } + } + + public boolean isOnBackground() { + return mIsOnBackground; + } + + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + switch (mCurrentFragment) { + case DIALER: + case CONTACTS_LIST: + case HISTORY_LIST: + case CHAT_LIST: + if (LinphoneUtils.onKeyBackGoHome(this, keyCode, event)) { + return true; + } + break; + case GROUP_CHAT: + hideTopBar(); // just in case + LinphoneActivity.instance().goToChatList(); + return true; + case SETTINGS_SUBLEVEL: + if (!isTablet()) { + popBackStack(); + return true; + } + case SETTINGS: + case ACCOUNT_SETTINGS: + case ABOUT: + hideTopBar(); // just in case + LinphoneActivity.instance().goToDialerFragment(); + return true; + default: + break; + } + } + return super.onKeyDown(keyCode, event); + } + + // SIDE MENU + private void openOrCloseSideMenu(boolean open) { + if (open) { + mSideMenu.openDrawer(mSideMenuContent); + } else { + mSideMenu.closeDrawer(mSideMenuContent); + } + } + + private void initSideMenu() { + mSideMenu = findViewById(R.id.side_menu); + mSideMenuItems = new ArrayList<>(); + if (getResources().getBoolean(R.bool.show_log_out_in_side_menu)) { + mSideMenuItems.add( + new MenuItem( + getResources().getString(R.string.menu_logout), + R.drawable.quit_default)); + } + if (!getResources().getBoolean(R.bool.hide_assistant_from_side_menu)) { + mSideMenuItems.add( + new MenuItem( + getResources().getString(R.string.menu_assistant), + R.drawable.menu_assistant)); + } + if (!getResources().getBoolean(R.bool.hide_settings_from_side_menu)) { + mSideMenuItems.add( + new MenuItem( + getResources().getString(R.string.menu_settings), + R.drawable.menu_options)); + } + if (getResources().getBoolean(R.bool.enable_in_app_purchase)) { + mSideMenuItems.add( + new MenuItem( + getResources().getString(R.string.inapp), R.drawable.menu_options)); + } + if (!getResources().getBoolean(R.bool.hide_recordings_from_side_menu)) { + mSideMenuItems.add( + new MenuItem( + getResources().getString(R.string.menu_recordings), + R.drawable.menu_recordings)); + } + mSideMenuItems.add( + new MenuItem(getResources().getString(R.string.menu_about), R.drawable.menu_about)); + mSideMenuContent = findViewById(R.id.side_menu_content); + mSideMenuItemList = findViewById(R.id.item_list); + mMenu = findViewById(R.id.side_menu_button); + + mSideMenuItemList.setAdapter( + new MenuAdapter(this, R.layout.side_menu_item_cell, mSideMenuItems)); + mSideMenuItemList.setOnItemClickListener( + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + String selectedItem = mSideMenuItemList.getAdapter().getItem(i).toString(); + if (selectedItem.equals(getString(R.string.menu_logout))) { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.setDefaultProxyConfig(null); + lc.clearAllAuthInfo(); + lc.clearProxyConfig(); + startActivity( + new Intent() + .setClass( + LinphoneManager.getInstance().getContext(), + AssistantActivity.class)); + finish(); + } + } else if (selectedItem.equals(getString(R.string.menu_settings))) { + LinphoneActivity.instance().displaySettings(); + } else if (selectedItem.equals(getString(R.string.menu_about))) { + LinphoneActivity.instance().displayAbout(); + } else if (selectedItem.equals(getString(R.string.menu_assistant))) { + LinphoneActivity.instance().displayAssistant(); + } + if (getResources().getBoolean(R.bool.enable_in_app_purchase)) { + if (mSideMenuItemList + .getAdapter() + .getItem(i) + .toString() + .equals(getString(R.string.inapp))) { + LinphoneActivity.instance().displayInapp(); + } + } + if (mSideMenuItemList + .getAdapter() + .getItem(i) + .toString() + .equals(getString(R.string.menu_recordings))) { + LinphoneActivity.instance().displayRecordings(); + } + openOrCloseSideMenu(false); + } + }); + + initAccounts(); + + mMenu.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + + if (mSideMenu.isDrawerVisible(Gravity.LEFT)) { + mSideMenu.closeDrawer(mSideMenuContent); + } else { + mSideMenu.openDrawer(mSideMenuContent); + } + } + }); + + mQuitLayout = findViewById(R.id.side_menu_quit); + mQuitLayout.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + LinphoneActivity.instance().quit(); + } + }); + } + + private int getStatusIconResource(RegistrationState state) { + try { + if (state == RegistrationState.Ok) { + return R.drawable.led_connected; + } else if (state == RegistrationState.Progress) { + return R.drawable.led_inprogress; + } else if (state == RegistrationState.Failed) { + 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() { + mDefaultAccount.setVisibility(View.VISIBLE); + ImageView status = mDefaultAccount.findViewById(R.id.main_account_status); + TextView address = mDefaultAccount.findViewById(R.id.main_account_address); + TextView displayName = mDefaultAccount.findViewById(R.id.main_account_display_name); + + ProxyConfig proxy = LinphoneManager.getLc().getDefaultProxyConfig(); + if (proxy == null) { + displayName.setText(getString(R.string.no_account)); + status.setVisibility(View.GONE); + address.setText(""); + mStatusFragment.resetAccountStatus(); + + mDefaultAccount.setOnClickListener(null); + } else { + address.setText(proxy.getIdentityAddress().asStringUriOnly()); + displayName.setText(LinphoneUtils.getAddressDisplayName(proxy.getIdentityAddress())); + status.setImageResource(getStatusIconResource(proxy.getState())); + status.setVisibility(View.VISIBLE); + + if (!getResources().getBoolean(R.bool.disable_accounts_settings_from_side_menu)) { + mDefaultAccount.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + LinphoneActivity.instance() + .displayAccountSettings( + LinphonePreferences.instance() + .getDefaultAccountIndex()); + openOrCloseSideMenu(false); + } + }); + } + } + } + + public void refreshAccounts() { + if (LinphoneManager.getLc().getProxyConfigList() != null + && LinphoneManager.getLc().getProxyConfigList().length > 1) { + mAccountsList.setVisibility(View.VISIBLE); + mAccountsList.setAdapter(new AccountsListAdapter()); + mAccountsList.setOnItemClickListener( + new AdapterView.OnItemClickListener() { + @Override + public void onItemClick( + AdapterView adapterView, View view, int i, long l) { + if (view != null && view.getTag() != null) { + int position = Integer.parseInt(view.getTag().toString()); + LinphoneActivity.instance().displayAccountSettings(position); + } + openOrCloseSideMenu(false); + } + }); + } else { + mAccountsList.setVisibility(View.GONE); + } + displayMainAccount(); + } + + private void initAccounts() { + mAccountsList = findViewById(R.id.accounts_list); + mDefaultAccount = findViewById(R.id.default_account); + } + + // 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) { + mIsTrialAccount = isTrial; + getExpirationAccount(); + } + + @Override + public void onError() {} + }, + 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() {} + }, + LinphonePreferences.instance() + .getAccountUsername( + LinphonePreferences.instance().getDefaultAccountIndex()), + LinphonePreferences.instance() + .getAccountHa1( + LinphonePreferences.instance().getDefaultAccountIndex())); + } + } + + private 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 (mIsTrialAccount) { + LinphoneService.instance() + .getNotificationManager() + .displayInappNotification( + String.format( + getString(R.string.inapp_notification_trial_expire), date)); + } else { + LinphoneService.instance() + .getNotificationManager() + .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; + } + + private class LocalOrientationEventListener extends OrientationEventListener { + 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; + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.setDeviceRotation(rotation); + } + } + } + + class AccountsListAdapter extends BaseAdapter { + List proxy_list; + + AccountsListAdapter() { + proxy_list = new ArrayList<>(); + refresh(); + } + + void refresh() { + proxy_list = new ArrayList<>(); + for (ProxyConfig 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; + ProxyConfig lpc = (ProxyConfig) getItem(position); + if (convertView != null) { + view = convertView; + } else { + view = getLayoutInflater().inflate(R.layout.side_menu_account_cell, parent, false); + } + + ImageView status = view.findViewById(R.id.account_status); + TextView address = view.findViewById(R.id.account_address); + String sipAddress = lpc.getIdentityAddress().asStringUriOnly(); + + address.setText(sipAddress); + + int nbAccounts = LinphonePreferences.instance().getAccountCount(); + int accountIndex; + + 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; + } + } + + private class MenuItem { + final String name; + final int icon; + + MenuItem(String name, int icon) { + this.name = name; + this.icon = icon; + } + + public String toString() { + return name; + } + } + + private class MenuAdapter extends ArrayAdapter { + private final List mItems; + private final int mResource; + + MenuAdapter(@NonNull Context context, int resource, @NonNull List objects) { + super(context, resource, objects); + mResource = resource; + mItems = objects; + } + + @Nullable + @Override + public MenuItem getItem(int position) { + return mItems.get(position); + } + + @Override + public int getCount() { + return mItems.size(); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + LayoutInflater inflater = + (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + View rowView = inflater.inflate(mResource, parent, false); + + TextView textView = rowView.findViewById(R.id.item_name); + ImageView imageView = rowView.findViewById(R.id.item_icon); + + MenuItem item = getItem(position); + textView.setText(item.name); + imageView.setImageResource(item.icon); + + return rowView; + } + } +} diff --git a/app/src/main/java/org/linphone/LinphoneLauncherActivity.java b/app/src/main/java/org/linphone/LinphoneLauncherActivity.java new file mode 100644 index 000000000..eb4e1fbc1 --- /dev/null +++ b/app/src/main/java/org/linphone/LinphoneLauncherActivity.java @@ -0,0 +1,107 @@ +package org.linphone; + +/* +LinphoneLauncherActivity.java +Copyright (C) 2017 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 static android.content.Intent.ACTION_MAIN; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.os.Bundle; +import android.os.Handler; +import org.linphone.assistant.RemoteProvisioningActivity; +import org.linphone.settings.LinphonePreferences; + +/** Launch Linphone main activity when Service is ready. */ +public class LinphoneLauncherActivity extends Activity { + + private Handler mHandler; + private ServiceWaitThread mServiceThread; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Hack to avoid to draw twice LinphoneActivity on tablets + if (getResources().getBoolean(R.bool.orientation_portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + if (getResources().getBoolean(R.bool.use_full_screen_image_splashscreen)) { + setContentView(R.layout.launch_screen_full_image); + } else { + setContentView(R.layout.launch_screen); + } + + mHandler = new Handler(); + + if (LinphoneService.isReady()) { + onServiceReady(); + } else { + // start linphone as background + startService(new Intent(ACTION_MAIN).setClass(this, LinphoneService.class)); + mServiceThread = new ServiceWaitThread(); + mServiceThread.start(); + } + } + + private void onServiceReady() { + final Class classToStart; + /*if (getResources().getBoolean(R.bool.show_tutorials_instead_of_app)) { + classToStart = TutorialLauncherActivity.class; + } else */ + if (getResources().getBoolean(R.bool.display_sms_remote_provisioning_activity) + && LinphonePreferences.instance().isFirstRemoteProvisioning()) { + classToStart = RemoteProvisioningActivity.class; + } else { + classToStart = LinphoneActivity.class; + } + + mHandler.postDelayed( + new Runnable() { + @Override + public void run() { + startActivity( + getIntent().setClass(LinphoneLauncherActivity.this, classToStart)); + } + }, + 500); + + LinphoneManager.getInstance().changeStatusToOnline(); + } + + 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"); + } + } + mHandler.post( + new Runnable() { + @Override + public void run() { + onServiceReady(); + } + }); + mServiceThread = null; + } + } +} diff --git a/app/src/main/java/org/linphone/LinphoneManager.java b/app/src/main/java/org/linphone/LinphoneManager.java new file mode 100644 index 000000000..75fe5b326 --- /dev/null +++ b/app/src/main/java/org/linphone/LinphoneManager.java @@ -0,0 +1,1869 @@ +package org.linphone; + +/* +LinphoneManager.java +Copyright (C) 2018 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 static android.media.AudioManager.MODE_RINGTONE; +import static android.media.AudioManager.STREAM_RING; +import static android.media.AudioManager.STREAM_VOICE_CALL; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Build; +import android.os.Handler; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.Vibrator; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.telephony.TelephonyManager; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.Toast; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.Timestamp; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import org.linphone.assistant.AssistantActivity; +import org.linphone.call.CallActivity; +import org.linphone.call.CallIncomingActivity; +import org.linphone.call.CallManager; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.AccountCreator; +import org.linphone.core.AccountCreatorListener; +import org.linphone.core.Address; +import org.linphone.core.AuthInfo; +import org.linphone.core.AuthMethod; +import org.linphone.core.Call; +import org.linphone.core.Call.State; +import org.linphone.core.CallLog; +import org.linphone.core.CallParams; +import org.linphone.core.CallStats; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomCapabilities; +import org.linphone.core.ConfiguringState; +import org.linphone.core.Content; +import org.linphone.core.Core; +import org.linphone.core.Core.LogCollectionUploadState; +import org.linphone.core.CoreListener; +import org.linphone.core.EcCalibratorStatus; +import org.linphone.core.Event; +import org.linphone.core.Factory; +import org.linphone.core.Friend; +import org.linphone.core.FriendList; +import org.linphone.core.GlobalState; +import org.linphone.core.InfoMessage; +import org.linphone.core.PresenceActivity; +import org.linphone.core.PresenceBasicStatus; +import org.linphone.core.PresenceModel; +import org.linphone.core.ProxyConfig; +import org.linphone.core.PublishState; +import org.linphone.core.Reason; +import org.linphone.core.RegistrationState; +import org.linphone.core.SubscriptionState; +import org.linphone.core.Tunnel; +import org.linphone.core.TunnelConfig; +import org.linphone.core.VersionUpdateCheckResult; +import org.linphone.core.tools.H264Helper; +import org.linphone.core.tools.Log; +import org.linphone.core.tools.OpenH264DownloadHelper; +import org.linphone.core.tools.OpenH264DownloadHelperListener; +import org.linphone.mediastream.Version; +import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration; +import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration.AndroidCamera; +import org.linphone.mediastream.video.capture.hwconf.Hacks; +import org.linphone.receivers.BluetoothManager; +import org.linphone.receivers.HookReceiver; +import org.linphone.receivers.OutgoingCallReceiver; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.FileUtils; +import org.linphone.utils.LinphoneUtils; +import org.linphone.utils.MediaScanner; +import org.linphone.utils.MediaScannerListener; +import org.linphone.utils.PushNotificationUtils; + +/** + * Manager of the low level LibLinphone stuff.
+ * Including: + * + *
    + *
  • Starting C liblinphone + *
  • Reacting to C liblinphone state changes + *
  • Calling Linphone android service listener methods + *
  • Interacting from Android GUI/service with low level SIP stuff/ + *
+ * + *

Add Service Listener to react to Linphone state changes. + */ +public class LinphoneManager implements CoreListener, SensorEventListener, AccountCreatorListener { + + private static final int LINPHONE_VOLUME_STREAM = STREAM_VOICE_CALL; + + private static LinphoneManager sInstance; + private static boolean sExited; + + public final String configFile; + public String wizardLoginViewDomain = null; + + /** Called when the activity is first created. */ + private final String mLPConfigXsd; + + private final String mLinphoneFactoryConfigFile; + private final String mLinphoneDynamicConfigFile, mDefaultDynamicConfigFile; + private final String mChatDatabaseFile; + private final String mRingSoundFile; + private final String mCallLogDatabaseFile; + private final String mFriendsDatabaseFile; + private final String mUserCertsPath; + private final Context mServiceContext; + private final AudioManager mAudioManager; + private final PowerManager mPowerManager; + private final Resources mRessources; + private final LinphonePreferences mPrefs; + private Core mCore; + private OpenH264DownloadHelper mCodecDownloader; + private OpenH264DownloadHelperListener mCodecListener; + private final String mBasePath; + private boolean mAudioFocused; + private boolean mEchoTesterIsRunning; + private boolean mCallGsmON; + private final ConnectivityManager mConnectivityManager; + private BroadcastReceiver mHookReceiver; + private BroadcastReceiver mCallReceiver; + private IntentFilter mHookIntentFilter; + private IntentFilter mCallIntentFilter; + private final Handler mHandler = new Handler(); + private WakeLock mProximityWakelock; + private AccountCreator mAccountCreator; + private final SensorManager mSensorManager; + private final Sensor mProximity; + private boolean mProximitySensingEnabled; + private boolean mHandsetON = false; + private Address mCurrentChatRoomAddress; + private Timer mTimer; + private final Map mUnreadChatsPerRoom; + private final MediaScanner mMediaScanner; + private Call mRingingCall; + private MediaPlayer mRingerPlayer; + private final Vibrator mVibrator; + private boolean mIsRinging; + + private LinphoneManager(Context c) { + mUnreadChatsPerRoom = new HashMap(); + sExited = false; + mEchoTesterIsRunning = false; + mServiceContext = c; + mBasePath = c.getFilesDir().getAbsolutePath(); + mLPConfigXsd = mBasePath + "/lpconfig.xsd"; + mLinphoneFactoryConfigFile = mBasePath + "/linphonerc"; + configFile = mBasePath + "/.linphonerc"; + mLinphoneDynamicConfigFile = mBasePath + "/linphone_assistant_create.rc"; + mDefaultDynamicConfigFile = mBasePath + "/default_assistant_create.rc"; + mChatDatabaseFile = mBasePath + "/linphone-history.db"; + mCallLogDatabaseFile = mBasePath + "/linphone-log-history.db"; + mFriendsDatabaseFile = mBasePath + "/linphone-friends.db"; + mRingSoundFile = mBasePath + "/share/sounds/linphone/rings/notes_of_the_optimistic.mkv"; + mUserCertsPath = mBasePath + "/user-certs"; + + mPrefs = LinphonePreferences.instance(); + mAudioManager = ((AudioManager) c.getSystemService(Context.AUDIO_SERVICE)); + mVibrator = (Vibrator) c.getSystemService(Context.VIBRATOR_SERVICE); + mPowerManager = (PowerManager) c.getSystemService(Context.POWER_SERVICE); + mConnectivityManager = + (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE); + mSensorManager = (SensorManager) c.getSystemService(Context.SENSOR_SERVICE); + mProximity = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); + mRessources = c.getResources(); + + File f = new File(mUserCertsPath); + if (!f.exists()) { + if (!f.mkdir()) { + Log.e("[Manager] " + mUserCertsPath + " can't be created."); + } + } + + mMediaScanner = new MediaScanner(c); + } + + public static synchronized void createAndStart(Context c, boolean isPush) { + if (sInstance != null) { + Log.e( + "[Manager] Linphone Manager is already initialized ! Destroying it and creating a new one..."); + destroy(); + } + + sInstance = new LinphoneManager(c); + sInstance.startLibLinphone(c, isPush); + sInstance.initOpenH264DownloadHelper(); + + // H264 codec Management - set to auto mode -> MediaCodec >= android 5.0 >= OpenH264 + H264Helper.setH264Mode(H264Helper.MODE_AUTO, getLc()); + } + + public static synchronized LinphoneManager getInstance() { + if (sInstance != null) return sInstance; + + if (sExited) { + throw new RuntimeException( + "[Manager] Linphone Manager was already destroyed. " + + "Better use getLcIfManagerNotDestroyedOrNull and check returned value"); + } + + throw new RuntimeException("[Manager] Linphone Manager should be created before accessed"); + } + + public static synchronized Core getLc() { + return getInstance().mCore; + } + + private 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( + "[Manager] Proximity sensor report [" + + distanceInCm + + "] , for max range [" + + maxDistance + + "]"); + + if (maxDistance <= threshold) { + // Case binary 0/1 and short sensors + threshold = maxDistance; + } + return distanceInCm < threshold; + } + + private static void ContactsManagerDestroy() { + if (LinphoneManager.sInstance != null && LinphoneManager.sInstance.mServiceContext != null) + LinphoneManager.sInstance + .mServiceContext + .getContentResolver() + .unregisterContentObserver(ContactsManager.getInstance()); + ContactsManager.getInstance().destroy(); + } + + private static void BluetoothManagerDestroy() { + BluetoothManager.getInstance().destroy(); + } + + public static synchronized void destroy() { + if (sInstance == null) return; + sInstance.changeStatusToOffline(); + sInstance.mMediaScanner.destroy(); + sExited = true; + sInstance.destroyCore(); + sInstance = null; + } + + private static boolean reinviteWithVideo() { + return CallManager.getInstance().reinviteWithVideo(); + } + + public static synchronized Core getLcIfManagerNotDestroyedOrNull() { + if (sExited || sInstance == null) { + // Can occur if the UI thread play a posted event but in the meantime the + // LinphoneManager was destroyed + // Ex: stop call and quickly terminate application. + return null; + } + return getLc(); + } + + public static boolean isInstanciated() { + return sInstance != null; + } + + private void routeAudioToSpeakerHelper(boolean speakerOn) { + Log.w( + "[Manager] Routing audio to " + + (speakerOn ? "speaker" : "earpiece") + + ", disabling bluetooth audio route"); + BluetoothManager.getInstance().disableBluetoothSCO(); + + enableSpeaker(speakerOn); + } + + public boolean isSpeakerEnabled() { + return mAudioManager != null && mAudioManager.isSpeakerphoneOn(); + } + + public void enableSpeaker(boolean enable) { + mAudioManager.setSpeakerphoneOn(enable); + } + + private void initOpenH264DownloadHelper() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + Log.i("[Manager] Android >= 5.1 we disable the download of OpenH264"); + OpenH264DownloadHelper.setOpenH264DownloadEnabled(false); + return; + } + + mCodecDownloader = Factory.instance().createOpenH264DownloadHelper(getContext()); + mCodecListener = + new OpenH264DownloadHelperListener() { + ProgressDialog progress; + final int ctxt = 0; + + @Override + public void OnProgress(final int current, final int max) { + mHandler.post( + new Runnable() { + @Override + public void run() { + OpenH264DownloadHelper ohcodec = + LinphoneManager.getInstance() + .getOpenH264DownloadHelper(); + if (progress == null) { + progress = + new ProgressDialog( + (Context) ohcodec.getUserData(ctxt)); + progress.setCanceledOnTouchOutside(false); + progress.setCancelable(false); + progress.setProgressStyle( + ProgressDialog.STYLE_HORIZONTAL); + } else if (current <= max) { + progress.setMessage( + getString( + R.string + .assistant_openh264_downloading)); + progress.setMax(max); + progress.setProgress(current); + progress.show(); + } else { + progress.dismiss(); + progress = null; + if (Build.VERSION.SDK_INT + >= Build.VERSION_CODES.LOLLIPOP_MR1) { + LinphoneManager.getLc() + .reloadMsPlugins( + AssistantActivity.instance() + .getApplicationInfo() + .nativeLibraryDir); + AssistantActivity.instance().endDownloadCodec(); + } else { + // We need to restart due to bad android linker + AssistantActivity.instance().restartApplication(); + } + } + } + }); + } + + @Override + public void OnError(final String error) { + mHandler.post( + new Runnable() { + @Override + public void run() { + if (progress != null) progress.dismiss(); + AlertDialog.Builder builder = + new AlertDialog.Builder( + (Context) + LinphoneManager.getInstance() + .getOpenH264DownloadHelper() + .getUserData(ctxt)); + builder.setMessage( + getString(R.string.assistant_openh264_error)); + builder.setCancelable(false); + builder.setNeutralButton(getString(R.string.ok), null); + builder.show(); + } + }); + } + }; + mCodecDownloader.setOpenH264HelperListener(mCodecListener); + } + + public OpenH264DownloadHelperListener getOpenH264HelperListener() { + return mCodecListener; + } + + public OpenH264DownloadHelper getOpenH264DownloadHelper() { + return mCodecDownloader; + } + + public void routeAudioToSpeaker() { + routeAudioToSpeakerHelper(true); + } + + public void routeAudioToReceiver() { + routeAudioToSpeakerHelper(false); + } + + private boolean isPresenceModelActivitySet() { + Core lc = getLcIfManagerNotDestroyedOrNull(); + if (isInstanciated() && lc != null) { + return lc.getPresenceModel() != null && lc.getPresenceModel().getActivity() != null; + } + return false; + } + + public void changeStatusToOnline() { + Core lc = getLcIfManagerNotDestroyedOrNull(); + if (lc == null) return; + PresenceModel model = lc.createPresenceModel(); + model.setBasicStatus(PresenceBasicStatus.Open); + lc.setPresenceModel(model); + } + + public void changeStatusToOnThePhone() { + Core lc = getLcIfManagerNotDestroyedOrNull(); + if (lc == null) return; + + if (isInstanciated() + && isPresenceModelActivitySet() + && lc.getPresenceModel().getActivity().getType() + != PresenceActivity.Type.OnThePhone) { + lc.getPresenceModel().getActivity().setType(PresenceActivity.Type.OnThePhone); + } else if (isInstanciated() && !isPresenceModelActivitySet()) { + PresenceModel model = + lc.createPresenceModelWithActivity(PresenceActivity.Type.OnThePhone, null); + lc.setPresenceModel(model); + } + } + + private void changeStatusToOffline() { + Core lc = getLcIfManagerNotDestroyedOrNull(); + if (isInstanciated() && lc != null) { + PresenceModel model = lc.getPresenceModel(); + model.setBasicStatus(PresenceBasicStatus.Closed); + lc.setPresenceModel(model); + } + } + + public void subscribeFriendList(boolean enabled) { + Core lc = getLcIfManagerNotDestroyedOrNull(); + if (lc != null && lc.getFriendsLists() != null && lc.getFriendsLists().length > 0) { + FriendList friendList = (lc.getFriendsLists())[0]; + Log.i("[Manager] Presence list subscription is " + (enabled ? "enabled" : "disabled")); + friendList.enableSubscriptions(enabled); + } + } + + public void newOutgoingCall(AddressType address) { + String to = address.getText().toString(); + newOutgoingCall(to, address.getDisplayedName()); + } + + public void newOutgoingCall(String to, String displayName) { + // if (mCore.inCall()) { + // listenerDispatcher.tryingNewOutgoingCallButAlreadyInCall(); + // return; + // } + if (to == null) return; + + // If to is only a username, try to find the contact to get an alias if existing + if (!to.startsWith("sip:") || !to.contains("@")) { + LinphoneContact contact = ContactsManager.getInstance().findContactFromPhoneNumber(to); + if (contact != null) { + String alias = contact.getContactFromPresenceModelForUriOrTel(to); + if (alias != null) { + to = alias; + } + } + } + + Address lAddress; + lAddress = mCore.interpretUrl(to); // InterpretUrl does normalizePhoneNumber + if (lAddress == null) { + Log.e("[Manager] Couldn't convert to String to Address : " + to); + return; + } + + ProxyConfig lpc = mCore.getDefaultProxyConfig(); + if (mRessources.getBoolean(R.bool.forbid_self_call) + && lpc != null + && lAddress.weakEqual(lpc.getIdentityAddress())) { + return; + } + lAddress.setDisplayName(displayName); + + boolean isLowBandwidthConnection = + !LinphoneUtils.isHighBandwidthConnection( + LinphoneService.instance().getApplicationContext()); + + if (mCore.isNetworkReachable()) { + if (Version.isVideoCapable()) { + boolean prefVideoEnable = mPrefs.isVideoEnabled(); + boolean prefInitiateWithVideo = mPrefs.shouldInitiateVideoCall(); + CallManager.getInstance() + .inviteAddress( + lAddress, + prefVideoEnable && prefInitiateWithVideo, + isLowBandwidthConnection); + } else { + CallManager.getInstance().inviteAddress(lAddress, false, isLowBandwidthConnection); + } + } else if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .displayCustomToast( + getString(R.string.error_network_unreachable), Toast.LENGTH_LONG); + } else { + Log.e("[Manager] Error: " + getString(R.string.error_network_unreachable)); + } + } + + private void resetCameraFromPreferences() { + boolean useFrontCam = mPrefs.useFrontCam(); + int camId = 0; + AndroidCamera[] cameras = AndroidCameraConfiguration.retrieveCameras(); + for (AndroidCamera androidCamera : cameras) { + if (androidCamera.frontFacing == useFrontCam) { + camId = androidCamera.id; + break; + } + } + String[] devices = getLc().getVideoDevicesList(); + if (camId >= devices.length) { + Log.e( + "[Manager] Trying to use a camera id that's higher than the linphone's devices list, using 0 to prevent crash..."); + camId = 0; + } + String newDevice = devices[camId]; + LinphoneManager.getLc().setVideoDevice(newDevice); + } + + private void enableCamera(Call call, boolean enable) { + if (call != null) { + call.enableCamera(enable); + if (mServiceContext.getResources().getBoolean(R.bool.enable_call_notification)) + LinphoneService.instance() + .getNotificationManager() + .displayCallNotification(mCore.getCurrentCall()); + } + } + + public void playDtmf(ContentResolver r, char dtmf) { + try { + if (Settings.System.getInt(r, Settings.System.DTMF_TONE_WHEN_DIALING) == 0) { + // audible touch disabled: don't play on speaker, only send in outgoing stream + return; + } + } catch (SettingNotFoundException e) { + Log.e("[Manager] playDtmf exception: " + e); + } + + getLc().playDtmf(dtmf, -1); + } + + private void terminateCall() { + if (mCore.inCall()) { + mCore.terminateCall(mCore.getCurrentCall()); + } + } + + public void initTunnelFromConf() { + if (!mCore.tunnelAvailable()) return; + + NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); + Tunnel tunnel = mCore.getTunnel(); + tunnel.cleanServers(); + TunnelConfig config = mPrefs.getTunnelConfig(); + if (config.getHost() != null) { + tunnel.addServer(config); + manageTunnelServer(info); + } + } + + private boolean isTunnelNeeded(NetworkInfo info) { + if (info == null) { + Log.i("[Manager] No connectivity: tunnel should be disabled"); + return false; + } + + String pref = mPrefs.getTunnelMode(); + + if (getString(R.string.tunnel_mode_entry_value_always).equals(pref)) { + return true; + } + + if (info.getType() != ConnectivityManager.TYPE_WIFI + && getString(R.string.tunnel_mode_entry_value_3G_only).equals(pref)) { + Log.i("[Manager] Need tunnel: 'no wifi' connection"); + return true; + } + + return false; + } + + private void manageTunnelServer(NetworkInfo info) { + if (mCore == null) return; + if (!mCore.tunnelAvailable()) return; + Tunnel tunnel = mCore.getTunnel(); + + Log.i("[Manager] Managing tunnel"); + if (isTunnelNeeded(info)) { + Log.i("[Manager] Tunnel need to be activated"); + tunnel.setMode(Tunnel.Mode.Enable); + } else { + Log.i("[Manager] Tunnel should not be used"); + String pref = mPrefs.getTunnelMode(); + tunnel.setMode(Tunnel.Mode.Disable); + if (getString(R.string.tunnel_mode_entry_value_auto).equals(pref)) { + tunnel.setMode(Tunnel.Mode.Auto); + } + } + } + + private synchronized void destroyCore() { + Log.w("[Manager] Destroying Core"); + sExited = true; + ContactsManagerDestroy(); + BluetoothManagerDestroy(); + try { + mTimer.cancel(); + destroyLinphoneCore(); + } catch (RuntimeException e) { + Log.e("[Manager] Destroy Core Runtime Exception: " + e); + } finally { + try { + mServiceContext.unregisterReceiver(mHookReceiver); + } catch (Exception e) { + Log.e("[Manager] unregister receiver exception: " + e); + } + try { + mServiceContext.unregisterReceiver(mCallReceiver); + } catch (Exception e) { + Log.e("[Manager] unregister receiver exception: " + e); + } + mCore = null; + } + } + + public void restartCore() { + mCore.stop(); + mCore.start(); + } + + private synchronized void startLibLinphone(Context c, boolean isPush) { + try { + copyAssetsFromPackage(); + // traces alway start with traces enable to not missed first initialization + mCore = Factory.instance().createCore(configFile, mLinphoneFactoryConfigFile, c); + mCore.addListener(this); + if (isPush) { + Log.w( + "[Manager] We are here because of a received push notification, enter background mode before starting the Core"); + mCore.enterBackground(); + } + mCore.start(); + TimerTask lTask = + new TimerTask() { + @Override + public void run() { + LinphoneUtils.dispatchOnUIThread( + new Runnable() { + @Override + public void run() { + if (mCore != null) { + mCore.iterate(); + } + } + }); + } + }; + /*use schedule instead of scheduleAtFixedRate to avoid iterate from being call in burst after cpu wake up*/ + mTimer = new Timer("Linphone scheduler"); + mTimer.schedule(lTask, 0, 20); + } catch (Exception e) { + Log.e(e, "[Manager] Cannot start linphone"); + } + } + + private void initPushNotificationsService() { + PushNotificationUtils.init(mServiceContext); + } + + private synchronized void initLiblinphone(Core lc) { + mCore = lc; + + mCore.setZrtpSecretsFile(mBasePath + "/zrtp_secrets"); + + String deviceName = mPrefs.getDeviceName(mServiceContext); + String appName = mServiceContext.getResources().getString(R.string.user_agent); + String androidVersion = BuildConfig.VERSION_NAME; + String userAgent = appName + "/" + androidVersion + " (" + deviceName + ") LinphoneSDK"; + + mCore.setUserAgent( + userAgent, + getString(R.string.linphone_sdk_version) + + " (" + + getString(R.string.linphone_sdk_branch) + + ")"); + + // mCore.setChatDatabasePath(mChatDatabaseFile); + mCore.setCallLogsDatabasePath(mCallLogDatabaseFile); + mCore.setFriendsDatabasePath(mFriendsDatabaseFile); + mCore.setUserCertificatesPath(mUserCertsPath); + // mCore.setCallErrorTone(Reason.NotFound, mErrorToneFile); + enableDeviceRingtone(mPrefs.isDeviceRingtoneEnabled()); + + int availableCores = Runtime.getRuntime().availableProcessors(); + Log.w("[Manager] MediaStreamer : " + availableCores + " cores detected and configured"); + + mCore.migrateLogsFromRcToDb(); + + // Migrate existing linphone accounts to have conference factory uri and LIME X3Dh url set + String uri = getString(R.string.default_conference_factory_uri); + for (ProxyConfig lpc : mCore.getProxyConfigList()) { + if (lpc.getIdentityAddress().getDomain().equals(getString(R.string.default_domain))) { + if (lpc.getConferenceFactoryUri() == null) { + lpc.edit(); + Log.i( + "[Manager] Setting conference factory on proxy config " + + lpc.getIdentityAddress().asString() + + " to default value: " + + uri); + lpc.setConferenceFactoryUri(uri); + lpc.done(); + } + + if (mCore.limeX3DhAvailable()) { + String url = mCore.getLimeX3DhServerUrl(); + if (url == null || url.length() == 0) { + url = getString(R.string.default_lime_x3dh_server_url); + Log.i("[Manager] Setting LIME X3Dh server url to default value: " + url); + mCore.setLimeX3DhServerUrl(url); + } + } + } + } + + if (mServiceContext.getResources().getBoolean(R.bool.enable_push_id)) { + initPushNotificationsService(); + } + + mCallIntentFilter = new IntentFilter("android.intent.action.ACTION_NEW_OUTGOING_CALL"); + mCallIntentFilter.setPriority(99999999); + mCallReceiver = new OutgoingCallReceiver(); + try { + mServiceContext.registerReceiver(mCallReceiver, mCallIntentFilter); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } + mProximityWakelock = + mPowerManager.newWakeLock( + PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, + mServiceContext.getPackageName() + ";manager_proximity_sensor"); + + mHookIntentFilter = new IntentFilter("com.base.module.phone.HOOKEVENT"); + mHookIntentFilter.setPriority(999); + mHookReceiver = new HookReceiver(); + mServiceContext.registerReceiver(mHookReceiver, mHookIntentFilter); + + resetCameraFromPreferences(); + + mAccountCreator = + LinphoneManager.getLc() + .createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); + mAccountCreator.setListener(this); + mCallGsmON = false; + + updateMissedChatCount(); + } + + public void setHandsetMode(Boolean on) { + if (mCore.isIncomingInvitePending() && on) { + mHandsetON = true; + acceptCall(mCore.getCurrentCall()); + LinphoneActivity.instance().startIncallActivity(); + } else if (on && CallActivity.isInstanciated()) { + mHandsetON = true; + CallActivity.instance().setSpeakerEnabled(true); + CallActivity.instance().refreshInCallActions(); + } else if (!on) { + mHandsetON = false; + LinphoneManager.getInstance().terminateCall(); + } + } + + public boolean isHansetModeOn() { + return mHandsetON; + } + + private void copyAssetsFromPackage() throws IOException { + copyIfNotExist(R.raw.linphonerc_default, configFile); + copyFromPackage(R.raw.linphonerc_factory, new File(mLinphoneFactoryConfigFile).getName()); + copyIfNotExist(R.raw.lpconfig, mLPConfigXsd); + copyFromPackage( + R.raw.default_assistant_create, new File(mDefaultDynamicConfigFile).getName()); + copyFromPackage( + R.raw.linphone_assistant_create, new File(mLinphoneDynamicConfigFile).getName()); + } + + 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 = mServiceContext.openFileOutput(target, 0); + InputStream lInputStream = mRessources.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(); + } + + private void destroyLinphoneCore() { + if (LinphonePreferences.instance() != null) { + // We set network reachable at false before destroy LC to not send register with expires + // at 0 + if (LinphonePreferences.instance().isPushNotificationEnabled()) { + Log.w( + "[Manager] Setting network reachability to False to prevent unregister and allow incoming push notifications"); + mCore.setNetworkReachable(false); + } + } + mCore.stop(); + } + + public 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; + // Don't forgeting to release wakelock if held + if (mProximityWakelock.isHeld()) { + mProximityWakelock.release(); + } + } + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.timestamp == 0) return; + if (isProximitySensorNearby(event)) { + if (!mProximityWakelock.isHeld()) { + mProximityWakelock.acquire(); + } + } else { + if (mProximityWakelock.isHeld()) { + mProximityWakelock.release(); + } + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} + + public MediaScanner getMediaScanner() { + return mMediaScanner; + } + + private String getString(int key) { + return mRessources.getString(key); + } + + public void onNewSubscriptionRequested(Core lc, Friend lf, String url) {} + + public void onNotifyPresenceReceived(Core lc, Friend lf) {} + + @Override + public void onEcCalibrationAudioInit(Core lc) {} + + @Override + public void onDtmfReceived(Core lc, Call call, int dtmf) { + Log.d("[Manager] DTMF received: " + dtmf); + } + + @Override + public void onMessageReceived(Core lc, final ChatRoom cr, final ChatMessage message) { + if (mServiceContext.getResources().getBoolean(R.bool.disable_chat)) { + return; + } + + if (mCurrentChatRoomAddress != null + && cr.getPeerAddress() + .asStringUriOnly() + .equals(mCurrentChatRoomAddress.asStringUriOnly())) { + Log.i( + "[Manager] Message received for currently displayed chat room, do not make a notification"); + return; + } + + if (message.getErrorInfo() != null + && message.getErrorInfo().getReason() == Reason.UnsupportedContent) { + Log.w("[Manager] Message received but content is unsupported, do not notify it"); + return; + } + + if (!message.hasTextContent() && message.getFileTransferInformation() == null) { + Log.w( + "[Manager] Message has no text or file transfer information to display, ignoring it..."); + return; + } + + increaseUnreadCountForChatRoom(cr); + + if (mServiceContext.getResources().getBoolean(R.bool.disable_chat_message_notification) + || message.isOutgoing()) { + return; + } + + final Address from = message.getFromAddress(); + final LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(from); + final String textMessage = + (message.hasTextContent()) + ? message.getTextContent() + : getString(R.string.content_description_incoming_file); + + String file = null; + for (Content c : message.getContents()) { + if (c.isFile()) { + file = c.getFilePath(); + getMediaScanner() + .scanFile( + new File(file), + new MediaScannerListener() { + @Override + public void onMediaScanned(String path, Uri uri) { + createNotification( + cr, + contact, + from, + textMessage, + message.getTime(), + uri, + FileUtils.getMimeFromFile(path)); + } + }); + break; + } + } + + if (file == null) { + createNotification(cr, contact, from, textMessage, message.getTime(), null, null); + } + } + + private void createNotification( + ChatRoom cr, + LinphoneContact contact, + Address from, + String textMessage, + long time, + Uri file, + String mime) { + if (cr.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + if (contact != null) { + LinphoneService.instance() + .getNotificationManager() + .displayMessageNotification( + cr.getPeerAddress().asStringUriOnly(), + contact.getFullName(), + contact.getThumbnailUri(), + textMessage, + cr.getLocalAddress(), + time, + file, + mime); + } else { + LinphoneService.instance() + .getNotificationManager() + .displayMessageNotification( + cr.getPeerAddress().asStringUriOnly(), + from.getUsername(), + null, + textMessage, + cr.getLocalAddress(), + time, + file, + mime); + } + } else { + String subject = cr.getSubject(); + if (contact != null) { + LinphoneService.instance() + .getNotificationManager() + .displayGroupChatMessageNotification( + subject, + cr.getPeerAddress().asStringUriOnly(), + contact.getFullName(), + contact.getThumbnailUri(), + textMessage, + cr.getLocalAddress(), + time, + file, + mime); + } else { + LinphoneService.instance() + .getNotificationManager() + .displayGroupChatMessageNotification( + subject, + cr.getPeerAddress().asStringUriOnly(), + from.getUsername(), + null, + textMessage, + cr.getLocalAddress(), + time, + file, + mime); + } + } + } + + public void setCurrentChatRoomAddress(Address address) { + mCurrentChatRoomAddress = address; + LinphoneService.instance() + .setCurrentlyDisplayedChatRoom(address != null ? address.asStringUriOnly() : null); + } + + @Override + public void onEcCalibrationResult(Core lc, EcCalibratorStatus status, int delay_ms) { + ((AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE)) + .setMode(AudioManager.MODE_NORMAL); + mAudioManager.abandonAudioFocus(null); + Log.i("[Manager] Set audio mode on 'Normal'"); + } + + public void onGlobalStateChanged(final Core lc, final GlobalState state, final String message) { + Log.i("New global state [", state, "]"); + if (state == GlobalState.On) { + try { + initLiblinphone(lc); + } catch (IllegalArgumentException iae) { + Log.e("[Manager] Global State Changed Illegal Argument Exception: " + iae); + } + } + } + + public void onRegistrationStateChanged( + final Core lc, + final ProxyConfig proxy, + final RegistrationState state, + final String message) { + Log.i("[Manager] New registration state [" + state + "]"); + + if (state == RegistrationState.Failed) { + ConnectivityManager connectivityManager = + (ConnectivityManager) + mServiceContext.getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); + Log.i("[Manager] Active network type: " + activeNetworkInfo.getTypeName()); + if (activeNetworkInfo.isAvailable() && activeNetworkInfo.isConnected()) { + Log.i("[Manager] Active network is available"); + } + Log.i( + "[Manager] Active network reason and extra info: " + + activeNetworkInfo.getReason() + + " / " + + activeNetworkInfo.getExtraInfo()); + Log.i( + "[Manager] Active network state " + + activeNetworkInfo.getState() + + " / " + + activeNetworkInfo.getDetailedState()); + } + } + + public Context getContext() { + try { + if (LinphoneActivity.isInstanciated()) return LinphoneActivity.instance(); + else if (CallActivity.isInstanciated()) return CallActivity.instance(); + else if (CallIncomingActivity.isInstanciated()) return CallIncomingActivity.instance(); + else if (mServiceContext != null) return mServiceContext; + else if (LinphoneService.isReady()) + return LinphoneService.instance().getApplicationContext(); + } catch (Exception e) { + Log.e(e); + } + return null; + } + + public void setAudioManagerModeNormal() { + mAudioManager.setMode(AudioManager.MODE_NORMAL); + } + + private void setAudioManagerInCallMode() { + if (mAudioManager.getMode() == AudioManager.MODE_IN_COMMUNICATION) { + Log.w("[Manager][AudioManager] already in MODE_IN_COMMUNICATION, skipping..."); + return; + } + Log.d("[Manager][AudioManager] Mode: MODE_IN_COMMUNICATION"); + + mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + } + + @SuppressLint("Wakelock") + public void onCallStateChanged( + final Core lc, final Call call, final State state, final String message) { + Log.i("[Manager] New call state [", state, "]"); + if (state == State.IncomingReceived && !call.equals(lc.getCurrentCall())) { + if (call.getReplacedCall() != null) { + // attended transfer + // it will be accepted automatically. + return; + } + } + + if ((state == State.IncomingReceived || state == State.IncomingEarlyMedia) + && getCallGsmON()) { + if (mCore != null) { + mCore.declineCall(call, Reason.Busy); + } + } else if (state == State.IncomingReceived + && (LinphonePreferences.instance().isAutoAnswerEnabled()) + && !getCallGsmON()) { + TimerTask lTask = + new TimerTask() { + @Override + public void run() { + if (mCore != null) { + if (mCore.getCallsNb() > 0) { + acceptCall(call); + if (LinphoneManager.getInstance() != null) { + LinphoneManager.getInstance().routeAudioToReceiver(); + if (LinphoneActivity.instance() != null) + LinphoneActivity.instance().startIncallActivity(); + } + } + } + } + }; + mTimer = new Timer("Auto answer"); + mTimer.schedule(lTask, mPrefs.getAutoAnswerTime()); + } else if (state == State.IncomingReceived + || (state == State.IncomingEarlyMedia + && mRessources.getBoolean(R.bool.allow_ringing_while_early_media))) { + // Brighten screen for at least 10 seconds + if (mCore.getCallsNb() == 1) { + requestAudioFocus(STREAM_RING); + + mRingingCall = call; + startRinging(); + // otherwise there is the beep + } + } else if (call == mRingingCall && mIsRinging) { + // previous state was ringing, so stop ringing + stopRinging(); + } + + if (state == State.Connected) { + if (mCore.getCallsNb() == 1) { + // It is for incoming calls, because outgoing calls enter MODE_IN_COMMUNICATION + // immediately when they start. + // However, incoming call first use the MODE_RINGING to play the local ring. + if (call.getDir() == Call.Dir.Incoming) { + setAudioManagerInCallMode(); + // mAudioManager.abandonAudioFocus(null); + requestAudioFocus(STREAM_VOICE_CALL); + } + } + + if (Hacks.needSoftvolume()) { + Log.w("[Manager] Using soft volume audio hack"); + adjustVolume(0); // Synchronize + } + } + + if (state == State.End || state == State.Error) { + if (mCore.getCallsNb() == 0) { + // Disabling proximity sensor + enableProximitySensing(false); + Context activity = getContext(); + if (mAudioFocused) { + int res = mAudioManager.abandonAudioFocus(null); + Log.d( + "[Manager] Audio focus released a bit later: " + + (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED + ? "Granted" + : "Denied")); + mAudioFocused = false; + } + if (activity != null) { + TelephonyManager tm = + (TelephonyManager) activity.getSystemService(Context.TELEPHONY_SERVICE); + if (tm.getCallState() == TelephonyManager.CALL_STATE_IDLE) { + Log.d("[Manager] ---AudioManager: back to MODE_NORMAL"); + mAudioManager.setMode(AudioManager.MODE_NORMAL); + Log.d("[Manager] All call terminated, routing back to earpiece"); + routeAudioToReceiver(); + } + } + } + } + if (state == State.UpdatedByRemote) { + // If the correspondent proposes video while audio call + boolean remoteVideo = call.getRemoteParams().videoEnabled(); + boolean localVideo = call.getCurrentParams().videoEnabled(); + boolean autoAcceptCameraPolicy = + LinphonePreferences.instance().shouldAutomaticallyAcceptVideoRequests(); + if (remoteVideo + && !localVideo + && !autoAcceptCameraPolicy + && LinphoneManager.getLc().getConference() == null) { + LinphoneManager.getLc().deferCallUpdate(call); + } + } + if (state == State.OutgoingInit) { + // Enter the MODE_IN_COMMUNICATION mode as soon as possible, so that ringback + // is heard normally in earpiece or bluetooth receiver. + setAudioManagerInCallMode(); + requestAudioFocus(STREAM_VOICE_CALL); + startBluetooth(); + } + + if (state == State.StreamsRunning) { + startBluetooth(); + setAudioManagerInCallMode(); + } + } + + private void startBluetooth() { + if (BluetoothManager.getInstance().isBluetoothHeadsetAvailable()) { + BluetoothManager.getInstance().routeAudioToBluetooth(); + } + } + + public void onCallStatsUpdated(final Core lc, final Call call, final CallStats stats) {} + + @Override + public void onChatRoomStateChanged(Core lc, ChatRoom cr, ChatRoom.State state) {} + + @Override + public void onQrcodeFound(Core lc, String result) {} + + public void onCallEncryptionChanged( + Core lc, Call call, boolean encrypted, String authenticationToken) {} + + public void startEcCalibration() { + routeAudioToSpeaker(); + setAudioManagerInCallMode(); + Log.i("[Manager] Set audio mode on 'Voice Communication'"); + requestAudioFocus(STREAM_VOICE_CALL); + int oldVolume = mAudioManager.getStreamVolume(STREAM_VOICE_CALL); + int maxVolume = mAudioManager.getStreamMaxVolume(STREAM_VOICE_CALL); + mAudioManager.setStreamVolume(STREAM_VOICE_CALL, maxVolume, 0); + mCore.startEchoCancellerCalibration(); + mAudioManager.setStreamVolume(STREAM_VOICE_CALL, oldVolume, 0); + } + + public int startEchoTester() { + routeAudioToSpeaker(); + setAudioManagerInCallMode(); + Log.i("[Manager] Set audio mode on 'Voice Communication'"); + requestAudioFocus(STREAM_VOICE_CALL); + int maxVolume = mAudioManager.getStreamMaxVolume(STREAM_VOICE_CALL); + int sampleRate; + mAudioManager.setStreamVolume(STREAM_VOICE_CALL, maxVolume, 0); + String sampleRateProperty = + mAudioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE); + sampleRate = Integer.parseInt(sampleRateProperty); + mCore.startEchoTester(sampleRate); + mEchoTesterIsRunning = true; + return 1; + } + + public int stopEchoTester() { + mEchoTesterIsRunning = false; + mCore.stopEchoTester(); + routeAudioToReceiver(); + ((AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE)) + .setMode(AudioManager.MODE_NORMAL); + Log.i("[Manager] Set audio mode on 'Normal'"); + return 1; // status; + } + + public boolean getEchoTesterStatus() { + return mEchoTesterIsRunning; + } + + private void requestAudioFocus(int stream) { + if (!mAudioFocused) { + int res = + mAudioManager.requestAudioFocus( + null, stream, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE); + Log.d( + "[Manager] Audio focus requested: " + + (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED + ? "Granted" + : "Denied")); + if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) mAudioFocused = true; + } + } + + public void enableDeviceRingtone(boolean use) { + if (use) { + mCore.setRing(null); + } else { + mCore.setRing(mRingSoundFile); + } + } + + private synchronized void startRinging() { + if (!LinphonePreferences.instance().isDeviceRingtoneEnabled()) { + // Enable speaker audio route, linphone library will do the ringing itself automatically + routeAudioToSpeaker(); + return; + } + + if (mRessources.getBoolean(R.bool.allow_ringing_while_early_media)) { + routeAudioToSpeaker(); // Need to be able to ear the ringtone during the early media + } + + // if (Hacks.needGalaxySAudioHack()) + mAudioManager.setMode(MODE_RINGTONE); + + try { + if ((mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE + || mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL) + && mVibrator != null + && LinphonePreferences.instance().isIncomingCallVibrationEnabled()) { + long[] patern = {0, 1000, 1000}; + mVibrator.vibrate(patern, 1); + } + if (mRingerPlayer == null) { + requestAudioFocus(STREAM_RING); + mRingerPlayer = new MediaPlayer(); + mRingerPlayer.setAudioStreamType(STREAM_RING); + + String ringtone = + LinphonePreferences.instance() + .getRingtone(Settings.System.DEFAULT_RINGTONE_URI.toString()); + try { + if (ringtone.startsWith("content://")) { + mRingerPlayer.setDataSource(mServiceContext, Uri.parse(ringtone)); + } else { + FileInputStream fis = new FileInputStream(ringtone); + mRingerPlayer.setDataSource(fis.getFD()); + fis.close(); + } + } catch (IOException e) { + Log.e(e, "[Manager] Cannot set ringtone"); + } + + mRingerPlayer.prepare(); + mRingerPlayer.setLooping(true); + mRingerPlayer.start(); + } else { + Log.w("[Manager] Already ringing"); + } + } catch (Exception e) { + Log.e(e, "[Manager] Cannot handle incoming call"); + } + mIsRinging = true; + } + + private synchronized void stopRinging() { + if (mRingerPlayer != null) { + mRingerPlayer.stop(); + mRingerPlayer.release(); + mRingerPlayer = null; + } + if (mVibrator != null) { + mVibrator.cancel(); + } + + if (Hacks.needGalaxySAudioHack()) mAudioManager.setMode(AudioManager.MODE_NORMAL); + + mIsRinging = false; + // You may need to call galaxys audio hack after this method + if (!BluetoothManager.getInstance().isBluetoothHeadsetAvailable()) { + if (mServiceContext.getResources().getBoolean(R.bool.isTablet)) { + Log.d("[Manager] Stopped ringing, routing back to speaker"); + routeAudioToSpeaker(); + } else { + Log.d("[Manager] Stopped ringing, routing back to earpiece"); + routeAudioToReceiver(); + } + } + } + + /** @return false if already in video call. */ + public boolean addVideo() { + Call call = mCore.getCurrentCall(); + enableCamera(call, true); + return reinviteWithVideo(); + } + + public boolean acceptCall(Call call) { + if (call == null) return false; + + CallParams params = LinphoneManager.getLc().createCallParams(call); + + boolean isLowBandwidthConnection = + !LinphoneUtils.isHighBandwidthConnection( + LinphoneService.instance().getApplicationContext()); + + if (params != null) { + params.enableLowBandwidth(isLowBandwidthConnection); + params.setRecordFile( + FileUtils.getCallRecordingFilename(getContext(), call.getRemoteAddress())); + } else { + Log.e("[Manager] Could not create call params for call"); + return false; + } + + mCore.acceptCallWithParams(call, params); + return true; + } + + public void adjustVolume(int i) { + // starting from ICS, volume must be adjusted by the application, at least for + // STREAM_VOICE_CALL volume stream + mAudioManager.adjustStreamVolume( + LINPHONE_VOLUME_STREAM, + i < 0 ? AudioManager.ADJUST_LOWER : AudioManager.ADJUST_RAISE, + AudioManager.FLAG_SHOW_UI); + } + + public void isAccountWithAlias() { + if (LinphoneManager.getLc().getDefaultProxyConfig() != null) { + long now = new Timestamp(new Date().getTime()).getTime(); + if (mAccountCreator != null && LinphonePreferences.instance().getLinkPopupTime() == null + || Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) < now) { + mAccountCreator.setUsername( + LinphonePreferences.instance() + .getAccountUsername( + LinphonePreferences.instance().getDefaultAccountIndex())); + mAccountCreator.isAccountExist(); + } + } else { + LinphonePreferences.instance().setLinkPopupTime(null); + } + } + + private void askLinkWithPhoneNumber() { + if (!LinphonePreferences.instance().isLinkPopupEnabled()) return; + + long now = new Timestamp(new Date().getTime()).getTime(); + if (LinphonePreferences.instance().getLinkPopupTime() != null + && Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) >= now) return; + + long future = + new Timestamp( + LinphoneActivity.instance() + .getResources() + .getInteger(R.integer.popup_time_interval)) + .getTime(); + long newDate = now + future; + + LinphonePreferences.instance().setLinkPopupTime(String.valueOf(newDate)); + + final Dialog dialog = + LinphoneActivity.instance() + .displayDialog( + String.format( + getString(R.string.link_account_popup), + LinphoneManager.getLc() + .getDefaultProxyConfig() + .getIdentityAddress() + .asStringUriOnly())); + Button delete = dialog.findViewById(R.id.dialog_delete_button); + delete.setVisibility(View.GONE); + Button ok = dialog.findViewById(R.id.dialog_ok_button); + ok.setText(getString(R.string.link)); + ok.setVisibility(View.VISIBLE); + Button cancel = dialog.findViewById(R.id.dialog_cancel_button); + cancel.setText(getString(R.string.maybe_later)); + + dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE); + final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain); + dialog.findViewById(R.id.doNotAskAgainLabel) + .setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + doNotAskAgain.setChecked(!doNotAskAgain.isChecked()); + } + }); + + ok.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent assistant = new Intent(); + assistant.setClass(LinphoneActivity.instance(), AssistantActivity.class); + assistant.putExtra("LinkPhoneNumber", true); + assistant.putExtra("LinkPhoneNumberAsk", true); + mServiceContext.startActivity(assistant); + dialog.dismiss(); + } + }); + + cancel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (doNotAskAgain.isChecked()) { + LinphonePreferences.instance().enableLinkPopup(false); + } + dialog.dismiss(); + } + }); + dialog.show(); + } + + public String getDefaultDynamicConfigFile() { + return mDefaultDynamicConfigFile; + } + + public String getLinphoneDynamicConfigFile() { + return mLinphoneDynamicConfigFile; + } + + public boolean getCallGsmON() { + return mCallGsmON; + } + + public void setCallGsmON(boolean on) { + mCallGsmON = on; + } + + @Override + public void onTransferStateChanged(Core lc, Call call, State new_call_state) {} + + @Override + public void onInfoReceived(Core lc, Call call, InfoMessage info) { + Log.d("[Manager] Info message received from " + call.getRemoteAddress().asString()); + Content ct = info.getContent(); + if (ct != null) { + Log.d( + "[Manager] Info received with body with mime type " + + ct.getType() + + "/" + + ct.getSubtype() + + " and data [" + + ct.getStringBuffer() + + "]"); + } + } + + @Override + public void onSubscriptionStateChanged(Core lc, Event ev, SubscriptionState state) { + Log.d( + "[Manager] Subscription state changed to " + + state + + " event name is " + + ev.getName()); + } + + @Override + public void onCallLogUpdated(Core lc, CallLog newcl) {} + + @Override + public void onNotifyReceived(Core lc, Event ev, String eventName, Content content) { + Log.d("[Manager] Notify received for event " + eventName); + if (content != null) + Log.d( + "[Manager] With content " + + content.getType() + + "/" + + content.getSubtype() + + " data:" + + content.getStringBuffer()); + } + + @Override + public void onSubscribeReceived(Core lc, Event lev, String subscribeEvent, Content body) {} + + @Override + public void onPublishStateChanged(Core lc, Event ev, PublishState state) { + Log.d("[Manager] Publish state changed to " + state + " for event name " + ev.getName()); + } + + @Override + public void onIsComposingReceived(Core lc, ChatRoom cr) { + Log.d("[Manager] Composing received for chatroom " + cr.getPeerAddress().asStringUriOnly()); + } + + @Override + public void onMessageReceivedUnableDecrypt(Core lc, ChatRoom room, ChatMessage message) {} + + @Override + public void onConfiguringStatus(Core lc, ConfiguringState state, String message) { + Log.d("[Manager] Remote provisioning status = " + state.toString() + " (" + message + ")"); + + LinphonePreferences prefs = LinphonePreferences.instance(); + if (state == ConfiguringState.Successful) { + if (prefs.isProvisioningLoginViewEnabled()) { + ProxyConfig proxyConfig = lc.createProxyConfig(); + Address addr = proxyConfig.getIdentityAddress(); + wizardLoginViewDomain = addr.getDomain(); + } + prefs.setPushNotificationEnabled(prefs.isPushNotificationEnabled()); + } + } + + @Override + public void onCallCreated(Core lc, Call call) {} + + @Override + public void onLogCollectionUploadProgressIndication(Core linphoneCore, int offset, int total) { + if (total > 0) + Log.d( + "[Manager] Log upload progress: currently uploaded = " + + offset + + " , total = " + + total + + ", % = " + + String.valueOf((offset * 100) / total)); + } + + @Override + public void onVersionUpdateCheckResultReceived( + Core lc, VersionUpdateCheckResult result, String version, String url) { + if (result == VersionUpdateCheckResult.NewVersionAvailable) { + final String urlToUse = url; + final String versionAv = version; + mHandler.postDelayed( + new Runnable() { + @Override + public void run() { + AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); + builder.setMessage( + getString(R.string.update_available) + ": " + versionAv); + builder.setCancelable(false); + builder.setNeutralButton( + getString(R.string.ok), + new DialogInterface.OnClickListener() { + @Override + public void onClick( + DialogInterface dialogInterface, int i) { + if (urlToUse != null) { + Intent urlIntent = new Intent(Intent.ACTION_VIEW); + urlIntent.setData(Uri.parse(urlToUse)); + getContext().startActivity(urlIntent); + } + } + }); + builder.show(); + } + }, + 1000); + } + } + + @Override + public void onEcCalibrationAudioUninit(Core lc) {} + + private void sendLogs(String info) { + Context context = LinphoneActivity.instance(); + 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 { + context.startActivity(Intent.createChooser(i, "Send mail...")); + } catch (android.content.ActivityNotFoundException ex) { + Log.e(ex); + } + } + + @Override + public void onLogCollectionUploadStateChanged( + Core linphoneCore, LogCollectionUploadState state, String info) { + Log.d("[Manager] Log upload state: " + state.toString() + ", info = " + info); + if (state == LogCollectionUploadState.Delivered) { + ClipboardManager clipboard = + (ClipboardManager) mServiceContext.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Logs url", info); + clipboard.setPrimaryClip(clip); + Toast.makeText( + LinphoneActivity.instance(), + getString(R.string.logs_url_copied_to_clipboard), + Toast.LENGTH_SHORT) + .show(); + sendLogs(info); + } + } + + @Override + public void onFriendListCreated(Core lc, FriendList list) { + if (LinphoneService.isReady()) { + list.addListener(ContactsManager.getInstance()); + } + } + + @Override + public void onFriendListRemoved(Core lc, FriendList list) { + list.removeListener(ContactsManager.getInstance()); + } + + @Override + public void onReferReceived(Core lc, String refer_to) {} + + @Override + public void onNetworkReachable(Core lc, boolean enable) {} + + @Override + public void onAuthenticationRequested(Core lc, AuthInfo authInfo, AuthMethod method) { + // TODO Auto-generated method stub + + } + + @Override + public void onNotifyPresenceReceivedForUriOrTel( + Core lc, Friend lf, String uri_or_tel, PresenceModel presence_model) {} + + @Override + public void onBuddyInfoUpdated(Core lc, Friend lf) {} + + @Override + public void onIsAccountExist( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (status.equals(AccountCreator.Status.AccountExist)) { + accountCreator.isAccountLinked(); + } + } + + @Override + public void onCreateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onActivateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onLinkAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (status.equals(AccountCreator.Status.AccountNotLinked)) { + askLinkWithPhoneNumber(); + } + } + + @Override + public void onActivateAlias( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAccountActivated( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onRecoverAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAccountLinked( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (status.equals(AccountCreator.Status.AccountNotLinked)) { + askLinkWithPhoneNumber(); + } + } + + @Override + public void onIsAliasUsed( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onUpdateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + private void updateMissedChatCount() { + for (ChatRoom cr : LinphoneManager.getLc().getChatRooms()) { + updateUnreadCountForChatRoom(cr, cr.getUnreadMessagesCount()); + } + } + + public int getUnreadMessageCount() { + int count = 0; + for (ChatRoom room : mCore.getChatRooms()) { + count += room.getUnreadMessagesCount(); + } + return count; + } + + public void updateUnreadCountForChatRoom( + String localSipUri, String remoteSipUri, Integer value) { + String key = localSipUri + "//" + remoteSipUri; + mUnreadChatsPerRoom.put(key, value); + } + + public void updateUnreadCountForChatRoom(ChatRoom cr, Integer value) { + String localSipUri = cr.getLocalAddress().asStringUriOnly(); + String remoteSipUri = cr.getPeerAddress().asStringUriOnly(); + updateUnreadCountForChatRoom(localSipUri, remoteSipUri, value); + } + + private void increaseUnreadCountForChatRoom(ChatRoom cr) { + String localSipUri = cr.getLocalAddress().asStringUriOnly(); + String remoteSipUri = cr.getPeerAddress().asStringUriOnly(); + String key = localSipUri + "//" + remoteSipUri; + if (mUnreadChatsPerRoom.containsKey(key)) { + mUnreadChatsPerRoom.put(key, mUnreadChatsPerRoom.get(key) + 1); + } else { + mUnreadChatsPerRoom.put(key, 1); + } + } + + public interface AddressType { + CharSequence getText(); + + void setText(CharSequence s); + + String getDisplayedName(); + + void setDisplayedName(String s); + } +} diff --git a/app/src/main/java/org/linphone/LinphoneService.java b/app/src/main/java/org/linphone/LinphoneService.java new file mode 100644 index 000000000..f9171df31 --- /dev/null +++ b/app/src/main/java/org/linphone/LinphoneService.java @@ -0,0 +1,542 @@ +package org.linphone; + +/* +LinphoneService.java +Copyright (C) 2017 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.Activity; +import android.app.Application; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.provider.ContactsContract; +import android.view.WindowManager; +import java.util.ArrayList; +import org.linphone.call.CallIncomingActivity; +import org.linphone.contacts.ContactsManager; +import org.linphone.core.Call; +import org.linphone.core.Call.State; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.Factory; +import org.linphone.core.GlobalState; +import org.linphone.core.LogLevel; +import org.linphone.core.LoggingService; +import org.linphone.core.LoggingServiceListener; +import org.linphone.core.ProxyConfig; +import org.linphone.core.RegistrationState; +import org.linphone.core.tools.Log; +import org.linphone.mediastream.Version; +import org.linphone.notifications.NotificationsManager; +import org.linphone.receivers.BluetoothManager; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.LinphoneGL2JNIViewOverlay; +import org.linphone.views.LinphoneOverlay; +import org.linphone.views.LinphoneTextureViewOverlay; + +/** + * Linphone service, reacting to Incoming calls, ...
+ * + *

Roles include: + * + *

    + *
  • Initializing LinphoneManager + *
  • Starting C libLinphone through LinphoneManager + *
  • Reacting to LinphoneManager state changes + *
  • Delegating GUI state change actions to GUI listener + */ +public final class LinphoneService extends Service { + /* Listener needs to be implemented in the Service as it calls + * setLatestEventInfo and startActivity() which needs a context. + */ + private static final String START_LINPHONE_LOGS = " ==== Phone information dump ===="; + + private static LinphoneService sInstance; + + public final Handler handler = new Handler(); + + private boolean mTestDelayElapsed = true; + private CoreListenerStub mListener; + private WindowManager mWindowManager; + private LinphoneOverlay mOverlay; + private Application.ActivityLifecycleCallbacks mActivityCallbacks; + private NotificationsManager mNotificationManager; + private String mIncomingReceivedActivityName; + private Class mIncomingReceivedActivity = CallIncomingActivity.class; + + private LoggingServiceListener mJavaLoggingService = + new LoggingServiceListener() { + @Override + public void onLogMessageWritten( + LoggingService logService, String domain, LogLevel lev, String message) { + switch (lev) { + case Debug: + android.util.Log.d(domain, message); + break; + case Message: + android.util.Log.i(domain, message); + break; + case Warning: + android.util.Log.w(domain, message); + break; + case Error: + android.util.Log.e(domain, message); + break; + case Fatal: + default: + android.util.Log.wtf(domain, message); + break; + } + } + }; + + public LoggingServiceListener getJavaLoggingService() { + return mJavaLoggingService; + } + + public static boolean isReady() { + return sInstance != null && sInstance.mTestDelayElapsed; + } + + public static LinphoneService instance() { + if (isReady()) return sInstance; + + throw new RuntimeException("LinphoneService not instantiated yet"); + } + + public NotificationsManager getNotificationManager() { + return mNotificationManager; + } + + public void removeForegroundServiceNotificationIfPossible() { + mNotificationManager.removeForegroundServiceNotificationIfPossible(); + } + + public Class getIncomingReceivedActivity() { + return mIncomingReceivedActivity; + } + + public void setCurrentlyDisplayedChatRoom(String address) { + if (address != null) { + mNotificationManager.resetMessageNotifCount(address); + } + } + + private void onBackgroundMode() { + Log.i("[Service] App has entered background mode"); + if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null) { + LinphoneManager.getLcIfManagerNotDestroyedOrNull().enterBackground(); + } + } + + private void onForegroundMode() { + Log.i("[Service] App has left background mode"); + if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null) { + LinphoneManager.getLcIfManagerNotDestroyedOrNull().enterForeground(); + } + } + + private void setupActivityMonitor() { + if (mActivityCallbacks != null) return; + getApplication() + .registerActivityLifecycleCallbacks(mActivityCallbacks = new ActivityMonitor()); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + boolean isPush = false; + if (intent != null && intent.getBooleanExtra("PushNotification", false)) { + Log.i("[Service] [Push Notification] LinphoneService started because of a push"); + isPush = true; + } + + if (sInstance != null) { + Log.w("[Service] Attempt to start the LinphoneService but it is already running !"); + return START_STICKY; + } + + LinphoneManager.createAndStart(this, isPush); + + sInstance = this; // sInstance is ready once linphone manager has been created + mNotificationManager = new NotificationsManager(this); + LinphoneManager.getLc() + .addListener( + mListener = + new CoreListenerStub() { + @Override + public void onCallStateChanged( + Core lc, Call call, Call.State state, String message) { + if (sInstance == null) { + Log.i( + "[Service] Service not ready, discarding call state change to ", + state.toString()); + return; + } + + if (getResources() + .getBoolean(R.bool.enable_call_notification)) { + mNotificationManager.displayCallNotification(call); + } + + if (state == Call.State.IncomingReceived + || state == State.IncomingEarlyMedia) { + if (!LinphoneManager.getInstance().getCallGsmON()) + onIncomingReceived(); + } + + if (state == State.End + || state == State.Released + || state == State.Error) { + destroyOverlay(); + } + + if (state == State.Released + && call.getCallLog().getStatus() + == Call.Status.Missed) { + mNotificationManager.displayMissedCallNotification( + call); + } + } + + @Override + public void onGlobalStateChanged( + Core lc, GlobalState state, String message) { + // TODO global state if ON + } + + @Override + public void onRegistrationStateChanged( + Core lc, + ProxyConfig cfg, + RegistrationState state, + String smessage) { + // TODO registration status + } + }); + + if (Version.sdkAboveOrEqual(Version.API26_O_80) + && intent.getBooleanExtra("ForceStartForeground", false)) { + mNotificationManager.startForeground(); + } + + if (!Version.sdkAboveOrEqual(Version.API26_O_80) + || (ContactsManager.getInstance() != null + && ContactsManager.getInstance().hasReadContactsAccess())) { + getContentResolver() + .registerContentObserver( + ContactsContract.Contacts.CONTENT_URI, + true, + ContactsManager.getInstance()); + } + + if (!mTestDelayElapsed) { + // Only used when testing. Simulates a 5 seconds delay for launching service + handler.postDelayed( + new Runnable() { + @Override + public void run() { + mTestDelayElapsed = true; + } + }, + 5000); + } + + BluetoothManager.getInstance().initBluetooth(); + + return START_STICKY; + } + + @SuppressWarnings("unchecked") + @Override + public void onCreate() { + super.onCreate(); + + setupActivityMonitor(); + + // Needed in order for the two next calls to succeed, libraries must have been loaded first + LinphonePreferences.instance().setContext(getBaseContext()); + Factory.instance().setLogCollectionPath(getFilesDir().getAbsolutePath()); + boolean isDebugEnabled = LinphonePreferences.instance().isDebugEnabled(); + LinphoneUtils.configureLoggingService(isDebugEnabled, getString(R.string.app_name)); + // LinphoneService isn't ready yet so we have to manually set up the Java logging service + if (LinphonePreferences.instance().useJavaLogger()) { + Factory.instance().getLoggingService().addListener(mJavaLoggingService); + } + + // Dump some debugging information to the logs + Log.i(START_LINPHONE_LOGS); + dumpDeviceInformation(); + dumpInstalledLinphoneInformation(); + + mIncomingReceivedActivityName = + LinphonePreferences.instance().getActivityToLaunchOnIncomingReceived(); + try { + mIncomingReceivedActivity = + (Class) Class.forName(mIncomingReceivedActivityName); + } catch (ClassNotFoundException e) { + Log.e(e); + } + + mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + } + + public void createOverlay() { + if (mOverlay != null) destroyOverlay(); + + Core core = LinphoneManager.getLc(); + Call call = core.getCurrentCall(); + if (call == null || !call.getCurrentParams().videoEnabled()) return; + + if ("MSAndroidOpenGLDisplay".equals(core.getVideoDisplayFilter())) { + mOverlay = new LinphoneGL2JNIViewOverlay(this); + } else { + mOverlay = new LinphoneTextureViewOverlay(this); + } + WindowManager.LayoutParams params = mOverlay.getWindowManagerLayoutParams(); + params.x = 0; + params.y = 0; + mOverlay.addToWindowManager(mWindowManager, params); + } + + public void destroyOverlay() { + if (mOverlay != null) { + mOverlay.removeFromWindowManager(mWindowManager); + mOverlay.destroy(); + } + mOverlay = null; + } + + 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 (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"); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onTaskRemoved(Intent rootIntent) { + boolean serviceNotif = LinphonePreferences.instance().getServiceNotificationVisibility(); + if (serviceNotif) { + Log.i("[Service] Service is running in foreground, don't stop it"); + } else if (getResources().getBoolean(R.bool.kill_service_with_task_manager)) { + Log.i("[Service] Task removed, stop service"); + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.terminateAllCalls(); + } + + // If push is enabled, don't unregister account, otherwise do unregister + if (LinphonePreferences.instance().isPushNotificationEnabled()) { + if (lc != null) lc.setNetworkReachable(false); + } + stopSelf(); + } + super.onTaskRemoved(rootIntent); + } + + @Override + public synchronized void onDestroy() { + if (mActivityCallbacks != null) { + getApplication().unregisterActivityLifecycleCallbacks(mActivityCallbacks); + mActivityCallbacks = null; + } + destroyOverlay(); + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + lc = null; // To allow the gc calls below to free the Core + } + + sInstance = null; + LinphoneManager.destroy(); + + // Make sure our notification is gone. + if (mNotificationManager != null) { + mNotificationManager.destroy(); + } + + // This will prevent the app from crashing if the service gets killed in background mode + if (LinphoneActivity.isInstanciated()) { + Log.w("[Service] Service is getting destroyed, finish LinphoneActivity"); + LinphoneActivity.instance().finish(); + } + + if (LinphonePreferences.instance().useJavaLogger()) { + Factory.instance().getLoggingService().removeListener(mJavaLoggingService); + } + + super.onDestroy(); + } + + @SuppressWarnings("unchecked") + public void setActivityToLaunchOnIncomingReceived(String activityName) { + try { + mIncomingReceivedActivity = (Class) Class.forName(activityName); + mIncomingReceivedActivityName = activityName; + LinphonePreferences.instance() + .setActivityToLaunchOnIncomingReceived(mIncomingReceivedActivityName); + } catch (ClassNotFoundException e) { + Log.e(e); + } + } + + private void onIncomingReceived() { + Intent intent = new Intent().setClass(this, mIncomingReceivedActivity); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().startActivity(intent); + } else { + // This flag is required to start an Activity from a Service context + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + } + } + + /*Believe me or not, but knowing the application visibility state on Android is a nightmare. + After two days of hard work I ended with the following class, that does the job more or less reliabily. + */ + class ActivityMonitor implements Application.ActivityLifecycleCallbacks { + private final ArrayList activities = new ArrayList<>(); + private boolean mActive = false; + private int mRunningActivities = 0; + private InactivityChecker mLastChecker; + + @Override + public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) { + Log.i("[Service] Activity created:" + activity); + if (!activities.contains(activity)) activities.add(activity); + } + + @Override + public void onActivityStarted(Activity activity) { + Log.i("Activity started:" + activity); + } + + @Override + public synchronized void onActivityResumed(Activity activity) { + Log.i("[Service] Activity resumed:" + activity); + if (activities.contains(activity)) { + mRunningActivities++; + Log.i("[Service] runningActivities=" + mRunningActivities); + checkActivity(); + } + } + + @Override + public synchronized void onActivityPaused(Activity activity) { + Log.i("[Service] Activity paused:" + activity); + if (activities.contains(activity)) { + mRunningActivities--; + Log.i("[Service] runningActivities=" + mRunningActivities); + checkActivity(); + } + } + + @Override + public void onActivityStopped(Activity activity) { + Log.i("[Service] Activity stopped:" + activity); + } + + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + + @Override + public synchronized void onActivityDestroyed(Activity activity) { + Log.i("[Service] Activity destroyed:" + activity); + activities.remove(activity); + } + + void startInactivityChecker() { + if (mLastChecker != null) mLastChecker.cancel(); + LinphoneService.this.handler.postDelayed( + (mLastChecker = new InactivityChecker()), 2000); + } + + void checkActivity() { + if (mRunningActivities == 0) { + if (mActive) startInactivityChecker(); + } else if (mRunningActivities > 0) { + if (!mActive) { + mActive = true; + LinphoneService.this.onForegroundMode(); + } + if (mLastChecker != null) { + mLastChecker.cancel(); + mLastChecker = null; + } + } + } + + class InactivityChecker implements Runnable { + private boolean isCanceled; + + void cancel() { + isCanceled = true; + } + + @Override + public void run() { + synchronized (LinphoneService.this) { + if (!isCanceled) { + if (ActivityMonitor.this.mRunningActivities == 0 && mActive) { + mActive = false; + LinphoneService.this.onBackgroundMode(); + } + } + } + } + } + } +} diff --git a/app/src/main/java/org/linphone/assistant/AssistantActivity.java b/app/src/main/java/org/linphone/assistant/AssistantActivity.java new file mode 100644 index 000000000..083d93eb5 --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/AssistantActivity.java @@ -0,0 +1,958 @@ +package org.linphone.assistant; +/* +AssistantActivity.java +Copyright (C) 2017 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.Manifest; +import android.app.ActivityManager; +import android.app.AlarmManager; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.app.PendingIntent; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +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.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneLauncherActivity; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.core.AccountCreator; +import org.linphone.core.AccountCreatorListener; +import org.linphone.core.Address; +import org.linphone.core.AuthInfo; +import org.linphone.core.ConfiguringState; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.DialPlan; +import org.linphone.core.Factory; +import org.linphone.core.ProxyConfig; +import org.linphone.core.RegistrationState; +import org.linphone.core.TransportType; +import org.linphone.core.tools.Log; +import org.linphone.core.tools.OpenH264DownloadHelper; +import org.linphone.fragments.StatusFragment; +import org.linphone.mediastream.Version; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneUtils; +import org.linphone.utils.ThemableActivity; + +public class AssistantActivity extends ThemableActivity + implements OnClickListener, + ActivityCompat.OnRequestPermissionsResultCallback, + AccountCreatorListener { + private static final int PERMISSIONS_REQUEST_RECORD_AUDIO = 201; + private static final int PERMISSIONS_REQUEST_CAMERA = 202; + + private static AssistantActivity sInstance; + + public DialPlan country; + + private ImageView mBack /*, mCancel*/; + private AssistantFragmentsEnum mCurrentFragment; + private AssistantFragmentsEnum mLastFragment; + private AssistantFragmentsEnum mFirstFragment; + private Fragment mFragment; + private LinphonePreferences mPrefs; + private boolean mAccountCreated = false, + mNewAccount = false, + mIsLink = false, + mFromPref = false; + private CoreListenerStub mListener; + private Address mAddress; + private StatusFragment mStatus; + private ProgressDialog mProgress; + private Dialog mDialog; + private boolean mRemoteProvisioningInProgress; + private boolean mEchoCancellerAlreadyDone; + private AccountCreator mAccountCreator; + private CountryListAdapter mCountryListAdapter; + private LinearLayout mTopBar; + + public static AssistantActivity instance() { + return sInstance; + } + + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getResources().getBoolean(R.bool.orientation_portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + + setContentView(R.layout.assistant); + initUI(); + + if (getIntent().getBooleanExtra("LinkPhoneNumber", false)) { + mIsLink = true; + if (getIntent().getBooleanExtra("FromPref", false)) mFromPref = true; + displayCreateAccount(); + } else { + mFirstFragment = + getResources().getBoolean(R.bool.assistant_use_linphone_login_as_first_fragment) + ? AssistantFragmentsEnum.LINPHONE_LOGIN + : AssistantFragmentsEnum.WELCOME; + if (mFirstFragment == AssistantFragmentsEnum.WELCOME) { + mFirstFragment = + getResources() + .getBoolean( + R.bool.assistant_use_create_linphone_account_as_first_fragment) + ? AssistantFragmentsEnum.CREATE_ACCOUNT + : AssistantFragmentsEnum.WELCOME; + } + + if (findViewById(R.id.fragment_container) != null) { + if (savedInstanceState == null) { + display(mFirstFragment); + } else { + mCurrentFragment = + (AssistantFragmentsEnum) + savedInstanceState.getSerializable("CurrentFragment"); + } + } + } + if (savedInstanceState != null && savedInstanceState.containsKey("echoCanceller")) { + mEchoCancellerAlreadyDone = savedInstanceState.getBoolean("echoCanceller"); + } else { + mEchoCancellerAlreadyDone = false; + } + mPrefs = LinphonePreferences.instance(); + mStatus.enableSideMenu(false); + + if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null) { + mAccountCreator = + LinphoneManager.getLc() + .createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); + mAccountCreator.setListener(this); + } + + mCountryListAdapter = new CountryListAdapter(getApplicationContext()); + mListener = + new CoreListenerStub() { + + @Override + public void onConfiguringStatus( + Core lc, final ConfiguringState state, String message) { + if (mProgress != null) mProgress.dismiss(); + if (state == ConfiguringState.Successful) { + goToLinphoneActivity(); + } else if (state == ConfiguringState.Failed) { + Toast.makeText( + AssistantActivity.instance(), + getString(R.string.remote_provisioning_failure), + Toast.LENGTH_LONG) + .show(); + } + } + + @Override + public void onRegistrationStateChanged( + Core lc, ProxyConfig cfg, RegistrationState state, String smessage) { + if (mRemoteProvisioningInProgress) { + if (mProgress != null) mProgress.dismiss(); + if (state == RegistrationState.Ok) { + mRemoteProvisioningInProgress = false; + success(); + } + } else if (mAccountCreated && !mNewAccount) { + if (mAddress != null + && mAddress.asString() + .equals(cfg.getIdentityAddress().asString())) { + if (state == RegistrationState.Ok) { + if (mProgress != null) mProgress.dismiss(); + if (getResources() + .getBoolean(R.bool.use_phone_number_validation) + && cfg.getDomain() + .equals(getString(R.string.default_domain)) + && LinphoneManager.getLc().getDefaultProxyConfig() + != null) { + loadAccountCreator(cfg).isAccountExist(); + } else { + success(); + } + } else if (state == RegistrationState.Failed) { + if (mProgress != null) mProgress.dismiss(); + if (mDialog == null || !mDialog.isShowing()) { + mDialog = createErrorDialog(cfg, smessage); + mDialog.setCancelable(false); + mDialog.show(); + } + } else if (!(state == RegistrationState.Progress)) { + if (mProgress != null) mProgress.dismiss(); + } + } + } + } + }; + sInstance = this; + } + + @Override + protected void onResume() { + super.onResume(); + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + } + } + + @Override + protected void onPause() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + } + + super.onPause(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putSerializable("CurrentFragment", mCurrentFragment); + outState.putBoolean("echoCanceller", mEchoCancellerAlreadyDone); + super.onSaveInstanceState(outState); + } + + public void updateStatusFragment(StatusFragment fragment) { + mStatus = fragment; + } + + private AccountCreator loadAccountCreator(ProxyConfig cfg) { + ProxyConfig cfgTab[] = LinphoneManager.getLc().getProxyConfigList(); + int n = -1; + for (int i = 0; i < cfgTab.length; i++) { + if (cfgTab[i].equals(cfg)) { + n = i; + break; + } + } + if (n >= 0) { + mAccountCreator.setDomain(mPrefs.getAccountDomain(n)); + mAccountCreator.setUsername(mPrefs.getAccountUsername(n)); + } + return mAccountCreator; + } + + private void initUI() { + mBack = findViewById(R.id.back); + mBack.setOnClickListener(this); + // mCancel = findViewById(R.id.assistant_cancel); + // mCancel.setOnClickListener(this); + + mTopBar = findViewById(R.id.topbar); + if (getResources().getBoolean(R.bool.assistant_hide_top_bar)) { + mTopBar.setVisibility(View.GONE); + } + } + + private void changeFragment(Fragment newFragment) { + hideKeyboard(); + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.fragment_container, newFragment); + transaction.commitAllowingStateLoss(); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + boolean firstLaunch = LinphonePreferences.instance().isFirstLaunch(); + + /*if (id == R.id.assistant_cancel) { + hideKeyboard(); + LinphonePreferences.instance().firstLaunchSuccessful(); + if (getResources().getBoolean(R.bool.assistant_cancel_move_to_back)) { + moveTaskToBack(true); + } else { + if (firstLaunch) startActivity(new Intent().setClass(this, LinphoneActivity.class)); + finish(); + } + } else*/ + if (id == R.id.back) { + hideKeyboard(); + if (mCurrentFragment == AssistantFragmentsEnum.WELCOME) { + LinphonePreferences.instance().firstLaunchSuccessful(); + if (getResources().getBoolean(R.bool.assistant_cancel_move_to_back)) { + moveTaskToBack(true); + } else { + if (firstLaunch) + startActivity(new Intent().setClass(this, LinphoneActivity.class)); + finish(); + } + } else { + onBackPressed(); + } + } + } + + @Override + public void onBackPressed() { + if (mIsLink) { + return; + } + boolean firstLaunch = LinphonePreferences.instance().isFirstLaunch(); + if (mCurrentFragment == mFirstFragment) { + LinphonePreferences.instance().firstLaunchSuccessful(); + if (getResources().getBoolean(R.bool.assistant_cancel_move_to_back)) { + moveTaskToBack(true); + } else { + LinphonePreferences.instance().firstLaunchSuccessful(); + if (firstLaunch) startActivity(new Intent().setClass(this, LinphoneActivity.class)); + finish(); + } + } else if (mCurrentFragment == AssistantFragmentsEnum.LOGIN + || mCurrentFragment == AssistantFragmentsEnum.LINPHONE_LOGIN + || mCurrentFragment == AssistantFragmentsEnum.CREATE_ACCOUNT + || mCurrentFragment == AssistantFragmentsEnum.REMOTE_PROVISIONING) { + displayMenu(); + } else if (mCurrentFragment == AssistantFragmentsEnum.WELCOME) { + if (firstLaunch) startActivity(new Intent().setClass(this, LinphoneActivity.class)); + finish(); + } else if (mCurrentFragment == AssistantFragmentsEnum.COUNTRY_CHOOSER) { + if (mLastFragment.equals(AssistantFragmentsEnum.LINPHONE_LOGIN)) { + displayLoginLinphone(null, null); + } else { + displayCreateAccount(); + } + } else if (mCurrentFragment == AssistantFragmentsEnum.QRCODE_READER) { + displayRemoteProvisioning(""); + } + } + + public void hideKeyboard() { + InputMethodManager imm = + (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + View view = this.getCurrentFocus(); + if (imm != null && view != null) { + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + private void checkAndRequestAudioPermission() { + checkAndRequestPermission( + Manifest.permission.RECORD_AUDIO, PERMISSIONS_REQUEST_RECORD_AUDIO); + } + + private void checkAndRequestVideoPermission() { + checkAndRequestPermission(Manifest.permission.CAMERA, PERMISSIONS_REQUEST_CAMERA); + } + + private 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) { + 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: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + displayQRCodeReader(); + } + break; + case PERMISSIONS_REQUEST_RECORD_AUDIO: + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + launchEchoCancellerCalibration(); + } else { + isEchoCalibrationFinished(); + } + break; + } + } + + private void launchEchoCancellerCalibration() { + 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) { + EchoCancellerCalibrationFragment fragment = new EchoCancellerCalibrationFragment(); + fragment.enableEcCalibrationResultSending(true); + changeFragment(fragment); + mCurrentFragment = AssistantFragmentsEnum.ECHO_CANCELLER_CALIBRATION; + mBack.setVisibility(View.VISIBLE); + mBack.setEnabled(false); + } else { + checkAndRequestAudioPermission(); + } + } + + private void configureProxyConfig(AccountCreator accountCreator) { + Core lc = LinphoneManager.getLc(); + ProxyConfig proxyConfig = lc.createProxyConfig(); + AuthInfo authInfo; + + String identity = proxyConfig.getIdentityAddress().asStringUriOnly(); + if (identity == null || accountCreator.getUsername() == null) { + LinphoneUtils.displayErrorAlert(getString(R.string.error), this); + return; + } + identity = identity.replace("?", accountCreator.getUsername()); + Address addr = Factory.instance().createAddress(identity); + addr.setDisplayName(accountCreator.getUsername()); + mAddress = addr; + proxyConfig.edit(); + + proxyConfig.setIdentityAddress(addr); + + if (accountCreator.getPhoneNumber() != null && accountCreator.getPhoneNumber().length() > 0) + proxyConfig.setDialPrefix( + org.linphone.core.Utils.getPrefixFromE164(accountCreator.getPhoneNumber())); + + proxyConfig.done(); + + authInfo = + Factory.instance() + .createAuthInfo( + accountCreator.getUsername(), + null, + accountCreator.getPassword(), + accountCreator.getHa1(), + proxyConfig.getRealm(), + proxyConfig.getDomain()); + + lc.addProxyConfig(proxyConfig); + + lc.addAuthInfo(authInfo); + + lc.setDefaultProxyConfig(proxyConfig); + + if (LinphonePreferences.instance() != null) + LinphonePreferences.instance().setPushNotificationEnabled(true); + + if (!mNewAccount) { + displayRegistrationInProgressDialog(); + } + mAccountCreated = true; + } + + public void linphoneLogIn(AccountCreator accountCreator) { + LinphoneManager.getLc() + .loadConfigFromXml(LinphoneManager.getInstance().getLinphoneDynamicConfigFile()); + configureProxyConfig(accountCreator); + // Restore default values for proxy config + LinphoneManager.getLc() + .loadConfigFromXml(LinphoneManager.getInstance().getDefaultDynamicConfigFile()); + } + + public void genericLogIn( + String username, + String userid, + String password, + String displayname, + String prefix, + String domain, + TransportType transport) { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core == null) return; + + AuthInfo authInfo = + Factory.instance().createAuthInfo(username, userid, password, null, null, domain); + core.addAuthInfo(authInfo); + + ProxyConfig proxyConfig = core.createProxyConfig(); + + String identity = "sip:" + username + "@" + domain; + Address identityAddr = Factory.instance().createAddress(identity); + if (identityAddr != null) { + identityAddr.setDisplayName(displayname); + proxyConfig.setIdentityAddress(identityAddr); + } + String proxy = ""; + proxyConfig.setServerAddr(proxy); + + proxyConfig.setDialPrefix(prefix); + + core.addProxyConfig(proxyConfig); + core.setDefaultProxyConfig(proxyConfig); + + mAccountCreated = true; + success(); + } + + private void display(AssistantFragmentsEnum fragment) { + switch (fragment) { + case WELCOME: + displayMenu(); + break; + case LINPHONE_LOGIN: + displayLoginLinphone(null, null); + break; + case CREATE_ACCOUNT: + displayCreateAccount(); + break; + default: + throw new IllegalStateException("Can't handle " + fragment); + } + } + + private void displayMenu() { + mFragment = new WelcomeFragment(); + changeFragment(mFragment); + country = null; + mCurrentFragment = AssistantFragmentsEnum.WELCOME; + } + + public void displayLoginGeneric() { + mFragment = new LoginFragment(); + changeFragment(mFragment); + mCurrentFragment = AssistantFragmentsEnum.LOGIN; + } + + public void displayLoginLinphone(String username, String password) { + mFragment = new LinphoneLoginFragment(); + Bundle extras = new Bundle(); + extras.putString("Phone", null); + extras.putString("Dialcode", null); + extras.putString("Username", username); + extras.putString("Password", password); + mFragment.setArguments(extras); + changeFragment(mFragment); + mCurrentFragment = AssistantFragmentsEnum.LINPHONE_LOGIN; + } + + public void displayCreateAccount() { + mFragment = new CreateAccountFragment(); + Bundle extra = new Bundle(); + extra.putBoolean("LinkPhoneNumber", mIsLink); + extra.putBoolean("LinkFromPref", mFromPref); + mFragment.setArguments(extra); + changeFragment(mFragment); + mCurrentFragment = AssistantFragmentsEnum.CREATE_ACCOUNT; + } + + public void displayRemoteProvisioning(String url) { + mFragment = new RemoteProvisioningFragment(); + Bundle extra = new Bundle(); + extra.putString("RemoteUrl", url); + mFragment.setArguments(extra); + changeFragment(mFragment); + mCurrentFragment = AssistantFragmentsEnum.REMOTE_PROVISIONING; + } + + public void displayQRCodeReader() { + if (getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName()) + != PackageManager.PERMISSION_GRANTED) { + checkAndRequestVideoPermission(); + } else { + mFragment = new QrCodeFragment(); + changeFragment(mFragment); + mCurrentFragment = AssistantFragmentsEnum.QRCODE_READER; + } + } + + public void displayCountryChooser() { + mFragment = new CountryListFragment(); + changeFragment(mFragment); + mLastFragment = mCurrentFragment; + mCurrentFragment = AssistantFragmentsEnum.COUNTRY_CHOOSER; + } + + private void launchDownloadCodec() { + if (OpenH264DownloadHelper.isOpenH264DownloadEnabled()) { + OpenH264DownloadHelper downloadHelper = + Factory.instance().createOpenH264DownloadHelper(this); + if (Version.getCpuAbis().contains("armeabi-v7a") + && !Version.getCpuAbis().contains("x86") + && !downloadHelper.isCodecFound()) { + CodecDownloaderFragment codecFragment = new CodecDownloaderFragment(); + changeFragment(codecFragment); + mCurrentFragment = AssistantFragmentsEnum.DOWNLOAD_CODEC; + mBack.setEnabled(false); + } else goToLinphoneActivity(); + } else { + goToLinphoneActivity(); + } + } + + public void endDownloadCodec() { + goToLinphoneActivity(); + } + + private void displayRegistrationInProgressDialog() { + if (LinphoneManager.getLc().isNetworkReachable()) { + mProgress = ProgressDialog.show(this, null, null); + Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.light_grey_color)); + d.setAlpha(200); + mProgress + .getWindow() + .setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT); + mProgress.getWindow().setBackgroundDrawable(d); + mProgress.setContentView(R.layout.wait_layout); + mProgress.show(); + } + } + + public void displayRemoteProvisioningInProgressDialog() { + mRemoteProvisioningInProgress = true; + + mProgress = ProgressDialog.show(this, null, null); + Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.light_grey_color)); + d.setAlpha(200); + mProgress + .getWindow() + .setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT); + mProgress.getWindow().setBackgroundDrawable(d); + mProgress.setContentView(R.layout.wait_layout); + mProgress.show(); + } + + public void displayAssistantConfirm(String username, String password, String email) { + CreateAccountActivationFragment fragment = new CreateAccountActivationFragment(); + mNewAccount = true; + Bundle extras = new Bundle(); + extras.putString("Username", username); + extras.putString("Password", password); + extras.putString("Email", email); + fragment.setArguments(extras); + changeFragment(fragment); + + mCurrentFragment = AssistantFragmentsEnum.CREATE_ACCOUNT_ACTIVATION; + } + + public void displayAssistantCodeConfirm( + String username, String phone, String dialcode, boolean recoverAccount) { + CreateAccountCodeActivationFragment fragment = new CreateAccountCodeActivationFragment(); + mNewAccount = true; + Bundle extras = new Bundle(); + extras.putString("Username", username); + extras.putString("Phone", phone); + extras.putString("Dialcode", dialcode); + extras.putBoolean("RecoverAccount", recoverAccount); + extras.putBoolean("LinkAccount", mIsLink); + fragment.setArguments(extras); + changeFragment(fragment); + + mCurrentFragment = AssistantFragmentsEnum.CREATE_ACCOUNT_CODE_ACTIVATION; + } + + public void displayAssistantLinphoneLogin(String phone, String dialcode) { + LinphoneLoginFragment fragment = new LinphoneLoginFragment(); + mNewAccount = true; + Bundle extras = new Bundle(); + extras.putString("Phone", phone); + extras.putString("Dialcode", dialcode); + fragment.setArguments(extras); + changeFragment(fragment); + + mCurrentFragment = AssistantFragmentsEnum.LINPHONE_LOGIN; + } + + public void isAccountVerified() { + Toast.makeText(this, getString(R.string.assistant_account_validated), Toast.LENGTH_LONG) + .show(); + hideKeyboard(); + success(); + } + + public void isEchoCalibrationFinished() { + launchDownloadCodec(); + } + + private Dialog createErrorDialog(ProxyConfig proxy, String message) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + if (message.equals("Forbidden")) { + message = getString(R.string.assistant_error_bad_credentials); + } + builder.setMessage(message) + .setTitle(proxy.getState().toString()) + .setPositiveButton( + getString(R.string.continue_text), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + success(); + } + }) + .setNegativeButton( + getString(R.string.cancel), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + LinphoneManager.getLc() + .removeProxyConfig( + LinphoneManager.getLc().getDefaultProxyConfig()); + LinphonePreferences.instance().resetDefaultProxyConfig(); + LinphoneManager.getLc().refreshRegisters(); + dialog.dismiss(); + } + }); + return builder.show(); + } + + public void success() { + boolean needsEchoCalibration = LinphoneManager.getLc().isEchoCancellerCalibrationRequired(); + if (needsEchoCalibration && mPrefs.isFirstLaunch()) { + launchEchoCancellerCalibration(); + } else { + launchDownloadCodec(); + } + } + + private void goToLinphoneActivity() { + mPrefs.firstLaunchSuccessful(); + startActivity( + new Intent() + .setClass(this, LinphoneActivity.class) + .putExtra("isNewProxyConfig", true)); + finish(); + } + + public void setCoreListener() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + } + if (mStatus != null) { + mStatus.setCoreListener(); + } + } + + public void restartApplication() { + mPrefs.firstLaunchSuccessful(); + + Intent mStartActivity = new Intent(this, LinphoneLauncherActivity.class); + PendingIntent mPendingIntent = + PendingIntent.getActivity( + this, + (int) System.currentTimeMillis(), + mStartActivity, + PendingIntent.FLAG_CANCEL_CURRENT); + AlarmManager mgr = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE); + mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 500, mPendingIntent); + + finish(); + stopService(new Intent(Intent.ACTION_MAIN).setClass(this, LinphoneService.class)); + ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + am.killBackgroundProcesses(getString(R.string.sync_account_type)); + android.os.Process.killProcess(android.os.Process.myPid()); + } + + @Override + public void onIsAccountExist( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (status.equals(AccountCreator.Status.AccountExistWithAlias)) { + success(); + } else { + mIsLink = true; + displayCreateAccount(); + } + if (mAccountCreator != null) mAccountCreator.setListener(null); + } + + @Override + public void onCreateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onActivateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onLinkAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onActivateAlias( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAccountActivated( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onRecoverAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAccountLinked( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAliasUsed( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onUpdateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + public CountryListAdapter getCountryListAdapter() { + return mCountryListAdapter; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + if (mCurrentFragment == AssistantFragmentsEnum.QRCODE_READER) { + this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + } + + /** + * This class reads a JSON file containing Country-specific phone number description, and allows + * to present them into a ListView + */ + public class CountryListAdapter extends BaseAdapter implements Filterable { + + private LayoutInflater mInflater; + private final DialPlan[] allCountries; + private List filteredCountries; + private final Context context; + + CountryListAdapter(Context ctx) { + context = ctx; + allCountries = Factory.instance().getDialPlans(); + filteredCountries = new ArrayList<>(Arrays.asList(allCountries)); + } + + public void setInflater(LayoutInflater inf) { + mInflater = inf; + } + + public DialPlan getCountryFromCountryCode(String countryCode) { + countryCode = (countryCode.startsWith("+")) ? countryCode.substring(1) : countryCode; + for (DialPlan c : allCountries) { + if (c.getCountryCallingCode().compareTo(countryCode) == 0) return c; + } + return null; + } + + @Override + public int getCount() { + return filteredCountries.size(); + } + + @Override + public DialPlan getItem(int position) { + return filteredCountries.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view; + + if (convertView != null) { + view = convertView; + } else { + view = mInflater.inflate(R.layout.country_cell, parent, false); + } + + DialPlan c = filteredCountries.get(position); + + TextView name = view.findViewById(R.id.country_name); + name.setText(c.getCountry()); + + TextView dial_code = view.findViewById(R.id.country_prefix); + if (context != null) + dial_code.setText( + String.format( + context.getString(R.string.country_code), + c.getCountryCallingCode())); + + view.setTag(c); + return view; + } + + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + ArrayList filteredCountries = new ArrayList<>(); + for (DialPlan c : allCountries) { + if (c.getCountry().toLowerCase().contains(constraint) + || c.getCountryCallingCode().contains(constraint)) { + filteredCountries.add(c); + } + } + FilterResults filterResults = new FilterResults(); + filterResults.values = filteredCountries; + return filterResults; + } + + @Override + @SuppressWarnings("unchecked") + protected void publishResults(CharSequence constraint, FilterResults results) { + filteredCountries = (List) results.values; + CountryListAdapter.this.notifyDataSetChanged(); + } + }; + } + } +} diff --git a/src/android/org/linphone/assistant/AssistantFragmentsEnum.java b/app/src/main/java/org/linphone/assistant/AssistantFragmentsEnum.java similarity index 96% rename from src/android/org/linphone/assistant/AssistantFragmentsEnum.java rename to app/src/main/java/org/linphone/assistant/AssistantFragmentsEnum.java index 4b8cf250a..699555cd6 100644 --- a/src/android/org/linphone/assistant/AssistantFragmentsEnum.java +++ b/app/src/main/java/org/linphone/assistant/AssistantFragmentsEnum.java @@ -28,5 +28,6 @@ public enum AssistantFragmentsEnum { LOGIN, REMOTE_PROVISIONING, ECHO_CANCELLER_CALIBRATION, - DOWNLOAD_CODEC; + DOWNLOAD_CODEC, + QRCODE_READER } diff --git a/app/src/main/java/org/linphone/assistant/CodecDownloaderFragment.java b/app/src/main/java/org/linphone/assistant/CodecDownloaderFragment.java new file mode 100644 index 000000000..4d4ec41ee --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/CodecDownloaderFragment.java @@ -0,0 +1,220 @@ +package org.linphone.assistant; + +/* +CodecDownloaderFragment.java +Copyright (C) 2017 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.Build; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.PayloadType; +import org.linphone.core.tools.OpenH264DownloadHelper; +import org.linphone.core.tools.OpenH264DownloadHelperListener; + +public class CodecDownloaderFragment extends Fragment { + private final Handler mHandler = new Handler(); + private TextView mQuestion; + private TextView mDownloading; + private TextView mDownloaded; + private Button mYes; + private Button mNo; + private Button mOk; + private ProgressBar mProgressBar; + private TextView mDownloadingInfo; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.assistant_codec_downloader, container, false); + + mQuestion = view.findViewById(R.id.question); + mDownloading = view.findViewById(R.id.downloading); + mDownloaded = view.findViewById(R.id.downloaded); + mYes = view.findViewById(R.id.answerYes); + mNo = view.findViewById(R.id.answerNo); + mOk = view.findViewById(R.id.answerOk); + mProgressBar = view.findViewById(R.id.progressBar); + mDownloadingInfo = view.findViewById(R.id.downloadingInfo); + + final OpenH264DownloadHelper codecDownloader = + LinphoneManager.getInstance().getOpenH264DownloadHelper(); + final OpenH264DownloadHelperListener codecListener = + new OpenH264DownloadHelperListener() { + + @Override + public void OnProgress(final int current, final int max) { + mHandler.post( + new Runnable() { + @Override + public void run() { + if (current <= max) { + hideAllItems(); + mDownloadingInfo.setText(current + " / " + max); + mDownloadingInfo.setVisibility(View.VISIBLE); + mDownloading.setVisibility(View.VISIBLE); + mProgressBar.setMax(max); + mProgressBar.setProgress(current); + mProgressBar.setVisibility(View.VISIBLE); + } else { + hideAllItems(); + mDownloaded.setVisibility(View.VISIBLE); + if (Build.VERSION.SDK_INT + >= Build.VERSION_CODES.LOLLIPOP_MR1) { + enabledH264(true); + LinphoneManager.getLc() + .reloadMsPlugins( + AssistantActivity.instance() + .getApplicationInfo() + .nativeLibraryDir); + AssistantActivity.instance().endDownloadCodec(); + } else { + // We need to restart due to bad android linker + AssistantActivity.instance().restartApplication(); + } + } + } + }); + } + + @Override + public void OnError(final String error) { + mHandler.post( + new Runnable() { + @Override + public void run() { + hideAllItems(); + mDownloaded.setText("Sorry an error has occurred."); + mDownloaded.setVisibility(View.VISIBLE); + mOk.setVisibility(View.VISIBLE); + enabledH264(false); + AssistantActivity.instance().endDownloadCodec(); + } + }); + } + }; + + codecDownloader.setOpenH264HelperListener(codecListener); + + mYes.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + hideAllItems(); + mProgressBar.setVisibility(View.VISIBLE); + codecDownloader.downloadCodec(); + } + }); + + mNo.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + enabledH264(false); + AssistantActivity.instance().endDownloadCodec(); + } + }); + hideAllItems(); + + if (savedInstanceState != null) { + if (savedInstanceState.containsKey("mQuestion")) + mQuestion.setVisibility((Integer) savedInstanceState.getSerializable("mQuestion")); + else mQuestion.setVisibility(View.VISIBLE); + + if (savedInstanceState.containsKey("mYes")) + mYes.setVisibility((Integer) savedInstanceState.getSerializable("mYes")); + else mYes.setVisibility(View.VISIBLE); + + if (savedInstanceState.containsKey("mNo")) + mNo.setVisibility((Integer) savedInstanceState.getSerializable("mNo")); + else mNo.setVisibility(View.VISIBLE); + + if (savedInstanceState.containsKey("mDownloading")) + mDownloading.setVisibility( + (Integer) savedInstanceState.getSerializable("mDownloading")); + + if (savedInstanceState.containsKey("mDownloaded")) + mDownloaded.setVisibility( + (Integer) savedInstanceState.getSerializable("mDownloaded")); + + if (savedInstanceState.containsKey("context_bar")) + mProgressBar.setVisibility( + (Integer) savedInstanceState.getSerializable("context_bar")); + + if (savedInstanceState.containsKey("mDownloadingInfo")) + mDownloadingInfo.setVisibility( + (Integer) savedInstanceState.getSerializable("mDownloadingInfo")); + + if (savedInstanceState.containsKey("mOk")) + mOk.setVisibility((Integer) savedInstanceState.getSerializable("mOk")); + } else { + mYes.setVisibility(View.VISIBLE); + mQuestion.setVisibility(View.VISIBLE); + mNo.setVisibility(View.VISIBLE); + } + + return view; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + if (mQuestion != null) outState.putSerializable("mQuestion", mQuestion.getVisibility()); + if (mDownloading != null) + outState.putSerializable("mDownloading", mDownloading.getVisibility()); + if (mDownloaded != null) + outState.putSerializable("mDownloaded", mDownloaded.getVisibility()); + if (mYes != null) outState.putSerializable("mYes", mYes.getVisibility()); + if (mNo != null) outState.putSerializable("mNo", mNo.getVisibility()); + if (mOk != null) outState.putSerializable("mOk", mOk.getVisibility()); + if (mProgressBar != null) + outState.putSerializable("context_bar", mProgressBar.getVisibility()); + if (mDownloadingInfo != null) + outState.putSerializable("mDownloadingInfo", mDownloadingInfo.getVisibility()); + super.onSaveInstanceState(outState); + } + + private void hideAllItems() { + if (mQuestion != null) mQuestion.setVisibility(View.INVISIBLE); + if (mDownloading != null) mDownloading.setVisibility(View.INVISIBLE); + if (mDownloaded != null) mDownloaded.setVisibility(View.INVISIBLE); + if (mYes != null) mYes.setVisibility(View.INVISIBLE); + if (mNo != null) mNo.setVisibility(View.INVISIBLE); + if (mOk != null) mOk.setVisibility(View.INVISIBLE); + if (mProgressBar != null) mProgressBar.setVisibility(View.INVISIBLE); + if (mDownloadingInfo != null) mDownloadingInfo.setVisibility(View.INVISIBLE); + } + + private void enabledH264(boolean enable) { + PayloadType h264 = null; + for (PayloadType pt : LinphoneManager.getLc().getVideoPayloadTypes()) { + if (pt.getMimeType().equals("H264")) h264 = pt; + } + + if (h264 != null) { + h264.enable(enable); + } + } +} diff --git a/src/android/org/linphone/assistant/CountryListFragment.java b/app/src/main/java/org/linphone/assistant/CountryListFragment.java similarity index 51% rename from src/android/org/linphone/assistant/CountryListFragment.java rename to app/src/main/java/org/linphone/assistant/CountryListFragment.java index 93a1a2eff..34dbedca7 100644 --- a/src/android/org/linphone/assistant/CountryListFragment.java +++ b/app/src/main/java/org/linphone/assistant/CountryListFragment.java @@ -30,63 +30,61 @@ import android.widget.AdapterView; import android.widget.EditText; import android.widget.ImageView; import android.widget.ListView; - import org.linphone.R; import org.linphone.core.DialPlan; -public class CountryListFragment extends Fragment implements AdapterView.OnItemClickListener, View.OnClickListener { - private ListView list; - private EditText search; - private ImageView clearSearchField; - private AssistantActivity.CountryListAdapter adapter; +public class CountryListFragment extends Fragment + implements AdapterView.OnItemClickListener, View.OnClickListener { + private ListView mList; + private EditText mSearch; + private ImageView mClearSearchField; + private AssistantActivity.CountryListAdapter mAdapter; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.assistant_country_list, container, false); - adapter = AssistantActivity.instance().getCountryListAdapter(); - adapter.setInflater(inflater); + mAdapter = AssistantActivity.instance().getCountryListAdapter(); + mAdapter.setInflater(inflater); - search = view.findViewById(R.id.search_country); - clearSearchField = view.findViewById(R.id.clearSearchField); - clearSearchField.setOnClickListener(this); + mSearch = view.findViewById(R.id.search_country); + mClearSearchField = view.findViewById(R.id.clearSearchField); + mClearSearchField.setOnClickListener(this); - list = view.findViewById(R.id.countryList); - list.setAdapter(adapter); - list.setOnItemClickListener(this); + mList = view.findViewById(R.id.countryList); + mList.setAdapter(mAdapter); + mList.setOnItemClickListener(this); - search.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } + mSearch.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - adapter.getFilter().filter(s); - } + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mAdapter.getFilter().filter(s); + } - @Override - public void afterTextChanged(Editable s) { - - } - }); - search.setText(""); + @Override + public void afterTextChanged(Editable s) {} + }); + mSearch.setText(""); return view; } @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - DialPlan c = (DialPlan) view.getTag(); - AssistantActivity.instance().country = c; + AssistantActivity.instance().country = (DialPlan) view.getTag(); AssistantActivity.instance().onBackPressed(); } @Override public void onClick(View v) { if (v.getId() == R.id.clearSearchField) { - search.setText(""); + mSearch.setText(""); } } } diff --git a/app/src/main/java/org/linphone/assistant/CreateAccountActivationFragment.java b/app/src/main/java/org/linphone/assistant/CreateAccountActivationFragment.java new file mode 100644 index 000000000..dc15184a9 --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/CreateAccountActivationFragment.java @@ -0,0 +1,138 @@ +package org.linphone.assistant; +/* +CreateAccountActivationFragment.java +Copyright (C) 2017 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.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.AccountCreator; +import org.linphone.core.AccountCreatorListener; +import org.linphone.settings.LinphonePreferences; + +public class CreateAccountActivationFragment extends Fragment + implements OnClickListener, AccountCreatorListener { + private String mUsername, mPassword; + private Button mCheckAccount; + private TextView mEmail; + private AccountCreator mAccountCreator; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = + inflater.inflate( + R.layout.assistant_account_creation_email_activation, container, false); + + mAccountCreator = + LinphoneManager.getLc() + .createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); + mAccountCreator.setListener(this); + + mUsername = getArguments().getString("Username"); + mPassword = getArguments().getString("Password"); + + mAccountCreator.setUsername(mUsername); + mAccountCreator.setPassword(mPassword); + + mEmail = view.findViewById(R.id.send_email); + mEmail.setText(getArguments().getString("Email")); + + mCheckAccount = view.findViewById(R.id.assistant_check); + mCheckAccount.setOnClickListener(this); + return view; + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.assistant_check) { + mCheckAccount.setEnabled(false); + mAccountCreator.isAccountActivated(); + } + } + + @Override + public void onIsAccountExist( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onCreateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onActivateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onLinkAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onActivateAlias( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAccountActivated( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (AssistantActivity.instance() == null) { + return; + } + if (status.equals(AccountCreator.Status.AccountNotActivated)) { + Toast.makeText( + getActivity(), + getString(R.string.assistant_account_not_validated), + Toast.LENGTH_LONG) + .show(); + } else if (status.equals(AccountCreator.Status.AccountActivated)) { + AssistantActivity.instance().linphoneLogIn(accountCreator); + AssistantActivity.instance().isAccountVerified(); + } else { + Toast.makeText( + getActivity(), + getString(R.string.wizard_server_unavailable), + Toast.LENGTH_LONG) + .show(); + } + mCheckAccount.setEnabled(true); + } + + @Override + public void onRecoverAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAccountLinked( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAliasUsed( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onUpdateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} +} diff --git a/app/src/main/java/org/linphone/assistant/CreateAccountCodeActivationFragment.java b/app/src/main/java/org/linphone/assistant/CreateAccountCodeActivationFragment.java new file mode 100644 index 000000000..8354458e3 --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/CreateAccountCodeActivationFragment.java @@ -0,0 +1,227 @@ +package org.linphone.assistant; +/* +CreateAccountCodeActivationFragment.java +Copyright (C) 2017 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +import android.app.Fragment; +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.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.AccountCreator; +import org.linphone.core.AccountCreatorListener; +import org.linphone.settings.LinphonePreferences; + +public class CreateAccountCodeActivationFragment extends Fragment + implements AccountCreatorListener { + private String mUsername, mPhone, mDialcode; + private TextView mTitle, mPhonenumber; + private EditText mCode; + private boolean mRecoverAccount = false, mLinkAccount = false; + private int mCodeLength, mAccountNumber; + private ImageView mBack; + private Button mCheckAccount; + private AccountCreator mAccountCreator; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = + inflater.inflate( + R.layout.assistant_account_creation_code_activation, container, false); + + mUsername = getArguments().getString("Username"); + mPhone = getArguments().getString("Phone"); + mDialcode = getArguments().getString("Dialcode"); + mRecoverAccount = getArguments().getBoolean("RecoverAccount"); + mLinkAccount = getArguments().getBoolean("LinkAccount"); + mAccountNumber = getArguments().getInt("AccountNumber"); + + mCodeLength = LinphonePreferences.instance().getCodeLength(); + mAccountCreator = + LinphoneManager.getLc() + .createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); + mAccountCreator.setListener(this); + mAccountCreator.setUsername(mUsername); + mAccountCreator.setPhoneNumber(mPhone, mDialcode); + + mBack = view.findViewById(R.id.back); + if (mBack != null) mBack.setVisibility(Button.INVISIBLE); + + mTitle = view.findViewById(R.id.title_account_activation); + if (mLinkAccount) { + mTitle.setText(getString(R.string.assistant_link_account)); + } else if (mRecoverAccount) { + mTitle.setText(getString(R.string.assistant_linphone_account)); + } + + mPhonenumber = view.findViewById(R.id.send_phone_number); + mPhonenumber.setText(mAccountCreator.getPhoneNumber()); + + mCode = view.findViewById(R.id.assistant_code); + mCode.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if (s.length() == mCodeLength) { + mCheckAccount.setEnabled(true); + } else { + mCheckAccount.setEnabled(false); + } + } + }); + + mCheckAccount = view.findViewById(R.id.assistant_check); + mCheckAccount.setEnabled(false); + mCheckAccount.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + mCheckAccount.setEnabled(false); + mAccountCreator.setActivationCode(mCode.getText().toString()); + if (mLinkAccount) { + linkAccount(); + } else { + activateAccount(); + } + } + }); + + return view; + } + + private void linkAccount() { + mAccountCreator.setUsername( + LinphonePreferences.instance().getAccountUsername(mAccountNumber)); + mAccountCreator.setHa1(LinphonePreferences.instance().getAccountHa1(mAccountNumber)); + mAccountCreator.activateAlias(); + } + + private void activateAccount() { + if (mAccountCreator.getUsername() == null) { + mAccountCreator.setUsername(mAccountCreator.getPhoneNumber()); + } + mAccountCreator.activateAccount(); + } + + @Override + public void onIsAccountExist( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onCreateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onActivateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (AssistantActivity.instance() == null) { + return; + } + if (status.equals(AccountCreator.Status.AccountActivated)) { + mCheckAccount.setEnabled(true); + if (accountCreator.getUsername() != null) { + AssistantActivity.instance().linphoneLogIn(accountCreator); + if (!mRecoverAccount) { + AssistantActivity.instance().isAccountVerified(); + } else { + AssistantActivity.instance().success(); + } + } else { + AssistantActivity.instance().linphoneLogIn(accountCreator); + if (!mRecoverAccount) { + AssistantActivity.instance().isAccountVerified(); + } else { + AssistantActivity.instance().success(); + } + } + } else if (status.equals(AccountCreator.Status.RequestFailed)) { + Toast.makeText( + getActivity(), + getString(R.string.wizard_server_unavailable), + Toast.LENGTH_LONG) + .show(); + } else { + Toast.makeText( + getActivity(), + getString(R.string.assistant_error_confirmation_code), + Toast.LENGTH_LONG) + .show(); + AssistantActivity.instance().displayAssistantLinphoneLogin(mPhone, mDialcode); + } + } + + @Override + public void onLinkAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onActivateAlias( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (AssistantActivity.instance() == null) { + return; + } + if (status.equals(AccountCreator.Status.AccountActivated)) { + LinphonePreferences.instance() + .setPrefix( + mAccountNumber, + org.linphone.core.Utils.getPrefixFromE164( + accountCreator.getPhoneNumber())); + LinphonePreferences.instance().setLinkPopupTime(""); + AssistantActivity.instance().hideKeyboard(); + AssistantActivity.instance().success(); + } + } + + @Override + public void onIsAccountActivated( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onRecoverAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAccountLinked( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAliasUsed( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onUpdateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} +} diff --git a/app/src/main/java/org/linphone/assistant/CreateAccountFragment.java b/app/src/main/java/org/linphone/assistant/CreateAccountFragment.java new file mode 100644 index 000000000..3d09a520f --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/CreateAccountFragment.java @@ -0,0 +1,804 @@ +package org.linphone.assistant; +/* +CreateAccountFragment.java +Copyright (C) 2017 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.accounts.Account; +import android.accounts.AccountManager; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.telephony.TelephonyManager; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Patterns; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +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.TextView; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.AccountCreator; +import org.linphone.core.AccountCreator.Status; +import org.linphone.core.AccountCreatorListener; +import org.linphone.core.DialPlan; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneUtils; + +public class CreateAccountFragment extends Fragment + implements CompoundButton.OnCheckedChangeListener, OnClickListener, AccountCreatorListener { + private final Pattern UPPER_CASE_REGEX = Pattern.compile("[A-Z]"); + + private EditText mPhoneNumberEdit, + mUsernameEdit, + mPasswordEdit, + mPasswordConfirmEdit, + mEmailEdit, + mDialCode; + private TextView mPhoneNumberError, + mPasswordError, + mPasswordConfirmError, + mEmailError, + mAssisstantTitle, + mSipUri, + mSkip, + mInstruction; + private ImageView mPhoneNumberInfo; + private boolean mPasswordOk = false; + private boolean mEmailOk = false; + private boolean mConfirmPasswordOk = false; + private boolean mLinkAccount = false; + private Button mCreateAccount, mSelectCountry; + private CheckBox mUseUsername, mUseEmail; + private String mAddressSip = ""; + private int mCountryCode; + private LinearLayout mPhoneNumberLayout, + mUsernameLayout, + mEmailLayout, + mPasswordLayout, + mPasswordConfirmLayout; + private AccountCreator mAccountCreator; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.assistant_account_creation, container, false); + + // Initialize mAccountCreator + mAccountCreator = + LinphoneManager.getLc() + .createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); + mAccountCreator.setListener(this); + mAccountCreator.setDomain(getString(R.string.default_domain)); + + mInstruction = view.findViewById(R.id.message_create_account); + + mCreateAccount = view.findViewById(R.id.assistant_create); + + mPhoneNumberLayout = view.findViewById(R.id.phone_number_layout); + mUsernameLayout = view.findViewById(R.id.username_layout); + mEmailLayout = view.findViewById(R.id.email_layout); + mPasswordLayout = view.findViewById(R.id.password_layout); + mPasswordConfirmLayout = view.findViewById(R.id.password_confirm_layout); + + mUseUsername = view.findViewById(R.id.use_username); + mUseEmail = view.findViewById(R.id.use_email); + + mUsernameEdit = view.findViewById(R.id.username); + + mPhoneNumberError = view.findViewById(R.id.phone_number_error); + mPhoneNumberEdit = view.findViewById(R.id.phone_number); + mSipUri = view.findViewById(R.id.sip_uri); + + mPhoneNumberInfo = view.findViewById(R.id.info_phone_number); + + mSelectCountry = view.findViewById(R.id.select_country); + mDialCode = view.findViewById(R.id.dial_code); + mAssisstantTitle = view.findViewById(R.id.assistant_title); + + mPasswordError = view.findViewById(R.id.password_error); + mPasswordEdit = view.findViewById(R.id.password); + + mPasswordConfirmError = view.findViewById(R.id.confirm_password_error); + mPasswordConfirmEdit = view.findViewById(R.id.confirm_password); + + mEmailError = view.findViewById(R.id.email_error); + mEmailEdit = view.findViewById(R.id.email); + + mSkip = view.findViewById(R.id.assistant_skip); + + // Phone number + if (getResources().getBoolean(R.bool.use_phone_number_validation)) { + getActivity().getApplicationContext(); + // Automatically get the country code from the phone + TelephonyManager tm = + (TelephonyManager) + getActivity() + .getApplicationContext() + .getSystemService(Context.TELEPHONY_SERVICE); + String countryIso = tm.getNetworkCountryIso(); + mCountryCode = org.linphone.core.Utils.getCccFromIso(countryIso.toUpperCase()); + + mPhoneNumberLayout.setVisibility(View.VISIBLE); + + mPhoneNumberInfo.setOnClickListener(this); + mSelectCountry.setOnClickListener(this); + + DialPlan c = AssistantActivity.instance().country; + if (c != null) { + mSelectCountry.setText(c.getCountry()); + mDialCode.setText( + c.getCountryCallingCode().contains("+") + ? c.getCountryCallingCode() + : "+" + c.getCountryCallingCode()); + } else { + c = + AssistantActivity.instance() + .getCountryListAdapter() + .getCountryFromCountryCode(String.valueOf(mCountryCode)); + if (c != null) { + mSelectCountry.setText(c.getCountry()); + mDialCode.setText( + c.getCountryCallingCode().contains("+") + ? c.getCountryCallingCode() + : "+" + c.getCountryCallingCode()); + } + } + + // Allow user to enter a username instead use the phone number as username + if (getResources().getBoolean(R.bool.assistant_allow_username)) { + mUseUsername.setVisibility(View.VISIBLE); + mUseUsername.setOnCheckedChangeListener(this); + } + addPhoneNumberHandler(mPhoneNumberEdit); + addPhoneNumberHandler(mDialCode); + } + + // Password & email address + if (getResources().getBoolean(R.bool.isTablet) + || !getResources().getBoolean(R.bool.use_phone_number_validation)) { + mUseEmail.setVisibility(View.VISIBLE); + mUseEmail.setOnCheckedChangeListener(this); + + if (getResources().getBoolean(R.bool.pre_fill_email_in_assistant)) { + Account[] accounts = + AccountManager.get(getActivity()).getAccountsByType("com.google"); + + for (Account account : accounts) { + if (isEmailCorrect(account.name)) { + String possibleEmail = account.name; + mEmailEdit.setText(possibleEmail); + mAccountCreator.setEmail(possibleEmail); + mEmailOk = true; + break; + } + } + } + + addPasswordHandler(mPasswordEdit); + addConfirmPasswordHandler(mPasswordEdit, mPasswordConfirmEdit); + addEmailHandler(mEmailEdit); + } + + // Hide phone number and display username/email/password + if (!getResources().getBoolean(R.bool.use_phone_number_validation)) { + mUseEmail.setVisibility(View.GONE); + mUseUsername.setVisibility(View.GONE); + + mUsernameLayout.setVisibility(View.VISIBLE); + mPasswordLayout.setVisibility(View.VISIBLE); + mPasswordConfirmLayout.setVisibility(View.VISIBLE); + mEmailLayout.setVisibility(View.VISIBLE); + } + + // Link account with phone number + if (getArguments().getBoolean("LinkPhoneNumber")) { + mLinkAccount = true; + mUseEmail.setVisibility(View.GONE); + mUseUsername.setVisibility(View.GONE); + + mUsernameLayout.setVisibility(View.GONE); + mPasswordLayout.setVisibility(View.GONE); + mPasswordConfirmLayout.setVisibility(View.GONE); + mEmailLayout.setVisibility(View.GONE); + + mSkip.setVisibility(View.VISIBLE); + mSkip.setOnClickListener(this); + + mCreateAccount.setText(getResources().getString(R.string.link_account)); + mAssisstantTitle.setText(getResources().getString(R.string.link_account)); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mAccountCreator.setLanguage(Locale.getDefault().toLanguageTag()); + } + + addUsernameHandler(mUsernameEdit); + + mCreateAccount.setEnabled(true); + mCreateAccount.setOnClickListener(this); + + return view; + } + + @Override + public void onPause() { + super.onPause(); + mAccountCreator.setListener(null); + } + + private String getUsername() { + if (mUsernameEdit != null) { + String username = mUsernameEdit.getText().toString(); + return username.toLowerCase(Locale.getDefault()); + } + return null; + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (buttonView.getId() == R.id.use_username) { + if (isChecked) { + mUsernameLayout.setVisibility(View.VISIBLE); + onTextChanged2(); + } else { + mUsernameLayout.setVisibility(View.GONE); + mAccountCreator.setUsername(null); + onTextChanged2(); + } + } else if (buttonView.getId() == R.id.use_email) { + if (isChecked) { + mDialCode.setBackgroundResource(R.drawable.resizable_textfield); + mPhoneNumberEdit.setBackgroundResource(R.drawable.resizable_textfield); + mUseUsername.setEnabled(false); + mDialCode.setEnabled(false); + mSelectCountry.setEnabled(false); + mPhoneNumberEdit.setEnabled(false); + mEmailLayout.setVisibility(View.VISIBLE); + mPasswordLayout.setVisibility(View.VISIBLE); + mPasswordConfirmLayout.setVisibility(View.VISIBLE); + mUsernameLayout.setVisibility(View.VISIBLE); + mUseUsername.setVisibility(CheckBox.GONE); + mPhoneNumberLayout.setVisibility(LinearLayout.GONE); + mInstruction.setText(getString(R.string.assistant_create_account_part_email)); + } else { + if (!mUseUsername.isChecked()) { + mUsernameLayout.setVisibility(View.GONE); + } + mUseUsername.setEnabled(true); + mDialCode.setEnabled(true); + mSelectCountry.setEnabled(true); + mPhoneNumberEdit.setEnabled(true); + mEmailLayout.setVisibility(View.GONE); + mPasswordLayout.setVisibility(View.GONE); + mPasswordConfirmLayout.setVisibility(View.GONE); + mUseUsername.setVisibility(CheckBox.VISIBLE); + mPhoneNumberLayout.setVisibility(LinearLayout.VISIBLE); + mInstruction.setText(getString(R.string.assistant_create_account_part_1)); + } + } + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.select_country) { + AssistantActivity.instance().displayCountryChooser(); + } else if (id == R.id.assistant_skip) { + if (getArguments().getBoolean("LinkFromPref")) { + AssistantActivity.instance().finish(); + } else { + AssistantActivity.instance().success(); + } + } else if (id == R.id.info_phone_number) { + if (mLinkAccount) { + new AlertDialog.Builder(getActivity()) + .setTitle(getString(R.string.phone_number_info_title)) + .setMessage( + getString(R.string.phone_number_link_info_content) + + "\n" + + getString( + R.string + .phone_number_link_info_content_already_account)) + .show(); + } else { + new AlertDialog.Builder(getActivity()) + .setTitle(getString(R.string.phone_number_info_title)) + .setMessage(getString(R.string.phone_number_info_content)) + .show(); + } + } else if (id == R.id.assistant_create) { + mCreateAccount.setEnabled(false); + if (mLinkAccount) { + addAlias(); + } else { + if (mUseEmail.isChecked()) mAccountCreator.setPhoneNumber(null, null); + if (!getResources().getBoolean(R.bool.isTablet) || getUsername().length() > 0) { + mAccountCreator.isAccountExist(); + } else { + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForUsernameStatus( + AccountCreator.UsernameStatus.TooShort), + AssistantActivity.instance()); + mCreateAccount.setEnabled(true); + } + } + } + } + + private boolean isEmailCorrect(String email) { + Pattern emailPattern = Patterns.EMAIL_ADDRESS; + return emailPattern.matcher(email).matches(); + } + + private boolean isPasswordCorrect(String password) { + return password.length() >= 1; + } + + private void addAlias() { + mAccountCreator.setUsername( + LinphonePreferences.instance() + .getAccountUsername( + LinphonePreferences.instance().getDefaultAccountIndex())); + int status = + mAccountCreator.setPhoneNumber( + mPhoneNumberEdit.getText().toString(), + LinphoneUtils.getCountryCode(mDialCode)); + boolean isOk = status == AccountCreator.PhoneNumberStatus.Ok.toInt(); + if (isOk) { + mAccountCreator.linkAccount(); + } else { + mCreateAccount.setEnabled(true); + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForPhoneNumberStatus(status), AssistantActivity.instance()); + LinphoneUtils.displayError( + isOk, mPhoneNumberError, LinphoneUtils.errorForPhoneNumberStatus(status)); + } + } + + private void createAccount() { + if ((getResources().getBoolean(R.bool.isTablet) + || !getResources().getBoolean(R.bool.use_phone_number_validation)) + && mUseEmail.isChecked()) { + AccountCreator.EmailStatus emailStatus; + AccountCreator.PasswordStatus passwordStatus; + + passwordStatus = mAccountCreator.setPassword(mPasswordEdit.getText().toString()); + emailStatus = mAccountCreator.setEmail(mEmailEdit.getText().toString()); + + if (!mEmailOk) { + LinphoneUtils.displayError( + false, mEmailError, LinphoneUtils.errorForEmailStatus(emailStatus)); + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForEmailStatus(emailStatus), + AssistantActivity.instance()); + } else if (!mPasswordOk) { + LinphoneUtils.displayError( + false, + mPasswordError, + LinphoneUtils.errorForPasswordStatus(passwordStatus)); + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForPasswordStatus(passwordStatus), + AssistantActivity.instance()); + } else if (!mConfirmPasswordOk) { + String msg; + if (mPasswordConfirmEdit + .getText() + .toString() + .equals(mPasswordEdit.getText().toString())) { + msg = getString(R.string.wizard_password_incorrect); + } else { + msg = getString(R.string.wizard_passwords_unmatched); + } + LinphoneUtils.displayError(false, mPasswordError, msg); + LinphoneUtils.displayErrorAlert(msg, AssistantActivity.instance()); + } else { + mAccountCreator.createAccount(); + } + } else { + if (mPhoneNumberEdit.length() > 0 || mDialCode.length() > 1) { + int phoneStatus; + boolean isOk; + phoneStatus = + mAccountCreator.setPhoneNumber( + mPhoneNumberEdit.getText().toString(), + LinphoneUtils.getCountryCode(mDialCode)); + isOk = phoneStatus == AccountCreator.PhoneNumberStatus.Ok.toInt(); + if (!mUseUsername.isChecked() && mAccountCreator.getUsername() == null) { + mAccountCreator.setUsername(mAccountCreator.getPhoneNumber()); + } else { + mAccountCreator.setUsername(mUsernameEdit.getText().toString()); + mAccountCreator.setPhoneNumber( + mPhoneNumberEdit.getText().toString(), mDialCode.getText().toString()); + } + if (isOk) { + mAccountCreator.createAccount(); + } else { + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForPhoneNumberStatus(phoneStatus), + AssistantActivity.instance()); + LinphoneUtils.displayError( + isOk, + mPhoneNumberError, + LinphoneUtils.errorForPhoneNumberStatus(phoneStatus)); + } + } else { + LinphoneUtils.displayErrorAlert( + getString(R.string.assistant_create_account_part_1), + AssistantActivity.instance()); + } + } + mCreateAccount.setEnabled(true); + } + + private int getPhoneNumberStatus() { + int status = + mAccountCreator.setPhoneNumber( + mPhoneNumberEdit.getText().toString(), + LinphoneUtils.getCountryCode(mDialCode)); + mAddressSip = mAccountCreator.getPhoneNumber(); + return status; + } + + private void onTextChanged2() { + String msg = ""; + mAccountCreator.setUsername(getUsername()); + + if (!mUseEmail.isChecked() + && getResources().getBoolean(R.bool.use_phone_number_validation)) { + int status = getPhoneNumberStatus(); + boolean isOk = (status == AccountCreator.PhoneNumberStatus.Ok.toInt()); + LinphoneUtils.displayError( + isOk, mPhoneNumberError, LinphoneUtils.errorForPhoneNumberStatus(status)); + + // Username or phone number + if (getResources().getBoolean(R.bool.assistant_allow_username) + && mUseUsername.isChecked()) { + mAddressSip = getUsername(); + } + + if (!isOk) { + if (status == AccountCreator.PhoneNumberStatus.InvalidCountryCode.toInt()) { + mDialCode.setBackgroundResource(R.drawable.resizable_textfield_error); + mPhoneNumberEdit.setBackgroundResource(R.drawable.resizable_textfield); + } else { + mDialCode.setBackgroundResource(R.drawable.resizable_textfield); + mPhoneNumberEdit.setBackgroundResource(R.drawable.resizable_textfield_error); + } + + } else { + mDialCode.setBackgroundResource(R.drawable.resizable_textfield); + mPhoneNumberEdit.setBackgroundResource(R.drawable.resizable_textfield); + if (!mLinkAccount && mAddressSip.length() > 0) { + msg = + getResources() + .getString( + R.string + .assistant_create_account_phone_number_address) + + " <" + + mAddressSip + + "@" + + getResources().getString(R.string.default_domain) + + ">"; + } + } + } else { + mAddressSip = getUsername(); + if (mAddressSip.length() > 0) { + msg = + getResources() + .getString( + R.string + .assistant_create_account_phone_number_address) + + " "; + } + } + mSipUri.setText(msg); + } + + private void addPhoneNumberHandler(final EditText field) { + field.addTextChangedListener( + new TextWatcher() { + public void afterTextChanged(Editable s) { + if (field.equals(mDialCode)) { + DialPlan c = + AssistantActivity.instance() + .getCountryListAdapter() + .getCountryFromCountryCode( + mDialCode.getText().toString()); + if (c != null) { + AssistantActivity.instance().country = c; + mSelectCountry.setText(c.getCountry()); + } else { + mSelectCountry.setText(R.string.select_your_country); + } + } + } + + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + public void onTextChanged(CharSequence s, int start, int count, int after) { + onTextChanged2(); + } + }); + } + + private void addUsernameHandler(final EditText field) { + field.addTextChangedListener( + new TextWatcher() { + public void afterTextChanged(Editable s) { + Matcher matcher = UPPER_CASE_REGEX.matcher(s); + while (matcher.find()) { + CharSequence upperCaseRegion = + s.subSequence(matcher.start(), matcher.end()); + s.replace( + matcher.start(), + matcher.end(), + upperCaseRegion.toString().toLowerCase()); + } + } + + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + public void onTextChanged(CharSequence s, int start, int count, int after) { + onTextChanged2(); + } + }); + } + + private void addEmailHandler(final EditText field) { + field.addTextChangedListener( + new TextWatcher() { + public void afterTextChanged(Editable s) { + mEmailOk = false; + AccountCreator.EmailStatus status = + mAccountCreator.setEmail(field.getText().toString()); + if (status.equals(AccountCreator.EmailStatus.Ok)) { + mEmailOk = true; + LinphoneUtils.displayError(mEmailOk, mEmailError, ""); + } else { + LinphoneUtils.displayError( + mEmailOk, + mEmailError, + LinphoneUtils.errorForEmailStatus(status)); + } + } + + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + public void onTextChanged(CharSequence s, int start, int count, int after) {} + }); + } + + private void addPasswordHandler(final EditText field1) { + TextWatcher passwordListener = + new TextWatcher() { + public void afterTextChanged(Editable s) { + mPasswordOk = false; + AccountCreator.PasswordStatus status = + mAccountCreator.setPassword(field1.getText().toString()); + if (isPasswordCorrect(field1.getText().toString())) { + mPasswordOk = true; + LinphoneUtils.displayError(mPasswordOk, mPasswordError, ""); + } else { + LinphoneUtils.displayError( + mPasswordOk, + mPasswordError, + LinphoneUtils.errorForPasswordStatus(status)); + } + } + + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + public void onTextChanged(CharSequence s, int start, int count, int after) {} + }; + + field1.addTextChangedListener(passwordListener); + } + + private void addConfirmPasswordHandler(final EditText field1, final EditText field2) { + TextWatcher passwordListener = + new TextWatcher() { + public void afterTextChanged(Editable s) { + mConfirmPasswordOk = false; + if (field1.getText().toString().equals(field2.getText().toString())) { + mConfirmPasswordOk = true; + if (!isPasswordCorrect(field1.getText().toString())) { + LinphoneUtils.displayError( + mPasswordOk, + mPasswordError, + getString(R.string.wizard_password_incorrect)); + } else { + LinphoneUtils.displayError( + mConfirmPasswordOk, mPasswordConfirmError, ""); + } + } else { + LinphoneUtils.displayError( + mConfirmPasswordOk, + mPasswordConfirmError, + getString(R.string.wizard_passwords_unmatched)); + } + } + + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + public void onTextChanged(CharSequence s, int start, int count, int after) {} + }; + field1.addTextChangedListener(passwordListener); + field2.addTextChangedListener(passwordListener); + } + + @Override + public void onIsAccountExist(AccountCreator accountCreator, final Status status, String resp) { + if (status.equals(Status.AccountExist) || status.equals(Status.AccountExistWithAlias)) { + if (mUseEmail.isChecked()) { + mCreateAccount.setEnabled(true); + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForStatus(status), AssistantActivity.instance()); + } else { + accountCreator.isAliasUsed(); + } + } else { + createAccount(); + } + } + + @Override + public void onCreateAccount(AccountCreator accountCreator, Status status, String resp) { + if (status.equals(Status.AccountCreated)) { + if (mUseEmail.isChecked() + || !getResources().getBoolean(R.bool.use_phone_number_validation)) { + AssistantActivity.instance() + .displayAssistantConfirm( + getUsername(), + mPasswordEdit.getText().toString(), + mEmailEdit.getText().toString()); + } else { + AssistantActivity.instance() + .displayAssistantCodeConfirm( + getUsername(), + mPhoneNumberEdit.getText().toString(), + LinphoneUtils.getCountryCode(mDialCode), + false); + } + } else { + mCreateAccount.setEnabled(true); + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForStatus(status), AssistantActivity.instance()); + } + } + + @Override + public void onActivateAccount(AccountCreator accountCreator, Status status, String resp) {} + + @Override + public void onLinkAccount(AccountCreator accountCreator, Status status, String resp) { + if (AssistantActivity.instance() == null) { + return; + } + if (status.equals(Status.RequestOk)) { + AssistantActivity.instance() + .displayAssistantCodeConfirm( + getUsername(), + mPhoneNumberEdit.getText().toString(), + LinphoneUtils.getCountryCode(mDialCode), + false); + } + } + + @Override + public void onActivateAlias(AccountCreator accountCreator, Status status, String resp) { + if (AssistantActivity.instance() == null) { + return; + } + if (status.equals(Status.RequestOk)) { + AssistantActivity.instance() + .displayAssistantCodeConfirm( + getUsername(), + mPhoneNumberEdit.getText().toString(), + LinphoneUtils.getCountryCode(mDialCode), + false); + } + } + + @Override + public void onIsAccountActivated(AccountCreator accountCreator, Status status, String resp) { + if (AssistantActivity.instance() == null) { + return; + } + if (status.equals(Status.AccountNotActivated)) { + if (getResources().getBoolean(R.bool.isTablet) + || !getResources().getBoolean(R.bool.use_phone_number_validation)) { + // mAccountCreator.activateAccount(); // Resend email TODO + } else { + accountCreator.recoverAccount(); // Resend SMS + } + } else { + mCreateAccount.setEnabled(true); + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForStatus(status), AssistantActivity.instance()); + } + } + + @Override + public void onRecoverAccount(AccountCreator accountCreator, Status status, String resp) { + if (AssistantActivity.instance() == null) { + return; + } + if (status.equals(Status.RequestOk)) { + AssistantActivity.instance() + .displayAssistantCodeConfirm( + getUsername(), + mPhoneNumberEdit.getText().toString(), + mDialCode.getText().toString(), + false); + } else { + mCreateAccount.setEnabled(true); + // SMS error + LinphoneUtils.displayErrorAlert( + getString(R.string.request_failed), AssistantActivity.instance()); + } + } + + @Override + public void onIsAccountLinked(AccountCreator accountCreator, Status status, String resp) {} + + @Override + public void onIsAliasUsed(AccountCreator ac, Status status, String resp) { + if (AssistantActivity.instance() == null) { + return; + } + if (status.equals(Status.AliasIsAccount) || status.equals(Status.AliasExist)) { + if (mAccountCreator.getPhoneNumber() != null + && mAccountCreator.getUsername() != null + && mAccountCreator.getPhoneNumber().compareTo(mAccountCreator.getUsername()) + == 0) { + mAccountCreator.isAccountActivated(); + } else { + mCreateAccount.setEnabled(true); + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForStatus(status), AssistantActivity.instance()); + } + } else { + mAccountCreator.isAccountActivated(); + } + } + + @Override + public void onUpdateAccount(AccountCreator accountCreator, Status status, String resp) {} +} diff --git a/app/src/main/java/org/linphone/assistant/EchoCancellerCalibrationFragment.java b/app/src/main/java/org/linphone/assistant/EchoCancellerCalibrationFragment.java new file mode 100644 index 000000000..bd463dccc --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/EchoCancellerCalibrationFragment.java @@ -0,0 +1,117 @@ +package org.linphone.assistant; + +/* +EchoCancellerCalibrationFragment.java +Copyright (C) 2017 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.Build; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.EcCalibratorStatus; +import org.linphone.core.XmlRpcArgType; +import org.linphone.core.XmlRpcRequest; +import org.linphone.core.XmlRpcRequestListener; +import org.linphone.core.XmlRpcSession; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; + +public class EchoCancellerCalibrationFragment extends Fragment implements XmlRpcRequestListener { + private final Handler mHandler = new Handler(); + private boolean mSendEcCalibrationResult = false; + private CoreListenerStub mListener; + private XmlRpcSession mXmlRpcSession; + private XmlRpcRequest mXmlRpcRequest; + private Runnable mRunFinished; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.assistant_ec_calibration, container, false); + + mListener = + new CoreListenerStub() { + @Override + public void onEcCalibrationResult( + Core lc, EcCalibratorStatus status, int delay_ms) { + lc.removeListener(mListener); + LinphoneManager.getInstance().routeAudioToReceiver(); + if (mSendEcCalibrationResult) { + sendEcCalibrationResult(status, delay_ms); + } else { + AssistantActivity.instance().isEchoCalibrationFinished(); + } + } + }; + mRunFinished = + new Runnable() { + public void run() { + AssistantActivity.instance().isEchoCalibrationFinished(); + } + }; + + mXmlRpcSession = + LinphoneManager.getLcIfManagerNotDestroyedOrNull() + .createXmlRpcSession(LinphonePreferences.instance().getXmlrpcUrl()); + mXmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.None, "add_ec_calibration_result"); + mXmlRpcRequest.setListener(this); + + LinphoneManager.getLc().addListener(mListener); + LinphoneManager.getInstance().startEcCalibration(); + return view; + } + + public void enableEcCalibrationResultSending(boolean enabled) { + mSendEcCalibrationResult = enabled; + } + + @Override + public void onResponse(XmlRpcRequest request) { + mHandler.post(mRunFinished); + } + + private void sendEcCalibrationResult(EcCalibratorStatus status, int delayMs) { + Boolean hasBuiltInEchoCanceler = LinphoneManager.getLc().hasBuiltinEchoCanceller(); + Log.i( + "Add echo canceller calibration result: manufacturer=" + + Build.MANUFACTURER + + " model=" + + Build.MODEL + + " status=" + + status + + " delay=" + + delayMs + + "ms" + + " hasBuiltInEchoCanceler " + + hasBuiltInEchoCanceler); + mXmlRpcRequest.addStringArg(Build.MANUFACTURER); + mXmlRpcRequest.addStringArg(Build.MODEL); + mXmlRpcRequest.addStringArg(status.toString()); + mXmlRpcRequest.addIntArg(delayMs); + mXmlRpcRequest.addIntArg(hasBuiltInEchoCanceler ? 1 : 0); + mXmlRpcSession.sendRequest(mXmlRpcRequest); + } +} diff --git a/app/src/main/java/org/linphone/assistant/LinphoneLoginFragment.java b/app/src/main/java/org/linphone/assistant/LinphoneLoginFragment.java new file mode 100644 index 000000000..e4029c6af --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/LinphoneLoginFragment.java @@ -0,0 +1,431 @@ +package org.linphone.assistant; +/* +LinphoneLoginFragment.java +Copyright (C) 2017 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.AlertDialog; +import android.app.Fragment; +import android.content.Context; +import android.os.Build; +import android.os.Bundle; +import android.telephony.TelephonyManager; +import android.text.Editable; +import android.text.Html; +import android.text.TextWatcher; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +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.TextView; +import java.util.Locale; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.AccountCreator; +import org.linphone.core.AccountCreatorListener; +import org.linphone.core.DialPlan; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneUtils; + +public class LinphoneLoginFragment extends Fragment + implements CompoundButton.OnCheckedChangeListener, + OnClickListener, + TextWatcher, + AccountCreatorListener { + private EditText mLogin, mPassword, mPhoneNumberEdit, mDialCode; + private Button mApply, mSelectCountry; + private CheckBox mUseUsername; + private LinearLayout mPhoneNumberLayout, mUsernameLayout, mPasswordLayout; + private TextView mForgotPassword, mMessagePhoneNumber, mPhoneNumberError; + private Boolean mRecoverAccount; + private AccountCreator mAccountCreator; + private int mCountryCode; + private String mPhone, mDialcode, mUsername, mPwd; + private ImageView mPhoneNumberInfo; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.assistant_linphone_login, container, false); + + mAccountCreator = + LinphoneManager.getLc() + .createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl()); + mAccountCreator.setListener(this); + + mLogin = view.findViewById(R.id.assistant_username); + mLogin.addTextChangedListener(this); + + mRecoverAccount = true; + + mDialCode = view.findViewById(R.id.dial_code); + + mPhoneNumberEdit = view.findViewById(R.id.phone_number); + mPhoneNumberLayout = view.findViewById(R.id.phone_number_layout); + mPhoneNumberError = view.findViewById(R.id.phone_number_error_2); + + mPhoneNumberInfo = view.findViewById(R.id.info_phone_number); + + mUseUsername = view.findViewById(R.id.use_username); + mUsernameLayout = view.findViewById(R.id.username_layout); + mPasswordLayout = view.findViewById(R.id.password_layout); + mPassword = view.findViewById(R.id.assistant_password); + mMessagePhoneNumber = view.findViewById(R.id.message_phone_number); + + mForgotPassword = view.findViewById(R.id.forgot_password); + mSelectCountry = view.findViewById(R.id.select_country); + + mApply = view.findViewById(R.id.assistant_apply); + mApply.setEnabled(true); + mApply.setOnClickListener(this); + + // Phone number + if (getResources().getBoolean(R.bool.use_phone_number_validation)) { + mMessagePhoneNumber.setText(getString(R.string.assistant_create_account_part_1)); + mPhone = getArguments().getString("Phone"); + mDialcode = getArguments().getString("Dialcode"); + + getActivity().getApplicationContext(); + // Automatically get the country code from the mPhone + TelephonyManager tm = + (TelephonyManager) + getActivity() + .getApplicationContext() + .getSystemService(Context.TELEPHONY_SERVICE); + String countryIso = tm.getNetworkCountryIso(); + mCountryCode = org.linphone.core.Utils.getCccFromIso(countryIso.toUpperCase()); + + DialPlan c = AssistantActivity.instance().country; + if (c != null) { + mSelectCountry.setText(c.getCountry()); + mDialCode.setText( + c.getCountryCallingCode().contains("+") + ? c.getCountryCallingCode() + : "+" + c.getCountryCallingCode()); + } else { + c = + AssistantActivity.instance() + .getCountryListAdapter() + .getCountryFromCountryCode(String.valueOf(mCountryCode)); + if (c != null) { + mSelectCountry.setText(c.getCountry()); + mDialCode.setText( + c.getCountryCallingCode().contains("+") + ? c.getCountryCallingCode() + : "+" + c.getCountryCallingCode()); + } + } + + mPhoneNumberLayout.setVisibility(View.VISIBLE); + mSelectCountry.setOnClickListener(this); + mPhoneNumberInfo.setOnClickListener(this); + + // Allow user to enter a mUsername instead use the mPhone number as mUsername + if (getResources().getBoolean(R.bool.assistant_allow_username)) { + mUseUsername.setVisibility(View.VISIBLE); + mUseUsername.setOnCheckedChangeListener(this); + } + + if (mPhone != null) mPhoneNumberEdit.setText(mPhone); + if (mDialcode != null) mDialCode.setText("+" + mDialcode); + } + + if (getResources().getBoolean(R.bool.assistant_allow_username)) { + mUseUsername.setVisibility(View.VISIBLE); + mUseUsername.setOnCheckedChangeListener(this); + mPassword.addTextChangedListener(this); + mForgotPassword.setText( + Html.fromHtml( + "" + + getString(R.string.forgot_password) + + "")); + mForgotPassword.setMovementMethod(LinkMovementMethod.getInstance()); + } + + // Hide mPhone number and display mUsername/email/mPassword + if (!getResources().getBoolean(R.bool.use_phone_number_validation)) { + mPhoneNumberLayout.setVisibility(View.GONE); + mUseUsername.setVisibility(View.GONE); + + mUsernameLayout.setVisibility(View.VISIBLE); + mPasswordLayout.setVisibility(View.VISIBLE); + } + + // When we come from generic mLogin fragment + mUsername = getArguments().getString("Username"); + mPwd = getArguments().getString("Password"); + if (mUsername != null && mPwd != null) { + mUseUsername.setChecked(true); + onCheckedChanged(mUseUsername, true); + mLogin.setText(mUsername); + mPassword.setText(mPwd); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mAccountCreator.setLanguage(Locale.getDefault().toLanguageTag()); + } + + addPhoneNumberHandler(mDialCode); + addPhoneNumberHandler(mPhoneNumberEdit); + + return view; + } + + private void linphoneLogIn() { + if (mLogin.getText() == null + || mLogin.length() == 0 + || mPassword.getText() == null + || mPassword.length() == 0) { + LinphoneUtils.displayErrorAlert( + getString(R.string.first_launch_no_login_password), + AssistantActivity.instance()); + mApply.setEnabled(true); + return; + } + mAccountCreator.setUsername(mLogin.getText().toString()); + mAccountCreator.setPassword(mPassword.getText().toString()); + mAccountCreator.setDomain(getString(R.string.default_domain)); + mAccountCreator.isAccountExist(); + } + + private int getPhoneNumberStatus() { + return mAccountCreator.setPhoneNumber( + mPhoneNumberEdit.getText().toString(), LinphoneUtils.getCountryCode(mDialCode)); + } + + private void addPhoneNumberHandler(final EditText field) { + field.addTextChangedListener( + new TextWatcher() { + public void afterTextChanged(Editable s) { + if (field.equals(mDialCode)) { + DialPlan c = + AssistantActivity.instance() + .getCountryListAdapter() + .getCountryFromCountryCode( + mDialCode.getText().toString()); + if (c != null) { + AssistantActivity.instance().country = c; + mSelectCountry.setText(c.getCountry()); + } else { + mSelectCountry.setText(R.string.select_your_country); + } + } + } + + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + public void onTextChanged(CharSequence s, int start, int count, int after) { + onTextChanged2(); + } + }); + } + + @Override + public void onResume() { + super.onResume(); + mRecoverAccount = mUseUsername == null || !mUseUsername.isChecked(); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.assistant_apply) { + mApply.setEnabled(false); + if (mRecoverAccount) { + recoverAccount(); + } else { + linphoneLogIn(); + } + } else if (id == R.id.info_phone_number) { + new AlertDialog.Builder(getActivity()) + .setTitle(getString(R.string.phone_number_info_title)) + .setMessage(getString(R.string.phone_number_link_info_content)) + .show(); + } else if (id == R.id.select_country) { + AssistantActivity.instance().displayCountryChooser(); + } + } + + private void recoverAccount() { + if (mPhoneNumberEdit.length() > 0 || mDialCode.length() > 1) { + int status = getPhoneNumberStatus(); + boolean isOk = status == AccountCreator.PhoneNumberStatus.Ok.toInt(); + if (isOk) { + mAccountCreator.isAliasUsed(); + } else { + mApply.setEnabled(true); + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForPhoneNumberStatus(status), + AssistantActivity.instance()); + LinphoneUtils.displayError( + isOk, mPhoneNumberError, LinphoneUtils.errorForPhoneNumberStatus(status)); + } + } else { + mApply.setEnabled(true); + LinphoneUtils.displayErrorAlert( + getString(R.string.assistant_create_account_part_1), + AssistantActivity.instance()); + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + private void onTextChanged2() { + int status = getPhoneNumberStatus(); + boolean isOk = status == AccountCreator.PhoneNumberStatus.Ok.toInt(); + LinphoneUtils.displayError( + isOk, mPhoneNumberError, LinphoneUtils.errorForPhoneNumberStatus(status)); + if (!isOk) { + if ((1 == (status & AccountCreator.PhoneNumberStatus.InvalidCountryCode.toInt()))) { + mDialCode.setBackgroundResource(R.drawable.resizable_textfield_error); + mPhoneNumberEdit.setBackgroundResource(R.drawable.resizable_textfield); + } else { + mDialCode.setBackgroundResource(R.drawable.resizable_textfield); + mPhoneNumberEdit.setBackgroundResource(R.drawable.resizable_textfield_error); + } + } else { + mAccountCreator.setPhoneNumber( + mPhoneNumberEdit.getText().toString(), mDialCode.getText().toString()); + mDialCode.setBackgroundResource(R.drawable.resizable_textfield); + mPhoneNumberEdit.setBackgroundResource(R.drawable.resizable_textfield); + } + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + onTextChanged2(); + } + + @Override + public void afterTextChanged(Editable s) {} + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (buttonView.getId() == R.id.use_username) { + if (isChecked) { + mUsernameLayout.setVisibility(View.VISIBLE); + mPasswordLayout.setVisibility(View.VISIBLE); + mPhoneNumberEdit.setVisibility(EditText.GONE); + mPhoneNumberLayout.setVisibility(LinearLayout.GONE); + mMessagePhoneNumber.setText(getString(R.string.assistant_linphone_login_desc)); + mRecoverAccount = false; + } else { + mUsernameLayout.setVisibility(View.GONE); + mPasswordLayout.setVisibility(View.GONE); + mPhoneNumberEdit.setVisibility(EditText.VISIBLE); + mPhoneNumberLayout.setVisibility(LinearLayout.VISIBLE); + mMessagePhoneNumber.setText(getString(R.string.assistant_create_account_part_1)); + mRecoverAccount = true; + } + } + } + + @Override + public void onIsAccountExist( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (AssistantActivity.instance() == null) { + mApply.setEnabled(true); + return; + } + if (status.equals(AccountCreator.Status.AccountExist) + || status.equals(AccountCreator.Status.AccountExistWithAlias)) { + AssistantActivity.instance().linphoneLogIn(accountCreator); + } else { + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForStatus(status), AssistantActivity.instance()); + } + mApply.setEnabled(true); + } + + @Override + public void onCreateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onActivateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onLinkAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onActivateAlias( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAccountActivated( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onRecoverAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (AssistantActivity.instance() == null) { + mApply.setEnabled(true); + return; + } + if (status.equals(AccountCreator.Status.ServerError)) { + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForStatus(AccountCreator.Status.RequestFailed), + AssistantActivity.instance()); + mApply.setEnabled(true); + } else { + AssistantActivity.instance() + .displayAssistantCodeConfirm( + accountCreator.getUsername(), + mPhoneNumberEdit.getText().toString(), + LinphoneUtils.getCountryCode(mDialCode), + true); + } + } + + @Override + public void onIsAccountLinked( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} + + @Override + public void onIsAliasUsed( + AccountCreator accountCreator, AccountCreator.Status status, String resp) { + if (AssistantActivity.instance() == null) { + mApply.setEnabled(true); + return; + } + if (status.equals(AccountCreator.Status.AliasIsAccount) + || status.equals(AccountCreator.Status.AliasExist)) { + accountCreator.recoverAccount(); + } else { + mApply.setEnabled(true); + LinphoneUtils.displayErrorAlert( + LinphoneUtils.errorForStatus(status), AssistantActivity.instance()); + } + } + + @Override + public void onUpdateAccount( + AccountCreator accountCreator, AccountCreator.Status status, String resp) {} +} diff --git a/app/src/main/java/org/linphone/assistant/LoginFragment.java b/app/src/main/java/org/linphone/assistant/LoginFragment.java new file mode 100644 index 000000000..e8465685e --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/LoginFragment.java @@ -0,0 +1,125 @@ +package org.linphone.assistant; +/* +LoginFragment.java +Copyright (C) 2017 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.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.Button; +import android.widget.EditText; +import android.widget.RadioGroup; +import android.widget.Toast; +import org.linphone.R; +import org.linphone.core.TransportType; + +public class LoginFragment extends Fragment implements OnClickListener, TextWatcher { + private EditText mLogin, mUserid, mPassword, mDomain, mDisplayName; + private RadioGroup mTransports; + private Button mApply; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.assistant_login, container, false); + + mLogin = view.findViewById(R.id.assistant_username); + mLogin.addTextChangedListener(this); + mDisplayName = view.findViewById(R.id.assistant_display_name); + mDisplayName.addTextChangedListener(this); + mUserid = view.findViewById(R.id.assistant_userid); + mUserid.addTextChangedListener(this); + mPassword = view.findViewById(R.id.assistant_password); + mPassword.addTextChangedListener(this); + mDomain = view.findViewById(R.id.assistant_domain); + mDomain.addTextChangedListener(this); + mTransports = view.findViewById(R.id.assistant_transports); + mApply = view.findViewById(R.id.assistant_apply); + mApply.setEnabled(false); + mApply.setOnClickListener(this); + + return view; + } + + @Override + public void onClick(View v) { + int id = v.getId(); + + if (id == R.id.assistant_apply) { + if (mLogin.getText() == null + || mLogin.length() == 0 + || mPassword.getText() == null + || mPassword.length() == 0 + || mDomain.getText() == null + || mDomain.length() == 0) { + Toast.makeText( + getActivity(), + getString(R.string.first_launch_no_login_password), + Toast.LENGTH_LONG) + .show(); + return; + } + + TransportType transport; + if (mTransports.getCheckedRadioButtonId() == R.id.transport_udp) { + transport = TransportType.Udp; + } else { + if (mTransports.getCheckedRadioButtonId() == R.id.transport_tcp) { + transport = TransportType.Tcp; + } else { + transport = TransportType.Tls; + } + } + + if (mDomain.getText().toString().compareTo(getString(R.string.default_domain)) == 0) { + AssistantActivity.instance() + .displayLoginLinphone( + mLogin.getText().toString(), mPassword.getText().toString()); + } else { + AssistantActivity.instance() + .genericLogIn( + mLogin.getText().toString(), + mUserid.getText().toString(), + mPassword.getText().toString(), + mDisplayName.getText().toString(), + null, + mDomain.getText().toString(), + transport); + } + } + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mApply.setEnabled( + !mLogin.getText().toString().isEmpty() + && !mPassword.getText().toString().isEmpty() + && !mDomain.getText().toString().isEmpty()); + } + + @Override + public void afterTextChanged(Editable s) {} +} diff --git a/app/src/main/java/org/linphone/assistant/QrCodeFragment.java b/app/src/main/java/org/linphone/assistant/QrCodeFragment.java new file mode 100644 index 000000000..95a06104a --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/QrCodeFragment.java @@ -0,0 +1,99 @@ +package org.linphone.assistant; + +/* +QrCodeFragment.java +Copyright (C) 2018 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.TextureView; +import android.view.View; +import android.view.ViewGroup; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration; + +public class QrCodeFragment extends Fragment { + private TextureView mQrcodeView; + private CoreListenerStub mListener; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.qrcode, container, false); + + mQrcodeView = view.findViewById(R.id.qrcodeCaptureSurface); + + LinphoneManager.getLc().setNativePreviewWindowId(mQrcodeView); + + mListener = + new CoreListenerStub() { + @Override + public void onQrcodeFound(Core lc, String result) { + enableQrcodeReader(false); + AssistantActivity.instance().displayRemoteProvisioning(result); + } + }; + + return view; + } + + private void enableQrcodeReader(boolean enable) { + LinphoneManager.getLc().enableQrcodeVideoPreview(enable); + LinphoneManager.getLc().enableVideoPreview(enable); + if (enable) { + LinphoneManager.getLc().addListener(mListener); + } else { + LinphoneManager.getLc().removeListener(mListener); + } + } + + private void setBackCamera() { + int camId = 0; + AndroidCameraConfiguration.AndroidCamera[] cameras = + AndroidCameraConfiguration.retrieveCameras(); + for (AndroidCameraConfiguration.AndroidCamera androidCamera : cameras) { + if (!androidCamera.frontFacing) camId = androidCamera.id; + } + String[] devices = LinphoneManager.getLc().getVideoDevicesList(); + String newDevice = devices[camId]; + LinphoneManager.getLc().setVideoDevice(newDevice); + } + + private void launchQrcodeReader() { + setBackCamera(); + + enableQrcodeReader(true); + } + + @Override + public void onResume() { + launchQrcodeReader(); + super.onResume(); + } + + @Override + public void onPause() { + enableQrcodeReader(false); + // setBackCamera(false); + super.onPause(); + } +} diff --git a/app/src/main/java/org/linphone/assistant/RemoteProvisioningActivity.java b/app/src/main/java/org/linphone/assistant/RemoteProvisioningActivity.java new file mode 100644 index 000000000..1660f1757 --- /dev/null +++ b/app/src/main/java/org/linphone/assistant/RemoteProvisioningActivity.java @@ -0,0 +1,217 @@ +package org.linphone.assistant; +/* +RemoteProvisioningActivity.java +Copyright (C) 2017 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.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import org.linphone.LinphoneLauncherActivity; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.core.ConfiguringState; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.ThemableActivity; + +public class RemoteProvisioningActivity extends ThemableActivity { + private final Handler mHandler = new Handler(); + private String mConfigUriParam = null; + private ProgressBar mSpinner; + private CoreListenerStub mListener; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.remote_provisioning); + mSpinner = findViewById(R.id.spinner); + + mListener = + new CoreListenerStub() { + @Override + public void onConfiguringStatus( + Core lc, final ConfiguringState state, String message) { + if (mSpinner != null) mSpinner.setVisibility(View.GONE); + if (state == ConfiguringState.Successful) { + goToLinphoneActivity(); + } else if (state == ConfiguringState.Failed) { + Toast.makeText( + RemoteProvisioningActivity.this, + R.string.remote_provisioning_failure, + Toast.LENGTH_LONG) + .show(); + } + } + }; + } + + @Override + protected void onResume() { + super.onResume(); + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + } + LinphonePreferences.instance().setContext(this); + + checkIntentForConfigUri(getIntent()); + } + + @Override + protected void onPause() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + } + super.onPause(); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + checkIntentForConfigUri(intent); + } + + private void checkIntentForConfigUri(final Intent intent) { + new Thread( + new Runnable() { + + @Override + public void run() { + Uri openUri = intent.getData(); + if (openUri != null) { + // We expect something like + // linphone-config://http://linphone.org/config.xml + mConfigUriParam = + openUri.getEncodedSchemeSpecificPart() + .substring(2); // Removes the linphone-config:// + try { + mConfigUriParam = + URLDecoder.decode(mConfigUriParam, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(e); + } + Log.d("Using config uri: " + mConfigUriParam); + } + + if (mConfigUriParam == null) { + if (!LinphonePreferences.instance() + .isFirstRemoteProvisioning()) { + mHandler.post( + new Runnable() { + @Override + public void run() { + goToLinphoneActivity(); + } + }); + } else if (!getResources() + .getBoolean( + R.bool.forbid_app_usage_until_remote_provisioning_completed)) { + // Show this view for a few seconds then go to the dialer + mHandler.postDelayed( + new Runnable() { + @Override + public void run() { + goToLinphoneActivity(); + } + }, + 1500); + } // else we do nothing if there is no config uri parameter and + // if user not allowed to leave this screen + } else { + if (getResources() + .getBoolean( + R.bool.display_confirmation_popup_after_first_configuration) + && !LinphonePreferences.instance() + .isFirstRemoteProvisioning()) { + mHandler.post( + new Runnable() { + @Override + public void run() { + displayDialogConfirmation(); + } + }); + } else { + mHandler.post( + new Runnable() { + @Override + public void run() { + setRemoteProvisioningAddressAndRestart( + mConfigUriParam); + } + }); + } + } + } + }) + .start(); + } + + private void displayDialogConfirmation() { + new AlertDialog.Builder(RemoteProvisioningActivity.this) + .setTitle(getString(R.string.remote_provisioning_again_title)) + .setMessage(getString(R.string.remote_provisioning_again_message)) + .setPositiveButton( + R.string.accept, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + setRemoteProvisioningAddressAndRestart(mConfigUriParam); + } + }) + .setNegativeButton( + R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + goToLinphoneActivity(); + } + }) + .show(); + } + + private void setRemoteProvisioningAddressAndRestart(final String configUri) { + if (mSpinner != null) mSpinner.setVisibility(View.VISIBLE); + + LinphonePreferences.instance().setContext(this); // Needed, else the next call will crash + LinphonePreferences.instance().setRemoteProvisioningUrl(configUri); + + LinphoneManager.getLc().getConfig().sync(); + LinphoneManager.getInstance().restartCore(); + } + + private void goToLinphoneActivity() { + if (LinphoneService.isReady()) { + LinphoneService.instance() + .setActivityToLaunchOnIncomingReceived("org.linphone.LinphoneLauncherActivity"); + // finish(); // To prevent the user to come back to this page using back button + startActivity(new Intent().setClass(this, LinphoneLauncherActivity.class)); + } else { + finish(); + } + } +} diff --git a/src/android/org/linphone/assistant/RemoteProvisioningFragment.java b/app/src/main/java/org/linphone/assistant/RemoteProvisioningFragment.java similarity index 64% rename from src/android/org/linphone/assistant/RemoteProvisioningFragment.java rename to app/src/main/java/org/linphone/assistant/RemoteProvisioningFragment.java index 9b1936e59..e710ac213 100644 --- a/src/android/org/linphone/assistant/RemoteProvisioningFragment.java +++ b/app/src/main/java/org/linphone/assistant/RemoteProvisioningFragment.java @@ -28,25 +28,30 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; - import org.linphone.LinphoneManager; -import org.linphone.LinphonePreferences; import org.linphone.R; +import org.linphone.settings.LinphonePreferences; public class RemoteProvisioningFragment extends Fragment implements OnClickListener, TextWatcher { - private EditText remoteProvisioningUrl; - private Button apply; + private EditText mRemoteProvisioningUrl; + private Button mApply, mQrcode; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.assistant_remote_provisioning, container, false); - remoteProvisioningUrl = view.findViewById(R.id.assistant_remote_provisioning_url); - remoteProvisioningUrl.addTextChangedListener(this); - apply = view.findViewById(R.id.assistant_apply); - apply.setEnabled(false); - apply.setOnClickListener(this); + mRemoteProvisioningUrl = view.findViewById(R.id.assistant_remote_provisioning_url); + mRemoteProvisioningUrl.addTextChangedListener(this); + mQrcode = view.findViewById(R.id.assistant_qrcode); + mQrcode.setOnClickListener(this); + mApply = view.findViewById(R.id.assistant_apply); + mApply.setEnabled(false); + mApply.setOnClickListener(this); + + if (getArguments() != null && !getArguments().getString("RemoteUrl").isEmpty()) { + mRemoteProvisioningUrl.setText(getArguments().getString("RemoteUrl")); + } return view; } @@ -56,27 +61,25 @@ public class RemoteProvisioningFragment extends Fragment implements OnClickListe int id = v.getId(); if (id == R.id.assistant_apply) { - String url = remoteProvisioningUrl.getText().toString(); + String url = mRemoteProvisioningUrl.getText().toString(); AssistantActivity.instance().displayRemoteProvisioningInProgressDialog(); LinphonePreferences.instance().setRemoteProvisioningUrl(url); LinphoneManager.getLc().getConfig().sync(); LinphoneManager.getInstance().restartCore(); AssistantActivity.instance().setCoreListener(); + } else if (id == R.id.assistant_qrcode) { + AssistantActivity.instance().displayQRCodeReader(); } } @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void onTextChanged(CharSequence s, int start, int before, int count) { - apply.setEnabled(!remoteProvisioningUrl.getText().toString().isEmpty()); + mApply.setEnabled(!mRemoteProvisioningUrl.getText().toString().isEmpty()); } @Override - public void afterTextChanged(Editable s) { - - } + public void afterTextChanged(Editable s) {} } diff --git a/src/android/org/linphone/assistant/RemoteProvisioningLoginActivity.java b/app/src/main/java/org/linphone/assistant/RemoteProvisioningLoginActivity.java similarity index 52% rename from src/android/org/linphone/assistant/RemoteProvisioningLoginActivity.java rename to app/src/main/java/org/linphone/assistant/RemoteProvisioningLoginActivity.java index c3384a391..0869985a3 100644 --- a/src/android/org/linphone/assistant/RemoteProvisioningLoginActivity.java +++ b/app/src/main/java/org/linphone/assistant/RemoteProvisioningLoginActivity.java @@ -25,19 +25,19 @@ import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; - import org.linphone.LinphoneManager; -import org.linphone.LinphonePreferences; import org.linphone.R; import org.linphone.core.ConfiguringState; import org.linphone.core.Core; import org.linphone.core.CoreListenerStub; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.ThemableActivity; import org.linphone.xmlrpc.XmlRpcHelper; import org.linphone.xmlrpc.XmlRpcListenerBase; -public class RemoteProvisioningLoginActivity extends Activity implements OnClickListener { - private EditText login, password, domain; - private Button connect; +public class RemoteProvisioningLoginActivity extends ThemableActivity implements OnClickListener { + private EditText mLogin, mPassword, mDomain; + private Button mConnect; private CoreListenerStub mListener; @Override @@ -45,53 +45,62 @@ public class RemoteProvisioningLoginActivity extends Activity implements OnClick super.onCreate(savedInstanceState); setContentView(R.layout.assistant_remote_provisioning_login); - login = findViewById(R.id.assistant_username); - password = findViewById(R.id.assistant_password); - domain = findViewById(R.id.assistant_domain); + mLogin = findViewById(R.id.assistant_username); + mPassword = findViewById(R.id.assistant_password); + mDomain = findViewById(R.id.assistant_domain); - connect = findViewById(R.id.assistant_connect); - connect.setOnClickListener(this); + mConnect = findViewById(R.id.assistant_connect); + mConnect.setOnClickListener(this); String defaultDomain = getIntent().getStringExtra("Domain"); if (defaultDomain != null) { - domain.setText(defaultDomain); - domain.setEnabled(false); + mDomain.setText(defaultDomain); + mDomain.setEnabled(false); } - mListener = new CoreListenerStub() { - @Override - public void onConfiguringStatus(Core lc, final ConfiguringState state, String message) { - if (state == ConfiguringState.Successful) { - //TODO - } else if (state == ConfiguringState.Failed) { - Toast.makeText(RemoteProvisioningLoginActivity.this, R.string.remote_provisioning_failure, Toast.LENGTH_LONG).show(); - } - } - }; + mListener = + new CoreListenerStub() { + @Override + public void onConfiguringStatus( + Core lc, final ConfiguringState state, String message) { + if (state == ConfiguringState.Successful) { + // TODO + } else if (state == ConfiguringState.Failed) { + Toast.makeText( + RemoteProvisioningLoginActivity.this, + R.string.remote_provisioning_failure, + Toast.LENGTH_LONG) + .show(); + } + } + }; } - private void cancelWizard(boolean bypassCheck) { - if (bypassCheck || getResources().getBoolean(R.bool.allow_cancel_remote_provisioning_login_activity)) { + private void cancelWizard() { + if (getResources().getBoolean(R.bool.allow_cancel_remote_provisioning_login_activity)) { LinphonePreferences.instance().disableProvisioningLoginView(); - setResult(bypassCheck ? Activity.RESULT_OK : Activity.RESULT_CANCELED); + setResult(Activity.RESULT_CANCELED); finish(); } } - private boolean storeAccount(String username, String password, String domain) { + private void storeAccount(String username, String password, String domain) { XmlRpcHelper xmlRpcHelper = new XmlRpcHelper(); - xmlRpcHelper.getRemoteProvisioningFilenameAsync(new XmlRpcListenerBase() { - @Override - public void onRemoteProvisioningFilenameSent(String result) { - LinphonePreferences.instance().setRemoteProvisioningUrl(result); - LinphoneManager.getInstance().restartCore(); - } - }, username.toString(), password.toString(), domain.toString()); + xmlRpcHelper.getRemoteProvisioningFilenameAsync( + new XmlRpcListenerBase() { + @Override + public void onRemoteProvisioningFilenameSent(String result) { + LinphonePreferences.instance().setRemoteProvisioningUrl(result); + LinphoneManager.getInstance().restartCore(); + } + }, + username, + password, + domain); LinphonePreferences.instance().firstLaunchSuccessful(); setResult(Activity.RESULT_OK); finish(); - return true; } @Override @@ -117,15 +126,18 @@ public class RemoteProvisioningLoginActivity extends Activity implements OnClick int id = v.getId(); if (id == R.id.cancel) { - cancelWizard(false); + cancelWizard(); } if (id == R.id.assistant_connect) { - storeAccount(login.getText().toString(), password.getText().toString(), domain.getText().toString()); + storeAccount( + mLogin.getText().toString(), + mPassword.getText().toString(), + mDomain.getText().toString()); } } @Override public void onBackPressed() { - cancelWizard(false); + cancelWizard(); } } diff --git a/src/android/org/linphone/assistant/WelcomeFragment.java b/app/src/main/java/org/linphone/assistant/WelcomeFragment.java similarity index 70% rename from src/android/org/linphone/assistant/WelcomeFragment.java rename to app/src/main/java/org/linphone/assistant/WelcomeFragment.java index 8440b409f..71f53ac2c 100644 --- a/src/android/org/linphone/assistant/WelcomeFragment.java +++ b/app/src/main/java/org/linphone/assistant/WelcomeFragment.java @@ -25,39 +25,38 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; - import org.linphone.R; public class WelcomeFragment extends Fragment implements OnClickListener { - private Button createAccount, logLinphoneAccount, logGenericAccount, remoteProvisioning; + private Button mCreateAccount, mLogLinphoneAccount, mLogGenericAccount, mRemoteProvisioning; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.assistant_welcome, container, false); - createAccount = view.findViewById(R.id.create_account); - createAccount.setOnClickListener(this); + mCreateAccount = view.findViewById(R.id.create_account); + mCreateAccount.setOnClickListener(this); - logLinphoneAccount = view.findViewById(R.id.login_linphone); + mLogLinphoneAccount = view.findViewById(R.id.login_linphone); if (getResources().getBoolean(R.bool.hide_linphone_accounts_in_assistant)) { - logLinphoneAccount.setVisibility(View.GONE); + mLogLinphoneAccount.setVisibility(View.GONE); } else { - logLinphoneAccount.setOnClickListener(this); + mLogLinphoneAccount.setOnClickListener(this); } - logGenericAccount = view.findViewById(R.id.login_generic); + mLogGenericAccount = view.findViewById(R.id.login_generic); if (getResources().getBoolean(R.bool.hide_generic_accounts_in_assistant)) { - logGenericAccount.setVisibility(View.GONE); + mLogGenericAccount.setVisibility(View.GONE); } else { - logGenericAccount.setOnClickListener(this); + mLogGenericAccount.setOnClickListener(this); } - remoteProvisioning = view.findViewById(R.id.remote_provisioning); + mRemoteProvisioning = view.findViewById(R.id.remote_provisioning); if (getResources().getBoolean(R.bool.hide_remote_provisioning_in_assistant)) { - remoteProvisioning.setVisibility(View.GONE); + mRemoteProvisioning.setVisibility(View.GONE); } else { - remoteProvisioning.setOnClickListener(this); + mRemoteProvisioning.setOnClickListener(this); } return view; @@ -73,7 +72,7 @@ public class WelcomeFragment extends Fragment implements OnClickListener { } else if (id == R.id.create_account) { AssistantActivity.instance().displayCreateAccount(); } else if (id == R.id.remote_provisioning) { - AssistantActivity.instance().displayRemoteProvisioning(); + AssistantActivity.instance().displayRemoteProvisioning(""); } } } diff --git a/src/android/org/linphone/BandwidthManager.java b/app/src/main/java/org/linphone/call/BandwidthManager.java similarity index 73% rename from src/android/org/linphone/BandwidthManager.java rename to app/src/main/java/org/linphone/call/BandwidthManager.java index 301fe0b67..a6f63105b 100644 --- a/src/android/org/linphone/BandwidthManager.java +++ b/app/src/main/java/org/linphone/call/BandwidthManager.java @@ -1,4 +1,4 @@ -package org.linphone; +package org.linphone.call; /* BandwithManager.java @@ -20,23 +20,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import org.linphone.core.CallParams; -import org.linphone.core.Core; 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 final int HIGH_RESOLUTION = 0; + private static final int LOW_RESOLUTION = 1; + private static final int LOW_BANDWIDTH = 2; - private static BandwidthManager instance; - - private int currentProfile = HIGH_RESOLUTION; - - public static final synchronized BandwidthManager getInstance() { - if (instance == null) instance = new BandwidthManager(); - return instance; - } + private static BandwidthManager sInstance; + private final int currentProfile = HIGH_RESOLUTION; private BandwidthManager() { // FIXME register a listener on NetworkManager to get notified of network state @@ -45,8 +38,12 @@ public class BandwidthManager { // FIXME initially get those values } + public static synchronized BandwidthManager getInstance() { + if (sInstance == null) sInstance = new BandwidthManager(); + return sInstance; + } - public void updateWithProfileSettings(Core lc, CallParams callParams) { + public void updateWithProfileSettings(CallParams callParams) { if (callParams != null) { // in call // Update video parm if if (!isVideoPossible()) { // NO VIDEO @@ -59,7 +56,7 @@ public class BandwidthManager { } } - public boolean isVideoPossible() { + private boolean isVideoPossible() { return currentProfile != LOW_BANDWIDTH; } } diff --git a/app/src/main/java/org/linphone/call/CallActivity.java b/app/src/main/java/org/linphone/call/CallActivity.java new file mode 100644 index 000000000..da23aeaff --- /dev/null +++ b/app/src/main/java/org/linphone/call/CallActivity.java @@ -0,0 +1,2019 @@ +package org.linphone.call; + +/* +CallActivity.java +Copyright (C) 2017 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.Manifest; +import android.app.Dialog; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.CountDownTimer; +import android.os.Handler; +import android.os.SystemClock; +import android.text.Html; +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 androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import java.text.DecimalFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.compatibility.Compatibility; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.AddressFamily; +import org.linphone.core.Call; +import org.linphone.core.Call.State; +import org.linphone.core.CallListenerStub; +import org.linphone.core.CallParams; +import org.linphone.core.CallStats; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatRoom; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.MediaEncryption; +import org.linphone.core.PayloadType; +import org.linphone.core.Player; +import org.linphone.core.StreamType; +import org.linphone.core.tools.Log; +import org.linphone.fragments.StatusFragment; +import org.linphone.mediastream.video.capture.hwconf.AndroidCameraConfiguration; +import org.linphone.receivers.BluetoothManager; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneGenericActivity; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; +import org.linphone.views.Numpad; + +public class CallActivity extends LinphoneGenericActivity + implements OnClickListener, ActivityCompat.OnRequestPermissionsResultCallback { + private static final int SECONDS_BEFORE_HIDING_CONTROLS = 4000; + private static final 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 final int PERMISSIONS_EXTERNAL_STORAGE = 205; + + private static CallActivity sInstance; + private static long sTimeRemind = 0; + private Handler mControlsHandler = new Handler(); + private Runnable mControls; + private ImageView mSwitchCamera; + private TextView mMissedChats; + private RelativeLayout mActiveCallHeader, mSideMenuContent, mAvatarLayout; + private ImageView mPause, + mHangUp, + mDialer, + mVideo, + mMicro, + mSpeaker, + mOptions, + mAddCall, + mTransfer, + mConference, + mConferenceStatus, + mRecordCall, + mRecording; + private ImageView mAudioRoute, mRouteSpeaker, mRouteEarpiece, mRouteBluetooth, mMenu, mChat; + private LinearLayout mNoCurrentCall, mCallInfo, mCallPaused; + private ProgressBar mVideoProgress; + private StatusFragment mStatus; + private CallAudioFragment mAudioCallFragment; + private CallVideoFragment mVideoCallFragment; + private boolean mIsSpeakerEnabled = false, + mIsMicMuted = false, + mIsTransferAllowed, + mIsVideoAsk, + mIsRecording = false; + private LinearLayout mControlsLayout; + private Numpad mNumpad; + private int mCameraNumber; + private CountDownTimer mCountDownTimer; + private boolean mIsVideoCallPaused = false; + private Dialog mDialog = null; + private HeadsetReceiver mHeadsetReceiver; + + private LinearLayout mCallsList, mConferenceList; + private LayoutInflater mInflater; + private ViewGroup mContainer; + private boolean mIsConferenceRunning = false; + private CoreListenerStub mListener; + private DrawerLayout mSideMenu; + + private final Handler mHandler = new Handler(); + private Timer mTimer; + private TimerTask mTask; + private HashMap mEncoderTexts; + private HashMap mDecoderTexts; + private CallListenerStub mCallListener; + private Call mCallDisplayedInStats; + + private boolean mOldIsSpeakerEnabled = false; + + public static CallActivity instance() { + return sInstance; + } + + public static boolean isInstanciated() { + return sInstance != null; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sInstance = this; + + if (getResources().getBoolean(R.bool.orientation_portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + Compatibility.setShowWhenLocked(this, true); + + setContentView(R.layout.call); + + // Earset Connectivity Broadcast Processing + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction("android.intent.action.HEADSET_PLUG"); + mHeadsetReceiver = new HeadsetReceiver(); + registerReceiver(mHeadsetReceiver, intentFilter); + + mIsTransferAllowed = + getApplicationContext().getResources().getBoolean(R.bool.allow_transfers); + + mCameraNumber = AndroidCameraConfiguration.retrieveCameras().length; + + mEncoderTexts = new HashMap<>(); + mDecoderTexts = new HashMap<>(); + + mListener = + new CoreListenerStub() { + @Override + public void onMessageReceived(Core lc, ChatRoom cr, ChatMessage message) { + displayMissedChats(); + } + + @Override + public void onCallStateChanged( + Core lc, final Call call, Call.State state, String message) { + if (LinphoneManager.getLc().getCallsNb() == 0) { + finish(); + return; + } + + if (state == State.IncomingReceived || state == State.IncomingEarlyMedia) { + // This scenario will be handled by the Service listener + return; + } else if (state == State.Paused + || state == State.PausedByRemote + || state == State.Pausing) { + if (LinphoneManager.getLc().getCurrentCall() != null) { + mVideo.setEnabled(false); + } + if (isVideoEnabled(call)) { + showAudioView(); + } + } else if (state == State.Resuming) { + if (LinphonePreferences.instance().isVideoEnabled()) { + mStatus.refreshStatusItems(call); + if (call.getCurrentParams().videoEnabled()) { + showVideoView(); + } + } + if (LinphoneManager.getLc().getCurrentCall() != null) { + mVideo.setEnabled(true); + } + } else if (state == State.StreamsRunning) { + switchVideo(isVideoEnabled(call)); + enableAndRefreshInCallActions(); + + if (mStatus != null) { + mVideoProgress.setVisibility(View.GONE); + mStatus.refreshStatusItems(call); + } + } else if (state == State.UpdatedByRemote) { + // If the correspondent proposes mVideo while audio call + boolean videoEnabled = LinphonePreferences.instance().isVideoEnabled(); + if (!videoEnabled) { + acceptCallUpdate(false); + } + + boolean remoteVideo = call.getRemoteParams().videoEnabled(); + boolean localVideo = call.getCurrentParams().videoEnabled(); + boolean autoAcceptCameraPolicy = + LinphonePreferences.instance() + .shouldAutomaticallyAcceptVideoRequests(); + if (remoteVideo + && !localVideo + && !autoAcceptCameraPolicy + && !LinphoneManager.getLc().isInConference()) { + showAcceptCallUpdateDialog(); + createTimerForDialog(SECONDS_BEFORE_DENYING_CALL_UPDATE); + } + } + + refreshIncallUi(); + mTransfer.setEnabled(LinphoneManager.getLc().getCurrentCall() != null); + } + + @Override + public void onCallEncryptionChanged( + Core lc, + final Call call, + boolean encrypted, + String authenticationToken) { + if (mStatus != null) { + if (call.getCurrentParams() + .getMediaEncryption() + .equals(MediaEncryption.ZRTP) + && !call.getAuthenticationTokenVerified()) { + mStatus.showZRTPDialog(call); + } + mStatus.refreshStatusItems(call); + } + } + }; + + if (findViewById(R.id.fragmentContainer) != null) { + initUI(); + + if (LinphoneManager.getLc().getCallsNb() > 0) { + Call 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) + mIsSpeakerEnabled = savedInstanceState.getBoolean("Speaker"); + mIsMicMuted = savedInstanceState.getBoolean("Mic"); + mIsVideoCallPaused = savedInstanceState.getBoolean("VideoCallPaused"); + if (savedInstanceState.getBoolean("AskingVideo")) { + showAcceptCallUpdateDialog(); + sTimeRemind = savedInstanceState.getLong("sTimeRemind"); + createTimerForDialog(sTimeRemind); + } + refreshInCallActions(); + return; + } else { + mIsSpeakerEnabled = LinphoneManager.getInstance().isSpeakerEnabled(); + mIsMicMuted = !LinphoneManager.getLc().micEnabled(); + } + + Fragment callFragment; + if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) { + callFragment = new CallVideoFragment(); + mVideoCallFragment = (CallVideoFragment) callFragment; + displayVideoCall(false); + LinphoneManager.getInstance().routeAudioToSpeaker(); + mIsSpeakerEnabled = true; + } else { + callFragment = new CallAudioFragment(); + mAudioCallFragment = (CallAudioFragment) callFragment; + } + + if (BluetoothManager.getInstance().isBluetoothHeadsetAvailable()) { + BluetoothManager.getInstance().routeAudioToBluetooth(); + } + + callFragment.setArguments(getIntent().getExtras()); + getFragmentManager() + .beginTransaction() + .add(R.id.fragmentContainer, callFragment) + .commitAllowingStateLoss(); + } + } + + private void createTimerForDialog(long time) { + mCountDownTimer = + new CountDownTimer(time, 1000) { + public void onTick(long millisUntilFinished) { + sTimeRemind = millisUntilFinished; + } + + public void onFinish() { + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + } + acceptCallUpdate(false); + } + }.start(); + } + + private boolean isVideoEnabled(Call call) { + if (call != null) { + return call.getCurrentParams().videoEnabled(); + } + return false; + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + outState.putBoolean("Speaker", LinphoneManager.getInstance().isSpeakerEnabled()); + outState.putBoolean("Mic", !LinphoneManager.getLc().micEnabled()); + outState.putBoolean("VideoCallPaused", mIsVideoCallPaused); + outState.putBoolean("AskingVideo", mIsVideoAsk); + outState.putLong("sTimeRemind", sTimeRemind); + if (mDialog != null) mDialog.dismiss(); + super.onSaveInstanceState(outState); + } + + private boolean isTablet() { + return getResources().getBoolean(R.bool.isTablet); + } + + private void initUI() { + mInflater = LayoutInflater.from(this); + mContainer = findViewById(R.id.topLayout); + mCallsList = findViewById(R.id.calls_list); + mConferenceList = findViewById(R.id.conference_list); + + // TopBar + mVideo = findViewById(R.id.video); + mVideo.setOnClickListener(this); + mVideo.setEnabled(false); + + mVideoProgress = findViewById(R.id.video_in_progress); + mVideoProgress.setVisibility(View.GONE); + + mMicro = findViewById(R.id.micro); + mMicro.setOnClickListener(this); + + mSpeaker = findViewById(R.id.speaker); + mSpeaker.setOnClickListener(this); + + mOptions = findViewById(R.id.options); + mOptions.setOnClickListener(this); + mOptions.setEnabled(false); + + // BottonBar + mHangUp = findViewById(R.id.hang_up); + mHangUp.setOnClickListener(this); + + mDialer = findViewById(R.id.dialer); + mDialer.setOnClickListener(this); + + mNumpad = findViewById(R.id.numpad); + mNumpad.getBackground().setAlpha(240); + + mChat = findViewById(R.id.chat); + mChat.setOnClickListener(this); + mMissedChats = findViewById(R.id.missed_chats); + + // Others + + // Active Call + mCallInfo = findViewById(R.id.active_call_info); + + mPause = findViewById(R.id.pause); + mPause.setOnClickListener(this); + mPause.setEnabled(false); + + mActiveCallHeader = findViewById(R.id.active_call); + mNoCurrentCall = findViewById(R.id.no_current_call); + mCallPaused = findViewById(R.id.remote_pause); + + mAvatarLayout = findViewById(R.id.avatar_layout); + + // Options + mAddCall = findViewById(R.id.add_call); + mAddCall.setOnClickListener(this); + mAddCall.setEnabled(false); + + mTransfer = findViewById(R.id.transfer); + mTransfer.setOnClickListener(this); + mTransfer.setEnabled(false); + + mConference = findViewById(R.id.conference); + mConference.setEnabled(false); + mConference.setOnClickListener(this); + + mRecordCall = findViewById(R.id.record_call); + mRecordCall.setOnClickListener(this); + mRecordCall.setEnabled(false); + + mRecording = findViewById(R.id.recording); + mRecording.setOnClickListener(this); + mRecording.setEnabled(false); + mRecording.setVisibility(View.GONE); + + try { + mAudioRoute = findViewById(R.id.audio_route); + mAudioRoute.setOnClickListener(this); + mRouteSpeaker = findViewById(R.id.route_speaker); + mRouteSpeaker.setOnClickListener(this); + mRouteEarpiece = findViewById(R.id.route_earpiece); + mRouteEarpiece.setOnClickListener(this); + mRouteBluetooth = findViewById(R.id.route_bluetooth); + mRouteBluetooth.setOnClickListener(this); + } catch (NullPointerException npe) { + Log.e("Bluetooth: Audio routes mMenu disabled on tablets for now (1)"); + } + + mSwitchCamera = findViewById(R.id.switchCamera); + mSwitchCamera.setOnClickListener(this); + + mControlsLayout = findViewById(R.id.menu); + + if (!mIsTransferAllowed) { + mAddCall.setBackgroundResource(R.drawable.options_add_call); + } + + if (BluetoothManager.getInstance().isBluetoothHeadsetAvailable()) { + try { + mAudioRoute.setVisibility(View.VISIBLE); + mSpeaker.setVisibility(View.GONE); + } catch (NullPointerException npe) { + Log.e("Bluetooth: Audio routes mMenu disabled on tablets for now (2)"); + } + } else { + try { + mAudioRoute.setVisibility(View.GONE); + mSpeaker.setVisibility(View.VISIBLE); + } catch (NullPointerException npe) { + Log.e("Bluetooth: Audio routes mMenu disabled on tablets for now (3)"); + } + } + + createInCallStats(); + LinphoneManager.getInstance().changeStatusToOnThePhone(); + } + + private 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) { + 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: + LinphoneUtils.dispatchOnUIThread( + new Runnable() { + @Override + public void run() { + acceptCallUpdate( + grantResults[0] == PackageManager.PERMISSION_GRANTED); + } + }); + break; + case PERMISSIONS_ENABLED_CAMERA: + LinphoneUtils.dispatchOnUIThread( + new Runnable() { + @Override + public void run() { + disableVideo(grantResults[0] != PackageManager.PERMISSION_GRANTED); + } + }); + break; + case PERMISSIONS_ENABLED_MIC: + LinphoneUtils.dispatchOnUIThread( + new Runnable() { + @Override + public void run() { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + toggleMicro(); + } + } + }); + break; + case PERMISSIONS_EXTERNAL_STORAGE: + LinphoneUtils.dispatchOnUIThread( + new Runnable() { + @Override + public void run() { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + toggleCallRecording(!mIsRecording); + } + } + }); + } + } + + private void createInCallStats() { + mSideMenu = findViewById(R.id.side_menu); + mMenu = findViewById(R.id.call_quality); + + mSideMenuContent = findViewById(R.id.side_menu_content); + + mMenu.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + if (mSideMenu.isDrawerVisible(Gravity.LEFT)) { + mSideMenu.closeDrawer(mSideMenuContent); + } else { + mSideMenu.openDrawer(mSideMenuContent); + } + } + }); + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + initCallStatsRefresher(lc.getCurrentCall(), findViewById(R.id.incall_stats)); + } + } + + private void refreshIncallUi() { + refreshInCallActions(); + refreshCallList(); + enableAndRefreshInCallActions(); + displayMissedChats(); + } + + public void setSpeakerEnabled(boolean enabled) { + mIsSpeakerEnabled = enabled; + } + + public void refreshInCallActions() { + if (!LinphonePreferences.instance().isVideoEnabled() || mIsConferenceRunning) { + mVideo.setEnabled(false); + } else { + if (mVideo.isEnabled()) { + if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) { + mVideo.setSelected(true); + mVideoProgress.setVisibility(View.INVISIBLE); + } else { + mVideo.setSelected(false); + } + } else { + mVideo.setSelected(false); + } + } + if (getPackageManager().checkPermission(Manifest.permission.CAMERA, getPackageName()) + != PackageManager.PERMISSION_GRANTED) { + mVideo.setSelected(false); + } + + mSpeaker.setSelected(mIsSpeakerEnabled); + + if (getPackageManager().checkPermission(Manifest.permission.RECORD_AUDIO, getPackageName()) + != PackageManager.PERMISSION_GRANTED) { + mIsMicMuted = true; + } + mMicro.setSelected(mIsMicMuted); + + try { + mRouteSpeaker.setSelected(false); + if (BluetoothManager.getInstance().isUsingBluetoothAudioRoute()) { + mIsSpeakerEnabled = false; // We need this if mIsSpeakerEnabled wasn't set correctly + mRouteEarpiece.setSelected(false); + mRouteBluetooth.setSelected(true); + return; + } else { + mRouteEarpiece.setSelected(true); + mRouteBluetooth.setSelected(false); + } + + if (mIsSpeakerEnabled) { + mRouteSpeaker.setSelected(true); + mRouteEarpiece.setSelected(false); + mRouteBluetooth.setSelected(false); + } + } catch (NullPointerException npe) { + Log.e("Bluetooth: Audio routes mMenu disabled on tablets for now (4)"); + } + } + + private void enableAndRefreshInCallActions() { + int confsize = 0; + + if (LinphoneManager.getLc().isInConference()) { + confsize = + LinphoneManager.getLc().getConferenceSize() + - (LinphoneManager.getLc().getConference() != null ? 1 : 0); + } + + // Enabled mTransfer button + mTransfer.setEnabled(mIsTransferAllowed && !LinphoneManager.getLc().soundResourcesLocked()); + + // Enable mConference button + mConference.setEnabled( + LinphoneManager.getLc().getCallsNb() > 1 + && LinphoneManager.getLc().getCallsNb() > confsize + && !LinphoneManager.getLc().soundResourcesLocked()); + + mAddCall.setEnabled( + LinphoneManager.getLc().getCallsNb() < LinphoneManager.getLc().getMaxCalls() + && !LinphoneManager.getLc().soundResourcesLocked()); + mOptions.setEnabled( + !getResources().getBoolean(R.bool.disable_options_in_call) + && (mAddCall.isEnabled() || mTransfer.isEnabled())); + + Call currentCall = LinphoneManager.getLc().getCurrentCall(); + + mRecordCall.setEnabled( + !LinphoneManager.getLc().soundResourcesLocked() + && currentCall != null + && currentCall.getCurrentParams().getRecordFile() != null); + mRecordCall.setSelected(mIsRecording); + + mRecording.setEnabled(mIsRecording); + mRecording.setVisibility(mIsRecording ? View.VISIBLE : View.GONE); + + mVideo.setEnabled( + currentCall != null + && LinphonePreferences.instance().isVideoEnabled() + && !currentCall.mediaInProgress()); + + mPause.setEnabled(currentCall != null && !currentCall.mediaInProgress()); + + mMicro.setEnabled(true); + mSpeaker.setEnabled(!isTablet()); + mTransfer.setEnabled(true); + mPause.setEnabled(true); + mDialer.setEnabled(true); + } + + public void updateStatusFragment(StatusFragment statusFragment) { + mStatus = statusFragment; + } + + @Override + public void onClick(View v) { + int id = v.getId(); + + 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.record_call) { + int externalStorage = + getPackageManager() + .checkPermission( + Manifest.permission.WRITE_EXTERNAL_STORAGE, getPackageName()); + Log.i( + "[Permission] External storage permission is " + + (externalStorage == PackageManager.PERMISSION_GRANTED + ? "granted" + : "denied")); + + if (externalStorage == PackageManager.PERMISSION_GRANTED) { + toggleCallRecording(!mIsRecording); + } else { + checkAndRequestPermission( + Manifest.permission.WRITE_EXTERNAL_STORAGE, PERMISSIONS_EXTERNAL_STORAGE); + } + } else if (id == R.id.recording) { + toggleCallRecording(false); + } 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 (mVideoCallFragment != null) { + mVideoCallFragment.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()) { + mIsSpeakerEnabled = false; + mRouteBluetooth.setSelected(true); + mRouteSpeaker.setSelected(false); + mRouteEarpiece.setSelected(false); + } + hideOrDisplayAudioRoutes(); + } else if (id == R.id.route_earpiece) { + LinphoneManager.getInstance().routeAudioToReceiver(); + mIsSpeakerEnabled = false; + mRouteBluetooth.setSelected(false); + mRouteSpeaker.setSelected(false); + mRouteEarpiece.setSelected(true); + hideOrDisplayAudioRoutes(); + } else if (id == R.id.route_speaker) { + LinphoneManager.getInstance().routeAudioToSpeaker(); + mIsSpeakerEnabled = true; + mRouteBluetooth.setSelected(false); + mRouteSpeaker.setSelected(true); + mRouteEarpiece.setSelected(false); + hideOrDisplayAudioRoutes(); + } else if (id == R.id.call_pause) { + Call call = (Call) v.getTag(); + pauseOrResumeCall(call); + } else if (id == R.id.conference_pause) { + pauseOrResumeConference(); + } + } + + private void toggleCallRecording(boolean enable) { + Call call = LinphoneManager.getLc().getCurrentCall(); + + if (call == null) return; + + if (enable && !mIsRecording) { + call.startRecording(); + Log.d("start call mRecording"); + mRecordCall.setSelected(true); + + mRecording.setVisibility(View.VISIBLE); + mRecording.setEnabled(true); + + mIsRecording = true; + } else if (!enable && mIsRecording) { + call.stopRecording(); + Log.d("stop call mRecording"); + mRecordCall.setSelected(false); + + mRecording.setVisibility(View.GONE); + mRecording.setEnabled(false); + + mIsRecording = false; + } + } + + private void disableVideo(final boolean videoDisabled) { + final Call call = LinphoneManager.getLc().getCurrentCall(); + if (call == null) { + return; + } + + if (videoDisabled) { + CallParams params = LinphoneManager.getLc().createCallParams(call); + params.enableVideo(false); + LinphoneManager.getLc().updateCall(call, params); + } else { + mVideoProgress.setVisibility(View.VISIBLE); + if (call.getRemoteParams() != null && !call.getRemoteParams().lowBandwidthEnabled()) { + LinphoneManager.getInstance().addVideo(); + } else { + displayCustomToast(getString(R.string.error_low_bandwidth), Toast.LENGTH_LONG); + } + } + } + + private 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 = 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 Call call = LinphoneManager.getLc().getCurrentCall(); + if (call == null) { + return; + } + + // Check if the call is not terminated + if (call.getState() == State.End || call.getState() == State.Released) return; + + if (!displayVideo) { + showAudioView(); + } else { + if (!call.getRemoteParams().lowBandwidthEnabled()) { + LinphoneManager.getInstance().addVideo(); + if (mVideoCallFragment == null || !mVideoCallFragment.isVisible()) showVideoView(); + } else { + displayCustomToast(getString(R.string.error_low_bandwidth), Toast.LENGTH_LONG); + } + } + } + + private void showAudioView() { + if (LinphoneManager.getLc().getCurrentCall() != null) { + if (!mIsSpeakerEnabled) { + LinphoneManager.getInstance().enableProximitySensing(true); + } + } + replaceFragmentVideoByAudio(); + displayAudioCall(); + showStatusBar(); + removeCallbacks(); + } + + private void showVideoView() { + if (!BluetoothManager.getInstance().isBluetoothHeadsetAvailable()) { + Log.w("Bluetooth not available, using mSpeaker"); + LinphoneManager.getInstance().routeAudioToSpeaker(); + mIsSpeakerEnabled = true; + } + refreshInCallActions(); + + LinphoneManager.getInstance().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); + mCallInfo.setVisibility(View.VISIBLE); + mAvatarLayout.setVisibility(View.VISIBLE); + mSwitchCamera.setVisibility(View.GONE); + } + + private void replaceFragmentVideoByAudio() { + mAudioCallFragment = new CallAudioFragment(); + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.fragmentContainer, mAudioCallFragment); + try { + transaction.commitAllowingStateLoss(); + } catch (Exception e) { + Log.e(e); + } + } + + private void replaceFragmentAudioByVideo() { + // Hiding controls to let displayVideoCallControlsIfHidden add them plus the callback + mVideoCallFragment = new CallVideoFragment(); + + FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.replace(R.id.fragmentContainer, mVideoCallFragment); + try { + transaction.commitAllowingStateLoss(); + } catch (Exception e) { + Log.e(e); + } + } + + private void toggleMicro() { + Core lc = LinphoneManager.getLc(); + mIsMicMuted = !mIsMicMuted; + lc.enableMic(!mIsMicMuted); + mMicro.setSelected(mIsMicMuted); + } + + private void toggleSpeaker() { + mIsSpeakerEnabled = !mIsSpeakerEnabled; + if (LinphoneManager.getLc().getCurrentCall() != null) { + if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) + LinphoneManager.getInstance().enableProximitySensing(false); + else LinphoneManager.getInstance().enableProximitySensing(!mIsSpeakerEnabled); + } + mSpeaker.setSelected(mIsSpeakerEnabled); + if (mIsSpeakerEnabled) { + LinphoneManager.getInstance().routeAudioToSpeaker(); + LinphoneManager.getInstance().enableSpeaker(mIsSpeakerEnabled); + } else { + Log.d("Toggle mSpeaker off, routing back to earpiece"); + LinphoneManager.getInstance().routeAudioToReceiver(); + } + } + + private void pauseOrResumeCall(Call call) { + Core lc = LinphoneManager.getLc(); + if (call != null && LinphoneManager.getLc().getCurrentCall() == call) { + lc.pauseCall(call); + if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) { + mIsVideoCallPaused = true; + } + mPause.setSelected(true); + } else if (call != null) { + if (call.getState() == State.Paused) { + lc.resumeCall(call); + if (mIsVideoCallPaused) { + mIsVideoCallPaused = false; + } + mPause.setSelected(false); + } + } + } + + private void hangUp() { + Core lc = LinphoneManager.getLc(); + Call currentCall = lc.getCurrentCall(); + + if (mIsRecording) { + toggleCallRecording(false); + } + + if (currentCall != null) { + lc.terminateCall(currentCall); + } else if (lc.isInConference()) { + lc.terminateConference(); + } else { + lc.terminateAllCalls(); + } + } + + private void displayVideoCall(boolean display) { + if (display) { + showStatusBar(); + mControlsLayout.setVisibility(View.VISIBLE); + mActiveCallHeader.setVisibility(View.VISIBLE); + mCallInfo.setVisibility(View.VISIBLE); + mAvatarLayout.setVisibility(View.GONE); + mCallsList.setVisibility(View.VISIBLE); + if (mCameraNumber > 1) { + mSwitchCamera.setVisibility(View.VISIBLE); + } + } else { + hideStatusBar(); + mControlsLayout.setVisibility(View.GONE); + mActiveCallHeader.setVisibility(View.GONE); + mSwitchCamera.setVisibility(View.GONE); + mCallsList.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(); + mVideo.setEnabled(true); + mTransfer.setVisibility(View.INVISIBLE); + mAddCall.setVisibility(View.INVISIBLE); + mConference.setVisibility(View.INVISIBLE); + mRecordCall.setVisibility(View.INVISIBLE); + displayVideoCall(false); + mNumpad.setVisibility(View.GONE); + mOptions.setSelected(false); + } + }, + SECONDS_BEFORE_HIDING_CONTROLS); + } + } + + public void removeCallbacks() { + if (mControlsHandler != null && mControls != null) { + mControlsHandler.removeCallbacks(mControls); + } + mControls = null; + } + + private void hideNumpad() { + if (mNumpad == null || mNumpad.getVisibility() != View.VISIBLE) { + return; + } + + mDialer.setImageResource(R.drawable.footer_dialer); + mNumpad.setVisibility(View.GONE); + } + + private void hideOrDisplayNumpad() { + if (mNumpad == null) { + return; + } + + if (mNumpad.getVisibility() == View.VISIBLE) { + hideNumpad(); + } else { + mDialer.setImageResource(R.drawable.dialer_alt_back); + mNumpad.setVisibility(View.VISIBLE); + } + } + + private void hideOrDisplayAudioRoutes() { + if (mRouteSpeaker.getVisibility() == View.VISIBLE) { + mRouteSpeaker.setVisibility(View.INVISIBLE); + mRouteBluetooth.setVisibility(View.INVISIBLE); + mRouteEarpiece.setVisibility(View.INVISIBLE); + mAudioRoute.setSelected(false); + } else { + mRouteSpeaker.setVisibility(View.VISIBLE); + mRouteBluetooth.setVisibility(View.VISIBLE); + mRouteEarpiece.setVisibility(View.VISIBLE); + mAudioRoute.setSelected(true); + } + } + + private void hideOrDisplayCallOptions() { + // Hide mOptions + if (mAddCall.getVisibility() == View.VISIBLE) { + mOptions.setSelected(false); + if (mIsTransferAllowed) { + mTransfer.setVisibility(View.INVISIBLE); + } + mAddCall.setVisibility(View.INVISIBLE); + mConference.setVisibility(View.INVISIBLE); + mRecordCall.setVisibility(View.INVISIBLE); + } else { // Display mOptions + if (mIsTransferAllowed) { + mTransfer.setVisibility(View.VISIBLE); + } + mAddCall.setVisibility(View.VISIBLE); + mConference.setVisibility(View.VISIBLE); + mRecordCall.setVisibility(View.VISIBLE); + mOptions.setSelected(true); + mTransfer.setEnabled(LinphoneManager.getLc().getCurrentCall() != null); + } + } + + private void goBackToDialer() { + Intent intent = new Intent(); + intent.setClass(this, LinphoneActivity.class); + intent.putExtra("AddCall", true); + startActivity(intent); + } + + private void goBackToDialerAndDisplayTransferButton() { + Intent intent = new Intent(); + intent.setClass(this, LinphoneActivity.class); + intent.putExtra("Transfer", true); + startActivity(intent); + } + + private void goToChatList() { + Intent intent = new Intent(); + intent.setClass(this, LinphoneActivity.class); + intent.putExtra("GoToChat", true); + startActivity(intent); + } + + private void acceptCallUpdate(boolean accept) { + if (mCountDownTimer != null) { + mCountDownTimer.cancel(); + } + + Call call = LinphoneManager.getLc().getCurrentCall(); + if (call == null) { + return; + } + + CallParams params = LinphoneManager.getLc().createCallParams(call); + if (accept) { + params.enableVideo(true); + LinphoneManager.getLc().enableVideoCapture(true); + LinphoneManager.getLc().enableVideoDisplay(true); + } + + LinphoneManager.getLc().acceptCallUpdate(call, params); + } + + private void hideStatusBar() { + if (isTablet()) { + return; + } + + findViewById(R.id.status).setVisibility(View.GONE); + findViewById(R.id.fragmentContainer).setPadding(0, 0, 0, 0); + } + + private void showStatusBar() { + if (isTablet()) { + return; + } + + if (mStatus != null && !mStatus.isVisible()) { + // Hack to ensure statusFragment is visible after coming back to + // mDialer from mChat + mStatus.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() { + mDialog = new Dialog(this); + mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + mDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.dark_grey_color)); + d.setAlpha(200); + mDialog.setContentView(R.layout.dialog); + mDialog.getWindow() + .setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT); + mDialog.getWindow().setBackgroundDrawable(d); + + TextView customText = mDialog.findViewById(R.id.dialog_message); + customText.setText(getResources().getString(R.string.add_video_dialog)); + mDialog.findViewById(R.id.dialog_delete_button).setVisibility(View.GONE); + Button accept = mDialog.findViewById(R.id.dialog_ok_button); + accept.setVisibility(View.VISIBLE); + accept.setText(R.string.accept); + Button cancel = mDialog.findViewById(R.id.dialog_cancel_button); + cancel.setText(R.string.decline); + mIsVideoAsk = true; + + accept.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); + } + mIsVideoAsk = false; + mDialog.dismiss(); + mDialog = null; + } + }); + + cancel.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + if (CallActivity.isInstanciated()) { + CallActivity.instance().acceptCallUpdate(false); + } + mIsVideoAsk = false; + mDialog.dismiss(); + mDialog = null; + } + }); + mDialog.show(); + } + + @Override + protected void onResume() { + + sInstance = this; + super.onResume(); + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + } + mIsSpeakerEnabled = LinphoneManager.getInstance().isSpeakerEnabled(); + + refreshIncallUi(); + handleViewIntent(); + + if (mStatus != null && lc != null) { + Call currentCall = lc.getCurrentCall(); + if (currentCall != null && !currentCall.getAuthenticationTokenVerified()) { + mStatus.showZRTPDialog(currentCall); + } + } + + if (!isVideoEnabled(LinphoneManager.getLc().getCurrentCall())) { + if (!mIsSpeakerEnabled) { + LinphoneManager.getInstance().enableProximitySensing(true); + removeCallbacks(); + } + } + } + + private void handleViewIntent() { + Intent intent = getIntent(); + if (intent != null && "android.intent.action.VIEW".equals(intent.getAction())) { + Call call = LinphoneManager.getLc().getCurrentCall(); + if (call != null && isVideoEnabled(call)) { + Player player = call.getPlayer(); + String path = intent.getData().getPath(); + Log.i("Openning " + path); + /*int openRes = */ player.open(path); + /*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() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + } + + super.onPause(); + + if (mControlsHandler != null && mControls != null) { + mControlsHandler.removeCallbacks(mControls); + } + mControls = null; + } + + @Override + protected void onDestroy() { + LinphoneManager.getInstance().changeStatusToOnline(); + LinphoneManager.getInstance().enableProximitySensing(false); + + unregisterReceiver(mHeadsetReceiver); + + if (mControlsHandler != null && mControls != null) { + mControlsHandler.removeCallbacks(mControls); + } + mControls = null; + mControlsHandler = null; + + unbindDrawables(findViewById(R.id.topLayout)); + if (mTimer != null) { + mTimer.cancel(); + } + sInstance = 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); + } + + @Override // Never invoke actually + public void onBackPressed() { + if (mDialog != null) { + acceptCallUpdate(false); + mDialog.dismiss(); + mDialog = null; + } + } + + public void bindAudioFragment(CallAudioFragment fragment) { + mAudioCallFragment = fragment; + } + + public void bindVideoFragment(CallVideoFragment fragment) { + mVideoCallFragment = fragment; + } + + // CALL INFORMATION + private void displayCurrentCall(Call call) { + Address lAddress = call.getRemoteAddress(); + TextView contactName = findViewById(R.id.current_contact_name); + setContactInformation(contactName, lAddress); + registerCallDurationTimer(null, call); + } + + private void displayPausedCalls(final Call call, int index) { + // Control Row + LinearLayout callView; + + if (call == null) { + callView = + (LinearLayout) + mInflater.inflate(R.layout.conference_paused_row, mContainer, false); + callView.setId(index + 1); + callView.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + pauseOrResumeConference(); + } + }); + } else { + callView = + (LinearLayout) mInflater.inflate(R.layout.call_inactive_row, mContainer, false); + callView.setId(index + 1); + + TextView contactName = callView.findViewById(R.id.contact_name); + + Address lAddress = call.getRemoteAddress(); + LinphoneContact lContact = + ContactsManager.getInstance().findContactFromAddress(lAddress); + + if (lContact == null) { + String displayName = LinphoneUtils.getAddressDisplayName(lAddress); + contactName.setText(displayName); + ContactAvatar.displayAvatar(displayName, callView.findViewById(R.id.avatar_layout)); + } else { + contactName.setText(lContact.getFullName()); + ContactAvatar.displayAvatar(lContact, callView.findViewById(R.id.avatar_layout)); + } + + displayCallStatusIconAndReturnCallPaused(callView, call); + registerCallDurationTimer(callView, call); + } + mCallsList.addView(callView); + } + + private void setContactInformation(TextView contactName, Address lAddress) { + LinphoneContact lContact = ContactsManager.getInstance().findContactFromAddress(lAddress); + if (lContact == null) { + String displayName = LinphoneUtils.getAddressDisplayName(lAddress); + contactName.setText(displayName); + ContactAvatar.displayAvatar(displayName, mAvatarLayout, true); + } else { + contactName.setText(lContact.getFullName()); + ContactAvatar.displayAvatar(lContact, mAvatarLayout, true); + } + } + + private void displayCallStatusIconAndReturnCallPaused(LinearLayout callView, Call call) { + ImageView onCallStateChanged = callView.findViewById(R.id.call_pause); + onCallStateChanged.setTag(call); + onCallStateChanged.setOnClickListener(this); + + if (call.getState() == State.Paused + || call.getState() == State.PausedByRemote + || call.getState() == State.Pausing) { + onCallStateChanged.setSelected(false); + } else if (call.getState() == State.OutgoingInit + || call.getState() == State.OutgoingProgress + || call.getState() == State.OutgoingRinging) { + } + } + + private void registerCallDurationTimer(View v, Call call) { + int callDuration = call.getDuration(); + if (callDuration == 0 && call.getState() != State.StreamsRunning) { + return; + } + + Chronometer timer; + if (v == null) { + timer = findViewById(R.id.current_call_timer); + } else { + timer = 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(); + } + + private void refreshCallList() { + mIsConferenceRunning = LinphoneManager.getLc().isInConference(); + List pausedCalls = + LinphoneUtils.getCallsInState( + LinphoneManager.getLc(), Collections.singletonList(State.PausedByRemote)); + + // MultiCalls + if (LinphoneManager.getLc().getCallsNb() > 1) { + mCallsList.setVisibility(View.VISIBLE); + } + + // Active call + if (LinphoneManager.getLc().getCurrentCall() != null) { + displayNoCurrentCall(false); + if (isVideoEnabled(LinphoneManager.getLc().getCurrentCall()) + && !mIsConferenceRunning + && pausedCalls.size() == 0) { + displayVideoCall(false); + } else { + displayAudioCall(); + } + } else { + showAudioView(); + displayNoCurrentCall(true); + if (LinphoneManager.getLc().getCallsNb() == 1) { + mCallsList.setVisibility(View.VISIBLE); + } + } + + // Conference + if (mIsConferenceRunning) { + displayConference(true); + } else { + displayConference(false); + } + + if (mCallsList != null) { + mCallsList.removeAllViews(); + int index = 0; + + if (LinphoneManager.getLc().getCallsNb() == 0) { + goBackToDialer(); + return; + } + + boolean isConfPaused = false; + for (Call call : LinphoneManager.getLc().getCalls()) { + if (call.getConference() != null && !mIsConferenceRunning) { + isConfPaused = true; + index++; + } else { + if (call != LinphoneManager.getLc().getCurrentCall() + && call.getConference() == null) { + displayPausedCalls(call, index); + index++; + } else { + displayCurrentCall(call); + } + } + } + + if (!mIsConferenceRunning) { + if (isConfPaused) { + mCallsList.setVisibility(View.VISIBLE); + displayPausedCalls(null, index); + } + } + } + + // Paused by remote + if (pausedCalls.size() == 1) { + displayCallPaused(true); + } else { + displayCallPaused(false); + } + } + + // Conference + private void exitConference(final Call call) { + Core lc = LinphoneManager.getLc(); + + if (lc.isInConference()) { + lc.removeFromConference(call); + if (lc.getConferenceSize() <= 1) { + lc.leaveConference(); + } + } + refreshIncallUi(); + } + + private void enterConference() { + LinphoneManager.getLc().addAllToConference(); + } + + private void pauseOrResumeConference() { + Core lc = LinphoneManager.getLc(); + mConferenceStatus = findViewById(R.id.conference_pause); + if (mConferenceStatus != null) { + if (lc.isInConference()) { + mConferenceStatus.setSelected(true); + lc.leaveConference(); + } else { + mConferenceStatus.setSelected(false); + lc.enterConference(); + } + } + refreshCallList(); + } + + private void displayConferenceParticipant(int index, final Call call) { + LinearLayout confView = + (LinearLayout) mInflater.inflate(R.layout.conf_call_control_row, mContainer, false); + mConferenceList.setId(index + 1); + TextView contact = 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 = confView.findViewById(R.id.quitConference); + quitConference.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + exitConference(call); + } + }); + mConferenceList.addView(confView); + } + + private void displayConferenceHeader() { + mConferenceList.setVisibility(View.VISIBLE); + RelativeLayout headerConference = + (RelativeLayout) mInflater.inflate(R.layout.conference_header, mContainer, false); + mConferenceStatus = headerConference.findViewById(R.id.conference_pause); + mConferenceStatus.setOnClickListener(this); + mConferenceList.addView(headerConference); + } + + private void displayConference(boolean isInConf) { + if (isInConf) { + mControlsLayout.setVisibility(View.VISIBLE); + mActiveCallHeader.setVisibility(View.GONE); + mNoCurrentCall.setVisibility(View.GONE); + mConferenceList.removeAllViews(); + + // Conference Header + displayConferenceHeader(); + + // Conference participant + int index = 1; + for (Call call : LinphoneManager.getLc().getCalls()) { + if (call.getConference() != null) { + displayConferenceParticipant(index, call); + index++; + } + } + mConferenceList.setVisibility(View.VISIBLE); + } else { + mConferenceList.setVisibility(View.GONE); + } + } + + private void displayMissedChats() { + int count = LinphoneManager.getInstance().getUnreadMessageCount(); + + if (count > 0) { + mMissedChats.setText(String.valueOf(count)); + mMissedChats.setVisibility(View.VISIBLE); + } else { + mMissedChats.clearAnimation(); + mMissedChats.setVisibility(View.GONE); + } + } + + @SuppressWarnings("deprecation") + private void formatText(TextView tv, String name, String value) { + tv.setText(Html.fromHtml("" + name + " " + value)); + } + + private String getEncoderText(String mime) { + String ret = mEncoderTexts.get(mime); + if (ret == null) { + org.linphone.mediastream.Factory msfactory = + LinphoneManager.getLc().getMediastreamerFactory(); + ret = msfactory.getEncoderText(mime); + mEncoderTexts.put(mime, ret); + } + return ret; + } + + private String getDecoderText(String mime) { + String ret = mDecoderTexts.get(mime); + if (ret == null) { + org.linphone.mediastream.Factory msfactory = + LinphoneManager.getLc().getMediastreamerFactory(); + ret = msfactory.getDecoderText(mime); + mDecoderTexts.put(mime, ret); + } + return ret; + } + + private void displayMediaStats( + CallParams params, + CallStats stats, + PayloadType media, + View layout, + TextView title, + TextView codec, + TextView dl, + TextView ul, + TextView edl, + TextView ice, + TextView ip, + TextView senderLossRate, + TextView receiverLossRate, + TextView enc, + TextView dec, + TextView videoResolutionSent, + TextView videoResolutionReceived, + TextView videoFpsSent, + TextView videoFpsReceived, + boolean isVideo, + TextView jitterBuffer) { + if (stats != null) { + String mime = null; + + layout.setVisibility(View.VISIBLE); + title.setVisibility(TextView.VISIBLE); + if (media != null) { + mime = media.getMimeType(); + formatText( + codec, + getString(R.string.call_stats_codec), + mime + " / " + (media.getClockRate() / 1000) + "kHz"); + } + if (mime != null) { + formatText(enc, getString(R.string.call_stats_encoder_name), getEncoderText(mime)); + formatText(dec, getString(R.string.call_stats_decoder_name), getDecoderText(mime)); + } + formatText( + dl, + getString(R.string.call_stats_download), + String.valueOf((int) stats.getDownloadBandwidth()) + " kbits/s"); + formatText( + ul, + getString(R.string.call_stats_upload), + String.valueOf((int) stats.getUploadBandwidth()) + " kbits/s"); + if (isVideo) { + formatText( + edl, + getString(R.string.call_stats_estimated_download), + String.valueOf(stats.getEstimatedDownloadBandwidth()) + " kbits/s"); + } + formatText(ice, getString(R.string.call_stats_ice), stats.getIceState().toString()); + formatText( + ip, + getString(R.string.call_stats_ip), + (stats.getIpFamilyOfRemote() == AddressFamily.Inet6) + ? "IpV6" + : (stats.getIpFamilyOfRemote() == AddressFamily.Inet) + ? "IpV4" + : "Unknown"); + formatText( + senderLossRate, + getString(R.string.call_stats_sender_loss_rate), + new DecimalFormat("##.##").format(stats.getSenderLossRate()) + "%"); + formatText( + receiverLossRate, + getString(R.string.call_stats_receiver_loss_rate), + new DecimalFormat("##.##").format(stats.getReceiverLossRate()) + "%"); + if (isVideo) { + formatText( + videoResolutionSent, + getString(R.string.call_stats_video_resolution_sent), + "\u2191 " + params.getSentVideoDefinition() != null + ? params.getSentVideoDefinition().getName() + : ""); + formatText( + videoResolutionReceived, + getString(R.string.call_stats_video_resolution_received), + "\u2193 " + params.getReceivedVideoDefinition() != null + ? params.getReceivedVideoDefinition().getName() + : ""); + formatText( + videoFpsSent, + getString(R.string.call_stats_video_fps_sent), + "\u2191 " + params.getSentFramerate()); + formatText( + videoFpsReceived, + getString(R.string.call_stats_video_fps_received), + "\u2193 " + params.getReceivedFramerate()); + } else { + formatText( + jitterBuffer, + getString(R.string.call_stats_jitter_buffer), + new DecimalFormat("##.##").format(stats.getJitterBufferSizeMs()) + " ms"); + } + } else { + layout.setVisibility(View.GONE); + title.setVisibility(TextView.GONE); + } + } + + private void initCallStatsRefresher(final Call call, final View view) { + if (mCallDisplayedInStats == call) return; + + if (mTimer != null && mTask != null) { + mTimer.cancel(); + mTimer = null; + mTask = null; + } + mCallDisplayedInStats = call; + + if (call == null) return; + + final TextView titleAudio = view.findViewById(R.id.call_stats_audio); + final TextView titleVideo = view.findViewById(R.id.call_stats_video); + final TextView codecAudio = view.findViewById(R.id.codec_audio); + final TextView codecVideo = view.findViewById(R.id.codec_video); + final TextView encoderAudio = view.findViewById(R.id.encoder_audio); + final TextView decoderAudio = view.findViewById(R.id.decoder_audio); + final TextView encoderVideo = view.findViewById(R.id.encoder_video); + final TextView decoderVideo = view.findViewById(R.id.decoder_video); + final TextView displayFilter = view.findViewById(R.id.display_filter); + final TextView dlAudio = view.findViewById(R.id.downloadBandwith_audio); + final TextView ulAudio = view.findViewById(R.id.uploadBandwith_audio); + final TextView dlVideo = view.findViewById(R.id.downloadBandwith_video); + final TextView ulVideo = view.findViewById(R.id.uploadBandwith_video); + final TextView edlVideo = view.findViewById(R.id.estimatedDownloadBandwidth_video); + final TextView iceAudio = view.findViewById(R.id.ice_audio); + final TextView iceVideo = view.findViewById(R.id.ice_video); + final TextView videoResolutionSent = view.findViewById(R.id.video_resolution_sent); + final TextView videoResolutionReceived = view.findViewById(R.id.video_resolution_received); + final TextView videoFpsSent = view.findViewById(R.id.video_fps_sent); + final TextView videoFpsReceived = view.findViewById(R.id.video_fps_received); + final TextView senderLossRateAudio = view.findViewById(R.id.senderLossRateAudio); + final TextView receiverLossRateAudio = view.findViewById(R.id.receiverLossRateAudio); + final TextView senderLossRateVideo = view.findViewById(R.id.senderLossRateVideo); + final TextView receiverLossRateVideo = view.findViewById(R.id.receiverLossRateVideo); + final TextView ipAudio = view.findViewById(R.id.ip_audio); + final TextView ipVideo = view.findViewById(R.id.ip_video); + final TextView jitterBufferAudio = view.findViewById(R.id.jitterBufferAudio); + final View videoLayout = view.findViewById(R.id.callStatsVideo); + final View audioLayout = view.findViewById(R.id.callStatsAudio); + + mCallListener = + new CallListenerStub() { + public void onStateChanged(Call call, Call.State cstate, String message) { + if (cstate == Call.State.End || cstate == Call.State.Error) { + if (mTimer != null) { + Log.i( + "Call is terminated, stopping mCountDownTimer in charge of stats refreshing."); + mTimer.cancel(); + } + } + } + }; + + mTimer = new Timer(); + mTask = + new TimerTask() { + @Override + public void run() { + if (call == null) { + mTimer.cancel(); + return; + } + + if (titleAudio == null + || codecAudio == null + || dlVideo == null + || edlVideo == null + || iceAudio == null + || videoResolutionSent == null + || videoLayout == null + || titleVideo == null + || ipVideo == null + || ipAudio == null + || codecVideo == null + || dlAudio == null + || ulAudio == null + || ulVideo == null + || iceVideo == null + || videoResolutionReceived == null) { + mTimer.cancel(); + return; + } + + mHandler.post( + new Runnable() { + @Override + public void run() { + if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() + == null) return; + synchronized (LinphoneManager.getLc()) { + if (LinphoneActivity.isInstanciated() + && call.getState() != Call.State.Released) { + CallParams params = call.getCurrentParams(); + if (params != null) { + CallStats audioStats = + call.getStats(StreamType.Audio); + CallStats videoStats = null; + + if (params.videoEnabled()) + videoStats = + call.getStats(StreamType.Video); + + PayloadType payloadAudio = + params.getUsedAudioPayloadType(); + PayloadType payloadVideo = + params.getUsedVideoPayloadType(); + + formatText( + displayFilter, + getString( + R.string + .call_stats_display_filter), + call.getCore().getVideoDisplayFilter()); + + displayMediaStats( + params, + audioStats, + payloadAudio, + audioLayout, + titleAudio, + codecAudio, + dlAudio, + ulAudio, + null, + iceAudio, + ipAudio, + senderLossRateAudio, + receiverLossRateAudio, + encoderAudio, + decoderAudio, + null, + null, + null, + null, + false, + jitterBufferAudio); + + displayMediaStats( + params, + videoStats, + payloadVideo, + videoLayout, + titleVideo, + codecVideo, + dlVideo, + ulVideo, + edlVideo, + iceVideo, + ipVideo, + senderLossRateVideo, + receiverLossRateVideo, + encoderVideo, + decoderVideo, + videoResolutionSent, + videoResolutionReceived, + videoFpsSent, + videoFpsReceived, + true, + null); + } + } + } + } + }); + } + }; + call.addListener(mCallListener); + mTimer.scheduleAtFixedRate(mTask, 0, 1000); + } + + //// Earset Connectivity Broadcast innerClass + public class HeadsetReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (!BluetoothManager.getInstance().isBluetoothHeadsetAvailable()) { + if (intent.hasExtra("state")) { + switch (intent.getIntExtra("state", 0)) { + case 0: + if (mOldIsSpeakerEnabled) { + LinphoneManager.getInstance().routeAudioToSpeaker(); + mIsSpeakerEnabled = true; + mSpeaker.setEnabled(true); + } + break; + case 1: + LinphoneManager.getInstance().routeAudioToReceiver(); + mOldIsSpeakerEnabled = mIsSpeakerEnabled; + mIsSpeakerEnabled = false; + mSpeaker.setEnabled(false); + break; + } + refreshInCallActions(); + } + } + } + } +} diff --git a/src/android/org/linphone/call/CallAudioFragment.java b/app/src/main/java/org/linphone/call/CallAudioFragment.java similarity index 74% rename from src/android/org/linphone/call/CallAudioFragment.java rename to app/src/main/java/org/linphone/call/CallAudioFragment.java index eca7df897..68b20c9c6 100644 --- a/src/android/org/linphone/call/CallAudioFragment.java +++ b/app/src/main/java/org/linphone/call/CallAudioFragment.java @@ -24,30 +24,23 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import org.linphone.R; 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; + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.audio, container, false); } @Override public void onStart() { super.onStart(); - incallActvityInstance = (CallActivity) getActivity(); + CallActivity incallActvityInstance = (CallActivity) getActivity(); if (incallActvityInstance != null) { incallActvityInstance.bindAudioFragment(this); - } - - // Just to be sure we have incall controls - if (incallActvityInstance != null) { + // Just to be sure we have incall controls incallActvityInstance.removeCallbacks(); } } diff --git a/app/src/main/java/org/linphone/call/CallIncomingActivity.java b/app/src/main/java/org/linphone/call/CallIncomingActivity.java new file mode 100644 index 000000000..62f6adb83 --- /dev/null +++ b/app/src/main/java/org/linphone/call/CallIncomingActivity.java @@ -0,0 +1,324 @@ +package org.linphone.call; + +/* +CallIncomingActivity.java +Copyright (C) 2017 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.Manifest; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.TextureView; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; +import androidx.core.app.ActivityCompat; +import java.util.ArrayList; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.compatibility.Compatibility; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.Call; +import org.linphone.core.Call.State; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneGenericActivity; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.CallIncomingAnswerButton; +import org.linphone.views.CallIncomingButtonListener; +import org.linphone.views.CallIncomingDeclineButton; +import org.linphone.views.ContactAvatar; + +public class CallIncomingActivity extends LinphoneGenericActivity { + private static CallIncomingActivity sInstance; + + private TextView mName, mNumber; + private ImageView mAcceptIcon; + private CallIncomingAnswerButton mAccept; + private CallIncomingDeclineButton mDecline; + private Call mCall; + private CoreListenerStub mListener; + private boolean mAlreadyAcceptedOrDeniedCall; + private KeyguardManager mKeyguardManager; + private TextureView mVideoDisplay; + + public static CallIncomingActivity instance() { + return sInstance; + } + + public static boolean isInstanciated() { + return sInstance != null; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getResources().getBoolean(R.bool.orientation_portrait_only)) { + setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + + Compatibility.setShowWhenLocked(this, true); + Compatibility.setTurnScreenOn(this, true); + + setContentView(R.layout.call_incoming); + + mName = findViewById(R.id.contact_name); + mNumber = findViewById(R.id.contact_number); + mVideoDisplay = findViewById(R.id.videoSurface); + + mAccept = findViewById(R.id.answer_button); + mDecline = findViewById(R.id.decline_button); + mAcceptIcon = findViewById(R.id.acceptIcon); + lookupCurrentCall(); + + if (LinphonePreferences.instance() != null + && mCall != null + && mCall.getRemoteParams() != null + && LinphonePreferences.instance().shouldAutomaticallyAcceptVideoRequests() + && mCall.getRemoteParams().videoEnabled()) { + mAcceptIcon.setImageResource(R.drawable.call_video_start); + } + + mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + boolean doNotUseSliders = + getResources() + .getBoolean( + R.bool.do_not_use_sliders_to_answer_hangup_call_if_phone_unlocked); + if (doNotUseSliders && !mKeyguardManager.inKeyguardRestrictedInputMode()) { + mAccept.setSliderMode(false); + mDecline.setSliderMode(false); + } else { + mAccept.setSliderMode(true); + mDecline.setSliderMode(true); + mAccept.setDeclineButton(mDecline); + mDecline.setAnswerButton(mAccept); + } + mAccept.setListener( + new CallIncomingButtonListener() { + @Override + public void onAction() { + answer(); + } + }); + mDecline.setListener( + new CallIncomingButtonListener() { + @Override + public void onAction() { + decline(); + } + }); + + mListener = + new CoreListenerStub() { + @Override + public void onCallStateChanged( + Core lc, Call call, State state, String message) { + if (call == mCall && State.End == state) { + finish(); + } else if (state == State.Connected) { + startActivity( + new Intent(CallIncomingActivity.this, CallActivity.class)); + } else if (state == State.StreamsRunning) { + Log.e( + "CallIncommingActivity - onCreate - State.StreamsRunning - speaker = " + + LinphoneManager.getInstance().isSpeakerEnabled()); + // The following should not be needed except some devices need it (e.g. + // Galaxy S). + LinphoneManager.getInstance() + .enableSpeaker( + LinphoneManager.getInstance().isSpeakerEnabled()); + } + } + }; + + sInstance = this; + } + + @Override + protected void onResume() { + super.onResume(); + sInstance = this; + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + } + + mAlreadyAcceptedOrDeniedCall = false; + mCall = null; + + // Only one call ringing at a time is allowed + lookupCurrentCall(); + if (mCall == null) { + // The incoming call no longer exists. + Log.d("Couldn't find incoming call"); + finish(); + return; + } + + Address address = mCall.getRemoteAddress(); + LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); + if (contact != null) { + ContactAvatar.displayAvatar(contact, findViewById(R.id.avatar_layout), true); + mName.setText(contact.getFullName()); + } else { + String displayName = LinphoneUtils.getAddressDisplayName(address); + ContactAvatar.displayAvatar(displayName, findViewById(R.id.avatar_layout), true); + mName.setText(displayName); + } + mNumber.setText(address.asStringUriOnly()); + + if (LinphonePreferences.instance().acceptIncomingEarlyMedia()) { + if (mCall.getCurrentParams().videoEnabled()) { + findViewById(R.id.avatar_layout).setVisibility(View.GONE); + mCall.getCore().setNativeVideoWindowId(mVideoDisplay); + } + } + } + + @Override + protected void onStart() { + super.onStart(); + checkAndRequestCallPermissions(); + } + + @Override + protected void onPause() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + } + super.onPause(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + sInstance = 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 lookupCurrentCall() { + if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null) { + for (Call call : LinphoneManager.getLc().getCalls()) { + if (State.IncomingReceived == call.getState() + || State.IncomingEarlyMedia == call.getState()) { + mCall = call; + break; + } + } + } + } + + private void decline() { + if (mAlreadyAcceptedOrDeniedCall) { + return; + } + mAlreadyAcceptedOrDeniedCall = true; + + LinphoneManager.getLc().terminateCall(mCall); + finish(); + } + + private void answer() { + if (mAlreadyAcceptedOrDeniedCall) { + return; + } + mAlreadyAcceptedOrDeniedCall = true; + + if (!LinphoneManager.getInstance().acceptCall(mCall)) { + // 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(); + } + } + + 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) { + 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) { + 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/android/org/linphone/call/CallManager.java b/app/src/main/java/org/linphone/call/CallManager.java similarity index 58% rename from src/android/org/linphone/call/CallManager.java rename to app/src/main/java/org/linphone/call/CallManager.java index 1a868fe29..4c8fcad88 100644 --- a/src/android/org/linphone/call/CallManager.java +++ b/app/src/main/java/org/linphone/call/CallManager.java @@ -19,39 +19,51 @@ 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.BandwidthManager; import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; import org.linphone.core.Address; import org.linphone.core.Call; import org.linphone.core.CallParams; import org.linphone.core.Core; -import org.linphone.core.CoreException; -import org.linphone.mediastream.Log; +import org.linphone.core.MediaEncryption; +import org.linphone.core.tools.Log; +import org.linphone.utils.FileUtils; +import org.linphone.utils.LinphoneUtils; -/** - * Handle call updating, reinvites. - */ +/** Handle call updating, reinvites. */ public class CallManager { - private static CallManager instance; + private static CallManager sInstance; - private CallManager() { + public static synchronized CallManager getInstance() { + if (sInstance == null) sInstance = new CallManager(); + return sInstance; } - public static final synchronized CallManager getInstance() { - if (instance == null) instance = new CallManager(); - return instance; - } + private CallManager() {} - private BandwidthManager bm() { + private BandwidthManager getBandwidthManager() { return BandwidthManager.getInstance(); } - public void inviteAddress(Address lAddress, boolean videoEnabled, boolean lowBandwidth) throws CoreException { + public void inviteAddress(Address lAddress, boolean forceZRTP) { + boolean isLowBandwidthConnection = + !LinphoneUtils.isHighBandwidthConnection( + LinphoneService.instance().getApplicationContext()); + + inviteAddress(lAddress, false, isLowBandwidthConnection, forceZRTP); + } + + public void inviteAddress(Address lAddress) { + inviteAddress(lAddress, false); + } + + public void inviteAddress( + Address lAddress, boolean videoEnabled, boolean lowBandwidth, boolean forceZRTP) { Core lc = LinphoneManager.getLc(); CallParams params = lc.createCallParams(null); - bm().updateWithProfileSettings(lc, params); + getBandwidthManager().updateWithProfileSettings(params); if (videoEnabled && params.videoEnabled()) { params.enableVideo(true); @@ -64,13 +76,25 @@ public class CallManager { Log.d("Low bandwidth enabled in call params"); } + if (forceZRTP) { + params.setMediaEncryption(MediaEncryption.ZRTP); + } + + String recordFile = + FileUtils.getCallRecordingFilename( + LinphoneManager.getInstance().getContext(), lAddress); + params.setRecordFile(recordFile); + lc.inviteAddressWithParams(lAddress, params); } + public void inviteAddress(Address lAddress, boolean videoEnabled, boolean lowBandwidth) { + inviteAddress(lAddress, videoEnabled, lowBandwidth, false); + } + /** - * 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. + * 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 */ @@ -85,9 +109,8 @@ public class CallManager { if (params.videoEnabled()) return false; - // Check if video possible regarding bandwidth limitations - bm().updateWithProfileSettings(lc, params); + getBandwidthManager().updateWithProfileSettings(params); // Abort if not enough bandwidth... if (!params.videoEnabled()) { @@ -101,8 +124,8 @@ public class CallManager { /** * 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. + * Update current call, without reinvite. The camera will be restarted when mediastreamer chain + * is recreated and setParameters is called. */ public void updateCall() { Core lc = LinphoneManager.getLc(); @@ -112,8 +135,7 @@ public class CallManager { return; } CallParams params = lc.createCallParams(lCall); - bm().updateWithProfileSettings(lc, params); + getBandwidthManager().updateWithProfileSettings(params); lc.updateCall(lCall, null); } - } diff --git a/app/src/main/java/org/linphone/call/CallOutgoingActivity.java b/app/src/main/java/org/linphone/call/CallOutgoingActivity.java new file mode 100644 index 000000000..9fc3e45e9 --- /dev/null +++ b/app/src/main/java/org/linphone/call/CallOutgoingActivity.java @@ -0,0 +1,303 @@ +package org.linphone.call; + +/* +CallOutgoingActivity.java +Copyright (C) 2017 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.Manifest; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +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 androidx.core.app.ActivityCompat; +import java.util.ArrayList; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.Call; +import org.linphone.core.Call.State; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.Reason; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneGenericActivity; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; + +public class CallOutgoingActivity extends LinphoneGenericActivity implements OnClickListener { + private TextView mName, mNumber; + private ImageView mMicro, mSpeaker, mHangUp; + private Call mCall; + private CoreListenerStub mListener; + private boolean mIsMicMuted, mIsSpeakerEnabled; + + @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); + + mName = findViewById(R.id.contact_name); + mNumber = findViewById(R.id.contact_number); + + mIsMicMuted = false; + mIsSpeakerEnabled = false; + + mMicro = findViewById(R.id.micro); + mMicro.setOnClickListener(this); + mSpeaker = findViewById(R.id.speaker); + mSpeaker.setOnClickListener(this); + + mHangUp = findViewById(R.id.outgoing_hang_up); + mHangUp.setOnClickListener(this); + + mListener = + new CoreListenerStub() { + @Override + public void onCallStateChanged( + Core lc, Call call, Call.State state, String message) { + if (call == mCall && State.Connected == state) { + if (!LinphoneActivity.isInstanciated()) { + return; + } + LinphoneActivity.instance().startIncallActivity(); + return; + } else if (state == State.Error) { + // Convert Core message for internalization + if (call.getErrorInfo().getReason() == Reason.Declined) { + displayCustomToast( + getString(R.string.error_call_declined), + Toast.LENGTH_SHORT); + decline(); + } else if (call.getErrorInfo().getReason() == Reason.NotFound) { + displayCustomToast( + getString(R.string.error_user_not_found), + Toast.LENGTH_SHORT); + decline(); + } else if (call.getErrorInfo().getReason() == Reason.NotAcceptable) { + displayCustomToast( + getString(R.string.error_incompatible_media), + Toast.LENGTH_SHORT); + decline(); + } else if (call.getErrorInfo().getReason() == Reason.Busy) { + displayCustomToast( + getString(R.string.error_user_busy), Toast.LENGTH_SHORT); + decline(); + } else if (message != null) { + displayCustomToast( + getString(R.string.error_unknown) + " - " + message, + Toast.LENGTH_SHORT); + decline(); + } + } else if (state == State.End) { + // Convert Core message for internalization + if (call.getErrorInfo().getReason() == Reason.Declined) { + displayCustomToast( + getString(R.string.error_call_declined), + Toast.LENGTH_SHORT); + decline(); + } + } + + if (LinphoneManager.getLc().getCallsNb() == 0) { + finish(); + } + } + }; + } + + @Override + protected void onResume() { + super.onResume(); + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + } + + mCall = null; + + // Only one call ringing at a time is allowed + if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null) { + for (Call call : LinphoneManager.getLc().getCalls()) { + 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(); + return; + } + } + } + if (mCall == null) { + Log.e("Couldn't find outgoing call"); + finish(); + return; + } + + Address address = mCall.getRemoteAddress(); + LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); + if (contact != null) { + ContactAvatar.displayAvatar(contact, findViewById(R.id.avatar_layout), true); + mName.setText(contact.getFullName()); + } else { + String displayName = LinphoneUtils.getAddressDisplayName(address); + ContactAvatar.displayAvatar(displayName, findViewById(R.id.avatar_layout), true); + mName.setText(displayName); + } + mNumber.setText(LinphoneUtils.getDisplayableAddress(address)); + } + + @Override + protected void onStart() { + super.onStart(); + checkAndRequestCallPermissions(); + } + + @Override + protected void onPause() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + } + super.onPause(); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + + if (id == R.id.micro) { + mIsMicMuted = !mIsMicMuted; + mMicro.setSelected(mIsMicMuted); + LinphoneManager.getLc().enableMic(!mIsMicMuted); + } + if (id == R.id.speaker) { + mIsSpeakerEnabled = !mIsSpeakerEnabled; + mSpeaker.setSelected(mIsSpeakerEnabled); + LinphoneManager.getInstance().enableSpeaker(mIsSpeakerEnabled); + } + 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); + } + + private 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 = 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) { + 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) { + 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/android/org/linphone/call/CallVideoFragment.java b/app/src/main/java/org/linphone/call/CallVideoFragment.java similarity index 57% rename from src/android/org/linphone/call/CallVideoFragment.java rename to app/src/main/java/org/linphone/call/CallVideoFragment.java index 9a768f89e..563c4b683 100644 --- a/src/android/org/linphone/call/CallVideoFragment.java +++ b/app/src/main/java/org/linphone/call/CallVideoFragment.java @@ -27,41 +27,39 @@ import android.view.GestureDetector.OnDoubleTapListener; import android.view.GestureDetector.OnGestureListener; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.widget.RelativeLayout; - import org.linphone.LinphoneManager; -import org.linphone.LinphonePreferences; import org.linphone.LinphoneService; -import org.linphone.LinphoneUtils; import org.linphone.R; -import org.linphone.compatibility.Compatibility; import org.linphone.compatibility.CompatibilityScaleGestureDetector; import org.linphone.compatibility.CompatibilityScaleGestureListener; import org.linphone.core.Call; import org.linphone.core.Core; import org.linphone.core.VideoDefinition; -import org.linphone.mediastream.Log; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneUtils; -public class CallVideoFragment extends Fragment implements OnGestureListener, OnDoubleTapListener, CompatibilityScaleGestureListener { +public class CallVideoFragment extends Fragment + implements OnGestureListener, OnDoubleTapListener, CompatibilityScaleGestureListener { private TextureView mVideoView; private TextureView mCaptureView; private GestureDetector mGestureDetector; private float mZoomFactor = 1.f; private float mZoomCenterX, mZoomCenterY; private CompatibilityScaleGestureDetector mScaleDetector; - private CallActivity inCallActivity; - private int previewX, previewY; + private CallActivity mInCallActivity; + private int mPreviewX, mPreviewY; @SuppressWarnings("deprecation") // Warning useless because value is ignored and automatically set by new APIs. @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + 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); @@ -71,57 +69,64 @@ public class CallVideoFragment extends Fragment implements OnGestureListener, On mVideoView = view.findViewById(R.id.videoSurface); mCaptureView = view.findViewById(R.id.videoCaptureSurface); - + LinphoneManager.getLc().setNativeVideoWindowId(mVideoView); LinphoneManager.getLc().setNativePreviewWindowId(mCaptureView); - mVideoView.setOnTouchListener(new OnTouchListener() { - public boolean onTouch(View v, MotionEvent event) { - if (mScaleDetector != null) { - mScaleDetector.onTouchEvent(event); - } + 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; - } - }); + mGestureDetector.onTouchEvent(event); + if (mInCallActivity != null) { + mInCallActivity.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; - } - }); + mCaptureView.setOnTouchListener( + new OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + mPreviewX = (int) motionEvent.getX(); + mPreviewY = (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 - mPreviewX); + int top = lp.topMargin + (y - mPreviewY); + 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); + mInCallActivity = (CallActivity) getActivity(); + if (mInCallActivity != null) { + mInCallActivity.bindVideoFragment(this); } } @@ -137,30 +142,36 @@ public class CallVideoFragment extends Fragment implements OnGestureListener, On DisplayMetrics metrics = new DisplayMetrics(); getActivity().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 + 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 + VideoDefinition videoSize = + call.getCurrentParams() + .getSentVideoDefinition(); // It already takes care of rotation if (videoSize.getWidth() == 0 || videoSize.getHeight() == 0) { - Log.w("Couldn't get sent video definition, using default video definition"); + Log.w( + "[Video Fragment] Couldn't get sent video definition, using default video definition"); videoSize = lc.getPreferredVideoDefinition(); } int width = videoSize.getWidth(); int height = videoSize.getHeight(); - Log.d("Video height is " + height + ", width is " + width); + Log.d("[Video Fragment] Video height is " + height + ", width is " + width); width = width * maxHeight / height; height = maxHeight; if (mCaptureView == null) { - Log.e("mCaptureView is null !"); + Log.e("[Video Fragment] 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_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 preview size set to " + width + "x" + height); + Log.d("[Video Fragment] Video preview size set to " + width + "x" + height); } } @@ -177,17 +188,14 @@ public class CallVideoFragment extends Fragment implements OnGestureListener, On } String newDevice; - if (index == 1) - newDevice = devices[0]; - else if (devices.length > 1) - newDevice = devices[1]; - else - newDevice = devices[index]; + if (index == 1) newDevice = devices[0]; + else if (devices.length > 1) newDevice = devices[1]; + else newDevice = devices[index]; LinphoneManager.getLc().setVideoDevice(newDevice); CallManager.getInstance().updateCall(); } catch (ArithmeticException ae) { - Log.e("Cannot swtich camera : no camera"); + Log.e("[Video Fragment] Cannot swtich camera : no camera"); } } @@ -199,16 +207,24 @@ public class CallVideoFragment extends Fragment implements OnGestureListener, On LinphoneService.instance().destroyOverlay(); } - mGestureDetector = new GestureDetector(inCallActivity, this); - mScaleDetector = Compatibility.getScaleGestureDetector(inCallActivity, this); + mGestureDetector = new GestureDetector(mInCallActivity, this); + mScaleDetector = new CompatibilityScaleGestureDetector(mInCallActivity); + mScaleDetector.setOnScaleListener(this); resizePreview(); } @Override public void onPause() { - if (LinphonePreferences.instance().isOverlayEnabled()) { - LinphoneService.instance().createOverlay(); + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (LinphonePreferences.instance().isOverlayEnabled() + && lc != null + && lc.getCurrentCall() != null) { + Call call = lc.getCurrentCall(); + if (call.getState() == Call.State.StreamsRunning) { + // Prevent overlay creation if video call is paused by remote + LinphoneService.instance().createOverlay(); + } } super.onPause(); @@ -218,10 +234,15 @@ public class CallVideoFragment extends Fragment implements OnGestureListener, On 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); + 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))); + float landscapeZoomFactor = + ((float) mVideoView.getWidth()) / (float) ((3 * mVideoView.getHeight()) / 4); + mZoomFactor = + Math.max( + 0.1f, + Math.min(mZoomFactor, Math.max(portraitZoomFactor, landscapeZoomFactor))); Call currentCall = LinphoneManager.getLc().getCurrentCall(); if (currentCall != null) { @@ -247,16 +268,14 @@ public class CallVideoFragment extends Fragment implements OnGestureListener, On mZoomCenterY -= 0.01; } - if (mZoomCenterX > 1) - mZoomCenterX = 1; - if (mZoomCenterX < 0) - mZoomCenterX = 0; - if (mZoomCenterY > 1) - mZoomCenterY = 1; - if (mZoomCenterY < 0) - mZoomCenterY = 0; + if (mZoomCenterX > 1) mZoomCenterX = 1; + if (mZoomCenterX < 0) mZoomCenterX = 0; + if (mZoomCenterY > 1) mZoomCenterY = 1; + if (mZoomCenterY < 0) mZoomCenterY = 0; - LinphoneManager.getLc().getCurrentCall().zoom(mZoomFactor, mZoomCenterX, mZoomCenterY); + LinphoneManager.getLc() + .getCurrentCall() + .zoom(mZoomFactor, mZoomCenterX, mZoomCenterY); return true; } } @@ -269,9 +288,13 @@ public class CallVideoFragment extends Fragment implements OnGestureListener, On 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); + 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); + float landscapeZoomFactor = + ((float) mVideoView.getWidth()) + / (float) ((3 * mVideoView.getHeight()) / 4); mZoomFactor = Math.max(portraitZoomFactor, landscapeZoomFactor); } else { @@ -292,7 +315,7 @@ public class CallVideoFragment extends Fragment implements OnGestureListener, On @Override public void onDestroy() { - inCallActivity = null; + mInCallActivity = null; mCaptureView = null; if (mVideoView != null) { @@ -327,20 +350,15 @@ public class CallVideoFragment extends Fragment implements OnGestureListener, On } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } @Override - public void onLongPress(MotionEvent e) { - - } + public void onLongPress(MotionEvent e) {} @Override - public void onShowPress(MotionEvent e) { - - } + public void onShowPress(MotionEvent e) {} @Override public boolean onSingleTapUp(MotionEvent e) { diff --git a/app/src/main/java/org/linphone/chat/ChatMessageOldViewHolder.java b/app/src/main/java/org/linphone/chat/ChatMessageOldViewHolder.java new file mode 100644 index 000000000..452d1a01e --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatMessageOldViewHolder.java @@ -0,0 +1,107 @@ +package org.linphone.chat; + +/* +ChatMessageViewHolder.java +Copyright (C) 2017 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.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import org.linphone.R; + +public class ChatMessageOldViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener { + public final LinearLayout eventLayout; + public final TextView eventMessage; + + public final RelativeLayout bubbleLayout; + public final LinearLayout separatorLayout; + public final LinearLayout background; + public final RelativeLayout avatarLayout; + public final TextView contactName; + + public final ImageView messageStatus; + public final ProgressBar messageSendingInProgress; + public final LinearLayout imdmLayout; + public final ImageView imdmIcon; + public final TextView imdmLabel; + + public final TextView messageText; + public final ImageView messageImage; + + public final RelativeLayout fileTransferLayout; + public final ProgressBar fileTransferProgressBar; + public final Button fileTransferAction; + + public final TextView fileName; + public final Button openFileButton; + + public final CheckBox delete; + + private ChatMessageViewHolderClickListener mListener; + + public ChatMessageOldViewHolder(View view, ChatMessageViewHolderClickListener listener) { + this(view); + mListener = listener; + view.setOnClickListener(this); + } + + public ChatMessageOldViewHolder(View view) { + super(view); + eventLayout = view.findViewById(R.id.event); + // eventTime = view.findViewById(R.id.event_date); + eventMessage = view.findViewById(R.id.event_text); + + bubbleLayout = view.findViewById(R.id.bubble); + background = view.findViewById(R.id.background); + avatarLayout = view.findViewById(R.id.avatar_layout); + contactName = view.findViewById(R.id.contact_header); + + messageStatus = view.findViewById(R.id.status); + messageSendingInProgress = view.findViewById(R.id.inprogress); + imdmLayout = view.findViewById(R.id.imdmLayout); + imdmIcon = view.findViewById(R.id.imdmIcon); + imdmLabel = view.findViewById(R.id.imdmText); + + messageText = view.findViewById(R.id.message); + messageImage = view.findViewById(R.id.image); + separatorLayout = view.findViewById(R.id.separator); + + fileTransferLayout = view.findViewById(R.id.file_transfer_layout); + fileTransferProgressBar = view.findViewById(R.id.progress_bar); + fileTransferAction = view.findViewById(R.id.file_transfer_action); + + fileName = view.findViewById(R.id.file_name); + openFileButton = view.findViewById(R.id.open_file); + + delete = view.findViewById(R.id.delete_message); + } + + @Override + public void onClick(View v) { + if (mListener != null) { + mListener.onItemClicked(getAdapterPosition()); + } + } +} diff --git a/app/src/main/java/org/linphone/chat/ChatMessageViewHolder.java b/app/src/main/java/org/linphone/chat/ChatMessageViewHolder.java new file mode 100644 index 000000000..d3177f56c --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatMessageViewHolder.java @@ -0,0 +1,377 @@ +package org.linphone.chat; + +/* +ChatMessageViewHolder.java +Copyright (C) 2017 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 static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.webkit.MimeTypeMap; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.core.content.FileProvider; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.google.android.flexbox.FlexboxLayout; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.R; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.ChatMessage; +import org.linphone.core.Content; +import org.linphone.core.tools.Log; +import org.linphone.utils.FileUtils; +import org.linphone.utils.ImageUtils; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; + +public class ChatMessageViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + + public final LinearLayout eventLayout; + public final TextView eventMessage; + + public final LinearLayout securityEventLayout; + public final TextView securityEventMessage; + + public final View rightAnchor; + public final RelativeLayout bubbleLayout; + public final LinearLayout background; + public final RelativeLayout avatarLayout; + + public final ProgressBar downloadInProgress, sendInProgress; + public final TextView timeText; + public final ImageView outgoingImdn; + public final TextView messageText; + + public final FlexboxLayout multiFileContents; + public final RelativeLayout singleFileContent; + + public final CheckBox delete; + + private Context mContext; + private ChatMessageViewHolderClickListener mListener; + + public ChatMessageViewHolder( + Context context, View view, ChatMessageViewHolderClickListener listener) { + this(view); + mContext = context; + mListener = listener; + view.setOnClickListener(this); + } + + public ChatMessageViewHolder(View view) { + super(view); + eventLayout = view.findViewById(R.id.event); + eventMessage = view.findViewById(R.id.event_text); + + securityEventLayout = view.findViewById(R.id.security_event); + securityEventMessage = view.findViewById(R.id.security_event_text); + + rightAnchor = view.findViewById(R.id.rightAnchor); + bubbleLayout = view.findViewById(R.id.bubble); + background = view.findViewById(R.id.background); + avatarLayout = view.findViewById(R.id.avatar_layout); + + downloadInProgress = view.findViewById(R.id.download_in_progress); + sendInProgress = view.findViewById(R.id.send_in_progress); + timeText = view.findViewById(R.id.time); + outgoingImdn = view.findViewById(R.id.imdn); + messageText = view.findViewById(R.id.message); + + singleFileContent = view.findViewById(R.id.single_content); + multiFileContents = view.findViewById(R.id.multi_content); + + delete = view.findViewById(R.id.delete_event); + } + + @Override + public void onClick(View v) { + if (mListener != null) { + mListener.onItemClicked(getAdapterPosition()); + } + } + + public void bindMessage(final ChatMessage message, LinphoneContact contact) { + eventLayout.setVisibility(View.GONE); + securityEventLayout.setVisibility(View.GONE); + rightAnchor.setVisibility(View.VISIBLE); + bubbleLayout.setVisibility(View.VISIBLE); + messageText.setVisibility(View.GONE); + timeText.setVisibility(View.VISIBLE); + outgoingImdn.setVisibility(View.GONE); + avatarLayout.setVisibility(View.GONE); + sendInProgress.setVisibility(View.GONE); + downloadInProgress.setVisibility(View.GONE); + singleFileContent.setVisibility(View.GONE); + multiFileContents.setVisibility(View.GONE); + + ChatMessage.State status = message.getState(); + Address remoteSender = message.getFromAddress(); + String displayName; + String time = + LinphoneUtils.timestampToHumanDate( + mContext, message.getTime(), R.string.messages_date_format); + + if (message.isOutgoing()) { + bubbleLayout.setPadding(0, 0, 0, 0); // Reset padding + outgoingImdn.setVisibility(View.INVISIBLE); // For anchoring purposes + + if (status == ChatMessage.State.DeliveredToUser) { + outgoingImdn.setVisibility(View.VISIBLE); + outgoingImdn.setImageResource(R.drawable.imdn_received); + } else if (status == ChatMessage.State.Displayed) { + outgoingImdn.setVisibility(View.VISIBLE); + outgoingImdn.setImageResource(R.drawable.imdn_read); + } else if (status == ChatMessage.State.NotDelivered) { + outgoingImdn.setVisibility(View.VISIBLE); + outgoingImdn.setImageResource(R.drawable.imdn_error); + } else if (status == ChatMessage.State.FileTransferError) { + outgoingImdn.setVisibility(View.VISIBLE); + outgoingImdn.setImageResource(R.drawable.imdn_error); + } else if (status == ChatMessage.State.InProgress + || status == ChatMessage.State.FileTransferInProgress) { + sendInProgress.setVisibility(View.VISIBLE); + } + + timeText.setVisibility(View.VISIBLE); + background.setBackgroundResource(R.drawable.chat_bubble_outgoing_full); + } else { + rightAnchor.setVisibility(View.GONE); + avatarLayout.setVisibility(View.VISIBLE); + background.setBackgroundResource(R.drawable.chat_bubble_incoming_full); + + // Can't anchor incoming messages, setting this to align max width with LIME icon + bubbleLayout.setPadding( + 0, 0, (int) ImageUtils.dpToPixels(LinphoneActivity.instance(), 18), 0); + + if (status == ChatMessage.State.FileTransferInProgress) { + downloadInProgress.setVisibility(View.VISIBLE); + } + } + + if (contact == null) { + contact = ContactsManager.getInstance().findContactFromAddress(remoteSender); + } + if (contact != null) { + if (contact.getFullName() != null) { + displayName = contact.getFullName(); + } else { + displayName = LinphoneUtils.getAddressDisplayName(remoteSender); + } + ContactAvatar.displayAvatar(contact, avatarLayout); + } else { + displayName = LinphoneUtils.getAddressDisplayName(remoteSender); + ContactAvatar.displayAvatar(displayName, avatarLayout); + } + + if (message.isOutgoing()) { + timeText.setText(time); + } else { + timeText.setText(time + " - " + displayName); + } + + if (message.hasTextContent()) { + String msg = message.getTextContent(); + Spanned text = LinphoneUtils.getTextWithHttpLinks(msg); + messageText.setText(text); + messageText.setMovementMethod(LinkMovementMethod.getInstance()); + messageText.setVisibility(View.VISIBLE); + } + + List fileContents = new ArrayList<>(); + for (Content c : message.getContents()) { + if (c.isFile() || c.isFileTransfer()) { + fileContents.add(c); + } + } + + if (fileContents.size() == 1) { + singleFileContent.setVisibility(View.VISIBLE); + displayContent(message, fileContents.get(0), singleFileContent, false); + } else if (fileContents.size() > 1) { + multiFileContents.removeAllViews(); + multiFileContents.setVisibility(View.VISIBLE); + + for (Content c : fileContents) { + View content = + LayoutInflater.from(mContext) + .inflate(R.layout.chat_bubble_content, null, false); + + displayContent(message, c, content, true); + + multiFileContents.addView(content); + } + } + } + + private void displayContent( + final ChatMessage message, Content c, View content, boolean isMultiContent) { + final Button downloadOrCancel = content.findViewById(R.id.download); + downloadOrCancel.setVisibility(View.GONE); + final ImageView bigImage = content.findViewById(R.id.bigImage); + bigImage.setVisibility(View.GONE); + final ImageView smallImage = content.findViewById(R.id.image); + smallImage.setVisibility(View.GONE); + final TextView fileName = content.findViewById(R.id.file); + fileName.setVisibility(View.GONE); + + if (c.isFile() || (c.isFileTransfer() && message.isOutgoing())) { + // If message is outgoing, even if content + // is file transfer we have the file available + final String filePath = c.getFilePath(); + + View v; + if (FileUtils.isExtensionImage(filePath)) { + if (!isMultiContent + && mContext.getResources() + .getBoolean( + R.bool.use_big_pictures_to_preview_images_file_transfers)) { + loadBitmap(c.getFilePath(), bigImage); + v = bigImage; + } else { + loadBitmap(c.getFilePath(), smallImage); + v = smallImage; + } + } else { + fileName.setText(FileUtils.getNameFromFilePath(filePath)); + v = fileName; + } + v.setVisibility(View.VISIBLE); + v.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + openFile(filePath); + } + }); + } else { + downloadOrCancel.setVisibility(View.VISIBLE); + + if (mContext.getPackageManager() + .checkPermission( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + mContext.getPackageName()) + == PackageManager.PERMISSION_GRANTED) { + String filename = c.getName(); + File file = new File(FileUtils.getStorageDirectory(mContext), filename); + + int prefix = 1; + while (file.exists()) { + file = + new File( + FileUtils.getStorageDirectory(mContext), + prefix + "_" + filename); + Log.w( + "File with that name already exists, renamed to " + + prefix + + "_" + + filename); + prefix += 1; + } + c.setFilePath(file.getPath()); + + downloadOrCancel.setTag(c); + if (!message.isFileTransferInProgress()) { + downloadOrCancel.setText(R.string.download_file); + } else { + downloadOrCancel.setText(R.string.cancel); + } + + downloadOrCancel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + Content c = (Content) v.getTag(); + if (!message.isFileTransferInProgress()) { + message.downloadContent(c); + } else { + message.cancelFileTransfer(); + } + } + }); + } else { + Log.w( + "WRITE_EXTERNAL_STORAGE permission not granted, won't be able to store the downloaded file"); + LinphoneActivity.instance().checkAndRequestExternalStoragePermission(); + } + } + } + + private void openFile(String path) { + Intent intent = new Intent(Intent.ACTION_VIEW); + File file; + Uri contentUri; + if (path.startsWith("file://")) { + path = path.substring("file://".length()); + file = new File(path); + contentUri = + FileProvider.getUriForFile( + mContext, + mContext.getResources().getString(R.string.file_provider), + file); + } else if (path.startsWith("content://")) { + contentUri = Uri.parse(path); + } else { + file = new File(path); + try { + contentUri = + FileProvider.getUriForFile( + mContext, + mContext.getResources().getString(R.string.file_provider), + file); + } catch (Exception e) { + contentUri = Uri.parse(path); + } + } + + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(contentUri.toString()); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + if (type != null) { + intent.setDataAndType(contentUri, type); + } else { + intent.setDataAndType(contentUri, "*/*"); + } + intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + mContext.startActivity(intent); + } + + private void loadBitmap(String path, ImageView imageView) { + Glide.with(mContext).load(path).into(imageView); + } +} diff --git a/app/src/main/java/org/linphone/chat/ChatMessageViewHolderClickListener.java b/app/src/main/java/org/linphone/chat/ChatMessageViewHolderClickListener.java new file mode 100644 index 000000000..e5b49ebb2 --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatMessageViewHolderClickListener.java @@ -0,0 +1,24 @@ +package org.linphone.chat; + +/* +ChatMessageViewHolderClickListener.java +Copyright (C) 2018 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. +*/ + +interface ChatMessageViewHolderClickListener { + void onItemClicked(int position); +} diff --git a/app/src/main/java/org/linphone/chat/ChatMessagesAdapter.java b/app/src/main/java/org/linphone/chat/ChatMessagesAdapter.java new file mode 100644 index 000000000..95eb0b0df --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatMessagesAdapter.java @@ -0,0 +1,366 @@ +package org.linphone.chat; + +/* +ChatMessagesAdapter.java +Copyright (C) 2017 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.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.linphone.R; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatMessageListenerStub; +import org.linphone.core.EventLog; +import org.linphone.utils.LinphoneUtils; +import org.linphone.utils.SelectableAdapter; +import org.linphone.utils.SelectableHelper; + +public class ChatMessagesAdapter extends SelectableAdapter + implements ChatMessagesGenericAdapter { + private static final int MAX_TIME_TO_GROUP_MESSAGES = 300; // 5 minutes + + private final Context mContext; + private List mHistory; + private List mParticipants; + private final int mItemResource; + private final ChatMessagesFragment mFragment; + + private final List mTransientMessages; + + private final ChatMessageViewHolderClickListener mClickListener; + private final ChatMessageListenerStub mListener; + + public ChatMessagesAdapter( + ChatMessagesFragment fragment, + SelectableHelper helper, + int itemResource, + EventLog[] history, + ArrayList participants, + ChatMessageViewHolderClickListener clickListener) { + super(helper); + mFragment = fragment; + mContext = mFragment.getActivity(); + mItemResource = itemResource; + mHistory = new ArrayList<>(Arrays.asList(history)); + Collections.reverse(mHistory); + mParticipants = participants; + mClickListener = clickListener; + mTransientMessages = new ArrayList<>(); + + mListener = + new ChatMessageListenerStub() { + @Override + public void onMsgStateChanged(ChatMessage message, ChatMessage.State state) { + ChatMessageViewHolder holder = + (ChatMessageViewHolder) message.getUserData(); + if (holder != null) { + int position = holder.getAdapterPosition(); + if (position >= 0) { + notifyItemChanged(position); + } else { + notifyDataSetChanged(); + } + } else { + // Just in case, better to refresh the whole view than to miss + // an update + notifyDataSetChanged(); + } + if (state == ChatMessage.State.Displayed) { + mTransientMessages.remove(message); + } + } + }; + } + + @Override + public ChatMessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(mItemResource, parent, false); + ChatMessageViewHolder VH = new ChatMessageViewHolder(mContext, v, mClickListener); + + // Allows onLongClick ContextMenu on bubbles + mFragment.registerForContextMenu(v); + v.setTag(VH); + return VH; + } + + @Override + public void onBindViewHolder(@NonNull ChatMessageViewHolder holder, int position) { + if (position < 0) return; + EventLog event = mHistory.get(position); + + holder.delete.setVisibility(View.GONE); + holder.eventLayout.setVisibility(View.GONE); + holder.securityEventLayout.setVisibility(View.GONE); + holder.rightAnchor.setVisibility(View.GONE); + holder.bubbleLayout.setVisibility(View.GONE); + holder.sendInProgress.setVisibility(View.GONE); + + if (isEditionEnabled()) { + holder.delete.setVisibility(View.VISIBLE); + holder.delete.setChecked(isSelected(position)); + holder.delete.setTag(position); + } + + if (event.getType() == EventLog.Type.ConferenceChatMessage) { + ChatMessage message = event.getChatMessage(); + + if ((message.isOutgoing() && message.getState() != ChatMessage.State.Displayed) + || (!message.isOutgoing() && message.isFileTransfer())) { + if (!mTransientMessages.contains(message)) { + mTransientMessages.add(message); + } + // This only works if JAVA object is kept, hence the transient list + message.setUserData(holder); + message.addListener(mListener); + } + + LinphoneContact contact = null; + Address remoteSender = message.getFromAddress(); + if (!message.isOutgoing()) { + for (LinphoneContact c : mParticipants) { + if (c != null && c.hasAddress(remoteSender.asStringUriOnly())) { + contact = c; + break; + } + } + } + holder.bindMessage(message, contact); + changeBackgroundDependingOnPreviousAndNextEvents(message, holder, position); + } else { // Event is not chat message + Address address = event.getParticipantAddress(); + if (address == null && event.getType() == EventLog.Type.ConferenceSecurityEvent) { + address = event.getSecurityEventFaultyDeviceAddress(); + } + String displayName = ""; + if (address != null) { + LinphoneContact contact = + ContactsManager.getInstance().findContactFromAddress(address); + if (contact != null) { + displayName = contact.getFullName(); + } else { + displayName = LinphoneUtils.getAddressDisplayName(address); + } + } + + switch (event.getType()) { + case ConferenceCreated: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText(mContext.getString(R.string.conference_created)); + break; + case ConferenceTerminated: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText(mContext.getString(R.string.conference_destroyed)); + break; + case ConferenceParticipantAdded: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText( + mContext.getString(R.string.participant_added) + .replace("%s", displayName)); + break; + case ConferenceParticipantRemoved: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText( + mContext.getString(R.string.participant_removed) + .replace("%s", displayName)); + break; + case ConferenceSubjectChanged: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText( + mContext.getString(R.string.subject_changed) + .replace("%s", event.getSubject())); + break; + case ConferenceParticipantSetAdmin: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText( + mContext.getString(R.string.admin_set).replace("%s", displayName)); + break; + case ConferenceParticipantUnsetAdmin: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText( + mContext.getString(R.string.admin_unset).replace("%s", displayName)); + break; + case ConferenceParticipantDeviceAdded: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText( + mContext.getString(R.string.device_added).replace("%s", displayName)); + break; + case ConferenceParticipantDeviceRemoved: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText( + mContext.getString(R.string.device_removed).replace("%s", displayName)); + break; + case ConferenceSecurityEvent: + holder.securityEventLayout.setVisibility(View.VISIBLE); + + switch (event.getSecurityEventType()) { + case EncryptionIdentityKeyChanged: + holder.securityEventMessage.setText( + mContext.getString(R.string.lime_identity_key_changed) + .replace("%s", displayName)); + break; + case ManInTheMiddleDetected: + holder.securityEventMessage.setText( + mContext.getString(R.string.man_in_the_middle_detected) + .replace("%s", displayName)); + break; + case SecurityLevelDowngraded: + holder.securityEventMessage.setText( + mContext.getString(R.string.security_level_downgraded) + .replace("%s", displayName)); + break; + case ParticipantMaxDeviceCountExceeded: + holder.securityEventMessage.setText( + mContext.getString(R.string.participant_max_count_exceeded) + .replace("%s", displayName)); + break; + case None: + default: + break; + } + break; + case None: + default: + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setText( + mContext.getString(R.string.unexpected_event) + .replace("%s", displayName) + .replace("%i", String.valueOf(event.getType().toInt()))); + break; + } + } + } + + @Override + public int getItemCount() { + return mHistory.size(); + } + + public void addToHistory(EventLog log) { + mHistory.add(0, log); + notifyItemInserted(0); + notifyItemChanged(1); // Update second to last item just in case for grouping purposes + } + + public void addAllToHistory(ArrayList logs) { + int currentSize = mHistory.size() - 1; + Collections.reverse(logs); + mHistory.addAll(logs); + notifyItemRangeInserted(currentSize + 1, logs.size()); + } + + public void setContacts(ArrayList participants) { + mParticipants = participants; + } + + public void refresh(EventLog[] history) { + mHistory = new ArrayList<>(Arrays.asList(history)); + Collections.reverse(mHistory); + notifyDataSetChanged(); + } + + public void clear() { + for (EventLog event : mHistory) { + if (event.getType() == EventLog.Type.ConferenceChatMessage) { + ChatMessage message = event.getChatMessage(); + message.removeListener(mListener); + } + } + mTransientMessages.clear(); + mHistory.clear(); + } + + public Object getItem(int i) { + return mHistory.get(i); + } + + public void removeItem(int i) { + mHistory.remove(i); + notifyItemRemoved(i); + } + + private void changeBackgroundDependingOnPreviousAndNextEvents( + ChatMessage message, ChatMessageViewHolder holder, int position) { + boolean hasPrevious = false, hasNext = false; + + // Do not forget history is reversed, so previous in order is next in list display and + // chronology ! + if (position > 0 + && mContext.getResources() + .getBoolean(R.bool.lower_space_between_chat_bubbles_if_same_person)) { + EventLog previousEvent = (EventLog) getItem(position - 1); + if (previousEvent.getType() == EventLog.Type.ConferenceChatMessage) { + ChatMessage previousMessage = previousEvent.getChatMessage(); + if (previousMessage.getFromAddress().weakEqual(message.getFromAddress())) { + if (previousMessage.getTime() - message.getTime() + < MAX_TIME_TO_GROUP_MESSAGES) { + hasPrevious = true; + } + } + } + } + if (position >= 0 + && position < mHistory.size() - 1 + && mContext.getResources() + .getBoolean(R.bool.lower_space_between_chat_bubbles_if_same_person)) { + EventLog nextEvent = (EventLog) getItem(position + 1); + if (nextEvent.getType() == EventLog.Type.ConferenceChatMessage) { + ChatMessage nextMessage = nextEvent.getChatMessage(); + if (nextMessage.getFromAddress().weakEqual(message.getFromAddress())) { + if (message.getTime() - nextMessage.getTime() < MAX_TIME_TO_GROUP_MESSAGES) { + holder.timeText.setVisibility(View.GONE); + if (!message.isOutgoing()) { + holder.avatarLayout.setVisibility(View.INVISIBLE); + } + hasNext = true; + } + } + } + } + + if (message.isOutgoing()) { + if (hasNext && hasPrevious) { + holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_split_2); + } else if (hasNext) { + holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_split_3); + } else if (hasPrevious) { + holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_split_1); + } else { + holder.background.setBackgroundResource(R.drawable.chat_bubble_outgoing_full); + } + } else { + if (hasNext && hasPrevious) { + holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_split_2); + } else if (hasNext) { + holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_split_3); + } else if (hasPrevious) { + holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_split_1); + } else { + holder.background.setBackgroundResource(R.drawable.chat_bubble_incoming_full); + } + } + } +} diff --git a/app/src/main/java/org/linphone/chat/ChatMessagesFragment.java b/app/src/main/java/org/linphone/chat/ChatMessagesFragment.java new file mode 100644 index 000000000..0c34c6a93 --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatMessagesFragment.java @@ -0,0 +1,1499 @@ +package org.linphone.chat; + +/* +ChatMessagesFragment.java +Copyright (C) 2017 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 static android.content.Context.INPUT_METHOD_SERVICE; +import static org.linphone.fragments.FragmentsAvailable.CHAT; + +import android.app.Activity; +import android.app.Dialog; +import android.app.Fragment; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcelable; +import android.provider.MediaStore; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.core.view.inputmethod.InputConnectionCompat; +import androidx.core.view.inputmethod.InputContentInfoCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.call.CallManager; +import org.linphone.contacts.ContactAddress; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.ContactsUpdatedListener; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomCapabilities; +import org.linphone.core.ChatRoomListener; +import org.linphone.core.ChatRoomSecurityLevel; +import org.linphone.core.Content; +import org.linphone.core.Core; +import org.linphone.core.EventLog; +import org.linphone.core.Factory; +import org.linphone.core.LimeState; +import org.linphone.core.Participant; +import org.linphone.core.ParticipantDevice; +import org.linphone.core.Reason; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.FileUtils; +import org.linphone.utils.LinphoneUtils; +import org.linphone.utils.SelectableHelper; +import org.linphone.views.RichEditText; + +public class ChatMessagesFragment extends Fragment + implements ChatRoomListener, + ContactsUpdatedListener, + ChatMessageViewHolderClickListener, + SelectableHelper.DeleteListener, + RichEditText.RichInputListener { + private static final int ADD_PHOTO = 1337; + private static final int MESSAGES_PER_PAGE = 20; + private static final String INPUT_CONTENT_INFO_KEY = "COMMIT_CONTENT_INPUT_CONTENT_INFO"; + private static final String COMMIT_CONTENT_FLAGS_KEY = "COMMIT_CONTENT_FLAGS"; + + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private ImageView mBackButton, mCallButton, mBackToCallButton, mGroupInfosButton; + private ImageView mAttachImageButton, mSendMessageButton; + private TextView mRoomLabel, mParticipantsLabel, mSipUriLabel, mRemoteComposing; + private RichEditText mMessageTextToSend; + private LayoutInflater mInflater; + private RecyclerView mChatEventsList; + private LinearLayout mFilesUploadLayout; + private SelectableHelper mSelectionHelper; + private Context mContext; + private ViewTreeObserver.OnGlobalLayoutListener mKeyboardListener; + private Uri mImageToUploadUri; + private ChatMessagesAdapter mEventsAdapter; + private ChatMessagesOldAdapter mOldEventsAdapter; + private String mLocalSipUri, mRemoteSipUri; + private Address mLocalSipAddress, mRemoteSipAddress, mRemoteParticipantAddress; + private ChatRoom mChatRoom; + private ArrayList mParticipants; + private LinearLayoutManager layoutManager; + private int mContextMenuMessagePosition; + private ChatScrollListener mChatScrollListener; + private LinearLayout mTopBar; + private ImageView mChatRoomSecurityLevel; + + private InputContentInfoCompat mCurrentInputContentInfo; + private int mCurrentFlags; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Retain the fragment across configuration changes + setRetainInstance(true); + + if (getArguments() != null) { + if (getArguments().getString("LocalSipUri") != null) { + mLocalSipUri = getArguments().getString("LocalSipUri"); + mLocalSipAddress = LinphoneManager.getLc().createAddress(mLocalSipUri); + } + if (getArguments().getString("RemoteSipUri") != null) { + mRemoteSipUri = getArguments().getString("RemoteSipUri"); + mRemoteSipAddress = LinphoneManager.getLc().createAddress(mRemoteSipUri); + } + } + + mContext = getActivity().getApplicationContext(); + mInflater = inflater; + View view = inflater.inflate(R.layout.chat, container, false); + + mTopBar = view.findViewById(R.id.top_bar); + + mChatRoomSecurityLevel = view.findViewById(R.id.room_security_level); + mChatRoomSecurityLevel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + boolean oneParticipantOneDevice = false; + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + ParticipantDevice[] devices = + mChatRoom.getParticipants()[0].getDevices(); + if (devices.length == 1) { + oneParticipantOneDevice = true; + } + } + + if (LinphonePreferences.instance().isLimeSecurityPopupEnabled()) { + showSecurityDialog(oneParticipantOneDevice); + } else { + if (oneParticipantOneDevice) { + ParticipantDevice device = + mChatRoom.getParticipants()[0].getDevices()[0]; + CallManager.getInstance().inviteAddress(device.getAddress(), true); + } else { + LinphoneActivity.instance() + .goToContactDevicesInfos(mLocalSipUri, mRemoteSipUri); + } + } + } + }); + + mBackButton = view.findViewById(R.id.back); + if (getResources().getBoolean(R.bool.isTablet)) { + mBackButton.setVisibility(View.INVISIBLE); + } else { + mBackButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + LinphoneActivity.instance().goToChatList(); + } + }); + } + + mCallButton = view.findViewById(R.id.start_call); + mCallButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + LinphoneActivity.instance() + .setAddresGoToDialerAndCall( + mRemoteParticipantAddress.asString(), null); + } + }); + + mBackToCallButton = view.findViewById(R.id.back_to_call); + mBackToCallButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + LinphoneActivity.instance() + .resetClassicMenuLayoutAndGoBackToCallIfStillRunning(); + } + }); + + mGroupInfosButton = view.findViewById(R.id.group_infos); + mGroupInfosButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mChatRoom == null) return; + ArrayList participants = new ArrayList<>(); + for (Participant p : mChatRoom.getParticipants()) { + Address a = p.getAddress(); + LinphoneContact c = + ContactsManager.getInstance().findContactFromAddress(a); + if (c == null) { + c = new LinphoneContact(); + String displayName = LinphoneUtils.getAddressDisplayName(a); + c.setFullName(displayName); + } + ContactAddress ca = + new ContactAddress( + c, a.asString(), "", c.isFriend(), p.isAdmin()); + participants.add(ca); + } + LinphoneActivity.instance() + .goToChatGroupInfos( + mRemoteSipAddress.asString(), + participants, + mChatRoom.getSubject(), + mChatRoom.getMe() != null && mChatRoom.getMe().isAdmin(), + false, + null, + mChatRoom.hasCapability( + ChatRoomCapabilities.Encrypted.toInt())); + } + }); + + mRoomLabel = view.findViewById(R.id.subject); + mParticipantsLabel = view.findViewById(R.id.participants); + mSipUriLabel = view.findViewById(R.id.sipUri); + + mFilesUploadLayout = view.findViewById(R.id.file_upload_layout); + + mAttachImageButton = view.findViewById(R.id.send_picture); + mAttachImageButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + LinphoneActivity.instance().checkAndRequestPermissionsToSendImage(); + pickFile(); + } + }); + + mSendMessageButton = view.findViewById(R.id.send_message); + mSendMessageButton.setEnabled(false); + mSendMessageButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + sendMessage(); + } + }); + + mMessageTextToSend = view.findViewById(R.id.message); + mMessageTextToSend.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged( + CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + mSendMessageButton.setEnabled( + mMessageTextToSend.getText().length() > 0 + || mFilesUploadLayout.getChildCount() > 0); + if (mChatRoom != null && mMessageTextToSend.getText().length() > 0) { + if (!getResources().getBoolean(R.bool.allow_multiple_images_and_text)) { + mAttachImageButton.setEnabled(false); + } + mChatRoom.compose(); + } else { + mAttachImageButton.setEnabled(true); + } + } + + @Override + public void afterTextChanged(Editable editable) {} + }); + mMessageTextToSend.clearFocus(); + mMessageTextToSend.setListener(this); + + mRemoteComposing = view.findViewById(R.id.remote_composing); + + mChatEventsList = view.findViewById(R.id.chat_message_list); + mSelectionHelper = new SelectableHelper(view, this); + layoutManager = + new LinphoneLinearLayoutManager(mContext, LinearLayoutManager.VERTICAL, true); + mChatEventsList.setLayoutManager(layoutManager); + + mChatScrollListener = + new ChatScrollListener(layoutManager) { + @Override + public void onLoadMore(int totalItemsCount) { + loadMoreData(totalItemsCount); + } + }; + mChatEventsList.addOnScrollListener(mChatScrollListener); + + if (getArguments() != null) { + String fileSharedUri = getArguments().getString("fileSharedUri"); + if (fileSharedUri != null) { + Log.i("[ChatMessages] Found shared file(s): " + fileSharedUri); + if (fileSharedUri.contains(":")) { + String[] files = fileSharedUri.split(":"); + for (String file : files) { + addFileIntoSharingArea(file); + } + } else { + addFileIntoSharingArea(fileSharedUri); + } + } + + if (getArguments().getString("messageDraft") != null) { + String sharedText = getArguments().getString("messageDraft"); + mMessageTextToSend.setText(sharedText); + Log.i("[ChatMessages] Found shared text: " + sharedText); + } + } + + if (savedInstanceState != null) { + onRestoreInstanceState(savedInstanceState); + } + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(CHAT); + } + ContactsManager.getInstance().addContactsListener(this); + + addVirtualKeyboardVisiblityListener(); + // Force hide keyboard + getActivity() + .getWindow() + .setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN + | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + InputMethodManager inputMethodManager = + (InputMethodManager) getActivity().getSystemService(INPUT_METHOD_SERVICE); + if (getActivity().getCurrentFocus() != null) { + inputMethodManager.hideSoftInputFromWindow( + getActivity().getCurrentFocus().getWindowToken(), 0); + } + + initChatRoom(); + displayChatRoomHeader(); + displayChatRoomHistory(); + LinphoneManager.getInstance().setCurrentChatRoomAddress(mRemoteSipAddress); + } + + public void changeDisplayedChat(String localSipUri, String remoteSipUri) { + mLocalSipUri = localSipUri; + mLocalSipAddress = LinphoneManager.getLc().createAddress(mLocalSipUri); + mRemoteSipUri = remoteSipUri; + mRemoteSipAddress = LinphoneManager.getLc().createAddress(mRemoteSipUri); + + initChatRoom(); + displayChatRoomHeader(); + displayChatRoomHistory(); + + LinphoneManager.getInstance().setCurrentChatRoomAddress(mRemoteSipAddress); + } + + @Override + public void onPause() { + ContactsManager.getInstance().removeContactsListener(this); + removeVirtualKeyboardVisiblityListener(); + LinphoneManager.getInstance().setCurrentChatRoomAddress(null); + if (mChatRoom != null) mChatRoom.removeListener(this); + if (mChatEventsList.getAdapter() != null) + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).clear(); + super.onPause(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (data != null) { + if (requestCode == ADD_PHOTO && resultCode == Activity.RESULT_OK) { + String fileToUploadPath = null; + if (data.getData() != null) { + if (data.getData().toString().contains("com.android.contacts/contacts/")) { + if (FileUtils.getCVSPathFromLookupUri(data.getData().toString()) != null) { + fileToUploadPath = + FileUtils.getCVSPathFromLookupUri(data.getData().toString()) + .toString(); + } else { + // TODO Error + return; + } + } else { + fileToUploadPath = + FileUtils.getRealPathFromURI(getActivity(), data.getData()); + } + if (fileToUploadPath == null) { + fileToUploadPath = data.getData().toString(); + } + } else if (mImageToUploadUri != null) { + fileToUploadPath = mImageToUploadUri.getPath(); + } + + if (fileToUploadPath.startsWith("content://") + || fileToUploadPath.startsWith("file://")) { + fileToUploadPath = + FileUtils.getFilePath( + getActivity().getApplicationContext(), + Uri.parse(fileToUploadPath)); + } else if (fileToUploadPath.contains("com.android.contacts/contacts/")) { + fileToUploadPath = + FileUtils.getCVSPathFromLookupUri(fileToUploadPath).toString(); + } + + if (FileUtils.isExtensionImage(fileToUploadPath)) { + addImageToPendingList(fileToUploadPath); + } else { + addFileToPendingList(fileToUploadPath); + } + } else { + super.onActivityResult(requestCode, resultCode, data); + } + } else { + if (FileUtils.isExtensionImage(mImageToUploadUri.getPath())) { + File file = new File(mImageToUploadUri.getPath()); + if (file.exists()) { + addImageToPendingList(mImageToUploadUri.getPath()); + } + } + } + } + + @Override + public void onDeleteSelection(Object[] objectsToDelete) { + for (Object obj : objectsToDelete) { + EventLog eventLog = (EventLog) obj; + if (eventLog.getType() == EventLog.Type.ConferenceChatMessage) { + ChatMessage message = eventLog.getChatMessage(); + if (message.getAppdata() != null && !message.isOutgoing()) { + File file = new File(message.getAppdata()); + if (file.exists()) { + // Delete downloaded file from incoming message that will be deleted + file.delete(); + } + } + } + eventLog.deleteFromDatabase(); + } + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) + .refresh(mChatRoom.getHistoryMessageEvents(MESSAGES_PER_PAGE)); + } else { + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) + .refresh(mChatRoom.getHistoryEvents(MESSAGES_PER_PAGE)); + } + } + + @Override + public void onCreateContextMenu( + ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + + if (mContext.getResources().getBoolean(R.bool.use_new_chat_bubbles_layout)) { + ChatMessageViewHolder holder = (ChatMessageViewHolder) v.getTag(); + mContextMenuMessagePosition = holder.getAdapterPosition(); + } else { + ChatMessageOldViewHolder holder = (ChatMessageOldViewHolder) v.getTag(); + mContextMenuMessagePosition = holder.getAdapterPosition(); + } + + EventLog event = + (EventLog) + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) + .getItem(mContextMenuMessagePosition); + if (event.getType() != EventLog.Type.ConferenceChatMessage) { + return; + } + + MenuInflater inflater = getActivity().getMenuInflater(); + ChatMessage message = event.getChatMessage(); + if (message.getState() == ChatMessage.State.NotDelivered) { + inflater.inflate(R.menu.chat_bubble_menu_with_resend, menu); + } else { + inflater.inflate(R.menu.chat_bubble_menu, menu); + } + + if (!message.isOutgoing() + || mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + // Do not show incoming messages IDMN state in 1 to 1 chat room as we don't receive IMDN + // for them + menu.removeItem(R.id.imdn_infos); + } + if (!message.hasTextContent()) { + // Do not show copy text option if message doesn't have any text + menu.removeItem(R.id.copy_text); + } + + if (!message.isOutgoing()) { + Address address = message.getFromAddress(); + LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); + if (contact != null) { + menu.removeItem(R.id.add_to_contacts); + } + } else { + menu.removeItem(R.id.add_to_contacts); + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + EventLog event = + (EventLog) + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) + .getItem(mContextMenuMessagePosition); + + if (event.getType() != EventLog.Type.ConferenceChatMessage) { + return super.onContextItemSelected(item); + } + + ChatMessage message = event.getChatMessage(); + String messageId = message.getMessageId(); + + if (item.getItemId() == R.id.resend) { + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) + .removeItem(mContextMenuMessagePosition); + message.resend(); + return true; + } + if (item.getItemId() == R.id.imdn_infos) { + LinphoneActivity.instance() + .goToChatMessageImdnInfos(mLocalSipUri, mRemoteSipUri, messageId); + return true; + } + if (item.getItemId() == R.id.copy_text) { + if (message.hasTextContent()) { + ClipboardManager clipboard = + (ClipboardManager) + getActivity().getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Message", message.getTextContent()); + clipboard.setPrimaryClip(clip); + } + return true; + } + if (item.getItemId() == R.id.delete_message) { + mChatRoom.deleteMessage(message); + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) + .removeItem(mContextMenuMessagePosition); + return true; + } + if (item.getItemId() == R.id.add_to_contacts) { + Address address = message.getFromAddress(); + if (address == null) return true; + String uri = address.getUsername() + "@" + address.getDomain(); // Get a clean address + if (address.getDisplayName() != null) { + LinphoneActivity.instance() + .displayContactsForEdition(uri, address.getDisplayName()); + } else { + LinphoneActivity.instance().displayContactsForEdition(uri); + } + return true; + } + return super.onContextItemSelected(item); + } + + private void addFileIntoSharingArea(String fileSharedUri) { + if (FileUtils.isExtensionImage(fileSharedUri)) { + addImageToPendingList(fileSharedUri); + } else { + if (fileSharedUri.startsWith("content://") || fileSharedUri.startsWith("file://")) { + fileSharedUri = + FileUtils.getFilePath( + getActivity().getApplicationContext(), Uri.parse(fileSharedUri)); + } else if (fileSharedUri.contains("com.android.contacts/contacts/")) { + fileSharedUri = FileUtils.getCVSPathFromLookupUri(fileSharedUri).toString(); + } + addFileToPendingList(fileSharedUri); + } + } + + private void loadMoreData(final int totalItemsCount) { + mHandler.post( + new Runnable() { + @Override + public void run() { + int maxSize; + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + maxSize = mChatRoom.getHistorySize(); + } else { + maxSize = mChatRoom.getHistoryEventsSize(); + } + if (totalItemsCount < maxSize) { + int upperBound = totalItemsCount + MESSAGES_PER_PAGE; + if (upperBound > maxSize) { + upperBound = maxSize; + } + EventLog[] newLogs; + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + newLogs = + mChatRoom.getHistoryRangeMessageEvents( + totalItemsCount, upperBound); + } else { + newLogs = + mChatRoom.getHistoryRangeEvents( + totalItemsCount, upperBound); + } + ArrayList logsList = new ArrayList<>(Arrays.asList(newLogs)); + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()) + .addAllToHistory(logsList); + } + } + }); + } + + /** Keyboard management */ + private void addVirtualKeyboardVisiblityListener() { + mKeyboardListener = + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + Rect visibleArea = new Rect(); + getActivity() + .getWindow() + .getDecorView() + .getWindowVisibleDisplayFrame(visibleArea); + + int screenHeight = + getActivity().getWindow().getDecorView().getRootView().getHeight(); + int heightDiff = screenHeight - (visibleArea.bottom - visibleArea.top); + if (heightDiff > screenHeight * 0.15) { + showKeyboardVisibleMode(); + } else { + hideKeyboardVisibleMode(); + } + } + }; + getActivity() + .getWindow() + .getDecorView() + .getViewTreeObserver() + .addOnGlobalLayoutListener(mKeyboardListener); + } + + private void removeVirtualKeyboardVisiblityListener() { + getActivity() + .getWindow() + .getDecorView() + .getViewTreeObserver() + .removeOnGlobalLayoutListener(mKeyboardListener); + hideKeyboardVisibleMode(); + } + + private void showKeyboardVisibleMode() { + LinphoneActivity.instance().hideTabBar(true); + LinphoneActivity.instance().hideStatusBar(); + mTopBar.setVisibility(View.GONE); + } + + private void hideKeyboardVisibleMode() { + LinphoneActivity.instance() + .hideTabBar( + getResources().getBoolean(R.bool.hide_bottom_bar_on_second_level_views)); + LinphoneActivity.instance().showStatusBar(); + mTopBar.setVisibility(View.VISIBLE); + } + + /** View initialization */ + private void setReadOnly() { + mMessageTextToSend.setEnabled(false); + mAttachImageButton.setEnabled(false); + mSendMessageButton.setEnabled(false); + } + + private void getContactsForParticipants() { + mParticipants = new ArrayList<>(); + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + LinphoneContact c = + ContactsManager.getInstance().findContactFromAddress(mRemoteParticipantAddress); + if (c != null) { + mParticipants.add(c); + } + } else { + int index = 0; + StringBuilder participantsLabel = new StringBuilder(); + for (Participant p : mChatRoom.getParticipants()) { + LinphoneContact c = + ContactsManager.getInstance().findContactFromAddress(p.getAddress()); + if (c != null) { + mParticipants.add(c); + participantsLabel.append(c.getFullName()); + } else { + String displayName = LinphoneUtils.getAddressDisplayName(p.getAddress()); + participantsLabel.append(displayName); + } + index++; + if (index != mChatRoom.getNbParticipants()) participantsLabel.append(", "); + } + mParticipantsLabel.setText(participantsLabel.toString()); + } + + if (mChatEventsList.getAdapter() != null) { + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).setContacts(mParticipants); + } + } + + private void initChatRoom() { + if (mChatRoom != null) { + // Required on tablets + mChatRoom.removeListener(this); + } + + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (mRemoteSipAddress == null + || mRemoteSipUri == null + || mRemoteSipUri.length() == 0 + || core == null) { + // TODO error + return; + } + + if (mLocalSipAddress != null) { + mChatRoom = core.getChatRoom(mRemoteSipAddress, mLocalSipAddress); + } else { + mChatRoom = core.getChatRoomFromUri(mRemoteSipAddress.asStringUriOnly()); + } + mChatRoom.addListener(this); + mChatRoom.markAsRead(); + LinphoneManager.getInstance().updateUnreadCountForChatRoom(mChatRoom, 0); + LinphoneActivity.instance().refreshMissedChatCountDisplay(); + + mRemoteParticipantAddress = mRemoteSipAddress; + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) + && mChatRoom.getParticipants().length > 0) { + mRemoteParticipantAddress = mChatRoom.getParticipants()[0].getAddress(); + } + + getContactsForParticipants(); + + mRemoteComposing.setVisibility(View.GONE); + } + + private void displayChatRoomHeader() { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core == null || mChatRoom == null) return; + + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + mCallButton.setVisibility(View.VISIBLE); + mGroupInfosButton.setVisibility(View.GONE); + mParticipantsLabel.setVisibility(View.GONE); + + if (mContext.getResources().getBoolean(R.bool.show_sip_uri_in_chat)) { + mSipUriLabel.setVisibility(View.VISIBLE); + } else { + mSipUriLabel.setVisibility(View.GONE); + mRoomLabel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + mSipUriLabel.setVisibility( + mSipUriLabel.getVisibility() == View.VISIBLE + ? View.GONE + : View.VISIBLE); + } + }); + } + + if (mParticipants.size() == 0) { + // Contact not found + String displayName = LinphoneUtils.getAddressDisplayName(mRemoteParticipantAddress); + mRoomLabel.setText(displayName); + } else { + mRoomLabel.setText(mParticipants.get(0).getFullName()); + } + mSipUriLabel.setText(mRemoteParticipantAddress.asStringUriOnly()); + } else { + mCallButton.setVisibility(View.GONE); + mGroupInfosButton.setVisibility(View.VISIBLE); + mRoomLabel.setText(mChatRoom.getSubject()); + mParticipantsLabel.setVisibility(View.VISIBLE); + mSipUriLabel.setVisibility(View.GONE); + } + + mBackToCallButton.setVisibility(View.GONE); + if (core.getCallsNb() > 0) { + mBackToCallButton.setVisibility(View.VISIBLE); + } + + if (mChatRoom.hasBeenLeft()) { + setReadOnly(); + } + + updateSecurityLevelIcon(); + } + + private void updateSecurityLevelIcon() { + if (!mChatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { + mChatRoomSecurityLevel.setVisibility(View.GONE); + } else { + mChatRoomSecurityLevel.setVisibility(View.VISIBLE); + ChatRoomSecurityLevel level = mChatRoom.getSecurityLevel(); + switch (level) { + case Safe: + mChatRoomSecurityLevel.setImageResource(R.drawable.security_2_indicator); + break; + case Encrypted: + mChatRoomSecurityLevel.setImageResource(R.drawable.security_1_indicator); + break; + case ClearText: + case Unsafe: + mChatRoomSecurityLevel.setImageResource(R.drawable.security_alert_indicator); + break; + } + } + } + + private void displayChatRoomHistory() { + if (mChatRoom == null) return; + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + mEventsAdapter = + new ChatMessagesAdapter( + this, + mSelectionHelper, + R.layout.chat_bubble, + mChatRoom.getHistoryMessageEvents(MESSAGES_PER_PAGE), + mParticipants, + this); + mOldEventsAdapter = + new ChatMessagesOldAdapter( + this, + mSelectionHelper, + R.layout.chat_bubble_old, + mChatRoom.getHistoryMessageEvents(MESSAGES_PER_PAGE), + mParticipants, + this); + } else { + mEventsAdapter = + new ChatMessagesAdapter( + this, + mSelectionHelper, + R.layout.chat_bubble, + mChatRoom.getHistoryEvents(MESSAGES_PER_PAGE), + mParticipants, + this); + mOldEventsAdapter = + new ChatMessagesOldAdapter( + this, + mSelectionHelper, + R.layout.chat_bubble_old, + mChatRoom.getHistoryEvents(MESSAGES_PER_PAGE), + mParticipants, + this); + } + if (mContext.getResources().getBoolean(R.bool.use_new_chat_bubbles_layout)) { + mSelectionHelper.setAdapter(mEventsAdapter); + mChatEventsList.setAdapter(mEventsAdapter); + } else { + mSelectionHelper.setAdapter(mOldEventsAdapter); + mChatEventsList.setAdapter(mOldEventsAdapter); + } + scrollToBottom(); + } + + private void showSecurityDialog(boolean oneParticipantOneDevice) { + final Dialog dialog = + LinphoneActivity.instance().displayDialog(getString(R.string.lime_security_popup)); + Button delete = dialog.findViewById(R.id.dialog_delete_button); + delete.setVisibility(View.GONE); + Button ok = dialog.findViewById(R.id.dialog_ok_button); + ok.setText(oneParticipantOneDevice ? getString(R.string.call) : getString(R.string.ok)); + ok.setVisibility(View.VISIBLE); + Button cancel = dialog.findViewById(R.id.dialog_cancel_button); + cancel.setText(getString(R.string.cancel)); + + dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE); + final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain); + dialog.findViewById(R.id.doNotAskAgainLabel) + .setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + doNotAskAgain.setChecked(!doNotAskAgain.isChecked()); + } + }); + + ok.setTag(oneParticipantOneDevice); + ok.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + boolean oneParticipantOneDevice = (boolean) view.getTag(); + if (doNotAskAgain.isChecked()) { + LinphonePreferences.instance().enableLimeSecurityPopup(false); + } + + if (oneParticipantOneDevice) { + ParticipantDevice device = + mChatRoom.getParticipants()[0].getDevices()[0]; + CallManager.getInstance().inviteAddress(device.getAddress(), true); + } else { + LinphoneActivity.instance() + .goToContactDevicesInfos(mLocalSipUri, mRemoteSipUri); + } + + dialog.dismiss(); + } + }); + + cancel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (doNotAskAgain.isChecked()) { + LinphonePreferences.instance().enableLimeSecurityPopup(false); + } + dialog.dismiss(); + } + }); + dialog.show(); + } + + private void scrollToBottom() { + mChatEventsList.getLayoutManager().scrollToPosition(0); + } + + @Override + public void onItemClicked(int position) { + if (mSelectionHelper.getAdapter().isEditionEnabled()) { + mSelectionHelper.getAdapter().toggleSelection(position); + } + } + + /** File transfer related */ + @Override + public void onSaveInstanceState(Bundle outState) { + if (mFilesUploadLayout != null) { + String files[] = new String[mFilesUploadLayout.getChildCount()]; + for (int i = 0; i < mFilesUploadLayout.getChildCount(); i++) { + View child = mFilesUploadLayout.getChildAt(i); + String path = (String) child.getTag(); + files[i] = path; + } + outState.putStringArray("Files", files); + } + + if (mCurrentInputContentInfo != null) { + outState.putParcelable( + INPUT_CONTENT_INFO_KEY, (Parcelable) mCurrentInputContentInfo.unwrap()); + outState.putInt(COMMIT_CONTENT_FLAGS_KEY, mCurrentFlags); + } + mCurrentInputContentInfo = null; + mCurrentFlags = 0; + super.onSaveInstanceState(outState); + } + + private void onRestoreInstanceState(Bundle savedInstanceState) { + String files[] = savedInstanceState.getStringArray("Files"); + if (files != null && files.length > 0) { + for (String file : files) { + if (FileUtils.isExtensionImage(file)) { + addImageToPendingList(file); + } else { + addFileToPendingList(file); + } + } + } + + final InputContentInfoCompat previousInputContentInfo = + InputContentInfoCompat.wrap( + savedInstanceState.getParcelable(INPUT_CONTENT_INFO_KEY)); + final int previousFlags = savedInstanceState.getInt(COMMIT_CONTENT_FLAGS_KEY); + if (previousInputContentInfo != null) { + onCommitContentInternal(previousInputContentInfo, previousFlags); + } + } + + private void pickFile() { + List cameraIntents = new ArrayList<>(); + Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + File file = + new File( + FileUtils.getStorageDirectory(mContext), + getString(R.string.temp_photo_name_with_date) + .replace( + "%s", + String.valueOf(System.currentTimeMillis()) + ".jpeg")); + mImageToUploadUri = Uri.fromFile(file); + captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mImageToUploadUri); + cameraIntents.add(captureIntent); + + Intent galleryIntent = new Intent(); + galleryIntent.setType("image/*"); + galleryIntent.setAction(Intent.ACTION_PICK); + + Intent fileIntent = new Intent(); + fileIntent.setType("*/*"); + fileIntent.setAction(Intent.ACTION_GET_CONTENT); + cameraIntents.add(fileIntent); + + 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); + } + + private void addFileToPendingList(String path) { + if (path == null) { + Log.e("Can't add file to pending list because it's path is null..."); + return; + } + + View pendingFile = mInflater.inflate(R.layout.file_upload_cell, mFilesUploadLayout, false); + pendingFile.setTag(path); + + TextView text = pendingFile.findViewById(R.id.pendingFileForUpload); + String extension = path.substring(path.lastIndexOf('.')); + text.setText(extension); + + ImageView remove = pendingFile.findViewById(R.id.remove); + remove.setTag(pendingFile); + remove.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + View pendingImage = (View) view.getTag(); + mFilesUploadLayout.removeView(pendingImage); + mAttachImageButton.setEnabled(true); + mMessageTextToSend.setEnabled(true); + mSendMessageButton.setEnabled( + mMessageTextToSend.getText().length() > 0 + || mFilesUploadLayout.getChildCount() > 0); + } + }); + + mFilesUploadLayout.addView(pendingFile); + + if (!getResources().getBoolean(R.bool.allow_multiple_images_and_text)) { + mAttachImageButton.setEnabled(false); + mMessageTextToSend.setEnabled(false); + } + mSendMessageButton.setEnabled(true); + } + + private void addImageToPendingList(String path) { + if (path == null) { + Log.e("Can't add image to pending list because it's path is null..."); + return; + } + + View pendingImage = + mInflater.inflate(R.layout.image_upload_cell, mFilesUploadLayout, false); + pendingImage.setTag(path); + + ImageView image = pendingImage.findViewById(R.id.pendingImageForUpload); + Glide.with(mContext).load(path).into(image); + + ImageView remove = pendingImage.findViewById(R.id.remove); + remove.setTag(pendingImage); + remove.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + View pendingImage = (View) view.getTag(); + mFilesUploadLayout.removeView(pendingImage); + mAttachImageButton.setEnabled(true); + mMessageTextToSend.setEnabled(true); + mSendMessageButton.setEnabled( + mMessageTextToSend.getText().length() > 0 + || mFilesUploadLayout.getChildCount() > 0); + } + }); + + mFilesUploadLayout.addView(pendingImage); + + if (!getResources().getBoolean(R.bool.allow_multiple_images_and_text)) { + mAttachImageButton.setEnabled(false); + mMessageTextToSend.setEnabled(false); + } + mSendMessageButton.setEnabled(true); + } + + /** Message sending */ + private void sendMessage() { + ChatMessage msg = mChatRoom.createEmptyMessage(); + boolean isBasicChatRoom = mChatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()); + boolean sendMultipleImagesAsDifferentMessages = + getResources().getBoolean(R.bool.send_multiple_images_as_different_messages); + boolean sendImageAndTextAsDifferentMessages = + getResources().getBoolean(R.bool.send_text_and_images_as_different_messages); + + String text = mMessageTextToSend.getText().toString(); + boolean hasText = text != null && text.length() > 0; + + int filesCount = mFilesUploadLayout.getChildCount(); + for (int i = 0; i < filesCount; i++) { + String filePath = (String) mFilesUploadLayout.getChildAt(i).getTag(); + String fileName = filePath.substring(filePath.lastIndexOf("/") + 1); + String extension = FileUtils.getExtensionFromFileName(fileName); + Content content = Factory.instance().createContent(); + if (FileUtils.isExtensionImage(fileName)) { + content.setType("image"); + } else { + content.setType("file"); + } + content.setSubtype(extension); + content.setName(fileName); + content.setFilePath(filePath); // Let the file body handler take care of the upload + + boolean split = + isBasicChatRoom; // Always split contents in basic chat rooms for compatibility + if (hasText && sendImageAndTextAsDifferentMessages) { + split = true; + } else if (mFilesUploadLayout.getChildCount() > 1 + && sendMultipleImagesAsDifferentMessages) { + split = true; + + // Allow the last image to be sent with text if image and text at the same time OK + if (hasText && i == filesCount - 1) { + split = false; + } + } + + if (split) { + ChatMessage fileMessage = mChatRoom.createFileTransferMessage(content); + fileMessage.send(); + } else { + msg.addFileContent(content); + } + } + + if (hasText) { + msg.addTextContent(text); + } + + // Set listener not required here anymore, message will be added to messages list and + // adapter will set the listener + if (msg.getContents().length > 0) { + msg.send(); + } + + mFilesUploadLayout.removeAllViews(); + mAttachImageButton.setEnabled(true); + mMessageTextToSend.setEnabled(true); + mMessageTextToSend.setText(""); + } + + /* + * Chat room callbacks + */ + + @Override + public void onChatMessageSent(ChatRoom cr, EventLog event) { + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); + scrollToBottom(); + } + + @Override + public void onConferenceAddressGeneration(ChatRoom cr) {} + + @Override + public void onParticipantRegistrationSubscriptionRequested( + ChatRoom cr, Address participantAddr) {} + + @Override + public void onParticipantRegistrationUnsubscriptionRequested( + ChatRoom cr, Address participantAddr) {} + + @Override + public void onUndecryptableMessageReceived(ChatRoom cr, ChatMessage msg) { + final Address from = msg.getFromAddress(); + final LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(from); + + if (LinphoneActivity.instance().isOnBackground()) { + if (!getResources().getBoolean(R.bool.disable_chat_message_notification)) { + if (contact != null) { + LinphoneService.instance() + .getNotificationManager() + .displayMessageNotification( + from.asStringUriOnly(), + contact.getFullName(), + contact.getThumbnailUri(), + getString(R.string.message_cant_be_decrypted_notif), + cr.getLocalAddress(), + msg.getTime(), + null, + null); + } else { + LinphoneService.instance() + .getNotificationManager() + .displayMessageNotification( + from.asStringUriOnly(), + from.getUsername(), + null, + getString(R.string.message_cant_be_decrypted_notif), + cr.getLocalAddress(), + msg.getTime(), + null, + null); + } + } + } else if (LinphoneManager.getLc().limeEnabled() == LimeState.Mandatory) { + final Dialog dialog = + LinphoneActivity.instance() + .displayDialog( + getString(R.string.message_cant_be_decrypted) + .replace( + "%s", + (contact != null) + ? contact.getFullName() + : from.getUsername())); + Button delete = dialog.findViewById(R.id.dialog_delete_button); + delete.setText(getString(R.string.call)); + Button cancel = dialog.findViewById(R.id.dialog_cancel_button); + cancel.setText(getString(R.string.ok)); + delete.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + LinphoneManager.getInstance() + .newOutgoingCall( + from.asStringUriOnly(), + (contact != null) + ? contact.getFullName() + : from.getUsername()); + dialog.dismiss(); + } + }); + + cancel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + dialog.dismiss(); + } + }); + dialog.show(); + } + } + + @Override + public void onChatMessageReceived(ChatRoom cr, EventLog event) { + cr.markAsRead(); + LinphoneManager.getInstance().updateUnreadCountForChatRoom(mChatRoom, 0); + LinphoneActivity.instance().refreshMissedChatCountDisplay(); + + ChatMessage msg = event.getChatMessage(); + if (msg.getErrorInfo() != null + && msg.getErrorInfo().getReason() == Reason.UnsupportedContent) { + Log.w("Message received but content is unsupported, do not display it"); + return; + } + + if (!msg.hasTextContent() && msg.getFileTransferInformation() == null) { + Log.w("Message has no text or file transfer information to display, ignoring it..."); + return; + } + + String externalBodyUrl = msg.getExternalBodyUrl(); + Content fileTransferContent = msg.getFileTransferInformation(); + if (externalBodyUrl != null || fileTransferContent != null) { + LinphoneActivity.instance().checkAndRequestExternalStoragePermission(); + } + + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); + scrollToBottom(); + } + + @Override + public void onIsComposingReceived(ChatRoom cr, Address remoteAddr, boolean isComposing) { + ArrayList composing = new ArrayList<>(); + for (Address a : cr.getComposingAddresses()) { + boolean found = false; + for (LinphoneContact c : mParticipants) { + if (c.hasAddress(a.asStringUriOnly())) { + composing.add(c.getFullName()); + found = true; + break; + } + } + if (!found) { + LinphoneContact contact = + ContactsManager.getInstance().findContactFromAddress(remoteAddr); + String displayName; + if (contact != null) { + if (contact.getFullName() != null) { + displayName = contact.getFullName(); + } else { + displayName = LinphoneUtils.getAddressDisplayName(remoteAddr); + } + } else { + displayName = LinphoneUtils.getAddressDisplayName(a); + } + composing.add(displayName); + } + } + + mRemoteComposing.setVisibility(View.VISIBLE); + if (composing.size() == 0) { + mRemoteComposing.setVisibility(View.GONE); + } else if (composing.size() == 1) { + mRemoteComposing.setText( + getString(R.string.remote_composing_single).replace("%s", composing.get(0))); + } else { + StringBuilder remotes = new StringBuilder(); + int i = 0; + for (String remote : composing) { + remotes.append(remote); + i++; + if (i != composing.size()) { + remotes.append(", "); + } + } + mRemoteComposing.setText( + getString(R.string.remote_composing_multiple) + .replace("%s", remotes.toString())); + } + } + + @Override + public void onMessageReceived(ChatRoom cr, ChatMessage msg) {} + + @Override + public void onConferenceJoined(ChatRoom cr, EventLog event) { + // Currently flexisip doesn't send the participants list in the INVITE + // So we have to refresh the display when information is available + // In the meantime header will be chatroom-xxxxxxx + if (mChatRoom == null) mChatRoom = cr; + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) + && mChatRoom.getParticipants().length > 0) { + mRemoteParticipantAddress = mChatRoom.getParticipants()[0].getAddress(); + } + getContactsForParticipants(); + displayChatRoomHeader(); + + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); + scrollToBottom(); + } + + @Override + public void onConferenceLeft(ChatRoom cr, EventLog event) { + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); + scrollToBottom(); + } + + @Override + public void onParticipantAdminStatusChanged(ChatRoom cr, EventLog event) { + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); + scrollToBottom(); + } + + @Override + public void onParticipantDeviceRemoved(ChatRoom cr, EventLog event) {} + + @Override + public void onParticipantRemoved(ChatRoom cr, EventLog event) { + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; + getContactsForParticipants(); + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); + scrollToBottom(); + } + + @Override + public void onChatMessageShouldBeStored(ChatRoom cr, ChatMessage msg) {} + + @Override + public void onParticipantDeviceAdded(ChatRoom cr, EventLog event) {} + + @Override + public void onSecurityEvent(ChatRoom cr, EventLog eventLog) { + updateSecurityLevelIcon(); + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(eventLog); + scrollToBottom(); + } + + @Override + public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { + if (mChatRoom.hasBeenLeft()) { + setReadOnly(); + } + } + + @Override + public void onParticipantAdded(ChatRoom cr, EventLog event) { + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; + getContactsForParticipants(); + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); + scrollToBottom(); + } + + @Override + public void onSubjectChanged(ChatRoom cr, EventLog event) { + if (mChatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) return; + mRoomLabel.setText(event.getSubject()); + ((ChatMessagesGenericAdapter) mChatEventsList.getAdapter()).addToHistory(event); + scrollToBottom(); + } + + @Override + public void onContactsUpdated() { + getContactsForParticipants(); + } + + @Override + public boolean onCommitContent( + InputContentInfoCompat inputContentInfo, + int flags, + Bundle opts, + String[] contentMimeTypes) { + try { + if (mCurrentInputContentInfo != null) { + mCurrentInputContentInfo.releasePermission(); + } + } catch (Exception e) { + Log.e("[TimelineFragment] releasePermission failed : ", e); + } finally { + mCurrentInputContentInfo = null; + } + + boolean supported = false; + for (final String mimeType : contentMimeTypes) { + if (inputContentInfo.getDescription().hasMimeType(mimeType)) { + supported = true; + break; + } + } + if (!supported) { + return false; + } + + return onCommitContentInternal(inputContentInfo, flags); + } + + private boolean onCommitContentInternal(InputContentInfoCompat inputContentInfo, int flags) { + if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) { + try { + inputContentInfo.requestPermission(); + } catch (Exception e) { + Log.e("[TimelineFragment] requestPermission failed : ", e); + return false; + } + } + + if (inputContentInfo.getContentUri() != null) { + String contentUri = FileUtils.getFilePath(mContext, inputContentInfo.getContentUri()); + addImageToPendingList(contentUri); + } + + mCurrentInputContentInfo = inputContentInfo; + mCurrentFlags = flags; + + return true; + } + + // This is a workaround to prevent a crash from happening while rotating the device + private class LinphoneLinearLayoutManager extends LinearLayoutManager { + LinphoneLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { + super(context, orientation, reverseLayout); + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + try { + super.onLayoutChildren(recycler, state); + } catch (IndexOutOfBoundsException e) { + Log.e("InvalidIndexOutOfBound Exception, probably while rotating the device"); + } + } + } +} diff --git a/src/androidTest/org/linphone/AinitTestEnv.java b/app/src/main/java/org/linphone/chat/ChatMessagesGenericAdapter.java similarity index 51% rename from src/androidTest/org/linphone/AinitTestEnv.java rename to app/src/main/java/org/linphone/chat/ChatMessagesGenericAdapter.java index 1daa95d39..b9e90ffd6 100644 --- a/src/androidTest/org/linphone/AinitTestEnv.java +++ b/app/src/main/java/org/linphone/chat/ChatMessagesGenericAdapter.java @@ -1,8 +1,8 @@ -package org.linphone; +package org.linphone.chat; /* -AinitTestEnv.java -Copyright (C) 2017 Belledonne Communications, Grenoble, France +ChatMessagesGenericAdapter.java +Copyright (C) 2018 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 @@ -19,22 +19,22 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import junit.framework.Assert; +import java.util.ArrayList; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.EventLog; -import android.test.suitebuilder.annotation.LargeTest; -import android.test.suitebuilder.annotation.MediumTest; -import android.test.suitebuilder.annotation.SmallTest; +interface ChatMessagesGenericAdapter { + void addToHistory(EventLog log); -public class AinitTestEnv extends SampleTest { + void addAllToHistory(ArrayList logs); - @SmallTest - @MediumTest - @LargeTest - public void testAInitLinphoneCore() { - LinphoneTestManager.createAndStart(aContext, iContext, 1); + void setContacts(ArrayList participants); - solo.sleep(5000); - Assert.assertEquals(1, LinphoneTestManager.getLc().getProxyConfigList().length); - waitForRegistration(LinphoneTestManager.getLc().getProxyConfigList()[0]); - } + void refresh(EventLog[] history); + + void clear(); + + Object getItem(int i); + + void removeItem(int i); } diff --git a/app/src/main/java/org/linphone/chat/ChatMessagesOldAdapter.java b/app/src/main/java/org/linphone/chat/ChatMessagesOldAdapter.java new file mode 100644 index 000000000..c45eff20a --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatMessagesOldAdapter.java @@ -0,0 +1,662 @@ +package org.linphone.chat; + +/* +ChatMessagesAdapter.java +Copyright (C) 2017 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 static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.MimeTypeMap; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.compatibility.Compatibility; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatMessageListenerStub; +import org.linphone.core.Content; +import org.linphone.core.EventLog; +import org.linphone.core.LimeState; +import org.linphone.core.tools.Log; +import org.linphone.utils.FileUtils; +import org.linphone.utils.LinphoneUtils; +import org.linphone.utils.SelectableAdapter; +import org.linphone.utils.SelectableHelper; +import org.linphone.views.AsyncBitmap; +import org.linphone.views.BitmapWorkerTask; +import org.linphone.views.ContactAvatar; + +public class ChatMessagesOldAdapter extends SelectableAdapter + implements ChatMessagesGenericAdapter { + + private static final int MARGIN_BETWEEN_MESSAGES = 10; + private static final int SIDE_MARGIN = 100; + private final Context mContext; + private List mHistory; + private List mParticipants; + private final int mItemResource; + private Bitmap mDefaultBitmap; + private final ChatMessagesFragment mFragment; + private final ChatMessageListenerStub mListener; + + private final ChatMessageViewHolderClickListener mClickListener; + + public ChatMessagesOldAdapter( + ChatMessagesFragment fragment, + SelectableHelper helper, + int itemResource, + EventLog[] history, + ArrayList participants, + ChatMessageViewHolderClickListener clickListener) { + super(helper); + mFragment = fragment; + mContext = mFragment.getActivity(); + mItemResource = itemResource; + mHistory = new ArrayList<>(Arrays.asList(history)); + Collections.reverse(mHistory); + mParticipants = participants; + mClickListener = clickListener; + mListener = + new ChatMessageListenerStub() { + @Override + public void onFileTransferProgressIndication( + ChatMessage message, Content content, int offset, int total) { + ChatMessageOldViewHolder holder = + (ChatMessageOldViewHolder) message.getUserData(); + if (holder == null) return; + + if (offset == total) { + holder.fileTransferProgressBar.setVisibility(View.GONE); + holder.fileTransferAction.setVisibility(View.GONE); + holder.fileTransferLayout.setVisibility(View.GONE); + + displayAttachedFile(message, holder); + } else { + holder.fileTransferProgressBar.setVisibility(View.VISIBLE); + holder.fileTransferProgressBar.setProgress(offset * 100 / total); + } + } + + @Override + public void onMsgStateChanged(ChatMessage message, ChatMessage.State state) { + if (state == ChatMessage.State.FileTransferDone) { + if (!message.isOutgoing()) { + message.setAppdata(message.getFileTransferFilepath()); + } + message.setFileTransferFilepath( + null); // Not needed anymore, will help differenciate between + // InProgress states for file transfer / message sending + } + for (int i = 0; i < mHistory.size(); i++) { + EventLog log = mHistory.get(i); + if (log.getType() == EventLog.Type.ConferenceChatMessage + && log.getChatMessage() == message) { + notifyItemChanged(i); + break; + } + } + } + }; + } + + @Override + public ChatMessageOldViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(mItemResource, parent, false); + ChatMessageOldViewHolder VH = new ChatMessageOldViewHolder(v, mClickListener); + + // Allows onLongClick ContextMenu on bubbles + mFragment.registerForContextMenu(v); + v.setTag(VH); + return VH; + } + + @Override + public void onBindViewHolder(@NonNull final ChatMessageOldViewHolder holder, int position) { + final EventLog event = mHistory.get(position); + holder.eventLayout.setVisibility(View.GONE); + holder.bubbleLayout.setVisibility(View.GONE); + holder.delete.setVisibility(isEditionEnabled() ? View.VISIBLE : 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.fileName.setVisibility(View.GONE); + holder.openFileButton.setVisibility(View.GONE); + holder.messageStatus.setVisibility(View.INVISIBLE); + holder.messageSendingInProgress.setVisibility(View.GONE); + holder.imdmLayout.setVisibility(View.INVISIBLE); + + if (isEditionEnabled()) { + holder.delete.setOnCheckedChangeListener(null); + holder.delete.setChecked(isSelected(position)); + holder.delete.setTag(position); + } + + if (event.getType() == EventLog.Type.ConferenceChatMessage) { + holder.bubbleLayout.setVisibility(View.VISIBLE); + final ChatMessage message = event.getChatMessage(); + + if (position > 0 + && mContext.getResources() + .getBoolean(R.bool.lower_space_between_chat_bubbles_if_same_person)) { + EventLog previousEvent = (EventLog) getItem(position - 1); + if (previousEvent.getType() == EventLog.Type.ConferenceChatMessage) { + ChatMessage previousMessage = previousEvent.getChatMessage(); + if (previousMessage.getFromAddress().weakEqual(message.getFromAddress())) { + holder.separatorLayout.setVisibility(View.GONE); + } + } else { + // No separator if previous event is not a message + holder.separatorLayout.setVisibility(View.GONE); + } + } + + message.setUserData(holder); + message.addListener(mListener); + + RelativeLayout.LayoutParams layoutParams = + new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT); + + ChatMessage.State status = message.getState(); + Address remoteSender = message.getFromAddress(); + String displayName; + + LinphoneContact contact = null; + if (message.isOutgoing()) { + if (status == ChatMessage.State.InProgress) { + holder.messageSendingInProgress.setVisibility(View.VISIBLE); + } + + if (!message.isSecured() + && LinphoneManager.getLc().limeEnabled() == LimeState.Mandatory + && status != ChatMessage.State.InProgress) { + holder.messageStatus.setVisibility(View.VISIBLE); + holder.messageStatus.setImageResource(R.drawable.chat_unsecure); + } + + if (status == ChatMessage.State.DeliveredToUser) { + holder.imdmLayout.setVisibility(View.VISIBLE); + holder.imdmIcon.setImageResource(R.drawable.imdn_received); + holder.imdmLabel.setText(R.string.delivered); + holder.imdmLabel.setTextColor( + mContext.getResources().getColor(R.color.grey_color)); + } else if (status == ChatMessage.State.Displayed) { + holder.imdmLayout.setVisibility(View.VISIBLE); + holder.imdmIcon.setImageResource(R.drawable.imdn_read); + holder.imdmLabel.setText(R.string.displayed); + holder.imdmLabel.setTextColor( + mContext.getResources().getColor(R.color.imdn_read_color)); + } else if (status == ChatMessage.State.NotDelivered) { + holder.imdmLayout.setVisibility(View.VISIBLE); + holder.imdmIcon.setImageResource(R.drawable.imdn_error); + holder.imdmLabel.setText(R.string.error); + holder.imdmLabel.setTextColor( + mContext.getResources().getColor(R.color.red_color)); + } else if (status == ChatMessage.State.FileTransferError) { + holder.imdmLayout.setVisibility(View.VISIBLE); + holder.imdmIcon.setImageResource(R.drawable.imdn_error); + holder.imdmLabel.setText(R.string.file_transfer_error); + holder.imdmLabel.setTextColor( + mContext.getResources().getColor(R.color.red_color)); + } + + // layoutParams allow bubbles alignment during selection mode + if (isEditionEnabled()) { + layoutParams.addRule(RelativeLayout.LEFT_OF, holder.delete.getId()); + layoutParams.setMargins( + SIDE_MARGIN, + MARGIN_BETWEEN_MESSAGES / 2, + 0, + MARGIN_BETWEEN_MESSAGES / 2); + } else { + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); + layoutParams.setMargins( + SIDE_MARGIN, + MARGIN_BETWEEN_MESSAGES / 2, + 0, + MARGIN_BETWEEN_MESSAGES / 2); + } + + holder.background.setBackgroundResource(R.drawable.resizable_chat_bubble_outgoing); + Compatibility.setTextAppearance(holder.contactName, mContext, R.style.font3); + Compatibility.setTextAppearance( + holder.fileTransferAction, mContext, R.style.font15); + holder.fileTransferAction.setBackgroundResource( + R.drawable.resizable_confirm_delete_button); + } else { + for (LinphoneContact c : mParticipants) { + if (c != null && c.hasAddress(remoteSender.asStringUriOnly())) { + contact = c; + break; + } + } + + if (isEditionEnabled()) { + layoutParams.addRule(RelativeLayout.LEFT_OF, holder.delete.getId()); + layoutParams.setMargins( + SIDE_MARGIN, + MARGIN_BETWEEN_MESSAGES / 2, + 0, + MARGIN_BETWEEN_MESSAGES / 2); + } else { + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT); + layoutParams.setMargins( + 0, + MARGIN_BETWEEN_MESSAGES / 2, + SIDE_MARGIN, + MARGIN_BETWEEN_MESSAGES / 2); + } + + holder.background.setBackgroundResource(R.drawable.resizable_chat_bubble_incoming); + Compatibility.setTextAppearance( + holder.contactName, mContext, R.style.contact_organization_font); + Compatibility.setTextAppearance( + holder.fileTransferAction, mContext, R.style.button_font); + holder.fileTransferAction.setBackgroundResource( + R.drawable.resizable_assistant_button); + } + + if (contact == null) { + contact = ContactsManager.getInstance().findContactFromAddress(remoteSender); + } + if (contact != null) { + if (contact.getFullName() != null) { + displayName = contact.getFullName(); + } else { + displayName = LinphoneUtils.getAddressDisplayName(remoteSender); + } + ContactAvatar.displayAvatar(contact, holder.avatarLayout); + } else { + displayName = LinphoneUtils.getAddressDisplayName(remoteSender); + ContactAvatar.displayAvatar(displayName, holder.avatarLayout); + } + holder.contactName.setText( + LinphoneUtils.timestampToHumanDate( + mContext, message.getTime(), R.string.messages_date_format) + + " - " + + displayName); + + if (message.hasTextContent()) { + String msg = message.getTextContent(); + Spanned text = LinphoneUtils.getTextWithHttpLinks(msg); + holder.messageText.setText(text); + holder.messageText.setMovementMethod(LinkMovementMethod.getInstance()); + holder.messageText.setVisibility(View.VISIBLE); + } + + String externalBodyUrl = message.getExternalBodyUrl(); + Content fileTransferContent = message.getFileTransferInformation(); + + boolean hasFile = message.getAppdata() != null; + boolean hasFileTransfer = externalBodyUrl != null; + for (Content c : message.getContents()) { + if (c.isFile()) { + hasFile = true; + } else if (c.isFileTransfer()) { + hasFileTransfer = true; + } + } + if (hasFile) { // Something to display + displayAttachedFile(message, holder); + } + + if (hasFileTransfer) { // Incoming file transfer not yet downloaded + holder.fileName.setVisibility(View.VISIBLE); + holder.fileName.setText(fileTransferContent.getName()); + + holder.fileTransferLayout.setVisibility(View.VISIBLE); + holder.fileTransferProgressBar.setVisibility(View.GONE); + if (message.isFileTransferInProgress()) { // Incoming file transfer in progress + holder.fileTransferAction.setVisibility(View.GONE); + } else { + holder.fileTransferAction.setText(mContext.getString(R.string.accept)); + holder.fileTransferAction.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mContext.getPackageManager() + .checkPermission( + Manifest.permission + .WRITE_EXTERNAL_STORAGE, + mContext.getPackageName()) + == PackageManager.PERMISSION_GRANTED) { + v.setEnabled(false); + String filename = + message.getFileTransferInformation().getName(); + File file = + new File( + FileUtils.getStorageDirectory(mContext), + filename); + int prefix = 1; + while (file.exists()) { + file = + new File( + FileUtils.getStorageDirectory(mContext), + prefix + "_" + filename); + Log.w( + "File with that name already exists, renamed to " + + prefix + + "_" + + filename); + prefix += 1; + } + 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(); + } + } + }); + } + } else if (message.isFileTransferInProgress()) { // Outgoing file transfer in progress + holder.messageSendingInProgress.setVisibility(View.GONE); + holder.fileTransferLayout.setVisibility(View.VISIBLE); + holder.fileTransferAction.setText(mContext.getString(R.string.cancel)); + holder.fileTransferAction.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + message.cancelFileTransfer(); + notifyItemChanged(holder.getAdapterPosition()); + } + }); + } + + holder.bubbleLayout.setLayoutParams(layoutParams); + } else { // Event is not chat message + holder.eventLayout.setVisibility(View.VISIBLE); + holder.eventMessage.setTextColor( + mContext.getResources().getColor(R.color.light_grey_color)); + holder.eventLayout.setBackgroundResource(R.drawable.event_decoration_gray); + holder.eventLayout.setBackgroundResource(R.drawable.event_decoration_gray); + + Address address = event.getParticipantAddress(); + if (address == null && event.getType() == EventLog.Type.ConferenceSecurityEvent) { + address = event.getSecurityEventFaultyDeviceAddress(); + } + String displayName = ""; + if (address != null) { + LinphoneContact contact = + ContactsManager.getInstance().findContactFromAddress(address); + if (contact != null) { + displayName = contact.getFullName(); + } else { + displayName = LinphoneUtils.getAddressDisplayName(address); + } + } + + switch (event.getType()) { + case ConferenceCreated: + holder.eventMessage.setText(mContext.getString(R.string.conference_created)); + break; + case ConferenceTerminated: + holder.eventMessage.setText(mContext.getString(R.string.conference_destroyed)); + break; + case ConferenceParticipantAdded: + holder.eventMessage.setText( + mContext.getString(R.string.participant_added) + .replace("%s", displayName)); + break; + case ConferenceParticipantRemoved: + holder.eventMessage.setText( + mContext.getString(R.string.participant_removed) + .replace("%s", displayName)); + break; + case ConferenceSubjectChanged: + holder.eventMessage.setText( + mContext.getString(R.string.subject_changed) + .replace("%s", event.getSubject())); + break; + case ConferenceParticipantSetAdmin: + holder.eventMessage.setText( + mContext.getString(R.string.admin_set).replace("%s", displayName)); + break; + case ConferenceParticipantUnsetAdmin: + holder.eventMessage.setText( + mContext.getString(R.string.admin_unset).replace("%s", displayName)); + break; + case ConferenceParticipantDeviceAdded: + holder.eventMessage.setText( + mContext.getString(R.string.device_added).replace("%s", displayName)); + break; + case ConferenceParticipantDeviceRemoved: + holder.eventMessage.setText( + mContext.getString(R.string.device_removed).replace("%s", displayName)); + break; + case ConferenceSecurityEvent: + holder.eventMessage.setTextColor( + mContext.getResources().getColor(R.color.red_color)); + holder.eventLayout.setBackgroundResource(R.drawable.event_decoration_red); + holder.eventLayout.setBackgroundResource(R.drawable.event_decoration_red); + + switch (event.getSecurityEventType()) { + case EncryptionIdentityKeyChanged: + holder.eventMessage.setText( + mContext.getString(R.string.lime_identity_key_changed) + .replace("%s", displayName)); + break; + case ManInTheMiddleDetected: + holder.eventMessage.setText( + mContext.getString(R.string.man_in_the_middle_detected) + .replace("%s", displayName)); + break; + case SecurityLevelDowngraded: + holder.eventMessage.setText( + mContext.getString(R.string.security_level_downgraded) + .replace("%s", displayName)); + break; + case ParticipantMaxDeviceCountExceeded: + holder.eventMessage.setText( + mContext.getString(R.string.participant_max_count_exceeded) + .replace("%s", displayName)); + break; + case None: + default: + break; + } + break; + case None: + default: + holder.eventMessage.setText( + mContext.getString(R.string.unexpected_event) + .replace("%s", displayName) + .replace("%i", String.valueOf(event.getType().toInt()))); + break; + } + } + } + + @Override + public int getItemCount() { + return mHistory.size(); + } + + public void addToHistory(EventLog log) { + mHistory.add(0, log); + notifyItemInserted(0); + } + + public void addAllToHistory(ArrayList logs) { + int currentSize = mHistory.size() - 1; + Collections.reverse(logs); + mHistory.addAll(logs); + notifyItemRangeInserted(currentSize + 1, logs.size()); + } + + public void setContacts(ArrayList participants) { + mParticipants = participants; + } + + public void refresh(EventLog[] history) { + mHistory = new ArrayList<>(Arrays.asList(history)); + Collections.reverse(mHistory); + notifyDataSetChanged(); + } + + public void clear() { + for (EventLog event : mHistory) { + if (event.getType() == EventLog.Type.ConferenceChatMessage) { + ChatMessage message = event.getChatMessage(); + message.removeListener(mListener); + } + } + mHistory.clear(); + } + + public Object getItem(int i) { + return mHistory.get(i); + } + + public void removeItem(int i) { + mHistory.remove(i); + notifyItemRemoved(i); + } + + private void loadBitmap(String path, ImageView imageView) { + if (cancelPotentialWork(path, imageView)) { + mDefaultBitmap = + BitmapFactory.decodeResource(mContext.getResources(), R.drawable.chat_file); + BitmapWorkerTask task = new BitmapWorkerTask(mContext, imageView, mDefaultBitmap); + final AsyncBitmap asyncBitmap = + new AsyncBitmap(mContext.getResources(), mDefaultBitmap, task); + imageView.setImageDrawable(asyncBitmap); + task.execute(path); + } + } + + private void openFile(String path) { + Intent intent = new Intent(Intent.ACTION_VIEW); + File file; + Uri contentUri; + if (path.startsWith("file://")) { + path = path.substring("file://".length()); + file = new File(path); + contentUri = + FileProvider.getUriForFile( + mContext, + mContext.getResources().getString(R.string.file_provider), + file); + } else if (path.startsWith("content://")) { + contentUri = Uri.parse(path); + } else { + file = new File(path); + try { + contentUri = + FileProvider.getUriForFile( + mContext, + mContext.getResources().getString(R.string.file_provider), + file); + } catch (Exception e) { + contentUri = Uri.parse(path); + } + } + String type = null; + String extension = MimeTypeMap.getFileExtensionFromUrl(contentUri.toString()); + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + if (type != null) { + intent.setDataAndType(contentUri, type); + } else { + intent.setDataAndType(contentUri, "*/*"); + } + intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + mContext.startActivity(intent); + } + + private void displayAttachedFile(ChatMessage message, ChatMessageOldViewHolder holder) { + holder.fileName.setVisibility(View.VISIBLE); + + String appData = message.getAppdata(); + if (appData == null) { + for (Content c : message.getContents()) { + if (c.isFile()) { + appData = c.getFilePath(); + } + } + } + + if (appData != null) { + FileUtils.scanFile(message); + holder.fileName.setText(FileUtils.getNameFromFilePath(appData)); + if (FileUtils.isExtensionImage(appData)) { + holder.messageImage.setVisibility(View.VISIBLE); + loadBitmap(appData, holder.messageImage); + holder.messageImage.setTag(appData); + } else { + holder.openFileButton.setVisibility(View.VISIBLE); + holder.openFileButton.setTag(appData); + holder.openFileButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + openFile((String) v.getTag()); + } + }); + } + } + } + + private boolean cancelPotentialWork(String path, ImageView imageView) { + final BitmapWorkerTask 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.equals(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; + } +} diff --git a/app/src/main/java/org/linphone/chat/ChatRoomCreationFragment.java b/app/src/main/java/org/linphone/chat/ChatRoomCreationFragment.java new file mode 100644 index 000000000..7e1be930d --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatRoomCreationFragment.java @@ -0,0 +1,665 @@ +/* +ChatRoomCreationFragment.java +Copyright (C) 2017 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 org.linphone.chat; + +import static android.content.Context.INPUT_METHOD_SERVICE; + +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.CompoundButton; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.SearchView; +import android.widget.Switch; +import android.widget.TextView; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.contacts.ContactAddress; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.ContactsUpdatedListener; +import org.linphone.contacts.LinphoneContact; +import org.linphone.contacts.SearchContactViewHolder; +import org.linphone.contacts.SearchContactsAdapter; +import org.linphone.core.Address; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomBackend; +import org.linphone.core.ChatRoomListenerStub; +import org.linphone.core.ChatRoomParams; +import org.linphone.core.Core; +import org.linphone.core.FriendCapability; +import org.linphone.core.ProxyConfig; +import org.linphone.core.SearchResult; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.LinphonePreferences; +import org.linphone.views.ContactSelectView; + +public class ChatRoomCreationFragment extends Fragment + implements View.OnClickListener, + SearchContactViewHolder.ClickListener, + ContactsUpdatedListener { + private RecyclerView mContactsList; + private LinearLayout mContactsSelectedLayout; + private HorizontalScrollView mContactsSelectLayout; + private ImageView mAllContactsButton, mLinphoneContactsButton, mBackButton, mNextButton; + private boolean mOnlyDisplayLinphoneContacts; + private View mAllContactsSelected, mLinphoneContactsSelected; + private RelativeLayout mSearchLayout, mWaitLayout, mLinphoneContactsToggle, mAllContactsToggle; + private SearchView mSearchField; + private ProgressBar mContactsFetchInProgress; + private SearchContactsAdapter mSearchAdapter; + private String mChatRoomSubject, mChatRoomAddress; + private ChatRoom mChatRoom; + private ChatRoomListenerStub mChatRoomCreationListener; + private Bundle mShareInfos; + private ImageView mSecurityToggleOff, mSecurityToggleOn; + private Switch mSecurityToggle; + private boolean mCreateGroupChatRoom; + private boolean mChatRoomEncrypted; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + View view = inflater.inflate(R.layout.chat_create, container, false); + + ArrayList selectedContacts = new ArrayList<>(); + mChatRoomSubject = null; + mChatRoomAddress = null; + mCreateGroupChatRoom = false; + + if (getArguments() != null) { + if (getArguments().getSerializable("selectedContacts") != null) { + selectedContacts = + (ArrayList) + getArguments().getSerializable("selectedContacts"); + } + mChatRoomSubject = getArguments().getString("subject"); + mChatRoomAddress = getArguments().getString("groupChatRoomAddress"); + mCreateGroupChatRoom = getArguments().getBoolean("createGroupChatRoom", false); + mChatRoomEncrypted = getArguments().getBoolean("encrypted", false); + } + + mWaitLayout = view.findViewById(R.id.waitScreen); + mWaitLayout.setVisibility(View.GONE); + + mContactsList = view.findViewById(R.id.contactsList); + mContactsSelectedLayout = view.findViewById(R.id.contactsSelected); + mContactsSelectLayout = view.findViewById(R.id.layoutContactsSelected); + + mAllContactsButton = view.findViewById(R.id.all_contacts); + mAllContactsButton.setOnClickListener(this); + + mLinphoneContactsButton = view.findViewById(R.id.linphone_contacts); + mLinphoneContactsButton.setOnClickListener(this); + + mAllContactsSelected = view.findViewById(R.id.all_contacts_select); + mLinphoneContactsSelected = view.findViewById(R.id.linphone_contacts_select); + + mBackButton = view.findViewById(R.id.back); + mBackButton.setOnClickListener(this); + + mNextButton = view.findViewById(R.id.next); + mNextButton.setOnClickListener(this); + mNextButton.setEnabled(false); + mSearchLayout = view.findViewById(R.id.layoutSearchField); + + mContactsFetchInProgress = view.findViewById(R.id.contactsFetchInProgress); + mContactsFetchInProgress.setVisibility(View.GONE); + + mSearchAdapter = new SearchContactsAdapter(this, !mCreateGroupChatRoom, mChatRoomEncrypted); + + mSearchField = view.findViewById(R.id.searchField); + mSearchField.setOnQueryTextListener( + new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + mSearchAdapter.searchContacts(newText); + return true; + } + }); + + mLinphoneContactsToggle = view.findViewById(R.id.layout_linphone_contacts); + mAllContactsToggle = view.findViewById(R.id.layout_all_contacts); + + mSecurityToggle = view.findViewById(R.id.security_toogle); + mSecurityToggle.setOnCheckedChangeListener( + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + setSecurityEnabled(isChecked); + } + }); + mSecurityToggleOn = view.findViewById(R.id.security_toogle_on); + mSecurityToggleOff = view.findViewById(R.id.security_toogle_off); + mSecurityToggleOn.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + setSecurityEnabled(true); + } + }); + mSecurityToggleOff.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + setSecurityEnabled(false); + } + }); + + mSecurityToggle.setChecked(mChatRoomEncrypted); + mSearchAdapter.setSecurityEnabled(mChatRoomEncrypted); + ProxyConfig lpc = LinphoneManager.getLc().getDefaultProxyConfig(); + if ((mChatRoomSubject != null && mChatRoomAddress != null) + || (lpc == null || lpc.getConferenceFactoryUri() == null)) { + mSecurityToggle.setVisibility(View.GONE); + mSecurityToggleOn.setVisibility(View.GONE); + mSecurityToggleOff.setVisibility(View.GONE); + } + + LinearLayoutManager layoutManager = + new LinearLayoutManager(getActivity().getApplicationContext()); + + mContactsList.setAdapter(mSearchAdapter); + + DividerItemDecoration dividerItemDecoration = + new DividerItemDecoration( + mContactsList.getContext(), layoutManager.getOrientation()); + dividerItemDecoration.setDrawable( + getActivity() + .getApplicationContext() + .getResources() + .getDrawable(R.drawable.divider)); + mContactsList.addItemDecoration(dividerItemDecoration); + + mContactsList.setLayoutManager(layoutManager); + + if (savedInstanceState != null + && savedInstanceState.getStringArrayList("selectedContacts") != null) { + mContactsSelectedLayout.removeAllViews(); + // We need to get all contacts not only sip + selectedContacts = + (ArrayList) + savedInstanceState.getSerializable("selectedContacts"); + } + + if (selectedContacts.size() != 0) { + mSearchAdapter.setContactsSelectedList(selectedContacts); + updateList(); + updateListSelected(); + } + + mOnlyDisplayLinphoneContacts = + ContactsManager.getInstance().isLinphoneContactsPrefered() + || getResources().getBoolean(R.bool.hide_non_linphone_contacts); + if (savedInstanceState != null) { + mOnlyDisplayLinphoneContacts = + savedInstanceState.getBoolean("onlySipContact", mOnlyDisplayLinphoneContacts); + } + mSearchAdapter.setOnlySipContact(mOnlyDisplayLinphoneContacts); + updateList(); + + displayChatCreation(); + + mChatRoomCreationListener = + new ChatRoomListenerStub() { + @Override + public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { + if (newState == ChatRoom.State.Created) { + mWaitLayout.setVisibility(View.GONE); + LinphoneActivity.instance() + .goToChat( + cr.getLocalAddress().asStringUriOnly(), + cr.getPeerAddress().asStringUriOnly(), + mShareInfos); + } else if (newState == ChatRoom.State.CreationFailed) { + mWaitLayout.setVisibility(View.GONE); + LinphoneActivity.instance().displayChatRoomError(); + Log.e( + "[Chat Room Creation] Group chat room for address " + + cr.getPeerAddress() + + " has failed !"); + } + } + }; + + if (getArguments() != null) { + String fileSharedUri = getArguments().getString("fileSharedUri"); + String messageDraft = getArguments().getString("messageDraft"); + + if (fileSharedUri != null || messageDraft != null) { + Log.i("[ChatRoomCreation] Forwarding arguments to new chat room"); + mShareInfos = new Bundle(); + } + + if (fileSharedUri != null) { + LinphoneActivity.instance().checkAndRequestPermissionsToSendImage(); + mShareInfos.putString("fileSharedUri", fileSharedUri); + } + + if (messageDraft != null) mShareInfos.putString("messageDraft", messageDraft); + } + + return view; + } + + @Override + public void onResume() { + ContactsManager.getInstance().addContactsListener(this); + super.onResume(); + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.CREATE_CHAT); + } + + InputMethodManager inputMethodManager = + (InputMethodManager) getActivity().getSystemService(INPUT_METHOD_SERVICE); + if (getActivity().getCurrentFocus() != null) { + inputMethodManager.hideSoftInputFromWindow( + getActivity().getCurrentFocus().getWindowToken(), 0); + } + } + + @Override + public void onPause() { + if (mChatRoom != null) { + mChatRoom.removeListener(mChatRoomCreationListener); + } + ContactsManager.getInstance().removeContactsListener(this); + super.onPause(); + } + + private void setSecurityEnabled(boolean enabled) { + mChatRoomEncrypted = enabled; + mSecurityToggle.setChecked(mChatRoomEncrypted); + mSearchAdapter.setSecurityEnabled(mChatRoomEncrypted); + + if (enabled) { + // Remove all contacts added before LIME switch was set + // and that can stay because they don't have the capability + mContactsSelectedLayout.removeAllViews(); + List toToggle = new ArrayList<>(); + for (ContactAddress ca : mSearchAdapter.getContactsSelectedList()) { + // If the ContactAddress doesn't have a contact keep it anyway + if (ca.getContact() != null && !ca.hasCapability(FriendCapability.LimeX3Dh)) { + toToggle.add(ca); + } else { + if (ca.getView() != null) { + mContactsSelectedLayout.addView(ca.getView()); + } + } + } + for (ContactAddress ca : toToggle) { + mSearchAdapter.toggleContactSelection(ca); + } + mContactsSelectedLayout.invalidate(); + } + } + + private void displayChatCreation() { + mNextButton.setVisibility(View.VISIBLE); + mNextButton.setEnabled(mSearchAdapter.getContactsSelectedList().size() > 0); + + mContactsList.setVisibility(View.VISIBLE); + mSearchLayout.setVisibility(View.VISIBLE); + + if (mCreateGroupChatRoom) { + mLinphoneContactsToggle.setVisibility(View.GONE); + mAllContactsToggle.setVisibility(View.GONE); + mContactsSelectLayout.setVisibility(View.VISIBLE); + mNextButton.setVisibility(View.VISIBLE); + } else { + mLinphoneContactsToggle.setVisibility(View.VISIBLE); + mAllContactsToggle.setVisibility(View.VISIBLE); + mContactsSelectLayout.setVisibility(View.GONE); + mNextButton.setVisibility(View.GONE); + } + + if (getResources().getBoolean(R.bool.hide_non_linphone_contacts)) { + mLinphoneContactsToggle.setVisibility(View.GONE); + mLinphoneContactsButton.setVisibility(View.INVISIBLE); + + mAllContactsButton.setEnabled(false); + mLinphoneContactsButton.setEnabled(false); + + mOnlyDisplayLinphoneContacts = true; + + mAllContactsButton.setOnClickListener(null); + mLinphoneContactsButton.setOnClickListener(null); + + mLinphoneContactsSelected.setVisibility(View.INVISIBLE); + mLinphoneContactsSelected.setVisibility(View.INVISIBLE); + } else { + mAllContactsButton.setVisibility(View.VISIBLE); + mLinphoneContactsButton.setVisibility(View.VISIBLE); + + if (mOnlyDisplayLinphoneContacts) { + mAllContactsSelected.setVisibility(View.INVISIBLE); + mLinphoneContactsSelected.setVisibility(View.VISIBLE); + } else { + mAllContactsSelected.setVisibility(View.VISIBLE); + mLinphoneContactsSelected.setVisibility(View.INVISIBLE); + } + + mAllContactsButton.setEnabled(mOnlyDisplayLinphoneContacts); + mLinphoneContactsButton.setEnabled(!mAllContactsButton.isEnabled()); + } + + mContactsSelectedLayout.removeAllViews(); + if (mSearchAdapter.getContactsSelectedList().size() > 0) { + for (ContactAddress ca : mSearchAdapter.getContactsSelectedList()) { + addSelectedContactAddress(ca); + } + } + } + + private void updateList() { + mSearchAdapter.searchContacts(mSearchField.getQuery().toString()); + mSearchAdapter.notifyDataSetChanged(); + } + + private void updateListSelected() { + if (mSearchAdapter.getContactsSelectedList().size() > 0) { + mContactsSelectLayout.invalidate(); + mNextButton.setEnabled(true); + } else { + mNextButton.setEnabled(false); + } + } + + private void resetAndResearch() { + ContactsManager.getInstance().getMagicSearch().resetSearchCache(); + mSearchAdapter.searchContacts(mSearchField.getQuery().toString()); + } + + private void addSelectedContactAddress(ContactAddress ca) { + View viewContact = + LayoutInflater.from(LinphoneActivity.instance()) + .inflate(R.layout.contact_selected, null); + if (ca.getContact() != null) { + String name = + (ca.getContact().getFullName() != null + && !ca.getContact().getFullName().isEmpty()) + ? ca.getContact().getFullName() + : (ca.getDisplayName() != null) + ? ca.getDisplayName() + : (ca.getUsername() != null) ? ca.getUsername() : ""; + ((TextView) viewContact.findViewById(R.id.sipUri)).setText(name); + } else { + ((TextView) viewContact.findViewById(R.id.sipUri)) + .setText(ca.getAddressAsDisplayableString()); + } + View removeContact = viewContact.findViewById(R.id.contactChatDelete); + removeContact.setTag(ca); + removeContact.setOnClickListener(this); + viewContact.setOnClickListener(this); + ca.setView(viewContact); + mContactsSelectedLayout.addView(viewContact); + mContactsSelectedLayout.invalidate(); + } + + private void updateContactsClick(ContactAddress ca) { + boolean isSelected = mSearchAdapter.toggleContactSelection(ca); + if (isSelected) { + ContactSelectView csv = new ContactSelectView(LinphoneActivity.instance()); + csv.setListener(this); + csv.setContactName(ca); + addSelectedContactAddress(ca); + } else { + mContactsSelectedLayout.removeAllViews(); + for (ContactAddress contactAddress : mSearchAdapter.getContactsSelectedList()) { + if (contactAddress.getView() != null) + mContactsSelectedLayout.addView(contactAddress.getView()); + } + } + mContactsSelectedLayout.invalidate(); + } + + private void addOrRemoveContactFromSelection(ContactAddress ca) { + updateContactsClick(ca); + mSearchAdapter.notifyDataSetChanged(); + updateListSelected(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + if (mSearchAdapter.getContactsSelectedList().size() > 0) { + outState.putSerializable("selectedContacts", mSearchAdapter.getContactsSelectedList()); + } + outState.putBoolean("onlySipContact", mOnlyDisplayLinphoneContacts); + super.onSaveInstanceState(outState); + } + + @Override + public void onClick(View view) { + int id = view.getId(); + if (id == R.id.all_contacts) { + mOnlyDisplayLinphoneContacts = false; + mSearchAdapter.setOnlySipContact(mOnlyDisplayLinphoneContacts); + mAllContactsSelected.setVisibility(View.VISIBLE); + mAllContactsButton.setEnabled(false); + mLinphoneContactsButton.setEnabled(true); + mLinphoneContactsSelected.setVisibility(View.INVISIBLE); + updateList(); + resetAndResearch(); + } else if (id == R.id.linphone_contacts) { + mSearchAdapter.setOnlySipContact(true); + mLinphoneContactsSelected.setVisibility(View.VISIBLE); + mLinphoneContactsButton.setEnabled(false); + mOnlyDisplayLinphoneContacts = true; + mAllContactsButton.setEnabled(mOnlyDisplayLinphoneContacts); + mAllContactsSelected.setVisibility(View.INVISIBLE); + updateList(); + resetAndResearch(); + } else if (id == R.id.back) { + if (LinphoneActivity.instance().isTablet()) { + LinphoneActivity.instance().goToChatList(); + } else { + mContactsSelectedLayout.removeAllViews(); + LinphoneActivity.instance().popBackStack(); + } + } else if (id == R.id.next) { + if (mChatRoomAddress == null && mChatRoomSubject == null) { + mContactsSelectedLayout.removeAllViews(); + LinphoneActivity.instance() + .goToChatGroupInfos( + null, + mSearchAdapter.getContactsSelectedList(), + null, + true, + false, + mShareInfos, + mSecurityToggle.isChecked()); + } else { + LinphoneActivity.instance() + .goToChatGroupInfos( + mChatRoomAddress, + mSearchAdapter.getContactsSelectedList(), + mChatRoomSubject, + true, + true, + mShareInfos, + mSecurityToggle.isChecked()); + } + } else if (id == R.id.clearSearchField) { + mSearchField.setQuery("", false); + mSearchAdapter.searchContacts(""); + } else if (id == R.id.contactChatDelete) { + ContactAddress ca = (ContactAddress) view.getTag(); + addOrRemoveContactFromSelection(ca); + } + } + + @Override + public void onItemClicked(int position) { + SearchResult searchResult = mSearchAdapter.getContacts().get(position); + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + ProxyConfig lpc = lc.getDefaultProxyConfig(); + boolean createEncryptedChatRoom = mSecurityToggle.isChecked(); + + if (createEncryptedChatRoom && !searchResult.hasCapability(FriendCapability.LimeX3Dh)) { + Log.w( + "[Chat Room Creation] Contact " + + searchResult.getFriend() + + " doesn't have LIME X3DH capability !"); + return; + } else if (mCreateGroupChatRoom + && !searchResult.hasCapability(FriendCapability.GroupChat)) { + Log.w( + "[Chat Room Creation] Contact " + + searchResult.getFriend() + + " doesn't have group chat capability !"); + return; + } + + if (lpc == null || lpc.getConferenceFactoryUri() == null || !mCreateGroupChatRoom) { + Address address = searchResult.getAddress(); + if (address == null) { + Log.w( + "[Chat Room Creation] Using search result without an address, trying with phone number..."); + address = lc.interpretUrl(searchResult.getPhoneNumber()); + } + if (address == null) { + Log.e("[Chat Room Creation] Can't create a chat room without a valid address !"); + return; + } + if (lpc != null && lpc.getIdentityAddress().weakEqual(address)) { + Log.e("[Chat Room Creation] Can't create a 1-to-1 chat room with myself !"); + return; + } + + if (createEncryptedChatRoom && lpc != null && lpc.getConferenceFactoryUri() != null) { + mChatRoom = lc.findOneToOneChatRoom(lpc.getIdentityAddress(), address, true); + if (mChatRoom != null) { + LinphoneActivity.instance() + .goToChat( + mChatRoom.getLocalAddress().asStringUriOnly(), + mChatRoom.getPeerAddress().asStringUriOnly(), + mShareInfos); + } else { + ChatRoomParams params = lc.createDefaultChatRoomParams(); + // This will set the backend to FlexisipChat automatically + params.enableEncryption(true); + params.enableGroup(false); + + Address participants[] = new Address[1]; + participants[0] = address; + + mChatRoom = + lc.createChatRoom( + params, + getString(R.string.dummy_group_chat_subject), + participants); + if (mChatRoom != null) { + mChatRoom.addListener(mChatRoomCreationListener); + } else { + Log.w("[Chat Room Creation Fragment] createChatRoom returned null..."); + mWaitLayout.setVisibility(View.GONE); + } + } + } else { + if (lpc != null + && lpc.getConferenceFactoryUri() != null + && !LinphonePreferences.instance().useBasicChatRoomFor1To1()) { + mChatRoom = lc.findOneToOneChatRoom(lpc.getIdentityAddress(), address, false); + if (mChatRoom == null) { + mWaitLayout.setVisibility(View.VISIBLE); + + ChatRoomParams params = lc.createDefaultChatRoomParams(); + params.enableEncryption(false); + params.enableGroup(false); + // We don't want a basic chat room + params.setBackend(ChatRoomBackend.FlexisipChat); + + Address participants[] = new Address[1]; + participants[0] = address; + + mChatRoom = + lc.createChatRoom( + params, + getString(R.string.dummy_group_chat_subject), + participants); + if (mChatRoom != null) { + mChatRoom.addListener(mChatRoomCreationListener); + } else { + Log.w("[Chat Room Creation Fragment] createChatRoom returned null..."); + mWaitLayout.setVisibility(View.GONE); + } + } else { + LinphoneActivity.instance() + .goToChat( + mChatRoom.getLocalAddress().asStringUriOnly(), + mChatRoom.getPeerAddress().asStringUriOnly(), + mShareInfos); + } + } else { + ChatRoom chatRoom = lc.getChatRoom(address); + LinphoneActivity.instance() + .goToChat( + chatRoom.getLocalAddress().asStringUriOnly(), + chatRoom.getPeerAddress().asStringUriOnly(), + mShareInfos); + } + } + } else { + LinphoneContact c = + searchResult.getFriend() != null + ? (LinphoneContact) searchResult.getFriend().getUserData() + : null; + if (c == null) { + c = ContactsManager.getInstance().findContactFromAddress(searchResult.getAddress()); + if (c == null) { + c = + ContactsManager.getInstance() + .findContactFromPhoneNumber(searchResult.getPhoneNumber()); + } + } + addOrRemoveContactFromSelection( + new ContactAddress( + c, + searchResult.getAddress().asStringUriOnly(), + searchResult.getPhoneNumber(), + searchResult.getFriend() != null)); + } + } + + @Override + public void onContactsUpdated() { + updateList(); + } +} diff --git a/app/src/main/java/org/linphone/chat/ChatRoomViewHolder.java b/app/src/main/java/org/linphone/chat/ChatRoomViewHolder.java new file mode 100644 index 000000000..3c3df2f2e --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatRoomViewHolder.java @@ -0,0 +1,198 @@ +/* +ChatRoomViewHolder.java +Copyright (C) 2017 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 org.linphone.chat; + +import android.content.Context; +import android.view.View; +import android.widget.CheckBox; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import org.linphone.R; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomCapabilities; +import org.linphone.core.Content; +import org.linphone.core.Participant; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; + +public class ChatRoomViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener, View.OnLongClickListener { + public final TextView lastMessageView; + public final TextView date; + public final TextView displayName; + public final TextView unreadMessages; + public final CheckBox delete; + public final RelativeLayout avatarLayout; + + private final Context mContext; + private final ClickListener mListener; + + public ChatRoomViewHolder(Context context, View itemView, ClickListener listener) { + super(itemView); + + mContext = context; + lastMessageView = itemView.findViewById(R.id.lastMessage); + date = itemView.findViewById(R.id.date); + displayName = itemView.findViewById(R.id.sipUri); + unreadMessages = itemView.findViewById(R.id.unreadMessages); + delete = itemView.findViewById(R.id.delete_chatroom); + avatarLayout = itemView.findViewById(R.id.avatar_layout); + mListener = listener; + + itemView.setOnClickListener(this); + itemView.setOnLongClickListener(this); + } + + public void bindChatRoom(ChatRoom room) { + ChatMessage lastMessage = room.getLastMessageInHistory(); + + if (lastMessage != null) { + StringBuilder messageContent = new StringBuilder(); + for (Content c : lastMessage.getContents()) { + if (c.isFile() || c.isFileTransfer()) { + messageContent.append(c.getName()).append(" "); + } else if (c.isText()) { + messageContent.insert(0, c.getStringBuffer() + " "); + } + } + lastMessageView.setText(getSender(room) + messageContent); + date.setText( + LinphoneUtils.timestampToHumanDate( + mContext, + room.getLastUpdateTime(), + R.string.messages_list_date_format)); + } else { + date.setText(""); + lastMessageView.setText(""); + } + + displayName.setText(getContact(room)); + unreadMessages.setText(String.valueOf(room.getUnreadMessagesCount())); + getAvatar(room); + } + + public void onClick(View v) { + if (mListener != null) { + mListener.onItemClicked(getAdapterPosition()); + } + } + + public boolean onLongClick(View v) { + if (mListener != null) { + return mListener.onItemLongClicked(getAdapterPosition()); + } + return false; + } + + public String getSender(ChatRoom mRoom) { + if (mRoom.getLastMessageInHistory() != null) { + LinphoneContact contact = + ContactsManager.getInstance() + .findContactFromAddress( + mRoom.getLastMessageInHistory().getFromAddress()); + if (contact != null) { + return (contact.getFullName() + mContext.getString(R.string.separator)); + } + return (LinphoneUtils.getAddressDisplayName( + mRoom.getLastMessageInHistory().getFromAddress()) + + mContext.getString(R.string.separator)); + } + return null; + } + + public String getContact(ChatRoom mRoom) { + Address contactAddress = mRoom.getPeerAddress(); + if (mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) + && mRoom.getParticipants().length > 0) { + contactAddress = mRoom.getParticipants()[0].getAddress(); + } + + if (mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + LinphoneContact contact; + contact = ContactsManager.getInstance().findContactFromAddress(contactAddress); + if (contact != null) { + return contact.getFullName(); + } + return LinphoneUtils.getAddressDisplayName(contactAddress); + } + return mRoom.getSubject(); + } + + public void getAvatar(ChatRoom mRoom) { + if (mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + LinphoneContact contact = null; + if (mRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())) { + contact = + ContactsManager.getInstance() + .findContactFromAddress(mRoom.getPeerAddress()); + } else { + Participant[] participants = mRoom.getParticipants(); + if (participants != null && participants.length > 0) { + contact = + ContactsManager.getInstance() + .findContactFromAddress(participants[0].getAddress()); + } + } + if (contact != null) { + if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { + ContactAvatar.displayAvatar(contact, mRoom.getSecurityLevel(), avatarLayout); + } else { + ContactAvatar.displayAvatar(contact, avatarLayout); + } + } else { + Address remoteAddr = null; + if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { + Participant[] participants = mRoom.getParticipants(); + if (participants.length > 0) { + remoteAddr = participants[0].getAddress(); + } else { + // TODO: error + } + } else { + remoteAddr = mRoom.getPeerAddress(); + } + + String username = LinphoneUtils.getAddressDisplayName(remoteAddr); + if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { + ContactAvatar.displayAvatar(username, mRoom.getSecurityLevel(), avatarLayout); + } else { + ContactAvatar.displayAvatar(username, avatarLayout); + } + } + } else { + if (mRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) { + ContactAvatar.displayGroupChatAvatar(mRoom.getSecurityLevel(), avatarLayout); + } else { + ContactAvatar.displayGroupChatAvatar(avatarLayout); + } + } + } + + public interface ClickListener { + void onItemClicked(int position); + + boolean onItemLongClicked(int position); + } +} diff --git a/src/android/org/linphone/chat/ChatRoomsAdapter.java b/app/src/main/java/org/linphone/chat/ChatRoomsAdapter.java similarity index 58% rename from src/android/org/linphone/chat/ChatRoomsAdapter.java rename to app/src/main/java/org/linphone/chat/ChatRoomsAdapter.java index 7653da095..ed54cbfcc 100644 --- a/src/android/org/linphone/chat/ChatRoomsAdapter.java +++ b/app/src/main/java/org/linphone/chat/ChatRoomsAdapter.java @@ -23,25 +23,29 @@ import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - -import org.linphone.LinphoneManager; -import org.linphone.core.ChatRoom; -import org.linphone.ui.SelectableAdapter; -import org.linphone.ui.SelectableHelper; - -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.ChatRoom; +import org.linphone.utils.LinphoneUtils; +import org.linphone.utils.SelectableAdapter; +import org.linphone.utils.SelectableHelper; public class ChatRoomsAdapter extends SelectableAdapter { - private Context mContext; - public List mRooms; - private int mItemResource; - private ChatRoomViewHolder.ClickListener mClickListener; + private final Context mContext; + private List mRooms; + private final int mItemResource; + private final ChatRoomViewHolder.ClickListener mClickListener; - public ChatRoomsAdapter(Context context, int itemResource, List rooms, ChatRoomViewHolder.ClickListener clickListener, SelectableHelper helper) { + public ChatRoomsAdapter( + Context context, + int itemResource, + List rooms, + ChatRoomViewHolder.ClickListener clickListener, + SelectableHelper helper) { super(helper); mClickListener = clickListener; mRooms = rooms; @@ -51,8 +55,7 @@ public class ChatRoomsAdapter extends SelectableAdapter { @Override public ChatRoomViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(mItemResource, parent, false); + View view = LayoutInflater.from(parent.getContext()).inflate(mItemResource, parent, false); return new ChatRoomViewHolder(mContext, view, mClickListener); } @@ -60,21 +63,33 @@ public class ChatRoomsAdapter extends SelectableAdapter { public void onBindViewHolder(ChatRoomViewHolder holder, int position) { ChatRoom room = mRooms.get(position); holder.delete.setVisibility(isEditionEnabled() ? View.VISIBLE : View.INVISIBLE); - holder.unreadMessages.setVisibility(isEditionEnabled() ? View.INVISIBLE : (room.getUnreadMessagesCount() > 0 ? View.VISIBLE : View.INVISIBLE)); + holder.unreadMessages.setVisibility( + isEditionEnabled() + ? View.INVISIBLE + : (room.getUnreadMessagesCount() > 0 ? View.VISIBLE : View.INVISIBLE)); holder.delete.setChecked(isSelected(position)); holder.bindChatRoom(room); } public void refresh() { - mRooms = new ArrayList<>(Arrays.asList(LinphoneManager.getLc().getChatRooms())); - Collections.sort(mRooms, new Comparator() { - public int compare(ChatRoom cr1, ChatRoom cr2) { - long timeDiff = cr1.getLastUpdateTime() - cr2.getLastUpdateTime(); - if (timeDiff > 0) return -1; - else if (timeDiff == 0) return 0; - return 1; - } - }); + ChatRoom[] rooms = LinphoneManager.getLc().getChatRooms(); + if (mContext.getResources().getBoolean(R.bool.hide_empty_one_to_one_chat_rooms)) { + mRooms = LinphoneUtils.removeEmptyOneToOneChatRooms(rooms); + } else { + mRooms = Arrays.asList(rooms); + } + + Collections.sort( + mRooms, + new Comparator() { + public int compare(ChatRoom cr1, ChatRoom cr2) { + long timeDiff = cr1.getLastUpdateTime() - cr2.getLastUpdateTime(); + if (timeDiff > 0) return -1; + else if (timeDiff == 0) return 0; + return 1; + } + }); + notifyDataSetChanged(); } @@ -83,10 +98,7 @@ public class ChatRoomsAdapter extends SelectableAdapter { notifyDataSetChanged(); } - /** - * Adapter's methods - */ - + /** Adapter's methods */ @Override public int getItemCount() { return mRooms.size(); diff --git a/app/src/main/java/org/linphone/chat/ChatRoomsFragment.java b/app/src/main/java/org/linphone/chat/ChatRoomsFragment.java new file mode 100644 index 000000000..d339ff346 --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatRoomsFragment.java @@ -0,0 +1,345 @@ +/* +ChatRoomsFragment.java +Copyright (C) 2017 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 org.linphone.chat; + +import static org.linphone.fragments.FragmentsAvailable.CHAT_LIST; + +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.io.File; +import java.util.Arrays; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.ContactsUpdatedListener; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomListenerStub; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.EventLog; +import org.linphone.core.ProxyConfig; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.utils.LinphoneUtils; +import org.linphone.utils.SelectableHelper; + +public class ChatRoomsFragment extends Fragment + implements ContactsUpdatedListener, + ChatRoomViewHolder.ClickListener, + SelectableHelper.DeleteListener { + + private RecyclerView mChatRoomsList; + private ImageView mNewDiscussionButton, mNewGroupDiscussionButton, mBackToCallButton; + private ChatRoomsAdapter mChatRoomsAdapter; + private CoreListenerStub mListener; + private RelativeLayout mWaitLayout; + private int mChatRoomDeletionPendingCount; + private ChatRoomListenerStub mChatRoomListener; + private Context mContext; + private List mRooms; + private SelectableHelper mSelectionHelper; + private TextView mNoChatHistory; + + @Override + public View onCreateView( + final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mContext = getActivity().getApplicationContext(); + View view = inflater.inflate(R.layout.chatlist, container, false); + + mChatRoomsList = view.findViewById(R.id.chatList); + mWaitLayout = view.findViewById(R.id.waitScreen); + mNewDiscussionButton = view.findViewById(R.id.new_discussion); + mNewGroupDiscussionButton = view.findViewById(R.id.new_group_discussion); + mBackToCallButton = view.findViewById(R.id.back_in_call); + mNoChatHistory = view.findViewById(R.id.noChatHistory); + + ChatRoom[] rooms = LinphoneManager.getLc().getChatRooms(); + if (mContext.getResources().getBoolean(R.bool.hide_empty_one_to_one_chat_rooms)) { + mRooms = LinphoneUtils.removeEmptyOneToOneChatRooms(rooms); + } else { + mRooms = Arrays.asList(rooms); + } + + mSelectionHelper = new SelectableHelper(view, this); + mChatRoomsAdapter = + new ChatRoomsAdapter( + mContext, R.layout.chatlist_cell, mRooms, this, mSelectionHelper); + + mChatRoomsList.setAdapter(mChatRoomsAdapter); + mSelectionHelper.setAdapter(mChatRoomsAdapter); + mSelectionHelper.setDialogMessage(R.string.chat_room_delete_dialog); + + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(mContext); + mChatRoomsList.setLayoutManager(layoutManager); + + DividerItemDecoration dividerItemDecoration = + new DividerItemDecoration( + mChatRoomsList.getContext(), + ((LinearLayoutManager) layoutManager).getOrientation()); + dividerItemDecoration.setDrawable( + getActivity() + .getApplicationContext() + .getResources() + .getDrawable(R.drawable.divider)); + mChatRoomsList.addItemDecoration(dividerItemDecoration); + + mWaitLayout.setVisibility(View.GONE); + + mNewDiscussionButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + Bundle extras = null; + if (getArguments() != null) { + Log.i("[ChatRooms] Forwarding arguments to new chat room"); + extras = (Bundle) getArguments().clone(); + getArguments().clear(); + } + LinphoneActivity.instance() + .goToChatCreator(null, null, null, false, extras, false, false); + } + }); + + mNewGroupDiscussionButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + Bundle extras = null; + if (getArguments() != null) { + Log.i("[ChatRooms] Forwarding arguments to new group chat room"); + extras = (Bundle) getArguments().clone(); + getArguments().clear(); + } + LinphoneActivity.instance() + .goToChatCreator(null, null, null, false, extras, true, false); + } + }); + + mBackToCallButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + LinphoneActivity.instance() + .resetClassicMenuLayoutAndGoBackToCallIfStillRunning(); + } + }); + + mListener = + new CoreListenerStub() { + @Override + public void onMessageReceived(Core lc, ChatRoom cr, ChatMessage message) { + refreshChatRoomsList(); + } + + @Override + public void onChatRoomStateChanged(Core lc, ChatRoom cr, ChatRoom.State state) { + if (state == ChatRoom.State.Created) { + refreshChatRoomsList(); + } + } + }; + + mChatRoomListener = + new ChatRoomListenerStub() { + @Override + public void onStateChanged(ChatRoom room, ChatRoom.State state) { + super.onStateChanged(room, state); + if (state == ChatRoom.State.Deleted + || state == ChatRoom.State.TerminationFailed) { + mChatRoomDeletionPendingCount -= 1; + + if (state == ChatRoom.State.TerminationFailed) { + // TODO error message + } + + if (mChatRoomDeletionPendingCount == 0) { + mWaitLayout.setVisibility(View.GONE); + refreshChatRoomsList(); + } + } + } + }; + + if (getArguments() != null) { + String fileSharedUri = getArguments().getString("fileSharedUri"); + String messageSharedUri = getArguments().getString("messageDraft"); + if (fileSharedUri != null || messageSharedUri != null) { + Toast.makeText( + LinphoneActivity.instance(), + R.string.toast_choose_chat_room_for_sharing, + Toast.LENGTH_LONG) + .show(); + } + Log.i("[ChatRooms] Arguments found: " + messageSharedUri + " / " + fileSharedUri); + } + + return view; + } + + @Override + public void onItemClicked(int position) { + if (mChatRoomsAdapter.isEditionEnabled()) { + mChatRoomsAdapter.toggleSelection(position); + } else { + ChatRoom room = (ChatRoom) mChatRoomsAdapter.getItem(position); + Bundle extras = null; + if (getArguments() != null) { + Log.i("[ChatRooms] Forwarding arguments to existing chat room"); + extras = (Bundle) getArguments().clone(); + getArguments().clear(); + } + LinphoneActivity.instance() + .goToChat( + room.getLocalAddress().asStringUriOnly(), + room.getPeerAddress().asString(), + extras); + } + } + + @Override + public boolean onItemLongClicked(int position) { + if (!mChatRoomsAdapter.isEditionEnabled()) { + mSelectionHelper.enterEditionMode(); + } + mChatRoomsAdapter.toggleSelection(position); + return true; + } + + private void refreshChatRoomsList() { + mChatRoomsAdapter.refresh(); + mNoChatHistory.setVisibility( + mChatRoomsAdapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + } + + public void displayFirstChat() { + ChatRoomsAdapter adapter = (ChatRoomsAdapter) mChatRoomsList.getAdapter(); + if (adapter != null && adapter.getItemCount() > 0) { + ChatRoom room = (ChatRoom) adapter.getItem(0); + LinphoneActivity.instance() + .goToChat( + room.getLocalAddress().asStringUriOnly(), + room.getPeerAddress().asStringUriOnly(), + null); + } else { + LinphoneActivity.instance().displayEmptyFragment(); + } + } + + public void invalidate() { + if (mChatRoomsAdapter != null) { + mChatRoomsAdapter.notifyDataSetChanged(); + } + } + + @Override + public void onResume() { + super.onResume(); + ContactsManager.getInstance().addContactsListener(this); + + if (LinphoneManager.getLc().getCallsNb() > 0) { + mBackToCallButton.setVisibility(View.VISIBLE); + } else { + mBackToCallButton.setVisibility(View.INVISIBLE); + } + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.CHAT_LIST); + } + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + } + + refreshChatRoomsList(); + + ProxyConfig lpc = lc.getDefaultProxyConfig(); + mNewGroupDiscussionButton.setVisibility( + (lpc != null && lpc.getConferenceFactoryUri() != null) ? View.VISIBLE : View.GONE); + } + + @Override + public void onPause() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + } + ContactsManager.getInstance().removeContactsListener(this); + mChatRoomsAdapter.clear(); + super.onPause(); + } + + @Override + public void onDeleteSelection(Object[] objectsToDelete) { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + mChatRoomDeletionPendingCount = objectsToDelete.length; + for (Object obj : objectsToDelete) { + ChatRoom room = (ChatRoom) obj; + + for (EventLog eventLog : room.getHistoryEvents(0)) { + if (eventLog.getType() == EventLog.Type.ConferenceChatMessage) { + ChatMessage message = eventLog.getChatMessage(); + if (message.getAppdata() != null && !message.isOutgoing()) { + File file = new File(message.getAppdata()); + if (file.exists()) { + file.delete(); // Delete downloaded file from incoming message that + // will be deleted + } + } + } + } + + room.addListener(mChatRoomListener); + lc.deleteChatRoom(room); + } + if (mChatRoomDeletionPendingCount > 0) { + mWaitLayout.setVisibility(View.VISIBLE); + } + LinphoneActivity.instance() + .displayMissedChats(LinphoneManager.getInstance().getUnreadMessageCount()); + } + + @Override + public void onContactsUpdated() { + if (!LinphoneActivity.isInstanciated() + || LinphoneActivity.instance().getCurrentFragment() != CHAT_LIST) return; + + ChatRoomsAdapter adapter = (ChatRoomsAdapter) mChatRoomsList.getAdapter(); + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } +} diff --git a/app/src/main/java/org/linphone/chat/ChatScrollListener.java b/app/src/main/java/org/linphone/chat/ChatScrollListener.java new file mode 100644 index 000000000..7140ed228 --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ChatScrollListener.java @@ -0,0 +1,78 @@ +package org.linphone.chat; + +/* +ChatScrollListener.java +Copyright (C) 2010-2018 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 androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +abstract class ChatScrollListener extends RecyclerView.OnScrollListener { + // The minimum amount of items to have below your current scroll position + // before mLoading more. + private final int mVisibleThreshold = 5; + // The total number of items in the dataset after the last load + private int mPreviousTotalItemCount = 0; + // True if we are still waiting for the last set of data to load. + private boolean mLoading = true; + + private final LinearLayoutManager mLayoutManager; + + public ChatScrollListener(LinearLayoutManager layoutManager) { + mLayoutManager = layoutManager; + } + + // This happens many times a second during a scroll, so be wary of the code you place here. + // We are given a few useful parameters to help us work out if we need to load some more data, + // but first we check if we are waiting for the previous load to finish. + @Override + public void onScrolled(RecyclerView view, int dx, int dy) { + int lastVisibleItemPosition; + int totalItemCount = mLayoutManager.getItemCount(); + + lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition(); + + // If the total item count is zero and the previous isn't, assume the + // list is invalidated and should be reset back to initial state + if (totalItemCount < mPreviousTotalItemCount) { + this.mPreviousTotalItemCount = totalItemCount; + if (totalItemCount == 0) { + this.mLoading = true; + } + } + // If it’s still mLoading, we check to see if the dataset count has + // changed, if so we conclude it has finished mLoading and update the current page + // number and total item count. + if (mLoading && (totalItemCount > mPreviousTotalItemCount)) { + mLoading = false; + mPreviousTotalItemCount = totalItemCount; + } + + // If it isn’t currently mLoading, we check to see if we have breached + // the mVisibleThreshold and need to reload more data. + // If we do need to reload some more data, we execute onLoadMore to fetch the data. + // threshold should reflect how many total columns there are too + if (!mLoading && (lastVisibleItemPosition + mVisibleThreshold) > totalItemCount) { + onLoadMore(totalItemCount); + mLoading = true; + } + } + + // Defines the process for actually mLoading more data based on page + protected abstract void onLoadMore(int totalItemsCount); +} diff --git a/src/android/org/linphone/compatibility/ApiNineteenPlus.java b/app/src/main/java/org/linphone/chat/DeviceChildViewHolder.java similarity index 51% rename from src/android/org/linphone/compatibility/ApiNineteenPlus.java rename to app/src/main/java/org/linphone/chat/DeviceChildViewHolder.java index 9c26fcc0b..2498372be 100644 --- a/src/android/org/linphone/compatibility/ApiNineteenPlus.java +++ b/app/src/main/java/org/linphone/chat/DeviceChildViewHolder.java @@ -1,11 +1,8 @@ -package org.linphone.compatibility; +package org.linphone.chat; -import android.annotation.TargetApi; -import android.app.AlarmManager; -import android.app.PendingIntent; - -/*ApiNineteenPlus.java -Copyright (C) 2017 Belledonne Communications, Grenoble, France +/* +DeviceChildViewHolder.java +Copyright (C) 2010-2018 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 @@ -22,13 +19,17 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -@TargetApi(19) -public class ApiNineteenPlus { - public static void scheduleAlarm(AlarmManager alarmManager, int type, long triggerAtMillis, PendingIntent operation) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { - alarmManager.setExact(type, triggerAtMillis, operation); - } else { - alarmManager.set(type, triggerAtMillis, operation); - } +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import org.linphone.R; + +class DeviceChildViewHolder { + public final TextView deviceName; + public final ImageView securityLevel; + + public DeviceChildViewHolder(View v) { + deviceName = v.findViewById(R.id.name); + securityLevel = v.findViewById(R.id.security_level); } } diff --git a/app/src/main/java/org/linphone/chat/DeviceGroupViewHolder.java b/app/src/main/java/org/linphone/chat/DeviceGroupViewHolder.java new file mode 100644 index 000000000..bc72f4004 --- /dev/null +++ b/app/src/main/java/org/linphone/chat/DeviceGroupViewHolder.java @@ -0,0 +1,40 @@ +package org.linphone.chat; + +/* +DeviceGroupViewHolder.java +Copyright (C) 2010-2018 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.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import org.linphone.R; + +class DeviceGroupViewHolder { + public final RelativeLayout avatarLayout; + public final TextView participantName, sipUri; + public final ImageView groupExpander, securityLevel; + + public DeviceGroupViewHolder(View v) { + avatarLayout = v.findViewById(R.id.avatar_layout); + participantName = v.findViewById(R.id.name); + sipUri = v.findViewById(R.id.sipUri); + groupExpander = v.findViewById(R.id.dropdown); + securityLevel = v.findViewById(R.id.device_security_level); + } +} diff --git a/app/src/main/java/org/linphone/chat/DevicesAdapter.java b/app/src/main/java/org/linphone/chat/DevicesAdapter.java new file mode 100644 index 000000000..fdd28d863 --- /dev/null +++ b/app/src/main/java/org/linphone/chat/DevicesAdapter.java @@ -0,0 +1,249 @@ +package org.linphone.chat; + +/* +DevicesAdapter.java +Copyright (C) 2010-2018 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.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseExpandableListAdapter; +import java.util.ArrayList; +import java.util.List; +import org.linphone.R; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.ChatRoomSecurityLevel; +import org.linphone.core.Participant; +import org.linphone.core.ParticipantDevice; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; + +class DevicesAdapter extends BaseExpandableListAdapter { + private final Context mContext; + private List mParticipants; + private boolean mOnlyDisplayChildsAsGroups; + + public DevicesAdapter(Context context) { + mContext = context; + mParticipants = new ArrayList<>(); + mOnlyDisplayChildsAsGroups = false; + } + + public void updateListItems(List participants, boolean childsAsGroups) { + mOnlyDisplayChildsAsGroups = childsAsGroups; + mParticipants = participants; + notifyDataSetChanged(); + } + + @Override + public View getGroupView( + int groupPosition, boolean isExpanded, View view, ViewGroup viewGroup) { + if (mOnlyDisplayChildsAsGroups) { + ParticipantDevice device = (ParticipantDevice) getGroup(groupPosition); + + DeviceChildViewHolder holder = null; + if (view != null) { + Object possibleHolder = view.getTag(); + if (possibleHolder instanceof DeviceChildViewHolder) { + holder = (DeviceChildViewHolder) possibleHolder; + } + } else { + LayoutInflater inflater = LayoutInflater.from(mContext); + view = inflater.inflate(R.layout.chat_device_cell_as_group, viewGroup, false); + } + if (holder == null) { + holder = new DeviceChildViewHolder(view); + view.setTag(holder); + } + + holder.deviceName.setText(device.getName()); + + ChatRoomSecurityLevel level = device.getSecurityLevel(); + switch (level) { + case Safe: + holder.securityLevel.setImageResource(R.drawable.security_2_indicator); + break; + case Encrypted: + holder.securityLevel.setImageResource(R.drawable.security_1_indicator); + break; + case ClearText: + case Unsafe: + default: + holder.securityLevel.setImageResource(R.drawable.security_alert_indicator); + break; + } + } else { + Participant participant = (Participant) getGroup(groupPosition); + + DeviceGroupViewHolder holder = null; + if (view != null) { + Object possibleHolder = view.getTag(); + if (possibleHolder instanceof DeviceGroupViewHolder) { + holder = (DeviceGroupViewHolder) possibleHolder; + } + } else { + LayoutInflater inflater = LayoutInflater.from(mContext); + view = inflater.inflate(R.layout.chat_device_group, viewGroup, false); + } + if (holder == null) { + holder = new DeviceGroupViewHolder(view); + view.setTag(holder); + } + + Address participantAddress = participant.getAddress(); + LinphoneContact contact = + ContactsManager.getInstance().findContactFromAddress(participantAddress); + if (contact != null) { + ContactAvatar.displayAvatar( + contact, participant.getSecurityLevel(), holder.avatarLayout); + holder.participantName.setText(contact.getFullName()); + } else { + String displayName = LinphoneUtils.getAddressDisplayName(participantAddress); + ContactAvatar.displayAvatar( + displayName, participant.getSecurityLevel(), holder.avatarLayout); + holder.participantName.setText(displayName); + } + + holder.sipUri.setText(participantAddress.asStringUriOnly()); + if (!mContext.getResources().getBoolean(R.bool.show_sip_uri_in_chat)) { + holder.sipUri.setVisibility(View.GONE); + } + + if (getChildrenCount(groupPosition) == 1) { + holder.securityLevel.setVisibility(View.VISIBLE); + holder.groupExpander.setVisibility(View.GONE); + + ParticipantDevice device = (ParticipantDevice) getChild(groupPosition, 0); + ChatRoomSecurityLevel level = device.getSecurityLevel(); + switch (level) { + case Safe: + holder.securityLevel.setImageResource(R.drawable.security_2_indicator); + break; + case Encrypted: + holder.securityLevel.setImageResource(R.drawable.security_1_indicator); + break; + case ClearText: + case Unsafe: + default: + holder.securityLevel.setImageResource(R.drawable.security_alert_indicator); + break; + } + } else { + holder.securityLevel.setVisibility(View.GONE); + holder.groupExpander.setVisibility(View.VISIBLE); + holder.groupExpander.setImageResource( + isExpanded ? R.drawable.chevron_list_open : R.drawable.chevron_list_close); + } + } + + return view; + } + + @Override + public View getChildView( + int groupPosition, int childPosition, boolean b, View view, ViewGroup viewGroup) { + ParticipantDevice device = (ParticipantDevice) getChild(groupPosition, childPosition); + + DeviceChildViewHolder holder = null; + if (view != null) { + Object possibleHolder = view.getTag(); + if (possibleHolder instanceof DeviceChildViewHolder) { + holder = (DeviceChildViewHolder) possibleHolder; + } + } else { + LayoutInflater inflater = LayoutInflater.from(mContext); + view = inflater.inflate(R.layout.chat_device_cell, viewGroup, false); + } + if (holder == null) { + holder = new DeviceChildViewHolder(view); + view.setTag(holder); + } + + holder.deviceName.setText(device.getName()); + + ChatRoomSecurityLevel level = device.getSecurityLevel(); + switch (level) { + case Safe: + holder.securityLevel.setImageResource(R.drawable.security_2_indicator); + break; + case Encrypted: + holder.securityLevel.setImageResource(R.drawable.security_1_indicator); + break; + case ClearText: + case Unsafe: + default: + holder.securityLevel.setImageResource(R.drawable.security_alert_indicator); + break; + } + + return view; + } + + @Override + public int getGroupCount() { + if (mParticipants.size() == 0) return 0; + return mOnlyDisplayChildsAsGroups + ? mParticipants.get(0).getDevices().length + : mParticipants.size(); + } + + @Override + public int getChildrenCount(int groupPosition) { + if (mParticipants.size() == 0) return 0; + return mOnlyDisplayChildsAsGroups + ? 0 + : mParticipants.get(groupPosition).getDevices().length; + } + + @Override + public Object getGroup(int groupPosition) { + if (mParticipants.size() == 0) return null; + return mOnlyDisplayChildsAsGroups + ? mParticipants.get(0).getDevices()[groupPosition] + : mParticipants.get(groupPosition); + } + + @Override + public Object getChild(int groupPosition, int childPosition) { + if (mParticipants.size() == 0) return null; + return mParticipants.get(groupPosition).getDevices()[childPosition]; + } + + @Override + public long getGroupId(int groupPosition) { + return groupPosition; + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + @Override + public boolean hasStableIds() { + return false; + } +} diff --git a/app/src/main/java/org/linphone/chat/DevicesFragment.java b/app/src/main/java/org/linphone/chat/DevicesFragment.java new file mode 100644 index 000000000..d48950b2f --- /dev/null +++ b/app/src/main/java/org/linphone/chat/DevicesFragment.java @@ -0,0 +1,187 @@ +/* +DevicesFragment.java +Copyright (C) 2010-2018 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 org.linphone.chat; + +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ExpandableListView; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.Nullable; +import java.util.Arrays; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.call.CallManager; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomCapabilities; +import org.linphone.core.Core; +import org.linphone.core.ParticipantDevice; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.utils.LinphoneUtils; + +public class DevicesFragment extends Fragment { + private LayoutInflater mInflater; + private ImageView mBackButton; + private TextView mTitle; + private ExpandableListView mExpandableList; + private DevicesAdapter mAdapter; + + private String mLocalSipUri, mRoomUri; + private Address mLocalSipAddr, mRoomAddr; + private ChatRoom mRoom; + private boolean mOnlyDisplayChilds; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() != null) { + mLocalSipUri = getArguments().getString("LocalSipUri"); + mLocalSipAddr = LinphoneManager.getLc().createAddress(mLocalSipUri); + mRoomUri = getArguments().getString("RemoteSipUri"); + mRoomAddr = LinphoneManager.getLc().createAddress(mRoomUri); + } + + mInflater = inflater; + View view = mInflater.inflate(R.layout.chat_devices, container, false); + + mOnlyDisplayChilds = false; + + mExpandableList = view.findViewById(R.id.devices_list); + mExpandableList.setOnChildClickListener( + new ExpandableListView.OnChildClickListener() { + @Override + public boolean onChildClick( + ExpandableListView expandableListView, + View view, + int groupPosition, + int childPosition, + long l) { + ParticipantDevice device = + (ParticipantDevice) mAdapter.getChild(groupPosition, childPosition); + CallManager.getInstance().inviteAddress(device.getAddress(), true); + return false; + } + }); + mExpandableList.setOnGroupClickListener( + new ExpandableListView.OnGroupClickListener() { + @Override + public boolean onGroupClick( + ExpandableListView expandableListView, + View view, + int groupPosition, + long l) { + if (mOnlyDisplayChilds) { + // in this case groups are childs, so call on click + ParticipantDevice device = + (ParticipantDevice) mAdapter.getGroup(groupPosition); + CallManager.getInstance().inviteAddress(device.getAddress(), true); + return true; + } else { + if (mAdapter.getChildrenCount(groupPosition) == 1) { + ParticipantDevice device = + (ParticipantDevice) mAdapter.getChild(groupPosition, 0); + CallManager.getInstance().inviteAddress(device.getAddress(), true); + return true; + } + } + return false; + } + }); + + initChatRoom(); + + mTitle = view.findViewById(R.id.title); + initHeader(); + + mBackButton = view.findViewById(R.id.back); + mBackButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (LinphoneActivity.instance().isTablet()) { + LinphoneActivity.instance().goToChat(mLocalSipUri, mRoomUri, null); + } else { + LinphoneActivity.instance().onBackPressed(); + } + } + }); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.CONTACT_DEVICES); + } + + initValues(); + } + + private void initChatRoom() { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + mRoom = core.getChatRoom(mRoomAddr, mLocalSipAddr); + } + + private void initHeader() { + if (mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) { + Address remoteParticipantAddr = mRoomAddr; + if (mRoom.getParticipants().length > 0) { + remoteParticipantAddr = mRoom.getParticipants()[0].getAddress(); + } + LinphoneContact c = + ContactsManager.getInstance().findContactFromAddress(remoteParticipantAddr); + String displayName; + if (c != null) { + displayName = c.getFullName(); + } else { + displayName = LinphoneUtils.getAddressDisplayName(remoteParticipantAddr); + } + mTitle.setText(getString(R.string.chat_room_devices).replace("%s", displayName)); + } + } + + private void initValues() { + if (mAdapter == null) { + mAdapter = new DevicesAdapter(getActivity()); + mExpandableList.setAdapter(mAdapter); + } + if (mRoom == null) { + initChatRoom(); + } + + if (mRoom != null && mRoom.getNbParticipants() > 0) { + mOnlyDisplayChilds = mRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()); + mAdapter.updateListItems(Arrays.asList(mRoom.getParticipants()), mOnlyDisplayChilds); + } + } +} diff --git a/app/src/main/java/org/linphone/chat/GroupInfoAdapter.java b/app/src/main/java/org/linphone/chat/GroupInfoAdapter.java new file mode 100644 index 000000000..e0d515a0a --- /dev/null +++ b/app/src/main/java/org/linphone/chat/GroupInfoAdapter.java @@ -0,0 +1,178 @@ +/* +GroupInfoAdapter.java +Copyright (C) 2017 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 org.linphone.chat; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.R; +import org.linphone.contacts.ContactAddress; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.ChatRoom; +import org.linphone.core.Participant; +import org.linphone.views.ContactAvatar; + +class GroupInfoAdapter extends RecyclerView.Adapter { + private List mItems; + private View.OnClickListener mDeleteListener; + private boolean mHideAdminFeatures; + private ChatRoom mChatRoom; + + public GroupInfoAdapter( + List items, boolean hideAdminFeatures, boolean isCreation) { + mItems = items; + mHideAdminFeatures = hideAdminFeatures || isCreation; + } + + @NonNull + @Override + public GroupInfoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.chat_infos_cell, parent, false); + return new GroupInfoViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull final GroupInfoViewHolder holder, int position) { + final ContactAddress ca = (ContactAddress) getItem(position); + LinphoneContact c = ca.getContact(); + + holder.name.setText( + (c != null && c.getFullName() != null) + ? c.getFullName() + : (ca.getDisplayName() != null) ? ca.getDisplayName() : ca.getUsername()); + + if (c != null) { + ContactAvatar.displayAvatar(c, holder.avatarLayout); + } else { + ContactAvatar.displayAvatar(holder.name.getText().toString(), holder.avatarLayout); + } + + holder.sipUri.setText(ca.getAddressAsDisplayableString()); + + if (!LinphoneActivity.instance().getResources().getBoolean(R.bool.show_sip_uri_in_chat)) { + holder.sipUri.setVisibility(View.GONE); + holder.name.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + holder.sipUri.setVisibility( + holder.sipUri.getVisibility() == View.VISIBLE + ? View.GONE + : View.VISIBLE); + } + }); + } + + holder.delete.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mDeleteListener != null) { + mDeleteListener.onClick(view); + } + } + }); + holder.delete.setTag(ca); + + holder.isAdmin.setVisibility(ca.isAdmin() ? View.VISIBLE : View.GONE); + holder.isNotAdmin.setVisibility(ca.isAdmin() ? View.GONE : View.VISIBLE); + + holder.isAdmin.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + holder.isNotAdmin.setVisibility(View.VISIBLE); + holder.isAdmin.setVisibility(View.GONE); + ca.setAdmin(false); + } + }); + + holder.isNotAdmin.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + holder.isNotAdmin.setVisibility(View.GONE); + holder.isAdmin.setVisibility(View.VISIBLE); + ca.setAdmin(true); + } + }); + + holder.delete.setVisibility(View.VISIBLE); + if (mHideAdminFeatures) { + holder.delete.setVisibility(View.INVISIBLE); + holder.isAdmin.setOnClickListener( + null); // Do not allow not admin to remove it's rights but display admins + holder.isNotAdmin.setVisibility( + View.GONE); // Hide not admin button for not admin participants + } else if (mChatRoom != null) { + boolean found = false; + for (Participant p : mChatRoom.getParticipants()) { + if (p.getAddress().weakEqual(ca.getAddress())) { + found = true; + break; + } + } + if (!found) { + holder.isNotAdmin.setVisibility( + View.GONE); // Hide not admin button for participant not yet added so + // even if user click it it won't have any effect + } + } + } + + @Override + public int getItemCount() { + return mItems.size(); + } + + public void setChatRoom(ChatRoom room) { + mChatRoom = room; + } + + private Object getItem(int i) { + return mItems.get(i); + } + + @Override + public long getItemId(int i) { + return i; + } + + public void setOnDeleteClickListener(View.OnClickListener onClickListener) { + mDeleteListener = onClickListener; + } + + public void updateDataSet(ArrayList mParticipants) { + mItems = mParticipants; + notifyDataSetChanged(); + } + + public void setAdminFeaturesVisible(boolean visible) { + mHideAdminFeatures = !visible; + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/org/linphone/chat/GroupInfoFragment.java b/app/src/main/java/org/linphone/chat/GroupInfoFragment.java new file mode 100644 index 000000000..1e3f5f5ec --- /dev/null +++ b/app/src/main/java/org/linphone/chat/GroupInfoFragment.java @@ -0,0 +1,590 @@ +/* +InfoGroupChatFragment.java +Copyright (C) 2017 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 org.linphone.chat; + +import static android.content.Context.INPUT_METHOD_SERVICE; + +import android.app.Dialog; +import android.app.Fragment; +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.contacts.ContactAddress; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomListener; +import org.linphone.core.ChatRoomListenerStub; +import org.linphone.core.ChatRoomParams; +import org.linphone.core.Core; +import org.linphone.core.EventLog; +import org.linphone.core.Participant; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.utils.LinphoneUtils; + +public class GroupInfoFragment extends Fragment implements ChatRoomListener { + private ImageView mBackButton, mConfirmButton, mAddParticipantsButton; + private RelativeLayout mAddParticipantsLayout; + private Address mGroupChatRoomAddress; + private EditText mSubjectField; + + private RecyclerView mParticipantsList; + + private LinearLayout mLeaveGroupButton; + private RelativeLayout mWaitLayout; + private GroupInfoAdapter mAdapter; + private boolean mIsAlreadyCreatedGroup; + private boolean mIsEditionEnabled; + private ArrayList mParticipants; + private String mSubject; + private ChatRoom mChatRoom, mTempChatRoom; + private Dialog mAdminStateChangedDialog; + private ChatRoomListenerStub mChatRoomCreationListener; + private Bundle mShareInfos; + private Context mContext; + private LinearLayoutManager layoutManager; + private boolean mIsEncryptionEnabled; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.chat_infos, container, false); + + if (getArguments() == null || getArguments().isEmpty()) { + return null; + } + mContext = getActivity().getApplicationContext(); + + mParticipants = + (ArrayList) getArguments().getSerializable("ContactAddress"); + + mGroupChatRoomAddress = null; + mChatRoom = null; + + String address = getArguments().getString("groupChatRoomAddress"); + if (address != null && address.length() > 0) { + mGroupChatRoomAddress = LinphoneManager.getLc().createAddress(address); + } + mIsAlreadyCreatedGroup = mGroupChatRoomAddress != null; + if (mIsAlreadyCreatedGroup) { + mChatRoom = LinphoneManager.getLc().getChatRoom(mGroupChatRoomAddress); + } + if (mChatRoom == null) mIsAlreadyCreatedGroup = false; + + mIsEditionEnabled = getArguments().getBoolean("isEditionEnabled"); + mSubject = getArguments().getString("subject"); + + if (mChatRoom != null && mChatRoom.hasBeenLeft()) { + mIsEditionEnabled = false; + } + + mIsEncryptionEnabled = getArguments().getBoolean("encryptionEnabled", false); + + mParticipantsList = view.findViewById(R.id.chat_room_participants); + mAdapter = new GroupInfoAdapter(mParticipants, !mIsEditionEnabled, !mIsAlreadyCreatedGroup); + mAdapter.setOnDeleteClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + ContactAddress ca = (ContactAddress) view.getTag(); + mParticipants.remove(ca); + mAdapter.updateDataSet(mParticipants); + mParticipantsList.setAdapter(mAdapter); + mConfirmButton.setEnabled( + mSubjectField.getText().length() > 0 && mParticipants.size() > 0); + } + }); + mParticipantsList.setAdapter(mAdapter); + mAdapter.setChatRoom(mChatRoom); + layoutManager = new LinearLayoutManager(mContext); + mParticipantsList.setLayoutManager(layoutManager); + + // Divider between items + DividerItemDecoration dividerItemDecoration = + new DividerItemDecoration( + mParticipantsList.getContext(), layoutManager.getOrientation()); + dividerItemDecoration.setDrawable(mContext.getResources().getDrawable(R.drawable.divider)); + mParticipantsList.addItemDecoration(dividerItemDecoration); + + String fileSharedUri = getArguments().getString("fileSharedUri"); + String messageDraft = getArguments().getString("messageDraft"); + + if (fileSharedUri != null || messageDraft != null) { + Log.i("[GroupInfo] Forwarding arguments to group chat room"); + mShareInfos = new Bundle(); + } + + if (fileSharedUri != null) mShareInfos.putString("fileSharedUri", fileSharedUri); + + if (messageDraft != null) mShareInfos.putString("messageDraft", messageDraft); + + mBackButton = view.findViewById(R.id.back); + mBackButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mIsAlreadyCreatedGroup) { + if (LinphoneActivity.instance().isTablet()) { + LinphoneActivity.instance() + .goToChat( + mChatRoom.getLocalAddress().asStringUriOnly(), + mGroupChatRoomAddress.asStringUriOnly(), + mShareInfos); + } else { + getFragmentManager().popBackStack(); + } + } else { + LinphoneActivity.instance() + .goToChatCreator( + null, + mParticipants, + null, + true, + mShareInfos, + true, + mIsEncryptionEnabled); + } + } + }); + + mConfirmButton = view.findViewById(R.id.confirm); + + mLeaveGroupButton = view.findViewById(R.id.leaveGroupLayout); + mLeaveGroupButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + final Dialog dialog = + LinphoneActivity.instance() + .displayDialog(getString(R.string.chat_room_leave_dialog)); + Button delete = dialog.findViewById(R.id.dialog_delete_button); + delete.setText(getString(R.string.chat_room_leave_button)); + Button cancel = dialog.findViewById(R.id.dialog_cancel_button); + + delete.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mChatRoom != null) { + mChatRoom.leave(); + LinphoneActivity.instance() + .goToChat( + mChatRoom + .getLocalAddress() + .asStringUriOnly(), + mGroupChatRoomAddress.asString(), + null); + } else { + Log.e( + "Can't leave, chatRoom for address " + + mGroupChatRoomAddress.asString() + + " is null..."); + } + dialog.dismiss(); + } + }); + + cancel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + dialog.dismiss(); + } + }); + dialog.show(); + } + }); + mLeaveGroupButton.setVisibility( + mIsAlreadyCreatedGroup && mChatRoom.hasBeenLeft() + ? View.GONE + : mIsAlreadyCreatedGroup ? View.VISIBLE : View.GONE); + + mAddParticipantsLayout = view.findViewById(R.id.addParticipantsLayout); + mAddParticipantsLayout.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mIsEditionEnabled && mIsAlreadyCreatedGroup) { + LinphoneActivity.instance() + .goToChatCreator( + mGroupChatRoomAddress != null + ? mGroupChatRoomAddress.asString() + : null, + mParticipants, + mSubject, + !mIsAlreadyCreatedGroup, + null, + true, + mIsEncryptionEnabled); + } + } + }); + mAddParticipantsButton = view.findViewById(R.id.addParticipants); + mAddParticipantsButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mIsEditionEnabled && mIsAlreadyCreatedGroup) { + LinphoneActivity.instance() + .goToChatCreator( + mGroupChatRoomAddress != null + ? mGroupChatRoomAddress.asString() + : null, + mParticipants, + mSubject, + !mIsAlreadyCreatedGroup, + null, + true, + mIsEncryptionEnabled); + } + } + }); + mAddParticipantsButton.setVisibility(mIsAlreadyCreatedGroup ? View.VISIBLE : View.GONE); + + mSubjectField = view.findViewById(R.id.subjectField); + mSubjectField.addTextChangedListener( + new TextWatcher() { + @Override + public void beforeTextChanged( + CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} + + @Override + public void afterTextChanged(Editable editable) { + mConfirmButton.setEnabled( + mSubjectField.getText().length() > 0 && mParticipants.size() > 0); + } + }); + mSubjectField.setText(mSubject); + + mChatRoomCreationListener = + new ChatRoomListenerStub() { + @Override + public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { + if (newState == ChatRoom.State.Created) { + mWaitLayout.setVisibility(View.GONE); + // This will remove both the creation fragment and the group info + // fragment from the back stack + getFragmentManager().popBackStack(); + getFragmentManager().popBackStack(); + LinphoneActivity.instance() + .goToChat( + cr.getLocalAddress().asStringUriOnly(), + cr.getPeerAddress().asStringUriOnly(), + mShareInfos); + } else if (newState == ChatRoom.State.CreationFailed) { + mWaitLayout.setVisibility(View.GONE); + LinphoneActivity.instance().displayChatRoomError(); + Log.e( + "Group chat room for address " + + cr.getPeerAddress() + + " has failed !"); + } + } + }; + + mConfirmButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (!mIsAlreadyCreatedGroup) { + mWaitLayout.setVisibility(View.VISIBLE); + Core core = LinphoneManager.getLc(); + + int i = 0; + Address[] participants = new Address[mParticipants.size()]; + for (ContactAddress ca : mParticipants) { + participants[i] = ca.getAddress(); + i++; + } + + ChatRoomParams params = core.createDefaultChatRoomParams(); + params.enableEncryption(mIsEncryptionEnabled); + params.enableGroup(true); + + mTempChatRoom = + core.createChatRoom( + params, + mSubjectField.getText().toString(), + participants); + if (mTempChatRoom != null) { + mTempChatRoom.addListener(mChatRoomCreationListener); + } else { + Log.w("[Group Info Fragment] createChatRoom returned null..."); + mWaitLayout.setVisibility(View.GONE); + } + } else { + // Subject + String newSubject = mSubjectField.getText().toString(); + if (!newSubject.equals(mSubject)) { + mChatRoom.setSubject(newSubject); + } + + // Participants removed + ArrayList toRemove = new ArrayList<>(); + for (Participant p : mChatRoom.getParticipants()) { + boolean found = false; + for (ContactAddress c : mParticipants) { + if (c.getAddress().weakEqual(p.getAddress())) { + found = true; + break; + } + } + if (!found) { + toRemove.add(p); + } + } + Participant[] participantsToRemove = new Participant[toRemove.size()]; + toRemove.toArray(participantsToRemove); + mChatRoom.removeParticipants(participantsToRemove); + + // Participants added + ArrayList
    toAdd = new ArrayList<>(); + for (ContactAddress c : mParticipants) { + boolean found = false; + for (Participant p : mChatRoom.getParticipants()) { + if (p.getAddress().weakEqual(c.getAddress())) { + // Admin rights + if (c.isAdmin() != p.isAdmin()) { + mChatRoom.setParticipantAdminStatus(p, c.isAdmin()); + } + found = true; + break; + } + } + if (!found) { + Address addr = c.getAddress(); + if (addr != null) { + toAdd.add(addr); + } else { + // TODO error + } + } + } + Address[] participantsToAdd = new Address[toAdd.size()]; + toAdd.toArray(participantsToAdd); + mChatRoom.addParticipants(participantsToAdd); + + LinphoneActivity.instance() + .goToChat( + mChatRoom.getLocalAddress().asStringUriOnly(), + mGroupChatRoomAddress.asString(), + null); + } + } + }); + mConfirmButton.setEnabled(mSubjectField.getText().length() > 0 && mParticipants.size() > 0); + + if (!mIsEditionEnabled) { + mSubjectField.setEnabled(false); + mConfirmButton.setVisibility(View.INVISIBLE); + mAddParticipantsButton.setVisibility(View.GONE); + } + + mWaitLayout = view.findViewById(R.id.waitScreen); + mWaitLayout.setVisibility(View.GONE); + + if (mChatRoom != null) { + mChatRoom.addListener(this); + } + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.INFO_GROUP_CHAT); + } + + InputMethodManager inputMethodManager = + (InputMethodManager) getActivity().getSystemService(INPUT_METHOD_SERVICE); + if (getActivity().getCurrentFocus() != null) { + inputMethodManager.hideSoftInputFromWindow( + getActivity().getCurrentFocus().getWindowToken(), 0); + } + } + + @Override + public void onPause() { + if (mTempChatRoom != null) { + mTempChatRoom.removeListener(mChatRoomCreationListener); + } + super.onPause(); + } + + @Override + public void onDestroy() { + if (mChatRoom != null) { + mChatRoom.removeListener(this); + } + super.onDestroy(); + } + + private void refreshParticipantsList() { + if (mChatRoom == null) return; + mParticipants = new ArrayList<>(); + for (Participant p : mChatRoom.getParticipants()) { + Address a = p.getAddress(); + LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(a); + if (c == null) { + c = new LinphoneContact(); + String displayName = LinphoneUtils.getAddressDisplayName(a); + c.setFullName(displayName); + } + ContactAddress ca = new ContactAddress(c, a.asString(), "", c.isFriend(), p.isAdmin()); + mParticipants.add(ca); + } + + mAdapter.updateDataSet(mParticipants); + mAdapter.setChatRoom(mChatRoom); + } + + private void refreshAdminRights() { + mAdapter.setAdminFeaturesVisible(mIsEditionEnabled); + mAdapter.setChatRoom(mChatRoom); + mSubjectField.setEnabled(mIsEditionEnabled); + mConfirmButton.setVisibility(mIsEditionEnabled ? View.VISIBLE : View.INVISIBLE); + mAddParticipantsButton.setVisibility(mIsEditionEnabled ? View.VISIBLE : View.GONE); + } + + private void displayMeAdminStatusUpdated() { + if (mAdminStateChangedDialog != null) mAdminStateChangedDialog.dismiss(); + + mAdminStateChangedDialog = + LinphoneActivity.instance() + .displayDialog( + getString( + mIsEditionEnabled + ? R.string.chat_room_you_are_now_admin + : R.string.chat_room_you_are_no_longer_admin)); + Button cancel = mAdminStateChangedDialog.findViewById(R.id.dialog_cancel_button); + mAdminStateChangedDialog.findViewById(R.id.dialog_delete_button).setVisibility(View.GONE); + cancel.setText(getString(R.string.ok)); + cancel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + mAdminStateChangedDialog.dismiss(); + } + }); + + mAdminStateChangedDialog.show(); + } + + @Override + public void onParticipantAdminStatusChanged(ChatRoom cr, EventLog event_log) { + if (mChatRoom.getMe().isAdmin() != mIsEditionEnabled) { + // Either we weren't admin and we are now or the other way around + mIsEditionEnabled = mChatRoom.getMe().isAdmin(); + displayMeAdminStatusUpdated(); + refreshAdminRights(); + } + refreshParticipantsList(); + } + + @Override + public void onSubjectChanged(ChatRoom cr, EventLog event_log) { + mSubjectField.setText(event_log.getSubject()); + } + + @Override + public void onConferenceJoined(ChatRoom cr, EventLog event_log) {} + + @Override + public void onConferenceLeft(ChatRoom cr, EventLog event_log) {} + + @Override + public void onParticipantAdded(ChatRoom cr, EventLog event_log) { + refreshParticipantsList(); + } + + @Override + public void onParticipantRemoved(ChatRoom cr, EventLog event_log) { + refreshParticipantsList(); + } + + @Override + public void onChatMessageShouldBeStored(ChatRoom cr, ChatMessage msg) {} + + @Override + public void onIsComposingReceived(ChatRoom cr, Address remoteAddr, boolean isComposing) {} + + @Override + public void onChatMessageSent(ChatRoom cr, EventLog event_log) {} + + @Override + public void onConferenceAddressGeneration(ChatRoom cr) {} + + @Override + public void onChatMessageReceived(ChatRoom cr, EventLog event_log) {} + + @Override + public void onMessageReceived(ChatRoom cr, ChatMessage msg) {} + + @Override + public void onParticipantDeviceRemoved(ChatRoom cr, EventLog event_log) {} + + @Override + public void onParticipantDeviceAdded(ChatRoom cr, EventLog event_log) {} + + @Override + public void onSecurityEvent(ChatRoom cr, EventLog eventLog) { + refreshParticipantsList(); + } + + @Override + public void onUndecryptableMessageReceived(ChatRoom cr, ChatMessage msg) {} + + @Override + public void onStateChanged(ChatRoom cr, ChatRoom.State newState) {} + + @Override + public void onParticipantRegistrationSubscriptionRequested( + ChatRoom cr, Address participantAddr) {} + + @Override + public void onParticipantRegistrationUnsubscriptionRequested( + ChatRoom cr, Address participantAddr) {} +} diff --git a/app/src/main/java/org/linphone/chat/GroupInfoViewHolder.java b/app/src/main/java/org/linphone/chat/GroupInfoViewHolder.java new file mode 100644 index 000000000..5f0e2017b --- /dev/null +++ b/app/src/main/java/org/linphone/chat/GroupInfoViewHolder.java @@ -0,0 +1,46 @@ +package org.linphone.chat; + +/* +GroupInfoViewHolder.java +Copyright (C) 2018 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.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import org.linphone.R; + +public class GroupInfoViewHolder extends RecyclerView.ViewHolder { + public final TextView name, sipUri; + public final RelativeLayout avatarLayout; + public final ImageView delete; + public final LinearLayout isAdmin; + public final LinearLayout isNotAdmin; + + public GroupInfoViewHolder(View view) { + super(view); + name = view.findViewById(R.id.name); + sipUri = view.findViewById(R.id.sipUri); + avatarLayout = view.findViewById(R.id.avatar_layout); + delete = view.findViewById(R.id.delete); + isAdmin = view.findViewById(R.id.isAdminLayout); + isNotAdmin = view.findViewById(R.id.isNotAdminLayout); + } +} diff --git a/app/src/main/java/org/linphone/chat/ImdnFragment.java b/app/src/main/java/org/linphone/chat/ImdnFragment.java new file mode 100644 index 000000000..deb26ceb1 --- /dev/null +++ b/app/src/main/java/org/linphone/chat/ImdnFragment.java @@ -0,0 +1,370 @@ +/* +ImdnOldFragment.java +Copyright (C) 2010-2018 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 org.linphone.chat; + +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.Nullable; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatMessageListenerStub; +import org.linphone.core.ChatRoom; +import org.linphone.core.Core; +import org.linphone.core.ParticipantImdnState; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; + +public class ImdnFragment extends Fragment { + private LayoutInflater mInflater; + private LinearLayout mRead, + mReadHeader, + mDelivered, + mDeliveredHeader, + mSent, + mSentHeader, + mUndelivered, + mUndeliveredHeader; + private ImageView mBackButton; + private ChatMessageViewHolder mBubble; + private ViewGroup mContainer; + + private String mLocalSipuri, mRoomUri, mMessageId; + private Address mLocalSipAddr, mRoomAddr; + private ChatRoom mRoom; + private ChatMessage mMessage; + private ChatMessageListenerStub mListener; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() != null) { + mLocalSipuri = getArguments().getString("LocalSipUri"); + mLocalSipAddr = LinphoneManager.getLc().createAddress(mLocalSipuri); + mRoomUri = getArguments().getString("RemoteSipUri"); + mRoomAddr = LinphoneManager.getLc().createAddress(mRoomUri); + mMessageId = getArguments().getString("MessageId"); + } + + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + mRoom = core.getChatRoom(mRoomAddr, mLocalSipAddr); + + mInflater = inflater; + mContainer = container; + View view = mInflater.inflate(R.layout.chat_imdn, container, false); + + mBackButton = view.findViewById(R.id.back); + mBackButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (LinphoneActivity.instance().isTablet()) { + LinphoneActivity.instance().goToChat(mLocalSipuri, mRoomUri, null); + } else { + LinphoneActivity.instance().onBackPressed(); + } + } + }); + + mRead = view.findViewById(R.id.read_layout); + mDelivered = view.findViewById(R.id.delivered_layout); + mSent = view.findViewById(R.id.sent_layout); + mUndelivered = view.findViewById(R.id.undelivered_layout); + mReadHeader = view.findViewById(R.id.read_layout_header); + mDeliveredHeader = view.findViewById(R.id.delivered_layout_header); + mSentHeader = view.findViewById(R.id.sent_layout_header); + mUndeliveredHeader = view.findViewById(R.id.undelivered_layout_header); + + mBubble = new ChatMessageViewHolder(getActivity(), view.findViewById(R.id.bubble), null); + + mMessage = mRoom.findMessage(mMessageId); + mListener = + new ChatMessageListenerStub() { + @Override + public void onParticipantImdnStateChanged( + ChatMessage msg, ParticipantImdnState state) { + refreshInfo(); + } + }; + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.MESSAGE_IMDN); + } + + refreshInfo(); + if (mMessage != null) { + mMessage.addListener(mListener); + } + } + + @Override + public void onPause() { + if (mMessage != null) { + mMessage.removeListener(mListener); + } + super.onPause(); + } + + private void refreshInfo() { + if (mMessage == null) { + // TODO: error + return; + } + Address remoteSender = mMessage.getFromAddress(); + LinphoneContact contact = + ContactsManager.getInstance().findContactFromAddress(remoteSender); + + mBubble.delete.setVisibility(View.GONE); + mBubble.eventLayout.setVisibility(View.GONE); + mBubble.securityEventLayout.setVisibility(View.GONE); + mBubble.rightAnchor.setVisibility(View.GONE); + mBubble.bubbleLayout.setVisibility(View.GONE); + mBubble.bindMessage(mMessage, contact); + + mRead.removeAllViews(); + mDelivered.removeAllViews(); + mSent.removeAllViews(); + mUndelivered.removeAllViews(); + + ParticipantImdnState[] participants = + mMessage.getParticipantsByImdnState(ChatMessage.State.Displayed); + mReadHeader.setVisibility(participants.length == 0 ? View.GONE : View.VISIBLE); + boolean first = true; + for (ParticipantImdnState participant : participants) { + Address address = participant.getParticipant().getAddress(); + + LinphoneContact participantContact = + ContactsManager.getInstance().findContactFromAddress(address); + String participantDisplayName = + participantContact != null + ? participantContact.getFullName() + : LinphoneUtils.getAddressDisplayName(address); + + View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); + v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); + ((TextView) v.findViewById(R.id.time)) + .setText( + LinphoneUtils.timestampToHumanDate( + getActivity(), + participant.getStateChangeTime(), + R.string.messages_date_format)); + TextView name = v.findViewById(R.id.name); + name.setText(participantDisplayName); + if (participantContact != null) { + ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); + } else { + ContactAvatar.displayAvatar( + participantDisplayName, v.findViewById(R.id.avatar_layout)); + } + + final TextView sipUri = v.findViewById(R.id.sipUri); + sipUri.setText(address.asStringUriOnly()); + if (!LinphoneActivity.instance() + .getResources() + .getBoolean(R.bool.show_sip_uri_in_chat)) { + sipUri.setVisibility(View.GONE); + name.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + sipUri.setVisibility( + sipUri.getVisibility() == View.VISIBLE + ? View.GONE + : View.VISIBLE); + } + }); + } + + mRead.addView(v); + first = false; + } + + participants = mMessage.getParticipantsByImdnState(ChatMessage.State.DeliveredToUser); + mDeliveredHeader.setVisibility(participants.length == 0 ? View.GONE : View.VISIBLE); + first = true; + for (ParticipantImdnState participant : participants) { + Address address = participant.getParticipant().getAddress(); + + LinphoneContact participantContact = + ContactsManager.getInstance().findContactFromAddress(address); + String participantDisplayName = + participantContact != null + ? participantContact.getFullName() + : LinphoneUtils.getAddressDisplayName(address); + + View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); + v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); + ((TextView) v.findViewById(R.id.time)) + .setText( + LinphoneUtils.timestampToHumanDate( + getActivity(), + participant.getStateChangeTime(), + R.string.messages_date_format)); + TextView name = v.findViewById(R.id.name); + name.setText(participantDisplayName); + if (participantContact != null) { + ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); + } else { + ContactAvatar.displayAvatar( + participantDisplayName, v.findViewById(R.id.avatar_layout)); + } + + final TextView sipUri = v.findViewById(R.id.sipUri); + sipUri.setText(address.asStringUriOnly()); + if (!LinphoneActivity.instance() + .getResources() + .getBoolean(R.bool.show_sip_uri_in_chat)) { + sipUri.setVisibility(View.GONE); + name.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + sipUri.setVisibility( + sipUri.getVisibility() == View.VISIBLE + ? View.GONE + : View.VISIBLE); + } + }); + } + + mDelivered.addView(v); + first = false; + } + + participants = mMessage.getParticipantsByImdnState(ChatMessage.State.Delivered); + mSentHeader.setVisibility(participants.length == 0 ? View.GONE : View.VISIBLE); + first = true; + for (ParticipantImdnState participant : participants) { + Address address = participant.getParticipant().getAddress(); + + LinphoneContact participantContact = + ContactsManager.getInstance().findContactFromAddress(address); + String participantDisplayName = + participantContact != null + ? participantContact.getFullName() + : LinphoneUtils.getAddressDisplayName(address); + + View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); + v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); + ((TextView) v.findViewById(R.id.time)) + .setText( + LinphoneUtils.timestampToHumanDate( + getActivity(), + participant.getStateChangeTime(), + R.string.messages_date_format)); + TextView name = v.findViewById(R.id.name); + name.setText(participantDisplayName); + if (participantContact != null) { + ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); + } else { + ContactAvatar.displayAvatar( + participantDisplayName, v.findViewById(R.id.avatar_layout)); + } + + final TextView sipUri = v.findViewById(R.id.sipUri); + sipUri.setText(address.asStringUriOnly()); + if (!LinphoneActivity.instance() + .getResources() + .getBoolean(R.bool.show_sip_uri_in_chat)) { + sipUri.setVisibility(View.GONE); + name.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + sipUri.setVisibility( + sipUri.getVisibility() == View.VISIBLE + ? View.GONE + : View.VISIBLE); + } + }); + } + + mSent.addView(v); + first = false; + } + + participants = mMessage.getParticipantsByImdnState(ChatMessage.State.NotDelivered); + mUndeliveredHeader.setVisibility(participants.length == 0 ? View.GONE : View.VISIBLE); + first = true; + for (ParticipantImdnState participant : participants) { + Address address = participant.getParticipant().getAddress(); + + LinphoneContact participantContact = + ContactsManager.getInstance().findContactFromAddress(address); + String participantDisplayName = + participantContact != null + ? participantContact.getFullName() + : LinphoneUtils.getAddressDisplayName(address); + + View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); + v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); + TextView name = v.findViewById(R.id.name); + name.setText(participantDisplayName); + if (participantContact != null) { + ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); + } else { + ContactAvatar.displayAvatar( + participantDisplayName, v.findViewById(R.id.avatar_layout)); + } + + final TextView sipUri = v.findViewById(R.id.sipUri); + sipUri.setText(address.asStringUriOnly()); + if (!LinphoneActivity.instance() + .getResources() + .getBoolean(R.bool.show_sip_uri_in_chat)) { + sipUri.setVisibility(View.GONE); + name.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + sipUri.setVisibility( + sipUri.getVisibility() == View.VISIBLE + ? View.GONE + : View.VISIBLE); + } + }); + } + + mUndelivered.addView(v); + first = false; + } + } +} diff --git a/src/android/org/linphone/chat/ImdnFragment.java b/app/src/main/java/org/linphone/chat/ImdnOldFragment.java similarity index 52% rename from src/android/org/linphone/chat/ImdnFragment.java rename to app/src/main/java/org/linphone/chat/ImdnOldFragment.java index 59175d01c..ae38309d9 100644 --- a/src/android/org/linphone/chat/ImdnFragment.java +++ b/app/src/main/java/org/linphone/chat/ImdnOldFragment.java @@ -1,5 +1,7 @@ +package org.linphone.chat; + /* -ImdnFragment.java +ImdnOldFragment.java Copyright (C) 2010-2018 Belledonne Communications, Grenoble, France This program is free software; you can redistribute it and/or @@ -17,11 +19,8 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.linphone.chat; - import android.app.Fragment; import android.os.Bundle; -import android.support.annotation.Nullable; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.view.LayoutInflater; @@ -31,11 +30,10 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; - +import androidx.annotation.Nullable; +import org.linphone.LinphoneActivity; import org.linphone.LinphoneManager; -import org.linphone.LinphoneUtils; import org.linphone.R; -import org.linphone.activities.LinphoneActivity; import org.linphone.compatibility.Compatibility; import org.linphone.contacts.ContactsManager; import org.linphone.contacts.LinphoneContact; @@ -45,54 +43,64 @@ import org.linphone.core.ChatMessageListenerStub; import org.linphone.core.ChatRoom; import org.linphone.core.Core; import org.linphone.core.ParticipantImdnState; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.utils.FileUtils; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; -public class ImdnFragment extends Fragment { +public class ImdnOldFragment extends Fragment { private LayoutInflater mInflater; - private LinearLayout mRead, mReadHeader, mDelivered, mDeliveredHeader, mSent, mSentHeader, mUndelivered, mUndeliveredHeader; + private LinearLayout mRead, + mReadHeader, + mDelivered, + mDeliveredHeader, + mSent, + mSentHeader, + mUndelivered, + mUndeliveredHeader; private ImageView mBackButton; - private ChatBubbleViewHolder mBubble; + private ChatMessageOldViewHolder mBubble; private ViewGroup mContainer; - private String mRoomUri, mMessageId; - private Address mRoomAddr; + private String mLocalSipUri, mRoomUri, mMessageId; + private Address mLocalAddr, mRoomAddr; private ChatRoom mRoom; private ChatMessage mMessage; private ChatMessageListenerStub mListener; @Nullable @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { - mRoomUri = getArguments().getString("SipUri"); + mLocalSipUri = getArguments().getString("LocalSipUri"); + mLocalAddr = LinphoneManager.getLc().createAddress(mLocalSipUri); + mRoomUri = getArguments().getString("RemoteSipUri"); mRoomAddr = LinphoneManager.getLc().createAddress(mRoomUri); mMessageId = getArguments().getString("MessageId"); } + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); - Address proxyConfigContact = core.getDefaultProxyConfig().getContact(); - if (proxyConfigContact != null) { - mRoom = core.findOneToOneChatRoom(proxyConfigContact, mRoomAddr); - } - if (mRoom == null) { - mRoom = core.getChatRoomFromUri(mRoomAddr.asStringUriOnly()); - } + mRoom = core.getChatRoom(mRoomAddr, mLocalAddr); mInflater = inflater; mContainer = container; - View view = mInflater.inflate(R.layout.chat_imdn, container, false); + View view = mInflater.inflate(R.layout.chat_imdn_old, container, false); mBackButton = view.findViewById(R.id.back); - mBackButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (LinphoneActivity.instance().isTablet()) { - LinphoneActivity.instance().goToChat(mRoomUri, null, mRoom.getLocalAddress().asString()); - } else { - LinphoneActivity.instance().onBackPressed(); - } - } - }); + mBackButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (LinphoneActivity.instance().isTablet()) { + LinphoneActivity.instance().goToChat(mLocalSipUri, mRoomUri, null); + } else { + LinphoneActivity.instance().onBackPressed(); + } + } + }); mRead = view.findViewById(R.id.read_layout); mDelivered = view.findViewById(R.id.delivered_layout); @@ -103,7 +111,7 @@ public class ImdnFragment extends Fragment { mSentHeader = view.findViewById(R.id.sent_layout_header); mUndeliveredHeader = view.findViewById(R.id.undelivered_layout_header); - mBubble = new ChatBubbleViewHolder(view.findViewById(R.id.bubble)); + mBubble = new ChatMessageOldViewHolder(view.findViewById(R.id.bubble)); mBubble.eventLayout.setVisibility(View.GONE); mBubble.bubbleLayout.setVisibility(View.VISIBLE); mBubble.delete.setVisibility(View.GONE); @@ -115,32 +123,38 @@ public class ImdnFragment extends Fragment { mBubble.messageStatus.setVisibility(View.INVISIBLE); mBubble.messageSendingInProgress.setVisibility(View.GONE); mBubble.imdmLayout.setVisibility(View.INVISIBLE); - mBubble.contactPicture.setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); mMessage = mRoom.findMessage(mMessageId); - mListener = new ChatMessageListenerStub() { - @Override - public void onParticipantImdnStateChanged(ChatMessage msg, ParticipantImdnState state) { - refreshInfo(); - } - }; - mMessage.setListener(mListener); + mListener = + new ChatMessageListenerStub() { + @Override + public void onParticipantImdnStateChanged( + ChatMessage msg, ParticipantImdnState state) { + refreshInfo(); + } + }; + if (mMessage == null) return null; - RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + RelativeLayout.LayoutParams layoutParams = + new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, + RelativeLayout.LayoutParams.WRAP_CONTENT); layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); layoutParams.setMargins(100, 10, 10, 10); if (mMessage.isOutgoing()) { mBubble.background.setBackgroundResource(R.drawable.resizable_chat_bubble_outgoing); Compatibility.setTextAppearance(mBubble.contactName, getActivity(), R.style.font3); - Compatibility.setTextAppearance(mBubble.fileTransferAction, getActivity(), R.style.font15); - mBubble.fileTransferAction.setBackgroundResource(R.drawable.resizable_confirm_delete_button); - mBubble.contactPictureMask.setImageResource(R.drawable.avatar_chat_mask_outgoing); + Compatibility.setTextAppearance( + mBubble.fileTransferAction, getActivity(), R.style.font15); + mBubble.fileTransferAction.setBackgroundResource( + R.drawable.resizable_confirm_delete_button); } else { mBubble.background.setBackgroundResource(R.drawable.resizable_chat_bubble_incoming); - Compatibility.setTextAppearance(mBubble.contactName, getActivity(), R.style.font9); - Compatibility.setTextAppearance(mBubble.fileTransferAction, getActivity(), R.style.font8); + Compatibility.setTextAppearance( + mBubble.contactName, getActivity(), R.style.contact_organization_font); + Compatibility.setTextAppearance( + mBubble.fileTransferAction, getActivity(), R.style.button_font); mBubble.fileTransferAction.setBackgroundResource(R.drawable.resizable_assistant_button); - mBubble.contactPictureMask.setImageResource(R.drawable.avatar_chat_mask); } return view; @@ -150,12 +164,29 @@ public class ImdnFragment extends Fragment { public void onResume() { super.onResume(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.MESSAGE_IMDN); + } + if (mMessage != null) { + mMessage.addListener(mListener); + } + refreshInfo(); } + @Override + public void onPause() { + if (mMessage != null) { + mMessage.removeListener(mListener); + } + + super.onPause(); + } + private void refreshInfo() { Address remoteSender = mMessage.getFromAddress(); - LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(remoteSender); + LinphoneContact contact = + ContactsManager.getInstance().findContactFromAddress(remoteSender); String displayName; if (contact != null) { @@ -165,15 +196,16 @@ public class ImdnFragment extends Fragment { displayName = LinphoneUtils.getAddressDisplayName(remoteSender); } - mBubble.contactPicture.setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); - if (contact.hasPhoto()) { - LinphoneUtils.setThumbnailPictureFromUri(getActivity(), mBubble.contactPicture, contact.getThumbnailUri()); - } + ContactAvatar.displayAvatar(contact, mBubble.avatarLayout); } else { displayName = LinphoneUtils.getAddressDisplayName(remoteSender); - mBubble.contactPicture.setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); + ContactAvatar.displayAvatar(displayName, mBubble.avatarLayout); } - mBubble.contactName.setText(LinphoneUtils.timestampToHumanDate(getActivity(), mMessage.getTime(), R.string.messages_date_format) + " - " + displayName); + mBubble.contactName.setText( + LinphoneUtils.timestampToHumanDate( + getActivity(), mMessage.getTime(), R.string.messages_date_format) + + " - " + + displayName); if (mMessage.hasTextContent()) { String msg = mMessage.getTextContent(); @@ -186,7 +218,7 @@ public class ImdnFragment extends Fragment { String appData = mMessage.getAppdata(); if (appData != null) { // Something to display mBubble.fileName.setVisibility(View.VISIBLE); - mBubble.fileName.setText(LinphoneUtils.getNameFromFilePath(appData)); + mBubble.fileName.setText(FileUtils.getNameFromFilePath(appData)); // We purposely chose not to display the image } @@ -195,23 +227,34 @@ public class ImdnFragment extends Fragment { mSent.removeAllViews(); mUndelivered.removeAllViews(); - ParticipantImdnState[] participants = mMessage.getParticipantsByImdnState(ChatMessage.State.Displayed); + ParticipantImdnState[] participants = + mMessage.getParticipantsByImdnState(ChatMessage.State.Displayed); mReadHeader.setVisibility(participants.length == 0 ? View.GONE : View.VISIBLE); boolean first = true; for (ParticipantImdnState participant : participants) { Address address = participant.getParticipant().getAddress(); - LinphoneContact participantContact = ContactsManager.getInstance().findContactFromAddress(address); - String participantDisplayName = participantContact != null ? participantContact.getFullName() : LinphoneUtils.getAddressDisplayName(address); + LinphoneContact participantContact = + ContactsManager.getInstance().findContactFromAddress(address); + String participantDisplayName = + participantContact != null + ? participantContact.getFullName() + : LinphoneUtils.getAddressDisplayName(address); View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); - ((TextView) v.findViewById(R.id.time)).setText(LinphoneUtils.timestampToHumanDate(getActivity(), participant.getStateChangeTime(), R.string.messages_date_format)); + ((TextView) v.findViewById(R.id.time)) + .setText( + LinphoneUtils.timestampToHumanDate( + getActivity(), + participant.getStateChangeTime(), + R.string.messages_date_format)); ((TextView) v.findViewById(R.id.name)).setText(participantDisplayName); - if (participantContact != null && participantContact.hasPhoto()) { - LinphoneUtils.setThumbnailPictureFromUri(getActivity(), ((ImageView) v.findViewById(R.id.contact_picture)), participantContact.getThumbnailUri()); + if (participantContact != null) { + ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); } else { - ((ImageView) v.findViewById(R.id.contact_picture)).setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); + ContactAvatar.displayAvatar( + participantDisplayName, v.findViewById(R.id.avatar_layout)); } mRead.addView(v); @@ -224,17 +267,27 @@ public class ImdnFragment extends Fragment { for (ParticipantImdnState participant : participants) { Address address = participant.getParticipant().getAddress(); - LinphoneContact participantContact = ContactsManager.getInstance().findContactFromAddress(address); - String participantDisplayName = participantContact != null ? participantContact.getFullName() : LinphoneUtils.getAddressDisplayName(address); + LinphoneContact participantContact = + ContactsManager.getInstance().findContactFromAddress(address); + String participantDisplayName = + participantContact != null + ? participantContact.getFullName() + : LinphoneUtils.getAddressDisplayName(address); View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); - ((TextView) v.findViewById(R.id.time)).setText(LinphoneUtils.timestampToHumanDate(getActivity(), participant.getStateChangeTime(), R.string.messages_date_format)); + ((TextView) v.findViewById(R.id.time)) + .setText( + LinphoneUtils.timestampToHumanDate( + getActivity(), + participant.getStateChangeTime(), + R.string.messages_date_format)); ((TextView) v.findViewById(R.id.name)).setText(participantDisplayName); - if (participantContact != null && participantContact.hasPhoto()) { - LinphoneUtils.setThumbnailPictureFromUri(getActivity(), ((ImageView) v.findViewById(R.id.contact_picture)), participantContact.getThumbnailUri()); + if (participantContact != null) { + ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); } else { - ((ImageView) v.findViewById(R.id.contact_picture)).setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); + ContactAvatar.displayAvatar( + participantDisplayName, v.findViewById(R.id.avatar_layout)); } mDelivered.addView(v); @@ -247,17 +300,27 @@ public class ImdnFragment extends Fragment { for (ParticipantImdnState participant : participants) { Address address = participant.getParticipant().getAddress(); - LinphoneContact participantContact = ContactsManager.getInstance().findContactFromAddress(address); - String participantDisplayName = participantContact != null ? participantContact.getFullName() : LinphoneUtils.getAddressDisplayName(address); + LinphoneContact participantContact = + ContactsManager.getInstance().findContactFromAddress(address); + String participantDisplayName = + participantContact != null + ? participantContact.getFullName() + : LinphoneUtils.getAddressDisplayName(address); View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); - ((TextView) v.findViewById(R.id.time)).setText(LinphoneUtils.timestampToHumanDate(getActivity(), participant.getStateChangeTime(), R.string.messages_date_format)); + ((TextView) v.findViewById(R.id.time)) + .setText( + LinphoneUtils.timestampToHumanDate( + getActivity(), + participant.getStateChangeTime(), + R.string.messages_date_format)); ((TextView) v.findViewById(R.id.name)).setText(participantDisplayName); - if (participantContact != null && participantContact.hasPhoto()) { - LinphoneUtils.setThumbnailPictureFromUri(getActivity(), ((ImageView) v.findViewById(R.id.contact_picture)), participantContact.getThumbnailUri()); + if (participantContact != null) { + ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); } else { - ((ImageView) v.findViewById(R.id.contact_picture)).setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); + ContactAvatar.displayAvatar( + participantDisplayName, v.findViewById(R.id.avatar_layout)); } mSent.addView(v); @@ -270,16 +333,21 @@ public class ImdnFragment extends Fragment { for (ParticipantImdnState participant : participants) { Address address = participant.getParticipant().getAddress(); - LinphoneContact participantContact = ContactsManager.getInstance().findContactFromAddress(address); - String participantDisplayName = participantContact != null ? participantContact.getFullName() : LinphoneUtils.getAddressDisplayName(address); + LinphoneContact participantContact = + ContactsManager.getInstance().findContactFromAddress(address); + String participantDisplayName = + participantContact != null + ? participantContact.getFullName() + : LinphoneUtils.getAddressDisplayName(address); View v = mInflater.inflate(R.layout.chat_imdn_cell, mContainer, false); v.findViewById(R.id.separator).setVisibility(first ? View.GONE : View.VISIBLE); ((TextView) v.findViewById(R.id.name)).setText(participantDisplayName); - if (participantContact != null && participantContact.hasPhoto()) { - LinphoneUtils.setThumbnailPictureFromUri(getActivity(), ((ImageView) v.findViewById(R.id.contact_picture)), participantContact.getThumbnailUri()); + if (participantContact != null) { + ContactAvatar.displayAvatar(participantContact, v.findViewById(R.id.avatar_layout)); } else { - ((ImageView) v.findViewById(R.id.contact_picture)).setImageBitmap(ContactsManager.getInstance().getDefaultAvatarBitmap()); + ContactAvatar.displayAvatar( + participantDisplayName, v.findViewById(R.id.avatar_layout)); } mUndelivered.addView(v); diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentyEightPlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyEightPlus.java new file mode 100644 index 000000000..1235608ec --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/ApiTwentyEightPlus.java @@ -0,0 +1,109 @@ +package org.linphone.compatibility; + +/* +ApiTwentyEightPlus.java +Copyright (C) 2017 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 static org.linphone.compatibility.Compatibility.CHAT_NOTIFICATIONS_GROUP; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Person; +import android.app.usage.UsageStatsManager; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.drawable.Icon; +import org.linphone.R; +import org.linphone.notifications.Notifiable; +import org.linphone.notifications.NotifiableMessage; + +@TargetApi(28) +class ApiTwentyEightPlus { + public static Notification createMessageNotification( + Context context, Notifiable notif, Bitmap contactIcon, PendingIntent intent) { + + Person me = new Person.Builder().setName(notif.getMyself()).build(); + Notification.MessagingStyle style = new Notification.MessagingStyle(me); + for (NotifiableMessage message : notif.getMessages()) { + Icon userIcon = Icon.createWithBitmap(message.getSenderBitmap()); + Person user = + new Person.Builder().setName(message.getSender()).setIcon(userIcon).build(); + Notification.MessagingStyle.Message msg = + new Notification.MessagingStyle.Message( + message.getMessage(), message.getTime(), user); + if (message.getFilePath() != null) + msg.setData(message.getFileMime(), message.getFilePath()); + style.addMessage(msg); + } + if (notif.isGroup()) { + style.setConversationTitle(notif.getGroupTitle()); + } + style.setGroupConversation(notif.isGroup()); + + return new Notification.Builder( + context, context.getString(R.string.notification_channel_id)) + .setSmallIcon(R.drawable.topbar_chat_notification) + .setAutoCancel(true) + .setContentIntent(intent) + .setDefaults( + Notification.DEFAULT_SOUND + | Notification.DEFAULT_VIBRATE + | Notification.DEFAULT_LIGHTS) + .setLargeIcon(contactIcon) + .setCategory(Notification.CATEGORY_MESSAGE) + .setGroup(CHAT_NOTIFICATIONS_GROUP) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setPriority(Notification.PRIORITY_HIGH) + .setNumber(notif.getMessages().size()) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setColor(context.getColor(R.color.notification_led_color)) + .setStyle(style) + .addAction(ApiTwentyFourPlus.getReplyMessageAction(context, notif)) + .addAction(ApiTwentyFourPlus.getMarkMessageAsReadAction(context, notif)) + .build(); + } + + public static boolean isAppUserRestricted(Context context) { + ActivityManager activityManager = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + return activityManager.isBackgroundRestricted(); + } + + public static int getAppStandbyBucket(Context context) { + UsageStatsManager usageStatsManager = + (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); + return usageStatsManager.getAppStandbyBucket(); + } + + public static String getAppStandbyBucketNameFromValue(int bucket) { + switch (bucket) { + case UsageStatsManager.STANDBY_BUCKET_ACTIVE: + return "STANDBY_BUCKET_ACTIVE"; + case UsageStatsManager.STANDBY_BUCKET_FREQUENT: + return "STANDBY_BUCKET_FREQUENT"; + case UsageStatsManager.STANDBY_BUCKET_RARE: + return "STANDBY_BUCKET_RARE"; + case UsageStatsManager.STANDBY_BUCKET_WORKING_SET: + return "STANDBY_BUCKET_WORKING_SET"; + } + return null; + } +} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentyFourPlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyFourPlus.java new file mode 100644 index 000000000..a3e7995e9 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/ApiTwentyFourPlus.java @@ -0,0 +1,219 @@ +package org.linphone.compatibility; + +/* +ApiTwentyFourPlus.java +Copyright (C) 2017 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 static org.linphone.compatibility.Compatibility.CHAT_NOTIFICATIONS_GROUP; +import static org.linphone.compatibility.Compatibility.INTENT_ANSWER_CALL_NOTIF_ACTION; +import static org.linphone.compatibility.Compatibility.INTENT_HANGUP_CALL_NOTIF_ACTION; +import static org.linphone.compatibility.Compatibility.INTENT_LOCAL_IDENTITY; +import static org.linphone.compatibility.Compatibility.INTENT_MARK_AS_READ_ACTION; +import static org.linphone.compatibility.Compatibility.INTENT_NOTIF_ID; +import static org.linphone.compatibility.Compatibility.INTENT_REPLY_NOTIF_ACTION; +import static org.linphone.compatibility.Compatibility.KEY_TEXT_REPLY; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import org.linphone.R; +import org.linphone.notifications.Notifiable; +import org.linphone.notifications.NotifiableMessage; +import org.linphone.notifications.NotificationBroadcastReceiver; + +@TargetApi(24) +class ApiTwentyFourPlus { + + public static Notification createRepliedNotification(Context context, String reply) { + return new Notification.Builder(context) + .setSmallIcon(R.drawable.topbar_chat_notification) + .setContentText( + context.getString(R.string.notification_replied_label).replace("%s", reply)) + .build(); + } + + public static Notification createMessageNotification( + Context context, Notifiable notif, Bitmap contactIcon, PendingIntent intent) { + + Notification.MessagingStyle style = new Notification.MessagingStyle(notif.getMyself()); + for (NotifiableMessage message : notif.getMessages()) { + Notification.MessagingStyle.Message msg = + new Notification.MessagingStyle.Message( + message.getMessage(), message.getTime(), message.getSender()); + if (message.getFilePath() != null) + msg.setData(message.getFileMime(), message.getFilePath()); + style.addMessage(msg); + } + if (notif.isGroup()) { + style.setConversationTitle(notif.getGroupTitle()); + } + + return new Notification.Builder(context) + .setSmallIcon(R.drawable.topbar_chat_notification) + .setAutoCancel(true) + .setContentIntent(intent) + .setDefaults( + Notification.DEFAULT_SOUND + | Notification.DEFAULT_VIBRATE + | Notification.DEFAULT_LIGHTS) + .setLargeIcon(contactIcon) + .setCategory(Notification.CATEGORY_MESSAGE) + .setGroup(CHAT_NOTIFICATIONS_GROUP) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setPriority(Notification.PRIORITY_HIGH) + .setNumber(notif.getMessages().size()) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setColor(context.getColor(R.color.notification_led_color)) + .setStyle(style) + .addAction(getReplyMessageAction(context, notif)) + .addAction(getMarkMessageAsReadAction(context, notif)) + .build(); + } + + public static Notification createInCallNotification( + Context context, + int callId, + boolean showAnswerAction, + String msg, + int iconID, + Bitmap contactIcon, + String contactName, + PendingIntent intent) { + + Notification.Builder builder = + new Notification.Builder(context) + .setContentTitle(contactName) + .setContentText(msg) + .setSmallIcon(iconID) + .setAutoCancel(false) + .setContentIntent(intent) + .setLargeIcon(contactIcon) + .setCategory(Notification.CATEGORY_CALL) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setPriority(Notification.PRIORITY_HIGH) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setColor(context.getColor(R.color.notification_led_color)) + .addAction(getCallDeclineAction(context, callId)); + + if (showAnswerAction) { + builder.addAction(getCallAnswerAction(context, callId)); + } + + return builder.build(); + } + + public static Notification.Action getReplyMessageAction(Context context, Notifiable notif) { + String replyLabel = context.getResources().getString(R.string.notification_reply_label); + RemoteInput remoteInput = + new RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build(); + + Intent replyIntent = new Intent(context, NotificationBroadcastReceiver.class); + replyIntent.setAction(INTENT_REPLY_NOTIF_ACTION); + replyIntent.putExtra(INTENT_NOTIF_ID, notif.getNotificationId()); + replyIntent.putExtra(INTENT_LOCAL_IDENTITY, notif.getLocalIdentity()); + + PendingIntent replyPendingIntent = + PendingIntent.getBroadcast( + context, + notif.getNotificationId(), + replyIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + Notification.Action replyAction = + new Notification.Action.Builder( + R.drawable.chat_send_over, + context.getString(R.string.notification_reply_label), + replyPendingIntent) + .addRemoteInput(remoteInput) + .setAllowGeneratedReplies(true) + .build(); + return replyAction; + } + + public static Notification.Action getMarkMessageAsReadAction( + Context context, Notifiable notif) { + Intent markAsReadIntent = new Intent(context, NotificationBroadcastReceiver.class); + markAsReadIntent.setAction(INTENT_MARK_AS_READ_ACTION); + markAsReadIntent.putExtra(INTENT_NOTIF_ID, notif.getNotificationId()); + markAsReadIntent.putExtra(INTENT_LOCAL_IDENTITY, notif.getLocalIdentity()); + + PendingIntent markAsReadPendingIntent = + PendingIntent.getBroadcast( + context, + notif.getNotificationId(), + markAsReadIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + Notification.Action markAsReadAction = + new Notification.Action.Builder( + R.drawable.chat_send_over, + context.getString(R.string.notification_mark_as_read_label), + markAsReadPendingIntent) + .build(); + + return markAsReadAction; + } + + public static Notification.Action getCallAnswerAction(Context context, int callId) { + Intent answerIntent = new Intent(context, NotificationBroadcastReceiver.class); + answerIntent.setAction(INTENT_ANSWER_CALL_NOTIF_ACTION); + answerIntent.putExtra(INTENT_NOTIF_ID, callId); + + PendingIntent answerPendingIntent = + PendingIntent.getBroadcast( + context, callId, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + Notification.Action answerAction = + new Notification.Action.Builder( + R.drawable.call_audio_start, + context.getString(R.string.notification_call_answer_label), + answerPendingIntent) + .build(); + + return answerAction; + } + + public static Notification.Action getCallDeclineAction(Context context, int callId) { + Intent hangupIntent = new Intent(context, NotificationBroadcastReceiver.class); + hangupIntent.setAction(INTENT_HANGUP_CALL_NOTIF_ACTION); + hangupIntent.putExtra(INTENT_NOTIF_ID, callId); + + PendingIntent hangupPendingIntent = + PendingIntent.getBroadcast( + context, callId, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + Notification.Action declineAction = + new Notification.Action.Builder( + R.drawable.call_hangup, + context.getString(R.string.notification_call_hangup_label), + hangupPendingIntent) + .build(); + return declineAction; + } + + public static void closeContentProviderClient(ContentProviderClient client) { + client.close(); + } +} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentyOnePlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyOnePlus.java new file mode 100644 index 000000000..e84eab1ca --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/ApiTwentyOnePlus.java @@ -0,0 +1,215 @@ +package org.linphone.compatibility; + +/* +ApiTwentyOnePlus.java +Copyright (C) 2017 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.annotation.TargetApi; +import android.app.Activity; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.ContentProviderClient; +import android.content.Context; +import android.graphics.Bitmap; +import android.view.WindowManager; +import androidx.core.content.ContextCompat; +import org.linphone.R; + +@TargetApi(21) +class ApiTwentyOnePlus { + + @SuppressWarnings("deprecation") + public static Notification createMessageNotification( + Context context, + int msgCount, + String msgSender, + String msg, + Bitmap contactIcon, + PendingIntent intent) { + String title; + if (msgCount == 1) { + title = msgSender; + } else { + title = + context.getString(R.string.unread_messages) + .replace("%i", String.valueOf(msgCount)); + } + + return new Notification.Builder(context) + .setContentTitle(title) + .setContentText(msg) + .setSmallIcon(R.drawable.topbar_chat_notification) + .setAutoCancel(true) + .setContentIntent(intent) + .setDefaults( + Notification.DEFAULT_SOUND + | Notification.DEFAULT_VIBRATE + | Notification.DEFAULT_LIGHTS) + .setLargeIcon(contactIcon) + .setLights( + ContextCompat.getColor(context, R.color.notification_led_color), + context.getResources().getInteger(R.integer.notification_ms_on), + context.getResources().getInteger(R.integer.notification_ms_off)) + .setCategory(Notification.CATEGORY_MESSAGE) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setPriority(Notification.PRIORITY_HIGH) + .setNumber(msgCount) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .build(); + } + + public static Notification createInCallNotification( + Context context, + String msg, + int iconID, + Bitmap contactIcon, + String contactName, + PendingIntent intent) { + + return new Notification.Builder(context) + .setContentTitle(contactName) + .setContentText(msg) + .setSmallIcon(iconID) + .setAutoCancel(false) + .setContentIntent(intent) + .setLargeIcon(contactIcon) + .setCategory(Notification.CATEGORY_CALL) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setPriority(Notification.PRIORITY_HIGH) + .setLights( + ContextCompat.getColor(context, R.color.notification_led_color), + context.getResources().getInteger(R.integer.notification_ms_on), + context.getResources().getInteger(R.integer.notification_ms_off)) + .setShowWhen(true) + .build(); + } + + public static Notification createNotification( + Context context, + String title, + String message, + int icon, + int level, + Bitmap largeIcon, + PendingIntent intent, + int priority) { + Notification notif; + + if (largeIcon != null) { + notif = + new Notification.Builder(context) + .setContentTitle(title) + .setContentText(message) + .setSmallIcon(icon, level) + .setLargeIcon(largeIcon) + .setContentIntent(intent) + .setCategory(Notification.CATEGORY_SERVICE) + .setVisibility(Notification.VISIBILITY_SECRET) + .setLights( + ContextCompat.getColor(context, R.color.notification_led_color), + context.getResources().getInteger(R.integer.notification_ms_on), + context.getResources() + .getInteger(R.integer.notification_ms_off)) + .setWhen(System.currentTimeMillis()) + .setPriority(priority) + .setShowWhen(true) + .build(); + } else { + notif = + new Notification.Builder(context) + .setContentTitle(title) + .setContentText(message) + .setSmallIcon(icon, level) + .setContentIntent(intent) + .setCategory(Notification.CATEGORY_SERVICE) + .setVisibility(Notification.VISIBILITY_SECRET) + .setLights( + ContextCompat.getColor(context, R.color.notification_led_color), + context.getResources().getInteger(R.integer.notification_ms_on), + context.getResources() + .getInteger(R.integer.notification_ms_off)) + .setPriority(priority) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .build(); + } + + return notif; + } + + public static Notification createMissedCallNotification( + Context context, String title, String text, PendingIntent intent) { + + return new Notification.Builder(context) + .setContentTitle(title) + .setContentText(text) + .setSmallIcon(R.drawable.call_status_missed) + .setAutoCancel(true) + .setContentIntent(intent) + .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) + .setCategory(Notification.CATEGORY_EVENT) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setLights( + ContextCompat.getColor(context, R.color.notification_led_color), + context.getResources().getInteger(R.integer.notification_ms_on), + context.getResources().getInteger(R.integer.notification_ms_off)) + .setPriority(Notification.PRIORITY_HIGH) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .build(); + } + + public static Notification createSimpleNotification( + Context context, String title, String text, PendingIntent intent) { + + return new Notification.Builder(context) + .setContentTitle(title) + .setContentText(text) + .setSmallIcon(R.drawable.linphone_logo) + .setAutoCancel(true) + .setContentIntent(intent) + .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) + .setCategory(Notification.CATEGORY_MESSAGE) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setLights( + ContextCompat.getColor(context, R.color.notification_led_color), + context.getResources().getInteger(R.integer.notification_ms_on), + context.getResources().getInteger(R.integer.notification_ms_off)) + .setWhen(System.currentTimeMillis()) + .setPriority(Notification.PRIORITY_HIGH) + .setShowWhen(true) + .build(); + } + + public static void closeContentProviderClient(ContentProviderClient client) { + client.release(); + } + + public static void setShowWhenLocked(Activity activity, boolean enable) { + if (enable) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + } + } + + public static void setTurnScreenOn(Activity activity, boolean enable) { + if (enable) { + activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/ApiTwentySixPlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentySixPlus.java new file mode 100644 index 000000000..07ae1c969 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/ApiTwentySixPlus.java @@ -0,0 +1,263 @@ +package org.linphone.compatibility; + +/* +ApiTwentySixPlus.java +Copyright (C) 2017 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 static org.linphone.compatibility.Compatibility.CHAT_NOTIFICATIONS_GROUP; + +import android.annotation.TargetApi; +import android.app.FragmentTransaction; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Build; +import android.provider.Settings; +import org.linphone.R; +import org.linphone.notifications.Notifiable; +import org.linphone.notifications.NotifiableMessage; + +@TargetApi(26) +class ApiTwentySixPlus { + public static String getDeviceName(Context context) { + String name = + Settings.Global.getString( + context.getContentResolver(), Settings.Global.DEVICE_NAME); + if (name == null) { + name = BluetoothAdapter.getDefaultAdapter().getName(); + } + if (name == null) { + name = Settings.Secure.getString(context.getContentResolver(), "bluetooth_name"); + } + if (name == null) { + name = Build.MANUFACTURER + " " + Build.MODEL; + } + return name; + } + + public static Notification createRepliedNotification(Context context, String reply) { + return new Notification.Builder( + context, context.getString(R.string.notification_channel_id)) + .setSmallIcon(R.drawable.topbar_chat_notification) + .setContentText( + context.getString(R.string.notification_replied_label).replace("%s", reply)) + .build(); + } + + public static void createServiceChannel(Context context) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + // Create service/call notification channel + String id = context.getString(R.string.notification_service_channel_id); + CharSequence name = context.getString(R.string.content_title_notification_service); + String description = context.getString(R.string.content_title_notification_service); + NotificationChannel channel = + new NotificationChannel(id, name, NotificationManager.IMPORTANCE_NONE); + channel.setDescription(description); + channel.enableVibration(false); + channel.enableLights(false); + channel.setShowBadge(false); + notificationManager.createNotificationChannel(channel); + } + + public static void createMessageChannel(Context context) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + // Create message notification channel + String id = context.getString(R.string.notification_channel_id); + String name = context.getString(R.string.content_title_notification); + String description = context.getString(R.string.content_title_notification); + NotificationChannel channel = + new NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH); + channel.setDescription(description); + channel.setLightColor(context.getColor(R.color.notification_led_color)); + channel.enableLights(true); + channel.enableVibration(true); + channel.setShowBadge(true); + notificationManager.createNotificationChannel(channel); + } + + public static Notification createMessageNotification( + Context context, Notifiable notif, Bitmap contactIcon, PendingIntent intent) { + + Notification.MessagingStyle style = new Notification.MessagingStyle(notif.getMyself()); + for (NotifiableMessage message : notif.getMessages()) { + Notification.MessagingStyle.Message msg = + new Notification.MessagingStyle.Message( + message.getMessage(), message.getTime(), message.getSender()); + if (message.getFilePath() != null) + msg.setData(message.getFileMime(), message.getFilePath()); + style.addMessage(msg); + } + if (notif.isGroup()) { + style.setConversationTitle(notif.getGroupTitle()); + } + + return new Notification.Builder( + context, context.getString(R.string.notification_channel_id)) + .setSmallIcon(R.drawable.topbar_chat_notification) + .setAutoCancel(true) + .setContentIntent(intent) + .setDefaults( + Notification.DEFAULT_SOUND + | Notification.DEFAULT_VIBRATE + | Notification.DEFAULT_LIGHTS) + .setLargeIcon(contactIcon) + .setCategory(Notification.CATEGORY_MESSAGE) + .setGroup(CHAT_NOTIFICATIONS_GROUP) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setPriority(Notification.PRIORITY_HIGH) + .setNumber(notif.getMessages().size()) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setColor(context.getColor(R.color.notification_led_color)) + .setStyle(style) + .addAction(ApiTwentyFourPlus.getReplyMessageAction(context, notif)) + .addAction(ApiTwentyFourPlus.getMarkMessageAsReadAction(context, notif)) + .build(); + } + + public static Notification createInCallNotification( + Context context, + int callId, + boolean showAnswerAction, + String msg, + int iconID, + Bitmap contactIcon, + String contactName, + PendingIntent intent) { + + Notification.Builder builder = + new Notification.Builder( + context, + context.getString(R.string.notification_service_channel_id)) + .setContentTitle(contactName) + .setContentText(msg) + .setSmallIcon(iconID) + .setAutoCancel(false) + .setContentIntent(intent) + .setLargeIcon(contactIcon) + .setCategory(Notification.CATEGORY_CALL) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setPriority(Notification.PRIORITY_HIGH) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setColor(context.getColor(R.color.notification_led_color)) + .addAction(ApiTwentyFourPlus.getCallDeclineAction(context, callId)); + + if (showAnswerAction) { + builder.addAction(ApiTwentyFourPlus.getCallAnswerAction(context, callId)); + } + return builder.build(); + } + + public static Notification createNotification( + Context context, + String title, + String message, + int icon, + int level, + Bitmap largeIcon, + PendingIntent intent, + int priority) { + + if (largeIcon != null) { + return new Notification.Builder( + context, context.getString(R.string.notification_service_channel_id)) + .setContentTitle(title) + .setContentText(message) + .setSmallIcon(icon, level) + .setLargeIcon(largeIcon) + .setContentIntent(intent) + .setCategory(Notification.CATEGORY_SERVICE) + .setVisibility(Notification.VISIBILITY_SECRET) + .setPriority(priority) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setColor(context.getColor(R.color.notification_led_color)) + .build(); + } else { + return new Notification.Builder( + context, context.getString(R.string.notification_service_channel_id)) + .setContentTitle(title) + .setContentText(message) + .setSmallIcon(icon, level) + .setContentIntent(intent) + .setCategory(Notification.CATEGORY_SERVICE) + .setVisibility(Notification.VISIBILITY_SECRET) + .setPriority(priority) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setColor(context.getColor(R.color.notification_led_color)) + .build(); + } + } + + public static Notification createMissedCallNotification( + Context context, String title, String text, PendingIntent intent) { + return new Notification.Builder( + context, context.getString(R.string.notification_channel_id)) + .setContentTitle(title) + .setContentText(text) + .setSmallIcon(R.drawable.call_status_missed) + .setAutoCancel(true) + .setContentIntent(intent) + .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) + .setCategory(Notification.CATEGORY_EVENT) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setPriority(Notification.PRIORITY_HIGH) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setColor(context.getColor(R.color.notification_led_color)) + .build(); + } + + public static Notification createSimpleNotification( + Context context, String title, String text, PendingIntent intent) { + return new Notification.Builder( + context, context.getString(R.string.notification_channel_id)) + .setContentTitle(title) + .setContentText(text) + .setSmallIcon(R.drawable.linphone_logo) + .setAutoCancel(true) + .setContentIntent(intent) + .setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE) + .setCategory(Notification.CATEGORY_MESSAGE) + .setVisibility(Notification.VISIBILITY_PRIVATE) + .setPriority(Notification.PRIORITY_HIGH) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setColorized(true) + .setColor(context.getColor(R.color.notification_led_color)) + .build(); + } + + public static void startService(Context context, Intent intent) { + context.startForegroundService(intent); + } + + public static void setFragmentTransactionReorderingAllowed( + FragmentTransaction transaction, boolean allowed) { + transaction.setReorderingAllowed(allowed); + } +} diff --git a/src/android/org/linphone/compatibility/ApiTwentyThreePlus.java b/app/src/main/java/org/linphone/compatibility/ApiTwentyThreePlus.java similarity index 79% rename from src/android/org/linphone/compatibility/ApiTwentyThreePlus.java rename to app/src/main/java/org/linphone/compatibility/ApiTwentyThreePlus.java index 5e7124df4..e960b810e 100644 --- a/src/android/org/linphone/compatibility/ApiTwentyThreePlus.java +++ b/app/src/main/java/org/linphone/compatibility/ApiTwentyThreePlus.java @@ -1,8 +1,5 @@ package org.linphone.compatibility; -import android.annotation.TargetApi; -import android.widget.TextView; - /* ApiTwentyThreePlus.java Copyright (C) 2017 Belledonne Communications, Grenoble, France @@ -22,9 +19,18 @@ 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.annotation.TargetApi; +import android.content.Context; +import android.os.PowerManager; +import android.widget.TextView; + @TargetApi(23) -public class ApiTwentyThreePlus { +class ApiTwentyThreePlus { public static void setTextAppearance(TextView textview, int style) { textview.setTextAppearance(style); } + + public static boolean isAppIdleMode(Context context) { + return ((PowerManager) context.getSystemService(Context.POWER_SERVICE)).isDeviceIdleMode(); + } } diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.java b/app/src/main/java/org/linphone/compatibility/Compatibility.java new file mode 100644 index 000000000..f969935b1 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.java @@ -0,0 +1,240 @@ +package org.linphone.compatibility; +/* +Compatibility.java +Copyright (C) 2017 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.Activity; +import android.app.FragmentTransaction; +import android.app.Notification; +import android.app.PendingIntent; +import android.bluetooth.BluetoothAdapter; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Build; +import android.provider.Settings; +import android.widget.TextView; +import org.linphone.mediastream.Version; +import org.linphone.notifications.Notifiable; + +public class Compatibility { + public static final String CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP"; + public static final String KEY_TEXT_REPLY = "key_text_reply"; + public static final String INTENT_NOTIF_ID = "NOTIFICATION_ID"; + public static final String INTENT_REPLY_NOTIF_ACTION = "org.linphone.REPLY_ACTION"; + public static final String INTENT_HANGUP_CALL_NOTIF_ACTION = "org.linphone.HANGUP_CALL_ACTION"; + public static final String INTENT_ANSWER_CALL_NOTIF_ACTION = "org.linphone.ANSWER_CALL_ACTION"; + public static final String INTENT_LOCAL_IDENTITY = "LOCAL_IDENTITY"; + public static final String INTENT_MARK_AS_READ_ACTION = "org.linphone.MARK_AS_READ_ACTION"; + + public static String getDeviceName(Context context) { + if (Version.sdkAboveOrEqual(25)) { + return ApiTwentySixPlus.getDeviceName(context); + } + + String name = BluetoothAdapter.getDefaultAdapter().getName(); + if (name == null) { + name = Settings.Secure.getString(context.getContentResolver(), "bluetooth_name"); + } + if (name == null) { + name = Build.MANUFACTURER + " " + Build.MODEL; + } + return name; + } + + public static void createNotificationChannels(Context context) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + ApiTwentySixPlus.createServiceChannel(context); + ApiTwentySixPlus.createMessageChannel(context); + } + } + + public static Notification createSimpleNotification( + Context context, String title, String text, PendingIntent intent) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + return ApiTwentySixPlus.createSimpleNotification(context, title, text, intent); + } + return ApiTwentyOnePlus.createSimpleNotification(context, title, text, intent); + } + + public static Notification createMissedCallNotification( + Context context, String title, String text, PendingIntent intent) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + return ApiTwentySixPlus.createMissedCallNotification(context, title, text, intent); + } + return ApiTwentyOnePlus.createMissedCallNotification(context, title, text, intent); + } + + public static Notification createMessageNotification( + Context context, + Notifiable notif, + String msgSender, + String msg, + Bitmap contactIcon, + PendingIntent intent) { + if (Version.sdkAboveOrEqual(28)) { + return ApiTwentyEightPlus.createMessageNotification( + context, notif, contactIcon, intent); + } else if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + return ApiTwentySixPlus.createMessageNotification(context, notif, contactIcon, intent); + } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { + return ApiTwentyFourPlus.createMessageNotification(context, notif, contactIcon, intent); + } + return ApiTwentyOnePlus.createMessageNotification( + context, notif.getMessages().size(), msgSender, msg, contactIcon, intent); + } + + public static Notification createRepliedNotification(Context context, String reply) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + return ApiTwentySixPlus.createRepliedNotification(context, reply); + } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { + return ApiTwentyFourPlus.createRepliedNotification(context, reply); + } + return null; + } + + public static Notification createInCallNotification( + Context context, + int callId, + boolean showAnswerAction, + String msg, + int iconID, + Bitmap contactIcon, + String contactName, + PendingIntent intent) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + return ApiTwentySixPlus.createInCallNotification( + context, + callId, + showAnswerAction, + msg, + iconID, + contactIcon, + contactName, + intent); + } else if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { + return ApiTwentyFourPlus.createInCallNotification( + context, + callId, + showAnswerAction, + msg, + iconID, + contactIcon, + contactName, + intent); + } + return ApiTwentyOnePlus.createInCallNotification( + context, msg, iconID, contactIcon, contactName, intent); + } + + public static Notification createNotification( + Context context, + String title, + String message, + int icon, + int iconLevel, + Bitmap largeIcon, + PendingIntent intent, + int priority) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + return ApiTwentySixPlus.createNotification( + context, title, message, icon, iconLevel, largeIcon, intent, priority); + } + return ApiTwentyOnePlus.createNotification( + context, title, message, icon, iconLevel, largeIcon, intent, priority); + } + + public static boolean canDrawOverlays(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + return Settings.canDrawOverlays(context); + } + return true; + } + + public static void setTextAppearance(TextView textview, Context context, int style) { + if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { + ApiTwentyThreePlus.setTextAppearance(textview, style); + } else { + textview.setTextAppearance(context, style); + } + } + + public static void startService(Context context, Intent intent) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + ApiTwentySixPlus.startService(context, intent); + } else { + context.startService(intent); + } + } + + public static void setFragmentTransactionReorderingAllowed( + FragmentTransaction transaction, boolean allowed) { + if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + ApiTwentySixPlus.setFragmentTransactionReorderingAllowed(transaction, allowed); + } + } + + public static void closeContentProviderClient(ContentProviderClient client) { + if (Version.sdkAboveOrEqual(Version.API24_NOUGAT_70)) { + ApiTwentyFourPlus.closeContentProviderClient(client); + } else { + ApiTwentyOnePlus.closeContentProviderClient(client); + } + } + + public static boolean isAppUserRestricted(Context context) { + if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { + return ApiTwentyEightPlus.isAppUserRestricted(context); + } + return false; + } + + public static boolean isAppIdleMode(Context context) { + if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { + return ApiTwentyThreePlus.isAppIdleMode(context); + } + return false; + } + + public static int getAppStandbyBucket(Context context) { + if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { + return ApiTwentyEightPlus.getAppStandbyBucket(context); + } + return 0; + } + + public static String getAppStandbyBucketNameFromValue(int bucket) { + if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { + return ApiTwentyEightPlus.getAppStandbyBucketNameFromValue(bucket); + } + return null; + } + + public static void setShowWhenLocked(Activity activity, boolean enable) { + if (Version.sdkStrictlyBelow(Version.API27_OREO_81)) { + ApiTwentyOnePlus.setShowWhenLocked(activity, enable); + } + } + + public static void setTurnScreenOn(Activity activity, boolean enable) { + if (Version.sdkStrictlyBelow(Version.API27_OREO_81)) { + ApiTwentyOnePlus.setTurnScreenOn(activity, enable); + } + } +} diff --git a/src/android/org/linphone/compatibility/CompatibilityScaleGestureDetector.java b/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureDetector.java similarity index 91% rename from src/android/org/linphone/compatibility/CompatibilityScaleGestureDetector.java rename to app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureDetector.java index d24e4d110..14bdcef30 100644 --- a/src/android/org/linphone/compatibility/CompatibilityScaleGestureDetector.java +++ b/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureDetector.java @@ -19,13 +19,12 @@ 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.annotation.TargetApi; import android.content.Context; import android.view.MotionEvent; import android.view.ScaleGestureDetector; -@TargetApi(8) -public class CompatibilityScaleGestureDetector extends ScaleGestureDetector.SimpleOnScaleGestureListener { +public class CompatibilityScaleGestureDetector + extends ScaleGestureDetector.SimpleOnScaleGestureListener { private ScaleGestureDetector detector; private CompatibilityScaleGestureListener listener; @@ -58,4 +57,4 @@ public class CompatibilityScaleGestureDetector extends ScaleGestureDetector.Simp listener = null; detector = null; } -} \ No newline at end of file +} diff --git a/src/android/org/linphone/compatibility/CompatibilityScaleGestureListener.java b/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureListener.java similarity index 92% rename from src/android/org/linphone/compatibility/CompatibilityScaleGestureListener.java rename to app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureListener.java index bd4393385..8bc83e8fc 100644 --- a/src/android/org/linphone/compatibility/CompatibilityScaleGestureListener.java +++ b/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureListener.java @@ -20,5 +20,5 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ public interface CompatibilityScaleGestureListener { - public boolean onScale(CompatibilityScaleGestureDetector detector); -} \ No newline at end of file + boolean onScale(CompatibilityScaleGestureDetector detector); +} diff --git a/app/src/main/java/org/linphone/contacts/AndroidContact.java b/app/src/main/java/org/linphone/contacts/AndroidContact.java new file mode 100644 index 000000000..e3e8a1601 --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/AndroidContact.java @@ -0,0 +1,659 @@ +package org.linphone.contacts; + +/* +AndroidContact.java +Copyright (C) 2018 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.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.RawContacts; +import java.io.Serializable; +import java.util.ArrayList; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.core.tools.Log; + +class AndroidContact implements Serializable { + protected String mAndroidId, mAndroidRawId, mAndroidLookupKey; + protected boolean isAndroidRawIdLinphone; + + private transient ArrayList mChangesToCommit; + + protected AndroidContact() { + mChangesToCommit = new ArrayList<>(); + isAndroidRawIdLinphone = false; + } + + protected String getAndroidId() { + return mAndroidId; + } + + protected void setAndroidId(String id) { + mAndroidId = id; + } + + protected String getAndroidLookupKey() { + return mAndroidLookupKey; + } + + protected void setAndroidLookupKey(String lookupKey) { + mAndroidLookupKey = lookupKey; + } + + protected Uri getAndroidLookupUri() { + return ContactsContract.Contacts.getLookupUri( + Long.parseLong(mAndroidId), getAndroidLookupKey()); + } + + protected boolean isAndroidContact() { + return mAndroidId != null; + } + + protected void addChangesToCommit(ContentProviderOperation operation) { + Log.i("[Contact] Added operation " + operation); + mChangesToCommit.add(operation); + } + + protected void saveChangesCommited() { + if (ContactsManager.getInstance().hasReadContactsAccess() && mChangesToCommit.size() > 0) { + try { + ContentResolver contentResolver = LinphoneService.instance().getContentResolver(); + ContentProviderResult[] results = + contentResolver.applyBatch(ContactsContract.AUTHORITY, mChangesToCommit); + if (results != null + && results.length > 0 + && results[0] != null + && results[0].uri != null) { + String rawId = String.valueOf(ContentUris.parseId(results[0].uri)); + if (mAndroidId == null) { + Log.i("[Contact] Contact created with RAW ID " + rawId); + + final String[] projection = + new String[] {ContactsContract.RawContacts.CONTACT_ID}; + final Cursor cursor = + contentResolver.query(results[0].uri, projection, null, null, null); + if (cursor != null) { + cursor.moveToNext(); + long contactId = cursor.getLong(0); + mAndroidId = String.valueOf(contactId); + cursor.close(); + Log.i("[Contact] Contact created with ID " + mAndroidId); + } + } else { + if (mAndroidRawId == null || !isAndroidRawIdLinphone) { + Log.i( + "[Contact] Linphone RAW ID " + + rawId + + " created from existing RAW ID " + + mAndroidRawId); + mAndroidRawId = rawId; + isAndroidRawIdLinphone = true; + } + } + } + } catch (Exception e) { + Log.e("[Contact] Exception while saving changes: " + e); + } finally { + mChangesToCommit.clear(); + } + } + } + + protected void createAndroidContact() { + if (LinphoneManager.getInstance() + .getContext() + .getResources() + .getBoolean(R.bool.use_linphone_tag)) { + Log.i("[Contact] Creating contact using linphone account type"); + addChangesToCommit( + ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) + .withValue( + RawContacts.ACCOUNT_TYPE, + ContactsManager.getInstance() + .getString(R.string.sync_account_type)) + .withValue( + RawContacts.ACCOUNT_NAME, + ContactsManager.getInstance() + .getString(R.string.sync_account_name)) + .withValue( + RawContacts.AGGREGATION_MODE, + RawContacts.AGGREGATION_MODE_DEFAULT) + .build()); + isAndroidRawIdLinphone = true; + } else { + Log.i("[Contact] Creating contact using default account type"); + addChangesToCommit( + ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) + .withValue(RawContacts.ACCOUNT_TYPE, null) + .withValue(RawContacts.ACCOUNT_NAME, null) + .withValue( + RawContacts.AGGREGATION_MODE, + RawContacts.AGGREGATION_MODE_DEFAULT) + .build()); + } + } + + protected void deleteAndroidContact() { + ContactsManager.getInstance().delete(mAndroidId); + } + + protected Uri getContactThumbnailPictureUri() { + Uri person = ContentUris.withAppendedId(Contacts.CONTENT_URI, Long.parseLong(mAndroidId)); + return Uri.withAppendedPath(person, Contacts.Photo.CONTENT_DIRECTORY); + } + + protected Uri getContactPictureUri() { + Uri person = ContentUris.withAppendedId(Contacts.CONTENT_URI, Long.parseLong(mAndroidId)); + return Uri.withAppendedPath(person, Contacts.Photo.DISPLAY_PHOTO); + } + + protected void setName(String fn, String ln) { + if ((fn == null || fn.isEmpty()) && (ln == null || ln.isEmpty())) { + Log.e("[Contact] Can't set both first and last name to null or empty"); + return; + } + + if (mAndroidId == null) { + Log.i("[Contact] Setting given & family name " + fn + " " + ln + " to new contact."); + addChangesToCommit( + ContentProviderOperation.newInsert(Data.CONTENT_URI) + .withValueBackReference(Data.RAW_CONTACT_ID, 0) + .withValue( + Data.MIMETYPE, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.StructuredName.GIVEN_NAME, fn) + .withValue(CommonDataKinds.StructuredName.FAMILY_NAME, ln) + .build()); + } else { + Log.i( + "[Contact] Setting given & family name " + + fn + + " " + + ln + + " to existing contact " + + mAndroidId + + " (" + + mAndroidRawId + + ")"); + String select = + ContactsContract.Data.CONTACT_ID + + "=? AND " + + ContactsContract.Data.MIMETYPE + + "=?"; + String[] args = + new String[] { + getAndroidId(), + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE + }; + + addChangesToCommit( + ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) + .withSelection(select, args) + .withValue( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.StructuredName + .CONTENT_ITEM_TYPE) + .withValue( + ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, fn) + .withValue( + ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, ln) + .build()); + } + } + + protected void addNumberOrAddress(String value, String oldValueToReplace, boolean isSIP) { + if (value == null || value.isEmpty()) { + Log.e("[Contact] Can't add null or empty number or address"); + return; + } + + if (oldValueToReplace != null) { + if (mAndroidId == null) { + Log.e("[Contact] Can't update a number or address in non existing contact"); + return; + } + + Log.i( + "[Contact] Updating " + + oldValueToReplace + + " by " + + value + + " in contact " + + mAndroidId + + " (" + + mAndroidRawId + + ")"); + if (isSIP) { + String select = + ContactsContract.Data.CONTACT_ID + + "=? AND (" + + ContactsContract.Data.MIMETYPE + + "=? OR " + + ContactsContract.Data.MIMETYPE + + "=? OR " + + ContactsContract.Data.MIMETYPE + + "=?) AND data1=?"; + String[] args = + new String[] { + mAndroidId, + "vnd.android.cursor.item/org.linphone.profile", // Old value + ContactsManager.getInstance() + .getString(R.string.linphone_address_mime_type), + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, + oldValueToReplace + }; + + addChangesToCommit( + ContentProviderOperation.newUpdate(Data.CONTENT_URI) + .withSelection(select, args) + .withValue( + Data.MIMETYPE, + ContactsManager.getInstance() + .getString(R.string.linphone_address_mime_type)) + .withValue("data1", value) // Value + .withValue( + "data2", + ContactsManager.getInstance() + .getString(R.string.app_name)) // Summary + .withValue("data3", value) // Detail + .build()); + } else { + String select = + ContactsContract.Data.CONTACT_ID + + "=? AND " + + ContactsContract.Data.MIMETYPE + + "=? AND data1=?"; + String[] args = + new String[] { + mAndroidId, + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, + oldValueToReplace + }; + + addChangesToCommit( + ContentProviderOperation.newUpdate(Data.CONTENT_URI) + .withSelection(select, args) + .withValue(Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value) + .withValue( + ContactsContract.CommonDataKinds.Phone.TYPE, + CommonDataKinds.Phone.TYPE_MOBILE) + .build()); + } + } else { + if (mAndroidId == null) { + Log.i("[Contact] Adding number or address " + value + " to new contact."); + if (isSIP) { + addChangesToCommit( + ContentProviderOperation.newInsert(Data.CONTENT_URI) + .withValueBackReference(Data.RAW_CONTACT_ID, 0) + .withValue( + Data.MIMETYPE, + ContactsManager.getInstance() + .getString(R.string.linphone_address_mime_type)) + .withValue("data1", value) // Value + .withValue( + "data2", + ContactsManager.getInstance() + .getString(R.string.app_name)) // Summary + .withValue("data3", value) // Detail + .build()); + } else { + addChangesToCommit( + ContentProviderOperation.newInsert(Data.CONTENT_URI) + .withValueBackReference(Data.RAW_CONTACT_ID, 0) + .withValue( + Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value) + .withValue( + ContactsContract.CommonDataKinds.Phone.TYPE, + CommonDataKinds.Phone.TYPE_MOBILE) + .build()); + } + } else { + Log.i( + "[Contact] Adding number or address " + + value + + " to existing contact " + + mAndroidId + + " (" + + mAndroidRawId + + ")"); + if (isSIP) { + addChangesToCommit( + ContentProviderOperation.newInsert(Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) + .withValue( + Data.MIMETYPE, + ContactsManager.getInstance() + .getString(R.string.linphone_address_mime_type)) + .withValue("data1", value) // Value + .withValue( + "data2", + ContactsManager.getInstance() + .getString(R.string.app_name)) // Summary + .withValue("data3", value) // Detail + .build()); + } else { + addChangesToCommit( + ContentProviderOperation.newInsert(Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) + .withValue( + Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, value) + .withValue( + ContactsContract.CommonDataKinds.Phone.TYPE, + CommonDataKinds.Phone.TYPE_MOBILE) + .build()); + } + } + } + } + + protected void removeNumberOrAddress(String noa, boolean isSIP) { + if (noa == null || noa.isEmpty()) { + Log.e("[Contact] Can't remove null or empty number or address."); + return; + } + + if (mAndroidId == null) { + Log.e("[Contact] Can't remove a number or address from non existing contact"); + return; + } else { + Log.i( + "[Contact] Removing number or address " + + noa + + " from existing contact " + + mAndroidId + + " (" + + mAndroidRawId + + ")"); + if (isSIP) { + String select = + ContactsContract.Data.CONTACT_ID + + "=? AND (" + + ContactsContract.Data.MIMETYPE + + "=? OR " + + ContactsContract.Data.MIMETYPE + + "=?) AND data1=?"; + String[] args = + new String[] { + mAndroidId, + ContactsManager.getInstance() + .getString(R.string.linphone_address_mime_type), + ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, + noa + }; + + addChangesToCommit( + ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection(select, args) + .build()); + } else { + String select = + ContactsContract.Data.CONTACT_ID + + "=? AND " + + ContactsContract.Data.MIMETYPE + + "=? AND data1=?"; + String[] args = + new String[] { + mAndroidId, + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, + noa + }; + + addChangesToCommit( + ContentProviderOperation.newDelete(ContactsContract.Data.CONTENT_URI) + .withSelection(select, args) + .build()); + } + } + } + + protected void setOrganization(String org, String previousValue) { + if (org == null || org.isEmpty()) { + if (mAndroidId == null) { + Log.e("[Contact] Can't set organization to null or empty for new contact"); + return; + } + } + if (mAndroidId == null) { + Log.i("[Contact] Setting organization " + org + " to new contact."); + addChangesToCommit( + ContentProviderOperation.newInsert(Data.CONTENT_URI) + .withValueBackReference(Data.RAW_CONTACT_ID, 0) + .withValue( + Data.MIMETYPE, CommonDataKinds.Organization.CONTENT_ITEM_TYPE) + .withValue(CommonDataKinds.Organization.COMPANY, org) + .build()); + } else { + if (previousValue != null) { + String select = + ContactsContract.Data.CONTACT_ID + + "=? AND " + + ContactsContract.Data.MIMETYPE + + "=? AND " + + ContactsContract.CommonDataKinds.Organization.COMPANY + + "=?"; + String[] args = + new String[] { + getAndroidId(), + ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE, + previousValue + }; + + Log.i( + "[Contact] Updating organization " + + org + + " to existing contact " + + mAndroidId + + " (" + + mAndroidRawId + + ")"); + addChangesToCommit( + ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI) + .withSelection(select, args) + .withValue( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Organization + .CONTENT_ITEM_TYPE) + .withValue( + ContactsContract.CommonDataKinds.Organization.COMPANY, org) + .build()); + } else { + Log.i( + "[Contact] Setting organization " + + org + + " to existing contact " + + mAndroidId + + " (" + + mAndroidRawId + + ")"); + addChangesToCommit( + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) + .withValue( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Organization + .CONTENT_ITEM_TYPE) + .withValue( + ContactsContract.CommonDataKinds.Organization.COMPANY, org) + .build()); + } + } + } + + protected void setPhoto(byte[] photo) { + if (photo == null) { + Log.e("[Contact] Can't set null picture."); + return; + } + + if (mAndroidId == null) { + Log.i("[Contact] Setting picture to new contact."); + addChangesToCommit( + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) + .withValue( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, photo) + .build()); + } else { + Log.i( + "[Contact] Setting picture to existing contact " + + mAndroidId + + " (" + + mAndroidRawId + + ")"); + addChangesToCommit( + ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) + .withValue(ContactsContract.Data.RAW_CONTACT_ID, mAndroidRawId) + .withValue( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE) + .withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, photo) + .withValue(ContactsContract.Data.IS_PRIMARY, 1) + .withValue(ContactsContract.Data.IS_SUPER_PRIMARY, 1) + .build()); + } + } + + protected String findRawContactID() { + ContentResolver resolver = LinphoneService.instance().getContentResolver(); + String result = null; + String[] projection = {ContactsContract.RawContacts._ID}; + + String selection = ContactsContract.RawContacts.CONTACT_ID + "=?"; + Cursor c = + resolver.query( + ContactsContract.RawContacts.CONTENT_URI, + projection, + selection, + new String[] {mAndroidId}, + null); + if (c != null) { + if (c.moveToFirst()) { + result = c.getString(c.getColumnIndex(ContactsContract.RawContacts._ID)); + } + c.close(); + } + return result; + } + + protected void createRawLinphoneContactFromExistingAndroidContactIfNeeded(String fullName) { + if (LinphoneManager.getInstance() + .getContext() + .getResources() + .getBoolean(R.bool.use_linphone_tag)) { + if (mAndroidId != null && (mAndroidRawId == null || !isAndroidRawIdLinphone)) { + if (mAndroidRawId == null) { + Log.i("[Contact] RAW ID not found for contact " + mAndroidId); + mAndroidRawId = findRawContactID(); + } + Log.i("[Contact] Found RAW ID for contact " + mAndroidId + " : " + mAndroidRawId); + + String linphoneRawId = findLinphoneRawContactId(); + if (linphoneRawId == null) { + Log.i("[Contact] Linphone RAW ID not found for contact " + mAndroidId); + createRawLinphoneContactFromExistingAndroidContact(); + } else { + Log.i( + "[Contact] Linphone RAW ID found for contact " + + mAndroidId + + " : " + + linphoneRawId); + mAndroidRawId = linphoneRawId; + } + isAndroidRawIdLinphone = true; + } + } + } + + private void createRawLinphoneContactFromExistingAndroidContact() { + addChangesToCommit( + ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) + .withValue( + ContactsContract.RawContacts.ACCOUNT_TYPE, + ContactsManager.getInstance().getString(R.string.sync_account_type)) + .withValue( + ContactsContract.RawContacts.ACCOUNT_NAME, + ContactsManager.getInstance().getString(R.string.sync_account_name)) + .withValue( + ContactsContract.RawContacts.AGGREGATION_MODE, + ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT) + .build()); + + addChangesToCommit( + ContentProviderOperation.newUpdate( + ContactsContract.AggregationExceptions.CONTENT_URI) + .withValue( + ContactsContract.AggregationExceptions.TYPE, + ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER) + .withValue( + ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, + mAndroidRawId) + .withValueBackReference( + ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, 0) + .build()); + + Log.i( + "[Contact] Creating linphone RAW contact for contact " + + mAndroidId + + " linked with existing RAW contact " + + mAndroidRawId); + saveChangesCommited(); + } + + private String findLinphoneRawContactId() { + ContentResolver resolver = LinphoneService.instance().getContentResolver(); + String result = null; + String[] projection = {ContactsContract.RawContacts._ID}; + + String selection = + ContactsContract.RawContacts.CONTACT_ID + + "=? AND " + + ContactsContract.RawContacts.ACCOUNT_TYPE + + "=?"; + Cursor c = + resolver.query( + ContactsContract.RawContacts.CONTENT_URI, + projection, + selection, + new String[] { + mAndroidId, + ContactsManager.getInstance().getString(R.string.sync_account_type) + }, + null); + if (c != null) { + if (c.moveToFirst()) { + result = c.getString(c.getColumnIndex(ContactsContract.RawContacts._ID)); + } + c.close(); + } + return result; + } +} diff --git a/app/src/main/java/org/linphone/contacts/AsyncContactsLoader.java b/app/src/main/java/org/linphone/contacts/AsyncContactsLoader.java new file mode 100644 index 000000000..c74feddf8 --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/AsyncContactsLoader.java @@ -0,0 +1,244 @@ +package org.linphone.contacts; + +/* +AsyncContactsLoader.java +Copyright (C) 2018 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.annotation.SuppressLint; +import android.content.Context; +import android.database.Cursor; +import android.os.AsyncTask; +import android.provider.ContactsContract; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.core.Core; +import org.linphone.core.Friend; +import org.linphone.core.FriendList; +import org.linphone.core.PresenceBasicStatus; +import org.linphone.core.PresenceModel; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneUtils; + +class AsyncContactsLoader extends AsyncTask { + @SuppressLint("InlinedApi") + public static final String[] PROJECTION = { + ContactsContract.Data.CONTACT_ID, + ContactsContract.Contacts.LOOKUP_KEY, + ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, + ContactsContract.Data.MIMETYPE, + "data1", // Company, Phone or SIP Address + "data2", // ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME + "data3", // ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME + "data4", // Normalized phone number + }; + + private Context mContext; + + public AsyncContactsLoader(Context context) { + mContext = context; + } + + @Override + protected void onPreExecute() { + Log.i("[Contacts Manager] Synchronization started"); + if (mContext == null) { + mContext = LinphoneService.instance().getApplicationContext(); + } + + if (LinphonePreferences.instance().isFriendlistsubscriptionEnabled()) { + String rls = mContext.getString(R.string.rls_uri); + for (FriendList list : LinphoneManager.getLc().getFriendsLists()) { + if (rls != null + && (list.getRlsAddress() == null + || !list.getRlsAddress().asStringUriOnly().equals(rls))) { + list.setRlsUri(rls); + } + list.addListener(ContactsManager.getInstance()); + } + } + } + + @Override + protected AsyncContactsData doInBackground(Void... params) { + Log.i("[Contacts Manager] Background synchronization started"); + Cursor c = + mContext.getContentResolver() + .query( + ContactsContract.Data.CONTENT_URI, + PROJECTION, + ContactsContract.Data.IN_VISIBLE_GROUP + " == 1", + null, + null); + + HashMap androidContactsCache = new HashMap<>(); + AsyncContactsData data = new AsyncContactsData(); + List nativeIds = new ArrayList<>(); + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + for (FriendList list : lc.getFriendsLists()) { + for (Friend friend : list.getFriends()) { + if (isCancelled()) return data; + + LinphoneContact contact = (LinphoneContact) friend.getUserData(); + if (contact != null) { + contact.clearAddresses(); + if (contact.getAndroidId() != null) { + androidContactsCache.put(contact.getAndroidId(), contact); + nativeIds.add(contact.getAndroidId()); + } + } else { + if (friend.getRefKey() != null) { + // Friend has a refkey but no LinphoneContact => represents a + // native contact stored in db from a previous version of Linphone, + // remove it + list.removeFriend(friend); + } else { + // No refkey so it's a standalone contact + contact = new LinphoneContact(); + contact.setFriend(friend); + contact.syncValuesFromFriend(); + data.contacts.add(contact); + } + } + } + } + } + + if (c != null) { + while (c.moveToNext()) { + if (isCancelled()) return data; + + String id = c.getString(c.getColumnIndex(ContactsContract.Data.CONTACT_ID)); + String lookupKey = + c.getString(c.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)); + + LinphoneContact contact = androidContactsCache.get(id); + if (contact == null) { + nativeIds.add(id); + contact = new LinphoneContact(); + contact.setAndroidId(id); + contact.setAndroidLookupKey(lookupKey); + androidContactsCache.put(id, contact); + } + + contact.syncValuesFromAndroidCusor(c); + } + c.close(); + + for (FriendList list : lc.getFriendsLists()) { + for (Friend friend : list.getFriends()) { + if (isCancelled()) return data; + + LinphoneContact contact = (LinphoneContact) friend.getUserData(); + if (contact != null && contact.isAndroidContact()) { + String id = contact.getAndroidId(); + if (id != null && !nativeIds.contains(id)) { + // Has been removed since last fetch + androidContactsCache.remove(id); + } + } + } + } + nativeIds.clear(); + } + + for (LinphoneContact contact : androidContactsCache.values()) { + if (isCancelled()) return data; + + if (contact.getFullName() == null) { + for (LinphoneNumberOrAddress noa : contact.getNumbersOrAddresses()) { + if (noa.isSIPAddress()) { + contact.setFullName(LinphoneUtils.getAddressDisplayName(noa.getValue())); + Log.w( + "[Contacts Manager] Couldn't find a display name for contact " + + contact.getFullName() + + ", used SIP address display name / username instead..."); + break; + } + } + } + + if (contact.getFriend() != null) { + for (LinphoneNumberOrAddress noa : contact.getNumbersOrAddresses()) { + PresenceModel pm = + contact.getFriend().getPresenceModelForUriOrTel(noa.getValue()); + if (pm != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { + data.sipContacts.add(contact); + break; + } + } + } + + if (!mContext.getResources().getBoolean(R.bool.hide_sip_contacts_without_presence)) { + if (contact.hasAddress() && !data.sipContacts.contains(contact)) { + data.sipContacts.add(contact); + } + } + data.contacts.add(contact); + } + + androidContactsCache.clear(); + + Collections.sort(data.contacts); + Collections.sort(data.sipContacts); + + Log.i("[Contacts Manager] Background synchronization finished"); + return data; + } + + @Override + protected void onPostExecute(AsyncContactsData data) { + for (LinphoneContact contact : data.contacts) { + contact.createOrUpdateFriendFromNativeContact(); + } + + // Now that contact fetching is asynchronous, this is required to ensure + // presence subscription event will be sent with all friends + if (LinphonePreferences.instance().isFriendlistsubscriptionEnabled()) { + for (FriendList list : LinphoneManager.getLc().getFriendsLists()) { + list.updateSubscriptions(); + } + } + + ContactsManager.getInstance().setContacts(data.contacts); + ContactsManager.getInstance().setSipContacts(data.sipContacts); + + for (ContactsUpdatedListener listener : + ContactsManager.getInstance().getContactsListeners()) { + listener.onContactsUpdated(); + } + Log.i("[Contacts Manager] Synchronization finished"); + } + + class AsyncContactsData { + final List contacts; + final List sipContacts; + + AsyncContactsData() { + contacts = new ArrayList<>(); + sipContacts = new ArrayList<>(); + } + } +} diff --git a/src/android/org/linphone/contacts/ContactAddress.java b/app/src/main/java/org/linphone/contacts/ContactAddress.java similarity index 63% rename from src/android/org/linphone/contacts/ContactAddress.java rename to app/src/main/java/org/linphone/contacts/ContactAddress.java index 78a6d2e5e..9bec5e07b 100644 --- a/src/android/org/linphone/contacts/ContactAddress.java +++ b/app/src/main/java/org/linphone/contacts/ContactAddress.java @@ -20,64 +20,73 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import android.view.View; - +import java.io.Serializable; import org.linphone.core.Address; import org.linphone.core.Factory; -import org.linphone.core.SearchResult; - -import java.io.Serializable; +import org.linphone.core.FriendCapability; public class ContactAddress implements Serializable { - private LinphoneContact contact; - private SearchResult result; - private String address; - private String phoneNumber; - private boolean isLinphoneContact; - private boolean isSelect = false; - private boolean isAdmin = false; - private transient View view; + private LinphoneContact mContact; + private String mAddress; + private String mPhoneNumber; + private boolean mIsLinphoneContact; + private boolean mIsSelect = false; + private boolean mIsAdmin = false; + private transient View mView; + + public ContactAddress(LinphoneContact c, String a, String pn, boolean isLC) { + init(c, a, pn, isLC); + } + + public ContactAddress(LinphoneContact c, String a, String pn, boolean isLC, boolean isAdmin) { + init(c, a, pn, isLC); + mIsAdmin = isAdmin; + } public boolean isAdmin() { - return isAdmin; + return mIsAdmin; } public void setAdmin(boolean admin) { - isAdmin = admin; + mIsAdmin = admin; } public boolean isSelect() { - return isSelect; + return mIsSelect; } - public void setView(View v) { - view = v; + public void setSelect(boolean select) { + mIsSelect = select; } public View getView() { - return view; + return mView; + } + + public void setView(View v) { + mView = v; } public LinphoneContact getContact() { - return contact; - } - - public SearchResult getResult() { - return result; - } - - public void setResult(SearchResult result) { - this.result = result; + return mContact; } public String getAddressAsDisplayableString() { Address addr = getAddress(); if (addr != null && addr.getUsername() != null) return addr.asStringUriOnly(); - return address; + return mAddress; } public Address getAddress() { - String presence = contact.getPresenceModelForUriOrTel((phoneNumber != null && !phoneNumber.isEmpty()) ? phoneNumber : address); - Address addr = Factory.instance().createAddress(presence != null ? presence : address); + String presence = null; + if (mContact != null) { + presence = + mContact.getContactFromPresenceModelForUriOrTel( + (mPhoneNumber != null && !mPhoneNumber.isEmpty()) + ? mPhoneNumber + : mAddress); + } + Address addr = Factory.instance().createAddress(presence != null ? presence : mAddress); // Remove the user=phone URI param if existing, it will break everything otherwise if (addr.hasUriParam("user")) { addr.removeUriParam("user"); @@ -86,8 +95,8 @@ public class ContactAddress implements Serializable { } public String getDisplayName() { - if (address != null) { - Address addr = Factory.instance().createAddress(address); + if (mAddress != null) { + Address addr = Factory.instance().createAddress(mAddress); if (addr != null) { return addr.getDisplayName(); } @@ -96,8 +105,8 @@ public class ContactAddress implements Serializable { } public String getUsername() { - if (address != null) { - Address addr = Factory.instance().createAddress(address); + if (mAddress != null) { + Address addr = Factory.instance().createAddress(mAddress); if (addr != null) { return addr.getUsername(); } @@ -106,31 +115,18 @@ public class ContactAddress implements Serializable { } public String getPhoneNumber() { - return phoneNumber; + return mPhoneNumber; } - public void setSelect(boolean select) { - isSelect = select; - } - - public boolean isLinphoneContact() { - return isLinphoneContact; + public boolean hasCapability(FriendCapability capability) { + return mContact != null && mContact.hasFriendCapability(capability); } private void init(LinphoneContact c, String a, String pn, boolean isLC) { - contact = c; - address = a; - phoneNumber = pn; - isLinphoneContact = isLC; - } - - public ContactAddress(LinphoneContact c, String a, String pn, boolean isLC) { - init(c, a, pn, isLC); - } - - public ContactAddress(LinphoneContact c, String a, String pn, boolean isLC, boolean isAdmin) { - init(c, a, pn, isLC); - this.isAdmin = isAdmin; + mContact = c; + mAddress = a; + mPhoneNumber = pn; + mIsLinphoneContact = isLC; } @Override @@ -138,8 +134,8 @@ public class ContactAddress implements Serializable { if (other == null) return false; if (other == this) return true; if (!(other instanceof ContactAddress)) return false; - if (((ContactAddress) other).getAddressAsDisplayableString() == this.getAddressAsDisplayableString()) - return true; - return false; + return ((ContactAddress) other) + .getAddressAsDisplayableString() + .equals(getAddressAsDisplayableString()); } } diff --git a/app/src/main/java/org/linphone/contacts/ContactDetailsFragment.java b/app/src/main/java/org/linphone/contacts/ContactDetailsFragment.java new file mode 100644 index 000000000..65891acd9 --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/ContactDetailsFragment.java @@ -0,0 +1,398 @@ +package org.linphone.contacts; + +/* +ContactDetailsFragment.java +Copyright (C) 2017 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.annotation.SuppressLint; +import android.app.Dialog; +import android.app.Fragment; +import android.content.Intent; +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.Button; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TableLayout; +import android.widget.TextView; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.Address; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomBackend; +import org.linphone.core.ChatRoomListenerStub; +import org.linphone.core.ChatRoomParams; +import org.linphone.core.Core; +import org.linphone.core.Factory; +import org.linphone.core.FriendCapability; +import org.linphone.core.PresenceBasicStatus; +import org.linphone.core.PresenceModel; +import org.linphone.core.ProxyConfig; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; + +public class ContactDetailsFragment extends Fragment + implements OnClickListener, ContactsUpdatedListener { + private LinphoneContact mContact; + private ImageView mEditContact, mDeleteContact, mBack; + private TextView mOrganization; + private RelativeLayout mWaitLayout; + private LayoutInflater mInflater; + private View mView; + private boolean mDisplayChatAddressOnly = false; + private ChatRoom mChatRoom; + private ChatRoomListenerStub mChatRoomCreationListener; + + private final OnClickListener mDialListener = + new OnClickListener() { + @Override + public void onClick(View v) { + if (LinphoneActivity.isInstanciated()) { + String tag = (String) v.getTag(); + LinphoneActivity.instance() + .setAddresGoToDialerAndCall(tag, mContact.getFullName()); + } + } + }; + + private final OnClickListener mChatListener = + new OnClickListener() { + @Override + public void onClick(View v) { + if (LinphoneActivity.isInstanciated()) { + String tag = (String) v.getTag(); + Core lc = LinphoneManager.getLc(); + Address participant = Factory.instance().createAddress(tag); + ProxyConfig defaultProxyConfig = lc.getDefaultProxyConfig(); + boolean isSecured = v.getId() == R.id.contact_chat_secured; + + if (defaultProxyConfig != null) { + ChatRoom room = + lc.findOneToOneChatRoom( + defaultProxyConfig.getContact(), + participant, + isSecured); + if (room != null) { + LinphoneActivity.instance() + .goToChat( + room.getLocalAddress().asStringUriOnly(), + room.getPeerAddress().asStringUriOnly(), + null); + } else { + if (defaultProxyConfig.getConferenceFactoryUri() != null + && (isSecured + || !LinphonePreferences.instance() + .useBasicChatRoomFor1To1())) { + mWaitLayout.setVisibility(View.VISIBLE); + + ChatRoomParams params = lc.createDefaultChatRoomParams(); + params.enableEncryption(isSecured); + params.enableGroup(false); + // We don't want a basic chat room, + // so if isSecured is false we have to set this manually + params.setBackend(ChatRoomBackend.FlexisipChat); + + Address participants[] = new Address[1]; + participants[0] = participant; + + mChatRoom = + lc.createChatRoom( + params, + getString(R.string.dummy_group_chat_subject), + participants); + if (mChatRoom != null) { + mChatRoom.addListener(mChatRoomCreationListener); + } else { + Log.w( + "[Contact Details Fragment] createChatRoom returned null..."); + mWaitLayout.setVisibility(View.GONE); + } + } else { + room = lc.getChatRoom(participant); + LinphoneActivity.instance() + .goToChat( + room.getLocalAddress().asStringUriOnly(), + room.getPeerAddress().asStringUriOnly(), + null); + } + } + } + } + } + }; + + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mContact = (LinphoneContact) getArguments().getSerializable("Contact"); + + this.mInflater = inflater; + mView = inflater.inflate(R.layout.contact, container, false); + + if (getArguments() != null) { + mDisplayChatAddressOnly = getArguments().getBoolean("ChatAddressOnly"); + } + + mWaitLayout = mView.findViewById(R.id.waitScreen); + mWaitLayout.setVisibility(View.GONE); + + mEditContact = mView.findViewById(R.id.editContact); + mEditContact.setOnClickListener(this); + + mDeleteContact = mView.findViewById(R.id.deleteContact); + mDeleteContact.setOnClickListener(this); + + mOrganization = mView.findViewById(R.id.contactOrganization); + boolean isOrgVisible = getResources().getBoolean(R.bool.display_contact_organization); + String org = mContact.getOrganization(); + if (org != null && !org.isEmpty() && isOrgVisible) { + mOrganization.setText(org); + } else { + mOrganization.setVisibility(View.GONE); + } + + mBack = mView.findViewById(R.id.back); + if (getResources().getBoolean(R.bool.isTablet)) { + mBack.setVisibility(View.INVISIBLE); + } else { + mBack.setOnClickListener(this); + } + + mChatRoomCreationListener = + new ChatRoomListenerStub() { + @Override + public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { + if (newState == ChatRoom.State.Created) { + mWaitLayout.setVisibility(View.GONE); + LinphoneActivity.instance() + .goToChat( + cr.getLocalAddress().asStringUriOnly(), + cr.getPeerAddress().asStringUriOnly(), + null); + } else if (newState == ChatRoom.State.CreationFailed) { + mWaitLayout.setVisibility(View.GONE); + LinphoneActivity.instance().displayChatRoomError(); + Log.e( + "Group chat room for address " + + cr.getPeerAddress() + + " has failed !"); + } + } + }; + + return mView; + } + + public void changeDisplayedContact(LinphoneContact newContact) { + mContact = newContact; + displayContact(mInflater, mView); + } + + @SuppressLint("InflateParams") + private void displayContact(LayoutInflater inflater, View view) { + ContactAvatar.displayAvatar(mContact, view.findViewById(R.id.avatar_layout)); + + TextView contactName = view.findViewById(R.id.contact_name); + contactName.setText(mContact.getFullName()); + mOrganization.setText( + (mContact.getOrganization() != null) ? mContact.getOrganization() : ""); + + TableLayout controls = view.findViewById(R.id.controls); + controls.removeAllViews(); + for (LinphoneNumberOrAddress noa : mContact.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 = 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 = v.findViewById(R.id.numeroOrAddress); + tv.setText(displayednumberOrAddress); + tv.setSelected(true); + + ProxyConfig lpc = LinphoneManager.getLc().getDefaultProxyConfig(); + if (lpc != null) { + String username = lpc.normalizePhoneNumber(displayednumberOrAddress); + if (username != null) { + value = LinphoneUtils.getFullAddressFromUsername(username); + } + } + + v.findViewById(R.id.friendLinphone).setVisibility(View.GONE); + if (mContact.getFriend() != null) { + PresenceModel pm = mContact.getFriend().getPresenceModelForUriOrTel(noa.getValue()); + if (pm != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { + v.findViewById(R.id.friendLinphone).setVisibility(View.VISIBLE); + } else { + if (getResources() + .getBoolean(R.bool.hide_numbers_and_addresses_without_presence)) { + skip = true; + } + } + } + + v.findViewById(R.id.inviteFriend).setVisibility(View.GONE); + if (!noa.isSIPAddress() + && v.findViewById(R.id.friendLinphone).getVisibility() == View.GONE + && !getResources().getBoolean(R.bool.hide_invite_contact)) { + v.findViewById(R.id.inviteFriend).setVisibility(View.VISIBLE); + v.findViewById(R.id.inviteFriend).setTag(noa.getNormalizedPhone()); + v.findViewById(R.id.inviteFriend) + .setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + String number = (String) v.getTag(); + Intent smsIntent = new Intent(Intent.ACTION_SENDTO); + smsIntent.putExtra("address", number); + smsIntent.setData(Uri.parse("smsto:" + number)); + String text = + getString(R.string.invite_friend_text) + .replace( + "%s", + getString(R.string.download_link)); + smsIntent.putExtra("sms_body", text); + startActivity(smsIntent); + } + }); + } + + String contactAddress = mContact.getContactFromPresenceModelForUriOrTel(noa.getValue()); + if (!mDisplayChatAddressOnly) { + v.findViewById(R.id.contact_call).setOnClickListener(mDialListener); + 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(mChatListener); + v.findViewById(R.id.contact_chat_secured).setOnClickListener(mChatListener); + if (contactAddress != null) { + v.findViewById(R.id.contact_chat).setTag(contactAddress); + v.findViewById(R.id.contact_chat_secured).setTag(contactAddress); + } else { + v.findViewById(R.id.contact_chat).setTag(value); + v.findViewById(R.id.contact_chat_secured).setTag(value); + } + + if (v.findViewById(R.id.friendLinphone).getVisibility() == View.VISIBLE + && mContact.hasPresenceModelForUriOrTelCapability( + noa.getValue(), FriendCapability.LimeX3Dh)) { + v.findViewById(R.id.contact_chat_secured).setVisibility(View.VISIBLE); + } else { + v.findViewById(R.id.contact_chat_secured).setVisibility(View.GONE); + } + + if (getResources().getBoolean(R.bool.disable_chat)) { + v.findViewById(R.id.contact_chat).setVisibility(View.GONE); + v.findViewById(R.id.contact_chat_secured).setVisibility(View.GONE); + } + + if (!skip) { + controls.addView(v); + } + } + } + + @Override + public void onContactsUpdated() { + LinphoneContact contact = + ContactsManager.getInstance().findContactFromAndroidId(mContact.getAndroidId()); + if (contact != null) { + changeDisplayedContact(contact); + } + } + + @Override + public void onResume() { + super.onResume(); + + ContactsManager.getInstance().addContactsListener(this); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.CONTACT_DETAIL); + } + displayContact(mInflater, mView); + } + + @Override + public void onPause() { + if (mChatRoom != null) { + mChatRoom.removeListener(mChatRoomCreationListener); + } + ContactsManager.getInstance().removeContactsListener(this); + super.onPause(); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + + if (id == R.id.editContact) { + ContactsManager.getInstance().editContact(getActivity(), mContact, null); + } else if (id == R.id.deleteContact) { + final Dialog dialog = + LinphoneActivity.instance().displayDialog(getString(R.string.delete_text)); + Button delete = dialog.findViewById(R.id.dialog_delete_button); + Button cancel = dialog.findViewById(R.id.dialog_cancel_button); + + delete.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + mContact.delete(); + // To ensure removed contact won't appear in the contacts list anymore + ContactsManager.getInstance().fetchContactsAsync(); + LinphoneActivity.instance().displayContacts(false); + dialog.dismiss(); + } + }); + + cancel.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + dialog.dismiss(); + } + }); + dialog.show(); + } else if (id == R.id.back) { + getFragmentManager().popBackStackImmediate(); + } + } +} diff --git a/app/src/main/java/org/linphone/contacts/ContactEditorFragment.java b/app/src/main/java/org/linphone/contacts/ContactEditorFragment.java new file mode 100644 index 000000000..5bc796a0f --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/ContactEditorFragment.java @@ -0,0 +1,676 @@ +package org.linphone.contacts; + +/* +ContactEditorFragment.java +Copyright (C) 2017 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.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.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 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.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.tools.Log; +import org.linphone.mediastream.Version; +import org.linphone.utils.FileUtils; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; + +public class ContactEditorFragment extends Fragment { + private static final int ADD_PHOTO = 1337; + private static final int PHOTO_SIZE = 128; + + private View mView; + private ImageView mCancel, mDeleteContact, mOk; + private ImageView mAddNumber, mAddSipAddress, mContactPicture; + private LinearLayout mPhoneNumbersSection, mSipAddressesSection; + private EditText mFirstName, mLastName, mOrganization; + private LayoutInflater mInflater; + private boolean mIsNewContact; + private LinphoneContact mContact; + private List mNumbersAndAddresses; + private int mFirstSipAddressIndex = -1; + private LinearLayout mSipAddresses, mNumbers; + private String mNewSipOrNumberToAdd, mNewDisplayName; + private Uri mPickedPhotoForContactUri; + private byte[] mPhotoToAdd; + + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mInflater = inflater; + + mContact = null; + mIsNewContact = true; + + if (getArguments() != null) { + Serializable obj = getArguments().getSerializable("Contact"); + if (obj != null) { + mContact = (LinphoneContact) obj; + mContact.createRawLinphoneContactFromExistingAndroidContactIfNeeded( + mContact.getFullName()); + mIsNewContact = false; + if (getArguments().getString("NewSipAdress") != null) { + mNewSipOrNumberToAdd = getArguments().getString("NewSipAdress"); + } + if (getArguments().getString("NewDisplayName") != null) { + mNewDisplayName = getArguments().getString("NewDisplayName"); + } + } else if (getArguments().getString("NewSipAdress") != null) { + mNewSipOrNumberToAdd = getArguments().getString("NewSipAdress"); + if (getArguments().getString("NewDisplayName") != null) { + mNewDisplayName = getArguments().getString("NewDisplayName"); + } + } + } + + mView = inflater.inflate(R.layout.contact_edit, container, false); + + mPhoneNumbersSection = mView.findViewById(R.id.phone_numbers); + if (getResources().getBoolean(R.bool.hide_phone_numbers_in_editor) + || !ContactsManager.getInstance().hasReadContactsAccess()) { + // Currently linphone friends don't support phone mNumbers, so hide them + mPhoneNumbersSection.setVisibility(View.GONE); + } + + mSipAddressesSection = mView.findViewById(R.id.sip_addresses); + if (getResources().getBoolean(R.bool.hide_sip_addresses_in_editor)) { + mSipAddressesSection.setVisibility(View.GONE); + } + + mDeleteContact = mView.findViewById(R.id.delete_contact); + + mCancel = mView.findViewById(R.id.cancel); + mCancel.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + getFragmentManager().popBackStackImmediate(); + } + }); + + mOk = mView.findViewById(R.id.ok); + mOk.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + if (mIsNewContact) { + boolean areAllFielsEmpty = true; + for (LinphoneNumberOrAddress nounoa : mNumbersAndAddresses) { + if (nounoa.getValue() != null && !nounoa.getValue().equals("")) { + areAllFielsEmpty = false; + break; + } + } + if (areAllFielsEmpty) { + getFragmentManager().popBackStackImmediate(); + return; + } + mContact = LinphoneContact.createContact(); + } + + mContact.setFirstNameAndLastName( + mFirstName.getText().toString(), + mLastName.getText().toString(), + true); + + if (mPhotoToAdd != null) { + mContact.setPhoto(mPhotoToAdd); + } + + for (LinphoneNumberOrAddress noa : mNumbersAndAddresses) { + if (noa.getValue() == null || noa.getValue().isEmpty()) { + if (noa.getOldValue() != null && !noa.getOldValue().isEmpty()) { + Log.i("[Contact Editor] Removing number " + noa.getOldValue()); + mContact.removeNumberOrAddress(noa); + } + } else { + if (noa.getOldValue() != null + && noa.getOldValue().equals(noa.getValue())) { + Log.i( + "[Contact Editor] Keeping existing number " + + noa.getValue()); + continue; + } + + if (noa.isSIPAddress()) { + noa.setValue( + LinphoneUtils.getFullAddressFromUsername( + noa.getValue())); + } + Log.i("[Contact Editor] Adding new number " + noa.getValue()); + mContact.addOrUpdateNumberOrAddress(noa); + } + } + + if (!mOrganization.getText().toString().isEmpty() || !mIsNewContact) { + mContact.setOrganization(mOrganization.getText().toString(), true); + } + + mContact.save(); + + if (mIsNewContact) { + // Ensure fetch will be done so the new contact appears in the contacts + // list: contacts content observer may not be notified if contacts sync + // is disabled at system level + ContactsManager.getInstance().fetchContactsAsync(); + } + + getFragmentManager().popBackStackImmediate(); + if (mIsNewContact || LinphoneActivity.instance().isTablet()) { + LinphoneActivity.instance().displayContact(mContact, false); + } + } + }); + + mLastName = mView.findViewById(R.id.contactLastName); + // Hack to display keyboard when touching focused edittext on Nexus One + if (Version.sdkStrictlyBelow(Version.API11_HONEYCOMB_30)) { + mLastName.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); + } + }); + } + mLastName.addTextChangedListener( + new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mOk.setEnabled( + mLastName.getText().length() > 0 + || mFirstName.getText().length() > 0); + } + + @Override + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + @Override + public void afterTextChanged(Editable s) {} + }); + + mFirstName = mView.findViewById(R.id.contactFirstName); + mFirstName.addTextChangedListener( + new TextWatcher() { + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + mOk.setEnabled( + mFirstName.getText().length() > 0 + || mLastName.getText().length() > 0); + } + + @Override + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + @Override + public void afterTextChanged(Editable s) {} + }); + + mOrganization = mView.findViewById(R.id.contactOrganization); + boolean isOrgVisible = getResources().getBoolean(R.bool.display_contact_organization); + if (!isOrgVisible) { + mOrganization.setVisibility(View.GONE); + mView.findViewById(R.id.contactOrganizationTitle).setVisibility(View.GONE); + } else { + if (!mIsNewContact) { + mOrganization.setText(mContact.getOrganization()); + } + } + + if (!mIsNewContact) { + String fn = mContact.getFirstName(); + String ln = mContact.getLastName(); + if (fn != null || ln != null) { + mFirstName.setText(fn); + mLastName.setText(ln); + } else { + mLastName.setText(mContact.getFullName()); + mFirstName.setText(""); + } + + mDeleteContact.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + final Dialog dialog = + LinphoneActivity.instance() + .displayDialog(getString(R.string.delete_text)); + Button delete = dialog.findViewById(R.id.dialog_delete_button); + Button cancel = dialog.findViewById(R.id.dialog_cancel_button); + + delete.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + mContact.delete(); + LinphoneActivity.instance().displayContacts(false); + dialog.dismiss(); + } + }); + + cancel.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + dialog.dismiss(); + } + }); + dialog.show(); + } + }); + } else { + mDeleteContact.setVisibility(View.INVISIBLE); + } + + mContactPicture = mView.findViewById(R.id.contact_picture); + if (mContact != null) { + ContactAvatar.displayAvatar(mContact, mView.findViewById(R.id.avatar_layout)); + } else { + ContactAvatar.displayAvatar("", mView.findViewById(R.id.avatar_layout)); + } + + mContactPicture.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + pickImage(); + LinphoneActivity.instance().checkAndRequestCameraPermission(); + } + }); + + mNumbersAndAddresses = new ArrayList<>(); + mSipAddresses = initSipAddressFields(mContact); + mNumbers = initNumbersFields(mContact); + + mAddSipAddress = mView.findViewById(R.id.add_address_field); + if (getResources().getBoolean(R.bool.allow_only_one_sip_address)) { + mAddSipAddress.setVisibility(View.GONE); + } + mAddSipAddress.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + addEmptyRowToAllowNewNumberOrAddress(mSipAddresses, true); + } + }); + + mAddNumber = mView.findViewById(R.id.add_number_field); + if (getResources().getBoolean(R.bool.allow_only_one_phone_number)) { + mAddNumber.setVisibility(View.GONE); + } + mAddNumber.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + addEmptyRowToAllowNewNumberOrAddress(mNumbers, false); + } + }); + + mLastName.requestFocus(); + + return mView; + } + + @Override + public void onResume() { + super.onResume(); + + // 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() { + mPickedPhotoForContactUri = null; + final List cameraIntents = new ArrayList<>(); + final Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + File file = + new File( + FileUtils.getStorageDirectory(LinphoneActivity.instance()), + getString(R.string.temp_photo_name)); + mPickedPhotoForContactUri = 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, mPickedPhotoForContactUri); + 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 (mPickedPhotoForContactUri != null) { + String filePath = mPickedPhotoForContactUri.getPath(); + editContactPicture(filePath, null); + } else { + File file = + new File( + FileUtils.getStorageDirectory(LinphoneActivity.instance()), + getString(R.string.temp_photo_name)); + if (file.exists()) { + mPickedPhotoForContactUri = Uri.fromFile(file); + String filePath = mPickedPhotoForContactUri.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); + mContactPicture.setImageBitmap(scaledPhoto); + mPhotoToAdd = 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); + } + c.close(); + return value; + } + + private LinearLayout initNumbersFields(final LinphoneContact contact) { + LinearLayout controls = mView.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 (mNewSipOrNumberToAdd != null) { + boolean isSip = + LinphoneUtils.isStrictSipAddress(mNewSipOrNumberToAdd) + || !LinphoneUtils.isNumberAddress(mNewSipOrNumberToAdd); + if (!isSip) { + View view = displayNumberOrAddress(controls, mNewSipOrNumberToAdd, false); + if (view != null) controls.addView(view); + } + } + + if (mNewDisplayName != null) { + EditText lastNameEditText = mView.findViewById(R.id.contactLastName); + if (mView != null) lastNameEditText.setText(mNewDisplayName); + } + + if (controls.getChildCount() == 0) { + addEmptyRowToAllowNewNumberOrAddress(controls, false); + } + + return controls; + } + + private LinearLayout initSipAddressFields(final LinphoneContact contact) { + LinearLayout controls = mView.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 (mNewSipOrNumberToAdd != null) { + boolean isSip = + LinphoneUtils.isStrictSipAddress(mNewSipOrNumberToAdd) + || !LinphoneUtils.isNumberAddress(mNewSipOrNumberToAdd); + if (isSip) { + View view = displayNumberOrAddress(controls, mNewSipOrNumberToAdd, 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) { + String displayNumberOrAddress = numberOrAddress; + if (isSIP) { + if (mFirstSipAddressIndex == -1) { + mFirstSipAddressIndex = 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)) { + return null; + } + + LinphoneNumberOrAddress tempNounoa; + if (mIsNewContact || mNewSipOrNumberToAdd != null) { + tempNounoa = new LinphoneNumberOrAddress(numberOrAddress, isSIP); + } else { + tempNounoa = new LinphoneNumberOrAddress(numberOrAddress, isSIP, numberOrAddress); + } + final LinphoneNumberOrAddress nounoa = tempNounoa; + mNumbersAndAddresses.add(nounoa); + + final View view = mInflater.inflate(R.layout.contact_edit_row, null); + + final EditText noa = 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) {} + }); + + ImageView delete = 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 (mContact != null) { + mContact.removeNumberOrAddress(nounoa); + } + mNumbersAndAddresses.remove(nounoa); + view.setVisibility(View.GONE); + } + }); + return view; + } + + @SuppressLint("InflateParams") + private void addEmptyRowToAllowNewNumberOrAddress( + final LinearLayout controls, final boolean isSip) { + final View view = mInflater.inflate(R.layout.contact_edit_row, null); + final LinphoneNumberOrAddress nounoa = new LinphoneNumberOrAddress(null, isSip); + + final EditText noa = view.findViewById(R.id.numoraddr); + mNumbersAndAddresses.add(nounoa); + noa.setHint(isSip ? getString(R.string.sip_address) : getString(R.string.phone_number)); + if (!isSip) { + noa.setInputType(InputType.TYPE_CLASS_PHONE); + noa.setHint(R.string.phone_number); + } else { + noa.setHint(R.string.sip_address); + } + 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 = 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) { + mNumbersAndAddresses.remove(nounoa); + view.setVisibility(View.GONE); + } + }); + + controls.addView(view); + } +} diff --git a/app/src/main/java/org/linphone/contacts/ContactViewHolder.java b/app/src/main/java/org/linphone/contacts/ContactViewHolder.java new file mode 100644 index 000000000..78f9d6845 --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/ContactViewHolder.java @@ -0,0 +1,77 @@ +package org.linphone.contacts; + +/* +ContactViewHolder.java +Copyright (C) 2018 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.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import org.linphone.R; + +public class ContactViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener, View.OnLongClickListener { + public final CheckBox delete; + public final ImageView linphoneFriend; + public final TextView name; + public final LinearLayout separator; + public final TextView separatorText; + public final RelativeLayout avatarLayout; + public final TextView organization; + private final ClickListener mListener; + + public ContactViewHolder(View view, ClickListener listener) { + super(view); + + delete = view.findViewById(R.id.delete); + linphoneFriend = view.findViewById(R.id.friendLinphone); + name = view.findViewById(R.id.name); + separator = view.findViewById(R.id.separator); + separatorText = view.findViewById(R.id.separator_text); + avatarLayout = view.findViewById(R.id.avatar_layout); + organization = view.findViewById(R.id.contactOrganization); + // friendStatus = view.findViewById(R.id.friendStatus); + mListener = listener; + view.setOnClickListener(this); + view.setOnLongClickListener(this); + } + + @Override + public void onClick(View view) { + if (mListener != null) { + mListener.onItemClicked(getAdapterPosition()); + } + } + + public boolean onLongClick(View v) { + if (mListener != null) { + return mListener.onItemLongClicked(getAdapterPosition()); + } + return false; + } + + public interface ClickListener { + void onItemClicked(int position); + + boolean onItemLongClicked(int position); + } +} diff --git a/app/src/main/java/org/linphone/contacts/ContactsAdapter.java b/app/src/main/java/org/linphone/contacts/ContactsAdapter.java new file mode 100644 index 000000000..87a9b5329 --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/ContactsAdapter.java @@ -0,0 +1,173 @@ +package org.linphone.contacts; + +/* +ContactsAdapter.java +Copyright (C) 2018 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.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SectionIndexer; +import androidx.annotation.NonNull; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.linphone.R; +import org.linphone.utils.SelectableAdapter; +import org.linphone.utils.SelectableHelper; +import org.linphone.views.ContactAvatar; + +public class ContactsAdapter extends SelectableAdapter + implements SectionIndexer { + private List mContacts; + private String[] mSections; + private ArrayList mSectionsList; + private Map mMap = new LinkedHashMap<>(); + private final ContactViewHolder.ClickListener mClickListener; + private final Context mContext; + private boolean mIsSearchMode; + + ContactsAdapter( + Context context, + List contactsList, + ContactViewHolder.ClickListener clickListener, + SelectableHelper helper) { + super(helper); + mContext = context; + updateDataSet(contactsList); + mClickListener = clickListener; + } + + @NonNull + @Override + public ContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.contact_cell, parent, false); + return new ContactViewHolder(v, mClickListener); + } + + @Override + public void onBindViewHolder(@NonNull ContactViewHolder holder, final int position) { + LinphoneContact contact = (LinphoneContact) getItem(position); + + holder.name.setText(contact.getFullName()); + + if (!mIsSearchMode) { + String fullName = contact.getFullName(); + if (fullName != null && !fullName.isEmpty()) { + holder.separatorText.setText(String.valueOf(fullName.charAt(0))); + } + } + holder.separator.setVisibility( + mIsSearchMode + || (!mIsSearchMode + && getPositionForSection(getSectionForPosition(position)) + != position) + ? View.GONE + : View.VISIBLE); + holder.linphoneFriend.setVisibility(contact.isInFriendList() ? View.VISIBLE : View.GONE); + + ContactAvatar.displayAvatar(contact, holder.avatarLayout); + + boolean isOrgVisible = + mContext.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); + } + + holder.delete.setVisibility(isEditionEnabled() ? View.VISIBLE : View.INVISIBLE); + holder.delete.setChecked(isSelected(position)); + } + + @Override + public int getItemCount() { + return mContacts.size(); + } + + public Object getItem(int position) { + if (position >= getItemCount()) return null; + return mContacts.get(position); + } + + public void setIsSearchMode(boolean set) { + mIsSearchMode = set; + } + + public long getItemId(int position) { + return position; + } + + public void updateDataSet(List contactsList) { + mContacts = contactsList; + + mMap = new LinkedHashMap<>(); + String prevLetter = null; + for (int i = 0; i < mContacts.size(); i++) { + LinphoneContact contact = mContacts.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; + mMap.put(firstLetter, i); + } + } + mSectionsList = new ArrayList<>(mMap.keySet()); + mSections = new String[mSectionsList.size()]; + mSectionsList.toArray(mSections); + + notifyDataSetChanged(); + } + + @Override + public Object[] getSections() { + return mSections; + } + + @Override + public int getPositionForSection(int sectionIndex) { + if (sectionIndex >= mSections.length || sectionIndex < 0) { + return 0; + } + return mMap.get(mSections[sectionIndex]); + } + + @Override + public int getSectionForPosition(int position) { + if (position >= mContacts.size() || position < 0) { + return 0; + } + LinphoneContact contact = mContacts.get(position); + String fullName = contact.getFullName(); + if (fullName == null || fullName.isEmpty()) { + return 0; + } + String letter = fullName.substring(0, 1).toUpperCase(Locale.getDefault()); + return mSectionsList.indexOf(letter); + } +} diff --git a/app/src/main/java/org/linphone/contacts/ContactsFragment.java b/app/src/main/java/org/linphone/contacts/ContactsFragment.java new file mode 100644 index 000000000..c73c2439b --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/ContactsFragment.java @@ -0,0 +1,418 @@ +package org.linphone.contacts; + +/* +ContactsFragment.java +Copyright (C) 2017 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.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.SearchView; +import android.widget.TextView; +import android.widget.Toast; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import java.util.ArrayList; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.utils.SelectableHelper; + +public class ContactsFragment extends Fragment + implements OnItemClickListener, + ContactsUpdatedListener, + ContactViewHolder.ClickListener, + SelectableHelper.DeleteListener { + private RecyclerView mContactsList; + private TextView mNoSipContact, mNoContact; + private ImageView mAllContacts, mLinphoneContacts, mNewContact; + private boolean mOnlyDisplayLinphoneContacts; + private View mAllContactsSelected, mLinphoneContactsSelected; + private int mLastKnownPosition; + private boolean mEditOnClick = false, mEditConsumed = false, mOnlyDisplayChatAddress = false; + private String mSipAddressToAdd, mDisplayName = null; + private SearchView mSearchView; + private ProgressBar mContactsFetchInProgress; + private LinearLayoutManager mLayoutManager; + private Context mContext; + private SelectableHelper mSelectionHelper; + private ContactsAdapter mContactAdapter; + private SwipeRefreshLayout mContactsRefresher; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.contacts_list, container, false); + mContext = getActivity().getApplicationContext(); + mSelectionHelper = new SelectableHelper(view, this); + mSelectionHelper.setDialogMessage(R.string.delete_contacts_text); + + if (getArguments() != null) { + mEditOnClick = getArguments().getBoolean("EditOnClick"); + mSipAddressToAdd = getArguments().getString("SipAddress"); + if (getArguments().getString("DisplayName") != null) { + mDisplayName = getArguments().getString("DisplayName"); + } + mOnlyDisplayChatAddress = getArguments().getBoolean("ChatAddressOnly"); + + if (getArguments().getBoolean("EditOnClick")) { + Toast.makeText( + LinphoneActivity.instance(), + R.string.toast_choose_contact_for_edition, + Toast.LENGTH_LONG) + .show(); + } + getArguments().clear(); + } + + mNoSipContact = view.findViewById(R.id.noSipContact); + mNoContact = view.findViewById(R.id.noContact); + mContactsList = view.findViewById(R.id.contactsList); + + mAllContacts = view.findViewById(R.id.all_contacts); + mLinphoneContacts = view.findViewById(R.id.linphone_contacts); + mAllContactsSelected = view.findViewById(R.id.all_contacts_select); + mLinphoneContactsSelected = view.findViewById(R.id.linphone_contacts_select); + mContactsFetchInProgress = view.findViewById(R.id.contactsFetchInProgress); + mNewContact = view.findViewById(R.id.newContact); + mContactsRefresher = view.findViewById(R.id.contactsListRefresher); + + mContactsRefresher.setOnRefreshListener( + new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + ContactsManager.getInstance().fetchContactsAsync(); + } + }); + + mAllContacts.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + mOnlyDisplayLinphoneContacts = false; + mAllContactsSelected.setVisibility(View.VISIBLE); + mAllContacts.setEnabled(false); + mLinphoneContacts.setEnabled(true); + mLinphoneContactsSelected.setVisibility(View.INVISIBLE); + changeContactsAdapter(); + } + }); + + mLinphoneContacts.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + mAllContactsSelected.setVisibility(View.INVISIBLE); + mLinphoneContactsSelected.setVisibility(View.VISIBLE); + mLinphoneContacts.setEnabled(false); + mAllContacts.setEnabled(true); + mOnlyDisplayLinphoneContacts = true; + changeContactsAdapter(); + } + }); + + mNewContact.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + mEditConsumed = true; + ContactsManager.getInstance() + .createContact(getActivity(), mDisplayName, mSipAddressToAdd); + } + }); + + if (getResources().getBoolean(R.bool.hide_non_linphone_contacts)) { + mAllContacts.setEnabled(false); + mLinphoneContacts.setEnabled(false); + mOnlyDisplayLinphoneContacts = true; + mAllContacts.setOnClickListener(null); + mLinphoneContacts.setOnClickListener(null); + mLinphoneContacts.setVisibility(View.INVISIBLE); + mLinphoneContactsSelected.setVisibility(View.INVISIBLE); + } else { + mAllContacts.setEnabled(mOnlyDisplayLinphoneContacts); + mLinphoneContacts.setEnabled(!mAllContacts.isEnabled()); + } + mNewContact.setEnabled(LinphoneManager.getLc().getCallsNb() == 0); + + if (!ContactsManager.getInstance().contactsFetchedOnce()) { + if (ContactsManager.getInstance().hasReadContactsAccess()) { + mContactsFetchInProgress.setVisibility(View.VISIBLE); + } else { + LinphoneActivity.instance().checkAndRequestReadContactsPermission(); + } + } else { + if (!mOnlyDisplayLinphoneContacts + && ContactsManager.getInstance().getContacts().size() == 0) { + mNoContact.setVisibility(View.VISIBLE); + } else if (mOnlyDisplayLinphoneContacts + && ContactsManager.getInstance().getSIPContacts().size() == 0) { + mNoSipContact.setVisibility(View.VISIBLE); + } + } + + mSearchView = view.findViewById(R.id.searchField); + mSearchView.setOnQueryTextListener( + new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + searchContacts(newText); + return true; + } + }); + + mLayoutManager = new LinearLayoutManager(mContext); + mContactsList.setLayoutManager(mLayoutManager); + + DividerItemDecoration dividerItemDecoration = + new DividerItemDecoration( + mContactsList.getContext(), mLayoutManager.getOrientation()); + dividerItemDecoration.setDrawable( + getActivity().getResources().getDrawable(R.drawable.divider)); + mContactsList.addItemDecoration(dividerItemDecoration); + + return view; + } + + public void displayFirstContact() { + if (mContactsList != null + && mContactsList.getAdapter() != null + && mContactsList.getAdapter().getItemCount() > 0) { + ContactsAdapter mAdapt = (ContactsAdapter) mContactsList.getAdapter(); + LinphoneActivity.instance().displayContact((LinphoneContact) mAdapt.getItem(0), false); + } else { + LinphoneActivity.instance().displayEmptyFragment(); + } + } + + private void searchContacts(String search) { + boolean isEditionEnabled = false; + if (search == null || search.length() == 0) { + changeContactsAdapter(); + return; + } + changeContactsToggle(); + + List listContact; + + if (mOnlyDisplayLinphoneContacts) { + listContact = ContactsManager.getInstance().getSIPContacts(search); + } else { + listContact = ContactsManager.getInstance().getContacts(search); + } + if (mContactAdapter != null && mContactAdapter.isEditionEnabled()) { + isEditionEnabled = true; + } + + mContactAdapter = new ContactsAdapter(mContext, listContact, this, mSelectionHelper); + mContactAdapter.setIsSearchMode(true); + + // mContactsList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE); + mSelectionHelper.setAdapter(mContactAdapter); + if (isEditionEnabled) { + mSelectionHelper.enterEditionMode(); + } + mContactsList.setAdapter(mContactAdapter); + } + + private void changeContactsAdapter() { + changeContactsToggle(); + List listContact; + + mNoSipContact.setVisibility(View.GONE); + mNoContact.setVisibility(View.GONE); + mContactsList.setVisibility(View.VISIBLE); + boolean isEditionEnabled = false; + String query = mSearchView.getQuery().toString(); + if (query.equals("")) { + if (mOnlyDisplayLinphoneContacts) { + listContact = ContactsManager.getInstance().getSIPContacts(); + } else { + listContact = ContactsManager.getInstance().getContacts(); + } + } else { + if (mOnlyDisplayLinphoneContacts) { + listContact = ContactsManager.getInstance().getSIPContacts(query); + } else { + listContact = ContactsManager.getInstance().getContacts(query); + } + } + + if (mContactAdapter != null && mContactAdapter.isEditionEnabled()) { + isEditionEnabled = true; + } + + mContactAdapter = new ContactsAdapter(mContext, listContact, this, mSelectionHelper); + + mSelectionHelper.setAdapter(mContactAdapter); + + if (isEditionEnabled) { + mSelectionHelper.enterEditionMode(); + } + mContactsList.setAdapter(mContactAdapter); + + mContactAdapter.notifyDataSetChanged(); + + if (!mOnlyDisplayLinphoneContacts && mContactAdapter.getItemCount() == 0) { + mNoContact.setVisibility(View.VISIBLE); + } else if (mOnlyDisplayLinphoneContacts && mContactAdapter.getItemCount() == 0) { + mNoSipContact.setVisibility(View.VISIBLE); + } + } + + private void changeContactsToggle() { + if (mOnlyDisplayLinphoneContacts + && !getResources().getBoolean(R.bool.hide_non_linphone_contacts)) { + mAllContacts.setEnabled(true); + mAllContactsSelected.setVisibility(View.INVISIBLE); + mLinphoneContacts.setEnabled(false); + mLinphoneContactsSelected.setVisibility(View.VISIBLE); + } else { + mAllContacts.setEnabled(false); + mAllContactsSelected.setVisibility(View.VISIBLE); + mLinphoneContacts.setEnabled(true); + mLinphoneContactsSelected.setVisibility(View.INVISIBLE); + } + } + + @Override + public void onItemClick(AdapterView adapter, View view, int position, long id) { + LinphoneContact contact = (LinphoneContact) adapter.getItemAtPosition(position); + if (mEditOnClick) { + mEditConsumed = true; + ContactsManager.getInstance().editContact(getActivity(), contact, mSipAddressToAdd); + } else { + mLastKnownPosition = mLayoutManager.findFirstVisibleItemPosition(); + LinphoneActivity.instance().displayContact(contact, mOnlyDisplayChatAddress); + } + } + + @Override + public void onItemClicked(int position) { + LinphoneContact contact = (LinphoneContact) mContactAdapter.getItem(position); + + if (mContactAdapter.isEditionEnabled()) { + mContactAdapter.toggleSelection(position); + + } else if (mEditOnClick) { + mEditConsumed = true; + ContactsManager.getInstance().editContact(getActivity(), contact, mSipAddressToAdd); + } else { + mLastKnownPosition = mLayoutManager.findFirstVisibleItemPosition(); + LinphoneActivity.instance().displayContact(contact, mOnlyDisplayChatAddress); + } + } + + @Override + public boolean onItemLongClicked(int position) { + if (!mContactAdapter.isEditionEnabled()) { + mSelectionHelper.enterEditionMode(); + } + mContactAdapter.toggleSelection(position); + return true; + } + + @Override + public void onResume() { + super.onResume(); + ContactsManager.getInstance().addContactsListener(this); + + if (mEditConsumed) { + mEditOnClick = false; + mSipAddressToAdd = null; + } + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.CONTACTS_LIST); + mOnlyDisplayLinphoneContacts = + ContactsManager.getInstance().isLinphoneContactsPrefered() + || getResources().getBoolean(R.bool.hide_non_linphone_contacts); + } + changeContactsToggle(); + invalidate(); + } + + @Override + public void onPause() { + ContactsManager.getInstance().removeContactsListener(this); + super.onPause(); + } + + @Override + public void onContactsUpdated() { + if (!LinphoneActivity.isInstanciated() + || (LinphoneActivity.instance().getCurrentFragment() + != FragmentsAvailable.CONTACTS_LIST + && !LinphoneActivity.instance().isTablet())) return; + if (mContactAdapter != null) { + mContactAdapter.updateDataSet( + mOnlyDisplayLinphoneContacts + ? ContactsManager.getInstance().getSIPContacts() + : ContactsManager.getInstance().getContacts()); + mContactAdapter.notifyDataSetChanged(); + + if (mContactAdapter.getItemCount() > 0) { + mNoContact.setVisibility(View.GONE); + mNoSipContact.setVisibility(View.GONE); + } + } + mContactsFetchInProgress.setVisibility(View.GONE); + mContactsRefresher.setRefreshing(false); + } + + private void invalidate() { + if (mSearchView != null && mSearchView.getQuery().toString().length() > 0) { + searchContacts(mSearchView.getQuery().toString()); + } else { + changeContactsAdapter(); + } + mContactsList.scrollToPosition(mLastKnownPosition); + } + + @Override + public void onDeleteSelection(Object[] objectsToDelete) { + ArrayList ids = new ArrayList<>(); + int size = mContactAdapter.getSelectedItemCount(); + for (int i = size - 1; i >= 0; i--) { + LinphoneContact contact = (LinphoneContact) objectsToDelete[i]; + if (contact.isAndroidContact()) { + contact.deleteFriend(); + ids.add(contact.getAndroidId()); + } else { + contact.delete(); + } + } + ContactsManager.getInstance().deleteMultipleContactsAtOnce(ids); + } +} diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.java b/app/src/main/java/org/linphone/contacts/ContactsManager.java new file mode 100644 index 000000000..e455ab94a --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.java @@ -0,0 +1,517 @@ +package org.linphone.contacts; + +/* +ContactsManager.java +Copyright (C) 2017 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 static android.os.AsyncTask.THREAD_POOL_EXECUTOR; + +import android.Manifest; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.content.ContentProviderClient; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.ContactsContract; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.compatibility.Compatibility; +import org.linphone.core.Address; +import org.linphone.core.Core; +import org.linphone.core.Friend; +import org.linphone.core.FriendList; +import org.linphone.core.FriendListListener; +import org.linphone.core.MagicSearch; +import org.linphone.core.ProxyConfig; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; + +public class ContactsManager extends ContentObserver implements FriendListListener { + private static ContactsManager sInstance; + + private List mContacts, mSipContacts; + private ArrayList mContactsUpdatedListeners; + private MagicSearch mMagicSearch; + private boolean mContactsFetchedOnce = false; + private Context mContext; + private AsyncContactsLoader mLoadContactTask; + private boolean mInitialized = false; + + public static ContactsManager getInstance() { + if (sInstance == null) sInstance = new ContactsManager(); + return sInstance; + } + + private ContactsManager() { + super(LinphoneService.instance().handler); + mContactsUpdatedListeners = new ArrayList<>(); + mContacts = new ArrayList<>(); + mSipContacts = new ArrayList<>(); + + if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null) { + mMagicSearch = LinphoneManager.getLcIfManagerNotDestroyedOrNull().createMagicSearch(); + mMagicSearch.setLimitedSearch(false); // Do not limit the number of results + } + } + + public void addContactsListener(ContactsUpdatedListener listener) { + mContactsUpdatedListeners.add(listener); + } + + public void removeContactsListener(ContactsUpdatedListener listener) { + mContactsUpdatedListeners.remove(listener); + } + + public ArrayList getContactsListeners() { + return mContactsUpdatedListeners; + } + + @Override + public void onChange(boolean selfChange) { + onChange(selfChange, null); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + fetchContactsAsync(); + } + + public synchronized List getContacts() { + return mContacts; + } + + synchronized void setContacts(List c) { + mContacts = c; + } + + public synchronized List getSIPContacts() { + return mSipContacts; + } + + synchronized void setSipContacts(List c) { + mSipContacts = c; + } + + public void destroy() { + if (mLoadContactTask != null) { + mLoadContactTask.cancel(true); + } + // LinphoneContact has a Friend field and Friend can have a LinphoneContact has userData + // Friend also keeps a ref on the Core, so we have to clean them + for (LinphoneContact c : mContacts) { + c.setFriend(null); + } + mContacts.clear(); + for (LinphoneContact c : mSipContacts) { + c.setFriend(null); + } + mSipContacts.clear(); + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + for (FriendList list : lc.getFriendsLists()) { + list.removeListener(this); + } + } + sInstance = null; + } + + public void fetchContactsAsync() { + if (mLoadContactTask != null) { + mLoadContactTask.cancel(true); + } + if (!hasReadContactsAccess()) { + Log.w("[Contacts Manager] Can't fetch contact without READ permission"); + return; + } + mLoadContactTask = new AsyncContactsLoader(mContext); + mContactsFetchedOnce = true; + mLoadContactTask.executeOnExecutor(THREAD_POOL_EXECUTOR); + } + + public void editContact(Context context, LinphoneContact contact, String valueToAdd) { + if (context.getResources().getBoolean(R.bool.use_native_contact_editor)) { + Intent intent = new Intent(Intent.ACTION_EDIT); + Uri contactUri = contact.getAndroidLookupUri(); + intent.setDataAndType(contactUri, ContactsContract.Contacts.CONTENT_ITEM_TYPE); + intent.putExtra( + "finishActivityOnSaveCompleted", true); // So after save will go back here + if (valueToAdd != null) { + intent.putExtra(ContactsContract.Intents.Insert.IM_HANDLE, valueToAdd); + } + context.startActivity(intent); + } else { + LinphoneActivity.instance().editContact(contact, valueToAdd); + } + } + + public void createContact(Context context, String name, String valueToAdd) { + if (context.getResources().getBoolean(R.bool.use_native_contact_editor)) { + Intent intent = new Intent(ContactsContract.Intents.Insert.ACTION); + intent.setType(ContactsContract.RawContacts.CONTENT_TYPE); + intent.putExtra( + "finishActivityOnSaveCompleted", true); // So after save will go back here + if (name != null) { + intent.putExtra(ContactsContract.Intents.Insert.NAME, name); + } + if (valueToAdd != null) { + intent.putExtra(ContactsContract.Intents.Insert.IM_HANDLE, valueToAdd); + } + context.startActivity(intent); + } else { + LinphoneActivity.instance().addContact(name, valueToAdd); + } + } + + public MagicSearch getMagicSearch() { + return mMagicSearch; + } + + public boolean contactsFetchedOnce() { + return mContactsFetchedOnce; + } + + public List getContacts(String search) { + search = search.toLowerCase(Locale.getDefault()); + List searchContactsBegin = new ArrayList<>(); + List searchContactsContain = new ArrayList<>(); + for (LinphoneContact contact : getContacts()) { + 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 List getSIPContacts(String search) { + search = search.toLowerCase(Locale.getDefault()); + List searchContactsBegin = new ArrayList<>(); + List searchContactsContain = new ArrayList<>(); + for (LinphoneContact contact : getSIPContacts()) { + 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() { + LinphonePreferences.instance().disableFriendsStorage(); + } + + public boolean hasReadContactsAccess() { + if (mContext == null) { + return false; + } + boolean contactsR = + (PackageManager.PERMISSION_GRANTED + == mContext.getPackageManager() + .checkPermission( + android.Manifest.permission.READ_CONTACTS, + mContext.getPackageName())); + return contactsR + && !mContext.getResources().getBoolean(R.bool.force_use_of_linphone_friends); + } + + public boolean hasWriteContactsAccess() { + if (mContext == null) { + return false; + } + return (PackageManager.PERMISSION_GRANTED + == mContext.getPackageManager() + .checkPermission( + Manifest.permission.WRITE_CONTACTS, mContext.getPackageName())); + } + + public boolean hasWriteSyncPermission() { + if (mContext == null) { + return false; + } + return (PackageManager.PERMISSION_GRANTED + == mContext.getPackageManager() + .checkPermission( + Manifest.permission.WRITE_SYNC_SETTINGS, + mContext.getPackageName())); + } + + public boolean isLinphoneContactsPrefered() { + ProxyConfig lpc = LinphoneManager.getLc().getDefaultProxyConfig(); + return lpc != null + && lpc.getIdentityAddress() + .getDomain() + .equals(mContext.getString(R.string.default_domain)); + } + + public void initializeContactManager(Context context) { + mContext = context; + + if (!mInitialized) { + if (mContext.getResources().getBoolean(R.bool.use_linphone_tag)) { + if (hasReadContactsAccess() + && hasWriteContactsAccess() + && hasWriteSyncPermission()) { + if (LinphoneService.isReady()) { + ContactsManager.getInstance().initializeSyncAccount(); + mInitialized = true; + } + } + } + } + + if (mContext != null && getContacts().size() == 0 && hasReadContactsAccess()) { + fetchContactsAsync(); + } + } + + private void makeContactAccountVisible() { + ContentProviderClient client = + mContext.getContentResolver() + .acquireContentProviderClient(ContactsContract.AUTHORITY_URI); + ContentValues values = new ContentValues(); + values.put( + ContactsContract.Settings.ACCOUNT_NAME, + mContext.getString(R.string.sync_account_name)); + values.put( + ContactsContract.Settings.ACCOUNT_TYPE, + mContext.getString(R.string.sync_account_type)); + values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, true); + try { + client.insert( + ContactsContract.Settings.CONTENT_URI + .buildUpon() + .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true") + .build(), + values); + Log.i("[Contacts Manager] Contacts account made visible"); + } catch (RemoteException e) { + Log.e("[Contacts Manager] Couldn't make contacts account visible: " + e); + } + Compatibility.closeContentProviderClient(client); + } + + private void initializeSyncAccount() { + AccountManager accountManager = + (AccountManager) mContext.getSystemService(Context.ACCOUNT_SERVICE); + + Account[] accounts = + accountManager.getAccountsByType(mContext.getString(R.string.sync_account_type)); + + if (accounts != null && accounts.length == 0) { + Account newAccount = + new Account( + mContext.getString(R.string.sync_account_name), + mContext.getString(R.string.sync_account_type)); + try { + accountManager.addAccountExplicitly(newAccount, null, null); + Log.i("[Contacts Manager] Contact account added"); + makeContactAccountVisible(); + } catch (Exception e) { + Log.e("[Contacts Manager] Couldn't initialize sync account: " + e); + } + } else if (accounts != null) { + for (int i = 0; i < accounts.length; i++) { + Log.i( + "[Contacts Manager] Found account with name \"" + + accounts[i].name + + "\" and type \"" + + accounts[i].type + + "\""); + makeContactAccountVisible(); + } + } + } + + public synchronized LinphoneContact findContactFromAndroidId(String androidId) { + if (androidId == null) return null; + + for (LinphoneContact c : getContacts()) { + if (c.getAndroidId() != null && c.getAndroidId().equals(androidId)) { + return c; + } + } + return null; + } + + public synchronized LinphoneContact findContactFromAddress(Address address) { + if (address == null) return null; + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + Friend lf = lc.findFriend(address); + if (lf != null) { + return (LinphoneContact) lf.getUserData(); + } + return findContactFromPhoneNumber(address.getUsername()); + } + + public synchronized LinphoneContact findContactFromPhoneNumber(String phoneNumber) { + if (phoneNumber == null) return null; + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + ProxyConfig lpc = null; + if (lc != null) { + lpc = lc.getDefaultProxyConfig(); + } + if (lpc == null) return null; + String normalized = lpc.normalizePhoneNumber(phoneNumber); + if (normalized == null) normalized = phoneNumber; + + Address addr = lpc.normalizeSipUri(normalized); + if (addr == null) { + return null; + } + addr.setUriParam("user", "phone"); + Friend lf = + lc.findFriend( + addr); // Without this, the hashmap inside liblinphone won't find it... + if (lf != null) { + return (LinphoneContact) lf.getUserData(); + } + return null; + } + + public String getAddressOrNumberForAndroidContact(ContentResolver resolver, Uri contactUri) { + // Phone Numbers + String[] projection = new String[] {ContactsContract.CommonDataKinds.Phone.NUMBER}; + Cursor c = resolver.query(contactUri, projection, null, null, null); + if (c != null) { + if (c.moveToNext()) { + int numberIndex = c.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); + String number = c.getString(numberIndex); + c.close(); + return number; + } + } + c.close(); + + // SIP addresses + projection = new String[] {ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS}; + c = resolver.query(contactUri, projection, null, null, null); + if (c != null) { + if (c.moveToNext()) { + int numberIndex = + c.getColumnIndex(ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS); + String address = c.getString(numberIndex); + c.close(); + return address; + } + } + c.close(); + return null; + } + + private synchronized boolean refreshSipContact(Friend lf) { + LinphoneContact contact = (LinphoneContact) lf.getUserData(); + if (contact != null) { + if (!mSipContacts.contains(contact)) { + mSipContacts.add(contact); + return true; + } + } + return false; + } + + public void delete(String id) { + ArrayList ids = new ArrayList<>(); + ids.add(id); + deleteMultipleContactsAtOnce(ids); + } + + public void deleteMultipleContactsAtOnce(List ids) { + String select = ContactsContract.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()); + } + + try { + mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); + } catch (Exception e) { + Log.e("[Contacts Manager] " + e); + } + + // To ensure removed contacts won't appear in the contacts list anymore + fetchContactsAsync(); + } + + public String getString(int resourceID) { + if (mContext == null) return null; + return mContext.getString(resourceID); + } + + @Override + public void onContactCreated(FriendList list, Friend lf) {} + + @Override + public void onContactDeleted(FriendList list, Friend lf) {} + + @Override + public void onContactUpdated(FriendList list, Friend newFriend, Friend oldFriend) {} + + @Override + public void onSyncStatusChanged(FriendList list, FriendList.SyncStatus status, String msg) {} + + @Override + public void onPresenceReceived(FriendList list, Friend[] friends) { + boolean updated = false; + for (Friend lf : friends) { + boolean newContact = ContactsManager.getInstance().refreshSipContact(lf); + if (newContact) { + updated = true; + } + } + + if (updated) { + Collections.sort(mSipContacts); + } + + for (ContactsUpdatedListener listener : mContactsUpdatedListeners) { + listener.onContactsUpdated(); + } + } +} diff --git a/src/android/org/linphone/contacts/ContactsUpdatedListener.java b/app/src/main/java/org/linphone/contacts/ContactsUpdatedListener.java similarity index 99% rename from src/android/org/linphone/contacts/ContactsUpdatedListener.java rename to app/src/main/java/org/linphone/contacts/ContactsUpdatedListener.java index bc7f67f6a..6c50526c1 100644 --- a/src/android/org/linphone/contacts/ContactsUpdatedListener.java +++ b/app/src/main/java/org/linphone/contacts/ContactsUpdatedListener.java @@ -21,4 +21,4 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. public interface ContactsUpdatedListener { void onContactsUpdated(); -} \ No newline at end of file +} diff --git a/app/src/main/java/org/linphone/contacts/LinphoneContact.java b/app/src/main/java/org/linphone/contacts/LinphoneContact.java new file mode 100644 index 000000000..8913733c0 --- /dev/null +++ b/app/src/main/java/org/linphone/contacts/LinphoneContact.java @@ -0,0 +1,557 @@ +package org.linphone.contacts; + +/* +LinphoneContact.java +Copyright (C) 2017 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.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.Address; +import org.linphone.core.Core; +import org.linphone.core.Friend; +import org.linphone.core.FriendCapability; +import org.linphone.core.FriendList; +import org.linphone.core.PresenceBasicStatus; +import org.linphone.core.PresenceModel; +import org.linphone.core.SubscribePolicy; + +public class LinphoneContact extends AndroidContact + implements Serializable, Comparable { + private static final long serialVersionUID = 9015568163905205244L; + + private transient Friend mFriend; + private String mFullName, mFirstName, mLastName, mOrganization; + private transient Uri mPhotoUri, mThumbnailUri; + private List mAddresses; + private boolean mHasSipAddress; + + public LinphoneContact() { + super(); + mAddresses = new ArrayList<>(); + mAndroidId = null; + mAndroidLookupKey = null; + mThumbnailUri = null; + mPhotoUri = null; + mHasSipAddress = false; + } + + public static LinphoneContact createContact() { + LinphoneContact contact = new LinphoneContact(); + if (ContactsManager.getInstance().hasReadContactsAccess()) { + contact.createAndroidContact(); + } else { + contact.createFriend(); + } + return contact; + } + + @Override + public int compareTo(LinphoneContact contact) { + String fullName = + getFullName() != null ? getFullName().toUpperCase(Locale.getDefault()) : ""; + String contactFullName = + contact.getFullName() != null + ? contact.getFullName().toUpperCase(Locale.getDefault()) + : ""; + + if (fullName.equals(contactFullName)) { + if (getAndroidId() != null) { + if (contact.getAndroidId() != null) { + int idComp = getAndroidId().compareTo(contact.getAndroidId()); + if (idComp == 0) return 0; + List noas1 = getNumbersOrAddresses(); + List noas2 = contact.getNumbersOrAddresses(); + if (noas1.size() == noas2.size()) { + if (noas1.containsAll(noas2) && noas2.containsAll(noas1)) { + return 0; + } + return -1; + } + return Integer.compare(noas1.size(), noas2.size()); + } + return -1; + } + if (contact.getAndroidId() != null) return 1; + return 0; + } + return fullName.compareTo(contactFullName); + } + + @Override + public boolean equals(Object obj) { + if (obj.getClass() != LinphoneContact.class) return false; + LinphoneContact contact = (LinphoneContact) obj; + return (this.compareTo(contact) == 0); + } + + /* + Name related + */ + + public String getFullName() { + return mFullName; + } + + public void setFullName(String name) { + mFullName = name; + } + + public void setFirstNameAndLastName(String fn, String ln, boolean commitChanges) { + if (fn != null && fn.length() == 0 && ln != null && ln.length() == 0) return; + if (fn != null && fn.equals(mFirstName) && ln != null && ln.equals(mLastName)) return; + + if (commitChanges) { + setName(fn, ln); + } + + mFirstName = fn; + mLastName = ln; + if (mFullName == null) { + if (mFirstName != null + && mLastName != null + && mFirstName.length() > 0 + && mLastName.length() > 0) { + mFullName = mFirstName + " " + mLastName; + } else if (mFirstName != null && mFirstName.length() > 0) { + mFullName = mFirstName; + } else if (mLastName != null && mLastName.length() > 0) { + mFullName = mLastName; + } + } + } + + public String getFirstName() { + return mFirstName; + } + + public String getLastName() { + return mLastName; + } + + /* + Organization related + */ + + public String getOrganization() { + return mOrganization; + } + + public void setOrganization(String org, boolean commitChanges) { + if ((org == null || org.isEmpty()) && (mOrganization == null || mOrganization.isEmpty())) + return; + if (org != null && org.equals(mOrganization)) return; + + if (commitChanges) { + setOrganization(org, mOrganization); + } + + mOrganization = org; + } + + /* + Picture related + */ + + public boolean hasPhoto() { + return mPhotoUri != null; + } + + public Uri getPhotoUri() { + return mPhotoUri; + } + + private void setPhotoUri(Uri uri) { + if (uri.equals(mPhotoUri)) return; + mPhotoUri = uri; + } + + public Uri getThumbnailUri() { + return mThumbnailUri; + } + + private void setThumbnailUri(Uri uri) { + if (uri.equals(mThumbnailUri)) return; + mThumbnailUri = uri; + } + + /* + Number or address related + */ + + public void addNumberOrAddress(LinphoneNumberOrAddress noa) { + if (noa == null) return; + if (noa.isSIPAddress()) { + mHasSipAddress = true; + mAddresses.add(noa); + } else { + boolean found = false; + // Check for duplicated phone numbers but with different formats + for (LinphoneNumberOrAddress number : mAddresses) { + if (!number.isSIPAddress() + && noa.getNormalizedPhone().equals(number.getNormalizedPhone())) { + found = true; + break; + } + } + if (!found) { + mAddresses.add(noa); + } + } + } + + public List getNumbersOrAddresses() { + return mAddresses; + } + + public boolean hasAddress(String address) { + for (LinphoneNumberOrAddress noa : getNumbersOrAddresses()) { + if (noa.isSIPAddress()) { + String value = noa.getValue(); + if (address.startsWith(value) || value.equals("sip:" + address)) { + // Startswith is to workaround the fact that the + // address may have a ;gruu= at the end... + return true; + } + } + } + return false; + } + + public boolean hasAddress() { + return mHasSipAddress; + } + + public void removeNumberOrAddress(LinphoneNumberOrAddress noa) { + if (noa != null && noa.getOldValue() != null) { + + removeNumberOrAddress(noa.getOldValue(), noa.isSIPAddress()); + + if (isFriend()) { + if (noa.isSIPAddress()) { + if (!noa.getOldValue().startsWith("sip:")) { + noa.setOldValue("sip:" + noa.getOldValue()); + } + } + LinphoneNumberOrAddress toRemove = null; + for (LinphoneNumberOrAddress address : mAddresses) { + if (noa.getOldValue().equals(address.getValue()) + && noa.isSIPAddress() == address.isSIPAddress()) { + toRemove = address; + break; + } + } + if (toRemove != null) { + mAddresses.remove(toRemove); + } + } + } + } + + public void addOrUpdateNumberOrAddress(LinphoneNumberOrAddress noa) { + if (noa != null && noa.getValue() != null) { + + addNumberOrAddress(noa.getValue(), noa.getOldValue(), noa.isSIPAddress()); + + if (isFriend()) { + 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 : mAddresses) { + if (noa.getOldValue().equals(address.getValue()) + && noa.isSIPAddress() == address.isSIPAddress()) { + address.setValue(noa.getValue()); + break; + } + } + } else { + mAddresses.add(noa); + } + } + } + } + + public void clearAddresses() { + mAddresses.clear(); + } + + /* + Friend related + */ + + public Friend getFriend() { + return mFriend; + } + + private void createOrUpdateFriend() { + boolean created = false; + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc == null) return; + + if (!isFriend()) { + mFriend = lc.createFriend(); + mFriend.enableSubscribes(false); + mFriend.setIncSubscribePolicy(SubscribePolicy.SPDeny); + if (isAndroidContact()) { + mFriend.setRefKey(getAndroidId()); + } + mFriend.setUserData(this); + created = true; + } + if (isFriend()) { + mFriend.edit(); + mFriend.setName(mFullName); + if (mFriend.getVcard() != null) { + mFriend.getVcard().setFamilyName(mLastName); + mFriend.getVcard().setGivenName(mFirstName); + } + if (mOrganization != null) { + mFriend.getVcard().setOrganization(mOrganization); + } + + if (!created) { + for (Address address : mFriend.getAddresses()) { + mFriend.removeAddress(address); + } + for (String phone : mFriend.getPhoneNumbers()) { + mFriend.removePhoneNumber(phone); + } + } + for (LinphoneNumberOrAddress noa : mAddresses) { + if (noa.isSIPAddress()) { + Address addr = lc.interpretUrl(noa.getValue()); + if (addr != null) { + mFriend.addAddress(addr); + } + } else { + mFriend.addPhoneNumber(noa.getValue()); + } + } + mFriend.done(); + } + if (created) { + lc.addFriend(mFriend); + } + + if (!ContactsManager.getInstance().hasReadContactsAccess()) { + // This refresh is only needed if app has no contacts permission to refresh the list of + // Friends. + // Otherwise contacts will be refreshed due to changes in native contact and the handler + // in ContactsManager + ContactsManager.getInstance().fetchContactsAsync(); + } + } + + public void deleteFriend() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (mFriend != null && lc != null) { + for (FriendList list : lc.getFriendsLists()) { + list.removeFriend(mFriend); + } + } + } + + public void createOrUpdateFriendFromNativeContact() { + if (isAndroidContact()) { + createOrUpdateFriend(); + } + } + + public boolean isFriend() { + return mFriend != null; + } + + public void setFriend(Friend f) { + if (mFriend != null && (f == null || f != mFriend)) { + mFriend.setUserData(null); + } + mFriend = f; + if (mFriend != null) { + mFriend.setUserData(this); + } + } + + public boolean isInFriendList() { + if (mFriend == null) return false; + for (LinphoneNumberOrAddress noa : mAddresses) { + PresenceModel pm = mFriend.getPresenceModelForUriOrTel(noa.getValue()); + if (pm != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { + return true; + } + } + return false; + } + + public String getContactFromPresenceModelForUriOrTel(String uri) { + if (mFriend != null && mFriend.getPresenceModelForUriOrTel(uri) != null) { + return mFriend.getPresenceModelForUriOrTel(uri).getContact(); + } + return null; + } + + public PresenceBasicStatus getBasicStatusFromPresenceModelForUriOrTel(String uri) { + if (mFriend != null && mFriend.getPresenceModelForUriOrTel(uri) != null) { + return mFriend.getPresenceModelForUriOrTel(uri).getBasicStatus(); + } + return PresenceBasicStatus.Closed; + } + + public boolean hasPresenceModelForUriOrTelCapability(String uri, FriendCapability capability) { + if (mFriend != null && mFriend.getPresenceModelForUriOrTel(uri) != null) { + return mFriend.getPresenceModelForUriOrTel(uri).hasCapability(capability); + } + return false; + } + + private void createFriend() { + LinphoneContact contact = new LinphoneContact(); + Friend friend = LinphoneManager.getLc().createFriend(); + // Disable subscribes for now + friend.enableSubscribes(false); + friend.setIncSubscribePolicy(SubscribePolicy.SPDeny); + contact.mFriend = friend; + friend.setUserData(contact); + } + + /* + Contact related + */ + + protected void setAndroidId(String id) { + super.setAndroidId(id); + setThumbnailUri(getContactThumbnailPictureUri()); + setPhotoUri(getContactPictureUri()); + } + + public void syncValuesFromFriend() { + if (isFriend()) { + mAddresses = new ArrayList<>(); + mFullName = mFriend.getName(); + mLastName = mFriend.getVcard().getFamilyName(); + mFirstName = mFriend.getVcard().getGivenName(); + mThumbnailUri = null; + mPhotoUri = null; + mHasSipAddress = mFriend.getAddress() != null; + mOrganization = mFriend.getVcard().getOrganization(); + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null && lc.vcardSupported()) { + for (Address addr : mFriend.getAddresses()) { + if (addr != null) { + addNumberOrAddress( + new LinphoneNumberOrAddress(addr.asStringUriOnly(), true)); + } + } + for (String tel : mFriend.getPhoneNumbers()) { + if (tel != null) { + addNumberOrAddress(new LinphoneNumberOrAddress(tel, false)); + } + } + } else { + Address addr = mFriend.getAddress(); + addNumberOrAddress(new LinphoneNumberOrAddress(addr.asStringUriOnly(), true)); + } + } + } + + public void syncValuesFromAndroidContact(Context context) { + Cursor c = + context.getContentResolver() + .query( + ContactsContract.Data.CONTENT_URI, + AsyncContactsLoader.PROJECTION, + ContactsContract.Data.IN_VISIBLE_GROUP + + " == 1 AND " + + ContactsContract.Data.CONTACT_ID + + " == " + + mAndroidId, + null, + null); + if (c != null) { + mAddresses = new ArrayList<>(); + while (c.moveToNext()) { + syncValuesFromAndroidCusor(c); + } + c.close(); + } + } + + public void syncValuesFromAndroidCusor(Cursor c) { + String displayName = + c.getString(c.getColumnIndex(ContactsContract.Data.DISPLAY_NAME_PRIMARY)); + String mime = c.getString(c.getColumnIndex(ContactsContract.Data.MIMETYPE)); + String data1 = c.getString(c.getColumnIndex("data1")); + String data2 = c.getString(c.getColumnIndex("data2")); + String data3 = c.getString(c.getColumnIndex("data3")); + String data4 = c.getString(c.getColumnIndex("data4")); + String lookupKey = c.getString(c.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)); + + setAndroidLookupKey(lookupKey); + setFullName(displayName); + + if (ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mime)) { + addNumberOrAddress(new LinphoneNumberOrAddress(data1, data4)); + } else if (ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE.equals(mime) + || LinphoneManager.getInstance() + .getContext() + .getString(R.string.linphone_address_mime_type) + .equals(mime)) { + addNumberOrAddress(new LinphoneNumberOrAddress(data1, true)); + } else if (ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE.equals(mime)) { + setOrganization(data1, false); + } else if (ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals(mime)) { + setFirstNameAndLastName(data2, data3, false); + } + } + + public void save() { + saveChangesCommited(); + syncValuesFromAndroidContact(LinphoneActivity.instance()); + createOrUpdateFriend(); + } + + public void delete() { + deleteAndroidContact(); + + if (isFriend()) { + deleteFriend(); + } + } + + public boolean hasFriendCapability(FriendCapability capability) { + if (!isFriend()) return false; + + return getFriend().hasCapability(capability); + } +} diff --git a/src/android/org/linphone/contacts/LinphoneNumberOrAddress.java b/app/src/main/java/org/linphone/contacts/LinphoneNumberOrAddress.java similarity index 71% rename from src/android/org/linphone/contacts/LinphoneNumberOrAddress.java rename to app/src/main/java/org/linphone/contacts/LinphoneNumberOrAddress.java index 65c0ab608..37b777095 100644 --- a/src/android/org/linphone/contacts/LinphoneNumberOrAddress.java +++ b/app/src/main/java/org/linphone/contacts/LinphoneNumberOrAddress.java @@ -24,34 +24,34 @@ import java.io.Serializable; public class LinphoneNumberOrAddress implements Serializable, Comparable { private static final long serialVersionUID = -2301689469730072896L; - private boolean isSIPAddress; - private String value, oldValueForUpdatePurpose; - private String normalizedPhone; + private final boolean mIsSIPAddress; + private String mValue, mOldValueForUpdatePurpose; + private final String mNormalizedPhone; public LinphoneNumberOrAddress(String v, boolean isSIP) { - value = v; - isSIPAddress = isSIP; - oldValueForUpdatePurpose = null; - normalizedPhone = null; + mValue = v; + mIsSIPAddress = isSIP; + mOldValueForUpdatePurpose = null; + mNormalizedPhone = null; } public LinphoneNumberOrAddress(String v, String normalizedV) { - value = v; - normalizedPhone = normalizedV != null ? normalizedV : v; - isSIPAddress = false; - oldValueForUpdatePurpose = null; + mValue = v; + mNormalizedPhone = normalizedV != null ? normalizedV : v; + mIsSIPAddress = false; + mOldValueForUpdatePurpose = null; } public LinphoneNumberOrAddress(String v, boolean isSip, String old) { this(v, isSip); - oldValueForUpdatePurpose = old; + mOldValueForUpdatePurpose = old; } @Override public int compareTo(LinphoneNumberOrAddress noa) { - if (value != null) { + if (mValue != null) { if (noa.isSIPAddress() && isSIPAddress()) { - return value.compareTo(noa.getValue()); + return mValue.compareTo(noa.getValue()); } else if (!noa.isSIPAddress() && !isSIPAddress()) { return getNormalizedPhone().compareTo(noa.getNormalizedPhone()); } @@ -67,24 +67,26 @@ public class LinphoneNumberOrAddress implements Serializable, Comparable { + private List mContacts; + private ArrayList mContactsSelected; + private boolean mOnlySipContact = false; + private SearchContactViewHolder.ClickListener mListener; + private final boolean mIsOnlyOnePersonSelection; + private String mPreviousSearch; + private boolean mSecurityEnabled; + + public SearchContactsAdapter( + SearchContactViewHolder.ClickListener clickListener, + boolean hideSelectionMark, + boolean isSecurityEnabled) { + mIsOnlyOnePersonSelection = hideSelectionMark; + mListener = clickListener; + setContactsSelectedList(null); + mPreviousSearch = null; + mSecurityEnabled = isSecurityEnabled; + mContacts = new ArrayList<>(); + } + + public List getContacts() { + return mContacts; + } + + public void setOnlySipContact(boolean enable) { + mOnlySipContact = enable; + } + + public void setSecurityEnabled(boolean enable) { + mSecurityEnabled = enable; + notifyDataSetChanged(); + } + + @NonNull + @Override + public SearchContactViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.search_contact_cell, parent, false); + return new SearchContactViewHolder(v, mListener); + } + + @Override + public void onBindViewHolder(@NonNull SearchContactViewHolder holder, int position) { + SearchResult searchResult = getItem(position); + + LinphoneContact contact; + if (searchResult.getFriend() != null && searchResult.getFriend().getUserData() != null) { + contact = (LinphoneContact) searchResult.getFriend().getUserData(); + } else { + if (searchResult.getAddress() == null) { + contact = + ContactsManager.getInstance() + .findContactFromPhoneNumber(searchResult.getPhoneNumber()); + } else { + contact = + ContactsManager.getInstance() + .findContactFromAddress(searchResult.getAddress()); + } + } + + final String numberOrAddress = + (searchResult.getPhoneNumber() != null) + ? searchResult.getPhoneNumber() + : searchResult.getAddress().asStringUriOnly(); + + holder.name.setVisibility(View.GONE); + if (contact != null && contact.getFullName() != null) { + holder.name.setVisibility(View.VISIBLE); + holder.name.setText(contact.getFullName()); + } else if (searchResult.getAddress() != null) { + if (searchResult.getAddress().getUsername() != null) { + holder.name.setVisibility(View.VISIBLE); + holder.name.setText(searchResult.getAddress().getUsername()); + } else if (searchResult.getAddress().getDisplayName() != null) { + holder.name.setVisibility(View.VISIBLE); + holder.name.setText(searchResult.getAddress().getDisplayName()); + } + } else if (searchResult.getAddress() != null) { + holder.name.setVisibility(View.VISIBLE); + holder.name.setText( + (searchResult.getAddress().getDisplayName() != null) + ? searchResult.getAddress().getDisplayName() + : searchResult.getAddress().getUsername()); + } + + holder.disabled.setVisibility(View.GONE); + if (contact != null) { + if (contact.getFullName() == null + && contact.getFirstName() == null + && contact.getLastName() == null) { + contact.setFullName(holder.name.getText().toString()); + } + ContactAvatar.displayAvatar( + contact, + contact.hasFriendCapability(FriendCapability.LimeX3Dh), + holder.avatarLayout); + + if ((!mIsOnlyOnePersonSelection + && !searchResult.hasCapability(FriendCapability.GroupChat)) + || (mSecurityEnabled + && !searchResult.hasCapability(FriendCapability.LimeX3Dh))) { + // Disable row, contact doesn't have the required capabilities + holder.disabled.setVisibility(View.VISIBLE); + } else if (mSecurityEnabled || !mIsOnlyOnePersonSelection) { + ProxyConfig lpc = + LinphoneManager.getLcIfManagerNotDestroyedOrNull().getDefaultProxyConfig(); + if (lpc != null + && searchResult.getAddress() != null + && lpc.getIdentityAddress().weakEqual(searchResult.getAddress())) { + // Disable row, we can't use our own address in a group chat room + holder.disabled.setVisibility(View.VISIBLE); + } + } + } else { + ContactAvatar.displayAvatar(holder.name.getText().toString(), holder.avatarLayout); + } + + holder.address.setText(numberOrAddress); + if (holder.linphoneContact != null) { + holder.linphoneContact.setVisibility(View.GONE); + if (searchResult.getFriend() != null + && contact != null + && contact.getBasicStatusFromPresenceModelForUriOrTel(numberOrAddress) + == PresenceBasicStatus.Open) { + holder.linphoneContact.setVisibility(View.VISIBLE); + } + } + if (holder.isSelect != null) { + if (isContactSelected(searchResult)) { + holder.isSelect.setVisibility(View.VISIBLE); + } else { + holder.isSelect.setVisibility(View.INVISIBLE); + } + if (mIsOnlyOnePersonSelection) { + holder.isSelect.setVisibility(View.GONE); + } + } + } + + public long getItemId(int position) { + return position; + } + + public synchronized boolean isContactSelected(SearchResult sr) { + for (ContactAddress c : mContactsSelected) { + Address addr = c.getAddress(); + if (addr != null && sr.getAddress() != null) { + if (addr.weakEqual(sr.getAddress())) { + return true; + } + } else { + if (c.getPhoneNumber() != null && sr.getPhoneNumber() != null) { + if (c.getPhoneNumber().compareTo(sr.getPhoneNumber()) == 0) return true; + } + } + } + return false; + } + + public synchronized ArrayList getContactsSelectedList() { + return mContactsSelected; + } + + public synchronized void setContactsSelectedList(ArrayList contactsList) { + if (contactsList == null) { + mContactsSelected = new ArrayList<>(); + } else { + mContactsSelected = contactsList; + } + } + + public synchronized boolean toggleContactSelection(ContactAddress ca) { + if (mContactsSelected.contains(ca)) { + mContactsSelected.remove(ca); + return false; + } else { + mContactsSelected.add(ca); + return true; + } + } + + private SearchResult getItem(int position) { + return mContacts.get(position); + } + + @Override + public int getItemCount() { + return mContacts.size(); + } + + public void searchContacts(String search) { + List result = new ArrayList<>(); + + if (mPreviousSearch != null) { + if (mPreviousSearch.length() > search.length()) { + ContactsManager.getInstance().getMagicSearch().resetSearchCache(); + } + } + mPreviousSearch = search; + + String domain = ""; + ProxyConfig prx = LinphoneManager.getLc().getDefaultProxyConfig(); + if (prx != null) domain = prx.getDomain(); + SearchResult[] searchResults = + ContactsManager.getInstance() + .getMagicSearch() + .getContactListFromFilter(search, mOnlySipContact ? domain : ""); + + for (SearchResult sr : searchResults) { + if (LinphoneActivity.instance() + .getResources() + .getBoolean(R.bool.hide_sip_contacts_without_presence)) { + if (sr.getFriend() != null) { + PresenceModel pm = + sr.getFriend() + .getPresenceModelForUriOrTel(sr.getAddress().asStringUriOnly()); + if (pm != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { + result.add(sr); + } else { + pm = sr.getFriend().getPresenceModelForUriOrTel(sr.getPhoneNumber()); + if (pm != null && pm.getBasicStatus().equals(PresenceBasicStatus.Open)) { + result.add(sr); + } + } + } + } else { + result.add(sr); + } + } + + mContacts = result; + notifyDataSetChanged(); + } +} diff --git a/src/android/org/linphone/firebase/FirebaseMessaging.java b/app/src/main/java/org/linphone/firebase/FirebaseMessaging.java similarity index 65% rename from src/android/org/linphone/firebase/FirebaseMessaging.java rename to app/src/main/java/org/linphone/firebase/FirebaseMessaging.java index d13c64dc2..417bbccf4 100644 --- a/src/android/org/linphone/firebase/FirebaseMessaging.java +++ b/app/src/main/java/org/linphone/firebase/FirebaseMessaging.java @@ -2,7 +2,7 @@ package org.linphone.firebase; /* FirebaseMessaging.java -Copyright (C) 2017 Belledonne Communications, Grenoble, France +Copyright (C) 2017-2019 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 @@ -19,20 +19,29 @@ 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.content.Intent; - -import com.google.firebase.messaging.FirebaseMessagingService; -import com.google.firebase.messaging.RemoteMessage; - -import org.linphone.LinphoneManager; -import org.linphone.LinphoneService; -import org.linphone.LinphoneUtils; -import org.linphone.mediastream.Log; - import static android.content.Intent.ACTION_MAIN; +import android.content.Intent; +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; +import org.linphone.LinphoneService; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneUtils; + public class FirebaseMessaging extends FirebaseMessagingService { - public FirebaseMessaging() { + public FirebaseMessaging() {} + + @Override + public void onNewToken(final String token) { + android.util.Log.i("FirebaseIdService", "[Push Notification] Refreshed token: " + token); + + LinphoneUtils.dispatchOnUIThread( + new Runnable() { + @Override + public void run() { + LinphonePreferences.instance().setPushNotificationRegistrationID(token); + } + }); } @Override @@ -45,17 +54,6 @@ public class FirebaseMessaging extends FirebaseMessagingService { intent.setClass(this, LinphoneService.class); intent.putExtra("PushNotification", true); startService(intent); - } else if (LinphoneManager.isInstanciated() && LinphoneManager.getLc().getCallsNb() == 0) { - LinphoneUtils.dispatchOnUIThread(new Runnable() { - @Override - public void run() { - Log.i("[Push Notification] Push notification received with LinphoneManager still alive"); - if (LinphoneManager.isInstanciated() && LinphoneManager.getLc().getCallsNb() == 0) { - LinphoneManager.getLc().setNetworkReachable(false); - LinphoneManager.getLc().setNetworkReachable(true); - } - } - }); } } } diff --git a/app/src/main/java/org/linphone/firebase/FirebasePushHelper.java b/app/src/main/java/org/linphone/firebase/FirebasePushHelper.java new file mode 100644 index 000000000..5d7338014 --- /dev/null +++ b/app/src/main/java/org/linphone/firebase/FirebasePushHelper.java @@ -0,0 +1,73 @@ +package org.linphone.firebase; + +/* +FirebasePushHelper.java +Copyright (C) 2019 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.content.Context; +import androidx.annotation.NonNull; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; +import org.linphone.R; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.PushNotificationUtils; + +public class FirebasePushHelper implements PushNotificationUtils.PushHelperInterface { + public FirebasePushHelper() {} + + @Override + public void init(Context context) { + Log.i( + "[Push Notification] firebase push sender id " + + context.getString(R.string.gcm_defaultSenderId)); + try { + FirebaseInstanceId.getInstance() + .getInstanceId() + .addOnCompleteListener( + new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful()) { + Log.e( + "[Push Notification] firebase getInstanceId failed: " + + task.getException()); + return; + } + String token = task.getResult().getToken(); + Log.i("[Push Notification] firebase token is: " + token); + LinphonePreferences.instance() + .setPushNotificationRegistrationID(token); + } + }); + } catch (Exception e) { + Log.e("[Push Notification] firebase not available."); + } + } + + @Override + public boolean isAvailable(Context context) { + GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance(); + int resultCode = googleApiAvailability.isGooglePlayServicesAvailable(context); + return resultCode == ConnectionResult.SUCCESS; + } +} diff --git a/app/src/main/java/org/linphone/fragments/AboutFragment.java b/app/src/main/java/org/linphone/fragments/AboutFragment.java new file mode 100644 index 000000000..e67486b8e --- /dev/null +++ b/app/src/main/java/org/linphone/fragments/AboutFragment.java @@ -0,0 +1,188 @@ +package org.linphone.fragments; +/* +AboutFragment.java +Copyright (C) 2017 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.app.ProgressDialog; +import android.content.Intent; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +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.view.WindowManager; +import android.widget.TextView; +import androidx.core.content.ContextCompat; +import org.linphone.BuildConfig; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.Core; +import org.linphone.core.Core.LogCollectionUploadState; +import org.linphone.core.CoreListenerStub; +import org.linphone.settings.LinphonePreferences; + +public class AboutFragment extends Fragment implements OnClickListener { + private View mSendLogButton = null; + private View mResetLogButton = null; + private CoreListenerStub mListener; + private ProgressDialog mProgress; + private boolean mUploadInProgress; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.about, container, false); + + TextView aboutVersion = view.findViewById(R.id.about_android_version); + TextView aboutLiblinphoneVersion = view.findViewById(R.id.about_liblinphone_sdk_version); + aboutLiblinphoneVersion.setText( + String.format( + getString(R.string.about_liblinphone_sdk_version), + getString(R.string.linphone_sdk_version) + + " (" + + getString(R.string.linphone_sdk_branch) + + ")")); + // We can't access a library's BuildConfig, so we have to set it as a resource + aboutVersion.setText( + String.format( + getString(R.string.about_version), + BuildConfig.VERSION_NAME + " (" + BuildConfig.VERSION_CODE + ")")); + + TextView privacyPolicy = view.findViewById(R.id.privacy_policy_link); + privacyPolicy.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + Intent browserIntent = + new Intent( + Intent.ACTION_VIEW, + Uri.parse(getString(R.string.about_privacy_policy_link))); + startActivity(browserIntent); + } + }); + + TextView license = view.findViewById(R.id.about_text); + license.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + Intent browserIntent = + new Intent( + Intent.ACTION_VIEW, + Uri.parse(getString(R.string.about_license_link))); + startActivity(browserIntent); + } + }); + + mSendLogButton = view.findViewById(R.id.send_log); + mSendLogButton.setOnClickListener(this); + mSendLogButton.setVisibility( + LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE); + + mResetLogButton = view.findViewById(R.id.reset_log); + mResetLogButton.setOnClickListener(this); + mResetLogButton.setVisibility( + LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE); + + mListener = + new CoreListenerStub() { + @Override + public void onLogCollectionUploadProgressIndication( + Core lc, int offset, int total) {} + + @Override + public void onLogCollectionUploadStateChanged( + Core lc, LogCollectionUploadState state, String info) { + if (state == LogCollectionUploadState.InProgress) { + displayUploadLogsInProgress(); + } else if (state == LogCollectionUploadState.Delivered + || state == LogCollectionUploadState.NotDelivered) { + mUploadInProgress = false; + if (mProgress != null) mProgress.dismiss(); + } + } + }; + + return view; + } + + private void displayUploadLogsInProgress() { + if (mUploadInProgress) { + return; + } + mUploadInProgress = true; + + mProgress = ProgressDialog.show(LinphoneActivity.instance(), null, null); + Drawable d = + new ColorDrawable(ContextCompat.getColor(getActivity(), R.color.light_grey_color)); + d.setAlpha(200); + mProgress + .getWindow() + .setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT); + mProgress.getWindow().setBackgroundDrawable(d); + mProgress.setContentView(R.layout.wait_layout); + mProgress.show(); + } + + @Override + public void onPause() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + } + + super.onPause(); + } + + @Override + public void onResume() { + Core 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()) { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (v == mSendLogButton) { + if (lc != null) { + lc.uploadLogCollection(); + } + } else if (v == mResetLogButton) { + if (lc != null) { + lc.resetLogCollection(); + } + } + } + } +} diff --git a/src/android/org/linphone/fragments/DialerFragment.java b/app/src/main/java/org/linphone/fragments/DialerFragment.java similarity index 55% rename from src/android/org/linphone/fragments/DialerFragment.java rename to app/src/main/java/org/linphone/fragments/DialerFragment.java index 019e7ed5b..2dd72a331 100644 --- a/src/android/org/linphone/fragments/DialerFragment.java +++ b/app/src/main/java/org/linphone/fragments/DialerFragment.java @@ -30,33 +30,36 @@ import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.LinearLayout; - +import org.linphone.LinphoneActivity; import org.linphone.LinphoneManager; import org.linphone.LinphoneService; import org.linphone.R; -import org.linphone.activities.LinphoneActivity; import org.linphone.contacts.ContactsManager; import org.linphone.core.Core; -import org.linphone.mediastream.Log; -import org.linphone.ui.AddressAware; -import org.linphone.ui.AddressText; -import org.linphone.ui.CallButton; -import org.linphone.ui.EraseButton; +import org.linphone.core.tools.Log; +import org.linphone.views.AddressAware; +import org.linphone.views.AddressText; +import org.linphone.views.CallButton; +import org.linphone.views.EraseButton; public class DialerFragment extends Fragment { - private static DialerFragment instance; - private static boolean isCallTransferOngoing = false; + private static DialerFragment sInstance; + private static boolean sIsCallTransferOngoing = false; - private AddressAware numpad; + private AddressAware mNumpad; private AddressText mAddress; private CallButton mCall; private ImageView mAddContact; - private OnClickListener addContactListener, cancelListener, transferListener; - private boolean shouldEmptyAddressField = true; + private OnClickListener mAddContactListener, mCancelListener, mTransferListener; + + /** @return null if not ready yet */ + public static DialerFragment instance() { + return sInstance; + } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.dialer, container, false); mAddress = view.findViewById(R.id.address); @@ -67,141 +70,143 @@ public class DialerFragment extends Fragment { mCall = view.findViewById(R.id.call); mCall.setAddressWidget(mAddress); - if (LinphoneActivity.isInstanciated() && LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null && LinphoneManager.getLcIfManagerNotDestroyedOrNull().getCallsNb() > 0) { - if (isCallTransferOngoing) { + if (LinphoneActivity.isInstanciated() + && LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null + && LinphoneManager.getLcIfManagerNotDestroyedOrNull().getCallsNb() > 0) { + if (sIsCallTransferOngoing) { mCall.setImageResource(R.drawable.call_transfer); } else { mCall.setImageResource(R.drawable.call_add); } } else { - if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null && LinphoneManager.getLcIfManagerNotDestroyedOrNull().getVideoActivationPolicy().getAutomaticallyInitiate()) { + if (LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null + && LinphoneManager.getLcIfManagerNotDestroyedOrNull() + .getVideoActivationPolicy() + .getAutomaticallyInitiate()) { mCall.setImageResource(R.drawable.call_video_start); } else { mCall.setImageResource(R.drawable.call_audio_start); } } - numpad = view.findViewById(R.id.numpad); - if (numpad != null) { - numpad.setAddressWidget(mAddress); + mNumpad = view.findViewById(R.id.numpad); + if (mNumpad != null) { + mNumpad.setAddressWidget(mAddress); } mAddContact = view.findViewById(R.id.add_contact); - mAddContact.setEnabled(!(LinphoneActivity.isInstanciated() && LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null && LinphoneManager.getLc().getCallsNb() > 0)); + mAddContact.setEnabled( + !(LinphoneActivity.isInstanciated() + && LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null + && 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) { - Core lc = LinphoneManager.getLc(); - if (lc.getCurrentCall() == null) { - return; - } - lc.transferCall(lc.getCurrentCall(), mAddress.getText().toString()); - isCallTransferOngoing = false; - LinphoneActivity.instance().resetClassicMenuLayoutAndGoBackToCallIfStillRunning(); - } - }; + mAddContactListener = + new OnClickListener() { + @Override + public void onClick(View v) { + LinphoneActivity.instance() + .displayContactsForEdition(mAddress.getText().toString()); + } + }; + mCancelListener = + new OnClickListener() { + @Override + public void onClick(View v) { + LinphoneActivity.instance() + .resetClassicMenuLayoutAndGoBackToCallIfStillRunning(); + } + }; + mTransferListener = + new OnClickListener() { + @Override + public void onClick(View v) { + Core lc = LinphoneManager.getLc(); + if (lc.getCurrentCall() == null) { + return; + } + lc.transferCall(lc.getCurrentCall(), mAddress.getText().toString()); + sIsCallTransferOngoing = false; + LinphoneActivity.instance() + .resetClassicMenuLayoutAndGoBackToCallIfStillRunning(); + } + }; - resetLayout(isCallTransferOngoing); + resetLayout(); 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); } } - instance = this; + sInstance = this; return view; } - /** - * @return null if not ready yet - */ - public static DialerFragment instance() { - return instance; - } - @Override public void onPause() { - instance = null; + sInstance = null; super.onPause(); } @Override public void onResume() { super.onResume(); - instance = this; + sInstance = this; if (LinphoneActivity.isInstanciated()) { LinphoneActivity.instance().selectMenu(FragmentsAvailable.DIALER); - LinphoneActivity.instance().updateDialerFragment(this); + LinphoneActivity.instance().updateDialerFragment(); LinphoneActivity.instance().showStatusBar(); - LinphoneActivity.instance().hideTabBar(false); } - boolean isOrientationLandscape = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + boolean isOrientationLandscape = + getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; if (isOrientationLandscape && !getResources().getBoolean(R.bool.isTablet)) { - ((LinearLayout) numpad).setVisibility(View.GONE); + ((LinearLayout) mNumpad).setVisibility(View.GONE); } else { - ((LinearLayout) numpad).setVisibility(View.VISIBLE); + ((LinearLayout) mNumpad).setVisibility(View.VISIBLE); } - if (shouldEmptyAddressField) { - mAddress.setText(""); - } else { - shouldEmptyAddressField = true; - } - resetLayout(isCallTransferOngoing); + resetLayout(); - String addressWaitingToBeCalled = LinphoneActivity.instance().mAddressWaitingToBeCalled; + String addressWaitingToBeCalled = LinphoneActivity.instance().addressWaitingToBeCalled; if (addressWaitingToBeCalled != null) { mAddress.setText(addressWaitingToBeCalled); - if (getResources().getBoolean(R.bool.automatically_start_intercepted_outgoing_gsm_call)) { + if (getResources() + .getBoolean(R.bool.automatically_start_intercepted_outgoing_gsm_call)) { newOutgoingCall(addressWaitingToBeCalled); } - LinphoneActivity.instance().mAddressWaitingToBeCalled = null; + LinphoneActivity.instance().addressWaitingToBeCalled = null; } } - public void resetLayout(boolean callTransfer) { + public void resetLayout() { if (!LinphoneActivity.isInstanciated()) { return; } - isCallTransferOngoing = LinphoneActivity.instance().isCallTransfer(); + sIsCallTransferOngoing = LinphoneActivity.instance().isCallTransfer(); Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); if (lc == null) { return; } if (lc.getCallsNb() > 0) { - if (isCallTransferOngoing) { + if (sIsCallTransferOngoing) { mCall.setImageResource(R.drawable.call_transfer); - mCall.setExternalClickListener(transferListener); + mCall.setExternalClickListener(mTransferListener); } else { mCall.setImageResource(R.drawable.call_add); mCall.resetClickListener(); } mAddContact.setEnabled(true); - mAddContact.setImageResource(R.drawable.call_alt_back); - mAddContact.setOnClickListener(cancelListener); + mAddContact.setImageResource(R.drawable.call_back); + mAddContact.setOnClickListener(mCancelListener); } else { if (LinphoneManager.getLc().getVideoActivationPolicy().getAutomaticallyInitiate()) { mCall.setImageResource(R.drawable.call_video_start); @@ -209,18 +214,20 @@ public class DialerFragment extends Fragment { mCall.setImageResource(R.drawable.call_audio_start); } mAddContact.setEnabled(false); - mAddContact.setImageResource(R.drawable.contact_add_button); - mAddContact.setOnClickListener(addContactListener); + mAddContact.setImageResource(R.drawable.contact_add); + mAddContact.setOnClickListener(mAddContactListener); enableDisableAddContact(); } } public void enableDisableAddContact() { - mAddContact.setEnabled(LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null && LinphoneManager.getLc().getCallsNb() > 0 || !mAddress.getText().toString().equals("")); + mAddContact.setEnabled( + LinphoneManager.getLcIfManagerNotDestroyedOrNull() != null + && LinphoneManager.getLc().getCallsNb() > 0 + || !mAddress.getText().toString().equals("")); } public void displayTextInAddressBar(String numberOrSipAddress) { - shouldEmptyAddressField = false; mAddress.setText(numberOrSipAddress); } @@ -238,7 +245,11 @@ public class DialerFragment extends Fragment { mAddress.setText(intent.getData().getSchemeSpecificPart()); } else { Uri contactUri = intent.getData(); - String address = ContactsManager.getAddressOrNumberForAndroidContact(LinphoneService.instance().getContentResolver(), contactUri); + String address = + ContactsManager.getInstance() + .getAddressOrNumberForAndroidContact( + LinphoneService.instance().getContentResolver(), + contactUri); if (address != null) { mAddress.setText(address); } else { diff --git a/src/android/org/linphone/fragments/EmptyFragment.java b/app/src/main/java/org/linphone/fragments/EmptyFragment.java similarity index 81% rename from src/android/org/linphone/fragments/EmptyFragment.java rename to app/src/main/java/org/linphone/fragments/EmptyFragment.java index e5763481b..3aca04fa3 100644 --- a/src/android/org/linphone/fragments/EmptyFragment.java +++ b/app/src/main/java/org/linphone/fragments/EmptyFragment.java @@ -24,17 +24,14 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; - import org.linphone.R; public class EmptyFragment extends Fragment { @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.empty_fragment, container, false); - return view; + return inflater.inflate(R.layout.empty_fragment, container, false); } - } diff --git a/src/android/org/linphone/fragments/FragmentsAvailable.java b/app/src/main/java/org/linphone/fragments/FragmentsAvailable.java similarity index 67% rename from src/android/org/linphone/fragments/FragmentsAvailable.java rename to app/src/main/java/org/linphone/fragments/FragmentsAvailable.java index 6dfabfd74..60e237a9a 100644 --- a/src/android/org/linphone/fragments/FragmentsAvailable.java +++ b/app/src/main/java/org/linphone/fragments/FragmentsAvailable.java @@ -31,12 +31,15 @@ public enum FragmentsAvailable { ABOUT, ACCOUNT_SETTINGS, SETTINGS, + SETTINGS_SUBLEVEL, CHAT_LIST, CHAT, CREATE_CHAT, INFO_GROUP_CHAT, GROUP_CHAT, - MESSAGE_IMDN; + MESSAGE_IMDN, + CONTACT_DEVICES, + RECORDING_LIST; public boolean shouldAddItselfToTheRightOf(FragmentsAvailable fragment) { switch (this) { @@ -44,20 +47,33 @@ public enum FragmentsAvailable { return fragment == HISTORY_LIST || fragment == HISTORY_DETAIL; case CONTACT_DETAIL: - return fragment == CONTACTS_LIST || fragment == CONTACT_EDITOR || fragment == 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; + return fragment == CONTACTS_LIST + || fragment == CONTACT_DETAIL + || fragment == CONTACT_EDITOR; case CHAT: return fragment == CHAT_LIST || fragment == CHAT; case GROUP_CHAT: - return fragment == CHAT_LIST || fragment == GROUP_CHAT; + return fragment == CHAT_LIST + || fragment == GROUP_CHAT + || fragment == INFO_GROUP_CHAT + || fragment == CREATE_CHAT; case MESSAGE_IMDN: return fragment == GROUP_CHAT || fragment == MESSAGE_IMDN; + case SETTINGS_SUBLEVEL: + return fragment == SETTINGS || fragment == SETTINGS_SUBLEVEL; + + case CONTACT_DEVICES: + return fragment == GROUP_CHAT || fragment == CONTACT_DEVICES; + default: return false; } diff --git a/app/src/main/java/org/linphone/fragments/StatusFragment.java b/app/src/main/java/org/linphone/fragments/StatusFragment.java new file mode 100644 index 000000000..e80ddc749 --- /dev/null +++ b/app/src/main/java/org/linphone/fragments/StatusFragment.java @@ -0,0 +1,509 @@ +package org.linphone.fragments; +/* +StatusFragment.java +Copyright (C) 2017 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.Activity; +import android.app.Dialog; +import android.app.Fragment; +import android.content.Context; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +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.Button; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.core.content.ContextCompat; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.assistant.AssistantActivity; +import org.linphone.call.CallActivity; +import org.linphone.call.CallIncomingActivity; +import org.linphone.call.CallOutgoingActivity; +import org.linphone.core.Call; +import org.linphone.core.Content; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.Event; +import org.linphone.core.MediaEncryption; +import org.linphone.core.ProxyConfig; +import org.linphone.core.RegistrationState; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; + +public class StatusFragment extends Fragment { + private final Handler mRefreshHandler = new Handler(); + private TextView mStatusText, mVoicemailCount; + private ImageView mStatusLed, mCallQuality, mEncryption, mMenu, mVoicemail; + private Runnable mCallQualityUpdater; + private boolean mIsInCall, mIsAttached = false; + private CoreListenerStub mListener; + private Dialog mZrtpDialog = null; + private int mDisplayedQuality = -1; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.status, container, false); + + mStatusText = view.findViewById(R.id.status_text); + mStatusLed = view.findViewById(R.id.status_led); + mCallQuality = view.findViewById(R.id.call_quality); + mEncryption = view.findViewById(R.id.encryption); + mMenu = view.findViewById(R.id.side_menu_button); + mVoicemail = view.findViewById(R.id.voicemail); + mVoicemailCount = view.findViewById(R.id.voicemail_count); + + // We create it once to not delay the first display + populateSliderContent(); + + mListener = + new CoreListenerStub() { + @Override + public void onRegistrationStateChanged( + final Core lc, + final ProxyConfig proxy, + final RegistrationState state, + String smessage) { + if (!mIsAttached || !LinphoneService.isReady()) { + return; + } + + if (lc.getProxyConfigList() == null) { + mStatusLed.setImageResource(R.drawable.led_disconnected); + mStatusText.setText(getString(R.string.no_account)); + } else { + mStatusLed.setVisibility(View.VISIBLE); + } + + if (lc.getDefaultProxyConfig() != null + && lc.getDefaultProxyConfig().equals(proxy)) { + mStatusLed.setImageResource(getStatusIconResource(state)); + mStatusText.setText(getStatusIconText(state)); + } else if (lc.getDefaultProxyConfig() == null) { + mStatusLed.setImageResource(getStatusIconResource(state)); + mStatusText.setText(getStatusIconText(state)); + } + + try { + mStatusText.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + Core core = + LinphoneManager + .getLcIfManagerNotDestroyedOrNull(); + if (core != null) { + core.refreshRegisters(); + } + } + }); + } catch (IllegalStateException ise) { + Log.e(ise); + } + } + + @Override + public void onNotifyReceived( + Core lc, Event ev, String eventName, Content content) { + + if (!content.getType().equals("application")) return; + if (!content.getSubtype().equals("simple-message-summary")) return; + + if (content.getSize() == 0) return; + + int unreadCount = 0; + String data = content.getStringBuffer().toLowerCase(); + String[] voiceMail = data.split("voice-message: "); + if (voiceMail.length >= 2) { + final String[] intToParse = voiceMail[1].split("/", 0); + try { + unreadCount = Integer.parseInt(intToParse[0]); + } catch (NumberFormatException nfe) { + + } + if (unreadCount > 0) { + mVoicemailCount.setText(String.valueOf(unreadCount)); + mVoicemail.setVisibility(View.VISIBLE); + mVoicemailCount.setVisibility(View.VISIBLE); + } else { + mVoicemail.setVisibility(View.GONE); + mVoicemailCount.setVisibility(View.GONE); + } + } + } + }; + + mIsAttached = true; + Activity activity = getActivity(); + + if (activity instanceof LinphoneActivity) { + ((LinphoneActivity) activity).updateStatusFragment(this); + } else if (activity instanceof CallActivity) { + ((CallActivity) activity).updateStatusFragment(this); + } else if (activity instanceof AssistantActivity) { + ((AssistantActivity) activity).updateStatusFragment(this); + } + mIsInCall = + activity instanceof CallActivity + || activity instanceof CallIncomingActivity + || activity instanceof CallOutgoingActivity; + + return view; + } + + public void setCoreListener() { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + + ProxyConfig lpc = lc.getDefaultProxyConfig(); + if (lpc != null) { + mListener.onRegistrationStateChanged(lc, lpc, lpc.getState(), null); + } + } + } + + @Override + public void onDetach() { + super.onDetach(); + mIsAttached = false; + } + + // NORMAL STATUS BAR + + private void populateSliderContent() { + if (LinphoneManager.isInstanciated() && LinphoneManager.getLc() != null) { + mVoicemailCount.setVisibility(View.GONE); + + if (mIsInCall && mIsAttached) { + // Call call = LinphoneManager.getLc().getCurrentCall(); + // initCallStatsRefresher(call, callStats); + } else if (!mIsInCall) { + mVoicemailCount.setVisibility(View.VISIBLE); + } + + if (LinphoneManager.getLc().getProxyConfigList().length == 0) { + mStatusLed.setImageResource(R.drawable.led_disconnected); + mStatusText.setText(getString(R.string.no_account)); + } + } + } + + public void resetAccountStatus() { + if (LinphoneManager.getLc().getProxyConfigList().length == 0) { + mStatusLed.setImageResource(R.drawable.led_disconnected); + mStatusText.setText(getString(R.string.no_account)); + } + } + + public void enableSideMenu(boolean enabled) { + mMenu.setEnabled(enabled); + } + + private int getStatusIconResource(RegistrationState state) { + try { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + boolean defaultAccountConnected = + (lc != null + && lc.getDefaultProxyConfig() != null + && lc.getDefaultProxyConfig().getState() == RegistrationState.Ok); + if (state == RegistrationState.Ok && defaultAccountConnected) { + return R.drawable.led_connected; + } else if (state == RegistrationState.Progress) { + return R.drawable.led_inprogress; + } else if (state == RegistrationState.Failed) { + return R.drawable.led_error; + } else { + return R.drawable.led_disconnected; + } + } catch (Exception e) { + Log.e(e); + } + + return R.drawable.led_disconnected; + } + + private String getStatusIconText(RegistrationState state) { + Context context = getActivity(); + if (!mIsAttached && LinphoneActivity.isInstanciated()) + context = LinphoneActivity.instance(); + else if (!mIsAttached && LinphoneService.isReady()) context = LinphoneService.instance(); + + try { + if (state == RegistrationState.Ok + && LinphoneManager.getLcIfManagerNotDestroyedOrNull() + .getDefaultProxyConfig() + .getState() + == RegistrationState.Ok) { + return context.getString(R.string.status_connected); + } else if (state == RegistrationState.Progress) { + return context.getString(R.string.status_in_progress); + } else if (state == RegistrationState.Failed) { + return context.getString(R.string.status_error); + } else { + return context.getString(R.string.status_not_connected); + } + } catch (Exception e) { + Log.e(e); + } + + return context.getString(R.string.status_not_connected); + } + + // INCALL STATUS BAR + private void startCallQuality() { + mCallQuality.setVisibility(View.VISIBLE); + mRefreshHandler.postDelayed( + mCallQualityUpdater = + new Runnable() { + final Call mCurrentCall = LinphoneManager.getLc().getCurrentCall(); + + public void run() { + if (mCurrentCall == null) { + mCallQualityUpdater = null; + return; + } + float newQuality = mCurrentCall.getCurrentQuality(); + updateQualityOfSignalIcon(newQuality); + + if (mIsInCall) { + mRefreshHandler.postDelayed(this, 1000); + } else mCallQualityUpdater = null; + } + }, + 1000); + } + + private void updateQualityOfSignalIcon(float quality) { + int iQuality = (int) quality; + + if (iQuality == mDisplayedQuality) return; + if (quality >= 4) // Good Quality + { + mCallQuality.setImageResource(R.drawable.call_quality_indicator_4); + } else if (quality >= 3) // Average quality + { + mCallQuality.setImageResource(R.drawable.call_quality_indicator_3); + } else if (quality >= 2) // Low quality + { + mCallQuality.setImageResource(R.drawable.call_quality_indicator_2); + } else if (quality >= 1) // Very low quality + { + mCallQuality.setImageResource(R.drawable.call_quality_indicator_1); + } else // Worst quality + { + mCallQuality.setImageResource(R.drawable.call_quality_indicator_0); + } + mDisplayedQuality = iQuality; + } + + @Override + public void onResume() { + super.onResume(); + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.addListener(mListener); + ProxyConfig lpc = lc.getDefaultProxyConfig(); + if (lpc != null) { + mListener.onRegistrationStateChanged(lc, lpc, lpc.getState(), null); + } + + Call call = lc.getCurrentCall(); + if (mIsInCall && (call != null || lc.getConferenceSize() > 1 || lc.getCallsNb() > 0)) { + if (call != null) { + startCallQuality(); + refreshStatusItems(call); + } + mMenu.setVisibility(View.INVISIBLE); + mCallQuality.setVisibility(View.VISIBLE); + + // We are obviously connected + if (lc.getDefaultProxyConfig() == null) { + mStatusLed.setImageResource(R.drawable.led_disconnected); + mStatusText.setText(getString(R.string.no_account)); + } else { + mStatusLed.setImageResource( + getStatusIconResource(lc.getDefaultProxyConfig().getState())); + mStatusText.setText(getStatusIconText(lc.getDefaultProxyConfig().getState())); + } + } + } else { + mStatusText.setVisibility(View.VISIBLE); + mEncryption.setVisibility(View.GONE); + } + } + + @Override + public void onPause() { + super.onPause(); + + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.removeListener(mListener); + } + + if (mCallQualityUpdater != null) { + mRefreshHandler.removeCallbacks(mCallQualityUpdater); + mCallQualityUpdater = null; + } + } + + public void refreshStatusItems(final Call call) { + if (call != null) { + mVoicemailCount.setVisibility(View.GONE); + MediaEncryption mediaEncryption = call.getCurrentParams().getMediaEncryption(); + + mEncryption.setVisibility(View.VISIBLE); + if (mediaEncryption == MediaEncryption.SRTP + || (mediaEncryption == MediaEncryption.ZRTP + && call.getAuthenticationTokenVerified()) + || mediaEncryption == MediaEncryption.DTLS) { + mEncryption.setImageResource(R.drawable.security_ok); + } else if (mediaEncryption == MediaEncryption.ZRTP + && !call.getAuthenticationTokenVerified()) { + mEncryption.setImageResource(R.drawable.security_pending); + } else { + mEncryption.setImageResource(R.drawable.security_ko); + // Do not show the unsecure icon if user doesn't want to do call mEncryption + if (LinphonePreferences.instance().getMediaEncryption() == MediaEncryption.None) { + mEncryption.setVisibility(View.GONE); + } + } + + if (mediaEncryption == MediaEncryption.ZRTP) { + mEncryption.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + showZRTPDialog(call); + } + }); + } else { + mEncryption.setOnClickListener(null); + } + } + } + + public void showZRTPDialog(final Call call) { + if (getActivity() == null) { + Log.w("Can't display ZRTP popup, no Activity"); + return; + } + + if (mZrtpDialog == null || !mZrtpDialog.isShowing()) { + String token = call.getAuthenticationToken(); + + if (token == null) { + Log.w("Can't display ZRTP popup, no token !"); + return; + } + if (token.length() < 4) { + Log.w("Can't display ZRTP popup, token is invalid (" + token + ")"); + return; + } + + mZrtpDialog = new Dialog(getActivity()); + mZrtpDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + mZrtpDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); + mZrtpDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + mZrtpDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + Drawable d = + new ColorDrawable( + ContextCompat.getColor(getActivity(), R.color.dark_grey_color)); + d.setAlpha(200); + mZrtpDialog.setContentView(R.layout.dialog); + mZrtpDialog + .getWindow() + .setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT); + mZrtpDialog.getWindow().setBackgroundDrawable(d); + String zrtpToRead, zrtpToListen; + + if (call.getDir().equals(Call.Dir.Incoming)) { + zrtpToRead = token.substring(0, 2); + zrtpToListen = token.substring(2); + } else { + zrtpToListen = token.substring(0, 2); + zrtpToRead = token.substring(2); + } + + TextView localSas = mZrtpDialog.findViewById(R.id.zrtp_sas_local); + localSas.setText(zrtpToRead.toUpperCase()); + TextView remoteSas = mZrtpDialog.findViewById(R.id.zrtp_sas_remote); + remoteSas.setText(zrtpToListen.toUpperCase()); + TextView message = mZrtpDialog.findViewById(R.id.dialog_message); + message.setVisibility(View.GONE); + mZrtpDialog.findViewById(R.id.dialog_zrtp_layout).setVisibility(View.VISIBLE); + + TextView title = mZrtpDialog.findViewById(R.id.dialog_title); + title.setText(getString(R.string.zrtp_dialog_title)); + title.setVisibility(View.VISIBLE); + + Button delete = mZrtpDialog.findViewById(R.id.dialog_delete_button); + delete.setText(R.string.deny); + Button cancel = mZrtpDialog.findViewById(R.id.dialog_cancel_button); + cancel.setVisibility(View.GONE); + Button accept = mZrtpDialog.findViewById(R.id.dialog_ok_button); + accept.setVisibility(View.VISIBLE); + accept.setText(R.string.accept); + + ImageView icon = mZrtpDialog.findViewById(R.id.dialog_icon); + icon.setVisibility(View.VISIBLE); + icon.setImageResource(R.drawable.security_2_indicator); + + delete.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + if (call != null) { + call.setAuthenticationTokenVerified(false); + if (mEncryption != null) { + mEncryption.setImageResource(R.drawable.security_ko); + } + } + mZrtpDialog.dismiss(); + } + }); + + accept.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View view) { + call.setAuthenticationTokenVerified(true); + if (mEncryption != null) { + mEncryption.setImageResource(R.drawable.security_ok); + } + mZrtpDialog.dismiss(); + } + }); + mZrtpDialog.show(); + } + } +} diff --git a/app/src/main/java/org/linphone/history/HistoryAdapter.java b/app/src/main/java/org/linphone/history/HistoryAdapter.java new file mode 100644 index 000000000..b4f46bea3 --- /dev/null +++ b/app/src/main/java/org/linphone/history/HistoryAdapter.java @@ -0,0 +1,185 @@ +package org.linphone.history; + +/* +HistoryAdapter.java +Copyright (C) 2018 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.annotation.SuppressLint; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.R; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.Call; +import org.linphone.core.CallLog; +import org.linphone.utils.LinphoneUtils; +import org.linphone.utils.SelectableAdapter; +import org.linphone.utils.SelectableHelper; +import org.linphone.views.ContactAvatar; + +public class HistoryAdapter extends SelectableAdapter { + private final List mLogs; + private final Context mContext; + private final HistoryViewHolder.ClickListener mClickListener; + + public HistoryAdapter( + Context aContext, + List logs, + HistoryViewHolder.ClickListener listener, + SelectableHelper helper) { + super(helper); + mLogs = logs; + mContext = aContext; + mClickListener = listener; + } + + public Object getItem(int position) { + return mLogs.get(position); + } + + @NonNull + @Override + public HistoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = + LayoutInflater.from(parent.getContext()) + .inflate(R.layout.history_cell, parent, false); + return new HistoryViewHolder(v, mClickListener); + } + + @Override + public void onBindViewHolder(@NonNull final HistoryViewHolder holder, final int position) { + final CallLog log = mLogs.get(position); + long timestamp = log.getStartDate() * 1000; + Address address; + + holder.contact.setSelected(true); // For automated horizontal scrolling of long texts + Calendar logTime = Calendar.getInstance(); + logTime.setTimeInMillis(timestamp); + holder.separatorText.setText(timestampToHumanDate(logTime)); + holder.select.setVisibility(isEditionEnabled() ? View.VISIBLE : View.GONE); + holder.select.setChecked(isSelected(position)); + + if (position > 0) { + CallLog previousLog = mLogs.get(position - 1); + long previousTimestamp = previousLog.getStartDate() * 1000; + Calendar previousLogTime = Calendar.getInstance(); + previousLogTime.setTimeInMillis(previousTimestamp); + + if (isSameDay(previousLogTime, logTime)) { + holder.separator.setVisibility(View.GONE); + } else { + holder.separator.setVisibility(View.VISIBLE); + } + } else { + holder.separator.setVisibility(View.VISIBLE); + } + + if (log.getDir() == Call.Dir.Incoming) { + address = log.getFromAddress(); + if (log.getStatus() == Call.Status.Missed) { + holder.callDirection.setImageResource(R.drawable.call_status_missed); + } else { + holder.callDirection.setImageResource(R.drawable.call_status_incoming); + } + } else { + address = log.getToAddress(); + holder.callDirection.setImageResource(R.drawable.call_status_outgoing); + } + + LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(address); + String displayName = null; + final String sipUri = (address != null) ? address.asString() : ""; + + if (c != null) { + displayName = c.getFullName(); + } + if (displayName == null) { + holder.contact.setText(LinphoneUtils.getAddressDisplayName(sipUri)); + } else { + holder.contact.setText(displayName); + } + + if (c != null) { + ContactAvatar.displayAvatar(c, holder.avatarLayout); + } else { + ContactAvatar.displayAvatar(holder.contact.getText().toString(), holder.avatarLayout); + } + + holder.detail.setVisibility(isEditionEnabled() ? View.INVISIBLE : View.VISIBLE); + holder.detail.setOnClickListener( + !isEditionEnabled() + ? new View.OnClickListener() { + @Override + public void onClick(View v) { + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().displayHistoryDetail(sipUri, log); + } + } + } + : null); + } + + @Override + public int getItemCount() { + return mLogs.size(); + } + + @SuppressLint("SimpleDateFormat") + private String timestampToHumanDate(Calendar cal) { + SimpleDateFormat dateFormat; + if (isToday(cal)) { + return mContext.getString(R.string.today); + } else if (isYesterday(cal)) { + return mContext.getString(R.string.yesterday); + } else { + dateFormat = + new SimpleDateFormat( + mContext.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); + } +} diff --git a/app/src/main/java/org/linphone/history/HistoryDetailFragment.java b/app/src/main/java/org/linphone/history/HistoryDetailFragment.java new file mode 100644 index 000000000..d36bd66a7 --- /dev/null +++ b/app/src/main/java/org/linphone/history/HistoryDetailFragment.java @@ -0,0 +1,284 @@ +package org.linphone.history; + +/* +HistoryDetailFragment.java +Copyright (C) 2017 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.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import java.util.Arrays; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.CallLog; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomBackend; +import org.linphone.core.ChatRoomListenerStub; +import org.linphone.core.ChatRoomParams; +import org.linphone.core.Core; +import org.linphone.core.Factory; +import org.linphone.core.FriendCapability; +import org.linphone.core.ProxyConfig; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.LinphoneUtils; +import org.linphone.views.ContactAvatar; + +public class HistoryDetailFragment extends Fragment implements OnClickListener { + private ImageView mDialBack, mChat, mAddToContacts, mGoToContact, mBack; + private View mView; + private TextView mContactName, mContactAddress; + private String mSipUri, mDisplayName; + private RelativeLayout mWaitLayout, mAvatarLayout, mChatSecured; + private LinphoneContact mContact; + private ChatRoom mChatRoom; + private ChatRoomListenerStub mChatRoomCreationListener; + private ListView mLogsList; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + mSipUri = getArguments().getString("SipUri"); + mDisplayName = getArguments().getString("DisplayName"); + + mView = inflater.inflate(R.layout.history_detail, container, false); + + mWaitLayout = mView.findViewById(R.id.waitScreen); + mWaitLayout.setVisibility(View.GONE); + + mDialBack = mView.findViewById(R.id.call); + mDialBack.setOnClickListener(this); + + mBack = mView.findViewById(R.id.back); + if (getResources().getBoolean(R.bool.isTablet)) { + mBack.setVisibility(View.INVISIBLE); + } else { + mBack.setOnClickListener(this); + } + + mChat = mView.findViewById(R.id.chat); + mChat.setOnClickListener(this); + + mChatSecured = mView.findViewById(R.id.chat_secured); + mChatSecured.setOnClickListener(this); + + if (getResources().getBoolean(R.bool.disable_chat)) { + mChat.setVisibility(View.GONE); + mChatSecured.setVisibility(View.GONE); + } + + mAddToContacts = mView.findViewById(R.id.add_contact); + mAddToContacts.setOnClickListener(this); + + mGoToContact = mView.findViewById(R.id.goto_contact); + mGoToContact.setOnClickListener(this); + + mAvatarLayout = mView.findViewById(R.id.avatar_layout); + mContactName = mView.findViewById(R.id.contact_name); + mContactAddress = mView.findViewById(R.id.contact_address); + + mChatRoomCreationListener = + new ChatRoomListenerStub() { + @Override + public void onStateChanged(ChatRoom cr, ChatRoom.State newState) { + if (newState == ChatRoom.State.Created) { + mWaitLayout.setVisibility(View.GONE); + LinphoneActivity.instance() + .goToChat( + cr.getLocalAddress().asStringUriOnly(), + cr.getPeerAddress().asStringUriOnly(), + null); + } else if (newState == ChatRoom.State.CreationFailed) { + mWaitLayout.setVisibility(View.GONE); + LinphoneActivity.instance().displayChatRoomError(); + Log.e( + "Group mChat room for address " + + cr.getPeerAddress() + + " has failed !"); + } + } + }; + + mLogsList = mView.findViewById(R.id.logs_list); + displayHistory(); + + return mView; + } + + private void displayHistory() { + Address lAddress = Factory.instance().createAddress(mSipUri); + mChatSecured.setVisibility(View.GONE); + + if (lAddress != null) { + CallLog[] logs = + LinphoneManager.getLcIfManagerNotDestroyedOrNull() + .getCallHistoryForAddress(lAddress); + List logsList = Arrays.asList(logs); + mLogsList.setAdapter( + new HistoryLogAdapter( + LinphoneActivity.instance(), R.layout.history_detail_cell, logsList)); + + mContactAddress.setText(LinphoneUtils.getDisplayableAddress(lAddress)); + mContact = ContactsManager.getInstance().findContactFromAddress(lAddress); + + if (mContact != null) { + mContactName.setText(mContact.getFullName()); + ContactAvatar.displayAvatar(mContact, mAvatarLayout); + mAddToContacts.setVisibility(View.GONE); + mGoToContact.setVisibility(View.VISIBLE); + + if (!getResources().getBoolean(R.bool.disable_chat) + && mContact.hasPresenceModelForUriOrTelCapability( + mSipUri, FriendCapability.LimeX3Dh)) { + mChatSecured.setVisibility(View.VISIBLE); + } + } else { + mContactName.setText( + mDisplayName == null + ? LinphoneUtils.getAddressDisplayName(mSipUri) + : mDisplayName); + ContactAvatar.displayAvatar( + LinphoneUtils.getAddressDisplayName(lAddress), mAvatarLayout); + mAddToContacts.setVisibility(View.VISIBLE); + mGoToContact.setVisibility(View.GONE); + } + } else { + mContactAddress.setText(mSipUri); + mContactName.setText( + mDisplayName == null + ? LinphoneUtils.getAddressDisplayName(mSipUri) + : mDisplayName); + } + } + + @Override + public void onPause() { + if (mChatRoom != null) { + mChatRoom.removeListener(mChatRoomCreationListener); + } + super.onPause(); + } + + public void changeDisplayedHistory(String sipUri, String displayName) { + if (displayName == null) { + displayName = LinphoneUtils.getUsernameFromAddress(sipUri); + } + + mSipUri = sipUri; + mDisplayName = displayName; + displayHistory(); + } + + @Override + public void onResume() { + super.onResume(); + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.HISTORY_DETAIL); + } + } + + @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(mSipUri, mDisplayName); + } else if (id == R.id.chat || id == R.id.chat_secured) { + boolean isSecured = id == R.id.chat_secured; + Core lc = LinphoneManager.getLc(); + Address participant = Factory.instance().createAddress(mSipUri); + ChatRoom room = + lc.findOneToOneChatRoom( + lc.getDefaultProxyConfig().getContact(), participant, isSecured); + if (room != null) { + LinphoneActivity.instance() + .goToChat( + room.getLocalAddress().asStringUriOnly(), + room.getPeerAddress().asStringUriOnly(), + null); + } else { + ProxyConfig lpc = lc.getDefaultProxyConfig(); + if (lpc != null + && lpc.getConferenceFactoryUri() != null + && (isSecured + || !LinphonePreferences.instance().useBasicChatRoomFor1To1())) { + mWaitLayout.setVisibility(View.VISIBLE); + + ChatRoomParams params = lc.createDefaultChatRoomParams(); + params.enableEncryption(isSecured); + params.enableGroup(false); + // We don't want a basic chat room + params.setBackend(ChatRoomBackend.FlexisipChat); + + Address participants[] = new Address[1]; + participants[0] = participant; + + mChatRoom = + lc.createChatRoom( + params, + getString(R.string.dummy_group_chat_subject), + participants); + if (mChatRoom != null) { + mChatRoom.addListener(mChatRoomCreationListener); + } else { + Log.w("[History Detail Fragment] createChatRoom returned null..."); + mWaitLayout.setVisibility(View.GONE); + } + } else { + room = lc.getChatRoom(participant); + LinphoneActivity.instance() + .goToChat( + room.getLocalAddress().asStringUriOnly(), + room.getPeerAddress().asStringUriOnly(), + null); + } + } + } else if (id == R.id.add_contact) { + Address addr = Factory.instance().createAddress(mSipUri); + if (addr != null) { + String address = + "sip:" + addr.getUsername() + "@" + addr.getDomain(); // Clean gruu param + if (addr.getDisplayName() != null) { + LinphoneActivity.instance() + .displayContactsForEdition(address, addr.getDisplayName()); + } else { + LinphoneActivity.instance().displayContactsForEdition(address); + } + } + } else if (id == R.id.goto_contact) { + LinphoneActivity.instance().displayContact(mContact, false); + } + } +} diff --git a/src/android/org/linphone/fragments/HistoryListFragment.java b/app/src/main/java/org/linphone/history/HistoryFragment.java similarity index 57% rename from src/android/org/linphone/fragments/HistoryListFragment.java rename to app/src/main/java/org/linphone/history/HistoryFragment.java index a9f349c5a..5db3963d8 100644 --- a/src/android/org/linphone/fragments/HistoryListFragment.java +++ b/app/src/main/java/org/linphone/history/HistoryFragment.java @@ -1,7 +1,7 @@ -package org.linphone.fragments; +package org.linphone.history; /* -HistoryListFragment.java +HistoryFragment.java Copyright (C) 2017 Belledonne Communications, Grenoble, France This program is free software; you can redistribute it and/or @@ -22,9 +22,6 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import android.app.Fragment; import android.content.Context; import android.os.Bundle; -import android.support.v7.widget.DividerItemDecoration; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -33,73 +30,78 @@ import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ImageView; import android.widget.TextView; - +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.linphone.LinphoneActivity; import org.linphone.LinphoneManager; import org.linphone.R; -import org.linphone.activities.LinphoneActivity; -import org.linphone.call.CallHistoryAdapter; import org.linphone.contacts.ContactsManager; import org.linphone.contacts.ContactsUpdatedListener; import org.linphone.core.Address; import org.linphone.core.Call; import org.linphone.core.CallLog; -import org.linphone.ui.SelectableHelper; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.utils.SelectableHelper; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class HistoryListFragment extends Fragment implements OnClickListener, OnItemClickListener, CallHistoryAdapter.ViewHolder.ClickListener, ContactsUpdatedListener, SelectableHelper.DeleteListener { - private RecyclerView historyList; - private TextView noCallHistory, noMissedCallHistory; - private ImageView missedCalls, allCalls, edit; - private View allCallsSelected, missedCallsSelected; +public class HistoryFragment extends Fragment + implements OnClickListener, + OnItemClickListener, + HistoryViewHolder.ClickListener, + ContactsUpdatedListener, + SelectableHelper.DeleteListener { + private RecyclerView mHistoryList; + private TextView mNoCallHistory, mNoMissedCallHistory; + private ImageView mMissedCalls, mAllCalls; + private View mAllCallsSelected, mMissedCallsSelected; private boolean mOnlyDisplayMissedCalls; private List mLogs; - private CallHistoryAdapter mHistoryAdapter; + private HistoryAdapter mHistoryAdapter; private LinearLayoutManager mLayoutManager; private Context mContext; private SelectableHelper mSelectionHelper; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.history, container, false); mContext = getActivity().getApplicationContext(); mSelectionHelper = new SelectableHelper(view, this); - noCallHistory = view.findViewById(R.id.no_call_history); - noMissedCallHistory = view.findViewById(R.id.no_missed_call_history); + mNoCallHistory = view.findViewById(R.id.no_call_history); + mNoMissedCallHistory = view.findViewById(R.id.no_missed_call_history); - historyList = view.findViewById(R.id.history_list); + mHistoryList = view.findViewById(R.id.history_list); mLayoutManager = new LinearLayoutManager(mContext); - historyList.setLayoutManager(mLayoutManager); - //Divider between items - DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(historyList.getContext(), - mLayoutManager.getOrientation()); + mHistoryList.setLayoutManager(mLayoutManager); + // Divider between items + DividerItemDecoration dividerItemDecoration = + new DividerItemDecoration( + mHistoryList.getContext(), mLayoutManager.getOrientation()); dividerItemDecoration.setDrawable(mContext.getResources().getDrawable(R.drawable.divider)); - historyList.addItemDecoration(dividerItemDecoration); + mHistoryList.addItemDecoration(dividerItemDecoration); - allCalls = view.findViewById(R.id.all_calls); - allCalls.setOnClickListener(this); + mAllCalls = view.findViewById(R.id.all_calls); + mAllCalls.setOnClickListener(this); - allCallsSelected = view.findViewById(R.id.all_calls_select); + mAllCallsSelected = view.findViewById(R.id.all_calls_select); - missedCalls = view.findViewById(R.id.missed_calls); - missedCalls.setOnClickListener(this); + mMissedCalls = view.findViewById(R.id.missed_calls); + mMissedCalls.setOnClickListener(this); - missedCallsSelected = view.findViewById(R.id.missed_calls_select); + mMissedCallsSelected = view.findViewById(R.id.missed_calls_select); - allCalls.setEnabled(false); + mAllCalls.setEnabled(false); mOnlyDisplayMissedCalls = false; - edit = view.findViewById(R.id.edit); - return view; } - public void refresh() { + private void refresh() { mLogs = Arrays.asList(LinphoneManager.getLc().getCallLogs()); } @@ -120,7 +122,7 @@ public class HistoryListFragment extends Fragment implements OnClickListener, On private void removeNotMissedCallsFromLogs() { if (mOnlyDisplayMissedCalls) { - List missedCalls = new ArrayList(); + List missedCalls = new ArrayList<>(); for (CallLog log : mLogs) { if (log.getStatus() == Call.Status.Missed) { missedCalls.add(log); @@ -130,57 +132,57 @@ public class HistoryListFragment extends Fragment implements OnClickListener, On } } - private boolean hideHistoryListAndDisplayMessageIfEmpty() { + private void hideHistoryListAndDisplayMessageIfEmpty() { removeNotMissedCallsFromLogs(); + mNoCallHistory.setVisibility(View.GONE); + mNoMissedCallHistory.setVisibility(View.GONE); + if (mLogs.isEmpty()) { if (mOnlyDisplayMissedCalls) { - noMissedCallHistory.setVisibility(View.VISIBLE); + mNoMissedCallHistory.setVisibility(View.VISIBLE); } else { - noCallHistory.setVisibility(View.VISIBLE); + mNoCallHistory.setVisibility(View.VISIBLE); } - historyList.setVisibility(View.GONE); - edit.setEnabled(false); - return true; + mHistoryList.setVisibility(View.GONE); } else { - noCallHistory.setVisibility(View.GONE); - noMissedCallHistory.setVisibility(View.GONE); - historyList.setVisibility(View.VISIBLE); - edit.setEnabled(true); - return false; + mNoCallHistory.setVisibility(View.GONE); + mNoMissedCallHistory.setVisibility(View.GONE); + mHistoryList.setVisibility(View.VISIBLE); } } @Override public void onResume() { super.onResume(); - ContactsManager.addContactsListener(this); + ContactsManager.getInstance().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()) { - mHistoryAdapter = new CallHistoryAdapter(getActivity().getApplicationContext(), mLogs, this, mSelectionHelper); - historyList.setAdapter(mHistoryAdapter); - mSelectionHelper.setAdapter(mHistoryAdapter); - mSelectionHelper.setDialogMessage(R.string.chat_room_delete_dialog); - } + hideHistoryListAndDisplayMessageIfEmpty(); + mHistoryAdapter = + new HistoryAdapter( + getActivity().getApplicationContext(), mLogs, this, mSelectionHelper); + mHistoryList.setAdapter(mHistoryAdapter); + mSelectionHelper.setAdapter(mHistoryAdapter); + mSelectionHelper.setDialogMessage(R.string.call_log_delete_dialog); } @Override public void onPause() { - ContactsManager.removeContactsListener(this); + ContactsManager.getInstance().removeContactsListener(this); super.onPause(); } @Override public void onContactsUpdated() { - if (!LinphoneActivity.isInstanciated() || LinphoneActivity.instance().getCurrentFragment() != FragmentsAvailable.HISTORY_LIST) - return; - CallHistoryAdapter adapter = (CallHistoryAdapter) historyList.getAdapter(); + if (!LinphoneActivity.isInstanciated() + || LinphoneActivity.instance().getCurrentFragment() + != FragmentsAvailable.HISTORY_LIST) return; + HistoryAdapter adapter = (HistoryAdapter) mHistoryList.getAdapter(); if (adapter != null) { adapter.notifyDataSetChanged(); } @@ -191,28 +193,25 @@ public class HistoryListFragment extends Fragment implements OnClickListener, On int id = v.getId(); if (id == R.id.all_calls) { - allCalls.setEnabled(false); - allCallsSelected.setVisibility(View.VISIBLE); - missedCallsSelected.setVisibility(View.INVISIBLE); - missedCalls.setEnabled(true); + mAllCalls.setEnabled(false); + mAllCallsSelected.setVisibility(View.VISIBLE); + mMissedCallsSelected.setVisibility(View.INVISIBLE); + mMissedCalls.setEnabled(true); mOnlyDisplayMissedCalls = false; refresh(); } if (id == R.id.missed_calls) { - allCalls.setEnabled(true); - allCallsSelected.setVisibility(View.INVISIBLE); - missedCallsSelected.setVisibility(View.VISIBLE); - missedCalls.setEnabled(false); + mAllCalls.setEnabled(true); + mAllCallsSelected.setVisibility(View.INVISIBLE); + mMissedCallsSelected.setVisibility(View.VISIBLE); + mMissedCalls.setEnabled(false); mOnlyDisplayMissedCalls = true; } - if (!hideHistoryListAndDisplayMessageIfEmpty()) { -// historyList.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE); - mHistoryAdapter = new CallHistoryAdapter(mContext, mLogs, this, mSelectionHelper); - historyList.setAdapter(mHistoryAdapter); - mSelectionHelper.setAdapter(mHistoryAdapter); - mSelectionHelper.setDialogMessage(R.string.chat_room_delete_dialog); - } - + hideHistoryListAndDisplayMessageIfEmpty(); + mHistoryAdapter = new HistoryAdapter(mContext, mLogs, this, mSelectionHelper); + mHistoryList.setAdapter(mHistoryAdapter); + mSelectionHelper.setAdapter(mHistoryAdapter); + mSelectionHelper.setDialogMessage(R.string.chat_room_delete_dialog); } @Override @@ -247,7 +246,9 @@ public class HistoryListFragment extends Fragment implements OnClickListener, On } else { address = log.getToAddress(); } - LinphoneActivity.instance().setAddresGoToDialerAndCall(address.asStringUriOnly(), address.getDisplayName(), null); + LinphoneActivity.instance() + .setAddresGoToDialerAndCall( + address.asStringUriOnly(), address.getDisplayName()); } } } @@ -260,4 +261,4 @@ public class HistoryListFragment extends Fragment implements OnClickListener, On mHistoryAdapter.toggleSelection(position); return true; } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/linphone/history/HistoryLogAdapter.java b/app/src/main/java/org/linphone/history/HistoryLogAdapter.java new file mode 100644 index 000000000..351444913 --- /dev/null +++ b/app/src/main/java/org/linphone/history/HistoryLogAdapter.java @@ -0,0 +1,115 @@ +package org.linphone.history; + +/* +HistoryLogAdapter.java +Copyright (C) 2019 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.annotation.SuppressLint; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.List; +import org.linphone.R; +import org.linphone.core.Call; +import org.linphone.core.CallLog; +import org.linphone.utils.LinphoneUtils; + +class HistoryLogAdapter extends ArrayAdapter { + private Context mContext; + private final List mItems; + private final int mResource; + + HistoryLogAdapter(@NonNull Context context, int resource, @NonNull List objects) { + super(context, resource, objects); + mContext = context; + mResource = resource; + mItems = objects; + } + + @Nullable + @Override + public CallLog getItem(int position) { + return mItems.get(position); + } + + @Override + public int getCount() { + return mItems.size(); + } + + @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()); + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + LayoutInflater inflater = + (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + View rowView = inflater.inflate(mResource, parent, false); + CallLog callLog = getItem(position); + + String callTime = secondsToDisplayableString(callLog.getDuration()); + String callDate = String.valueOf(callLog.getStartDate()); + String status; + if (callLog.getDir() == Call.Dir.Outgoing) { + status = mContext.getString(R.string.outgoing); + } else { + if (callLog.getStatus() == Call.Status.Missed) { + status = mContext.getString(R.string.missed); + } else { + status = mContext.getString(R.string.incoming); + } + } + + TextView date = rowView.findViewById(R.id.date); + TextView time = rowView.findViewById(R.id.time); + ImageView callDirection = rowView.findViewById(R.id.direction); + + if (status.equals(mContext.getResources().getString(R.string.missed))) { + callDirection.setImageResource(R.drawable.call_missed); + } else if (status.equals(mContext.getResources().getString(R.string.incoming))) { + callDirection.setImageResource(R.drawable.call_incoming); + } else if (status.equals(mContext.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( + mContext, + longDate, + mContext.getString(R.string.history_detail_date_format))); + + return rowView; + } +} diff --git a/app/src/main/java/org/linphone/history/HistoryViewHolder.java b/app/src/main/java/org/linphone/history/HistoryViewHolder.java new file mode 100644 index 000000000..3823ec869 --- /dev/null +++ b/app/src/main/java/org/linphone/history/HistoryViewHolder.java @@ -0,0 +1,77 @@ +package org.linphone.history; + +/* +HistoryViewHolder.java +Copyright (C) 2018 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.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import org.linphone.R; + +public class HistoryViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener, View.OnLongClickListener { + public final TextView contact; + public final ImageView detail; + public final CheckBox select; + public final ImageView callDirection; + public final RelativeLayout avatarLayout; + public final LinearLayout separator; + public final TextView separatorText; + + private final ClickListener mListener; + + public HistoryViewHolder(View view, ClickListener listener) { + super(view); + contact = view.findViewById(R.id.sip_uri); + detail = view.findViewById(R.id.detail); + select = view.findViewById(R.id.delete); + callDirection = view.findViewById(R.id.icon); + avatarLayout = view.findViewById(R.id.avatar_layout); + separator = view.findViewById(R.id.separator); + separatorText = view.findViewById(R.id.separator_text); + mListener = listener; + view.setOnClickListener(this); + view.setOnLongClickListener(this); + } + + @Override + public void onClick(View view) { + if (mListener != null) { + mListener.onItemClicked(getAdapterPosition()); + } + } + + @Override + public boolean onLongClick(View view) { + if (mListener != null) { + return mListener.onItemLongClicked(getAdapterPosition()); + } + return false; + } + + public interface ClickListener { + void onItemClicked(int position); + + boolean onItemLongClicked(int position); + } +} diff --git a/app/src/main/java/org/linphone/notifications/Notifiable.java b/app/src/main/java/org/linphone/notifications/Notifiable.java new file mode 100644 index 000000000..1cd7839e5 --- /dev/null +++ b/app/src/main/java/org/linphone/notifications/Notifiable.java @@ -0,0 +1,106 @@ +package org.linphone.notifications; + +/* +Notifiable.java +Copyright (C) 2018 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; + +public class Notifiable { + private final int mNotificationId; + private List mMessages; + private boolean mIsGroup; + private String mGroupTitle; + private String mLocalIdentity; + private String mMyself; + private int iconId; + private int textId; + + public Notifiable(int id) { + mNotificationId = id; + mMessages = new ArrayList<>(); + mIsGroup = false; + iconId = 0; + textId = 0; + } + + public int getNotificationId() { + return mNotificationId; + } + + public void resetMessages() { + mMessages = new ArrayList<>(); + } + + public void addMessage(NotifiableMessage notifMessage) { + mMessages.add(notifMessage); + } + + public List getMessages() { + return mMessages; + } + + public boolean isGroup() { + return mIsGroup; + } + + public void setIsGroup(boolean isGroup) { + mIsGroup = isGroup; + } + + public String getGroupTitle() { + return mGroupTitle; + } + + public void setGroupTitle(String title) { + mGroupTitle = title; + } + + public String getMyself() { + return mMyself; + } + + public void setMyself(String myself) { + mMyself = myself; + } + + public String getLocalIdentity() { + return mLocalIdentity; + } + + public void setLocalIdentity(String localIdentity) { + mLocalIdentity = localIdentity; + } + + public int getIconResourceId() { + return iconId; + } + + public void setIconResourceId(int id) { + iconId = id; + } + + public int getTextResourceId() { + return textId; + } + + public void setTextResourceId(int id) { + textId = id; + } +} diff --git a/app/src/main/java/org/linphone/notifications/NotifiableMessage.java b/app/src/main/java/org/linphone/notifications/NotifiableMessage.java new file mode 100644 index 000000000..15700e148 --- /dev/null +++ b/app/src/main/java/org/linphone/notifications/NotifiableMessage.java @@ -0,0 +1,69 @@ +package org.linphone.notifications; + +/* +NotifiableMessage.java +Copyright (C) 2018 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.graphics.Bitmap; +import android.net.Uri; + +public class NotifiableMessage { + private final String mMessage; + private final String mSender; + private final long mTime; + private Bitmap mSenderBitmap; + private final Uri mFilePath; + private final String mFileMime; + + public NotifiableMessage( + String message, String sender, long time, Uri filePath, String fileMime) { + mMessage = message; + mSender = sender; + mTime = time; + mFilePath = filePath; + mFileMime = fileMime; + } + + public String getMessage() { + return mMessage; + } + + public String getSender() { + return mSender; + } + + public long getTime() { + return mTime; + } + + public Bitmap getSenderBitmap() { + return mSenderBitmap; + } + + public void setSenderBitmap(Bitmap bm) { + mSenderBitmap = bm; + } + + public Uri getFilePath() { + return mFilePath; + } + + public String getFileMime() { + return mFileMime; + } +} diff --git a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.java b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.java new file mode 100644 index 000000000..23565460b --- /dev/null +++ b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.java @@ -0,0 +1,168 @@ +package org.linphone.notifications; + +/* +NotificationBroadcastReceiver.java +Copyright (C) 2018 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.Notification; +import android.app.RemoteInput; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.compatibility.Compatibility; +import org.linphone.core.Address; +import org.linphone.core.Call; +import org.linphone.core.ChatMessage; +import org.linphone.core.ChatMessageListenerStub; +import org.linphone.core.ChatRoom; +import org.linphone.core.Core; +import org.linphone.core.tools.Log; + +public class NotificationBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(final Context context, Intent intent) { + final int notifId = intent.getIntExtra(Compatibility.INTENT_NOTIF_ID, 0); + final String localyIdentity = intent.getStringExtra(Compatibility.INTENT_LOCAL_IDENTITY); + + if (intent.getAction().equals(Compatibility.INTENT_REPLY_NOTIF_ACTION) + || intent.getAction().equals(Compatibility.INTENT_MARK_AS_READ_ACTION)) { + String remoteSipAddr = + LinphoneService.instance() + .getNotificationManager() + .getSipUriForNotificationId(notifId); + + Core core = LinphoneManager.getLc(); + if (core == null) { + Log.e("[Notification Broadcast Receiver] Couldn't get Core instance"); + onError(context, notifId); + return; + } + + Address remoteAddr = core.interpretUrl(remoteSipAddr); + if (remoteAddr == null) { + Log.e( + "[Notification Broadcast Receiver] Couldn't interpret remote address " + + remoteSipAddr); + onError(context, notifId); + return; + } + + Address localAddr = core.interpretUrl(localyIdentity); + if (localAddr == null) { + Log.e( + "[Notification Broadcast Receiver] Couldn't interpret local address " + + localyIdentity); + onError(context, notifId); + return; + } + + ChatRoom room = core.getChatRoom(remoteAddr, localAddr); + if (room == null) { + Log.e( + "[Notification Broadcast Receiver] Couldn't find chat room for remote address " + + remoteSipAddr + + " and local address " + + localyIdentity); + onError(context, notifId); + return; + } + + room.markAsRead(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .displayMissedChats(LinphoneManager.getInstance().getUnreadMessageCount()); + } + + if (intent.getAction().equals(Compatibility.INTENT_REPLY_NOTIF_ACTION)) { + final String reply = getMessageText(intent).toString(); + if (reply == null) { + Log.e("[Notification Broadcast Receiver] Couldn't get reply text"); + onError(context, notifId); + return; + } + + ChatMessage msg = room.createMessage(reply); + msg.send(); + msg.addListener( + new ChatMessageListenerStub() { + @Override + public void onMsgStateChanged( + ChatMessage msg, ChatMessage.State state) { + if (state == ChatMessage.State.Delivered) { + Notification replied = + Compatibility.createRepliedNotification(context, reply); + LinphoneService.instance() + .getNotificationManager() + .sendNotification(notifId, replied); + } else if (state == ChatMessage.State.NotDelivered) { + Log.e( + "[Notification Broadcast Receiver] Couldn't send reply, message is not delivered"); + onError(context, notifId); + } + } + }); + } else { + LinphoneService.instance().getNotificationManager().dismissNotification(notifId); + } + } else if (intent.getAction().equals(Compatibility.INTENT_ANSWER_CALL_NOTIF_ACTION) + || intent.getAction().equals(Compatibility.INTENT_HANGUP_CALL_NOTIF_ACTION)) { + String remoteAddr = + LinphoneService.instance() + .getNotificationManager() + .getSipUriForCallNotificationId(notifId); + + Core core = LinphoneManager.getLc(); + if (core == null) { + Log.e("[Notification Broadcast Receiver] Couldn't get Core instance"); + return; + } + Call call = core.findCallFromUri(remoteAddr); + if (call == null) { + Log.e( + "[Notification Broadcast Receiver] Couldn't find call from remote address " + + remoteAddr); + return; + } + + if (intent.getAction().equals(Compatibility.INTENT_ANSWER_CALL_NOTIF_ACTION)) { + call.accept(); + } else { + call.terminate(); + } + } + } + + private void onError(Context context, int notifId) { + Notification replyError = + Compatibility.createRepliedNotification(context, context.getString(R.string.error)); + LinphoneService.instance().getNotificationManager().sendNotification(notifId, replyError); + } + + private CharSequence getMessageText(Intent intent) { + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + return remoteInput.getCharSequence(Compatibility.KEY_TEXT_REPLY); + } + return null; + } +} diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.java b/app/src/main/java/org/linphone/notifications/NotificationsManager.java new file mode 100644 index 000000000..2af730f7c --- /dev/null +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.java @@ -0,0 +1,433 @@ +package org.linphone.notifications; + +/* +NotificationsManager.java +Copyright (C) 2018 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 static android.content.Context.NOTIFICATION_SERVICE; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import java.util.HashMap; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.call.CallActivity; +import org.linphone.call.CallIncomingActivity; +import org.linphone.call.CallOutgoingActivity; +import org.linphone.compatibility.Compatibility; +import org.linphone.contacts.ContactsManager; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.Address; +import org.linphone.core.Call; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; +import org.linphone.utils.ImageUtils; +import org.linphone.utils.LinphoneUtils; + +public class NotificationsManager { + private static final int SERVICE_NOTIF_ID = 1; + private static final int MISSED_CALLS_NOTIF_ID = 2; + private static final int IN_APP_NOTIF_ID = 3; + + private final Context mContext; + private final NotificationManager mNM; + private final HashMap mChatNotifMap; + private final HashMap mCallNotifMap; + private int mLastNotificationId; + private final Notification mServiceNotification; + private int mCurrentForegroundServiceNotification; + + public NotificationsManager(Context context) { + mContext = context; + mChatNotifMap = new HashMap<>(); + mCallNotifMap = new HashMap<>(); + mCurrentForegroundServiceNotification = 0; + + mNM = (NotificationManager) mContext.getSystemService(NOTIFICATION_SERVICE); + mNM.cancelAll(); + + mLastNotificationId = 5; // Do not conflict with hardcoded notifications ids ! + + Compatibility.createNotificationChannels(mContext); + + Bitmap bm = null; + try { + bm = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.ic_launcher); + } catch (Exception e) { + Log.e(e); + } + + Intent notifIntent = new Intent(mContext, LinphoneActivity.class); + notifIntent.putExtra("Notification", true); + + PendingIntent pendingIntent = + PendingIntent.getActivity( + mContext, SERVICE_NOTIF_ID, notifIntent, PendingIntent.FLAG_UPDATE_CURRENT); + mServiceNotification = + Compatibility.createNotification( + mContext, + mContext.getString(R.string.service_name), + "", + R.drawable.linphone_notification_icon, + R.mipmap.ic_launcher, + bm, + pendingIntent, + Notification.PRIORITY_MIN); + + if (isServiceNotificationDisplayed()) { + startForeground(); + } + } + + public void destroy() { + mNM.cancelAll(); + } + + public void startForeground() { + LinphoneService.instance().startForeground(SERVICE_NOTIF_ID, mServiceNotification); + mCurrentForegroundServiceNotification = SERVICE_NOTIF_ID; + } + + public void startForeground(Notification notification, int id) { + LinphoneService.instance().startForeground(id, notification); + mCurrentForegroundServiceNotification = id; + } + + public void stopForeground() { + LinphoneService.instance().stopForeground(true); + mCurrentForegroundServiceNotification = 0; + } + + public void removeForegroundServiceNotificationIfPossible() { + if (!isServiceNotificationDisplayed() + && mCurrentForegroundServiceNotification == SERVICE_NOTIF_ID) { + stopForeground(); + } + } + + public void sendNotification(int id, Notification notif) { + mNM.notify(id, notif); + } + + public void resetMessageNotifCount(String address) { + Notifiable notif = mChatNotifMap.get(address); + if (notif != null) { + notif.resetMessages(); + mNM.cancel(notif.getNotificationId()); + } + } + + private boolean isServiceNotificationDisplayed() { + return LinphonePreferences.instance().getServiceNotificationVisibility(); + } + + public String getSipUriForNotificationId(int notificationId) { + for (String addr : mChatNotifMap.keySet()) { + if (mChatNotifMap.get(addr).getNotificationId() == notificationId) { + return addr; + } + } + return null; + } + + public void displayGroupChatMessageNotification( + String subject, + String conferenceAddress, + String fromName, + Uri fromPictureUri, + String message, + Address localIdentity, + long timestamp, + Uri filePath, + String fileMime) { + + Bitmap bm = ImageUtils.getRoundBitmapFromUri(mContext, fromPictureUri); + Notifiable notif = mChatNotifMap.get(conferenceAddress); + NotifiableMessage notifMessage = + new NotifiableMessage(message, fromName, timestamp, filePath, fileMime); + if (notif == null) { + notif = new Notifiable(mLastNotificationId); + mLastNotificationId += 1; + mChatNotifMap.put(conferenceAddress, notif); + } + + notifMessage.setSenderBitmap(bm); + notif.addMessage(notifMessage); + notif.setIsGroup(true); + notif.setGroupTitle(subject); + notif.setMyself(LinphoneUtils.getAddressDisplayName(localIdentity)); + notif.setLocalIdentity(localIdentity.asString()); + + Intent notifIntent = new Intent(mContext, LinphoneActivity.class); + notifIntent.putExtra("GoToChat", true); + notifIntent.putExtra("ChatContactSipUri", conferenceAddress); + notifIntent.putExtra("LocalSipUri", localIdentity.asStringUriOnly()); + PendingIntent pendingIntent = + PendingIntent.getActivity( + mContext, + notif.getNotificationId(), + notifIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + Notification notification = + Compatibility.createMessageNotification( + mContext, + notif, + subject, + mContext.getString(R.string.group_chat_notif) + .replace("%1", fromName) + .replace("%2", message), + bm, + pendingIntent); + sendNotification(notif.getNotificationId(), notification); + } + + public void displayMessageNotification( + String fromSipUri, + String fromName, + Uri fromPictureUri, + String message, + Address localIdentity, + long timestamp, + Uri filePath, + String fileMime) { + if (fromName == null) { + fromName = fromSipUri; + } + + Bitmap bm = ImageUtils.getRoundBitmapFromUri(mContext, fromPictureUri); + Notifiable notif = mChatNotifMap.get(fromSipUri); + NotifiableMessage notifMessage = + new NotifiableMessage(message, fromName, timestamp, filePath, fileMime); + if (notif == null) { + notif = new Notifiable(mLastNotificationId); + mLastNotificationId += 1; + mChatNotifMap.put(fromSipUri, notif); + } + + notifMessage.setSenderBitmap(bm); + notif.addMessage(notifMessage); + notif.setIsGroup(false); + notif.setMyself(LinphoneUtils.getAddressDisplayName(localIdentity)); + notif.setLocalIdentity(localIdentity.asString()); + + Intent notifIntent = new Intent(mContext, LinphoneActivity.class); + notifIntent.putExtra("GoToChat", true); + notifIntent.putExtra("ChatContactSipUri", fromSipUri); + notifIntent.putExtra("LocalSipUri", localIdentity.asStringUriOnly()); + PendingIntent pendingIntent = + PendingIntent.getActivity( + mContext, + notif.getNotificationId(), + notifIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + Notification notification = + Compatibility.createMessageNotification( + mContext, notif, fromName, message, bm, pendingIntent); + sendNotification(notif.getNotificationId(), notification); + } + + public void displayMissedCallNotification(Call call) { + Intent missedCallNotifIntent = new Intent(mContext, LinphoneActivity.class); + missedCallNotifIntent.putExtra("GoToHistory", true); + PendingIntent pendingIntent = + PendingIntent.getActivity( + mContext, + MISSED_CALLS_NOTIF_ID, + missedCallNotifIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + + int missedCallCount = + LinphoneManager.getLcIfManagerNotDestroyedOrNull().getMissedCallsCount(); + String body; + if (missedCallCount > 1) { + body = + mContext.getString(R.string.missed_calls_notif_body) + .replace("%i", String.valueOf(missedCallCount)); + } else { + Address address = call.getRemoteAddress(); + LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(address); + if (c != null) { + body = c.getFullName(); + } else { + body = address.getDisplayName(); + if (body == null) { + body = address.asStringUriOnly(); + } + } + } + + Notification notif = + Compatibility.createMissedCallNotification( + mContext, + mContext.getString(R.string.missed_calls_notif_title), + body, + pendingIntent); + sendNotification(MISSED_CALLS_NOTIF_ID, notif); + } + + public void displayCallNotification(Call call) { + if (call == null) return; + + Intent callNotifIntent; + if (call.getState() == Call.State.IncomingReceived + || call.getState() == Call.State.IncomingEarlyMedia) { + callNotifIntent = new Intent(mContext, CallIncomingActivity.class); + } else if (call.getState() == Call.State.OutgoingInit + || call.getState() == Call.State.OutgoingProgress + || call.getState() == Call.State.OutgoingRinging + || call.getState() == Call.State.OutgoingEarlyMedia) { + callNotifIntent = new Intent(mContext, CallOutgoingActivity.class); + } else { + callNotifIntent = new Intent(mContext, CallActivity.class); + } + callNotifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + PendingIntent pendingIntent = + PendingIntent.getActivity( + mContext, 0, callNotifIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + Address address = call.getRemoteAddress(); + String addressAsString = address.asStringUriOnly(); + Notifiable notif = mCallNotifMap.get(addressAsString); + if (notif == null) { + notif = new Notifiable(mLastNotificationId); + mLastNotificationId += 1; + mCallNotifMap.put(addressAsString, notif); + } + + int notificationTextId; + int iconId; + switch (call.getState()) { + case Released: + case End: + if (mCurrentForegroundServiceNotification == notif.getNotificationId()) { + // Call is released, remove service notification to allow for an other call to + // be service notification + stopForeground(); + } + mNM.cancel(notif.getNotificationId()); + mCallNotifMap.remove(addressAsString); + return; + case Paused: + case PausedByRemote: + case Pausing: + iconId = R.drawable.topbar_call_notification; + notificationTextId = R.string.incall_notif_paused; + break; + case IncomingEarlyMedia: + case IncomingReceived: + iconId = R.drawable.topbar_call_notification; + notificationTextId = R.string.incall_notif_incoming; + break; + case OutgoingEarlyMedia: + case OutgoingInit: + case OutgoingProgress: + case OutgoingRinging: + iconId = R.drawable.topbar_call_notification; + notificationTextId = R.string.incall_notif_outgoing; + break; + default: + if (call.getCurrentParams().videoEnabled()) { + iconId = R.drawable.topbar_videocall_notification; + notificationTextId = R.string.incall_notif_video; + } else { + iconId = R.drawable.topbar_call_notification; + notificationTextId = R.string.incall_notif_active; + } + break; + } + + if (notif.getIconResourceId() == iconId + && notif.getTextResourceId() == notificationTextId) { + // Notification hasn't changed, do not "update" it to avoid blinking + return; + } + notif.setIconResourceId(iconId); + notif.setTextResourceId(notificationTextId); + + LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address); + Uri pictureUri = contact != null ? contact.getPhotoUri() : null; + Bitmap bm = ImageUtils.getRoundBitmapFromUri(mContext, pictureUri); + String name = LinphoneUtils.getAddressDisplayName(address); + + boolean showAnswerAction = + call.getState() == Call.State.IncomingReceived + || call.getState() == Call.State.IncomingEarlyMedia; + Notification notification = + Compatibility.createInCallNotification( + mContext, + notif.getNotificationId(), + showAnswerAction, + mContext.getString(notificationTextId), + iconId, + bm, + name, + pendingIntent); + + if (!isServiceNotificationDisplayed()) { + if (call.getCore().getCallsNb() == 0) { + stopForeground(); + } else { + if (mCurrentForegroundServiceNotification == 0) { + startForeground(notification, notif.getNotificationId()); + } else { + sendNotification(notif.getNotificationId(), notification); + } + } + } + } + + public String getSipUriForCallNotificationId(int notificationId) { + for (String addr : mCallNotifMap.keySet()) { + if (mCallNotifMap.get(addr).getNotificationId() == notificationId) { + return addr; + } + } + return null; + } + + public void displayInappNotification(String message) { + Intent notifIntent = new Intent(mContext, LinphoneActivity.class); + notifIntent.putExtra("GoToInapp", true); + PendingIntent pendingIntent = + PendingIntent.getActivity( + mContext, IN_APP_NOTIF_ID, notifIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + Notification notif = + Compatibility.createSimpleNotification( + mContext, + mContext.getString(R.string.inapp_notification_title), + message, + pendingIntent); + sendNotification(IN_APP_NOTIF_ID, notif); + } + + public void dismissNotification(int notifId) { + mNM.cancel(notifId); + } +} diff --git a/src/android/org/linphone/purchase/InAppPurchaseActivity.java b/app/src/main/java/org/linphone/purchase/InAppPurchaseActivity.java similarity index 52% rename from src/android/org/linphone/purchase/InAppPurchaseActivity.java rename to app/src/main/java/org/linphone/purchase/InAppPurchaseActivity.java index a9960816b..b78d5f793 100644 --- a/src/android/org/linphone/purchase/InAppPurchaseActivity.java +++ b/app/src/main/java/org/linphone/purchase/InAppPurchaseActivity.java @@ -29,43 +29,47 @@ import android.view.View.OnClickListener; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.Toast; - -import org.linphone.LinphonePreferences; +import java.util.ArrayList; +import java.util.List; import org.linphone.R; -import org.linphone.mediastream.Log; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; import org.linphone.xmlrpc.XmlRpcHelper; import org.linphone.xmlrpc.XmlRpcListenerBase; -import java.util.ArrayList; -import java.util.List; +public class InAppPurchaseActivity extends Activity + implements InAppPurchaseListener, OnClickListener { + private static InAppPurchaseActivity sInstance; -public class InAppPurchaseActivity extends Activity implements InAppPurchaseListener, OnClickListener { - private static InAppPurchaseActivity instance; - private InAppPurchaseHelper inAppPurchaseHelper; - private ImageView cancel, back; - private ProgressBar inProgress; + private InAppPurchaseHelper mInAppPurchaseHelper; + private ImageView mCancel, mBack; + private ProgressBar mInProgress; - private List purchasedItems; - private Fragment fragment; - private Handler mHandler = new Handler(); + private List mPurchasedItems; + private Fragment mFragment; + private final Handler mHandler = new Handler(); + + public static InAppPurchaseActivity instance() { + return sInstance; + } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - inAppPurchaseHelper = new InAppPurchaseHelper(this, this); + mInAppPurchaseHelper = new InAppPurchaseHelper(this, this); setContentView(R.layout.in_app); - inProgress = findViewById(R.id.purchaseItemsFetchInProgress); - inProgress.setVisibility(View.VISIBLE); + mInProgress = findViewById(R.id.purchaseItemsFetchInProgress); + mInProgress.setVisibility(View.VISIBLE); - back = findViewById(R.id.back); - back.setOnClickListener(this); - back.setVisibility(View.INVISIBLE); - cancel = findViewById(R.id.cancel); - cancel.setOnClickListener(this); + mBack = findViewById(R.id.back); + mBack.setOnClickListener(this); + mBack.setVisibility(View.INVISIBLE); + mCancel = findViewById(R.id.cancel); + mCancel.setOnClickListener(this); - instance = this; + sInstance = this; } private void changeFragment(Fragment newFragment) { @@ -74,47 +78,45 @@ public class InAppPurchaseActivity extends Activity implements InAppPurchaseList transaction.commitAllowingStateLoss(); } - public void displayInappList() { - fragment = new InAppPurchaseListFragment(); - changeFragment(fragment); + private void displayInappList() { + mFragment = new InAppPurchaseListFragment(); + changeFragment(mFragment); } public void displayPurchase(Purchasable item) { Bundle extra = new Bundle(); extra.putString("item_id", item.getId()); - fragment = new InAppPurchaseFragment(); - fragment.setArguments(extra); - changeFragment(fragment); + mFragment = new InAppPurchaseFragment(); + mFragment.setArguments(extra); + changeFragment(mFragment); } public void buyInapp(String username, Purchasable item) { LinphonePreferences.instance().setInAppPurchasedItem(item); - inAppPurchaseHelper.purchaseItemAsync(item.getId(), username); + mInAppPurchaseHelper.purchaseItemAsync(item.getId(), username); } - public String getGmailAccount() { - return inAppPurchaseHelper.getGmailAccount(); + return mInAppPurchaseHelper.getGmailAccount(); } - @Override protected void onDestroy() { - instance = null; - inAppPurchaseHelper.destroy(); + sInstance = null; + mInAppPurchaseHelper.destroy(); super.onDestroy(); } public List getPurchasedItems() { - if (purchasedItems == null || purchasedItems.size() == 0) { + if (mPurchasedItems == null || mPurchasedItems.size() == 0) { Log.w("nul"); } - return purchasedItems; + return mPurchasedItems; } public Purchasable getPurchasedItem(String id) { - for (Purchasable item : purchasedItems) { + for (Purchasable item : mPurchasedItems) { if (item.getId().equals(id)) { return item; } @@ -122,40 +124,36 @@ public class InAppPurchaseActivity extends Activity implements InAppPurchaseList return null; } - public static InAppPurchaseActivity instance() { - return instance; - } - @Override public void onServiceAvailableForQueries() { - //email.setText(inAppPurchaseHelper.getGmailAccount()); - //email.setEnabled(false); + // email.setText(mInAppPurchaseHelper.getGmailAccount()); + // email.setEnabled(false); - //inAppPurchaseHelper.getPurchasedItemsAsync(); - inAppPurchaseHelper.getAvailableItemsForPurchaseAsync(); + // mInAppPurchaseHelper.getPurchasedItemsAsync(); + mInAppPurchaseHelper.getAvailableItemsForPurchaseAsync(); } @Override public void onAvailableItemsForPurchaseQueryFinished(ArrayList items) { - //purchasableItemsLayout.removeAllViews(); - inProgress.setVisibility(View.GONE); - purchasedItems = new ArrayList<>(); - for (Purchasable item : items) { - purchasedItems.add(item); - } + // purchasableItemsLayout.removeAllViews(); + mInProgress.setVisibility(View.GONE); + mPurchasedItems = new ArrayList<>(); + mPurchasedItems.addAll(items); displayInappList(); } @Override public void onPurchasedItemsQueryFinished(ArrayList items) { - purchasedItems = items; + mPurchasedItems = items; if (items == null || items.size() == 0) { - inAppPurchaseHelper.getAvailableItemsForPurchaseAsync(); + mInAppPurchaseHelper.getAvailableItemsForPurchaseAsync(); } else { - for (Purchasable purchasedItem : purchasedItems) { - Log.d("[In-app purchase] Found already bought item, expires " + purchasedItem.getExpireDate()); - //displayRecoverAccountButton(purchasedItem); + for (Purchasable purchasedItem : mPurchasedItems) { + Log.d( + "[In-app purchase] Found already bought item, expires " + + purchasedItem.getExpireDate()); + // displayRecoverAccountButton(purchasedItem); } } } @@ -167,12 +165,18 @@ public class InAppPurchaseActivity extends Activity implements InAppPurchaseList Purchasable item = LinphonePreferences.instance().getInAppPurchasedItem(); - xmlRpcHelper.updateAccountExpireAsync(new XmlRpcListenerBase() { - @Override - public void onAccountExpireUpdated(String result) { - //TODO - } - }, LinphonePreferences.instance().getAccountUsername(0), LinphonePreferences.instance().getAccountHa1(0), getString(R.string.default_domain), item.getPayload(), item.getPayloadSignature()); + xmlRpcHelper.updateAccountExpireAsync( + new XmlRpcListenerBase() { + @Override + public void onAccountExpireUpdated() { + // TODO + } + }, + LinphonePreferences.instance().getAccountUsername(0), + LinphonePreferences.instance().getAccountHa1(0), + getString(R.string.default_domain), + item.getPayload(), + item.getPayloadSignature()); } } @@ -189,23 +193,23 @@ public class InAppPurchaseActivity extends Activity implements InAppPurchaseList @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - inAppPurchaseHelper.parseAndVerifyPurchaseItemResultAsync(requestCode, resultCode, data); + mInAppPurchaseHelper.parseAndVerifyPurchaseItemResultAsync(requestCode, resultCode, data); } @Override - public void onRecoverAccountSuccessful(boolean success) { - } + public void onRecoverAccountSuccessful() {} @Override public void onError(final String error) { Log.e(error); - mHandler.post(new Runnable() { - @Override - public void run() { - inProgress.setVisibility(View.GONE); - Toast.makeText(InAppPurchaseActivity.this, error, Toast.LENGTH_LONG).show(); - } - }); + mHandler.post( + new Runnable() { + @Override + public void run() { + mInProgress.setVisibility(View.GONE); + Toast.makeText(InAppPurchaseActivity.this, error, Toast.LENGTH_LONG).show(); + } + }); } @Override diff --git a/app/src/main/java/org/linphone/purchase/InAppPurchaseFragment.java b/app/src/main/java/org/linphone/purchase/InAppPurchaseFragment.java new file mode 100644 index 000000000..5792e11ea --- /dev/null +++ b/app/src/main/java/org/linphone/purchase/InAppPurchaseFragment.java @@ -0,0 +1,142 @@ +package org.linphone.purchase; +/* +InAppPurchaseFragment.java +Copyright (C) 2017 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.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; +import java.util.Locale; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.ProxyConfig; +import org.linphone.settings.LinphonePreferences; + +public class InAppPurchaseFragment extends Fragment implements View.OnClickListener { + private LinearLayout mUsernameLayout; + private EditText mUsername, mEmail; + private TextView mErrorMessage; + + private boolean mUsernameOk = false, mEmailOk = false; + private String mDefaultUsername, mDefaultEmail; + private Button mBuyItemButton; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + + View view = inflater.inflate(R.layout.in_app_store, container, false); + + String id = getArguments().getString("item_id"); + Purchasable item = InAppPurchaseActivity.instance().getPurchasedItem(id); + mBuyItemButton = view.findViewById(R.id.inapp_button); + + displayBuySubscriptionButton(item); + + mDefaultEmail = InAppPurchaseActivity.instance().getGmailAccount(); + mDefaultUsername = + LinphonePreferences.instance() + .getAccountUsername( + LinphonePreferences.instance().getDefaultAccountIndex()); + + mUsernameLayout = view.findViewById(R.id.username_layout); + mUsername = view.findViewById(R.id.username); + if (!getResources().getBoolean(R.bool.hide_username_in_inapp)) { + mUsernameLayout.setVisibility(View.VISIBLE); + mUsername.setText( + LinphonePreferences.instance() + .getAccountUsername( + LinphonePreferences.instance().getDefaultAccountIndex())); + + addUsernameHandler(mUsername, mErrorMessage); + } else { + if (mDefaultUsername != null) { + mUsernameLayout.setVisibility(View.GONE); + mUsernameOk = true; + } + } + + mEmail = view.findViewById(R.id.email); + if (mDefaultEmail != null) { + mEmail.setText(mDefaultEmail); + mEmailOk = true; + } + + mBuyItemButton.setEnabled(mEmailOk && mUsernameOk); + mErrorMessage = view.findViewById(R.id.username_error); + + return view; + } + + private void addUsernameHandler(final EditText field, final TextView errorMessage) { + field.addTextChangedListener( + new TextWatcher() { + public void afterTextChanged(Editable s) {} + + public void beforeTextChanged( + CharSequence s, int start, int count, int after) {} + + public void onTextChanged(CharSequence s, int start, int count, int after) { + mUsernameOk = false; + String username = s.toString(); + if (isUsernameCorrect(username)) { + mUsernameOk = true; + errorMessage.setText(""); + } else { + errorMessage.setText(R.string.wizard_username_incorrect); + } + if (mBuyItemButton != null) mBuyItemButton.setEnabled(mUsernameOk); + } + }); + } + + private boolean isUsernameCorrect(String username) { + ProxyConfig lpc = LinphoneManager.getLc().createProxyConfig(); + return lpc.isPhoneNumber(username); + } + + private void displayBuySubscriptionButton(Purchasable item) { + mBuyItemButton.setText("Buy account (" + item.getPrice() + ")"); + mBuyItemButton.setTag(item); + mBuyItemButton.setOnClickListener(this); + mBuyItemButton.setEnabled(mUsernameOk && mEmailOk); + } + + @Override + public void onClick(View v) { + Purchasable item = (Purchasable) v.getTag(); + InAppPurchaseActivity.instance().buyInapp(getUsername(), item); + } + + private String getUsername() { + String username = this.mUsername.getText().toString(); + ProxyConfig lpc = LinphoneManager.getLc().createProxyConfig(); + username = lpc.normalizePhoneNumber(username); + return username.toLowerCase(Locale.getDefault()); + } +} diff --git a/src/android/org/linphone/purchase/InAppPurchaseHelper.java b/app/src/main/java/org/linphone/purchase/InAppPurchaseHelper.java similarity index 62% rename from src/android/org/linphone/purchase/InAppPurchaseHelper.java rename to app/src/main/java/org/linphone/purchase/InAppPurchaseHelper.java index a0f224c38..517c17ff4 100644 --- a/src/android/org/linphone/purchase/InAppPurchaseHelper.java +++ b/app/src/main/java/org/linphone/purchase/InAppPurchaseHelper.java @@ -32,20 +32,17 @@ import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.util.Patterns; - import com.android.vending.billing.IInAppBillingService; - +import java.util.ArrayList; +import java.util.regex.Pattern; import org.json.JSONException; import org.json.JSONObject; -import org.linphone.LinphonePreferences; -import org.linphone.mediastream.Log; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; import org.linphone.xmlrpc.XmlRpcHelper; import org.linphone.xmlrpc.XmlRpcListenerBase; -import java.util.ArrayList; -import java.util.regex.Pattern; - -public class InAppPurchaseHelper { +class InAppPurchaseHelper { public static final int API_VERSION = 3; public static final int ACTIVITY_RESULT_CODE_PURCHASE_ITEM = 11089; @@ -86,16 +83,72 @@ public class InAppPurchaseHelper { public static final String PURCHASE_DETAILS_PAYLOAD = "developerPayload"; public static final String PURCHASE_DETAILS_PURCHASE_TOKEN = "purchaseToken"; - public static final String CLIENT_ERROR_SUBSCRIPTION_PURCHASE_NOT_AVAILABLE = "SUBSCRIPTION_PURCHASE_NOT_AVAILABLE"; - public static final String CLIENT_ERROR_BIND_TO_BILLING_SERVICE_FAILED = "BIND_TO_BILLING_SERVICE_FAILED"; - public static final String CLIENT_ERROR_BILLING_SERVICE_UNAVAILABLE = "BILLING_SERVICE_UNAVAILABLE"; + public static final String CLIENT_ERROR_SUBSCRIPTION_PURCHASE_NOT_AVAILABLE = + "SUBSCRIPTION_PURCHASE_NOT_AVAILABLE"; + public static final String CLIENT_ERROR_BIND_TO_BILLING_SERVICE_FAILED = + "BIND_TO_BILLING_SERVICE_FAILED"; + public static final String CLIENT_ERROR_BILLING_SERVICE_UNAVAILABLE = + "BILLING_SERVICE_UNAVAILABLE"; - private Context mContext; - private InAppPurchaseListener mListener; + private final Context mContext; + private final InAppPurchaseListener mListener; private IInAppBillingService mService; - private ServiceConnection mServiceConn; - private Handler mHandler = new Handler(); - private String mGmailAccount; + private final ServiceConnection mServiceConn; + private final Handler mHandler = new Handler(); + private final String mGmailAccount; + + public InAppPurchaseHelper(Activity context, InAppPurchaseListener listener) { + mContext = context; + mListener = listener; + mGmailAccount = getGmailAccount(); + + Log.d( + "[In-app purchase] creating InAppPurchaseHelper for context " + + context.getLocalClassName()); + + mServiceConn = + new ServiceConnection() { + @Override + public void onServiceDisconnected(ComponentName name) { + Log.d("[In-app purchase] onServiceDisconnected!"); + mService = null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d("[In-app purchase] onServiceConnected!"); + mService = IInAppBillingService.Stub.asInterface(service); + String packageName = mContext.getPackageName(); + try { + int response = + mService.isBillingSupported( + API_VERSION, packageName, ITEM_TYPE_SUBS); + if (response != RESPONSE_RESULT_OK || mGmailAccount == null) { + Log.e("[In-app purchase] Error: Subscriptions aren't supported!"); + mListener.onError(CLIENT_ERROR_SUBSCRIPTION_PURCHASE_NOT_AVAILABLE); + } else { + mListener.onServiceAvailableForQueries(); + } + } catch (RemoteException e) { + Log.e(e); + } + } + }; + + Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); + serviceIntent.setPackage("com.android.vending"); + if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) { + boolean ok = + mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); + if (!ok) { + Log.e("[In-app purchase] Error: Bind service failed"); + mListener.onError(CLIENT_ERROR_BIND_TO_BILLING_SERVICE_FAILED); + } + } else { + Log.e("[In-app purchase] Error: Billing service unavailable on device."); + mListener.onError(CLIENT_ERROR_BILLING_SERVICE_UNAVAILABLE); + } + } private String responseCodeToErrorMessage(int responseCode) { switch (responseCode) { @@ -119,54 +172,6 @@ public class InAppPurchaseHelper { return "UNKNOWN_RESPONSE_CODE"; } - public InAppPurchaseHelper(Activity context, InAppPurchaseListener listener) { - mContext = context; - mListener = listener; - mGmailAccount = getGmailAccount(); - - - Log.d("[In-app purchase] creating InAppPurchaseHelper for context " + context.getLocalClassName()); - - mServiceConn = new ServiceConnection() { - @Override - public void onServiceDisconnected(ComponentName name) { - Log.d("[In-app purchase] onServiceDisconnected!"); - mService = null; - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - Log.d("[In-app purchase] onServiceConnected!"); - mService = IInAppBillingService.Stub.asInterface(service); - String packageName = mContext.getPackageName(); - try { - int response = mService.isBillingSupported(API_VERSION, packageName, ITEM_TYPE_SUBS); - if (response != RESPONSE_RESULT_OK || mGmailAccount == null) { - Log.e("[In-app purchase] Error: Subscriptions aren't supported!"); - mListener.onError(CLIENT_ERROR_SUBSCRIPTION_PURCHASE_NOT_AVAILABLE); - } else { - mListener.onServiceAvailableForQueries(); - } - } catch (RemoteException e) { - Log.e(e); - } - } - }; - - Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); - serviceIntent.setPackage("com.android.vending"); - if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) { - boolean ok = mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); - if (!ok) { - Log.e("[In-app purchase] Error: Bind service failed"); - mListener.onError(CLIENT_ERROR_BIND_TO_BILLING_SERVICE_FAILED); - } - } else { - Log.e("[In-app purchase] Error: Billing service unavailable on device."); - mListener.onError(CLIENT_ERROR_BILLING_SERVICE_UNAVAILABLE); - } - } - private ArrayList getAvailableItemsForPurchase() { ArrayList products = new ArrayList<>(); ArrayList skuList = LinphonePreferences.instance().getInAppPurchasables(); @@ -175,7 +180,9 @@ public class InAppPurchaseHelper { Bundle skuDetails = null; try { - skuDetails = mService.getSkuDetails(API_VERSION, mContext.getPackageName(), ITEM_TYPE_SUBS, querySkus); + skuDetails = + mService.getSkuDetails( + API_VERSION, mContext.getPackageName(), ITEM_TYPE_SUBS, querySkus); } catch (RemoteException e) { Log.e(e); } @@ -192,7 +199,11 @@ public class InAppPurchaseHelper { String title = object.getString(SKU_DETAILS_TITLE); String desc = object.getString(SKU_DETAILS_DESC); - Purchasable purchasable = new Purchasable(id).setTitle(title).setDescription(desc).setPrice(price); + Purchasable purchasable = + new Purchasable(id) + .setTitle(title) + .setDescription(desc) + .setPrice(price); Log.w("Purchasable item " + purchasable.getDescription()); products.add(purchasable); } catch (JSONException e) { @@ -200,7 +211,9 @@ public class InAppPurchaseHelper { } } } else { - Log.e("[In-app purchase] Error: responde code is not ok: " + responseCodeToErrorMessage(response)); + Log.e( + "[In-app purchase] Error: responde code is not ok: " + + responseCodeToErrorMessage(response)); mListener.onError(responseCodeToErrorMessage(response)); } } @@ -209,21 +222,27 @@ public class InAppPurchaseHelper { } public void getAvailableItemsForPurchaseAsync() { - new Thread(new Runnable() { - public void run() { - final ArrayList items = getAvailableItemsForPurchase(); - if (mHandler != null && mListener != null) { - mHandler.post(new Runnable() { - public void run() { - mListener.onAvailableItemsForPurchaseQueryFinished(items); - } - }); - } - } - }).start(); + new Thread( + new Runnable() { + public void run() { + final ArrayList items = getAvailableItemsForPurchase(); + if (mHandler != null && mListener != null) { + mHandler.post( + new Runnable() { + public void run() { + mListener + .onAvailableItemsForPurchaseQueryFinished( + items); + } + }); + } + } + }) + .start(); } - public void parseAndVerifyPurchaseItemResultAsync(int requestCode, int resultCode, Intent data) { + public void parseAndVerifyPurchaseItemResultAsync( + int requestCode, int resultCode, Intent data) { if (requestCode == ACTIVITY_RESULT_CODE_PURCHASE_ITEM) { int responseCode = data.getIntExtra(RESPONSE_CODE, 0); @@ -236,12 +255,15 @@ public class InAppPurchaseHelper { LinphonePreferences.instance().setInAppPurchasedItem(item); XmlRpcHelper xmlRpcHelper = new XmlRpcHelper(); - xmlRpcHelper.verifySignatureAsync(new XmlRpcListenerBase() { - @Override - public void onSignatureVerified(boolean success) { - mListener.onPurchasedItemConfirmationQueryFinished(success); - } - }, payload, signature); + xmlRpcHelper.verifySignatureAsync( + new XmlRpcListenerBase() { + @Override + public void onSignatureVerified(boolean success) { + mListener.onPurchasedItemConfirmationQueryFinished(success); + } + }, + payload, + signature); } } } @@ -249,7 +271,13 @@ public class InAppPurchaseHelper { private void purchaseItem(String productId, String sipIdentity) { Bundle buyIntentBundle = null; try { - buyIntentBundle = mService.getBuyIntent(API_VERSION, mContext.getPackageName(), productId, ITEM_TYPE_SUBS, sipIdentity); + buyIntentBundle = + mService.getBuyIntent( + API_VERSION, + mContext.getPackageName(), + productId, + ITEM_TYPE_SUBS, + sipIdentity); } catch (RemoteException e) { Log.e(e); } @@ -258,7 +286,14 @@ public class InAppPurchaseHelper { PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); if (pendingIntent != null) { try { - ((Activity) mContext).startIntentSenderForResult(pendingIntent.getIntentSender(), ACTIVITY_RESULT_CODE_PURCHASE_ITEM, new Intent(), 0, 0, 0); + ((Activity) mContext) + .startIntentSenderForResult( + pendingIntent.getIntentSender(), + ACTIVITY_RESULT_CODE_PURCHASE_ITEM, + new Intent(), + 0, + 0, + 0); } catch (SendIntentException e) { Log.e(e); } @@ -267,11 +302,13 @@ public class InAppPurchaseHelper { } public void purchaseItemAsync(final String productId, final String sipIdentity) { - new Thread(new Runnable() { - public void run() { - purchaseItem(productId, sipIdentity); - } - }).start(); + new Thread( + new Runnable() { + public void run() { + purchaseItem(productId, sipIdentity); + } + }) + .start(); } public void destroy() { @@ -283,8 +320,7 @@ public class InAppPurchaseHelper { for (Account account : accounts) { if (isEmailCorrect(account.name)) { - String possibleEmail = account.name; - return possibleEmail; + return account.name; } } @@ -296,20 +332,20 @@ public class InAppPurchaseHelper { return emailPattern.matcher(email).matches(); } - private Purchasable verifySignature(String payload, String signature) { + private Purchasable verifySignature() { // TODO FIXME rework to be async - /*XmlRpcHelper helper = new XmlRpcHelper(); - if (helper.verifySignature(payload, signature)) { - try { - JSONObject json = new JSONObject(payload); - String productId = json.getString(PURCHASE_DETAILS_PRODUCT_ID); - Purchasable item = new Purchasable(productId); - item.setPayloadAndSignature(payload, signature); - return item; - } catch (JSONException e) { - Log.e(e); - } - }*/ + /*XmlRpcHelper helper = new XmlRpcHelper(); + if (helper.verifySignature(payload, signature)) { + try { + JSONObject json = new JSONObject(payload); + String productId = json.getString(PURCHASE_DETAILS_PRODUCT_ID); + Purchasable item = new Purchasable(productId); + item.setPayloadAndSignature(payload, signature); + return item; + } catch (JSONException e) { + Log.e(e); + } + }*/ return null; } diff --git a/src/android/org/linphone/purchase/InAppPurchaseListFragment.java b/app/src/main/java/org/linphone/purchase/InAppPurchaseListFragment.java similarity index 88% rename from src/android/org/linphone/purchase/InAppPurchaseListFragment.java rename to app/src/main/java/org/linphone/purchase/InAppPurchaseListFragment.java index 683111136..1bb2b0ab2 100644 --- a/src/android/org/linphone/purchase/InAppPurchaseListFragment.java +++ b/app/src/main/java/org/linphone/purchase/InAppPurchaseListFragment.java @@ -28,35 +28,38 @@ import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.ListView; import android.widget.TextView; - +import java.util.List; import org.linphone.R; -import java.util.List; - public class InAppPurchaseListFragment extends Fragment implements AdapterView.OnItemClickListener { - private ListView inappList; + private ListView mInappList; private LayoutInflater mInflater; private List mPurchasableItems; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mInflater = inflater; View view = inflater.inflate(R.layout.in_app_list, container, false); mPurchasableItems = InAppPurchaseActivity.instance().getPurchasedItems(); - inappList = view.findViewById(R.id.inapp_list); + mInappList = view.findViewById(R.id.inapp_list); if (mPurchasableItems != null) { - inappList.setAdapter(new InAppListAdapter()); - inappList.setOnItemClickListener(this); + mInappList.setAdapter(new InAppListAdapter()); + mInappList.setOnItemClickListener(this); } return view; } + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Purchasable item = (Purchasable) view.getTag(); + InAppPurchaseActivity.instance().displayPurchase(item); + } + class InAppListAdapter extends BaseAdapter { - InAppListAdapter() { - } + InAppListAdapter() {} public int getCount() { return mPurchasableItems.size(); @@ -92,11 +95,4 @@ public class InAppPurchaseListFragment extends Fragment implements AdapterView.O return view; } } - - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Purchasable item = (Purchasable) view.getTag(); - InAppPurchaseActivity.instance().displayPurchase(item); - } } diff --git a/src/android/org/linphone/purchase/InAppPurchaseListener.java b/app/src/main/java/org/linphone/purchase/InAppPurchaseListener.java similarity index 84% rename from src/android/org/linphone/purchase/InAppPurchaseListener.java rename to app/src/main/java/org/linphone/purchase/InAppPurchaseListener.java index deb3e9b9d..a67acf80d 100644 --- a/src/android/org/linphone/purchase/InAppPurchaseListener.java +++ b/app/src/main/java/org/linphone/purchase/InAppPurchaseListener.java @@ -20,10 +20,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import java.util.ArrayList; -public interface InAppPurchaseListener { - /** - * Callback called when the in-app purchase listener is connected and available for queries - */ +interface InAppPurchaseListener { + /** Callback called when the in-app purchase listener is connected and available for queries */ void onServiceAvailableForQueries(); /** @@ -47,12 +45,8 @@ public interface InAppPurchaseListener { */ void onPurchasedItemConfirmationQueryFinished(boolean success); - /** - * Callback called when the account has been recovered (or not) - * - * @param success true if the recover has been successful, false otherwise - */ - void onRecoverAccountSuccessful(boolean success); + /** Callback called when the account has been recovered (or not) */ + void onRecoverAccountSuccessful(); /** * Callback called when the account has been activated (or not) diff --git a/src/android/org/linphone/purchase/InAppPurchaseListenerBase.java b/app/src/main/java/org/linphone/purchase/InAppPurchaseListenerBase.java similarity index 96% rename from src/android/org/linphone/purchase/InAppPurchaseListenerBase.java rename to app/src/main/java/org/linphone/purchase/InAppPurchaseListenerBase.java index a961c51ba..774f1a312 100644 --- a/src/android/org/linphone/purchase/InAppPurchaseListenerBase.java +++ b/app/src/main/java/org/linphone/purchase/InAppPurchaseListenerBase.java @@ -47,7 +47,7 @@ public class InAppPurchaseListenerBase implements InAppPurchaseListener { } @Override - public void onRecoverAccountSuccessful(boolean success) { + public void onRecoverAccountSuccessful() { // TODO Auto-generated method stub } diff --git a/src/android/org/linphone/purchase/Purchasable.java b/app/src/main/java/org/linphone/purchase/Purchasable.java similarity index 69% rename from src/android/org/linphone/purchase/Purchasable.java rename to app/src/main/java/org/linphone/purchase/Purchasable.java index 87256f113..a9af68f37 100644 --- a/src/android/org/linphone/purchase/Purchasable.java +++ b/app/src/main/java/org/linphone/purchase/Purchasable.java @@ -25,81 +25,74 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ public class Purchasable { - private String id, title, description, price; - private long expire; - private String purchasePayload, purchasePayloadSignature; - private String userData; + private final String mId; + private String mTitle; + private String mDescription; + private String mPrice; + private String mPurchasePayload, mPurchasePayloadSignature; + private String mUserData; public Purchasable(String id) { - this.id = id; + this.mId = id; } public String getId() { - return id; + return mId; } public String getTitle() { - return title; + return mTitle; } public Purchasable setTitle(String title) { - this.title = title; + this.mTitle = title; return this; } public String getDescription() { - return description; + return mDescription; } public Purchasable setDescription(String description) { - this.description = description; + this.mDescription = description; return this; } public String getPrice() { - return price; + return mPrice; } public Purchasable setPrice(String price) { - this.price = price; + this.mPrice = price; return this; } - public long getExpire() { - return expire; - } - public String getExpireDate() { DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()); - Date date = new Date(expire); + Date date = new Date(0); return dateFormat.format(date); } - public Purchasable setExpire(long expire) { - this.expire = expire; - return this; - } - public Purchasable setPayloadAndSignature(String payload, String signature) { - this.purchasePayload = payload; - this.purchasePayloadSignature = signature; + this.mPurchasePayload = payload; + this.mPurchasePayloadSignature = signature; return this; } public String getPayload() { - return this.purchasePayload; + return this.mPurchasePayload; } public String getPayloadSignature() { - return this.purchasePayloadSignature; - } - - public Purchasable setUserData(String data) { - this.userData = data; - return this; + return this.mPurchasePayloadSignature; } public String getUserData() { - return this.userData; + return this.mUserData; + } + + public Purchasable setUserData(String data) { + this.mUserData = data; + return this; } } diff --git a/src/android/org/linphone/receivers/AccountEnableReceiver.java b/app/src/main/java/org/linphone/receivers/AccountEnableReceiver.java similarity index 78% rename from src/android/org/linphone/receivers/AccountEnableReceiver.java rename to app/src/main/java/org/linphone/receivers/AccountEnableReceiver.java index 4af64978d..7f856ee03 100644 --- a/src/android/org/linphone/receivers/AccountEnableReceiver.java +++ b/app/src/main/java/org/linphone/receivers/AccountEnableReceiver.java @@ -23,8 +23,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; - -import org.linphone.LinphonePreferences; +import org.linphone.settings.LinphonePreferences; public class AccountEnableReceiver extends BroadcastReceiver { private static final String TAG = "AccountEnableReceiver"; @@ -35,10 +34,14 @@ public class AccountEnableReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { int prefsAccountIndex = (int) (long) intent.getLongExtra(FIELD_ID, -1); boolean enable = intent.getBooleanExtra(FIELD_ACTIVE, true); - Log.i(TAG, "Received broadcast for index=" + Integer.toString(prefsAccountIndex) + ",enable=" + Boolean.toString(enable)); - if (prefsAccountIndex < 0 || prefsAccountIndex >= LinphonePreferences.instance().getAccountCount()) - return; + Log.i( + TAG, + "Received broadcast for index=" + + Integer.toString(prefsAccountIndex) + + ",enable=" + + Boolean.toString(enable)); + if (prefsAccountIndex < 0 + || prefsAccountIndex >= LinphonePreferences.instance().getAccountCount()) return; LinphonePreferences.instance().setAccountEnabled(prefsAccountIndex, enable); } } - diff --git a/src/android/org/linphone/receivers/BluetoothManager.java b/app/src/main/java/org/linphone/receivers/BluetoothManager.java similarity index 62% rename from src/android/org/linphone/receivers/BluetoothManager.java rename to app/src/main/java/org/linphone/receivers/BluetoothManager.java index 2b2f0cb76..f5c7e0411 100644 --- a/src/android/org/linphone/receivers/BluetoothManager.java +++ b/app/src/main/java/org/linphone/receivers/BluetoothManager.java @@ -28,15 +28,14 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; - +import java.util.List; import org.linphone.LinphoneManager; import org.linphone.LinphoneService; import org.linphone.call.CallActivity; - -import java.util.List; +import org.linphone.core.tools.Log; public class BluetoothManager extends BroadcastReceiver { - private static BluetoothManager instance; + private static BluetoothManager sInstance; private Context mContext; private AudioManager mAudioManager; @@ -44,32 +43,39 @@ public class BluetoothManager extends BroadcastReceiver { 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; - } + private boolean mIsBluetoothConnected; + private boolean mIsScoConnected; public BluetoothManager() { - isBluetoothConnected = false; + mIsBluetoothConnected = false; if (!ensureInit()) { - android.util.Log.w("BluetoothManager", "[Bluetooth] Manager tried to init but LinphoneService not ready yet..."); + android.util.Log.w( + "BluetoothManager", + "[Bluetooth] Manager tried to init but LinphoneService not ready yet..."); } - instance = this; + sInstance = this; + } + + public static BluetoothManager getInstance() { + if (sInstance == null) { + sInstance = new BluetoothManager(); + } + return sInstance; } public void initBluetooth() { if (!ensureInit()) { - android.util.Log.w("BluetoothManager", "[Bluetooth] Manager tried to init bluetooth but LinphoneService not ready yet..."); + android.util.Log.w( + "BluetoothManager", + "[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.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); @@ -80,7 +86,7 @@ public class BluetoothManager extends BroadcastReceiver { } private void startBluetooth() { - if (isBluetoothConnected) { + if (mIsBluetoothConnected) { android.util.Log.e("BluetoothManager", "[Bluetooth] Already started, skipping..."); return; } @@ -89,29 +95,36 @@ public class BluetoothManager extends BroadcastReceiver { if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) { if (mProfileListener != null) { - android.util.Log.w("BluetoothManager", "[Bluetooth] Headset profile was already opened, let's close it"); + android.util.Log.w( + "BluetoothManager", + "[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) { - android.util.Log.d("BluetoothManager", "[Bluetooth] Headset connected"); - mBluetoothHeadset = (BluetoothHeadset) proxy; - isBluetoothConnected = true; - } - } + mProfileListener = + new BluetoothProfile.ServiceListener() { + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (profile == BluetoothProfile.HEADSET) { + android.util.Log.d( + "BluetoothManager", "[Bluetooth] Headset connected"); + mBluetoothHeadset = (BluetoothHeadset) proxy; + mIsBluetoothConnected = true; + } + } - public void onServiceDisconnected(int profile) { - if (profile == BluetoothProfile.HEADSET) { - mBluetoothHeadset = null; - isBluetoothConnected = false; - android.util.Log.d("BluetoothManager", "[Bluetooth] Headset disconnected"); - LinphoneManager.getInstance().routeAudioToReceiver(); - } - } - }; - boolean success = mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEADSET); + public void onServiceDisconnected(int profile) { + if (profile == BluetoothProfile.HEADSET) { + mBluetoothHeadset = null; + mIsBluetoothConnected = false; + android.util.Log.d( + "BluetoothManager", "[Bluetooth] Headset disconnected"); + LinphoneManager.getInstance().routeAudioToReceiver(); + } + } + }; + boolean success = + mBluetoothAdapter.getProfileProxy( + mContext, mProfileListener, BluetoothProfile.HEADSET); if (!success) { android.util.Log.e("BluetoothManager", "[Bluetooth] getProfileProxy failed !"); } @@ -146,7 +159,10 @@ public class BluetoothManager extends BroadcastReceiver { public boolean routeAudioToBluetooth() { ensureInit(); - if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() && mAudioManager != null && mAudioManager.isBluetoothScoAvailableOffCall()) { + if (mBluetoothAdapter != null + && mBluetoothAdapter.isEnabled() + && mAudioManager != null + && mAudioManager.isBluetoothScoAvailableOffCall()) { if (isBluetoothHeadsetAvailable()) { if (mAudioManager != null && !mAudioManager.isBluetoothScoOn()) { android.util.Log.d("BluetoothManager", "[Bluetooth] SCO off, let's start it"); @@ -166,6 +182,7 @@ public class BluetoothManager extends BroadcastReceiver { try { Thread.sleep(200); } catch (InterruptedException e) { + Log.e(e); } if (mAudioManager != null) { @@ -177,7 +194,9 @@ public class BluetoothManager extends BroadcastReceiver { } if (ok) { if (retries > 0) { - android.util.Log.d("BluetoothManager", "[Bluetooth] Audio route ok after " + retries + " retries"); + android.util.Log.d( + "BluetoothManager", + "[Bluetooth] Audio route ok after " + retries + " retries"); } else { android.util.Log.d("BluetoothManager", "[Bluetooth] Audio route ok"); } @@ -192,24 +211,34 @@ public class BluetoothManager extends BroadcastReceiver { } public boolean isUsingBluetoothAudioRoute() { - return mBluetoothHeadset != null && mBluetoothHeadset.isAudioConnected(mBluetoothDevice) && isScoConnected; + return mBluetoothHeadset != null + && mBluetoothHeadset.isAudioConnected(mBluetoothDevice) + && mIsScoConnected; } public boolean isBluetoothHeadsetAvailable() { ensureInit(); - if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() && mAudioManager != null && mAudioManager.isBluetoothScoAvailableOffCall()) { + 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) { + if (mBluetoothHeadset.getConnectionState(dev) + == BluetoothHeadset.STATE_CONNECTED) { mBluetoothDevice = dev; isHeadsetConnected = true; break; } } - android.util.Log.d("BluetoothManager", isHeadsetConnected ? "[Bluetooth] Headset found, bluetooth audio route available" : "[Bluetooth] No headset found, bluetooth audio route unavailable"); + android.util.Log.d( + "BluetoothManager", + isHeadsetConnected + ? "[Bluetooth] Headset found, bluetooth audio route available" + : "[Bluetooth] No headset found, bluetooth audio route unavailable"); } return isHeadsetConnected; } @@ -224,12 +253,13 @@ public class BluetoothManager extends BroadcastReceiver { // Hack to ensure bluetooth sco is really stopped int retries = 0; - while (isScoConnected && retries < 10) { + while (mIsScoConnected && retries < 10) { retries++; try { Thread.sleep(200); } catch (InterruptedException e) { + Log.e(e); } mAudioManager.stopBluetoothSco(); @@ -239,9 +269,9 @@ public class BluetoothManager extends BroadcastReceiver { } } - public void stopBluetooth() { + private void stopBluetooth() { android.util.Log.w("BluetoothManager", "[Bluetooth] Stopping..."); - isBluetoothConnected = false; + mIsBluetoothConnected = false; disableBluetoothSCO(); @@ -268,6 +298,7 @@ public class BluetoothManager extends BroadcastReceiver { mContext.unregisterReceiver(this); android.util.Log.d("BluetoothManager", "[Bluetooth] Receiver stopped"); } catch (Exception e) { + Log.e(e); } } catch (Exception e) { android.util.Log.e("BluetoothManager", e.getMessage()); @@ -275,30 +306,32 @@ public class BluetoothManager extends BroadcastReceiver { } public void onReceive(Context context, Intent intent) { - if (!LinphoneManager.isInstanciated()) - return; + 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) { android.util.Log.d("BluetoothManager", "[Bluetooth] SCO state: connected"); -// LinphoneManager.getInstance().audioStateChanged(AudioState.BLUETOOTH); - isScoConnected = true; + // LinphoneManager.getInstance().audioStateChanged(AudioState.BLUETOOTH); + mIsScoConnected = true; } else if (state == AudioManager.SCO_AUDIO_STATE_DISCONNECTED) { android.util.Log.d("BluetoothManager", "[Bluetooth] SCO state: disconnected"); -// LinphoneManager.getInstance().audioStateChanged(AudioState.SPEAKER); - isScoConnected = false; + // LinphoneManager.getInstance().audioStateChanged(AudioState.SPEAKER); + mIsScoConnected = false; } else if (state == AudioManager.SCO_AUDIO_STATE_CONNECTING) { android.util.Log.d("BluetoothManager", "[Bluetooth] SCO state: connecting"); -// LinphoneManager.getInstance().audioStateChanged(AudioState.BLUETOOTH); - isScoConnected = true; + // LinphoneManager.getInstance().audioStateChanged(AudioState.BLUETOOTH); + mIsScoConnected = true; } else { android.util.Log.d("BluetoothManager", "[Bluetooth] SCO state: " + state); } refreshCallView(); } else if (BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED.equals(action)) { - int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, BluetoothAdapter.STATE_DISCONNECTED); + int state = + intent.getIntExtra( + BluetoothAdapter.EXTRA_CONNECTION_STATE, + BluetoothAdapter.STATE_DISCONNECTED); if (state == 0) { android.util.Log.d("BluetoothManager", "[Bluetooth] State: disconnected"); stopBluetooth(); @@ -308,22 +341,41 @@ public class BluetoothManager extends BroadcastReceiver { } else { android.util.Log.d("BluetoothManager", "[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); + } 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); + Object[] args = + (Object[]) + intent.getExtras() + .get(BluetoothHeadset.EXTRA_VENDOR_SPECIFIC_HEADSET_EVENT_ARGS); if (args.length <= 0) { - android.util.Log.d("BluetoothManager", "[Bluetooth] Event: " + command + ", no args"); + android.util.Log.d( + "BluetoothManager", "[Bluetooth] Event: " + command + ", no args"); return; } String eventName = (args[0]).toString(); if (eventName.equals("BUTTON") && args.length >= 3) { String buttonID = args[1].toString(); String mode = args[2].toString(); - android.util.Log.d("BluetoothManager", "[Bluetooth] Event: " + command + " : " + eventName + ", id = " + buttonID + " (" + mode + ")"); + android.util.Log.d( + "BluetoothManager", + "[Bluetooth] Event: " + + command + + " : " + + eventName + + ", id = " + + buttonID + + " (" + + mode + + ")"); } else { - android.util.Log.d("BluetoothManager", "[Bluetooth] Event: " + command + " : " + eventName); + android.util.Log.d( + "BluetoothManager", "[Bluetooth] Event: " + command + " : " + eventName); } } } diff --git a/src/android/org/linphone/receivers/BootReceiver.java b/app/src/main/java/org/linphone/receivers/BootReceiver.java similarity index 84% rename from src/android/org/linphone/receivers/BootReceiver.java rename to app/src/main/java/org/linphone/receivers/BootReceiver.java index 66a34b5cb..e10cec238 100644 --- a/src/android/org/linphone/receivers/BootReceiver.java +++ b/app/src/main/java/org/linphone/receivers/BootReceiver.java @@ -22,11 +22,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; - import org.linphone.LinphoneManager; -import org.linphone.LinphonePreferences; import org.linphone.LinphoneService; import org.linphone.compatibility.Compatibility; +import org.linphone.settings.LinphonePreferences; public class BootReceiver extends BroadcastReceiver { @@ -34,11 +33,14 @@ public class BootReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { LinphonePreferences.instance().setContext(context); if (intent.getAction().equalsIgnoreCase(Intent.ACTION_SHUTDOWN)) { - android.util.Log.d("LinphoneBootReceiver", "Device is shutting down, destroying Core to unregister"); + android.util.Log.d( + "LinphoneBootReceiver", + "Device is shutting down, destroying Core to unregister"); LinphoneManager.destroy(); } else { boolean autostart = LinphonePreferences.instance().isAutoStartEnabled(); - android.util.Log.i("LinphoneBootReceiver", "Device is starting, auto_start is " + autostart); + android.util.Log.i( + "LinphoneBootReceiver", "Device is starting, auto_start is " + autostart); if (autostart && !LinphoneService.isReady()) { Intent lLinphoneServiceIntent = new Intent(Intent.ACTION_MAIN); lLinphoneServiceIntent.setClass(context, LinphoneService.class); diff --git a/src/android/org/linphone/receivers/HookReceiver.java b/app/src/main/java/org/linphone/receivers/HookReceiver.java similarity index 91% rename from src/android/org/linphone/receivers/HookReceiver.java rename to app/src/main/java/org/linphone/receivers/HookReceiver.java index ac1f232c1..f58936667 100644 --- a/src/android/org/linphone/receivers/HookReceiver.java +++ b/app/src/main/java/org/linphone/receivers/HookReceiver.java @@ -23,27 +23,24 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Bundle; - import org.linphone.LinphoneManager; -import org.linphone.mediastream.Log; +import org.linphone.core.tools.Log; public class HookReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (isOrderedBroadcast()) - abortBroadcast(); + if (isOrderedBroadcast()) abortBroadcast(); Bundle extras = intent.getExtras(); boolean b = extras.getBoolean("hookoff"); if (b) { - //handset on + // handset on Log.i(" ======>>>>>> HookReceiver - handset ON"); LinphoneManager.getInstance().enableSpeaker(false); if (!LinphoneManager.getInstance().isHansetModeOn()) LinphoneManager.getInstance().setHandsetMode(true); - } else { - //handset off + // handset off Log.i(" ======>>>>>> HookReceiver - handset OFF"); LinphoneManager.getInstance().enableSpeaker(true); LinphoneManager.getInstance().setHandsetMode(false); diff --git a/src/android/org/linphone/receivers/OutgoingCallReceiver.java b/app/src/main/java/org/linphone/receivers/OutgoingCallReceiver.java similarity index 87% rename from src/android/org/linphone/receivers/OutgoingCallReceiver.java rename to app/src/main/java/org/linphone/receivers/OutgoingCallReceiver.java index 9fc3e0152..025f8d8ec 100644 --- a/src/android/org/linphone/receivers/OutgoingCallReceiver.java +++ b/app/src/main/java/org/linphone/receivers/OutgoingCallReceiver.java @@ -19,25 +19,21 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; - -import org.linphone.LinphonePreferences; - -import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; - +import org.linphone.settings.LinphonePreferences; public class OutgoingCallReceiver extends BroadcastReceiver { - private final static String TAG = "CallHandler"; - private final String ACTION_CALL_LINPHONE = "org.linphone.intent.action.CallLaunched"; - - private LinphonePreferences mPrefs; + private static final String TAG = "CallHandler"; + private static final String ACTION_CALL_LINPHONE = "org.linphone.intent.action.CallLaunched"; @Override public void onReceive(Context context, Intent intent) { - mPrefs = LinphonePreferences.instance(); + LinphonePreferences mPrefs = LinphonePreferences.instance(); Log.e(TAG, "===>>>> Linphone OutgoingCallReceiver "); if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)) { Log.e(TAG, "===>>>> Linphone OutgoingCallReceiver : ACTION_NEW_OUTGOING_CALL"); @@ -53,4 +49,4 @@ public class OutgoingCallReceiver extends BroadcastReceiver { } } } -} \ No newline at end of file +} diff --git a/src/android/org/linphone/receivers/PhoneStateChangedReceiver.java b/app/src/main/java/org/linphone/receivers/PhoneStateChangedReceiver.java similarity index 89% rename from src/android/org/linphone/receivers/PhoneStateChangedReceiver.java rename to app/src/main/java/org/linphone/receivers/PhoneStateChangedReceiver.java index ceab2d49d..6a988004d 100644 --- a/src/android/org/linphone/receivers/PhoneStateChangedReceiver.java +++ b/app/src/main/java/org/linphone/receivers/PhoneStateChangedReceiver.java @@ -23,22 +23,19 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.telephony.TelephonyManager; - import org.linphone.LinphoneManager; import org.linphone.core.Core; -/** - * Pause current SIP calls when GSM phone rings or is active. - */ +/** Pause current SIP calls when GSM phone rings or is active. */ public class PhoneStateChangedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { final String extraState = intent.getStringExtra(TelephonyManager.EXTRA_STATE); - if (!LinphoneManager.isInstanciated()) - return; + if (!LinphoneManager.isInstanciated()) return; - if (TelephonyManager.EXTRA_STATE_OFFHOOK.equals(extraState) || TelephonyManager.EXTRA_STATE_RINGING.equals(extraState)) { + if (TelephonyManager.EXTRA_STATE_OFFHOOK.equals(extraState) + || TelephonyManager.EXTRA_STATE_RINGING.equals(extraState)) { LinphoneManager.getInstance().setCallGsmON(true); Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); if (lc != null) { diff --git a/app/src/main/java/org/linphone/recording/Recording.java b/app/src/main/java/org/linphone/recording/Recording.java new file mode 100644 index 000000000..7e178695d --- /dev/null +++ b/app/src/main/java/org/linphone/recording/Recording.java @@ -0,0 +1,164 @@ +package org.linphone.recording; + +/* +Recording.java +Copyright (C) 2018 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.annotation.SuppressLint; +import android.content.Context; +import android.os.Handler; +import androidx.annotation.NonNull; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.linphone.LinphoneManager; +import org.linphone.core.Player; +import org.linphone.core.PlayerListener; +import org.linphone.core.tools.Log; + +class Recording implements PlayerListener, Comparable { + public static final Pattern RECORD_PATTERN = + Pattern.compile(".*/(.*)_(\\d{2}-\\d{2}-\\d{4}-\\d{2}-\\d{2}-\\d{2})\\..*"); + + private final String mRecordPath; + private String mName; + private Date mRecordDate; + private final Player mPlayer; + private RecordingListener mListener; + private final Handler mHandler; + private Runnable mUpdateCurrentPositionTimer; + + @SuppressLint("SimpleDateFormat") + public Recording(Context context, String recordPath) { + this.mRecordPath = recordPath; + + Matcher m = RECORD_PATTERN.matcher(recordPath); + if (m.matches()) { + mName = m.group(1); + + try { + mRecordDate = new SimpleDateFormat("dd-MM-yyyy-HH-mm-ss").parse(m.group(2)); + } catch (ParseException e) { + Log.e(e); + } + } + + mHandler = new Handler(context.getMainLooper()); + mUpdateCurrentPositionTimer = + new Runnable() { + @Override + public void run() { + if (mListener != null) + mListener.currentPositionChanged(getCurrentPosition()); + if (isPlaying()) mHandler.postDelayed(mUpdateCurrentPositionTimer, 20); + } + }; + + mPlayer = LinphoneManager.getLc().createLocalPlayer(null, null, null); + mPlayer.addListener(this); + } + + public String getRecordPath() { + return mRecordPath; + } + + public String getName() { + return mName; + } + + public Date getRecordDate() { + return mRecordDate; + } + + public boolean isClosed() { + return mPlayer.getState() == Player.State.Closed; + } + + public void play() { + if (isClosed()) { + mPlayer.open(mRecordPath); + } + + mPlayer.start(); + mHandler.post(mUpdateCurrentPositionTimer); + } + + public boolean isPlaying() { + return mPlayer.getState() == Player.State.Playing; + } + + public void pause() { + if (!isClosed()) { + mPlayer.pause(); + } + } + + public boolean isPaused() { + return mPlayer.getState() == Player.State.Paused; + } + + public void seek(int i) { + if (!isClosed()) mPlayer.seek(i); + } + + public int getCurrentPosition() { + if (isClosed()) { + mPlayer.open(mRecordPath); + } + + return mPlayer.getCurrentPosition(); + } + + public int getDuration() { + if (isClosed()) { + mPlayer.open(mRecordPath); + } + + return mPlayer.getDuration(); + } + + public void close() { + mPlayer.removeListener(this); + mPlayer.close(); + } + + public void setRecordingListener(RecordingListener listener) { + this.mListener = listener; + } + + @Override + public void onEofReached(Player player) { + if (mListener != null) mListener.endOfRecordReached(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Recording) { + Recording r = (Recording) o; + return mRecordPath.equals(r.getRecordPath()); + } + return false; + } + + @Override + public int compareTo(@NonNull Recording o) { + return -mRecordDate.compareTo(o.getRecordDate()); + } +} diff --git a/app/src/main/java/org/linphone/recording/RecordingListener.java b/app/src/main/java/org/linphone/recording/RecordingListener.java new file mode 100644 index 000000000..bb3abc82b --- /dev/null +++ b/app/src/main/java/org/linphone/recording/RecordingListener.java @@ -0,0 +1,26 @@ +package org.linphone.recording; + +/* +RecordingListener.java +Copyright (C) 2018 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. +*/ + +interface RecordingListener { + void currentPositionChanged(int currentPosition); + + void endOfRecordReached(); +} diff --git a/app/src/main/java/org/linphone/recording/RecordingViewHolder.java b/app/src/main/java/org/linphone/recording/RecordingViewHolder.java new file mode 100644 index 000000000..1075e46a4 --- /dev/null +++ b/app/src/main/java/org/linphone/recording/RecordingViewHolder.java @@ -0,0 +1,83 @@ +package org.linphone.recording; + +/* +RecordingViewHolder.java +Copyright (C) 2018 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.view.View; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import org.linphone.R; + +public class RecordingViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener, View.OnLongClickListener { + public final ImageView playButton; + public final TextView name; + public final TextView date; + public final TextView currentPosition; + public final TextView duration; + public final SeekBar progressionBar; + public final CheckBox select; + public final LinearLayout separator; + public final TextView separatorText; + + private final RecordingViewHolder.ClickListener mListener; + + public RecordingViewHolder(View view, RecordingViewHolder.ClickListener listener) { + super(view); + + playButton = view.findViewById(R.id.record_play); + name = view.findViewById(R.id.record_name); + date = view.findViewById(R.id.record_date); + currentPosition = view.findViewById(R.id.record_current_time); + duration = view.findViewById(R.id.record_duration); + progressionBar = view.findViewById(R.id.record_progression_bar); + select = view.findViewById(R.id.delete); + separator = view.findViewById(R.id.separator); + separatorText = view.findViewById(R.id.separator_text); + + mListener = listener; + view.setOnClickListener(this); + view.setOnLongClickListener(this); + } + + @Override + public void onClick(View view) { + if (mListener != null) { + mListener.onItemClicked(getAdapterPosition()); + } + } + + @Override + public boolean onLongClick(View view) { + if (mListener != null) { + return mListener.onItemLongClicked(getAdapterPosition()); + } + return false; + } + + public interface ClickListener { + void onItemClicked(int position); + + boolean onItemLongClicked(int position); + } +} diff --git a/app/src/main/java/org/linphone/recording/RecordingsAdapter.java b/app/src/main/java/org/linphone/recording/RecordingsAdapter.java new file mode 100644 index 000000000..47dd6fdc9 --- /dev/null +++ b/app/src/main/java/org/linphone/recording/RecordingsAdapter.java @@ -0,0 +1,244 @@ +package org.linphone.recording; + +/* +RecordingsAdapter.java +Copyright (C) 2018 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.annotation.SuppressLint; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SeekBar; +import androidx.annotation.NonNull; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import org.linphone.R; +import org.linphone.utils.SelectableAdapter; +import org.linphone.utils.SelectableHelper; + +public class RecordingsAdapter extends SelectableAdapter { + private final List mRecordings; + private final Context mContext; + private final RecordingViewHolder.ClickListener mClickListener; + + public RecordingsAdapter( + Context context, + List recordings, + RecordingViewHolder.ClickListener listener, + SelectableHelper helper) { + super(helper); + + mRecordings = recordings; + mContext = context; + mClickListener = listener; + } + + @Override + public Object getItem(int position) { + return mRecordings.get(position); + } + + @NonNull + @Override + public RecordingViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { + View v = + LayoutInflater.from(viewGroup.getContext()) + .inflate(R.layout.recording_cell, viewGroup, false); + return new RecordingViewHolder(v, mClickListener); + } + + @SuppressLint("SimpleDateFormat") + @Override + public void onBindViewHolder(@NonNull final RecordingViewHolder viewHolder, int i) { + final Recording record = mRecordings.get(i); + + viewHolder.name.setSelected(true); // For automated horizontal scrolling of long texts + + Calendar recordTime = Calendar.getInstance(); + recordTime.setTime(record.getRecordDate()); + viewHolder.separatorText.setText(DateToHumanDate(recordTime)); + viewHolder.select.setVisibility(isEditionEnabled() ? View.VISIBLE : View.GONE); + viewHolder.select.setChecked(isSelected(i)); + + if (i > 0) { + Recording previousRecord = mRecordings.get(i - 1); + Date previousRecordDate = previousRecord.getRecordDate(); + Calendar previousRecordTime = Calendar.getInstance(); + previousRecordTime.setTime(previousRecordDate); + + if (isSameDay(previousRecordTime, recordTime)) { + viewHolder.separator.setVisibility(View.GONE); + } else { + viewHolder.separator.setVisibility(View.VISIBLE); + } + } else { + viewHolder.separator.setVisibility(View.VISIBLE); + } + + if (record.isPlaying()) { + viewHolder.playButton.setImageResource(R.drawable.record_pause); + } else { + viewHolder.playButton.setImageResource(R.drawable.record_play); + } + viewHolder.playButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (record.isPaused()) { + record.play(); + viewHolder.playButton.setImageResource(R.drawable.record_pause); + } else { + record.pause(); + viewHolder.playButton.setImageResource(R.drawable.record_play); + } + } + }); + + viewHolder.name.setText(record.getName()); + viewHolder.date.setText(new SimpleDateFormat("HH:mm").format(record.getRecordDate())); + + int position = record.getCurrentPosition(); + viewHolder.currentPosition.setText( + String.format( + Locale.getDefault(), + "%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(position), + TimeUnit.MILLISECONDS.toSeconds(position) + - TimeUnit.MINUTES.toSeconds( + TimeUnit.MILLISECONDS.toMinutes(position)))); + + int duration = record.getDuration(); + viewHolder.duration.setText( + String.format( + Locale.getDefault(), + "%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(duration), + TimeUnit.MILLISECONDS.toSeconds(duration) + - TimeUnit.MINUTES.toSeconds( + TimeUnit.MILLISECONDS.toMinutes(duration)))); + + viewHolder.progressionBar.setMax(record.getDuration()); + viewHolder.progressionBar.setProgress(0); + viewHolder.progressionBar.setOnSeekBarChangeListener( + new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + int progressToSet = + progress > 0 && progress < seekBar.getMax() ? progress : 0; + + if (progress == seekBar.getMax()) { + if (record.isPlaying()) record.pause(); + } + + record.seek(progressToSet); + seekBar.setProgress(progressToSet); + + int currentPosition = record.getCurrentPosition(); + viewHolder.currentPosition.setText( + String.format( + Locale.getDefault(), + "%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(currentPosition), + TimeUnit.MILLISECONDS.toSeconds(currentPosition) + - TimeUnit.MINUTES.toSeconds( + TimeUnit.MILLISECONDS.toMinutes( + currentPosition)))); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) {} + + @Override + public void onStopTrackingTouch(SeekBar seekBar) {} + }); + + record.setRecordingListener( + new RecordingListener() { + @Override + public void currentPositionChanged(int currentPosition) { + viewHolder.currentPosition.setText( + String.format( + Locale.getDefault(), + "%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(currentPosition), + TimeUnit.MILLISECONDS.toSeconds(currentPosition) + - TimeUnit.MINUTES.toSeconds( + TimeUnit.MILLISECONDS.toMinutes( + currentPosition)))); + viewHolder.progressionBar.setProgress(currentPosition); + } + + @Override + public void endOfRecordReached() { + record.pause(); + record.seek(0); + viewHolder.progressionBar.setProgress(0); + viewHolder.currentPosition.setText("00:00"); + viewHolder.playButton.setImageResource(R.drawable.record_play); + } + }); + } + + @Override + public int getItemCount() { + return mRecordings.size(); + } + + @SuppressLint("SimpleDateFormat") + private String DateToHumanDate(Calendar cal) { + SimpleDateFormat dateFormat; + if (isToday(cal)) { + return mContext.getString(R.string.today); + } else if (isYesterday(cal)) { + return mContext.getString(R.string.yesterday); + } else { + dateFormat = + new SimpleDateFormat( + mContext.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); + } +} diff --git a/app/src/main/java/org/linphone/recording/RecordingsFragment.java b/app/src/main/java/org/linphone/recording/RecordingsFragment.java new file mode 100644 index 000000000..63251c904 --- /dev/null +++ b/app/src/main/java/org/linphone/recording/RecordingsFragment.java @@ -0,0 +1,250 @@ +package org.linphone.recording; + +/* +RecordingsFragment.java +Copyright (C) 2018 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.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.utils.FileUtils; +import org.linphone.utils.SelectableHelper; + +public class RecordingsFragment extends Fragment + implements AdapterView.OnItemClickListener, + RecordingViewHolder.ClickListener, + SelectableHelper.DeleteListener { + private RecyclerView mRecordingList; + private List mRecordings; + private TextView mNoRecordings; + private RecordingsAdapter mRecordingsAdapter; + private LinearLayoutManager mLayoutManager; + private Context mContext; + private SelectableHelper mSelectableHelper; + private ImageView mBackButton; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.recordings_list, container, false); + + mContext = getActivity().getApplicationContext(); + mSelectableHelper = new SelectableHelper(view, this); + + mRecordingList = view.findViewById(R.id.recording_list); + mNoRecordings = view.findViewById(R.id.no_recordings); + + mBackButton = view.findViewById(R.id.back); + if (getResources().getBoolean(R.bool.isTablet)) { + mBackButton.setVisibility(View.INVISIBLE); + } else { + mBackButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + LinphoneActivity.instance().popBackStack(); + } + }); + } + + mLayoutManager = new LinearLayoutManager(mContext); + mRecordingList.setLayoutManager(mLayoutManager); + + // Divider between items + DividerItemDecoration dividerItemDecoration = + new DividerItemDecoration( + mRecordingList.getContext(), mLayoutManager.getOrientation()); + dividerItemDecoration.setDrawable(mContext.getResources().getDrawable(R.drawable.divider)); + mRecordingList.addItemDecoration(dividerItemDecoration); + + mRecordings = new ArrayList<>(); + + return view; + } + + private void hideRecordingListAndDisplayMessageIfEmpty() { + if (mRecordings == null || mRecordings.isEmpty()) { + mNoRecordings.setVisibility(View.VISIBLE); + mRecordingList.setVisibility(View.GONE); + } else { + mNoRecordings.setVisibility(View.GONE); + mRecordingList.setVisibility(View.VISIBLE); + } + } + + private void removeDeletedRecordings() { + String recordingsDirectory = FileUtils.getRecordingsDirectory(mContext); + File directory = new File(recordingsDirectory); + + if (directory.exists() && directory.isDirectory()) { + File[] existingRecordings = directory.listFiles(); + + for (Recording r : mRecordings) { + boolean exists = false; + for (File f : existingRecordings) { + if (f.getPath().equals(r.getRecordPath())) { + exists = true; + break; + } + } + + if (!exists) mRecordings.remove(r); + } + + Collections.sort(mRecordings); + } + hideRecordingListAndDisplayMessageIfEmpty(); + } + + private void searchForRecordings() { + String recordingsDirectory = FileUtils.getRecordingsDirectory(mContext); + File directory = new File(recordingsDirectory); + + if (directory.exists() && directory.isDirectory()) { + File[] existingRecordings = directory.listFiles(); + + for (File f : existingRecordings) { + boolean exists = false; + for (Recording r : mRecordings) { + if (r.getRecordPath().equals(f.getPath())) { + exists = true; + break; + } + } + + if (!exists) { + if (Recording.RECORD_PATTERN.matcher(f.getPath()).matches()) { + mRecordings.add(new Recording(mContext, f.getPath())); + } + } + } + + Collections.sort(mRecordings); + } + } + + @Override + public void onResume() { + super.onResume(); + + // This is necessary, without it you won't be able to remove mRecordings as you won't be + // allowed to. + LinphoneActivity.instance().checkAndRequestExternalStoragePermission(); + + LinphoneManager.getInstance().setAudioManagerModeNormal(); + LinphoneManager.getInstance().routeAudioToSpeaker(); + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.RECORDING_LIST); + } + + removeDeletedRecordings(); + searchForRecordings(); + + hideRecordingListAndDisplayMessageIfEmpty(); + mRecordingsAdapter = + new RecordingsAdapter( + getActivity().getApplicationContext(), + mRecordings, + this, + mSelectableHelper); + mRecordingList.setAdapter(mRecordingsAdapter); + mSelectableHelper.setAdapter(mRecordingsAdapter); + mSelectableHelper.setDialogMessage(R.string.recordings_delete_dialog); + } + + @Override + public void onPause() { + super.onPause(); + + LinphoneManager.getInstance().routeAudioToReceiver(); + + // Close all opened mRecordings + for (Recording r : mRecordings) { + if (!r.isClosed()) { + if (r.isPlaying()) r.pause(); + r.close(); + } + } + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (mRecordingsAdapter.isEditionEnabled()) { + Recording record = mRecordings.get(position); + + if (record.isPlaying()) record.pause(); + record.close(); + + File recordingFile = new File(record.getRecordPath()); + if (recordingFile.delete()) { + mRecordings.remove(record); + } + } + } + + @Override + public void onItemClicked(int position) { + if (mRecordingsAdapter.isEditionEnabled()) { + mRecordingsAdapter.toggleSelection(position); + } + } + + @Override + public boolean onItemLongClicked(int position) { + if (!mRecordingsAdapter.isEditionEnabled()) { + mSelectableHelper.enterEditionMode(); + } + mRecordingsAdapter.toggleSelection(position); + return true; + } + + @Override + public void onDeleteSelection(Object[] objectsToDelete) { + int size = mRecordingsAdapter.getSelectedItemCount(); + for (int i = 0; i < size; i++) { + Recording record = (Recording) objectsToDelete[i]; + + if (record.isPlaying()) record.pause(); + record.close(); + + File recordingFile = new File(record.getRecordPath()); + if (recordingFile.delete()) { + mRecordings.remove(record); + } + } + hideRecordingListAndDisplayMessageIfEmpty(); + } +} diff --git a/app/src/main/java/org/linphone/settings/AccountSettingsFragment.java b/app/src/main/java/org/linphone/settings/AccountSettingsFragment.java new file mode 100644 index 000000000..f89373c81 --- /dev/null +++ b/app/src/main/java/org/linphone/settings/AccountSettingsFragment.java @@ -0,0 +1,676 @@ +package org.linphone.settings; + +/* +AccountSettingsFragment.java +Copyright (C) 2019 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.content.Intent; +import android.os.Bundle; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.assistant.AssistantActivity; +import org.linphone.core.AVPFMode; +import org.linphone.core.Address; +import org.linphone.core.AuthInfo; +import org.linphone.core.Core; +import org.linphone.core.Factory; +import org.linphone.core.NatPolicy; +import org.linphone.core.ProxyConfig; +import org.linphone.core.TransportType; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.widget.BasicSetting; +import org.linphone.settings.widget.ListSetting; +import org.linphone.settings.widget.SettingListenerBase; +import org.linphone.settings.widget.SwitchSetting; +import org.linphone.settings.widget.TextSetting; +import org.linphone.utils.PushNotificationUtils; + +public class AccountSettingsFragment extends Fragment { + protected View mRootView; + protected LinphonePreferences mPrefs; + private int mAccountIndex; + private ProxyConfig mProxyConfig; + private AuthInfo mAuthInfo; + private boolean mIsNewlyCreatedAccount; + + private TextSetting mUsername, + mUserId, + mPassword, + mDomain, + mDisplayName, + mProxy, + mStun, + mExpire, + mPrefix, + mAvpfInterval; + private SwitchSetting mDisable, + mUseAsDefault, + mOutboundProxy, + mIce, + mAvpf, + mReplacePlusBy00, + mPush; + private BasicSetting mChangePassword, mDeleteAccount, mLinkAccount; + private ListSetting mTransport; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.settings_account, container, false); + + loadSettings(); + + mIsNewlyCreatedAccount = true; + mAccountIndex = getArguments().getInt("Account", -1); + if (mAccountIndex == -1 && savedInstanceState != null) { + mAccountIndex = savedInstanceState.getInt("Account", -1); + } + + mProxyConfig = null; + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (mAccountIndex >= 0 && core != null) { + ProxyConfig[] proxyConfigs = core.getProxyConfigList(); + if (proxyConfigs.length > mAccountIndex) { + mProxyConfig = proxyConfigs[mAccountIndex]; + mIsNewlyCreatedAccount = false; + } else { + Log.e("[Account Settings] Proxy config not found !"); + } + } + + return mRootView; + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("Account", mAccountIndex); + } + + @Override + public void onResume() { + super.onResume(); + + mPrefs = LinphonePreferences.instance(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .selectMenu( + FragmentsAvailable.SETTINGS_SUBLEVEL, + getString(R.string.pref_sipaccount)); + } + + updateValues(); + } + + @Override + public void onPause() { + super.onPause(); + if (mIsNewlyCreatedAccount) { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null && mProxyConfig != null && mAuthInfo != null) { + core.addAuthInfo(mAuthInfo); + core.addProxyConfig(mProxyConfig); + if (mUseAsDefault.isChecked()) { + core.setDefaultProxyConfig(mProxyConfig); + } + } + } + } + + protected void loadSettings() { + mUsername = mRootView.findViewById(R.id.pref_username); + + mUserId = mRootView.findViewById(R.id.pref_auth_userid); + + mPassword = mRootView.findViewById(R.id.pref_passwd); + mPassword.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + mDomain = mRootView.findViewById(R.id.pref_domain); + + mDisplayName = mRootView.findViewById(R.id.pref_display_name); + mDisplayName.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PERSON_NAME); + + mProxy = mRootView.findViewById(R.id.pref_proxy); + mProxy.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + + mStun = mRootView.findViewById(R.id.pref_stun_server); + mStun.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + + mExpire = mRootView.findViewById(R.id.pref_expire); + mExpire.setInputType(InputType.TYPE_CLASS_NUMBER); + + mPrefix = mRootView.findViewById(R.id.pref_prefix); + + mAvpfInterval = mRootView.findViewById(R.id.pref_avpf_rr_interval); + mAvpfInterval.setInputType(InputType.TYPE_CLASS_NUMBER); + + mDisable = mRootView.findViewById(R.id.pref_disable_account); + + mUseAsDefault = mRootView.findViewById(R.id.pref_default_account); + + mOutboundProxy = mRootView.findViewById(R.id.pref_enable_outbound_proxy); + + mIce = mRootView.findViewById(R.id.pref_ice_enable); + + mAvpf = mRootView.findViewById(R.id.pref_avpf); + + mReplacePlusBy00 = mRootView.findViewById(R.id.pref_escape_plus); + + mPush = mRootView.findViewById(R.id.pref_push_notification); + mPush.setVisibility( + PushNotificationUtils.isAvailable(getActivity()) ? View.VISIBLE : View.GONE); + + mChangePassword = mRootView.findViewById(R.id.pref_change_password); + mChangePassword.setVisibility(View.GONE); // TODO + + mDeleteAccount = mRootView.findViewById(R.id.pref_delete_account); + + mLinkAccount = mRootView.findViewById(R.id.pref_link_account); + + mTransport = mRootView.findViewById(R.id.pref_transport); + initTransportList(); + } + + protected void setListeners() { + mUsername.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mAuthInfo != null) { + mAuthInfo.setUsername(newValue); + } else { + Log.e("[Account Settings] No auth info !"); + } + + if (mProxyConfig != null) { + mProxyConfig.edit(); + Address identity = mProxyConfig.getIdentityAddress(); + if (identity != null) { + identity.setUsername(newValue); + } + mProxyConfig.setIdentityAddress(identity); + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mUserId.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mAuthInfo != null) { + mAuthInfo.setUserid(newValue); + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null) { + core.refreshRegisters(); + } + } else { + Log.e("[Account Settings] No auth info !"); + } + } + }); + + mPassword.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mAuthInfo != null) { + mAuthInfo.setPassword(newValue); + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null) { + core.refreshRegisters(); + } + } else { + Log.e("[Account Settings] No auth info !"); + } + } + }); + + mDomain.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mAuthInfo != null) { + mAuthInfo.setDomain(newValue); + } else { + Log.e("[Account Settings] No auth info !"); + } + + if (mProxyConfig != null) { + mProxyConfig.edit(); + Address identity = mProxyConfig.getIdentityAddress(); + if (identity != null) { + identity.setDomain(newValue); + } + mProxyConfig.setIdentityAddress(identity); + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mDisplayName.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + Address identity = mProxyConfig.getIdentityAddress(); + if (identity != null) { + identity.setDisplayName(newValue); + } + mProxyConfig.setIdentityAddress(identity); + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mProxy.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + Address proxy = Factory.instance().createAddress(newValue); + if (proxy != null) { + mProxyConfig.setServerAddr(proxy.asString()); + if (mOutboundProxy.isChecked()) { + mProxyConfig.setRoute(proxy.asString()); + } + mTransport.setValue(proxy.getTransport().toInt()); + } + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mStun.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + NatPolicy natPolicy = mProxyConfig.getNatPolicy(); + if (natPolicy == null) { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null) { + natPolicy = core.createNatPolicy(); + mProxyConfig.setNatPolicy(natPolicy); + } + } + if (natPolicy != null) { + natPolicy.setStunServer(newValue); + } + if (newValue == null || newValue.isEmpty()) { + mIce.setChecked(false); + } + mIce.setEnabled(newValue != null && !newValue.isEmpty()); + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mExpire.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + try { + mProxyConfig.setExpires(Integer.parseInt(newValue)); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mPrefix.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + mProxyConfig.setDialPrefix(newValue); + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mAvpfInterval.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + try { + mProxyConfig.setAvpfRrInterval(Integer.parseInt(newValue)); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mDisable.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + mProxyConfig.enableRegister(!newValue); + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mUseAsDefault.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + if (mProxyConfig != null) { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null && newValue) { + core.setDefaultProxyConfig(mProxyConfig); + mUseAsDefault.setEnabled(false); + } + LinphoneActivity.instance().refreshAccounts(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mOutboundProxy.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + if (newValue) { + mProxyConfig.setRoute(mProxy.getValue()); + } else { + mProxyConfig.setRoute(null); + } + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mIce.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + + NatPolicy natPolicy = mProxyConfig.getNatPolicy(); + if (natPolicy == null) { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null) { + natPolicy = core.createNatPolicy(); + mProxyConfig.setNatPolicy(natPolicy); + } + } + + if (natPolicy != null) { + natPolicy.enableIce(newValue); + } + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mAvpf.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + mProxyConfig.setAvpfMode( + newValue ? AVPFMode.Enabled : AVPFMode.Disabled); + mAvpfInterval.setEnabled(mProxyConfig.avpfEnabled()); + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mReplacePlusBy00.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + mProxyConfig.setDialEscapePlus(newValue); + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mPush.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + mProxyConfig.setPushNotificationAllowed(newValue); + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + + mChangePassword.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + // TODO + } + }); + + mDeleteAccount.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null) { + if (mProxyConfig != null) { + core.removeProxyConfig(mProxyConfig); + } + if (mAuthInfo != null) { + core.removeAuthInfo(mAuthInfo); + } + } + LinphoneActivity.instance().displaySettings(); + LinphoneActivity.instance().refreshAccounts(); + } + }); + + mLinkAccount.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + Intent assistant = new Intent(); + assistant.setClass(LinphoneActivity.instance(), AssistantActivity.class); + assistant.putExtra("LinkPhoneNumber", true); + assistant.putExtra("FromPref", true); + assistant.putExtra("AccountNumber", mAccountIndex); + startActivity(assistant); + } + }); + + mTransport.setListener( + new SettingListenerBase() { + @Override + public void onListValueChanged(int position, String newLabel, String newValue) { + if (mProxyConfig != null) { + mProxyConfig.edit(); + String server = mProxyConfig.getServerAddr(); + Address serverAddr = Factory.instance().createAddress(server); + if (serverAddr != null) { + try { + serverAddr.setTransport( + TransportType.fromInt(Integer.parseInt(newValue))); + server = serverAddr.asString(); + mProxyConfig.setServerAddr(server); + if (mOutboundProxy.isChecked()) { + mProxyConfig.setRoute(server); + } + mProxy.setValue(server); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + mProxyConfig.done(); + } else { + Log.e("[Account Settings] No proxy config !"); + } + } + }); + } + + protected void updateValues() { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core == null) return; + + // Create a proxy config if there is none + if (mProxyConfig == null) { + // Ensure the default configuration is loaded first + String defaultConfig = LinphoneManager.getInstance().getDefaultDynamicConfigFile(); + core.loadConfigFromXml(defaultConfig); + mProxyConfig = core.createProxyConfig(); + mAuthInfo = Factory.instance().createAuthInfo(null, null, null, null, null, null); + mIsNewlyCreatedAccount = true; + } + + if (mProxyConfig != null) { + Address identityAddress = mProxyConfig.getIdentityAddress(); + mAuthInfo = mProxyConfig.findAuthInfo(); + NatPolicy natPolicy = mProxyConfig.getNatPolicy(); + if (natPolicy == null) { + natPolicy = core.createNatPolicy(); + core.setNatPolicy(natPolicy); + } + + if (mAuthInfo != null) { + mUserId.setValue(mAuthInfo.getUserid()); + // If password is hashed we can't display it + mPassword.setValue(mAuthInfo.getPassword()); + } + + mUsername.setValue(identityAddress.getUsername()); + + mDomain.setValue(identityAddress.getDomain()); + + mDisplayName.setValue(identityAddress.getDisplayName()); + + mProxy.setValue(mProxyConfig.getServerAddr()); + + mStun.setValue(natPolicy.getStunServer()); + + mExpire.setValue(mProxyConfig.getExpires()); + + mPrefix.setValue(mProxyConfig.getDialPrefix()); + + mAvpfInterval.setValue(mProxyConfig.getAvpfRrInterval()); + mAvpfInterval.setEnabled(mProxyConfig.avpfEnabled()); + + mDisable.setChecked(!mProxyConfig.registerEnabled()); + + mUseAsDefault.setChecked( + core != null && mProxyConfig.equals(core.getDefaultProxyConfig())); + mUseAsDefault.setEnabled(!mUseAsDefault.isChecked()); + + mOutboundProxy.setChecked(mProxyConfig.getRoute() != null); + + mIce.setChecked(natPolicy.iceEnabled()); + mIce.setEnabled( + natPolicy.getStunServer() != null && !natPolicy.getStunServer().isEmpty()); + + mAvpf.setChecked(mProxyConfig.avpfEnabled()); + + mReplacePlusBy00.setChecked(mProxyConfig.getDialEscapePlus()); + + mPush.setChecked(mProxyConfig.isPushNotificationAllowed()); + + Address proxy = Factory.instance().createAddress(mProxyConfig.getServerAddr()); + if (proxy != null) { + mTransport.setValue(proxy.getTransport().toInt()); + } + } + + setListeners(); + } + + private void initTransportList() { + List entries = new ArrayList<>(); + List values = new ArrayList<>(); + + entries.add(getString(R.string.pref_transport_udp)); + values.add(String.valueOf(TransportType.Udp.toInt())); + entries.add(getString(R.string.pref_transport_tcp)); + values.add(String.valueOf(TransportType.Tcp.toInt())); + + if (!getResources().getBoolean(R.bool.disable_all_security_features_for_markets)) { + entries.add(getString(R.string.pref_transport_tls)); + values.add(String.valueOf(TransportType.Tls.toInt())); + } + + mTransport.setItems(entries, values); + } +} diff --git a/app/src/main/java/org/linphone/settings/AdvancedSettingsFragment.java b/app/src/main/java/org/linphone/settings/AdvancedSettingsFragment.java new file mode 100644 index 000000000..97ba3d1be --- /dev/null +++ b/app/src/main/java/org/linphone/settings/AdvancedSettingsFragment.java @@ -0,0 +1,237 @@ +package org.linphone.settings; + +/* +AdvancedSettingsFragment.java +Copyright (C) 2019 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.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.widget.BasicSetting; +import org.linphone.settings.widget.SettingListenerBase; +import org.linphone.settings.widget.SwitchSetting; +import org.linphone.settings.widget.TextSetting; + +public class AdvancedSettingsFragment extends Fragment { + protected View mRootView; + protected LinphonePreferences mPrefs; + + private SwitchSetting mDebug, + mJavaLogger, + mFriendListSubscribe, + mBackgroundMode, + mStartAtBoot, + mDarkMode; + private TextSetting mRemoteProvisioningUrl, mDisplayName, mUsername, mDeviceName; + private BasicSetting mAndroidAppSettings; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.settings_advanced, container, false); + + loadSettings(); + + return mRootView; + } + + @Override + public void onResume() { + super.onResume(); + + mPrefs = LinphonePreferences.instance(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .selectMenu( + FragmentsAvailable.SETTINGS_SUBLEVEL, + getString(R.string.pref_advanced_title)); + } + + updateValues(); + } + + protected void loadSettings() { + mDebug = mRootView.findViewById(R.id.pref_debug); + + mJavaLogger = mRootView.findViewById(R.id.pref_java_debug); + // This is only required for blackberry users for all we know + mJavaLogger.setVisibility( + Build.MANUFACTURER.equals("BlackBerry") ? View.VISIBLE : View.GONE); + + mFriendListSubscribe = mRootView.findViewById(R.id.pref_friendlist_subscribe); + + mBackgroundMode = mRootView.findViewById(R.id.pref_background_mode); + + mStartAtBoot = mRootView.findViewById(R.id.pref_autostart); + + mDarkMode = mRootView.findViewById(R.id.pref_dark_mode); + + mRemoteProvisioningUrl = mRootView.findViewById(R.id.pref_remote_provisioning); + mRemoteProvisioningUrl.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + + mDisplayName = mRootView.findViewById(R.id.pref_display_name); + + mUsername = mRootView.findViewById(R.id.pref_user_name); + + mAndroidAppSettings = mRootView.findViewById(R.id.pref_android_app_settings); + + mDeviceName = mRootView.findViewById(R.id.pref_device_name); + } + + protected void setListeners() { + mDebug.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setDebugEnabled(newValue); + } + }); + + mJavaLogger.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setJavaLogger(newValue); + } + }); + + mFriendListSubscribe.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.enabledFriendlistSubscription(newValue); + } + }); + + mBackgroundMode.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setServiceNotificationVisibility(newValue); + if (newValue) { + LinphoneService.instance().getNotificationManager().startForeground(); + } else { + LinphoneService.instance().getNotificationManager().stopForeground(); + } + } + }); + + mStartAtBoot.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setAutoStart(newValue); + } + }); + + mDarkMode.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.enableDarkMode(newValue); + } + }); + + mRemoteProvisioningUrl.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setRemoteProvisioningUrl(newValue); + } + }); + + mDisplayName.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setDefaultDisplayName(newValue); + } + }); + + mUsername.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setDefaultUsername(newValue); + } + }); + + mAndroidAppSettings.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + Context context = LinphoneActivity.instance(); + Intent i = new Intent(); + i.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + i.addCategory(Intent.CATEGORY_DEFAULT); + i.setData(Uri.parse("package:" + context.getPackageName())); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + startActivityForResult(i, LinphoneActivity.ANDROID_APP_SETTINGS_ACTIVITY); + } + }); + + mDeviceName.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setDeviceName(newValue); + } + }); + } + + protected void updateValues() { + mDebug.setChecked(mPrefs.isDebugEnabled()); + + mJavaLogger.setChecked(mPrefs.useJavaLogger()); + + mFriendListSubscribe.setChecked(mPrefs.isFriendlistsubscriptionEnabled()); + + mBackgroundMode.setChecked(mPrefs.getServiceNotificationVisibility()); + + mStartAtBoot.setChecked(mPrefs.isAutoStartEnabled()); + + mDarkMode.setChecked(mPrefs.isDarkModeEnabled()); + + mRemoteProvisioningUrl.setValue(mPrefs.getRemoteProvisioningUrl()); + + mDisplayName.setValue(mPrefs.getDefaultDisplayName()); + + mUsername.setValue(mPrefs.getDefaultUsername()); + + mDeviceName.setValue(mPrefs.getDeviceName(LinphoneActivity.instance())); + + setListeners(); + } +} diff --git a/app/src/main/java/org/linphone/settings/AudioSettingsFragment.java b/app/src/main/java/org/linphone/settings/AudioSettingsFragment.java new file mode 100644 index 000000000..4348fa8ce --- /dev/null +++ b/app/src/main/java/org/linphone/settings/AudioSettingsFragment.java @@ -0,0 +1,294 @@ +package org.linphone.settings; + +/* +AudioSettingsFragment.java +Copyright (C) 2019 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.Manifest; +import android.app.Fragment; +import android.content.Context; +import android.content.pm.PackageManager; +import android.media.AudioManager; +import android.os.Bundle; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import androidx.annotation.Nullable; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.Core; +import org.linphone.core.CoreListenerStub; +import org.linphone.core.EcCalibratorStatus; +import org.linphone.core.PayloadType; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.widget.BasicSetting; +import org.linphone.settings.widget.ListSetting; +import org.linphone.settings.widget.SettingListenerBase; +import org.linphone.settings.widget.SwitchSetting; +import org.linphone.settings.widget.TextSetting; + +public class AudioSettingsFragment extends Fragment { + protected View mRootView; + protected LinphonePreferences mPrefs; + + private SwitchSetting mEchoCanceller, mAdaptiveRateControl; + private TextSetting mMicGain, mSpeakerGain; + private ListSetting mCodecBitrateLimit; + private BasicSetting mEchoCalibration, mEchoTester; + private LinearLayout mAudioCodecs; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.settings_audio, container, false); + + loadSettings(); + + return mRootView; + } + + @Override + public void onResume() { + super.onResume(); + + mPrefs = LinphonePreferences.instance(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .selectMenu( + FragmentsAvailable.SETTINGS_SUBLEVEL, + getString(R.string.pref_audio_title)); + } + + updateValues(); + } + + protected void loadSettings() { + mEchoCanceller = mRootView.findViewById(R.id.pref_echo_cancellation); + + mAdaptiveRateControl = mRootView.findViewById(R.id.pref_adaptive_rate_control); + + mMicGain = mRootView.findViewById(R.id.pref_mic_gain_db); + mMicGain.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + + mSpeakerGain = mRootView.findViewById(R.id.pref_playback_gain_db); + mSpeakerGain.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + + mCodecBitrateLimit = mRootView.findViewById(R.id.pref_codec_bitrate_limit); + + mEchoCalibration = mRootView.findViewById(R.id.pref_echo_canceller_calibration); + + mEchoTester = mRootView.findViewById(R.id.pref_echo_tester); + + mAudioCodecs = mRootView.findViewById(R.id.pref_audio_codecs); + } + + protected void setListeners() { + mEchoCanceller.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setEchoCancellation(newValue); + } + }); + + mAdaptiveRateControl.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.enableAdaptiveRateControl(newValue); + } + }); + + mMicGain.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setMicGainDb(Float.valueOf(newValue)); + } + }); + + mSpeakerGain.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setPlaybackGainDb(Float.valueOf(newValue)); + } + }); + + mCodecBitrateLimit.setListener( + new SettingListenerBase() { + @Override + public void onListValueChanged(int position, String newLabel, String newValue) { + int bitrate = Integer.valueOf(newValue); + mPrefs.setCodecBitrateLimit(bitrate); + + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + for (final PayloadType pt : core.getAudioPayloadTypes()) { + if (pt.isVbr()) { + pt.setNormalBitrate(bitrate); + } + } + } + }); + + mEchoCalibration.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + mEchoCalibration.setSubtitle(getString(R.string.ec_calibrating)); + + int recordAudio = + getActivity() + .getPackageManager() + .checkPermission( + Manifest.permission.RECORD_AUDIO, + getActivity().getPackageName()); + if (recordAudio == PackageManager.PERMISSION_GRANTED) { + startEchoCancellerCalibration(); + } else { + LinphoneActivity.instance() + .checkAndRequestRecordAudioPermissionForEchoCanceller(); + } + } + }); + + mEchoTester.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + int recordAudio = + getActivity() + .getPackageManager() + .checkPermission( + Manifest.permission.RECORD_AUDIO, + getActivity().getPackageName()); + if (recordAudio == PackageManager.PERMISSION_GRANTED) { + if (LinphoneManager.getInstance().getEchoTesterStatus()) { + stopEchoTester(); + } else { + startEchoTester(); + } + } else { + LinphoneActivity.instance() + .checkAndRequestRecordAudioPermissionsForEchoTester(); + } + } + }); + } + + protected void updateValues() { + mEchoCanceller.setChecked(mPrefs.echoCancellationEnabled()); + + mAdaptiveRateControl.setChecked(mPrefs.adaptiveRateControlEnabled()); + + mMicGain.setValue(mPrefs.getMicGainDb()); + + mSpeakerGain.setValue(mPrefs.getPlaybackGainDb()); + + mCodecBitrateLimit.setValue(mPrefs.getCodecBitrateLimit()); + + if (mPrefs.echoCancellationEnabled()) { + mEchoCalibration.setSubtitle( + String.format( + getString(R.string.ec_calibrated), + String.valueOf(mPrefs.getEchoCalibration()))); + } + + populateAudioCodecs(); + + setListeners(); + } + + private void populateAudioCodecs() { + mAudioCodecs.removeAllViews(); + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null) { + for (final PayloadType pt : core.getAudioPayloadTypes()) { + final SwitchSetting codec = new SwitchSetting(getActivity()); + codec.setTitle(pt.getMimeType()); + /* Special case */ + if (pt.getMimeType().equals("mpeg4-generic")) { + codec.setTitle("AAC-ELD"); + } + + codec.setSubtitle(pt.getClockRate() + " Hz"); + if (pt.enabled()) { + // Never use codec.setChecked(pt.enabled) ! + codec.setChecked(true); + } + codec.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + pt.enable(newValue); + } + }); + + mAudioCodecs.addView(codec); + } + } + } + + public void startEchoTester() { + if (LinphoneManager.getInstance().startEchoTester() > 0) { + mEchoTester.setSubtitle("Is running"); + } + } + + private void stopEchoTester() { + if (LinphoneManager.getInstance().stopEchoTester() > 0) { + mEchoTester.setSubtitle("Is stopped"); + } + } + + public void startEchoCancellerCalibration() { + if (LinphoneManager.getInstance().getEchoTesterStatus()) stopEchoTester(); + LinphoneManager.getLc() + .addListener( + new CoreListenerStub() { + @Override + public void onEcCalibrationResult( + Core core, EcCalibratorStatus status, int delayMs) { + if (status == EcCalibratorStatus.InProgress) return; + core.removeListener(this); + LinphoneManager.getInstance().routeAudioToReceiver(); + + if (status == EcCalibratorStatus.DoneNoEcho) { + mEchoCalibration.setSubtitle(getString(R.string.no_echo)); + } else if (status == EcCalibratorStatus.Done) { + mEchoCalibration.setSubtitle( + String.format( + getString(R.string.ec_calibrated), + String.valueOf(delayMs))); + } else if (status == EcCalibratorStatus.Failed) { + mEchoCalibration.setSubtitle(getString(R.string.failed)); + } + mEchoCanceller.setChecked(status != EcCalibratorStatus.DoneNoEcho); + ((AudioManager) + getActivity() + .getSystemService(Context.AUDIO_SERVICE)) + .setMode(AudioManager.MODE_NORMAL); + } + }); + LinphoneManager.getInstance().startEcCalibration(); + } +} diff --git a/app/src/main/java/org/linphone/settings/CallSettingsFragment.java b/app/src/main/java/org/linphone/settings/CallSettingsFragment.java new file mode 100644 index 000000000..a973c727d --- /dev/null +++ b/app/src/main/java/org/linphone/settings/CallSettingsFragment.java @@ -0,0 +1,253 @@ +package org.linphone.settings; + +/* +CallSettingsFragment.java +Copyright (C) 2019 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.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.Core; +import org.linphone.core.MediaEncryption; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.widget.ListSetting; +import org.linphone.settings.widget.SettingListenerBase; +import org.linphone.settings.widget.SwitchSetting; +import org.linphone.settings.widget.TextSetting; + +public class CallSettingsFragment extends Fragment { + protected View mRootView; + protected LinphonePreferences mPrefs; + + private SwitchSetting mDeviceRingtone, + mVibrateIncomingCall, + mDtmfSipInfo, + mDtmfRfc2833, + mAutoAnswer; + private ListSetting mMediaEncryption; + private TextSetting mAutoAnswerTime, mIncomingCallTimeout, mVoiceMailUri; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.settings_call, container, false); + + loadSettings(); + + return mRootView; + } + + @Override + public void onResume() { + super.onResume(); + + mPrefs = LinphonePreferences.instance(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .selectMenu( + FragmentsAvailable.SETTINGS_SUBLEVEL, + getString(R.string.pref_call_title)); + } + + updateValues(); + } + + protected void loadSettings() { + mDeviceRingtone = mRootView.findViewById(R.id.pref_device_ringtone); + + mVibrateIncomingCall = mRootView.findViewById(R.id.pref_vibrate_on_incoming_calls); + + mDtmfSipInfo = mRootView.findViewById(R.id.pref_sipinfo_dtmf); + + mDtmfRfc2833 = mRootView.findViewById(R.id.pref_rfc2833_dtmf); + + mAutoAnswer = mRootView.findViewById(R.id.pref_auto_answer); + + mMediaEncryption = mRootView.findViewById(R.id.pref_media_encryption); + initMediaEncryptionList(); + + mAutoAnswerTime = mRootView.findViewById(R.id.pref_auto_answer_time); + mAutoAnswerTime.setInputType(InputType.TYPE_CLASS_NUMBER); + + mIncomingCallTimeout = mRootView.findViewById(R.id.pref_incoming_call_timeout); + mAutoAnswerTime.setInputType(InputType.TYPE_CLASS_NUMBER); + + mVoiceMailUri = mRootView.findViewById(R.id.pref_voice_mail); + mAutoAnswerTime.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + } + + protected void setListeners() { + mDeviceRingtone.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.enableDeviceRingtone(newValue); + } + }); + + mVibrateIncomingCall.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.enableIncomingCallVibration(newValue); + } + }); + + mDtmfSipInfo.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + if (newValue) mDtmfRfc2833.setChecked(false); + mPrefs.sendDTMFsAsSipInfo(newValue); + } + }); + + mDtmfRfc2833.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + if (newValue) mDtmfSipInfo.setChecked(false); + mPrefs.sendDtmfsAsRfc2833(newValue); + } + }); + + mAutoAnswer.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.enableAutoAnswer(newValue); + mAutoAnswerTime.setVisibility( + mPrefs.isAutoAnswerEnabled() ? View.VISIBLE : View.GONE); + } + }); + + mMediaEncryption.setListener( + new SettingListenerBase() { + @Override + public void onListValueChanged(int position, String newLabel, String newValue) { + try { + mPrefs.setMediaEncryption( + MediaEncryption.fromInt(Integer.parseInt(newValue))); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + }); + + mAutoAnswerTime.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + try { + mPrefs.setAutoAnswerTime(Integer.parseInt(newValue)); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + }); + + mIncomingCallTimeout.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + try { + mPrefs.setIncTimeout(Integer.parseInt(newValue)); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + }); + + mVoiceMailUri.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setVoiceMailUri(newValue); + } + }); + } + + protected void updateValues() { + mDeviceRingtone.setChecked(mPrefs.isDeviceRingtoneEnabled()); + + mVibrateIncomingCall.setChecked(mPrefs.isIncomingCallVibrationEnabled()); + + mDtmfSipInfo.setChecked(mPrefs.useSipInfoDtmfs()); + + mDtmfRfc2833.setChecked(mPrefs.useRfc2833Dtmfs()); + + mAutoAnswer.setChecked(mPrefs.isAutoAnswerEnabled()); + + mMediaEncryption.setValue(mPrefs.getMediaEncryption().toInt()); + + mAutoAnswerTime.setValue(mPrefs.getAutoAnswerTime()); + mAutoAnswerTime.setVisibility(mPrefs.isAutoAnswerEnabled() ? View.VISIBLE : View.GONE); + + mIncomingCallTimeout.setValue(mPrefs.getIncTimeout()); + + mVoiceMailUri.setValue(mPrefs.getVoiceMailUri()); + + setListeners(); + } + + private void initMediaEncryptionList() { + List entries = new ArrayList<>(); + List values = new ArrayList<>(); + + entries.add(getString(R.string.pref_none)); + values.add(String.valueOf(MediaEncryption.None.toInt())); + + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null + && !getResources().getBoolean(R.bool.disable_all_security_features_for_markets)) { + boolean hasZrtp = core.mediaEncryptionSupported(MediaEncryption.ZRTP); + boolean hasSrtp = core.mediaEncryptionSupported(MediaEncryption.SRTP); + boolean hasDtls = core.mediaEncryptionSupported(MediaEncryption.DTLS); + + if (!hasSrtp && !hasZrtp && !hasDtls) { + mMediaEncryption.setEnabled(false); + } else { + if (hasSrtp) { + entries.add("SRTP"); + values.add(String.valueOf(MediaEncryption.SRTP.toInt())); + } + if (hasZrtp) { + entries.add("ZRTP"); + values.add(String.valueOf(MediaEncryption.ZRTP.toInt())); + } + if (hasDtls) { + entries.add("DTLS"); + values.add(String.valueOf(MediaEncryption.DTLS.toInt())); + } + } + } + + mMediaEncryption.setItems(entries, values); + } +} diff --git a/app/src/main/java/org/linphone/settings/ChatSettingsFragment.java b/app/src/main/java/org/linphone/settings/ChatSettingsFragment.java new file mode 100644 index 000000000..25db7cde1 --- /dev/null +++ b/app/src/main/java/org/linphone/settings/ChatSettingsFragment.java @@ -0,0 +1,172 @@ +package org.linphone.settings; + +/* +ChatSettingsFragment.java +Copyright (C) 2019 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.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import org.linphone.LinphoneActivity; +import org.linphone.R; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.mediastream.Version; +import org.linphone.settings.widget.BasicSetting; +import org.linphone.settings.widget.ListSetting; +import org.linphone.settings.widget.SettingListenerBase; +import org.linphone.settings.widget.TextSetting; + +public class ChatSettingsFragment extends Fragment { + protected View mRootView; + protected LinphonePreferences mPrefs; + + private TextSetting mSharingServer, mMaxSizeForAutoDownloadIncomingFiles; + private BasicSetting mAndroidNotificationSettings; + private ListSetting mAutoDownloadIncomingFilesPolicy; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.settings_chat, container, false); + + loadSettings(); + + return mRootView; + } + + @Override + public void onResume() { + super.onResume(); + + mPrefs = LinphonePreferences.instance(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .selectMenu( + FragmentsAvailable.SETTINGS_SUBLEVEL, + getString(R.string.pref_chat_title)); + } + + updateValues(); + } + + protected void loadSettings() { + mSharingServer = mRootView.findViewById(R.id.pref_image_sharing_server); + mSharingServer.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + + mMaxSizeForAutoDownloadIncomingFiles = + mRootView.findViewById(R.id.pref_auto_download_max_size); + + mAutoDownloadIncomingFilesPolicy = mRootView.findViewById(R.id.pref_auto_download_policy); + + mAndroidNotificationSettings = mRootView.findViewById(R.id.pref_android_app_notif_settings); + } + + protected void setListeners() { + mSharingServer.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setSharingPictureServerUrl(newValue); + } + }); + + mAutoDownloadIncomingFilesPolicy.setListener( + new SettingListenerBase() { + @Override + public void onListValueChanged(int position, String newLabel, String newValue) { + try { + int max_size = Integer.valueOf(newValue); + mPrefs.setAutoDownloadFileMaxSize(max_size); + updateAutoDownloadSettingsFromValue(max_size); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + }); + + mMaxSizeForAutoDownloadIncomingFiles.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + try { + mPrefs.setAutoDownloadFileMaxSize(Integer.valueOf(newValue)); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + }); + + mAndroidNotificationSettings.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + if (Build.VERSION.SDK_INT >= Version.API26_O_80) { + Context context = LinphoneActivity.instance(); + Intent i = new Intent(); + i.setAction(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS); + i.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); + i.putExtra( + Settings.EXTRA_CHANNEL_ID, + context.getString(R.string.notification_channel_id)); + i.addCategory(Intent.CATEGORY_DEFAULT); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + startActivity(i); + } + } + }); + } + + protected void updateValues() { + mSharingServer.setValue(mPrefs.getSharingPictureServerUrl()); + + updateAutoDownloadSettingsFromValue(mPrefs.getAutoDownloadFileMaxSize()); + + if (Version.sdkStrictlyBelow(Version.API26_O_80)) { + mAndroidNotificationSettings.setVisibility(View.GONE); + } + + setListeners(); + } + + private void updateAutoDownloadSettingsFromValue(int max_size) { + if (max_size == -1) { + mAutoDownloadIncomingFilesPolicy.setValue( + getString(R.string.pref_auto_download_policy_disabled_key)); + } else if (max_size == 0) { + mAutoDownloadIncomingFilesPolicy.setValue( + getString(R.string.pref_auto_download_policy_always_key)); + } else { + mAutoDownloadIncomingFilesPolicy.setValue( + getString(R.string.pref_auto_download_policy_size_key)); + } + mMaxSizeForAutoDownloadIncomingFiles.setValue(max_size); + mMaxSizeForAutoDownloadIncomingFiles.setVisibility(max_size > 0 ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/main/java/org/linphone/settings/LinphonePreferences.java b/app/src/main/java/org/linphone/settings/LinphonePreferences.java new file mode 100644 index 000000000..402a134ed --- /dev/null +++ b/app/src/main/java/org/linphone/settings/LinphonePreferences.java @@ -0,0 +1,1079 @@ +package org.linphone.settings; + +/* +LinphonePreferences.java +Copyright (C) 2017 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.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import androidx.appcompat.app.AppCompatDelegate; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.compatibility.Compatibility; +import org.linphone.core.Address; +import org.linphone.core.AuthInfo; +import org.linphone.core.Config; +import org.linphone.core.Core; +import org.linphone.core.Factory; +import org.linphone.core.MediaEncryption; +import org.linphone.core.NatPolicy; +import org.linphone.core.ProxyConfig; +import org.linphone.core.Transports; +import org.linphone.core.Tunnel; +import org.linphone.core.TunnelConfig; +import org.linphone.core.VideoActivationPolicy; +import org.linphone.core.tools.Log; +import org.linphone.purchase.Purchasable; +import org.linphone.utils.LinphoneUtils; + +public class LinphonePreferences { + private static final int LINPHONE_CORE_RANDOM_PORT = -1; + private static LinphonePreferences sInstance; + + private Context mContext; + private String mBasePath; + // Tunnel settings + private TunnelConfig mTunnelConfig = null; + + private LinphonePreferences() {} + + public static synchronized LinphonePreferences instance() { + if (sInstance == null) { + sInstance = new LinphonePreferences(); + } + return sInstance; + } + + public void setContext(Context c) { + mContext = c; + mBasePath = mContext.getFilesDir().getAbsolutePath(); + } + + private String getString(int key) { + if (mContext == null && LinphoneManager.isInstanciated()) { + mContext = LinphoneManager.getInstance().getContext(); + } + + return mContext.getString(key); + } + + private Core getLc() { + if (!LinphoneManager.isInstanciated()) return null; + + return LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + } + + public Config getConfig() { + Core lc = getLc(); + if (lc != null) { + return lc.getConfig(); + } + + if (!LinphoneManager.isInstanciated()) { + File linphonerc = new File(mBasePath + "/.linphonerc"); + if (linphonerc.exists()) { + return Factory.instance().createConfig(linphonerc.getAbsolutePath()); + } else if (mContext != null) { + InputStream inputStream = + mContext.getResources().openRawResource(R.raw.linphonerc_default); + InputStreamReader inputreader = new InputStreamReader(inputStream); + BufferedReader buffreader = new BufferedReader(inputreader); + StringBuilder text = new StringBuilder(); + String line; + try { + while ((line = buffreader.readLine()) != null) { + text.append(line); + text.append('\n'); + } + } catch (IOException ioe) { + Log.e(ioe); + } + return Factory.instance().createConfigFromString(text.toString()); + } + } else { + return Factory.instance().createConfig(LinphoneManager.getInstance().configFile); + } + return null; + } + + // App settings + public boolean isFirstLaunch() { + return getConfig().getBool("app", "first_launch", true); + } + + public void firstLaunchSuccessful() { + getConfig().setBool("app", "first_launch", false); + } + + public String getRingtone(String defaultRingtone) { + String ringtone = getConfig().getString("app", "ringtone", defaultRingtone); + if (ringtone == null || ringtone.length() == 0) ringtone = defaultRingtone; + return ringtone; + } + + // Accounts settings + private ProxyConfig getProxyConfig(int n) { + if (getLc() == null) return null; + ProxyConfig[] prxCfgs = getLc().getProxyConfigList(); + if (n < 0 || n >= prxCfgs.length) return null; + return prxCfgs[n]; + } + + private AuthInfo getAuthInfo(int n) { + ProxyConfig prxCfg = getProxyConfig(n); + if (prxCfg == null) return null; + Address addr = prxCfg.getIdentityAddress(); + return getLc().findAuthInfo(null, addr.getUsername(), addr.getDomain()); + } + + public String getAccountUsername(int n) { + AuthInfo authInfo = getAuthInfo(n); + return authInfo == null ? null : authInfo.getUsername(); + } + + public String getAccountHa1(int n) { + AuthInfo authInfo = getAuthInfo(n); + return authInfo == null ? null : authInfo.getHa1(); + } + + public String getAccountDomain(int n) { + ProxyConfig proxyConf = getProxyConfig(n); + return (proxyConf != null) ? proxyConf.getDomain() : ""; + } + + public void setPrefix(int n, String prefix) { + ProxyConfig prxCfg = getProxyConfig(n); + prxCfg.edit(); + prxCfg.setDialPrefix(prefix); + prxCfg.done(); + } + + public boolean isFriendlistsubscriptionEnabled() { + if (getConfig().getBool("app", "friendlist_subscription_enabled", false)) { + // Old setting, do migration + getConfig().setBool("app", "friendlist_subscription_enabled", false); + enabledFriendlistSubscription(true); + } + return getLc().isFriendListSubscriptionEnabled(); + } + + public void enabledFriendlistSubscription(boolean enabled) { + getLc().enableFriendListSubscription(enabled); + } + + public int getDefaultAccountIndex() { + if (getLc() == null) return -1; + ProxyConfig defaultPrxCfg = getLc().getDefaultProxyConfig(); + if (defaultPrxCfg == null) return -1; + + ProxyConfig[] prxCfgs = getLc().getProxyConfigList(); + for (int i = 0; i < prxCfgs.length; i++) { + if (defaultPrxCfg.getIdentityAddress().equals(prxCfgs[i].getIdentityAddress())) { + return i; + } + } + return -1; + } + + public int getAccountCount() { + if (getLc() == null || getLc().getProxyConfigList() == null) return 0; + + return getLc().getProxyConfigList().length; + } + + public void setAccountEnabled(int n, boolean enabled) { + if (getLc() == null) return; + ProxyConfig prxCfg = getProxyConfig(n); + if (prxCfg == null) { + LinphoneUtils.displayErrorAlert(getString(R.string.error), mContext); + return; + } + prxCfg.edit(); + prxCfg.enableRegister(enabled); + prxCfg.done(); + + // If default proxy config is disabled, try to set another one as default proxy + if (!enabled + && getLc().getDefaultProxyConfig() + .getIdentityAddress() + .equals(prxCfg.getIdentityAddress())) { + int count = getLc().getProxyConfigList().length; + if (count > 1) { + for (int i = 0; i < count; i++) { + if (isAccountEnabled(i)) { + getLc().setDefaultProxyConfig(getProxyConfig(i)); + break; + } + } + } + } + } + + public boolean isAccountEnabled(int n) { + return getProxyConfig(n).registerEnabled(); + } + + public void resetDefaultProxyConfig() { + if (getLc() == null) return; + int count = getLc().getProxyConfigList().length; + for (int i = 0; i < count; i++) { + if (isAccountEnabled(i)) { + getLc().setDefaultProxyConfig(getProxyConfig(i)); + break; + } + } + + if (getLc().getDefaultProxyConfig() == null) { + getLc().setDefaultProxyConfig(getProxyConfig(0)); + } + } + // End of accounts settings + + // Audio settings + public void setEchoCancellation(boolean enable) { + if (getLc() == null) return; + getLc().enableEchoCancellation(enable); + } + + public boolean echoCancellationEnabled() { + if (getLc() == null) return false; + return getLc().echoCancellationEnabled(); + } + + public int getEchoCalibration() { + return getConfig().getInt("sound", "ec_delay", -1); + } + + public float getMicGainDb() { + return getLc().getMicGainDb(); + } + + public void setMicGainDb(float gain) { + getLc().setMicGainDb(gain); + } + + public float getPlaybackGainDb() { + return getLc().getPlaybackGainDb(); + } + + public void setPlaybackGainDb(float gain) { + getLc().setPlaybackGainDb(gain); + } + + // End of audio settings + + // Video settings + public boolean useFrontCam() { + return getConfig().getBool("app", "front_camera_default", true); + } + + public void setFrontCamAsDefault(boolean frontcam) { + getConfig().setBool("app", "front_camera_default", frontcam); + } + + public boolean isVideoEnabled() { + if (getLc() == null) return false; + return getLc().videoSupported() && getLc().videoEnabled(); + } + + public void enableVideo(boolean enable) { + if (getLc() == null) return; + getLc().enableVideoCapture(enable); + getLc().enableVideoDisplay(enable); + } + + public boolean shouldInitiateVideoCall() { + if (getLc() == null) return false; + return getLc().getVideoActivationPolicy().getAutomaticallyInitiate(); + } + + public void setInitiateVideoCall(boolean initiate) { + if (getLc() == null) return; + VideoActivationPolicy vap = getLc().getVideoActivationPolicy(); + vap.setAutomaticallyInitiate(initiate); + getLc().setVideoActivationPolicy(vap); + } + + public boolean shouldAutomaticallyAcceptVideoRequests() { + if (getLc() == null) return false; + VideoActivationPolicy vap = getLc().getVideoActivationPolicy(); + return vap.getAutomaticallyAccept(); + } + + public void setAutomaticallyAcceptVideoRequests(boolean accept) { + if (getLc() == null) return; + VideoActivationPolicy vap = getLc().getVideoActivationPolicy(); + vap.setAutomaticallyAccept(accept); + getLc().setVideoActivationPolicy(vap); + } + + public String getVideoPreset() { + if (getLc() == null) return null; + String preset = getLc().getVideoPreset(); + if (preset == null) preset = "default"; + return preset; + } + + public void setVideoPreset(String preset) { + if (getLc() == null) return; + if (preset.equals("default")) preset = null; + getLc().setVideoPreset(preset); + preset = getVideoPreset(); + if (!preset.equals("custom")) { + getLc().setPreferredFramerate(0); + } + setPreferredVideoSize(getPreferredVideoSize()); // Apply the bandwidth limit + } + + public String getPreferredVideoSize() { + // Core can only return video size (width and height), not the name + return getConfig().getString("video", "size", "qvga"); + } + + public void setPreferredVideoSize(String preferredVideoSize) { + if (getLc() == null) return; + getLc().setPreferredVideoSizeByName(preferredVideoSize); + } + + public int getPreferredVideoFps() { + if (getLc() == null) return 0; + return (int) getLc().getPreferredFramerate(); + } + + public void setPreferredVideoFps(int fps) { + if (getLc() == null) return; + getLc().setPreferredFramerate(fps); + } + + public int getBandwidthLimit() { + if (getLc() == null) return 0; + return getLc().getDownloadBandwidth(); + } + + public void setBandwidthLimit(int bandwidth) { + if (getLc() == null) return; + getLc().setUploadBandwidth(bandwidth); + getLc().setDownloadBandwidth(bandwidth); + } + // End of video settings + + // Call settings + public boolean acceptIncomingEarlyMedia() { + return getConfig().getBool("sip", "incoming_calls_early_media", false); + } + + public void setAcceptIncomingEarlyMedia(boolean accept) { + getConfig().setBool("sip", "incoming_calls_early_media", accept); + } + + public boolean useRfc2833Dtmfs() { + if (getLc() == null) return false; + return getLc().getUseRfc2833ForDtmf(); + } + + public void sendDtmfsAsRfc2833(boolean use) { + if (getLc() == null) return; + getLc().setUseRfc2833ForDtmf(use); + } + + public boolean useSipInfoDtmfs() { + if (getLc() == null) return false; + return getLc().getUseInfoForDtmf(); + } + + public void sendDTMFsAsSipInfo(boolean use) { + if (getLc() == null) return; + getLc().setUseInfoForDtmf(use); + } + + public int getIncTimeout() { + if (getLc() == null) return 0; + return getLc().getIncTimeout(); + } + + public void setIncTimeout(int timeout) { + if (getLc() == null) return; + getLc().setIncTimeout(timeout); + } + + public String getVoiceMailUri() { + return getConfig().getString("app", "voice_mail", null); + } + + public void setVoiceMailUri(String uri) { + getConfig().setString("app", "voice_mail", uri); + } + + public boolean getNativeDialerCall() { + return getConfig().getBool("app", "native_dialer_call", false); + } + + public void setNativeDialerCall(boolean use) { + getConfig().setBool("app", "native_dialer_call", use); + } + // End of call settings + + public boolean isWifiOnlyEnabled() { + return getConfig().getBool("app", "wifi_only", false); + } + + // Network settings + public void setWifiOnlyEnabled(Boolean enable) { + getConfig().setBool("app", "wifi_only", enable); + } + + public void useRandomPort(boolean enabled) { + useRandomPort(enabled, true); + } + + private void useRandomPort(boolean enabled, boolean apply) { + getConfig().setBool("app", "random_port", enabled); + if (apply) { + if (enabled) { + setSipPort(LINPHONE_CORE_RANDOM_PORT); + } else { + setSipPort(5060); + } + } + } + + public boolean isUsingRandomPort() { + return getConfig().getBool("app", "random_port", true); + } + + public String getSipPort() { + if (getLc() == null) return null; + Transports transports = getLc().getTransports(); + int port; + if (transports.getUdpPort() > 0) port = transports.getUdpPort(); + else port = transports.getTcpPort(); + return String.valueOf(port); + } + + public void setSipPort(int port) { + if (getLc() == null) return; + Transports transports = getLc().getTransports(); + transports.setUdpPort(port); + transports.setTcpPort(port); + transports.setTlsPort(LINPHONE_CORE_RANDOM_PORT); + getLc().setTransports(transports); + } + + private NatPolicy getOrCreateNatPolicy() { + if (getLc() == null) return null; + NatPolicy nat = getLc().getNatPolicy(); + if (nat == null) { + nat = getLc().createNatPolicy(); + } + return nat; + } + + public String getStunServer() { + NatPolicy nat = getOrCreateNatPolicy(); + return nat.getStunServer(); + } + + public void setStunServer(String stun) { + if (getLc() == null) return; + NatPolicy nat = getOrCreateNatPolicy(); + nat.setStunServer(stun); + + getLc().setNatPolicy(nat); + } + + public boolean isIceEnabled() { + NatPolicy nat = getOrCreateNatPolicy(); + return nat.iceEnabled(); + } + + public void setIceEnabled(boolean enabled) { + if (getLc() == null) return; + NatPolicy nat = getOrCreateNatPolicy(); + nat.enableIce(enabled); + nat.enableStun(enabled); + getLc().setNatPolicy(nat); + } + + public boolean isTurnEnabled() { + NatPolicy nat = getOrCreateNatPolicy(); + return nat.turnEnabled(); + } + + public void setTurnEnabled(boolean enabled) { + if (getLc() == null) return; + NatPolicy nat = getOrCreateNatPolicy(); + nat.enableTurn(enabled); + getLc().setNatPolicy(nat); + } + + public String getTurnUsername() { + NatPolicy nat = getOrCreateNatPolicy(); + return nat.getStunServerUsername(); + } + + public void setTurnUsername(String username) { + if (getLc() == null) return; + NatPolicy nat = getOrCreateNatPolicy(); + AuthInfo authInfo = getLc().findAuthInfo(null, nat.getStunServerUsername(), null); + + if (authInfo != null) { + AuthInfo cloneAuthInfo = authInfo.clone(); + getLc().removeAuthInfo(authInfo); + cloneAuthInfo.setUsername(username); + cloneAuthInfo.setUserid(username); + getLc().addAuthInfo(cloneAuthInfo); + } else { + authInfo = + Factory.instance().createAuthInfo(username, username, null, null, null, null); + getLc().addAuthInfo(authInfo); + } + nat.setStunServerUsername(username); + getLc().setNatPolicy(nat); + } + + public void setTurnPassword(String password) { + if (getLc() == null) return; + NatPolicy nat = getOrCreateNatPolicy(); + AuthInfo authInfo = getLc().findAuthInfo(null, nat.getStunServerUsername(), null); + + if (authInfo != null) { + AuthInfo cloneAuthInfo = authInfo.clone(); + getLc().removeAuthInfo(authInfo); + cloneAuthInfo.setPassword(password); + getLc().addAuthInfo(cloneAuthInfo); + } else { + authInfo = + Factory.instance() + .createAuthInfo( + nat.getStunServerUsername(), + nat.getStunServerUsername(), + password, + null, + null, + null); + getLc().addAuthInfo(authInfo); + } + } + + public MediaEncryption getMediaEncryption() { + if (getLc() == null) return null; + return getLc().getMediaEncryption(); + } + + public void setMediaEncryption(MediaEncryption menc) { + if (getLc() == null) return; + if (menc == null) return; + + getLc().setMediaEncryption(menc); + } + + public boolean isPushNotificationEnabled() { + return getConfig().getBool("app", "push_notification", true); + } + + public void setPushNotificationEnabled(boolean enable) { + getConfig().setBool("app", "push_notification", enable); + + Core lc = getLc(); + if (lc == null) { + return; + } + + if (enable) { + // Add push infos to exisiting proxy configs + String regId = getPushNotificationRegistrationID(); + String appId = getString(R.string.gcm_defaultSenderId); + if (regId != null && lc.getProxyConfigList().length > 0) { + for (ProxyConfig lpc : lc.getProxyConfigList()) { + if (lpc == null) continue; + if (!lpc.isPushNotificationAllowed()) { + lpc.edit(); + lpc.setContactUriParameters(null); + lpc.done(); + if (lpc.getIdentityAddress() != null) + Log.d( + "[Push Notification] infos removed from proxy config " + + lpc.getIdentityAddress().asStringUriOnly()); + } else { + String contactInfos = + "app-id=" + + appId + + ";pn-type=" + + getString(R.string.push_type) + + ";pn-timeout=0" + + ";pn-tok=" + + regId + + ";pn-silent=1"; + String prevContactParams = lpc.getContactParameters(); + if (prevContactParams == null + || prevContactParams.compareTo(contactInfos) != 0) { + lpc.edit(); + lpc.setContactUriParameters(contactInfos); + lpc.done(); + if (lpc.getIdentityAddress() != null) + Log.d( + "[Push Notification] infos added to proxy config " + + lpc.getIdentityAddress().asStringUriOnly()); + } + } + } + Log.i( + "[Push Notification] Refreshing registers to ensure token is up to date: " + + regId); + lc.refreshRegisters(); + } + } else { + if (lc.getProxyConfigList().length > 0) { + for (ProxyConfig lpc : lc.getProxyConfigList()) { + lpc.edit(); + lpc.setContactUriParameters(null); + lpc.done(); + if (lpc.getIdentityAddress() != null) + Log.d( + "[Push Notification] infos removed from proxy config " + + lpc.getIdentityAddress().asStringUriOnly()); + } + lc.refreshRegisters(); + } + } + } + + private String getPushNotificationRegistrationID() { + return getConfig().getString("app", "push_notification_regid", null); + } + + public void setPushNotificationRegistrationID(String regId) { + if (getConfig() == null) return; + Log.i("[Push Notification] New token received: " + regId); + getConfig().setString("app", "push_notification_regid", (regId != null) ? regId : ""); + setPushNotificationEnabled(isPushNotificationEnabled()); + } + + public void useIpv6(Boolean enable) { + if (getLc() == null) return; + getLc().enableIpv6(enable); + } + + public boolean isUsingIpv6() { + if (getLc() == null) return false; + return getLc().ipv6Enabled(); + } + // End of network settings + + public boolean isDebugEnabled() { + return getConfig().getBool("app", "debug", false); + } + + // Advanced settings + public void setDebugEnabled(boolean enabled) { + getConfig().setBool("app", "debug", enabled); + LinphoneUtils.configureLoggingService(enabled, mContext.getString(R.string.app_name)); + } + + public void setJavaLogger(boolean enabled) { + getConfig().setBool("app", "java_logger", enabled); + LinphoneUtils.configureLoggingService( + isDebugEnabled(), mContext.getString(R.string.app_name)); + } + + public boolean useJavaLogger() { + return getConfig().getBool("app", "java_logger", false); + } + + public boolean isAutoStartEnabled() { + return getConfig().getBool("app", "auto_start", false); + } + + public void setAutoStart(boolean autoStartEnabled) { + getConfig().setBool("app", "auto_start", autoStartEnabled); + } + + public String getSharingPictureServerUrl() { + if (getLc() == null) return null; + return getLc().getFileTransferServer(); + } + + public void setSharingPictureServerUrl(String url) { + if (getLc() == null) return; + getLc().setFileTransferServer(url); + } + + public String getRemoteProvisioningUrl() { + if (getLc() == null) return null; + return getLc().getProvisioningUri(); + } + + public void setRemoteProvisioningUrl(String url) { + if (getLc() == null) return; + if (url != null && url.length() == 0) { + url = null; + } + getLc().setProvisioningUri(url); + } + + public String getDefaultDisplayName() { + if (getLc() == null) return null; + return getLc().getPrimaryContactParsed().getDisplayName(); + } + + public void setDefaultDisplayName(String displayName) { + if (getLc() == null) return; + Address primary = getLc().getPrimaryContactParsed(); + primary.setDisplayName(displayName); + getLc().setPrimaryContact(primary.asString()); + } + + public String getDefaultUsername() { + if (getLc() == null) return null; + return getLc().getPrimaryContactParsed().getUsername(); + } + + public void setDefaultUsername(String username) { + if (getLc() == null) return; + Address primary = getLc().getPrimaryContactParsed(); + primary.setUsername(username); + getLc().setPrimaryContact(primary.asString()); + } + // End of advanced settings + + public TunnelConfig getTunnelConfig() { + if (getLc() == null) return null; + if (getLc().tunnelAvailable()) { + Tunnel tunnel = getLc().getTunnel(); + if (mTunnelConfig == null) { + TunnelConfig servers[] = tunnel.getServers(); + if (servers.length > 0) { + mTunnelConfig = servers[0]; + } else { + mTunnelConfig = Factory.instance().createTunnelConfig(); + } + } + return mTunnelConfig; + } else { + return null; + } + } + + public String getTunnelHost() { + TunnelConfig config = getTunnelConfig(); + if (config != null) { + return config.getHost(); + } else { + return null; + } + } + + public void setTunnelHost(String host) { + TunnelConfig config = getTunnelConfig(); + if (config != null) { + config.setHost(host); + LinphoneManager.getInstance().initTunnelFromConf(); + } + } + + public int getTunnelPort() { + TunnelConfig config = getTunnelConfig(); + if (config != null) { + return config.getPort(); + } else { + return -1; + } + } + + public void setTunnelPort(int port) { + TunnelConfig config = getTunnelConfig(); + if (config != null) { + config.setPort(port); + LinphoneManager.getInstance().initTunnelFromConf(); + } + } + + public String getTunnelMode() { + return getConfig().getString("app", "tunnel", null); + } + + public void setTunnelMode(String mode) { + getConfig().setString("app", "tunnel", mode); + LinphoneManager.getInstance().initTunnelFromConf(); + } + + public boolean isProvisioningLoginViewEnabled() { + + return (getConfig() != null) && getConfig().getBool("app", "show_login_view", false); + } + // End of tunnel settings + + public void disableProvisioningLoginView() { + if (isProvisioningLoginViewEnabled()) { // Only do it if it was previously enabled + getConfig().setBool("app", "show_login_view", false); + } else { + Log.w("Remote provisioning login view wasn't enabled, ignoring"); + } + } + + public boolean isFirstRemoteProvisioning() { + return getConfig().getBool("app", "first_remote_provisioning", true); + } + + public boolean adaptiveRateControlEnabled() { + if (getLc() == null) return false; + return getLc().adaptiveRateControlEnabled(); + } + + public void enableAdaptiveRateControl(boolean enabled) { + if (getLc() == null) return; + getLc().enableAdaptiveRateControl(enabled); + } + + public int getCodecBitrateLimit() { + return getConfig().getInt("audio", "codec_bitrate_limit", 36); + } + + public void setCodecBitrateLimit(int bitrate) { + getConfig().setInt("audio", "codec_bitrate_limit", bitrate); + } + + public String getInAppPurchaseValidatingServerUrl() { + return getConfig().getString("in-app-purchase", "server_url", null); + } + + public Purchasable getInAppPurchasedItem() { + String id = getConfig().getString("in-app-purchase", "purchase_item_id", null); + String payload = getConfig().getString("in-app-purchase", "purchase_item_payload", null); + String signature = + getConfig().getString("in-app-purchase", "purchase_item_signature", null); + String username = getConfig().getString("in-app-purchase", "purchase_item_username", null); + + return new Purchasable(id).setPayloadAndSignature(payload, signature).setUserData(username); + } + + public void setInAppPurchasedItem(Purchasable item) { + if (item == null) return; + + getConfig().setString("in-app-purchase", "purchase_item_id", item.getId()); + getConfig().setString("in-app-purchase", "purchase_item_payload", item.getPayload()); + getConfig() + .setString( + "in-app-purchase", "purchase_item_signature", item.getPayloadSignature()); + getConfig().setString("in-app-purchase", "purchase_item_username", item.getUserData()); + } + + public ArrayList getInAppPurchasables() { + ArrayList purchasables = new ArrayList<>(); + String list = getConfig().getString("in-app-purchase", "purchasable_items_ids", null); + if (list != null) { + for (String purchasable : list.split(";")) { + if (purchasable.length() > 0) { + purchasables.add(purchasable); + } + } + } + return purchasables; + } + + public String getXmlrpcUrl() { + return getConfig().getString("assistant", "xmlrpc_url", null); + } + + public String getInappPopupTime() { + return getConfig().getString("app", "inapp_popup_time", null); + } + + public void setInappPopupTime(String date) { + getConfig().setString("app", "inapp_popup_time", date); + } + + public String getLinkPopupTime() { + return getConfig().getString("app", "link_popup_time", null); + } + + public void setLinkPopupTime(String date) { + getConfig().setString("app", "link_popup_time", date); + } + + public boolean isLinkPopupEnabled() { + return getConfig().getBool("app", "link_popup_enabled", true); + } + + public void enableLinkPopup(boolean enable) { + getConfig().setBool("app", "link_popup_enabled", enable); + } + + public boolean isLimeSecurityPopupEnabled() { + return getConfig().getBool("app", "lime_security_popup_enabled", true); + } + + public void enableLimeSecurityPopup(boolean enable) { + getConfig().setBool("app", "lime_security_popup_enabled", enable); + } + + public String getDebugPopupAddress() { + return getConfig().getString("app", "debug_popup_magic", null); + } + + public String getActivityToLaunchOnIncomingReceived() { + return getConfig() + .getString( + "app", "incoming_call_activity", "org.linphone.call.CallIncomingActivity"); + } + + public void setActivityToLaunchOnIncomingReceived(String name) { + getConfig().setString("app", "incoming_call_activity", name); + } + + public boolean getServiceNotificationVisibility() { + return getConfig().getBool("app", "show_service_notification", false); + } + + public void setServiceNotificationVisibility(boolean enable) { + getConfig().setBool("app", "show_service_notification", enable); + } + + public String getCheckReleaseUrl() { + return getConfig().getString("misc", "version_check_url_root", null); + } + + public int getLastCheckReleaseTimestamp() { + return getConfig().getInt("app", "version_check_url_last_timestamp", 0); + } + + public void setLastCheckReleaseTimestamp(int timestamp) { + getConfig().setInt("app", "version_check_url_last_timestamp", timestamp); + } + + public boolean isOverlayEnabled() { + return getConfig().getBool("app", "display_overlay", false); + } + + public void enableOverlay(boolean enable) { + getConfig() + .setBool( + "app", + "display_overlay", + enable + && LinphoneActivity.isInstanciated() + && LinphoneActivity.instance().checkAndRequestOverlayPermission()); + } + + public boolean isDeviceRingtoneEnabled() { + int readExternalStorage = + mContext.getPackageManager() + .checkPermission( + Manifest.permission.READ_EXTERNAL_STORAGE, + mContext.getPackageName()); + return getConfig().getBool("app", "device_ringtone", true) + && readExternalStorage == PackageManager.PERMISSION_GRANTED; + } + + public void enableDeviceRingtone(boolean enable) { + getConfig().setBool("app", "device_ringtone", enable); + LinphoneManager.getInstance().enableDeviceRingtone(enable); + } + + public boolean isIncomingCallVibrationEnabled() { + return getConfig().getBool("app", "incoming_call_vibration", true); + } + + public void enableIncomingCallVibration(boolean enable) { + getConfig().setBool("app", "incoming_call_vibration", enable); + } + + public boolean isBisFeatureEnabled() { + return getConfig().getBool("app", "bis_feature", true); + } + + public boolean isAutoAnswerEnabled() { + return getConfig().getBool("app", "auto_answer", false); + } + + public void enableAutoAnswer(boolean enable) { + getConfig().setBool("app", "auto_answer", enable); + } + + public int getAutoAnswerTime() { + return getConfig().getInt("app", "auto_answer_delay", 0); + } + + public void setAutoAnswerTime(int time) { + getConfig().setInt("app", "auto_answer_delay", time); + } + + public int getCodeLength() { + return getConfig().getInt("app", "activation_code_length", 0); + } + + public void disableFriendsStorage() { + getConfig().setBool("misc", "store_friends", false); + } + + public boolean useBasicChatRoomFor1To1() { + return getConfig().getBool("app", "prefer_basic_chat_room", false); + } + + // 0 is download all, -1 is disable feature, else size is bytes + public int getAutoDownloadFileMaxSize() { + return getLc().getMaxSizeForAutoDownloadIncomingFiles(); + } + + // 0 is download all, -1 is disable feature, else size is bytes + public void setAutoDownloadFileMaxSize(int size) { + getLc().setMaxSizeForAutoDownloadIncomingFiles(size); + } + + public boolean hasPowerSaverDialogBeenPrompted() { + return getConfig().getBool("app", "android_power_saver_dialog", false); + } + + public void powerSaverDialogPrompted(boolean b) { + getConfig().setBool("app", "android_power_saver_dialog", b); + } + + public boolean isDarkModeEnabled() { + if (getConfig() == null) return false; + return getConfig() + .getBool( + "app", + "dark_mode", + AppCompatDelegate.getDefaultNightMode() + == AppCompatDelegate.MODE_NIGHT_YES); + } + + public void enableDarkMode(boolean enable) { + getConfig().setBool("app", "dark_mode", enable); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().recreate(); + } + } + + public String getDeviceName(Context context) { + String defaultValue = Compatibility.getDeviceName(context); + return getConfig().getString("app", "device_name", defaultValue); + } + + public void setDeviceName(String name) { + getConfig().setString("app", "device_name", name); + } +} diff --git a/app/src/main/java/org/linphone/settings/NetworkSettingsFragment.java b/app/src/main/java/org/linphone/settings/NetworkSettingsFragment.java new file mode 100644 index 000000000..16489460c --- /dev/null +++ b/app/src/main/java/org/linphone/settings/NetworkSettingsFragment.java @@ -0,0 +1,250 @@ +package org.linphone.settings; + +/* +NetworkSettingsFragment.java +Copyright (C) 2019 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.content.Intent; +import android.os.Bundle; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import org.linphone.LinphoneActivity; +import org.linphone.R; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.widget.BasicSetting; +import org.linphone.settings.widget.SettingListenerBase; +import org.linphone.settings.widget.SwitchSetting; +import org.linphone.settings.widget.TextSetting; +import org.linphone.utils.DeviceUtils; +import org.linphone.utils.PushNotificationUtils; + +public class NetworkSettingsFragment extends Fragment { + protected View mRootView; + protected LinphonePreferences mPrefs; + + private SwitchSetting mWifiOnly, mIpv6, mPush, mRandomPorts, mIce, mTurn; + private TextSetting mSipPort, mStunServer, mTurnUsername, mTurnPassword; + private BasicSetting mAndroidBatterySaverSettings; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.settings_network, container, false); + + loadSettings(); + + return mRootView; + } + + @Override + public void onResume() { + super.onResume(); + + mPrefs = LinphonePreferences.instance(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .selectMenu( + FragmentsAvailable.SETTINGS_SUBLEVEL, + getString(R.string.pref_network_title)); + } + + updateValues(); + } + + protected void loadSettings() { + mWifiOnly = mRootView.findViewById(R.id.pref_wifi_only); + + mIpv6 = mRootView.findViewById(R.id.pref_ipv6); + + mPush = mRootView.findViewById(R.id.pref_push_notification); + mPush.setVisibility( + PushNotificationUtils.isAvailable(getActivity()) ? View.VISIBLE : View.GONE); + + mRandomPorts = mRootView.findViewById(R.id.pref_transport_use_random_ports); + + mIce = mRootView.findViewById(R.id.pref_ice_enable); + + mTurn = mRootView.findViewById(R.id.pref_turn_enable); + + mSipPort = mRootView.findViewById(R.id.pref_sip_port); + mSipPort.setInputType(InputType.TYPE_CLASS_NUMBER); + + mStunServer = mRootView.findViewById(R.id.pref_stun_server); + mStunServer.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + + mTurnUsername = mRootView.findViewById(R.id.pref_turn_username); + + mTurnPassword = mRootView.findViewById(R.id.pref_turn_passwd); + mTurnPassword.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + + mAndroidBatterySaverSettings = + mRootView.findViewById(R.id.pref_android_battery_protected_settings); + mAndroidBatterySaverSettings.setVisibility( + DeviceUtils.hasDevicePowerManager(LinphoneActivity.instance()) + ? View.VISIBLE + : View.GONE); + } + + protected void setListeners() { + mWifiOnly.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setWifiOnlyEnabled(newValue); + } + }); + + mIpv6.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.useIpv6(newValue); + } + }); + + mPush.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setPushNotificationEnabled(newValue); + } + }); + + mRandomPorts.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.useRandomPort(newValue); + mSipPort.setVisibility( + mPrefs.isUsingRandomPort() ? View.GONE : View.VISIBLE); + } + }); + + mIce.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setIceEnabled(newValue); + } + }); + + mTurn.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setTurnEnabled(newValue); + mTurnUsername.setEnabled(mPrefs.isTurnEnabled()); + mTurnPassword.setEnabled(mPrefs.isTurnEnabled()); + } + }); + + mSipPort.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + try { + mPrefs.setSipPort(Integer.valueOf(newValue)); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + }); + + mStunServer.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setStunServer(newValue); + mIce.setEnabled( + mPrefs.getStunServer() != null + && !mPrefs.getStunServer().isEmpty()); + mTurn.setEnabled( + mPrefs.getStunServer() != null + && !mPrefs.getStunServer().isEmpty()); + if (newValue == null || newValue.isEmpty()) { + mIce.setChecked(false); + mTurn.setChecked(false); + } + } + }); + + mTurnUsername.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setTurnUsername(newValue); + } + }); + + mTurnPassword.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setTurnPassword(newValue); + } + }); + + mAndroidBatterySaverSettings.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + mPrefs.powerSaverDialogPrompted(true); + Intent intent = + DeviceUtils.getDevicePowerManagerIntent( + LinphoneActivity.instance()); + if (intent != null) { + startActivity(intent); + } + } + }); + } + + protected void updateValues() { + mWifiOnly.setChecked(mPrefs.isWifiOnlyEnabled()); + + mIpv6.setChecked(mPrefs.isUsingIpv6()); + + mPush.setChecked(mPrefs.isPushNotificationEnabled()); + + mRandomPorts.setChecked(mPrefs.isUsingRandomPort()); + + mIce.setChecked(mPrefs.isIceEnabled()); + mIce.setEnabled(mPrefs.getStunServer() != null && !mPrefs.getStunServer().isEmpty()); + + mTurn.setChecked(mPrefs.isTurnEnabled()); + mTurn.setEnabled(mPrefs.getStunServer() != null && !mPrefs.getStunServer().isEmpty()); + + mSipPort.setValue(mPrefs.getSipPort()); + mSipPort.setVisibility(mPrefs.isUsingRandomPort() ? View.GONE : View.VISIBLE); + + mStunServer.setValue(mPrefs.getStunServer()); + + mTurnUsername.setValue(mPrefs.getTurnUsername()); + mTurnUsername.setEnabled(mPrefs.isTurnEnabled()); + mTurnPassword.setEnabled(mPrefs.isTurnEnabled()); + + setListeners(); + } +} diff --git a/app/src/main/java/org/linphone/settings/SettingsFragment.java b/app/src/main/java/org/linphone/settings/SettingsFragment.java new file mode 100644 index 000000000..f383c818c --- /dev/null +++ b/app/src/main/java/org/linphone/settings/SettingsFragment.java @@ -0,0 +1,206 @@ +package org.linphone.settings; + +/* +SettingsFragment.java +Copyright (C) 2019 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.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.Nullable; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.Core; +import org.linphone.core.ProxyConfig; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.widget.BasicSetting; +import org.linphone.settings.widget.LedSetting; +import org.linphone.settings.widget.SettingListenerBase; +import org.linphone.utils.LinphoneUtils; + +public class SettingsFragment extends Fragment { + protected View mRootView; + private BasicSetting mTunnel, mAudio, mVideo, mCall, mChat, mNetwork, mAdvanced; + private LinearLayout mAccounts; + private TextView mAccountsHeader; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.settings, container, false); + + loadSettings(); + setListeners(); + + return mRootView; + } + + @Override + public void onResume() { + super.onResume(); + + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance().selectMenu(FragmentsAvailable.SETTINGS); + } + + updateValues(); + } + + protected void loadSettings() { + mAccounts = mRootView.findViewById(R.id.accounts_settings_list); + mAccountsHeader = mRootView.findViewById(R.id.accounts_settings_list_header); + + mTunnel = mRootView.findViewById(R.id.pref_tunnel); + + mAudio = mRootView.findViewById(R.id.pref_audio); + + mVideo = mRootView.findViewById(R.id.pref_video); + + mCall = mRootView.findViewById(R.id.pref_call); + + mChat = mRootView.findViewById(R.id.pref_chat); + + mNetwork = mRootView.findViewById(R.id.pref_network); + + mAdvanced = mRootView.findViewById(R.id.pref_advanced); + } + + protected void setListeners() { + mTunnel.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + LinphoneActivity.instance() + .displaySubSettings(new TunnelSettingsFragment()); + } + }); + + mAudio.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + LinphoneActivity.instance().displaySubSettings(new AudioSettingsFragment()); + } + }); + + mVideo.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + LinphoneActivity.instance().displaySubSettings(new VideoSettingsFragment()); + } + }); + + mCall.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + LinphoneActivity.instance().displaySubSettings(new CallSettingsFragment()); + } + }); + + mChat.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + LinphoneActivity.instance().displaySubSettings(new ChatSettingsFragment()); + } + }); + + mNetwork.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + LinphoneActivity.instance() + .displaySubSettings(new NetworkSettingsFragment()); + } + }); + + mAdvanced.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + LinphoneActivity.instance() + .displaySubSettings(new AdvancedSettingsFragment()); + } + }); + } + + protected void updateValues() { + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null) { + mTunnel.setVisibility(core.tunnelAvailable() ? View.VISIBLE : View.GONE); + initAccounts(core); + } + } + + private void initAccounts(Core core) { + mAccounts.removeAllViews(); + ProxyConfig[] proxyConfigs = core.getProxyConfigList(); + + if (proxyConfigs == null || proxyConfigs.length == 0) { + mAccountsHeader.setVisibility(View.GONE); + } else { + mAccountsHeader.setVisibility(View.VISIBLE); + int i = 0; + for (ProxyConfig proxyConfig : proxyConfigs) { + final LedSetting account = new LedSetting(getActivity()); + account.setTitle( + LinphoneUtils.getDisplayableAddress(proxyConfig.getIdentityAddress())); + + if (proxyConfig.equals(core.getDefaultProxyConfig())) { + account.setSubtitle(getString(R.string.default_account_flag)); + } + + switch (proxyConfig.getState()) { + case Ok: + account.setColor(LedSetting.Color.GREEN); + break; + case Failed: + account.setColor(LedSetting.Color.RED); + break; + case Progress: + account.setColor(LedSetting.Color.ORANGE); + break; + case None: + case Cleared: + account.setColor(LedSetting.Color.GRAY); + break; + } + + final int accountIndex = i; + account.setListener( + new SettingListenerBase() { + @Override + public void onClicked() { + LinphoneActivity.instance().displayAccountSettings(accountIndex); + } + }); + + mAccounts.addView(account); + i += 1; + } + } + } +} diff --git a/app/src/main/java/org/linphone/settings/TunnelSettingsFragment.java b/app/src/main/java/org/linphone/settings/TunnelSettingsFragment.java new file mode 100644 index 000000000..a64027201 --- /dev/null +++ b/app/src/main/java/org/linphone/settings/TunnelSettingsFragment.java @@ -0,0 +1,118 @@ +package org.linphone.settings; + +/* +TunnelSettingsFragment.java +Copyright (C) 2019 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.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import org.linphone.LinphoneActivity; +import org.linphone.R; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.widget.ListSetting; +import org.linphone.settings.widget.SettingListenerBase; +import org.linphone.settings.widget.TextSetting; + +public class TunnelSettingsFragment extends Fragment { + protected View mRootView; + protected LinphonePreferences mPrefs; + + private TextSetting mHost, mPort; + private ListSetting mMode; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.settings_tunnel, container, false); + + loadSettings(); + + return mRootView; + } + + @Override + public void onResume() { + super.onResume(); + + mPrefs = LinphonePreferences.instance(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .selectMenu( + FragmentsAvailable.SETTINGS_SUBLEVEL, + getString(R.string.pref_tunnel_title)); + } + + updateValues(); + } + + protected void loadSettings() { + mHost = mRootView.findViewById(R.id.pref_tunnel_host); + + mPort = mRootView.findViewById(R.id.pref_tunnel_port); + mPort.setInputType(InputType.TYPE_CLASS_NUMBER); + + mMode = mRootView.findViewById(R.id.pref_tunnel_mode); + } + + protected void setListeners() { + mHost.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + mPrefs.setTunnelHost(newValue); + } + }); + + mPort.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + try { + mPrefs.setTunnelPort(Integer.valueOf(newValue)); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + }); + + mMode.setListener( + new SettingListenerBase() { + @Override + public void onListValueChanged(int position, String newLabel, String newValue) { + mPrefs.setTunnelMode(newValue); + } + }); + } + + protected void updateValues() { + mHost.setValue(mPrefs.getTunnelHost()); + + mPort.setValue(mPrefs.getTunnelPort()); + + mMode.setValue(mPrefs.getTunnelMode()); + + setListeners(); + } +} diff --git a/app/src/main/java/org/linphone/settings/VideoSettingsFragment.java b/app/src/main/java/org/linphone/settings/VideoSettingsFragment.java new file mode 100644 index 000000000..3cba62a4c --- /dev/null +++ b/app/src/main/java/org/linphone/settings/VideoSettingsFragment.java @@ -0,0 +1,278 @@ +package org.linphone.settings; + +/* +VideoSettingsFragment.java +Copyright (C) 2019 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.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.R; +import org.linphone.core.Core; +import org.linphone.core.Factory; +import org.linphone.core.PayloadType; +import org.linphone.core.VideoDefinition; +import org.linphone.core.tools.Log; +import org.linphone.fragments.FragmentsAvailable; +import org.linphone.settings.widget.ListSetting; +import org.linphone.settings.widget.SettingListenerBase; +import org.linphone.settings.widget.SwitchSetting; +import org.linphone.settings.widget.TextSetting; + +public class VideoSettingsFragment extends Fragment { + protected View mRootView; + protected LinphonePreferences mPrefs; + + private SwitchSetting mEnable, mAutoInitiate, mAutoAccept, mOverlay; + private ListSetting mPreset, mSize, mFps; + private TextSetting mBandwidth; + private LinearLayout mVideoCodecs; + private TextView mVideoCodecsHeader; + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.settings_video, container, false); + + loadSettings(); + + return mRootView; + } + + @Override + public void onResume() { + super.onResume(); + + mPrefs = LinphonePreferences.instance(); + if (LinphoneActivity.isInstanciated()) { + LinphoneActivity.instance() + .selectMenu( + FragmentsAvailable.SETTINGS_SUBLEVEL, + getString(R.string.pref_video_title)); + } + + updateValues(); + } + + protected void loadSettings() { + mEnable = mRootView.findViewById(R.id.pref_video_enable); + + mAutoInitiate = mRootView.findViewById(R.id.pref_video_initiate_call_with_video); + + mAutoAccept = mRootView.findViewById(R.id.pref_video_automatically_accept_video); + + mOverlay = mRootView.findViewById(R.id.pref_overlay); + + mPreset = mRootView.findViewById(R.id.pref_video_preset); + + mSize = mRootView.findViewById(R.id.pref_preferred_video_size); + initVideoSizeList(); + + mFps = mRootView.findViewById(R.id.pref_preferred_fps); + initFpsList(); + + mBandwidth = mRootView.findViewById(R.id.pref_bandwidth_limit); + mBandwidth.setInputType(InputType.TYPE_CLASS_NUMBER); + + mVideoCodecs = mRootView.findViewById(R.id.pref_video_codecs); + mVideoCodecsHeader = mRootView.findViewById(R.id.pref_video_codecs_header); + } + + protected void setListeners() { + mEnable.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.enableVideo(newValue); + if (!newValue) { + mAutoAccept.setChecked(false); + mAutoInitiate.setChecked(false); + } + updateVideoSettingsVisibility(newValue); + } + }); + + mAutoInitiate.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setInitiateVideoCall(newValue); + } + }); + + mAutoAccept.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.setAutomaticallyAcceptVideoRequests(newValue); + } + }); + + mOverlay.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + mPrefs.enableOverlay(newValue); + } + }); + + mPreset.setListener( + new SettingListenerBase() { + @Override + public void onListValueChanged(int position, String newLabel, String newValue) { + mPrefs.setVideoPreset(newValue); + mFps.setVisibility(newValue.equals("custom") ? View.VISIBLE : View.GONE); + mBandwidth.setVisibility( + newValue.equals("custom") ? View.VISIBLE : View.GONE); + } + }); + + mSize.setListener( + new SettingListenerBase() { + @Override + public void onListValueChanged(int position, String newLabel, String newValue) { + mPrefs.setPreferredVideoSize(newValue); + } + }); + + mFps.setListener( + new SettingListenerBase() { + @Override + public void onListValueChanged(int position, String newLabel, String newValue) { + try { + mPrefs.setPreferredVideoFps(Integer.valueOf(newValue)); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + }); + + mBandwidth.setListener( + new SettingListenerBase() { + @Override + public void onTextValueChanged(String newValue) { + try { + mPrefs.setBandwidthLimit(Integer.valueOf(newValue)); + } catch (NumberFormatException nfe) { + Log.e(nfe); + } + } + }); + } + + protected void updateValues() { + mEnable.setChecked(mPrefs.isVideoEnabled()); + updateVideoSettingsVisibility(mPrefs.isVideoEnabled()); + + mAutoInitiate.setChecked(mPrefs.shouldInitiateVideoCall()); + + mAutoAccept.setChecked(mPrefs.shouldAutomaticallyAcceptVideoRequests()); + + mOverlay.setChecked(mPrefs.isOverlayEnabled()); + + mBandwidth.setValue(mPrefs.getBandwidthLimit()); + mBandwidth.setVisibility( + mPrefs.getVideoPreset().equals("custom") ? View.VISIBLE : View.GONE); + + mPreset.setValue(mPrefs.getVideoPreset()); + + mSize.setValue(mPrefs.getPreferredVideoSize()); + + mFps.setValue(mPrefs.getPreferredVideoFps()); + mFps.setVisibility(mPrefs.getVideoPreset().equals("custom") ? View.VISIBLE : View.GONE); + + populateVideoCodecs(); + + setListeners(); + } + + private void initVideoSizeList() { + List entries = new ArrayList<>(); + List values = new ArrayList<>(); + + for (VideoDefinition vd : Factory.instance().getSupportedVideoDefinitions()) { + entries.add(vd.getName()); + values.add(vd.getName()); + } + + mSize.setItems(entries, values); + } + + private void initFpsList() { + List entries = new ArrayList<>(); + List values = new ArrayList<>(); + + entries.add(getString(R.string.pref_none)); + values.add("0"); + for (int i = 5; i <= 30; i += 5) { + String str = Integer.toString(i); + entries.add(str); + values.add(str); + } + + mFps.setItems(entries, values); + } + + private void populateVideoCodecs() { + mVideoCodecs.removeAllViews(); + Core core = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (core != null) { + for (final PayloadType pt : core.getVideoPayloadTypes()) { + final SwitchSetting codec = new SwitchSetting(getActivity()); + codec.setTitle(pt.getMimeType()); + + if (pt.enabled()) { + // Never use codec.setChecked(pt.enabled) ! + codec.setChecked(true); + } + codec.setListener( + new SettingListenerBase() { + @Override + public void onBoolValueChanged(boolean newValue) { + pt.enable(newValue); + } + }); + + mVideoCodecs.addView(codec); + } + } + } + + private void updateVideoSettingsVisibility(boolean show) { + mAutoInitiate.setVisibility(show ? View.VISIBLE : View.GONE); + mAutoAccept.setVisibility(show ? View.VISIBLE : View.GONE); + mOverlay.setVisibility(show ? View.VISIBLE : View.GONE); + mBandwidth.setVisibility(show ? View.VISIBLE : View.GONE); + mPreset.setVisibility(show ? View.VISIBLE : View.GONE); + mSize.setVisibility(show ? View.VISIBLE : View.GONE); + mFps.setVisibility(show ? View.VISIBLE : View.GONE); + mVideoCodecs.setVisibility(show ? View.VISIBLE : View.GONE); + mVideoCodecsHeader.setVisibility(show ? View.VISIBLE : View.GONE); + } +} diff --git a/app/src/main/java/org/linphone/settings/widget/BasicSetting.java b/app/src/main/java/org/linphone/settings/widget/BasicSetting.java new file mode 100644 index 000000000..99191a3e9 --- /dev/null +++ b/app/src/main/java/org/linphone/settings/widget/BasicSetting.java @@ -0,0 +1,127 @@ +package org.linphone.settings.widget; + +/* +BasicSetting.java +Copyright (C) 2019 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.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.annotation.Nullable; +import org.linphone.R; + +public class BasicSetting extends LinearLayout { + protected Context mContext; + protected View mView; + protected TextView mTitle, mSubtitle; + protected SettingListener mListener; + + public BasicSetting(Context context) { + super(context); + mContext = context; + init(null, 0, 0); + } + + public BasicSetting(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mContext = context; + init(attrs, 0, 0); + } + + public BasicSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + mContext = context; + init(attrs, defStyleAttr, 0); + } + + public BasicSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + mContext = context; + init(attrs, defStyleAttr, defStyleRes); + } + + protected void inflateView() { + mView = LayoutInflater.from(mContext).inflate(R.layout.settings_widget_basic, this, true); + } + + public void setListener(SettingListener listener) { + mListener = listener; + } + + protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + inflateView(); + + mTitle = mView.findViewById(R.id.setting_title); + mSubtitle = mView.findViewById(R.id.setting_subtitle); + + RelativeLayout rlayout = mView.findViewById(R.id.setting_layout); + rlayout.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + if (mTitle.isEnabled() && mListener != null) { + mListener.onClicked(); + } + } + }); + + if (attrs != null) { + TypedArray a = + mContext.getTheme() + .obtainStyledAttributes( + attrs, R.styleable.Settings, defStyleAttr, defStyleRes); + try { + String title = a.getString(R.styleable.Settings_title); + if (title != null) { + mTitle.setText(title); + } else { + mTitle.setVisibility(GONE); + } + + String subtitle = a.getString(R.styleable.Settings_subtitle); + if (subtitle != null) { + mSubtitle.setText(subtitle); + } else { + mSubtitle.setVisibility(GONE); + } + } finally { + a.recycle(); + } + } + } + + public void setTitle(String title) { + mTitle.setText(title); + mTitle.setVisibility(title == null || title.isEmpty() ? GONE : VISIBLE); + } + + public void setSubtitle(String subtitle) { + mSubtitle.setText(subtitle); + mSubtitle.setVisibility(subtitle == null || subtitle.isEmpty() ? GONE : VISIBLE); + } + + public void setEnabled(boolean enabled) { + mTitle.setEnabled(enabled); + mSubtitle.setEnabled(enabled); + } +} diff --git a/app/src/main/java/org/linphone/settings/widget/CheckBoxSetting.java b/app/src/main/java/org/linphone/settings/widget/CheckBoxSetting.java new file mode 100644 index 000000000..253a8ac41 --- /dev/null +++ b/app/src/main/java/org/linphone/settings/widget/CheckBoxSetting.java @@ -0,0 +1,99 @@ +package org.linphone.settings.widget; + +/* +CheckBoxSetting.java +Copyright (C) 2019 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.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.RelativeLayout; +import androidx.annotation.Nullable; +import org.linphone.R; + +public class CheckBoxSetting extends BasicSetting { + protected CheckBox mCheckBox; + + public CheckBoxSetting(Context context) { + super(context); + } + + public CheckBoxSetting(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public CheckBoxSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CheckBoxSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + protected void inflateView() { + mView = + LayoutInflater.from(mContext) + .inflate(R.layout.settings_widget_checkbox, this, true); + } + + protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super.init(attrs, defStyleAttr, defStyleRes); + + mCheckBox = mView.findViewById(R.id.setting_checkbox); + mCheckBox.setOnCheckedChangeListener( + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (mListener != null) { + mListener.onBoolValueChanged(isChecked); + } + } + }); + + RelativeLayout rlayout = mView.findViewById(R.id.setting_layout); + rlayout.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + if (mCheckBox.isEnabled()) { + toggle(); + } + } + }); + } + + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mCheckBox.setEnabled(enabled); + } + + public void setChecked(boolean checked) { + mCheckBox.setChecked(checked); + } + + public boolean isChecked() { + return mCheckBox.isChecked(); + } + + public void toggle() { + mCheckBox.toggle(); + } +} diff --git a/app/src/main/java/org/linphone/settings/widget/LedSetting.java b/app/src/main/java/org/linphone/settings/widget/LedSetting.java new file mode 100644 index 000000000..182672257 --- /dev/null +++ b/app/src/main/java/org/linphone/settings/widget/LedSetting.java @@ -0,0 +1,81 @@ +package org.linphone.settings.widget; + +/* +LedSetting.java +Copyright (C) 2019 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.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.ImageView; +import androidx.annotation.Nullable; +import org.linphone.R; + +public class LedSetting extends BasicSetting { + public enum Color { + GRAY, + GREEN, + ORANGE, + RED + } + + protected ImageView mLed; + + public LedSetting(Context context) { + super(context); + } + + public LedSetting(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public LedSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public LedSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + protected void inflateView() { + mView = LayoutInflater.from(mContext).inflate(R.layout.settings_widget_led, this, true); + } + + protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super.init(attrs, defStyleAttr, defStyleRes); + + mLed = mView.findViewById(R.id.setting_led); + } + + public void setColor(Color color) { + switch (color) { + case GRAY: + mLed.setImageResource(R.drawable.led_disconnected); + break; + case GREEN: + mLed.setImageResource(R.drawable.led_connected); + break; + case ORANGE: + mLed.setImageResource(R.drawable.led_inprogress); + break; + case RED: + mLed.setImageResource(R.drawable.led_error); + break; + } + } +} diff --git a/app/src/main/java/org/linphone/settings/widget/ListSetting.java b/app/src/main/java/org/linphone/settings/widget/ListSetting.java new file mode 100644 index 000000000..b0633f75b --- /dev/null +++ b/app/src/main/java/org/linphone/settings/widget/ListSetting.java @@ -0,0 +1,140 @@ +package org.linphone.settings.widget; + +/* +ListSetting.java +Copyright (C) 2019 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.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Spinner; +import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import org.linphone.R; + +public class ListSetting extends BasicSetting implements AdapterView.OnItemSelectedListener { + protected Spinner mSpinner; + protected List mItems; + protected List mItemsValues; + + public ListSetting(Context context) { + super(context); + } + + public ListSetting(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public ListSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ListSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + protected void inflateView() { + mView = LayoutInflater.from(mContext).inflate(R.layout.settings_widget_list, this, true); + } + + protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super.init(attrs, defStyleAttr, defStyleRes); + mItems = new ArrayList<>(); + mItemsValues = new ArrayList<>(); + + mSpinner = mView.findViewById(R.id.setting_spinner); + mSpinner.setOnItemSelectedListener(this); + + if (attrs != null) { + TypedArray a = + mContext.getTheme() + .obtainStyledAttributes( + attrs, R.styleable.Settings, defStyleAttr, defStyleRes); + try { + CharSequence[] names = a.getTextArray(R.styleable.Settings_list_items_names); + CharSequence[] values = a.getTextArray(R.styleable.Settings_list_items_values); + if (values != null && names != null) { + for (CharSequence cs : names) { + mItems.add(cs.toString()); + } + for (CharSequence cs : values) { + mItemsValues.add(cs.toString()); + } + setItems(mItems, mItemsValues); + } + } finally { + a.recycle(); + } + } + } + + public void setItems(List list, List valuesList) { + mItems = list; + mItemsValues = valuesList; + ArrayAdapter dataAdapter = + new ArrayAdapter<>(mContext, android.R.layout.simple_spinner_item, list); + dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mSpinner.setAdapter(dataAdapter); + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if (mListener != null && position < mItems.size()) { + String itemValue = null; + if (mItemsValues != null && position < mItemsValues.size()) { + itemValue = mItemsValues.get(position); + } + mListener.onListValueChanged(position, mItems.get(position), itemValue); + } + } + + @Override + public void onNothingSelected(AdapterView parent) {} + + public void setValue(String value) { + int index = mItemsValues.indexOf(value); + if (index == -1) { + index = mItems.indexOf(value); + } + if (index != -1) { + mSpinner.setSelection(index); + } + } + + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mSpinner.setEnabled(enabled); + } + + public void setValue(int value) { + setValue(String.valueOf(value)); + } + + public void setValue(float value) { + setValue(String.valueOf(value)); + } + + public void setValue(double value) { + setValue(String.valueOf(value)); + } +} diff --git a/app/src/main/java/org/linphone/settings/widget/SettingListener.java b/app/src/main/java/org/linphone/settings/widget/SettingListener.java new file mode 100644 index 000000000..8e4f997f3 --- /dev/null +++ b/app/src/main/java/org/linphone/settings/widget/SettingListener.java @@ -0,0 +1,30 @@ +package org.linphone.settings.widget; + +/* +SettingListener.java +Copyright (C) 2019 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. +*/ + +public interface SettingListener { + void onClicked(); + + void onTextValueChanged(String newValue); + + void onBoolValueChanged(boolean newValue); + + void onListValueChanged(int position, String newLabel, String newValue); +} diff --git a/app/src/main/java/org/linphone/settings/widget/SettingListenerBase.java b/app/src/main/java/org/linphone/settings/widget/SettingListenerBase.java new file mode 100644 index 000000000..a3d567d12 --- /dev/null +++ b/app/src/main/java/org/linphone/settings/widget/SettingListenerBase.java @@ -0,0 +1,30 @@ +package org.linphone.settings.widget; + +/* +SettingListenerBase.java +Copyright (C) 2019 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. +*/ + +public class SettingListenerBase implements SettingListener { + public void onClicked() {} + + public void onTextValueChanged(String newValue) {} + + public void onBoolValueChanged(boolean newValue) {} + + public void onListValueChanged(int position, String newLabel, String newValue) {} +} diff --git a/app/src/main/java/org/linphone/settings/widget/SwitchSetting.java b/app/src/main/java/org/linphone/settings/widget/SwitchSetting.java new file mode 100644 index 000000000..f788de59a --- /dev/null +++ b/app/src/main/java/org/linphone/settings/widget/SwitchSetting.java @@ -0,0 +1,97 @@ +package org.linphone.settings.widget; + +/* +SwitchSetting.java +Copyright (C) 2019 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.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.RelativeLayout; +import android.widget.Switch; +import androidx.annotation.Nullable; +import org.linphone.R; + +public class SwitchSetting extends BasicSetting { + protected Switch mSwitch; + + public SwitchSetting(Context context) { + super(context); + } + + public SwitchSetting(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public SwitchSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SwitchSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + protected void inflateView() { + mView = LayoutInflater.from(mContext).inflate(R.layout.settings_widget_switch, this, true); + } + + protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super.init(attrs, defStyleAttr, defStyleRes); + + mSwitch = mView.findViewById(R.id.setting_switch); + mSwitch.setOnCheckedChangeListener( + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (mListener != null) { + mListener.onBoolValueChanged(isChecked); + } + } + }); + + RelativeLayout rlayout = mView.findViewById(R.id.setting_layout); + rlayout.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + if (mSwitch.isEnabled()) { + toggle(); + } + } + }); + } + + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mSwitch.setEnabled(enabled); + } + + public void setChecked(boolean checked) { + mSwitch.setChecked(checked); + } + + public boolean isChecked() { + return mSwitch.isChecked(); + } + + public void toggle() { + mSwitch.toggle(); + } +} diff --git a/app/src/main/java/org/linphone/settings/widget/TextSetting.java b/app/src/main/java/org/linphone/settings/widget/TextSetting.java new file mode 100644 index 000000000..94437b089 --- /dev/null +++ b/app/src/main/java/org/linphone/settings/widget/TextSetting.java @@ -0,0 +1,117 @@ +package org.linphone.settings.widget; + +/* +TextSetting.java +Copyright (C) 2019 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.content.Context; +import android.content.res.TypedArray; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.widget.EditText; +import androidx.annotation.Nullable; +import org.linphone.R; + +public class TextSetting extends BasicSetting implements TextWatcher { + protected EditText mInput; + + public TextSetting(Context context) { + super(context); + } + + public TextSetting(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public TextSetting(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public TextSetting(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + protected void inflateView() { + mView = LayoutInflater.from(mContext).inflate(R.layout.settings_widget_text, this, true); + } + + protected void init(@Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super.init(attrs, defStyleAttr, defStyleRes); + + mInput = mView.findViewById(R.id.setting_input); + + if (attrs != null) { + TypedArray a = + mContext.getTheme() + .obtainStyledAttributes( + attrs, R.styleable.Settings, defStyleAttr, defStyleRes); + try { + String hint = a.getString(R.styleable.Settings_hint); + mInput.setHint(hint); + } finally { + a.recycle(); + } + } + + mInput.addTextChangedListener(this); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if (mListener != null) { + mListener.onTextValueChanged(mInput.getText().toString()); + } + } + + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + mInput.setEnabled(enabled); + } + + public void setInputType(int inputType) { + mInput.setInputType(inputType); + } + + public void setValue(String value) { + mInput.setText(value); + } + + public void setValue(int value) { + setValue(String.valueOf(value)); + } + + public void setValue(float value) { + setValue(String.valueOf(value)); + } + + public void setValue(double value) { + setValue(String.valueOf(value)); + } + + public String getValue() { + return mInput.getText().toString(); + } +} diff --git a/src/android/org/linphone/sync/AuthenticationService.java b/app/src/main/java/org/linphone/sync/AuthenticationService.java similarity index 99% rename from src/android/org/linphone/sync/AuthenticationService.java rename to app/src/main/java/org/linphone/sync/AuthenticationService.java index ca791026d..6bb0eb778 100644 --- a/src/android/org/linphone/sync/AuthenticationService.java +++ b/app/src/main/java/org/linphone/sync/AuthenticationService.java @@ -19,7 +19,6 @@ 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.Service; import android.content.Intent; import android.os.IBinder; @@ -37,4 +36,4 @@ public class AuthenticationService extends Service { public IBinder onBind(Intent intent) { return mAuthenticator.getIBinder(); } -} \ No newline at end of file +} diff --git a/src/android/org/linphone/sync/Authenticator.java b/app/src/main/java/org/linphone/sync/Authenticator.java similarity index 64% rename from src/android/org/linphone/sync/Authenticator.java rename to app/src/main/java/org/linphone/sync/Authenticator.java index c5b9cfb53..3d04fde20 100644 --- a/src/android/org/linphone/sync/Authenticator.java +++ b/app/src/main/java/org/linphone/sync/Authenticator.java @@ -22,46 +22,35 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. import android.accounts.AbstractAccountAuthenticator; import android.accounts.Account; import android.accounts.AccountAuthenticatorResponse; -import android.accounts.NetworkErrorException; import android.content.Context; import android.os.Bundle; -public class Authenticator extends AbstractAccountAuthenticator { +class Authenticator extends AbstractAccountAuthenticator { public Authenticator(Context context) { super(context); } @Override - public Bundle editProperties( - AccountAuthenticatorResponse r, String s) { + public Bundle editProperties(AccountAuthenticatorResponse r, String s) { throw new UnsupportedOperationException(); } @Override public Bundle addAccount( - AccountAuthenticatorResponse r, - String s, - String s2, - String[] strings, - Bundle bundle) throws NetworkErrorException { + AccountAuthenticatorResponse r, String s, String s2, String[] strings, Bundle bundle) { return null; } @Override public Bundle confirmCredentials( - AccountAuthenticatorResponse r, - Account account, - Bundle bundle) throws NetworkErrorException { + AccountAuthenticatorResponse r, Account account, Bundle bundle) { return null; } @Override public Bundle getAuthToken( - AccountAuthenticatorResponse r, - Account account, - String s, - Bundle bundle) throws NetworkErrorException { + AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) { throw new UnsupportedOperationException(); } @@ -72,16 +61,12 @@ public class Authenticator extends AbstractAccountAuthenticator { @Override public Bundle updateCredentials( - AccountAuthenticatorResponse r, - Account account, - String s, Bundle bundle) throws NetworkErrorException { + AccountAuthenticatorResponse r, Account account, String s, Bundle bundle) { throw new UnsupportedOperationException(); } @Override - public Bundle hasFeatures( - AccountAuthenticatorResponse r, - Account account, String[] strings) throws NetworkErrorException { + public Bundle hasFeatures(AccountAuthenticatorResponse r, Account account, String[] strings) { throw new UnsupportedOperationException(); } -} \ No newline at end of file +} diff --git a/src/android/org/linphone/sync/SyncAdapter.java b/app/src/main/java/org/linphone/sync/SyncAdapter.java similarity index 81% rename from src/android/org/linphone/sync/SyncAdapter.java rename to app/src/main/java/org/linphone/sync/SyncAdapter.java index cb18046b9..c41553f34 100755 --- a/src/android/org/linphone/sync/SyncAdapter.java +++ b/app/src/main/java/org/linphone/sync/SyncAdapter.java @@ -26,15 +26,17 @@ import android.content.Context; import android.content.SyncResult; import android.os.Bundle; -public class SyncAdapter extends AbstractThreadedSyncAdapter { +class SyncAdapter extends AbstractThreadedSyncAdapter { public SyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); } @Override - public void onPerformSync(Account account, Bundle extras, String authority, - ContentProviderClient provider, SyncResult syncResult) { - } + public void onPerformSync( + Account account, + Bundle extras, + String authority, + ContentProviderClient provider, + SyncResult syncResult) {} } - diff --git a/src/android/org/linphone/sync/SyncService.java b/app/src/main/java/org/linphone/sync/SyncService.java similarity index 99% rename from src/android/org/linphone/sync/SyncService.java rename to app/src/main/java/org/linphone/sync/SyncService.java index 483297a5f..b3d6e7ddc 100755 --- a/src/android/org/linphone/sync/SyncService.java +++ b/app/src/main/java/org/linphone/sync/SyncService.java @@ -19,14 +19,13 @@ 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.Service; import android.content.Intent; import android.os.IBinder; public class SyncService extends Service { - private static SyncAdapter sSyncAdapter = null; private static final Object sSyncAdapterLock = new Object(); + private static SyncAdapter sSyncAdapter = null; @Override public void onCreate() { diff --git a/app/src/main/java/org/linphone/utils/DeviceUtils.java b/app/src/main/java/org/linphone/utils/DeviceUtils.java new file mode 100644 index 000000000..b7855ee21 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/DeviceUtils.java @@ -0,0 +1,224 @@ +package org.linphone.utils; + +/* +DeviceUtils.java +Copyright (C) 2018 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.Dialog; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.TextView; +import androidx.core.content.ContextCompat; +import java.util.List; +import org.linphone.R; +import org.linphone.compatibility.Compatibility; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; + +public class DeviceUtils { + private static final Intent[] POWERMANAGER_INTENTS = { + new Intent() + .setComponent( + new ComponentName( + "com.miui.securitycenter", + "com.miui.permcenter.autostart.AutoStartManagementActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.letv.android.letvsafe", + "com.letv.android.letvsafe.AutobootManageActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.huawei.systemmanager", + "com.huawei.systemmanager.optimize.process.ProtectActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.huawei.systemmanager", + "com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.coloros.safecenter", + "com.coloros.safecenter.permission.startup.StartupAppListActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.coloros.safecenter", + "com.coloros.safecenter.startupapp.StartupAppListActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.oppo.safe", + "com.oppo.safe.permission.startup.StartupAppListActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.iqoo.secure", + "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.iqoo.secure", + "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager")), + new Intent() + .setComponent( + new ComponentName( + "com.vivo.permissionmanager", + "com.vivo.permissionmanager.activity.BgStartUpManagerActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.samsung.android.lool", + "com.samsung.android.sm.ui.battery.BatteryActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.htc.pitroad", + "com.htc.pitroad.landingpage.activity.LandingPageActivity")), + new Intent() + .setComponent( + new ComponentName( + "com.asus.mobilemanager", "com.asus.mobilemanager.MainActivity")) + }; + + public static Intent getDevicePowerManagerIntent(Context context) { + for (Intent intent : POWERMANAGER_INTENTS) { + if (DeviceUtils.isIntentCallable(context, intent)) { + return intent; + } + } + return null; + } + + public static boolean hasDevicePowerManager(Context context) { + return getDevicePowerManagerIntent(context) != null; + } + + public static boolean isAppUserRestricted(Context context) { + return Compatibility.isAppUserRestricted(context); + } + + public static int getAppStandbyBucket(Context context) { + return Compatibility.getAppStandbyBucket(context); + } + + public static void displayDialogIfDeviceHasPowerManagerThatCouldPreventPushNotifications( + final Context context) { + for (final Intent intent : POWERMANAGER_INTENTS) { + if (DeviceUtils.isIntentCallable(context, intent)) { + Log.w( + "[Hacks] " + + android.os.Build.MANUFACTURER + + " device with power saver detected !"); + if (!LinphonePreferences.instance().hasPowerSaverDialogBeenPrompted()) { + Log.w("[Hacks] Asking power saver for whitelist !"); + + final Dialog dialog = new Dialog(context); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + Drawable d = + new ColorDrawable( + ContextCompat.getColor(context, R.color.dark_grey_color)); + 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 = dialog.findViewById(R.id.dialog_message); + customText.setText(R.string.device_power_saver_dialog_message); + + TextView customTitle = dialog.findViewById(R.id.dialog_title); + customTitle.setText(R.string.device_power_saver_dialog_title); + + dialog.findViewById(R.id.dialog_do_not_ask_again_layout) + .setVisibility(View.VISIBLE); + final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain); + dialog.findViewById(R.id.doNotAskAgainLabel) + .setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + doNotAskAgain.setChecked(!doNotAskAgain.isChecked()); + } + }); + + Button accept = dialog.findViewById(R.id.dialog_ok_button); + accept.setVisibility(View.VISIBLE); + accept.setText(R.string.device_power_saver_dialog_button_go_to_settings); + accept.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + Log.w( + "[Hacks] Power saver detected, user is going to settings :)"); + if (doNotAskAgain.isChecked()) { + LinphonePreferences.instance() + .powerSaverDialogPrompted(true); + } + + context.startActivity(intent); + dialog.dismiss(); + } + }); + + Button cancel = dialog.findViewById(R.id.dialog_cancel_button); + cancel.setText(R.string.device_power_saver_dialog_button_later); + cancel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + Log.w( + "[Hacks] Power saver detected, user didn't go to settings :("); + if (doNotAskAgain.isChecked()) { + LinphonePreferences.instance() + .powerSaverDialogPrompted(true); + } + dialog.dismiss(); + } + }); + + Button delete = dialog.findViewById(R.id.dialog_delete_button); + delete.setVisibility(View.GONE); + + dialog.show(); + } + } + } + } + + private static boolean isIntentCallable(Context context, Intent intent) { + List list = + context.getPackageManager() + .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + return list.size() > 0; + } +} diff --git a/app/src/main/java/org/linphone/utils/FileUtils.java b/app/src/main/java/org/linphone/utils/FileUtils.java new file mode 100644 index 000000000..8a636c546 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/FileUtils.java @@ -0,0 +1,283 @@ +package org.linphone.utils; + +/* +FileUtils.java +Copyright (C) 2018 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.annotation.SuppressLint; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.text.TextUtils; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import org.linphone.LinphoneManager; +import org.linphone.core.Address; +import org.linphone.core.ChatMessage; +import org.linphone.core.Content; +import org.linphone.core.Friend; +import org.linphone.core.FriendList; +import org.linphone.core.tools.Log; + +public class FileUtils { + public static String getNameFromFilePath(String filePath) { + String name = filePath; + int i = filePath.lastIndexOf('/'); + if (i > 0) { + name = filePath.substring(i + 1); + } + return name; + } + + public static String getExtensionFromFileName(String fileName) { + String extension = null; + int i = fileName.lastIndexOf('.'); + if (i > 0) { + extension = fileName.substring(i + 1); + } + return extension; + } + + public static Boolean isExtensionImage(String path) { + String extension = getExtensionFromFileName(path); + if (extension != null) extension = extension.toLowerCase(); + return (extension != null && extension.matches("(png|jpg|jpeg|bmp|gif)")); + } + + public static void recursiveFileRemoval(File root) { + if (!root.delete()) { + if (root.isDirectory()) { + File[] files = root.listFiles(); + if (files != null) { + for (File f : files) { + recursiveFileRemoval(f); + } + } + } + } + } + + public static String getFilePath(final Context context, final Uri uri) { + if (uri == null) return null; + + String result = null; + String name = getNameFromUri(uri, context); + + try { + File localFile = createFile(context, name); + InputStream remoteFile = context.getContentResolver().openInputStream(uri); + + if (copyToFile(remoteFile, localFile)) { + result = localFile.getAbsolutePath(); + } + + remoteFile.close(); + } catch (IOException e) { + Log.e("Enable to get sharing file", e); + } + + return result; + } + + private static String getNameFromUri(Uri uri, Context context) { + String name = null; + if (uri.getScheme().equals("content")) { + Cursor returnCursor = context.getContentResolver().query(uri, null, null, null, null); + if (returnCursor != null) { + returnCursor.moveToFirst(); + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + name = returnCursor.getString(nameIndex); + returnCursor.close(); + } + } else if (uri.getScheme().equals("file")) { + name = uri.getLastPathSegment(); + } + return name; + } + + /** + * Copy data from a source stream to destFile. Return true if succeed, return false if failed. + */ + private static boolean copyToFile(InputStream inputStream, File destFile) { + if (inputStream == null || destFile == null) return false; + try { + try (OutputStream out = new FileOutputStream(destFile)) { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) >= 0) { + out.write(buffer, 0, bytesRead); + } + } + return true; + } catch (IOException e) { + return false; + } + } + + private static File createFile(Context context, String fileName) { + if (TextUtils.isEmpty(fileName)) fileName = getStartDate(); + + if (!fileName.contains(".")) { + fileName = fileName + ".unknown"; + } + + final File root; + root = context.getExternalCacheDir(); + + if (root != null && !root.exists()) root.mkdirs(); + return new File(root, fileName); + } + + public static Uri getCVSPathFromLookupUri(String content) { + String contactId = getNameFromFilePath(content); + FriendList[] friendList = LinphoneManager.getLc().getFriendsLists(); + for (FriendList list : friendList) { + for (Friend friend : list.getFriends()) { + if (friend.getRefKey().equals(contactId)) { + String contactVcard = friend.getVcard().asVcard4String(); + return createCvsFromString(contactVcard); + } + } + } + return null; + } + + public static String getRealPathFromURI(Context context, Uri contentUri) { + String[] proj = {MediaStore.Images.Media.DATA}; + Cursor cursor = context.getContentResolver().query(contentUri, proj, null, null, null); + 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; + } + + public static String getStorageDirectory(Context mContext) { + String storageDir = + Environment.getExternalStorageDirectory() + + "/" + + mContext.getString( + mContext.getResources() + .getIdentifier( + "app_name", "string", mContext.getPackageName())); + File file = new File(storageDir); + if (!file.isDirectory() || !file.exists()) { + Log.w("Directory " + file + " doesn't seem to exists yet, let's create it"); + file.mkdirs(); + LinphoneManager.getInstance().getMediaScanner().scanFile(file, null); + } + return storageDir; + } + + public static String getRecordingsDirectory(Context mContext) { + String recordingsDir = + Environment.getExternalStorageDirectory() + + "/" + + mContext.getString( + mContext.getResources() + .getIdentifier( + "app_name", "string", mContext.getPackageName())) + + "/recordings"; + File file = new File(recordingsDir); + if (!file.isDirectory() || !file.exists()) { + Log.w("Directory " + file + " doesn't seem to exists yet, let's create it"); + file.mkdirs(); + LinphoneManager.getInstance().getMediaScanner().scanFile(file, null); + } + return recordingsDir; + } + + @SuppressLint("SimpleDateFormat") + public static String getCallRecordingFilename(Context context, Address address) { + String fileName = getRecordingsDirectory(context) + "/"; + + String name = + address.getDisplayName() == null ? address.getUsername() : address.getDisplayName(); + fileName += name + "_"; + + DateFormat format = new SimpleDateFormat("dd-MM-yyyy-HH-mm-ss"); + fileName += format.format(new Date()) + ".mkv"; + + return fileName; + } + + public static void scanFile(ChatMessage message) { + String appData = message.getAppdata(); + if (appData == null) { + for (Content c : message.getContents()) { + if (c.isFile()) { + appData = c.getFilePath(); + } + } + } + LinphoneManager.getInstance().getMediaScanner().scanFile(new File(appData), null); + } + + private static Uri createCvsFromString(String vcardString) { + String contactName = getContactNameFromVcard(vcardString); + File vcfFile = new File(Environment.getExternalStorageDirectory(), contactName + ".cvs"); + try { + FileWriter fw = new FileWriter(vcfFile); + fw.write(vcardString); + fw.close(); + return Uri.fromFile(vcfFile); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + private static String getContactNameFromVcard(String vcard) { + if (vcard != null) { + String contactName = vcard.substring(vcard.indexOf("FN:") + 3); + contactName = contactName.substring(0, contactName.indexOf("\n") - 1); + contactName = contactName.replace(";", ""); + contactName = contactName.replace(" ", ""); + return contactName; + } + return null; + } + + private static String getStartDate() { + try { + return new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ROOT).format(new Date()); + } catch (RuntimeException e) { + return new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); + } + } + + public static String getMimeFromFile(String path) { + if (isExtensionImage(path)) { + return "image/" + getExtensionFromFileName(path); + } + return "file/" + getExtensionFromFileName(path); + } +} diff --git a/app/src/main/java/org/linphone/utils/ImageUtils.java b/app/src/main/java/org/linphone/utils/ImageUtils.java new file mode 100644 index 000000000..fa2b7a459 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/ImageUtils.java @@ -0,0 +1,92 @@ +package org.linphone.utils; + +/* +ImageUtils.java +Copyright (C) 2018 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.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.net.Uri; +import android.provider.MediaStore; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import org.linphone.R; + +public class ImageUtils { + + public static Bitmap getRoundBitmapFromUri(Context context, Uri fromPictureUri) { + Bitmap bm; + Bitmap roundBm; + if (fromPictureUri != null) { + try { + bm = + MediaStore.Images.Media.getBitmap( + context.getContentResolver(), fromPictureUri); + } catch (Exception e) { + bm = BitmapFactory.decodeResource(context.getResources(), R.drawable.topbar_avatar); + } + } else { + bm = BitmapFactory.decodeResource(context.getResources(), R.drawable.topbar_avatar); + } + if (bm != null) { + roundBm = getRoundBitmap(bm); + if (roundBm != null) { + bm.recycle(); + bm = roundBm; + } + } + return bm; + } + + private static Bitmap getRoundBitmap(Bitmap bitmap) { + Bitmap output = + Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + final int color = 0xff424242; + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + canvas.drawCircle( + bitmap.getWidth() / 2, bitmap.getHeight() / 2, bitmap.getWidth() / 2, paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + + return output; + } + + public static float dpToPixels(Context context, float dp) { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); + } + + public static float pixelsToDp(Context context, float pixels) { + return pixels + / ((float) context.getResources().getDisplayMetrics().densityDpi + / DisplayMetrics.DENSITY_DEFAULT); + } +} diff --git a/app/src/main/java/org/linphone/utils/IntentUtils.java b/app/src/main/java/org/linphone/utils/IntentUtils.java new file mode 100644 index 000000000..6bb9a661e --- /dev/null +++ b/app/src/main/java/org/linphone/utils/IntentUtils.java @@ -0,0 +1,101 @@ +package org.linphone.utils; + +/* +IntentUtils.java +Copyright (C) 2018 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.content.Context; +import android.content.Intent; +import android.net.Uri; +import java.util.ArrayList; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.contacts.ContactsManager; +import org.linphone.core.tools.Log; + +public class IntentUtils { + private static final String ACTION_CALL_LINPHONE = "org.linphone.intent.action.CallLaunched"; + + public static void handleIntent(Context context, Intent intent) { + if (intent == null) return; + + Intent newIntent = new Intent(context, LinphoneActivity.class); + String stringFileShared; + String stringUriFileShared; + Uri fileUri; + String addressToCall; + + String action = intent.getAction(); + String type = intent.getType(); + newIntent.setData(intent.getData()); + + if (Intent.ACTION_SEND.equals(action) && type != null) { + if (("text/plain").equals(type) && intent.getStringExtra(Intent.EXTRA_TEXT) != null) { + stringFileShared = intent.getStringExtra(Intent.EXTRA_TEXT); + newIntent.putExtra("msgShared", stringFileShared); + Log.i("[Intent Utils] ACTION_SEND with text/plain data: " + stringFileShared); + } else { + fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + stringUriFileShared = FileUtils.getFilePath(context, fileUri); + newIntent.putExtra("fileShared", stringUriFileShared); + Log.i("[Intent Utils] ACTION_SEND with file: " + stringUriFileShared); + } + } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null) { + if (type.startsWith("image/")) { + ArrayList imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + String filePaths = ""; + for (Uri uri : imageUris) { + filePaths += FileUtils.getFilePath(context, uri); + filePaths += ":"; + } + newIntent.putExtra("fileShared", filePaths); + Log.i("[Intent Utils] ACTION_SEND_MULTIPLE with files: " + filePaths); + } + } else if (ACTION_CALL_LINPHONE.equals(action) + && (intent.getStringExtra("NumberToCall") != null)) { + String numberToCall = intent.getStringExtra("NumberToCall"); + Log.i("[Intent Utils] ACTION_CALL_LINPHONE with number: " + numberToCall); + LinphoneManager.getInstance().newOutgoingCall(numberToCall, null); + } else if (Intent.ACTION_CALL.equals(action)) { + if (intent.getData() != null) { + addressToCall = intent.getData().toString(); + addressToCall = addressToCall.replace("%40", "@"); + addressToCall = addressToCall.replace("%3A", ":"); + if (addressToCall.startsWith("sip:")) { + addressToCall = addressToCall.substring("sip:".length()); + } else if (addressToCall.startsWith("tel:")) { + addressToCall = addressToCall.substring("tel:".length()); + } + Log.i("[Intent Utils] ACTION_CALL with number: " + addressToCall); + newIntent.putExtra("SipUriOrNumber", addressToCall); + } + } else if (Intent.ACTION_VIEW.equals(action)) { + addressToCall = + ContactsManager.getInstance() + .getAddressOrNumberForAndroidContact( + context.getContentResolver(), intent.getData()); + newIntent.putExtra("SipUriOrNumber", addressToCall); + Log.i("[Intent Utils] ACTION_VIEW with number: " + addressToCall); + } else { + Log.i("[Intent Utils] Unknown action [" + action + "], skipping"); + return; + } + + context.startActivity(newIntent); + } +} diff --git a/src/android/org/linphone/activities/LinphoneGenericActivity.java b/app/src/main/java/org/linphone/utils/LinphoneGenericActivity.java similarity index 90% rename from src/android/org/linphone/activities/LinphoneGenericActivity.java rename to app/src/main/java/org/linphone/utils/LinphoneGenericActivity.java index e55625741..73925d363 100644 --- a/src/android/org/linphone/activities/LinphoneGenericActivity.java +++ b/app/src/main/java/org/linphone/utils/LinphoneGenericActivity.java @@ -1,4 +1,4 @@ -package org.linphone.activities; +package org.linphone.utils; /* LinphoneGenericActivity.java @@ -19,13 +19,12 @@ 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.Activity; import android.os.Bundle; - +import org.linphone.LinphoneLauncherActivity; import org.linphone.LinphoneManager; import org.linphone.LinphoneService; -public class LinphoneGenericActivity extends Activity { +public class LinphoneGenericActivity extends ThemableActivity { @Override protected void onCreate(Bundle savedInstanceState) { @@ -37,7 +36,6 @@ public class LinphoneGenericActivity extends Activity { if (!LinphoneService.isReady() || !LinphoneManager.isInstanciated()) { finish(); startActivity(getIntent().setClass(this, LinphoneLauncherActivity.class)); - return; } } } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.java b/app/src/main/java/org/linphone/utils/LinphoneUtils.java new file mode 100644 index 000000000..b732e0eb8 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.java @@ -0,0 +1,504 @@ +package org.linphone.utils; + +/* +LinphoneUtils.java +Copyright (C) 2017 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.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Handler; +import android.os.Looper; +import android.telephony.TelephonyManager; +import android.text.Html; +import android.text.Spanned; +import android.view.KeyEvent; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.TextView; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.core.AccountCreator; +import org.linphone.core.Address; +import org.linphone.core.Call; +import org.linphone.core.Call.State; +import org.linphone.core.ChatRoom; +import org.linphone.core.ChatRoomCapabilities; +import org.linphone.core.Core; +import org.linphone.core.Factory; +import org.linphone.core.LogCollectionState; +import org.linphone.core.ProxyConfig; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; + +/** Helpers. */ +public final class LinphoneUtils { + private static Context sContext = null; + private static final Handler sHandler = new Handler(Looper.getMainLooper()); + + private LinphoneUtils() {} + + public static void configureLoggingService(boolean isDebugEnabled, String appName) { + if (!LinphonePreferences.instance().useJavaLogger()) { + Factory.instance().enableLogCollection(LogCollectionState.Enabled); + Factory.instance().setDebugMode(isDebugEnabled, appName); + } else { + Factory.instance().setDebugMode(isDebugEnabled, appName); + Factory.instance() + .enableLogCollection(LogCollectionState.EnabledWithoutPreviousLogHandler); + if (isDebugEnabled) { + if (LinphoneService.isReady()) { + Factory.instance() + .getLoggingService() + .addListener(LinphoneService.instance().getJavaLoggingService()); + } + } else { + if (LinphoneService.isReady()) { + Factory.instance() + .getLoggingService() + .removeListener(LinphoneService.instance().getJavaLoggingService()); + } + } + } + } + + public static void dispatchOnUIThread(Runnable r) { + sHandler.post(r); + } + + // private static final String sipAddressRegExp = + // "^(sip:)?(\\+)?[a-z0-9]+([_\\.-][a-z0-9]+)*@([a-z0-9]+([\\.-][a-z0-9]+)*)+\\.[a-z]{2,}(:[0-9]{2,5})?$"; + // private static final String strictSipAddressRegExp = + // "^sip:(\\+)?[a-z0-9]+([_\\.-][a-z0-9]+)*@([a-z0-9]+([\\.-][a-z0-9]+)*)+\\.[a-z]{2,}$"; + + private static boolean isSipAddress(String numberOrAddress) { + Factory.instance().createAddress(numberOrAddress); + return true; + } + + public static boolean isNumberAddress(String numberOrAddress) { + ProxyConfig proxy = LinphoneManager.getLc().createProxyConfig(); + return proxy.normalizePhoneNumber(numberOrAddress) != null; + } + + public static boolean isStrictSipAddress(String numberOrAddress) { + return isSipAddress(numberOrAddress) && numberOrAddress.startsWith("sip:"); + } + + public static String getDisplayableAddress(Address addr) { + return "sip:" + addr.getUsername() + "@" + addr.getDomain(); + } + + public static String getAddressDisplayName(String uri) { + Address lAddress; + lAddress = Factory.instance().createAddress(uri); + return getAddressDisplayName(lAddress); + } + + public static String getAddressDisplayName(Address address) { + if (address == null) return null; + + String displayName = address.getDisplayName(); + if (displayName == null || displayName.isEmpty()) { + displayName = address.getUsername(); + } + if (displayName == null || displayName.isEmpty()) { + displayName = address.asStringUriOnly(); + } + return displayName; + } + + public static String getUsernameFromAddress(String address) { + if (address.contains("sip:")) address = address.replace("sip:", ""); + + if (address.contains("@")) address = address.split("@")[0]; + + return address; + } + + public static boolean onKeyBackGoHome(Activity activity, int keyCode, KeyEvent event) { + if (!(keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0)) { + return false; // continue + } + + activity.startActivity( + new Intent().setAction(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)); + return true; + } + + public static String timestampToHumanDate(Context context, long timestamp, int format) { + return timestampToHumanDate(context, timestamp, context.getString(format)); + } + + public static String timestampToHumanDate(Context context, long timestamp, String format) { + try { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(timestamp * 1000); // Core returns timestamps in seconds... + + SimpleDateFormat dateFormat; + if (isToday(cal)) { + dateFormat = + new SimpleDateFormat( + context.getResources().getString(R.string.today_date_format), + Locale.getDefault()); + } else { + dateFormat = new SimpleDateFormat(format, Locale.getDefault()); + } + + return dateFormat.format(cal.getTime()); + } catch (NumberFormatException nfe) { + return String.valueOf(timestamp); + } + } + + private static boolean isToday(Calendar cal) { + return isSameDay(cal, Calendar.getInstance()); + } + + private static 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)); + } + + public static boolean onKeyVolumeAdjust(int keyCode) { + if (!LinphoneService.isReady()) { + Log.i("Couldn't change softvolume has service is not running"); + return true; + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) { + LinphoneManager.getInstance().adjustVolume(1); + } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + LinphoneManager.getInstance().adjustVolume(-1); + } + return true; + } + + public static List getCallsInState(Core lc, Collection states) { + List foundCalls = new ArrayList<>(); + for (Call call : lc.getCalls()) { + if (states.contains(call.getState())) { + foundCalls.add(call); + } + } + return foundCalls; + } + + private static boolean isCallRunning(Call call) { + if (call == null) { + return false; + } + + Call.State state = call.getState(); + + return state == Call.State.Connected + || state == Call.State.Updating + || state == Call.State.UpdatedByRemote + || state == Call.State.StreamsRunning + || state == Call.State.Resuming; + } + + public static boolean isCallEstablished(Call call) { + if (call == null) { + return false; + } + + Call.State state = call.getState(); + + return isCallRunning(call) + || state == Call.State.Paused + || state == Call.State.PausedByRemote + || state == Call.State.Pausing; + } + + public static boolean isHighBandwidthConnection(Context context) { + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + return (info != null + && info.isConnected() + && isConnectionFast(info.getType(), info.getSubtype())); + } + + private static boolean isConnectionFast(int type, int subType) { + if (type == ConnectivityManager.TYPE_MOBILE) { + switch (subType) { + case TelephonyManager.NETWORK_TYPE_EDGE: + case TelephonyManager.NETWORK_TYPE_GPRS: + case TelephonyManager.NETWORK_TYPE_IDEN: + return false; + } + } + // in doubt, assume connection is good. + return true; + } + + public static String getDisplayableUsernameFromAddress(String sipAddress) { + String username = sipAddress; + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc == null) return username; + + if (username.startsWith("sip:")) { + username = username.substring(4); + } + + if (username.contains("@")) { + String domain = username.split("@")[1]; + ProxyConfig lpc = lc.getDefaultProxyConfig(); + if (lpc != null) { + if (domain.equals(lpc.getDomain())) { + return username.split("@")[0]; + } + } else { + if (domain.equals( + LinphoneManager.getInstance() + .getContext() + .getString(R.string.default_domain))) { + return username.split("@")[0]; + } + } + } + return username; + } + + public static String getFullAddressFromUsername(String username) { + String sipAddress = username; + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc == null || username == null) return sipAddress; + + if (!sipAddress.startsWith("sip:")) { + sipAddress = "sip:" + sipAddress; + } + + if (!sipAddress.contains("@")) { + ProxyConfig lpc = lc.getDefaultProxyConfig(); + if (lpc != null) { + sipAddress = sipAddress + "@" + lpc.getDomain(); + } else { + sipAddress = + sipAddress + + "@" + + LinphoneManager.getInstance() + .getContext() + .getString(R.string.default_domain); + } + } + return sipAddress; + } + + public static void displayError(boolean isOk, TextView error, String errorText) { + if (isOk) { + error.setVisibility(View.INVISIBLE); + error.setText(""); + } else { + error.setVisibility(View.VISIBLE); + error.setText(errorText); + } + } + + public static String errorForPhoneNumberStatus(int status) { + Context ctxt = getContext(); + if (ctxt != null) { + if (AccountCreator.PhoneNumberStatus.InvalidCountryCode.toInt() + == (status & AccountCreator.PhoneNumberStatus.InvalidCountryCode.toInt())) + return ctxt.getString(R.string.country_code_invalid); + if (AccountCreator.PhoneNumberStatus.TooShort.toInt() + == (status & AccountCreator.PhoneNumberStatus.TooShort.toInt())) + return ctxt.getString(R.string.phone_number_too_short); + if (AccountCreator.PhoneNumberStatus.TooLong.toInt() + == (status & AccountCreator.PhoneNumberStatus.TooLong.toInt())) + return ctxt.getString(R.string.phone_number_too_long); + if (AccountCreator.PhoneNumberStatus.Invalid.toInt() + == (status & AccountCreator.PhoneNumberStatus.Invalid.toInt())) + return ctxt.getString(R.string.phone_number_invalid); + } + return null; + } + + public static String errorForEmailStatus(AccountCreator.EmailStatus status) { + Context ctxt = getContext(); + if (ctxt != null) { + if (status.equals(AccountCreator.EmailStatus.InvalidCharacters) + || status.equals(AccountCreator.EmailStatus.Malformed)) + return ctxt.getString(R.string.invalid_email); + } + return null; + } + + public static String errorForUsernameStatus(AccountCreator.UsernameStatus status) { + Context ctxt = getContext(); + if (ctxt != null) { + if (status.equals(AccountCreator.UsernameStatus.InvalidCharacters)) + return ctxt.getString(R.string.invalid_username); + if (status.equals(AccountCreator.UsernameStatus.TooShort)) + return ctxt.getString(R.string.username_too_short); + if (status.equals(AccountCreator.UsernameStatus.TooLong)) + return ctxt.getString(R.string.username_too_long); + if (status.equals(AccountCreator.UsernameStatus.Invalid)) + return ctxt.getString(R.string.username_invalid_size); + if (status.equals(AccountCreator.UsernameStatus.InvalidCharacters)) + return ctxt.getString(R.string.invalid_display_name); + } + return null; + } + + public static String errorForPasswordStatus(AccountCreator.PasswordStatus status) { + Context ctxt = getContext(); + if (ctxt != null) { + if (status.equals(AccountCreator.PasswordStatus.TooShort)) + return ctxt.getString(R.string.password_too_short); + if (status.equals(AccountCreator.PasswordStatus.TooLong)) + return ctxt.getString(R.string.password_too_long); + } + return null; + } + + public static String errorForStatus(AccountCreator.Status status) { + Context ctxt = getContext(); + if (ctxt != null) { + if (status.equals(AccountCreator.Status.RequestFailed)) + return ctxt.getString(R.string.request_failed); + if (status.equals(AccountCreator.Status.ServerError)) + return ctxt.getString(R.string.wizard_failed); + if (status.equals(AccountCreator.Status.AccountExist) + || status.equals(AccountCreator.Status.AccountExistWithAlias)) + return ctxt.getString(R.string.account_already_exist); + if (status.equals(AccountCreator.Status.AliasIsAccount) + || status.equals(AccountCreator.Status.AliasExist)) + return ctxt.getString(R.string.assistant_phone_number_unavailable); + if (status.equals(AccountCreator.Status.AccountNotExist)) + return ctxt.getString(R.string.assistant_error_bad_credentials); + if (status.equals(AccountCreator.Status.AliasNotExist)) + return ctxt.getString(R.string.phone_number_not_exist); + if (status.equals(AccountCreator.Status.AliasNotExist) + || status.equals(AccountCreator.Status.AccountNotActivated) + || status.equals(AccountCreator.Status.AccountAlreadyActivated) + || status.equals(AccountCreator.Status.AccountActivated) + || status.equals(AccountCreator.Status.AccountNotCreated) + || status.equals(AccountCreator.Status.RequestOk)) return ""; + } + return null; + } + + public static String getCountryCode(EditText dialCode) { + if (dialCode != null) { + String code = dialCode.getText().toString(); + if (code != null && code.startsWith("+")) { + code = code.substring(1); + } + return code; + } + return null; + } + + public static void displayErrorAlert(String msg, Context ctxt) { + if (ctxt != null && msg != null) { + AlertDialog.Builder builder = new AlertDialog.Builder(ctxt); + builder.setMessage(msg) + .setCancelable(false) + .setNeutralButton(ctxt.getString(R.string.ok), null) + .show(); + } + } + + public static Spanned getTextWithHttpLinks(String text) { + if (text == null) return null; + + if (text.contains("<")) { + text = text.replace("<", "<"); + } + if (text.contains(">")) { + text = text.replace(">", ">"); + } + if (text.contains("\n")) { + text = text.replace("\n", "
    "); + } + 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( + Pattern.quote(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( + Pattern.quote(link), + "" + linkWithoutScheme + ""); + } + + return Html.fromHtml(text); + } + + public static void hideKeyboard(Activity activity) { + InputMethodManager imm = + (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE); + View view = activity.getCurrentFocus(); + if (view == null) { + view = new View(activity); + } + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + private static Context getContext() { + if (sContext == null && LinphoneManager.isInstanciated()) + sContext = LinphoneManager.getInstance().getContext(); + return sContext; + } + + public static ArrayList removeEmptyOneToOneChatRooms(ChatRoom[] rooms) { + ArrayList newRooms = new ArrayList<>(); + for (ChatRoom room : rooms) { + if (room.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) + && room.getHistorySize() == 0) { + // Hide 1-1 chat rooms without messages + } else { + newRooms.add(room); + } + } + return newRooms; + } +} diff --git a/app/src/main/java/org/linphone/utils/MediaScanner.java b/app/src/main/java/org/linphone/utils/MediaScanner.java new file mode 100644 index 000000000..7051924ee --- /dev/null +++ b/app/src/main/java/org/linphone/utils/MediaScanner.java @@ -0,0 +1,81 @@ +package org.linphone.utils; + +/* +MediaScanner.java +Copyright (C) 2018 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.content.Context; +import android.media.MediaScannerConnection; +import android.net.Uri; +import java.io.File; +import org.linphone.core.tools.Log; + +public class MediaScanner implements MediaScannerConnection.MediaScannerConnectionClient { + private final MediaScannerConnection mMediaConnection; + private boolean mIsConnected; + private File mFileWaitingForScan; + private MediaScannerListener mListener; + + public MediaScanner(Context context) { + mIsConnected = false; + mMediaConnection = new MediaScannerConnection(context, this); + mMediaConnection.connect(); + mFileWaitingForScan = null; + } + + @Override + public void onMediaScannerConnected() { + mIsConnected = true; + Log.i("[MediaScanner] Connected"); + if (mFileWaitingForScan != null) { + scanFile(mFileWaitingForScan, null); + mFileWaitingForScan = null; + } + } + + public void scanFile(File file, MediaScannerListener listener) { + scanFile(file, FileUtils.getMimeFromFile(file.getAbsolutePath()), listener); + } + + private void scanFile(File file, String mime, MediaScannerListener listener) { + mListener = listener; + + if (!mIsConnected) { + Log.w("[MediaScanner] Not connected yet..."); + mFileWaitingForScan = file; + return; + } + + Log.i("[MediaScanner] Scanning file " + file.getAbsolutePath() + " with MIME " + mime); + mMediaConnection.scanFile(file.getAbsolutePath(), mime); + } + + @Override + public void onScanCompleted(String path, Uri uri) { + Log.i("[MediaScanner] Scan completed : " + path + " => " + uri); + if (mListener != null) { + mListener.onMediaScanned(path, uri); + } + } + + public void destroy() { + Log.i("[MediaScanner] Disconnecting"); + mMediaConnection.disconnect(); + mIsConnected = false; + } +} diff --git a/app/src/main/java/org/linphone/utils/MediaScannerListener.java b/app/src/main/java/org/linphone/utils/MediaScannerListener.java new file mode 100644 index 000000000..1e5512ca3 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/MediaScannerListener.java @@ -0,0 +1,7 @@ +package org.linphone.utils; + +import android.net.Uri; + +public interface MediaScannerListener { + void onMediaScanned(String path, Uri uri); +} diff --git a/app/src/main/java/org/linphone/utils/PushNotificationUtils.java b/app/src/main/java/org/linphone/utils/PushNotificationUtils.java new file mode 100644 index 000000000..9ed6551e7 --- /dev/null +++ b/app/src/main/java/org/linphone/utils/PushNotificationUtils.java @@ -0,0 +1,65 @@ +package org.linphone.utils; + +/* +PushNotificationUtils.java +Copyright (C) 2019 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.content.Context; +import java.lang.reflect.Constructor; +import org.linphone.R; +import org.linphone.core.tools.Log; + +public class PushNotificationUtils { + private static PushHelperInterface mHelper; + + public static void init(Context context) { + mHelper = null; + String push_type = context.getString(R.string.push_type); + + if (push_type.equals("firebase")) { + String className = "org.linphone.firebase.FirebasePushHelper"; + try { + Class pushHelper = Class.forName(className); + Class[] types = {}; + Constructor constructor = pushHelper.getConstructor(types); + Object[] parameters = {}; + mHelper = (PushHelperInterface) constructor.newInstance(parameters); + mHelper.init(context); + } catch (NoSuchMethodException e) { + Log.w("[Push Utils] Couldn't get push helper constructor"); + } catch (ClassNotFoundException e) { + Log.w("[Push Utils] Couldn't find class " + className); + } catch (Exception e) { + Log.w("[Push Utils] Couldn't get push helper instance: " + e); + } + } else { + Log.w("[Push Utils] Unknow push type " + push_type); + } + } + + public static boolean isAvailable(Context context) { + if (mHelper == null) return false; + return mHelper.isAvailable(context); + } + + public interface PushHelperInterface { + void init(Context context); + + boolean isAvailable(Context context); + } +} diff --git a/src/android/org/linphone/ui/SelectableAdapter.java b/app/src/main/java/org/linphone/utils/SelectableAdapter.java similarity index 64% rename from src/android/org/linphone/ui/SelectableAdapter.java rename to app/src/main/java/org/linphone/utils/SelectableAdapter.java index fa1f490de..ebc0203c2 100644 --- a/src/android/org/linphone/ui/SelectableAdapter.java +++ b/app/src/main/java/org/linphone/utils/SelectableAdapter.java @@ -1,36 +1,36 @@ -package org.linphone.ui; +package org.linphone.utils; /* - SelectableAdapter.java - Copyright (C) 2018 Belledonne Communications, Grenoble, France +SelectableAdapter.java +Copyright (C) 2018 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 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. +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. - */ +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.support.v7.widget.RecyclerView; import android.util.SparseBooleanArray; - +import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.List; -public abstract class SelectableAdapter extends RecyclerView.Adapter { - private SparseBooleanArray mSelectedItems; +public abstract class SelectableAdapter + extends RecyclerView.Adapter { + private final SparseBooleanArray mSelectedItems; private boolean mIsEditionEnabled = false; - private SelectableHelper mListHelper; + private final SelectableHelper mListHelper; - public SelectableAdapter(SelectableHelper helper) { + protected SelectableAdapter(SelectableHelper helper) { mSelectedItems = new SparseBooleanArray(); mListHelper = helper; } @@ -52,7 +52,7 @@ public abstract class SelectableAdapter exte * @param position Position of the item to check * @return true if the item is selected, false otherwise */ - public boolean isSelected(int position) { + protected boolean isSelected(int position) { return getSelectedItems().contains(position); } @@ -67,7 +67,8 @@ public abstract class SelectableAdapter exte } else { mSelectedItems.put(position, true); } - mListHelper.updateSelectionButtons(getSelectedItemCount() == 0, getSelectedItemCount() == getItemCount()); + mListHelper.updateSelectionButtons( + getSelectedItemCount() == 0, getSelectedItemCount() == getItemCount()); notifyItemChanged(position); } diff --git a/src/android/org/linphone/ui/SelectableHelper.java b/app/src/main/java/org/linphone/utils/SelectableHelper.java similarity index 51% rename from src/android/org/linphone/ui/SelectableHelper.java rename to app/src/main/java/org/linphone/utils/SelectableHelper.java index 92ba14421..ad183ab2d 100644 --- a/src/android/org/linphone/ui/SelectableHelper.java +++ b/app/src/main/java/org/linphone/utils/SelectableHelper.java @@ -17,35 +17,31 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.linphone.ui; +package org.linphone.utils; import android.app.Dialog; import android.content.Context; -import android.support.v7.widget.RecyclerView; import android.view.View; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; - +import androidx.recyclerview.widget.RecyclerView; +import org.linphone.LinphoneActivity; import org.linphone.R; -import org.linphone.activities.LinphoneActivity; public class SelectableHelper { - private ImageView mEditButton, mSelectAllButton, mDeselectAllButton, mDeleteSelectionButton, mCancelButton; - private LinearLayout mEditTopBar, mTopBar; + private final ImageView mEditButton; + private final ImageView mSelectAllButton; + private final ImageView mDeselectAllButton; + private final ImageView mDeleteSelectionButton; + private final ImageView mCancelButton; + private final LinearLayout mEditTopBar; + private final LinearLayout mTopBar; private SelectableAdapter mAdapter; - private DeleteListener mDeleteListener; - private Context mContext; + private final DeleteListener mDeleteListener; + private final Context mContext; private int mDialogDeleteMessageResourceId; - public void setDialogMessage(int id) { - mDialogDeleteMessageResourceId = id; - } - - public interface DeleteListener { - void onDeleteSelection(Object[] objectsToDelete); - } - public SelectableHelper(View view, DeleteListener listener) { mContext = view.getContext(); mDeleteListener = listener; @@ -54,77 +50,98 @@ public class SelectableHelper { mTopBar = view.findViewById(R.id.top_bar); mCancelButton = view.findViewById(R.id.cancel); - mCancelButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - quitEditionMode(); - } - }); + mCancelButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + quitEditionMode(); + } + }); mEditButton = view.findViewById(R.id.edit); + mEditButton.setEnabled(false); - mEditButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (mAdapter.getItemCount() > 0) { - enterEditionMode(); - mTopBar.setVisibility(View.GONE); - mEditTopBar.setVisibility(View.VISIBLE); - } - } - }); + mEditButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mAdapter.getItemCount() > 0) { + enterEditionMode(); + mTopBar.setVisibility(View.GONE); + mEditTopBar.setVisibility(View.VISIBLE); + } + } + }); mSelectAllButton = view.findViewById(R.id.select_all); - mSelectAllButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mAdapter.selectAll(); - } - }); + mSelectAllButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + mAdapter.selectAll(); + } + }); mDeselectAllButton = view.findViewById(R.id.deselect_all); - mDeselectAllButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mAdapter.deselectAll(); - } - }); + mDeselectAllButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + mAdapter.deselectAll(); + } + }); mDeleteSelectionButton = view.findViewById(R.id.delete); mDeleteSelectionButton.setEnabled(false); - mDeleteSelectionButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - final Dialog dialog = LinphoneActivity.instance().displayDialog(mContext.getString(mDialogDeleteMessageResourceId)); - Button delete = dialog.findViewById(R.id.delete_button); - Button cancel = dialog.findViewById(R.id.cancel); - - delete.setOnClickListener(new View.OnClickListener() { + mDeleteSelectionButton.setOnClickListener( + new View.OnClickListener() { @Override - public void onClick(View view) { - mDeleteListener.onDeleteSelection(getSelectedObjects()); - dialog.dismiss(); - quitEditionMode(); + public void onClick(View v) { + final Dialog dialog = + LinphoneActivity.instance() + .displayDialog( + mContext.getString(mDialogDeleteMessageResourceId)); + Button delete = dialog.findViewById(R.id.dialog_delete_button); + Button cancel = dialog.findViewById(R.id.dialog_cancel_button); + + delete.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + mDeleteListener.onDeleteSelection(getSelectedObjects()); + mEditButton.setEnabled(mAdapter.getItemCount() != 0); + dialog.dismiss(); + quitEditionMode(); + } + }); + + cancel.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + dialog.dismiss(); + quitEditionMode(); + } + }); + dialog.show(); } }); - cancel.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - dialog.dismiss(); - quitEditionMode(); - } - }); - dialog.show(); - } - }); - mDialogDeleteMessageResourceId = R.string.delete_text; } + public void setDialogMessage(int id) { + mDialogDeleteMessageResourceId = id; + } + + public SelectableAdapter getAdapter() { + return mAdapter; + } + public void setAdapter(SelectableAdapter adapter) { mAdapter = adapter; + mEditButton.setEnabled(mAdapter.getItemCount() != 0); } public void updateSelectionButtons(boolean isSelectionEmpty, boolean isSelectionFull) { @@ -143,7 +160,7 @@ public class SelectableHelper { } } - public void quitEditionMode() { + private void quitEditionMode() { mAdapter.enableEdition(false); mTopBar.setVisibility(View.VISIBLE); mEditTopBar.setVisibility(View.GONE); @@ -170,4 +187,8 @@ public class SelectableHelper { } return objects; } + + public interface DeleteListener { + void onDeleteSelection(Object[] objectsToDelete); + } } diff --git a/sample/src/org/linphone/mini/LinphoneMiniActivity.java b/app/src/main/java/org/linphone/utils/ThemableActivity.java similarity index 54% rename from sample/src/org/linphone/mini/LinphoneMiniActivity.java rename to app/src/main/java/org/linphone/utils/ThemableActivity.java index 2c69ecd91..871607a51 100644 --- a/sample/src/org/linphone/mini/LinphoneMiniActivity.java +++ b/app/src/main/java/org/linphone/utils/ThemableActivity.java @@ -1,8 +1,8 @@ -package org.linphone.mini; +package org.linphone.utils; /* -LinphoneMiniActivity.java -Copyright (C) 2017 Belledonne Communications, Grenoble, France +ThemableActivity.java +Copyright (C) 2019 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 @@ -19,33 +19,19 @@ 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.Activity; import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import org.linphone.R; +import org.linphone.settings.LinphonePreferences; -public class LinphoneMiniActivity extends Activity { - private LinphoneMiniManager mManager; +public class ThemableActivity extends AppCompatActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + @Override + protected void onCreate(Bundle savedInstanceState) { + if (LinphonePreferences.instance().isDarkModeEnabled()) { + setTheme(R.style.LinphoneStyleDark); + } - mManager = new LinphoneMiniManager(this); - } - - @Override - protected void onResume() { - super.onResume(); - } - - @Override - protected void onPause() { - super.onPause(); - } - - @Override - protected void onDestroy() { - mManager.destroy(); - - super.onDestroy(); - } + super.onCreate(savedInstanceState); + } } diff --git a/src/android/org/linphone/ui/AddressAware.java b/app/src/main/java/org/linphone/views/AddressAware.java similarity index 96% rename from src/android/org/linphone/ui/AddressAware.java rename to app/src/main/java/org/linphone/views/AddressAware.java index e943a09ee..15dc50094 100644 --- a/src/android/org/linphone/ui/AddressAware.java +++ b/app/src/main/java/org/linphone/views/AddressAware.java @@ -1,4 +1,4 @@ -package org.linphone.ui; +package org.linphone.views; /* AddressAwareWidget.java diff --git a/src/android/org/linphone/ui/AddressText.java b/app/src/main/java/org/linphone/views/AddressText.java similarity index 87% rename from src/android/org/linphone/ui/AddressText.java rename to app/src/main/java/org/linphone/views/AddressText.java index 78b233501..8d932e406 100644 --- a/src/android/org/linphone/ui/AddressText.java +++ b/app/src/main/java/org/linphone/views/AddressText.java @@ -1,4 +1,4 @@ -package org.linphone.ui; +package org.linphone.views; /* AddressView.java @@ -19,21 +19,22 @@ 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.annotation.SuppressLint; import android.content.Context; import android.graphics.Paint; import android.util.AttributeSet; import android.util.TypedValue; import android.widget.EditText; - import org.linphone.LinphoneManager.AddressType; import org.linphone.R; import org.linphone.fragments.DialerFragment; +@SuppressLint("AppCompatCustomView") public class AddressText extends EditText implements AddressType { - private String displayedName; - private Paint mTestPaint; - private DialerFragment dialer; + private String mDisplayedName; + private final Paint mTestPaint; + private DialerFragment mDialer; public AddressText(Context context, AttributeSet attrs) { super(context, attrs); @@ -43,15 +44,15 @@ public class AddressText extends EditText implements AddressType { } public void clearDisplayedName() { - displayedName = null; + mDisplayedName = null; } public String getDisplayedName() { - return displayedName; + return mDisplayedName; } public void setDisplayedName(String displayedName) { - this.displayedName = displayedName; + this.mDisplayedName = displayedName; } private String getHintText() { @@ -63,14 +64,13 @@ public class AddressText extends EditText implements AddressType { } @Override - protected void onTextChanged(CharSequence text, int start, int before, - int after) { + protected void onTextChanged(CharSequence text, int start, int before, int after) { clearDisplayedName(); refitText(getWidth(), getHeight()); - if (dialer != null) { - dialer.enableDisableAddContact(); + if (mDialer != null) { + mDialer.enableDisableAddContact(); } super.onTextChanged(text, start, before, after); @@ -112,8 +112,7 @@ public class AddressText extends EditText implements AddressType { float size = getOptimizedTextSize(getHintText(), textWidth, textHeight); float entrySize = getOptimizedTextSize(getText().toString(), textWidth, textHeight); - if (entrySize < size) - size = entrySize; + if (entrySize < size) size = entrySize; setTextSize(TypedValue.COMPLEX_UNIT_PX, size); } @@ -127,6 +126,6 @@ public class AddressText extends EditText implements AddressType { } public void setDialerFragment(DialerFragment dialerFragment) { - dialer = dialerFragment; + mDialer = dialerFragment; } } diff --git a/app/src/main/java/org/linphone/views/AsyncBitmap.java b/app/src/main/java/org/linphone/views/AsyncBitmap.java new file mode 100644 index 000000000..c043a11ea --- /dev/null +++ b/app/src/main/java/org/linphone/views/AsyncBitmap.java @@ -0,0 +1,38 @@ +package org.linphone.views; + +/* +AsyncBitmap.java +Copyright (C) 2018 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.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import java.lang.ref.WeakReference; + +public class AsyncBitmap extends BitmapDrawable { + private final WeakReference mBitmapWorkerTaskReference; + + public AsyncBitmap(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + mBitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return mBitmapWorkerTaskReference.get(); + } +} diff --git a/app/src/main/java/org/linphone/views/BitmapWorkerTask.java b/app/src/main/java/org/linphone/views/BitmapWorkerTask.java new file mode 100644 index 000000000..20930181e --- /dev/null +++ b/app/src/main/java/org/linphone/views/BitmapWorkerTask.java @@ -0,0 +1,165 @@ +package org.linphone.views; + +/* +BitmapWorkerTask.java +Copyright (C) 2018 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.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.drawable.Drawable; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.MediaStore; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import java.io.IOException; +import java.lang.ref.WeakReference; +import org.linphone.core.tools.Log; +import org.linphone.utils.FileUtils; +import org.linphone.utils.ImageUtils; + +public class BitmapWorkerTask extends AsyncTask { + public String path; + + private final WeakReference mImageViewReference; + private final Context mContext; + private final Bitmap mDefaultBitmap; + private final int mImageViewHeight; + + public BitmapWorkerTask(Context context, ImageView imageView, Bitmap defaultBitmap) { + mContext = context; + mDefaultBitmap = defaultBitmap; + path = null; + // Use a WeakReference to ensure the ImageView can be garbage collected + mImageViewReference = new WeakReference<>(imageView); + mImageViewHeight = imageView.getMeasuredHeight(); + } + + public static 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; + } + + private Bitmap scaleToFitHeight(Bitmap b, int height) { + float factor = height / (float) b.getHeight(); + int dstWidth = (int) (b.getWidth() * factor); + if (dstWidth > 0 && height > 0) { + return Bitmap.createScaledBitmap(b, dstWidth, height, true); + } + return b; + } + + // Decode image in background. + @Override + protected Bitmap doInBackground(String... params) { + path = params[0]; + Bitmap bm = null; + Bitmap thumbnail = null; + if (FileUtils.isExtensionImage(path)) { + if (path.startsWith("content")) { + try { + bm = + MediaStore.Images.Media.getBitmap( + mContext.getContentResolver(), Uri.parse(path)); + } catch (IOException e) { + Log.e(e); + } + } else { + bm = BitmapFactory.decodeFile(path); + } + + ImageView imageView = mImageViewReference.get(); + + try { + // Rotate the bitmap if possible/needed, using EXIF data + Matrix matrix = new Matrix(); + ExifInterface exif = new ExifInterface(path); + int width = bm.getWidth(); + int height = bm.getHeight(); + + int pictureOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0); + if (pictureOrientation == 6 || pictureOrientation == 3 || pictureOrientation == 8) { + if (imageView != null) { + float factor = (float) mImageViewHeight / height; + matrix.postScale(factor, factor); + } + if (pictureOrientation == 6) { + matrix.preRotate(90); + } else if (pictureOrientation == 3) { + matrix.preRotate(180); + } else { + matrix.preRotate(270); + } + thumbnail = Bitmap.createBitmap(bm, 0, 0, width, height, matrix, true); + if (thumbnail != bm) { + bm.recycle(); + bm = null; + } + } + } catch (Exception e) { + Log.e(e); + } + + if (thumbnail == null && bm != null) { + if (imageView == null) return bm; + thumbnail = scaleToFitHeight(bm, mImageViewHeight); + if (thumbnail != bm) { + bm.recycle(); + } + } + return thumbnail; + } else { + return mDefaultBitmap; + } + } + + // Once complete, see if ImageView is still around and set bitmap. + @Override + protected void onPostExecute(Bitmap bitmap) { + if (isCancelled()) { + bitmap.recycle(); + bitmap = null; + } + if (mImageViewReference != null && bitmap != null) { + final ImageView imageView = mImageViewReference.get(); + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + if (this == bitmapWorkerTask && imageView != null) { + imageView.setImageBitmap(bitmap); + if (bitmap.getWidth() > ImageUtils.dpToPixels(mContext, 300)) { + RelativeLayout.LayoutParams params = + new RelativeLayout.LayoutParams( + bitmap.getWidth(), ViewGroup.LayoutParams.WRAP_CONTENT); + int margin = (int) ImageUtils.dpToPixels(mContext, 5); + params.setMargins(margin, margin, margin, margin); + imageView.setLayoutParams(params); + imageView.invalidate(); + } + } + } + } +} diff --git a/app/src/main/java/org/linphone/views/CallButton.java b/app/src/main/java/org/linphone/views/CallButton.java new file mode 100644 index 000000000..3b0b59293 --- /dev/null +++ b/app/src/main/java/org/linphone/views/CallButton.java @@ -0,0 +1,84 @@ +package org.linphone.views; + +/* +CallButton.java +Copyright (C) 2017 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.annotation.SuppressLint; +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.ImageView; +import org.linphone.LinphoneManager; +import org.linphone.core.Call; +import org.linphone.core.CallLog; +import org.linphone.core.ProxyConfig; +import org.linphone.settings.LinphonePreferences; + +@SuppressLint("AppCompatCustomView") +public class CallButton extends ImageView implements OnClickListener, AddressAware { + + private AddressText mAddress; + + public CallButton(Context context, AttributeSet attrs) { + super(context, attrs); + setOnClickListener(this); + } + + public void setAddressWidget(AddressText a) { + mAddress = a; + } + + public void setExternalClickListener(OnClickListener e) { + setOnClickListener(e); + } + + public void resetClickListener() { + setOnClickListener(this); + } + + public void onClick(View v) { + if (mAddress.getText().length() > 0) { + LinphoneManager.getInstance().newOutgoingCall(mAddress); + } else { + if (LinphonePreferences.instance().isBisFeatureEnabled()) { + CallLog[] logs = LinphoneManager.getLc().getCallLogs(); + CallLog log = null; + for (CallLog l : logs) { + if (l.getDir() == Call.Dir.Outgoing) { + log = l; + break; + } + } + if (log == null) { + return; + } + + ProxyConfig lpc = LinphoneManager.getLc().getDefaultProxyConfig(); + if (lpc != null && log.getToAddress().getDomain().equals(lpc.getDomain())) { + mAddress.setText(log.getToAddress().getUsername()); + } else { + mAddress.setText(log.getToAddress().asStringUriOnly()); + } + mAddress.setSelection(mAddress.getText().toString().length()); + mAddress.setDisplayedName(log.getToAddress().getDisplayName()); + } + } + } +} diff --git a/app/src/main/java/org/linphone/views/CallIncomingAnswerButton.java b/app/src/main/java/org/linphone/views/CallIncomingAnswerButton.java new file mode 100644 index 000000000..048585ce7 --- /dev/null +++ b/app/src/main/java/org/linphone/views/CallIncomingAnswerButton.java @@ -0,0 +1,125 @@ +package org.linphone.views; + +/* +CallIncomingAnswerButton.java +Copyright (C) 2018 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.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; +import androidx.annotation.Nullable; +import org.linphone.R; + +public class CallIncomingAnswerButton extends LinearLayout + implements View.OnClickListener, View.OnTouchListener { + private LinearLayout mRoot; + private boolean mUseSliderMode = false; + private CallIncomingButtonListener mListener; + private View mDeclineButton; + + private int mScreenWidth; + private boolean mBegin; + private float mAnswerX, mOldSize; + + public CallIncomingAnswerButton(Context context) { + super(context); + init(); + } + + public CallIncomingAnswerButton(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public CallIncomingAnswerButton( + Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public void setSliderMode(boolean enabled) { + mUseSliderMode = enabled; + findViewById(R.id.acceptUnlock).setVisibility(enabled ? VISIBLE : GONE); + } + + public void setListener(CallIncomingButtonListener listener) { + mListener = listener; + } + + public void setDeclineButton(View decline) { + mDeclineButton = decline; + } + + private void init() { + inflate(getContext(), R.layout.call_incoming_answer_button, this); + mRoot = findViewById(R.id.root); + mRoot.setOnClickListener(this); + mRoot.setOnTouchListener(this); + mScreenWidth = getResources().getDisplayMetrics().widthPixels; + } + + @Override + public void onClick(View v) { + if (!mUseSliderMode) { + performClick(); + } + } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + if (mUseSliderMode) { + float curX; + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + mDeclineButton.setVisibility(View.GONE); + mAnswerX = motionEvent.getX() - mRoot.getWidth(); + mBegin = true; + mOldSize = 0; + break; + case MotionEvent.ACTION_MOVE: + curX = motionEvent.getX() - mRoot.getWidth(); + view.scrollBy((int) (mAnswerX - curX), view.getScrollY()); + mOldSize -= mAnswerX - curX; + mAnswerX = curX; + if (mOldSize < -25) mBegin = false; + if (curX < (mScreenWidth / 4) - mRoot.getWidth() && !mBegin) { + performClick(); + return true; + } + break; + case MotionEvent.ACTION_UP: + mDeclineButton.setVisibility(View.VISIBLE); + view.scrollTo(0, view.getScrollY()); + break; + } + return true; + } + return false; + } + + @Override + public boolean performClick() { + super.performClick(); + if (mListener != null) { + mListener.onAction(); + } + return true; + } +} diff --git a/app/src/main/java/org/linphone/views/CallIncomingButtonListener.java b/app/src/main/java/org/linphone/views/CallIncomingButtonListener.java new file mode 100644 index 000000000..57dd3c7f0 --- /dev/null +++ b/app/src/main/java/org/linphone/views/CallIncomingButtonListener.java @@ -0,0 +1,24 @@ +package org.linphone.views; + +/* +CallIncomingButtonListener.java +Copyright (C) 2018 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. +*/ + +public interface CallIncomingButtonListener { + void onAction(); +} diff --git a/app/src/main/java/org/linphone/views/CallIncomingDeclineButton.java b/app/src/main/java/org/linphone/views/CallIncomingDeclineButton.java new file mode 100644 index 000000000..6cf86cae6 --- /dev/null +++ b/app/src/main/java/org/linphone/views/CallIncomingDeclineButton.java @@ -0,0 +1,120 @@ +package org.linphone.views; + +/* +CallIncomingDeclineButton.java +Copyright (C) 2018 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.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.LinearLayout; +import androidx.annotation.Nullable; +import org.linphone.R; + +public class CallIncomingDeclineButton extends LinearLayout + implements View.OnClickListener, View.OnTouchListener { + private LinearLayout mRoot; + private boolean mUseSliderMode = false; + private CallIncomingButtonListener mListener; + private View mAnswerButton; + + private int mScreenWidth; + private float mDeclineX; + + public CallIncomingDeclineButton(Context context) { + super(context); + init(); + } + + public CallIncomingDeclineButton(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(); + } + + public CallIncomingDeclineButton( + Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public void setSliderMode(boolean enabled) { + mUseSliderMode = enabled; + findViewById(R.id.declineUnlock).setVisibility(enabled ? VISIBLE : GONE); + } + + public void setListener(CallIncomingButtonListener listener) { + mListener = listener; + } + + public void setAnswerButton(View answer) { + mAnswerButton = answer; + } + + private void init() { + inflate(getContext(), R.layout.call_incoming_decline_button, this); + mRoot = findViewById(R.id.root); + mRoot.setOnClickListener(this); + mRoot.setOnTouchListener(this); + mScreenWidth = getResources().getDisplayMetrics().widthPixels; + } + + @Override + public void onClick(View v) { + if (!mUseSliderMode) { + performClick(); + } + } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + if (mUseSliderMode) { + float curX; + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + mAnswerButton.setVisibility(View.GONE); + mDeclineX = motionEvent.getX(); + break; + case MotionEvent.ACTION_MOVE: + curX = motionEvent.getX(); + view.scrollBy((int) (mDeclineX - curX), view.getScrollY()); + mDeclineX = curX; + if (curX > (3 * mScreenWidth / 4)) { + performClick(); + return true; + } + break; + case MotionEvent.ACTION_UP: + mAnswerButton.setVisibility(View.VISIBLE); + view.scrollTo(0, view.getScrollY()); + break; + } + return true; + } + return false; + } + + @Override + public boolean performClick() { + super.performClick(); + if (mListener != null) { + mListener.onAction(); + } + return true; + } +} diff --git a/app/src/main/java/org/linphone/views/ContactAvatar.java b/app/src/main/java/org/linphone/views/ContactAvatar.java new file mode 100644 index 000000000..04fdc8d36 --- /dev/null +++ b/app/src/main/java/org/linphone/views/ContactAvatar.java @@ -0,0 +1,235 @@ +package org.linphone.views; + +/* +ContactAvatar.java +Copyright (C) 2010-2018 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.graphics.Bitmap; +import android.provider.MediaStore; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; +import java.io.IOException; +import org.linphone.LinphoneService; +import org.linphone.R; +import org.linphone.contacts.LinphoneContact; +import org.linphone.core.ChatRoomSecurityLevel; +import org.linphone.core.tools.Log; + +class ContactAvatarHolder { + public final ImageView contactPicture; + public final ImageView avatarMask; + public final ImageView avatarBorder; + public final ImageView securityLevel; + public final TextView generatedAvatar; + + public ContactAvatarHolder(View v) { + contactPicture = v.findViewById(R.id.contact_picture); + avatarMask = v.findViewById(R.id.mask); + securityLevel = v.findViewById(R.id.security_level); + generatedAvatar = v.findViewById(R.id.generated_avatar); + avatarBorder = v.findViewById(R.id.border); + } + + public void init() { + contactPicture.setVisibility(View.VISIBLE); + generatedAvatar.setVisibility(View.VISIBLE); + securityLevel.setVisibility(View.GONE); + avatarBorder.setVisibility(View.GONE); + } +} + +public class ContactAvatar { + + private static String generateAvatar(String displayName) { + String[] names = displayName.split(" "); + StringBuilder generatedAvatarText = new StringBuilder(); + for (String name : names) { + if (name != null && name.length() > 0) { + generatedAvatarText.append(name.charAt(0)); + } + } + return generatedAvatarText.toString().toUpperCase(); + } + + private static void setSecurityLevel(ChatRoomSecurityLevel level, View v) { + ContactAvatarHolder holder = new ContactAvatarHolder(v); + if (holder.securityLevel != null) { + holder.securityLevel.setVisibility(View.VISIBLE); + switch (level) { + case Safe: + holder.securityLevel.setImageResource(R.drawable.security_2_indicator); + break; + case Encrypted: + holder.securityLevel.setImageResource(R.drawable.security_1_indicator); + break; + case ClearText: + case Unsafe: + default: + holder.securityLevel.setImageResource(R.drawable.security_alert_indicator); + break; + } + } else { + holder.securityLevel.setVisibility(View.GONE); + } + } + + private static void showHasLimeX3dhCapability(View v) { + ContactAvatarHolder holder = new ContactAvatarHolder(v); + if (holder.securityLevel != null) { + holder.securityLevel.setVisibility(View.VISIBLE); + holder.securityLevel.setImageResource(R.drawable.security_toogle_icon_green); + } else { + holder.securityLevel.setVisibility(View.GONE); + } + } + + public static void displayAvatar( + String displayName, View v, boolean showBorder, int maskResource) { + if (displayName == null || v == null) return; + + ContactAvatarHolder holder = new ContactAvatarHolder(v); + holder.init(); + + boolean generated_avatars = + v.getContext().getResources().getBoolean(R.bool.generate_text_avatar); + if (displayName.startsWith("+") || !generated_avatars) { + // If display name is a phone number, use default avatar because generated one will be + // +... + holder.generatedAvatar.setVisibility(View.GONE); + } else { + String generatedAvatar = generateAvatar(displayName); + if (generatedAvatar != null && generatedAvatar.length() > 0) { + holder.generatedAvatar.setText(generatedAvatar); + holder.generatedAvatar.setVisibility(View.VISIBLE); + } else { + holder.generatedAvatar.setVisibility(View.GONE); + } + } + holder.securityLevel.setVisibility(View.GONE); + + if (maskResource != 0) { + holder.avatarMask.setImageResource(maskResource); + } + if (showBorder) { + holder.avatarBorder.setVisibility(View.VISIBLE); + } + } + + public static void displayAvatar(String displayName, View v, boolean showBorder) { + displayAvatar(displayName, v, showBorder, 0); + } + + public static void displayAvatar(String displayName, View v) { + displayAvatar(displayName, v, false, 0); + } + + public static void displayAvatar( + String displayName, ChatRoomSecurityLevel securityLevel, View v) { + displayAvatar(displayName, v); + setSecurityLevel(securityLevel, v); + } + + public static void displayAvatar( + LinphoneContact contact, View v, boolean showBorder, int maskResource) { + if (contact == null || v == null) return; + + ContactAvatarHolder holder = new ContactAvatarHolder(v); + holder.init(); + + boolean generated_avatars = + v.getContext().getResources().getBoolean(R.bool.generate_text_avatar); + + // Kepp the generated avatar ready in case of failure while loading picture + holder.generatedAvatar.setText( + generateAvatar( + contact.getFullName() == null + ? contact.getFirstName() + " " + contact.getLastName() + : contact.getFullName())); + + holder.generatedAvatar.setVisibility(View.GONE); + holder.contactPicture.setVisibility(View.VISIBLE); + holder.securityLevel.setVisibility(View.GONE); + + Bitmap bm = null; + try { + if (contact.getThumbnailUri() != null) { + bm = + MediaStore.Images.Media.getBitmap( + LinphoneService.instance().getContentResolver(), + contact.getThumbnailUri()); + } + } catch (IOException e) { + Log.e(e); + } + if (bm != null) { + holder.contactPicture.setImageBitmap(bm); + holder.contactPicture.setVisibility(View.VISIBLE); + holder.generatedAvatar.setVisibility(View.GONE); + } else if (generated_avatars) { + holder.generatedAvatar.setText( + generateAvatar( + contact.getFullName() == null + ? contact.getFirstName() + " " + contact.getLastName() + : contact.getFullName())); + holder.generatedAvatar.setVisibility(View.VISIBLE); + } + + if (maskResource != 0) { + holder.avatarMask.setImageResource(maskResource); + } + if (showBorder) { + holder.avatarBorder.setVisibility(View.VISIBLE); + } + } + + public static void displayAvatar(LinphoneContact contact, View v, boolean showBorder) { + displayAvatar(contact, v, showBorder, 0); + } + + public static void displayAvatar(LinphoneContact contact, View v) { + displayAvatar(contact, v, false, 0); + } + + public static void displayAvatar( + LinphoneContact contact, boolean hasLimeX3dhCapability, View v) { + displayAvatar(contact, v); + if (hasLimeX3dhCapability) { + showHasLimeX3dhCapability(v); + } + } + + public static void displayAvatar( + LinphoneContact contact, ChatRoomSecurityLevel securityLevel, View v) { + displayAvatar(contact, v); + setSecurityLevel(securityLevel, v); + } + + public static void displayGroupChatAvatar(View v) { + ContactAvatarHolder holder = new ContactAvatarHolder(v); + holder.contactPicture.setImageResource(R.drawable.chat_group_avatar); + holder.generatedAvatar.setVisibility(View.GONE); + holder.securityLevel.setVisibility(View.GONE); + holder.avatarBorder.setVisibility(View.GONE); + } + + public static void displayGroupChatAvatar(ChatRoomSecurityLevel level, View v) { + displayGroupChatAvatar(v); + setSecurityLevel(level, v); + } +} diff --git a/src/android/org/linphone/ui/ContactSelectView.java b/app/src/main/java/org/linphone/views/ContactSelectView.java similarity index 67% rename from src/android/org/linphone/ui/ContactSelectView.java rename to app/src/main/java/org/linphone/views/ContactSelectView.java index 7f840b462..2a331db6f 100644 --- a/src/android/org/linphone/ui/ContactSelectView.java +++ b/app/src/main/java/org/linphone/views/ContactSelectView.java @@ -1,4 +1,4 @@ -package org.linphone.ui; +package org.linphone.views; /* ContactSelectView.java @@ -24,37 +24,38 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; - import org.linphone.LinphoneManager; import org.linphone.R; import org.linphone.contacts.ContactAddress; public class ContactSelectView extends View { - private TextView contactName; - private ImageView deleteContact; + private final TextView mContactName; + private final ImageView mDeleteContact; public ContactSelectView(Context context) { super(context); - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.contact_selected, null); - contactName = view.findViewById(R.id.sipUri); - deleteContact = view.findViewById(R.id.contactChatDelete); - + mContactName = view.findViewById(R.id.sipUri); + mDeleteContact = view.findViewById(R.id.contactChatDelete); } public void setContactName(ContactAddress ca) { if (ca.getContact() != null) { - contactName.setText(ca.getContact().getFirstName()); + mContactName.setText(ca.getContact().getFirstName()); } else { - LinphoneManager.getLc().createFriendWithAddress(ca.getAddressAsDisplayableString()).getName(); - contactName.setText(ca.getAddressAsDisplayableString()); + LinphoneManager.getLc() + .createFriendWithAddress(ca.getAddressAsDisplayableString()) + .getName(); + mContactName.setText(ca.getAddressAsDisplayableString()); } } public void setListener(OnClickListener listener) { - deleteContact.setOnClickListener(listener); + mDeleteContact.setOnClickListener(listener); } } diff --git a/src/android/org/linphone/ui/Digit.java b/app/src/main/java/org/linphone/views/Digit.java similarity index 73% rename from src/android/org/linphone/ui/Digit.java rename to app/src/main/java/org/linphone/views/Digit.java index cf75c84bc..5d092fedd 100644 --- a/src/android/org/linphone/ui/Digit.java +++ b/app/src/main/java/org/linphone/views/Digit.java @@ -1,4 +1,4 @@ -package org.linphone.ui; +package org.linphone.views; /* Digit.java @@ -19,6 +19,7 @@ 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.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -27,51 +28,20 @@ import android.view.MotionEvent; import android.view.View; import android.widget.Button; import android.widget.Toast; - import org.linphone.LinphoneManager; -import org.linphone.LinphonePreferences; import org.linphone.LinphoneService; import org.linphone.R; import org.linphone.call.CallActivity; import org.linphone.core.Core; -import org.linphone.mediastream.Log; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; +@SuppressLint("AppCompatCustomView") public class Digit extends Button implements AddressAware { private AddressText mAddress; - - public void setAddressWidget(AddressText address) { - mAddress = address; - } - private boolean mPlayDtmf; - public void setPlayDtmf(boolean play) { - mPlayDtmf = play; - } - - @Override - protected void onTextChanged(CharSequence text, int start, int before, - int after) { - super.onTextChanged(text, start, before, after); - - if (text == null || text.length() < 1) { - return; - } - - DialKeyListener lListener = new DialKeyListener(); - setOnClickListener(lListener); - setOnTouchListener(lListener); - - if ("0+".equals(text)) { - setOnLongClickListener(lListener); - } - - if ("1".equals(text)) { - setOnLongClickListener(lListener); - } - } - public Digit(Context context, AttributeSet attrs, int style) { super(context, attrs, style); setLongClickable(true); @@ -87,6 +57,35 @@ public class Digit extends Button implements AddressAware { setLongClickable(true); } + public void setAddressWidget(AddressText address) { + mAddress = address; + } + + public void setPlayDtmf(boolean play) { + mPlayDtmf = play; + } + + @Override + protected void onTextChanged(CharSequence text, int start, int before, int after) { + super.onTextChanged(text, start, before, after); + + if (text == null || text.length() < 1) { + return; + } + + DialKeyListener lListener = new DialKeyListener(); + setOnClickListener(lListener); + setOnTouchListener(lListener); + + if ("0+".equals(text.toString())) { + setOnLongClickListener(lListener); + } + + if ("1".equals(text.toString())) { + setOnLongClickListener(lListener); + } + } + private class DialKeyListener implements OnClickListener, OnTouchListener, OnLongClickListener { final char mKeyCode; boolean mIsDtmfStarted; @@ -98,7 +97,11 @@ public class Digit extends Button implements AddressAware { private boolean linphoneServiceReady() { if (!LinphoneService.isReady()) { Log.w("Service is not ready while pressing digit"); - Toast.makeText(getContext(), getContext().getString(R.string.skipable_error_service_not_ready), Toast.LENGTH_SHORT).show(); + Toast.makeText( + getContext(), + getContext().getString(R.string.skipable_error_service_not_ready), + Toast.LENGTH_SHORT) + .show(); return false; } return true; @@ -125,38 +128,44 @@ public class Digit extends Button implements AddressAware { } if (LinphonePreferences.instance().getDebugPopupAddress() != null - && mAddress.getText().toString().equals(LinphonePreferences.instance().getDebugPopupAddress())) { + && mAddress.getText() + .toString() + .equals(LinphonePreferences.instance().getDebugPopupAddress())) { displayDebugPopup(); } } } - public void displayDebugPopup() { + void displayDebugPopup() { AlertDialog.Builder alertDialog = new AlertDialog.Builder(getContext()); alertDialog.setTitle(getContext().getString(R.string.debug_popup_title)); if (LinphonePreferences.instance().isDebugEnabled()) { - alertDialog.setItems(getContext().getResources().getStringArray(R.array.popup_send_log), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - LinphonePreferences.instance().setDebugEnabled(false); - } - if (which == 1) { - Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); - if (lc != null) { - lc.uploadLogCollection(); + alertDialog.setItems( + getContext().getResources().getStringArray(R.array.popup_send_log), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + LinphonePreferences.instance().setDebugEnabled(false); + } + if (which == 1) { + Core lc = LinphoneManager.getLcIfManagerNotDestroyedOrNull(); + if (lc != null) { + lc.uploadLogCollection(); + } + } } - } - } - }); + }); } else { - alertDialog.setItems(getContext().getResources().getStringArray(R.array.popup_enable_log), new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - LinphonePreferences.instance().setDebugEnabled(true); - } - } - }); + alertDialog.setItems( + getContext().getResources().getStringArray(R.array.popup_enable_log), + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + if (which == 0) { + LinphonePreferences.instance().setDebugEnabled(true); + } + } + }); } alertDialog.show(); mAddress.getEditableText().clear(); @@ -203,7 +212,6 @@ public class Digit extends Button implements AddressAware { return true; } - if (mAddress == null) return true; int lBegin = mAddress.getSelectionStart(); @@ -216,6 +224,4 @@ public class Digit extends Button implements AddressAware { return true; } } - - } diff --git a/src/android/org/linphone/ui/EraseButton.java b/app/src/main/java/org/linphone/views/EraseButton.java similarity index 72% rename from src/android/org/linphone/ui/EraseButton.java rename to app/src/main/java/org/linphone/views/EraseButton.java index 8b8e03924..0d73d0e94 100644 --- a/src/android/org/linphone/ui/EraseButton.java +++ b/app/src/main/java/org/linphone/views/EraseButton.java @@ -16,8 +16,9 @@ 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 org.linphone.ui; +package org.linphone.views; +import android.annotation.SuppressLint; import android.content.Context; import android.text.Editable; import android.text.TextWatcher; @@ -27,52 +28,50 @@ import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.widget.ImageView; -public class EraseButton extends ImageView implements AddressAware, OnClickListener, OnLongClickListener, TextWatcher { +@SuppressLint("AppCompatCustomView") +public class EraseButton extends ImageView + implements AddressAware, OnClickListener, OnLongClickListener, TextWatcher { - private AddressText address; + private AddressText mAddress; public EraseButton(Context context, AttributeSet attrs) { super(context, attrs); + setEnabled(false); setOnClickListener(this); setOnLongClickListener(this); } public void onClick(View v) { - if (address.getText().length() > 0) { - int lBegin = address.getSelectionStart(); + if (mAddress.getText().length() > 0) { + int lBegin = mAddress.getSelectionStart(); if (lBegin == -1) { - lBegin = address.getEditableText().length() - 1; + lBegin = mAddress.getEditableText().length() - 1; } if (lBegin > 0) { - address.getEditableText().delete(lBegin - 1, lBegin); + mAddress.getEditableText().delete(lBegin - 1, lBegin); } } + setEnabled(mAddress.getText().length() > 0); } public boolean onLongClick(View v) { - address.getEditableText().clear(); + mAddress.getEditableText().clear(); return true; } public void setAddressWidget(AddressText view) { - address = view; + mAddress = view; view.addTextChangedListener(this); } + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, - int after) { - } + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @Override public void afterTextChanged(Editable s) { setEnabled(s.length() > 0); } - } diff --git a/src/android/org/linphone/ui/LedPreference.java b/app/src/main/java/org/linphone/views/LedPreference.java similarity index 79% rename from src/android/org/linphone/ui/LedPreference.java rename to app/src/main/java/org/linphone/views/LedPreference.java index 418b9d521..c234f76e1 100644 --- a/src/android/org/linphone/ui/LedPreference.java +++ b/app/src/main/java/org/linphone/views/LedPreference.java @@ -1,4 +1,4 @@ -package org.linphone.ui; +package org.linphone.views; /* LedPreference.java @@ -23,30 +23,29 @@ import android.content.Context; import android.preference.Preference; import android.view.View; import android.widget.ImageView; - import org.linphone.R; public class LedPreference extends Preference { - private int ledDrawable; + private int mLedDrawable; public LedPreference(Context context) { super(context); - ledDrawable = R.drawable.led_disconnected; - this.setWidgetLayoutResource(R.layout.preference_led); + mLedDrawable = R.drawable.led_disconnected; + setWidgetLayoutResource(R.layout.preference_led); } @Override protected void onBindView(final View view) { super.onBindView(view); - final ImageView imageView = (ImageView) view.findViewById(R.id.led); + final ImageView imageView = view.findViewById(R.id.led); if (imageView != null) { - imageView.setImageResource(ledDrawable); + imageView.setImageResource(mLedDrawable); } } public void setLed(int led) { - ledDrawable = led; + mLedDrawable = led; notifyChanged(); } } diff --git a/app/src/main/java/org/linphone/views/LinphoneGL2JNIViewOverlay.java b/app/src/main/java/org/linphone/views/LinphoneGL2JNIViewOverlay.java new file mode 100644 index 000000000..156c21ed9 --- /dev/null +++ b/app/src/main/java/org/linphone/views/LinphoneGL2JNIViewOverlay.java @@ -0,0 +1,180 @@ +package org.linphone.views; + +/* +LinphoneGL2JNIViewOverlay.java +Copyright (C) 2017 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.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.os.Build; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.View; +import android.view.WindowManager; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.core.Call; +import org.linphone.core.CallParams; +import org.linphone.mediastream.Version; +import org.linphone.mediastream.video.AndroidVideoWindowImpl; + +public class LinphoneGL2JNIViewOverlay extends org.linphone.mediastream.video.display.GL2JNIView + implements LinphoneOverlay { + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mParams; + private final DisplayMetrics mMetrics; + private float mX, mY, mTouchX, mTouchY; + private boolean mDragEnabled; + private final AndroidVideoWindowImpl mAndroidVideoWindowImpl; + + public LinphoneGL2JNIViewOverlay(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs); + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + int LAYOUT_FLAG; + if (Build.VERSION.SDK_INT >= Version.API26_O_80) { + LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_PHONE; + } + + mParams = + new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + LAYOUT_FLAG, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + mParams.gravity = Gravity.TOP | Gravity.LEFT; + mMetrics = new DisplayMetrics(); + mWindowManager.getDefaultDisplay().getMetrics(mMetrics); + + mAndroidVideoWindowImpl = + new AndroidVideoWindowImpl( + this, + null, + new AndroidVideoWindowImpl.VideoWindowListener() { + public void onVideoRenderingSurfaceReady( + AndroidVideoWindowImpl vw, SurfaceView surface) { + LinphoneManager.getLc().setNativeVideoWindowId(vw); + } + + public void onVideoRenderingSurfaceDestroyed( + AndroidVideoWindowImpl vw) {} + + public void onVideoPreviewSurfaceReady( + AndroidVideoWindowImpl vw, SurfaceView surface) {} + + public void onVideoPreviewSurfaceDestroyed(AndroidVideoWindowImpl vw) {} + }); + + Call call = LinphoneManager.getLc().getCurrentCall(); + CallParams callParams = call.getCurrentParams(); + mParams.width = callParams.getReceivedVideoDefinition().getWidth(); + mParams.height = callParams.getReceivedVideoDefinition().getHeight(); + LinphoneManager.getLc().setNativeVideoWindowId(mAndroidVideoWindowImpl); + + setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + Context context = LinphoneService.instance(); + Intent intent = new Intent(context, LinphoneActivity.class); + context.startActivity(intent); + } + }); + setOnLongClickListener( + new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + mDragEnabled = true; + return true; + } + }); + } + + public LinphoneGL2JNIViewOverlay(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LinphoneGL2JNIViewOverlay(Context context) { + this(context, null); + } + + @Override + public void destroy() { + mAndroidVideoWindowImpl.release(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + mX = event.getRawX(); + mY = event.getRawY(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mTouchX = event.getX(); + mTouchY = event.getY(); + break; + case MotionEvent.ACTION_MOVE: + if (mDragEnabled) { + updateViewPostion(); + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mTouchX = mTouchY = 0; + mDragEnabled = false; + break; + default: + break; + } + return super.onTouchEvent(event); + } + + private void updateViewPostion() { + mParams.x = + Math.min( + Math.max(0, (int) (mX - mTouchX)), + mMetrics.widthPixels - getMeasuredWidth()); + mParams.y = + Math.min( + Math.max(0, (int) (mY - mTouchY)), + mMetrics.heightPixels - getMeasuredHeight()); + mWindowManager.updateViewLayout(this, mParams); + } + + @Override + public WindowManager.LayoutParams getWindowManagerLayoutParams() { + return mParams; + } + + @Override + public void addToWindowManager(WindowManager windowManager, WindowManager.LayoutParams params) { + windowManager.addView(this, params); + } + + @Override + public void removeFromWindowManager(WindowManager windowManager) { + windowManager.removeViewImmediate(this); + } +} diff --git a/app/src/main/java/org/linphone/views/LinphoneOverlay.java b/app/src/main/java/org/linphone/views/LinphoneOverlay.java new file mode 100644 index 000000000..ff946c38e --- /dev/null +++ b/app/src/main/java/org/linphone/views/LinphoneOverlay.java @@ -0,0 +1,32 @@ +package org.linphone.views; + +/* +LinphoneOverlay.java +Copyright (C) 2018 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.view.WindowManager; + +public interface LinphoneOverlay { + WindowManager.LayoutParams getWindowManagerLayoutParams(); + + void addToWindowManager(WindowManager mWindowManager, WindowManager.LayoutParams params); + + void removeFromWindowManager(WindowManager mWindowManager); + + void destroy(); +} diff --git a/app/src/main/java/org/linphone/views/LinphoneTextureViewOverlay.java b/app/src/main/java/org/linphone/views/LinphoneTextureViewOverlay.java new file mode 100644 index 000000000..74b653643 --- /dev/null +++ b/app/src/main/java/org/linphone/views/LinphoneTextureViewOverlay.java @@ -0,0 +1,187 @@ +package org.linphone.views; + +/* +LinphoneTextureViewOverlay.java +Copyright (C) 2018 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.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.os.Build; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.RelativeLayout; +import org.linphone.LinphoneActivity; +import org.linphone.LinphoneManager; +import org.linphone.LinphoneService; +import org.linphone.core.Call; +import org.linphone.core.CallParams; +import org.linphone.core.Core; +import org.linphone.core.VideoDefinition; +import org.linphone.mediastream.Version; + +public class LinphoneTextureViewOverlay extends RelativeLayout implements LinphoneOverlay { + private final WindowManager mWindowManager; + private final WindowManager.LayoutParams mParams; + private final DisplayMetrics mMetrics; + private float mX, mY, mTouchX, mTouchY; + private boolean mDragEnabled; + private TextureView mRemoteVideo, mLocalPreview; + + public LinphoneTextureViewOverlay(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs); + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + int LAYOUT_FLAG; + if (Build.VERSION.SDK_INT >= Version.API26_O_80) { + LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + LAYOUT_FLAG = WindowManager.LayoutParams.TYPE_PHONE; + } + + mParams = + new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + LAYOUT_FLAG, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + mParams.gravity = Gravity.TOP | Gravity.LEFT; + mMetrics = new DisplayMetrics(); + mWindowManager.getDefaultDisplay().getMetrics(mMetrics); + + Call call = LinphoneManager.getLc().getCurrentCall(); + CallParams callParams = call.getCurrentParams(); + mParams.width = callParams.getReceivedVideoDefinition().getWidth(); + mParams.height = callParams.getReceivedVideoDefinition().getHeight(); + + mRemoteVideo = new TextureView(context); + addView(mRemoteVideo); + mLocalPreview = new TextureView(context); + addView(mLocalPreview); + + RelativeLayout.LayoutParams remoteVideoParams = + new RelativeLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + mRemoteVideo.setLayoutParams(remoteVideoParams); + + VideoDefinition videoSize = call.getCurrentParams().getSentVideoDefinition(); + int localPreviewWidth = videoSize.getWidth(); + int localPreviewHeight = videoSize.getHeight(); + int localPreviewMaxHeight = mParams.height / 4; + localPreviewWidth = localPreviewWidth * localPreviewMaxHeight / localPreviewHeight; + localPreviewHeight = localPreviewMaxHeight; + + RelativeLayout.LayoutParams localPreviewParams = + new RelativeLayout.LayoutParams(localPreviewWidth, localPreviewHeight); + localPreviewParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, TRUE); + localPreviewParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE); + mLocalPreview.setLayoutParams(localPreviewParams); + + Core lc = LinphoneManager.getLc(); + lc.setNativeVideoWindowId(mRemoteVideo); + lc.setNativePreviewWindowId(mLocalPreview); + + setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + Context context = LinphoneService.instance(); + Intent intent = new Intent(context, LinphoneActivity.class); + context.startActivity(intent); + } + }); + setOnLongClickListener( + new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + mDragEnabled = true; + return true; + } + }); + } + + public LinphoneTextureViewOverlay(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public LinphoneTextureViewOverlay(Context context) { + this(context, null); + } + + @Override + public void destroy() {} + + @Override + public boolean onTouchEvent(MotionEvent event) { + mX = event.getRawX(); + mY = event.getRawY(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mTouchX = event.getX(); + mTouchY = event.getY(); + break; + case MotionEvent.ACTION_MOVE: + if (mDragEnabled) { + updateViewPostion(); + } + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + mTouchX = mTouchY = 0; + mDragEnabled = false; + break; + default: + break; + } + return super.onTouchEvent(event); + } + + private void updateViewPostion() { + mParams.x = + Math.min( + Math.max(0, (int) (mX - mTouchX)), + mMetrics.widthPixels - getMeasuredWidth()); + mParams.y = + Math.min( + Math.max(0, (int) (mY - mTouchY)), + mMetrics.heightPixels - getMeasuredHeight()); + mWindowManager.updateViewLayout(this, mParams); + } + + @Override + public WindowManager.LayoutParams getWindowManagerLayoutParams() { + return mParams; + } + + @Override + public void addToWindowManager(WindowManager windowManager, WindowManager.LayoutParams params) { + windowManager.addView(this, params); + } + + @Override + public void removeFromWindowManager(WindowManager windowManager) { + windowManager.removeViewImmediate(this); + } +} diff --git a/app/src/main/java/org/linphone/views/MultiLineWrapContentWidthTextView.java b/app/src/main/java/org/linphone/views/MultiLineWrapContentWidthTextView.java new file mode 100644 index 000000000..402f495b4 --- /dev/null +++ b/app/src/main/java/org/linphone/views/MultiLineWrapContentWidthTextView.java @@ -0,0 +1,77 @@ +package org.linphone.views; + +/* +MultiLineWrapContentWidthTextView.java +Copyright (C) 2019 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.annotation.SuppressLint; +import android.content.Context; +import android.text.Layout; +import android.util.AttributeSet; +import android.widget.TextView; +import androidx.annotation.Nullable; + +/** + * The purpose of this class is to have a TextView declared with wrap_content as width that won't + * fill it's parent if it is multi line + */ +@SuppressLint("AppCompatCustomView") +public class MultiLineWrapContentWidthTextView extends TextView { + + public MultiLineWrapContentWidthTextView(Context context) { + super(context); + } + + public MultiLineWrapContentWidthTextView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public MultiLineWrapContentWidthTextView( + Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + int widthMode = MeasureSpec.getMode(widthSpec); + + if (widthMode == MeasureSpec.AT_MOST) { + Layout layout = getLayout(); + if (layout != null) { + int maxWidth = + (int) Math.ceil(getMaxLineWidth(layout)) + + getTotalPaddingLeft() + + getTotalPaddingRight(); + widthSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST); + } + } + + super.onMeasure(widthSpec, heightSpec); + } + + private float getMaxLineWidth(Layout layout) { + float max_width = 0.0f; + int lines = layout.getLineCount(); + for (int i = 0; i < lines; i++) { + if (layout.getLineWidth(i) > max_width) { + max_width = layout.getLineWidth(i); + } + } + return max_width; + } +} diff --git a/src/android/org/linphone/ui/Numpad.java b/app/src/main/java/org/linphone/views/Numpad.java similarity index 88% rename from src/android/org/linphone/ui/Numpad.java rename to app/src/main/java/org/linphone/views/Numpad.java index 15aa1a609..3dbbcdbf7 100644 --- a/src/android/org/linphone/ui/Numpad.java +++ b/app/src/main/java/org/linphone/views/Numpad.java @@ -1,4 +1,4 @@ -package org.linphone.ui; +package org.linphone.views; /* NumpadView.java @@ -26,19 +26,13 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; - -import org.linphone.R; - import java.util.ArrayList; import java.util.Collection; +import org.linphone.R; public class Numpad extends LinearLayout implements AddressAware { - private boolean mPlayDtmf; - - public void setPlayDtmf(boolean sendDtmf) { - this.mPlayDtmf = sendDtmf; - } + private final boolean mPlayDtmf; public Numpad(Context context, boolean playDtmf) { super(context); @@ -71,8 +65,7 @@ public class Numpad extends LinearLayout implements AddressAware { } } - - private final Collection retrieveChildren(ViewGroup viewGroup, Class clazz) { + private Collection retrieveChildren(ViewGroup viewGroup, Class clazz) { final Collection views = new ArrayList<>(); for (int i = 0; i < viewGroup.getChildCount(); i++) { @@ -80,12 +73,10 @@ public class Numpad extends LinearLayout implements AddressAware { if (v instanceof ViewGroup) { views.addAll(retrieveChildren((ViewGroup) v, clazz)); } else { - if (clazz.isInstance(v)) - views.add(clazz.cast(v)); + if (clazz.isInstance(v)) views.add(clazz.cast(v)); } } return views; } - } diff --git a/app/src/main/java/org/linphone/views/RichEditText.java b/app/src/main/java/org/linphone/views/RichEditText.java new file mode 100644 index 000000000..a2bbe6fc2 --- /dev/null +++ b/app/src/main/java/org/linphone/views/RichEditText.java @@ -0,0 +1,90 @@ +package org.linphone.views; + +/* +RichInputEditText.java +Copyright (C) 2019 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.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.EditText; +import androidx.core.view.inputmethod.EditorInfoCompat; +import androidx.core.view.inputmethod.InputConnectionCompat; +import androidx.core.view.inputmethod.InputContentInfoCompat; + +@SuppressLint("AppCompatCustomView") +public class RichEditText extends EditText { + public interface RichInputListener { + boolean onCommitContent( + InputContentInfoCompat inputContentInfo, + int flags, + Bundle opts, + String[] contentMimeTypes); + } + + private RichInputListener mListener; + private String[] mSupportedMimeTypes; + + public RichEditText(Context context) { + super(context); + } + + public RichEditText(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public RichEditText(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public RichEditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public void setListener(RichInputListener listener) { + mListener = listener; + mSupportedMimeTypes = new String[] {"image/png", "image/gif", "image/jpeg"}; + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo editorInfo) { + final InputConnection ic = super.onCreateInputConnection(editorInfo); + EditorInfoCompat.setContentMimeTypes(editorInfo, mSupportedMimeTypes); + + final InputConnectionCompat.OnCommitContentListener callback = + new InputConnectionCompat.OnCommitContentListener() { + @Override + public boolean onCommitContent( + InputContentInfoCompat inputContentInfo, int flags, Bundle opts) { + if (mListener != null) { + return mListener.onCommitContent( + inputContentInfo, flags, opts, mSupportedMimeTypes); + } + return false; + } + }; + + if (ic != null) { + return InputConnectionCompat.createWrapper(ic, editorInfo, callback); + } + return null; + } +} diff --git a/app/src/main/java/org/linphone/xmlrpc/XmlRpcHelper.java b/app/src/main/java/org/linphone/xmlrpc/XmlRpcHelper.java new file mode 100644 index 000000000..79bf8e734 --- /dev/null +++ b/app/src/main/java/org/linphone/xmlrpc/XmlRpcHelper.java @@ -0,0 +1,485 @@ +package org.linphone.xmlrpc; + +/* +XmlRpcHelper.java +Copyright (C) 2017 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.LinphoneManager; +import org.linphone.core.XmlRpcArgType; +import org.linphone.core.XmlRpcRequest; +import org.linphone.core.XmlRpcRequestListener; +import org.linphone.core.XmlRpcSession; +import org.linphone.core.XmlRpcStatus; +import org.linphone.core.tools.Log; +import org.linphone.settings.LinphonePreferences; + +public class XmlRpcHelper { + public static final String SERVER_ERROR_INVALID_ACCOUNT = "ERROR_INVALID_ACCOUNT"; + public static final String SERVER_RESPONSE_OK = "OK"; + public static final String SERVER_ERROR_INCORRECT_PHONE_NUMBER = "ERROR_PHONE_ISNT_E164"; + public static final String SERVER_ERROR_ACCOUNT_DOESNT_EXIST = "ERROR_ACCOUNT_DOESNT_EXIST"; + public static final String SERVER_ERROR_PURCHASE_CANCELLED = "ERROR_PURCHASE_CANCELLED"; + public static final String SERVER_ERROR_RECEIPT_PARSING_FAILED = "ERROR_RECEIPT_PARSING_FAILED"; + public static final String SERVER_ERROR_UID_ALREADY_IN_USE = "ERROR_UID_ALREADY_IN_USE"; + public static final String SERVER_ERROR_SIGNATURE_VERIFICATION_FAILED = + "ERROR_SIGNATURE_VERIFICATION_FAILED"; + public static final String SERVER_ERROR_ACCOUNT_ALREADY_EXISTS = "ERROR_ACCOUNT_ALREADY_EXISTS"; + public static final String SERVER_ERROR_UNKNOWN_ERROR = "ERROR_UNKNOWN_ERROR"; + + public static final String CLIENT_ERROR_INVALID_SERVER_URL = "INVALID_SERVER_URL"; + public static final String CLIENT_ERROR_SERVER_NOT_REACHABLE = "SERVER_NOT_REACHABLE"; + + private final XmlRpcSession mXmlRpcSession; + + public XmlRpcHelper() { + mXmlRpcSession = + LinphoneManager.getLcIfManagerNotDestroyedOrNull() + .createXmlRpcSession( + LinphonePreferences.instance() + .getInAppPurchaseValidatingServerUrl()); + } + + public void createAccountAsync( + final XmlRpcListener listener, String username, String email, String password) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "create_account"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + listener.onAccountCreated(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + xmlRpcRequest.addStringArg(email); + xmlRpcRequest.addStringArg(password == null ? "" : password); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void getAccountExpireAsync( + final XmlRpcListener listener, String username, String password) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "get_account_expiration"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + listener.onAccountExpireFetched(result); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + xmlRpcRequest.addStringArg(password); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void updateAccountExpireAsync( + final XmlRpcListener listener, + String username, + String password, + String domain, + String payload, + String signature) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "update_expiration_date"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + listener.onAccountExpireUpdated(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + xmlRpcRequest.addStringArg(password); + xmlRpcRequest.addStringArg(domain); + xmlRpcRequest.addStringArg(payload); + xmlRpcRequest.addStringArg(signature); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void activateAccountAsync( + final XmlRpcListener listener, String username, String password) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "activate_account"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + listener.onAccountActivated(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + xmlRpcRequest.addStringArg(password); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void isAccountActivatedAsync(final XmlRpcListener listener, String username) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "check_account_activated"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if ("OK".equals(result)) { + listener.onAccountActivatedFetched(); + return; + } else if (!"ERROR_ACCOUNT_NOT_ACTIVATED".equals(result)) { + Log.e(result); + listener.onError(); + } + listener.onAccountActivatedFetched(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void isTrialAccountAsync( + final XmlRpcListener listener, String username, String password) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "is_account_trial"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (!"NOK".equals(result) && !"OK".equals(result)) { + listener.onError(); + } + listener.onTrialAccountFetched("OK".equals(result)); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + xmlRpcRequest.addStringArg(password); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void isAccountAsync(final XmlRpcListener listener, String username) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "check_account_activated"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if ("OK".equals(result)) { + listener.onAccountFetched(); + return; + } else if (!"ERROR_ACCOUNT_DOESNT_EXIST".equals(result)) { + Log.e(result); + listener.onError(); + } + listener.onAccountFetched(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void changeAccountEmailAsync( + final XmlRpcListener listener, String username, String password, String newEmail) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "change_email"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + + listener.onAccountEmailChanged(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + xmlRpcRequest.addStringArg(password); + xmlRpcRequest.addStringArg(newEmail); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void changeAccountPasswordAsync( + final XmlRpcListener listener, + String username, + String oldPassword, + String newPassword) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "change_password"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + + listener.onAccountPasswordChanged(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + xmlRpcRequest.addStringArg(oldPassword); + xmlRpcRequest.addStringArg(newPassword); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void changeAccountHashPasswordAsync( + final XmlRpcListener listener, + String username, + String oldPassword, + String newPassword) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "change_hash"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + + listener.onAccountPasswordChanged(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + xmlRpcRequest.addStringArg(oldPassword); + xmlRpcRequest.addStringArg(newPassword); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void sendRecoverPasswordLinkByEmailAsync( + final XmlRpcListener listener, String usernameOrEmail) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest( + XmlRpcArgType.String, "send_reset_account_password_email"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + + listener.onRecoverPasswordLinkSent(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(usernameOrEmail); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void sendActivateAccountLinkByEmailAsync( + final XmlRpcListener listener, String usernameOrEmail) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "resend_activation_email"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + + listener.onActivateAccountLinkSent(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(usernameOrEmail); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void sendUsernameByEmailAsync(final XmlRpcListener listener, String email) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "recover_username_from_email"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + + listener.onUsernameSent(); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(email); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void verifySignatureAsync( + final XmlRpcListener listener, String payload, String signature) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest(XmlRpcArgType.String, "check_payload_signature"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + Log.w(result); + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + + listener.onSignatureVerified("OK".equals(result)); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(payload); + xmlRpcRequest.addStringArg(signature); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } + + public void getRemoteProvisioningFilenameAsync( + final XmlRpcListener listener, String username, String domain, String password) { + XmlRpcRequest xmlRpcRequest = + mXmlRpcSession.createRequest( + XmlRpcArgType.String, "get_remote_provisioning_filename"); + xmlRpcRequest.setListener( + new XmlRpcRequestListener() { + @Override + public void onResponse(XmlRpcRequest request) { + String result = request.getStringResponse(); + if (request.getStatus() == XmlRpcStatus.Ok) { + if (result.startsWith("ERROR_")) { + Log.e(result); + listener.onError(); + return; + } + + listener.onRemoteProvisioningFilenameSent(result); + } else if (request.getStatus() == XmlRpcStatus.Failed) { + Log.e(result); + listener.onError(); + } + } + }); + xmlRpcRequest.addStringArg(username); + xmlRpcRequest.addStringArg(domain); + xmlRpcRequest.addStringArg(password); + mXmlRpcSession.sendRequest(xmlRpcRequest); + } +} diff --git a/src/android/org/linphone/xmlrpc/XmlRpcListener.java b/app/src/main/java/org/linphone/xmlrpc/XmlRpcListener.java similarity index 65% rename from src/android/org/linphone/xmlrpc/XmlRpcListener.java rename to app/src/main/java/org/linphone/xmlrpc/XmlRpcListener.java index 96104f844..d7c981622 100644 --- a/src/android/org/linphone/xmlrpc/XmlRpcListener.java +++ b/app/src/main/java/org/linphone/xmlrpc/XmlRpcListener.java @@ -19,34 +19,34 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -public interface XmlRpcListener { - void onError(String error); +interface XmlRpcListener { + void onError(); - void onAccountCreated(String result); + void onAccountCreated(); void onAccountExpireFetched(String result); - void onAccountExpireUpdated(String result); + void onAccountExpireUpdated(); - void onAccountActivated(String result); + void onAccountActivated(); - void onAccountActivatedFetched(boolean isActivated); + void onAccountActivatedFetched(); void onTrialAccountFetched(boolean isTrial); - void onAccountFetched(boolean isExisting); + void onAccountFetched(); - void onAccountEmailChanged(String result); + void onAccountEmailChanged(); - void onAccountPasswordChanged(String result); + void onAccountPasswordChanged(); - void onRecoverPasswordLinkSent(String result); + void onRecoverPasswordLinkSent(); - void onActivateAccountLinkSent(String result); + void onActivateAccountLinkSent(); void onSignatureVerified(boolean success); - void onUsernameSent(String result); + void onUsernameSent(); void onRemoteProvisioningFilenameSent(String result); } diff --git a/src/android/org/linphone/xmlrpc/XmlRpcListenerBase.java b/app/src/main/java/org/linphone/xmlrpc/XmlRpcListenerBase.java similarity index 77% rename from src/android/org/linphone/xmlrpc/XmlRpcListenerBase.java rename to app/src/main/java/org/linphone/xmlrpc/XmlRpcListenerBase.java index c0ad7771f..5d49d601f 100644 --- a/src/android/org/linphone/xmlrpc/XmlRpcListenerBase.java +++ b/app/src/main/java/org/linphone/xmlrpc/XmlRpcListenerBase.java @@ -21,13 +21,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. public class XmlRpcListenerBase implements XmlRpcListener { @Override - public void onError(String error) { + public void onError() { // TODO Auto-generated method stub } @Override - public void onAccountCreated(String result) { + public void onAccountCreated() { // TODO Auto-generated method stub } @@ -39,13 +39,13 @@ public class XmlRpcListenerBase implements XmlRpcListener { } @Override - public void onAccountActivated(String result) { + public void onAccountActivated() { // TODO Auto-generated method stub } @Override - public void onAccountActivatedFetched(boolean isActivated) { + public void onAccountActivatedFetched() { // TODO Auto-generated method stub } @@ -57,37 +57,37 @@ public class XmlRpcListenerBase implements XmlRpcListener { } @Override - public void onAccountFetched(boolean isExisting) { + public void onAccountFetched() { // TODO Auto-generated method stub } @Override - public void onAccountEmailChanged(String result) { + public void onAccountEmailChanged() { // TODO Auto-generated method stub } @Override - public void onAccountPasswordChanged(String result) { + public void onAccountPasswordChanged() { // TODO Auto-generated method stub } @Override - public void onRecoverPasswordLinkSent(String result) { + public void onRecoverPasswordLinkSent() { // TODO Auto-generated method stub } @Override - public void onActivateAccountLinkSent(String result) { + public void onActivateAccountLinkSent() { // TODO Auto-generated method stub } @Override - public void onAccountExpireUpdated(String result) { + public void onAccountExpireUpdated() { // TODO Auto-generated method stub } @@ -99,7 +99,7 @@ public class XmlRpcListenerBase implements XmlRpcListener { } @Override - public void onUsernameSent(String result) { + public void onUsernameSent() { // TODO Auto-generated method stub } diff --git a/app/src/main/res/anim/slide_in_bottom_to_top.xml b/app/src/main/res/anim/slide_in_bottom_to_top.xml new file mode 100644 index 000000000..f7bb0ec08 --- /dev/null +++ b/app/src/main/res/anim/slide_in_bottom_to_top.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_left_to_right.xml b/app/src/main/res/anim/slide_in_left_to_right.xml new file mode 100644 index 000000000..d57afb2b5 --- /dev/null +++ b/app/src/main/res/anim/slide_in_left_to_right.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_right_to_left.xml b/app/src/main/res/anim/slide_in_right_to_left.xml new file mode 100644 index 000000000..47fd9ee8a --- /dev/null +++ b/app/src/main/res/anim/slide_in_right_to_left.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_in_top_to_bottom.xml b/app/src/main/res/anim/slide_in_top_to_bottom.xml new file mode 100644 index 000000000..b928aabfa --- /dev/null +++ b/app/src/main/res/anim/slide_in_top_to_bottom.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_bottom_to_top.xml b/app/src/main/res/anim/slide_out_bottom_to_top.xml new file mode 100644 index 000000000..190217cd8 --- /dev/null +++ b/app/src/main/res/anim/slide_out_bottom_to_top.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_left_to_right.xml b/app/src/main/res/anim/slide_out_left_to_right.xml new file mode 100644 index 000000000..fd1dae982 --- /dev/null +++ b/app/src/main/res/anim/slide_out_left_to_right.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_right_to_left.xml b/app/src/main/res/anim/slide_out_right_to_left.xml new file mode 100644 index 000000000..2f19c3f6f --- /dev/null +++ b/app/src/main/res/anim/slide_out_right_to_left.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_out_top_to_bottom.xml b/app/src/main/res/anim/slide_out_top_to_bottom.xml new file mode 100644 index 000000000..fdfe2a714 --- /dev/null +++ b/app/src/main/res/anim/slide_out_top_to_bottom.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/color/dark_primary_text_color.xml b/app/src/main/res/color/dark_primary_text_color.xml new file mode 100644 index 000000000..a0eaff7cb --- /dev/null +++ b/app/src/main/res/color/dark_primary_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/light_primary_text_color.xml b/app/src/main/res/color/light_primary_text_color.xml new file mode 100644 index 000000000..13b5f1ece --- /dev/null +++ b/app/src/main/res/color/light_primary_text_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/security_switch_track_color.xml b/app/src/main/res/color/security_switch_track_color.xml new file mode 100644 index 000000000..2d884e585 --- /dev/null +++ b/app/src/main/res/color/security_switch_track_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/.directory b/app/src/main/res/drawable-xhdpi/.directory new file mode 100644 index 000000000..d5c44ccc7 --- /dev/null +++ b/app/src/main/res/drawable-xhdpi/.directory @@ -0,0 +1,4 @@ +[Dolphin] +PreviewsShown=true +Timestamp=2019,3,5,11,4,59 +Version=4 diff --git a/app/src/main/res/drawable-xhdpi/add_field_default.png b/app/src/main/res/drawable-xhdpi/add_field_default.png new file mode 100644 index 000000000..1d0fbdd34 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/add_field_default.png differ diff --git a/res/drawable-xhdpi/arrow_accept.png b/app/src/main/res/drawable-xhdpi/arrow_accept.png similarity index 100% rename from res/drawable-xhdpi/arrow_accept.png rename to app/src/main/res/drawable-xhdpi/arrow_accept.png diff --git a/res/drawable-xhdpi/arrow_hangup.png b/app/src/main/res/drawable-xhdpi/arrow_hangup.png similarity index 100% rename from res/drawable-xhdpi/arrow_hangup.png rename to app/src/main/res/drawable-xhdpi/arrow_hangup.png diff --git a/res/drawable-xhdpi/avatar.png b/app/src/main/res/drawable-xhdpi/avatar.png similarity index 100% rename from res/drawable-xhdpi/avatar.png rename to app/src/main/res/drawable-xhdpi/avatar.png diff --git a/app/src/main/res/drawable-xhdpi/avatar_border.png b/app/src/main/res/drawable-xhdpi/avatar_border.png new file mode 100644 index 000000000..612e9b10c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/avatar_border.png differ diff --git a/res/drawable-xhdpi/avatar_mask.png b/app/src/main/res/drawable-xhdpi/avatar_mask.png similarity index 100% rename from res/drawable-xhdpi/avatar_mask.png rename to app/src/main/res/drawable-xhdpi/avatar_mask.png diff --git a/res/drawable-xhdpi/back.png b/app/src/main/res/drawable-xhdpi/back_default.png similarity index 100% rename from res/drawable-xhdpi/back.png rename to app/src/main/res/drawable-xhdpi/back_default.png diff --git a/res/drawable-xhdpi/backspace_default.png b/app/src/main/res/drawable-xhdpi/backspace_default.png similarity index 100% rename from res/drawable-xhdpi/backspace_default.png rename to app/src/main/res/drawable-xhdpi/backspace_default.png diff --git a/res/drawable-xhdpi/call_add.png b/app/src/main/res/drawable-xhdpi/call_add.png similarity index 100% rename from res/drawable-xhdpi/call_add.png rename to app/src/main/res/drawable-xhdpi/call_add.png diff --git a/res/drawable-xhdpi/call_alt_start.png b/app/src/main/res/drawable-xhdpi/call_alt_start_default.png similarity index 100% rename from res/drawable-xhdpi/call_alt_start.png rename to app/src/main/res/drawable-xhdpi/call_alt_start_default.png diff --git a/res/drawable-xhdpi/call_audio_start.png b/app/src/main/res/drawable-xhdpi/call_audio_start.png similarity index 100% rename from res/drawable-xhdpi/call_audio_start.png rename to app/src/main/res/drawable-xhdpi/call_audio_start.png diff --git a/res/drawable-xhdpi/call_back.png b/app/src/main/res/drawable-xhdpi/call_back_default.png similarity index 100% rename from res/drawable-xhdpi/call_back.png rename to app/src/main/res/drawable-xhdpi/call_back_default.png diff --git a/res/drawable-xhdpi/call_hangup.png b/app/src/main/res/drawable-xhdpi/call_hangup.png similarity index 100% rename from res/drawable-xhdpi/call_hangup.png rename to app/src/main/res/drawable-xhdpi/call_hangup.png diff --git a/res/drawable-xhdpi/call_incoming.png b/app/src/main/res/drawable-xhdpi/call_incoming.png similarity index 100% rename from res/drawable-xhdpi/call_incoming.png rename to app/src/main/res/drawable-xhdpi/call_incoming.png diff --git a/res/drawable-xhdpi/call_missed.png b/app/src/main/res/drawable-xhdpi/call_missed.png similarity index 100% rename from res/drawable-xhdpi/call_missed.png rename to app/src/main/res/drawable-xhdpi/call_missed.png diff --git a/res/drawable-xhdpi/call_outgoing.png b/app/src/main/res/drawable-xhdpi/call_outgoing.png similarity index 100% rename from res/drawable-xhdpi/call_outgoing.png rename to app/src/main/res/drawable-xhdpi/call_outgoing.png diff --git a/res/drawable-xhdpi/call_quality_indicator_0.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_0.png similarity index 100% rename from res/drawable-xhdpi/call_quality_indicator_0.png rename to app/src/main/res/drawable-xhdpi/call_quality_indicator_0.png diff --git a/res/drawable-xhdpi/call_quality_indicator_1.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_1.png similarity index 100% rename from res/drawable-xhdpi/call_quality_indicator_1.png rename to app/src/main/res/drawable-xhdpi/call_quality_indicator_1.png diff --git a/res/drawable-xhdpi/call_quality_indicator_2.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_2.png similarity index 100% rename from res/drawable-xhdpi/call_quality_indicator_2.png rename to app/src/main/res/drawable-xhdpi/call_quality_indicator_2.png diff --git a/res/drawable-xhdpi/call_quality_indicator_3.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_3.png similarity index 100% rename from res/drawable-xhdpi/call_quality_indicator_3.png rename to app/src/main/res/drawable-xhdpi/call_quality_indicator_3.png diff --git a/res/drawable-xhdpi/call_quality_indicator_4.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_4.png similarity index 100% rename from res/drawable-xhdpi/call_quality_indicator_4.png rename to app/src/main/res/drawable-xhdpi/call_quality_indicator_4.png diff --git a/app/src/main/res/drawable-xhdpi/call_start_default.png b/app/src/main/res/drawable-xhdpi/call_start_default.png new file mode 100644 index 000000000..bcbd2e068 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/call_start_default.png differ diff --git a/res/drawable-xhdpi/call_status_incoming.png b/app/src/main/res/drawable-xhdpi/call_status_incoming.png similarity index 100% rename from res/drawable-xhdpi/call_status_incoming.png rename to app/src/main/res/drawable-xhdpi/call_status_incoming.png diff --git a/res/drawable-xhdpi/call_status_missed.png b/app/src/main/res/drawable-xhdpi/call_status_missed.png similarity index 100% rename from res/drawable-xhdpi/call_status_missed.png rename to app/src/main/res/drawable-xhdpi/call_status_missed.png diff --git a/res/drawable-xhdpi/call_status_outgoing.png b/app/src/main/res/drawable-xhdpi/call_status_outgoing.png similarity index 100% rename from res/drawable-xhdpi/call_status_outgoing.png rename to app/src/main/res/drawable-xhdpi/call_status_outgoing.png diff --git a/res/drawable-xhdpi/call_transfer.png b/app/src/main/res/drawable-xhdpi/call_transfer.png similarity index 100% rename from res/drawable-xhdpi/call_transfer.png rename to app/src/main/res/drawable-xhdpi/call_transfer.png diff --git a/res/drawable-xhdpi/call_video_start.png b/app/src/main/res/drawable-xhdpi/call_video_start.png similarity index 100% rename from res/drawable-xhdpi/call_video_start.png rename to app/src/main/res/drawable-xhdpi/call_video_start.png diff --git a/res/drawable-xhdpi/camera_default.png b/app/src/main/res/drawable-xhdpi/camera_default.png similarity index 100% rename from res/drawable-xhdpi/camera_default.png rename to app/src/main/res/drawable-xhdpi/camera_default.png diff --git a/res/drawable-xhdpi/camera_switch_default.png b/app/src/main/res/drawable-xhdpi/camera_switch_default.png similarity index 100% rename from res/drawable-xhdpi/camera_switch_default.png rename to app/src/main/res/drawable-xhdpi/camera_switch_default.png diff --git a/res/drawable-xhdpi/camera_switch_over.png b/app/src/main/res/drawable-xhdpi/camera_switch_over.png similarity index 100% rename from res/drawable-xhdpi/camera_switch_over.png rename to app/src/main/res/drawable-xhdpi/camera_switch_over.png diff --git a/res/drawable-xhdpi/cancel_edit.png b/app/src/main/res/drawable-xhdpi/cancel_edit_default.png similarity index 100% rename from res/drawable-xhdpi/cancel_edit.png rename to app/src/main/res/drawable-xhdpi/cancel_edit_default.png diff --git a/app/src/main/res/drawable-xhdpi/chat_file_default.png b/app/src/main/res/drawable-xhdpi/chat_file_default.png new file mode 100644 index 000000000..8aa4bbdb4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chat_file_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/chat_file_over.png b/app/src/main/res/drawable-xhdpi/chat_file_over.png new file mode 100644 index 000000000..3781846f9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chat_file_over.png differ diff --git a/res/drawable-xhdpi/chat_group_add.png b/app/src/main/res/drawable-xhdpi/chat_group_add.png similarity index 100% rename from res/drawable-xhdpi/chat_group_add.png rename to app/src/main/res/drawable-xhdpi/chat_group_add.png diff --git a/res/drawable-xhdpi/chat_group_avatar.png b/app/src/main/res/drawable-xhdpi/chat_group_avatar.png similarity index 100% rename from res/drawable-xhdpi/chat_group_avatar.png rename to app/src/main/res/drawable-xhdpi/chat_group_avatar.png diff --git a/res/drawable-xhdpi/chat_group_delete.png b/app/src/main/res/drawable-xhdpi/chat_group_delete.png similarity index 100% rename from res/drawable-xhdpi/chat_group_delete.png rename to app/src/main/res/drawable-xhdpi/chat_group_delete.png diff --git a/res/drawable-xhdpi/chat_group_informations.png b/app/src/main/res/drawable-xhdpi/chat_group_informations_default.png similarity index 100% rename from res/drawable-xhdpi/chat_group_informations.png rename to app/src/main/res/drawable-xhdpi/chat_group_informations_default.png diff --git a/app/src/main/res/drawable-xhdpi/chat_group_new_default.png b/app/src/main/res/drawable-xhdpi/chat_group_new_default.png new file mode 100644 index 000000000..50d061eed Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chat_group_new_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/chat_new_default.png b/app/src/main/res/drawable-xhdpi/chat_new_default.png new file mode 100644 index 000000000..2c2f41889 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chat_new_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/chat_send_default.png b/app/src/main/res/drawable-xhdpi/chat_send_default.png new file mode 100644 index 000000000..6f16bc29e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chat_send_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/chat_send_over.png b/app/src/main/res/drawable-xhdpi/chat_send_over.png new file mode 100644 index 000000000..cd1f69268 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chat_send_over.png differ diff --git a/app/src/main/res/drawable-xhdpi/chat_start_default.png b/app/src/main/res/drawable-xhdpi/chat_start_default.png new file mode 100644 index 000000000..3bb7892f9 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chat_start_default.png differ diff --git a/res/drawable-xhdpi/chat_unsecure.png b/app/src/main/res/drawable-xhdpi/chat_unsecure.png similarity index 100% rename from res/drawable-xhdpi/chat_unsecure.png rename to app/src/main/res/drawable-xhdpi/chat_unsecure.png diff --git a/res/drawable-xhdpi/check_selected.png b/app/src/main/res/drawable-xhdpi/check_selected.png similarity index 100% rename from res/drawable-xhdpi/check_selected.png rename to app/src/main/res/drawable-xhdpi/check_selected.png diff --git a/res/drawable-xhdpi/check_unselected.png b/app/src/main/res/drawable-xhdpi/check_unselected.png similarity index 100% rename from res/drawable-xhdpi/check_unselected.png rename to app/src/main/res/drawable-xhdpi/check_unselected.png diff --git a/res/drawable-xhdpi/checkbox_checked.png b/app/src/main/res/drawable-xhdpi/checkbox_checked.png similarity index 100% rename from res/drawable-xhdpi/checkbox_checked.png rename to app/src/main/res/drawable-xhdpi/checkbox_checked.png diff --git a/res/drawable-xhdpi/checkbox_unchecked.png b/app/src/main/res/drawable-xhdpi/checkbox_unchecked.png similarity index 100% rename from res/drawable-xhdpi/checkbox_unchecked.png rename to app/src/main/res/drawable-xhdpi/checkbox_unchecked.png diff --git a/app/src/main/res/drawable-xhdpi/chevron_list_close_default.png b/app/src/main/res/drawable-xhdpi/chevron_list_close_default.png new file mode 100644 index 000000000..80de198cc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chevron_list_close_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/chevron_list_open_default.png b/app/src/main/res/drawable-xhdpi/chevron_list_open_default.png new file mode 100644 index 000000000..422f69f9c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/chevron_list_open_default.png differ diff --git a/res/drawable-xhdpi/clean_field_default.png b/app/src/main/res/drawable-xhdpi/clean_field_default.png similarity index 100% rename from res/drawable-xhdpi/clean_field_default.png rename to app/src/main/res/drawable-xhdpi/clean_field_default.png diff --git a/res/drawable-xhdpi/clean_field_over.png b/app/src/main/res/drawable-xhdpi/clean_field_over.png similarity index 100% rename from res/drawable-xhdpi/clean_field_over.png rename to app/src/main/res/drawable-xhdpi/clean_field_over.png diff --git a/res/drawable-xhdpi/conference_exit_default.png b/app/src/main/res/drawable-xhdpi/conference_exit_default.png similarity index 100% rename from res/drawable-xhdpi/conference_exit_default.png rename to app/src/main/res/drawable-xhdpi/conference_exit_default.png diff --git a/res/drawable-xhdpi/conference_start.png b/app/src/main/res/drawable-xhdpi/conference_start.png similarity index 100% rename from res/drawable-xhdpi/conference_start.png rename to app/src/main/res/drawable-xhdpi/conference_start.png diff --git a/res/drawable-xhdpi/contact_add.png b/app/src/main/res/drawable-xhdpi/contact_add_default.png similarity index 100% rename from res/drawable-xhdpi/contact_add.png rename to app/src/main/res/drawable-xhdpi/contact_add_default.png diff --git a/res/drawable-xhdpi/contact.png b/app/src/main/res/drawable-xhdpi/contact_default.png similarity index 100% rename from res/drawable-xhdpi/contact.png rename to app/src/main/res/drawable-xhdpi/contact_default.png diff --git a/res/drawable-xhdpi/contacts_all_default.png b/app/src/main/res/drawable-xhdpi/contacts_all_default.png similarity index 100% rename from res/drawable-xhdpi/contacts_all_default.png rename to app/src/main/res/drawable-xhdpi/contacts_all_default.png diff --git a/res/drawable-xhdpi/contacts_sip_default.png b/app/src/main/res/drawable-xhdpi/contacts_sip_default.png similarity index 100% rename from res/drawable-xhdpi/contacts_sip_default.png rename to app/src/main/res/drawable-xhdpi/contacts_sip_default.png diff --git a/res/drawable-xhdpi/delete.png b/app/src/main/res/drawable-xhdpi/delete_default.png similarity index 100% rename from res/drawable-xhdpi/delete.png rename to app/src/main/res/drawable-xhdpi/delete_default.png diff --git a/res/drawable-xhdpi/delete_field_default.png b/app/src/main/res/drawable-xhdpi/delete_field_default.png similarity index 100% rename from res/drawable-xhdpi/delete_field_default.png rename to app/src/main/res/drawable-xhdpi/delete_field_default.png diff --git a/res/drawable-xhdpi/delete_field_over.png b/app/src/main/res/drawable-xhdpi/delete_field_over.png similarity index 100% rename from res/drawable-xhdpi/delete_field_over.png rename to app/src/main/res/drawable-xhdpi/delete_field_over.png diff --git a/res/drawable-xhdpi/deselect_all.png b/app/src/main/res/drawable-xhdpi/deselect_all_default.png similarity index 100% rename from res/drawable-xhdpi/deselect_all.png rename to app/src/main/res/drawable-xhdpi/deselect_all_default.png diff --git a/res/drawable-xhdpi/dialer_alt_back.png b/app/src/main/res/drawable-xhdpi/dialer_alt_back.png similarity index 100% rename from res/drawable-xhdpi/dialer_alt_back.png rename to app/src/main/res/drawable-xhdpi/dialer_alt_back.png diff --git a/res/drawable-xhdpi/dialer_background.png b/app/src/main/res/drawable-xhdpi/dialer_background.png similarity index 100% rename from res/drawable-xhdpi/dialer_background.png rename to app/src/main/res/drawable-xhdpi/dialer_background.png diff --git a/res/drawable-xhdpi/edit.png b/app/src/main/res/drawable-xhdpi/edit_default.png similarity index 100% rename from res/drawable-xhdpi/edit.png rename to app/src/main/res/drawable-xhdpi/edit_default.png diff --git a/res/drawable-xhdpi/edit_list.png b/app/src/main/res/drawable-xhdpi/edit_list_default.png similarity index 100% rename from res/drawable-xhdpi/edit_list.png rename to app/src/main/res/drawable-xhdpi/edit_list_default.png diff --git a/res/drawable-xhdpi/footer_chat.png b/app/src/main/res/drawable-xhdpi/footer_chat.png similarity index 100% rename from res/drawable-xhdpi/footer_chat.png rename to app/src/main/res/drawable-xhdpi/footer_chat.png diff --git a/res/drawable-xhdpi/footer_contacts.png b/app/src/main/res/drawable-xhdpi/footer_contacts.png similarity index 100% rename from res/drawable-xhdpi/footer_contacts.png rename to app/src/main/res/drawable-xhdpi/footer_contacts.png diff --git a/res/drawable-xhdpi/footer_dialer.png b/app/src/main/res/drawable-xhdpi/footer_dialer.png similarity index 100% rename from res/drawable-xhdpi/footer_dialer.png rename to app/src/main/res/drawable-xhdpi/footer_dialer.png diff --git a/res/drawable-xhdpi/footer_history.png b/app/src/main/res/drawable-xhdpi/footer_history.png similarity index 100% rename from res/drawable-xhdpi/footer_history.png rename to app/src/main/res/drawable-xhdpi/footer_history.png diff --git a/res/drawable-xhdpi/history_all_default.png b/app/src/main/res/drawable-xhdpi/history_all_default.png similarity index 100% rename from res/drawable-xhdpi/history_all_default.png rename to app/src/main/res/drawable-xhdpi/history_all_default.png diff --git a/res/drawable-xhdpi/history_missed_default.png b/app/src/main/res/drawable-xhdpi/history_missed_default.png similarity index 100% rename from res/drawable-xhdpi/history_missed_default.png rename to app/src/main/res/drawable-xhdpi/history_missed_default.png diff --git a/app/src/main/res/drawable-xhdpi/imdn_error.png b/app/src/main/res/drawable-xhdpi/imdn_error.png new file mode 100644 index 000000000..c3f8b3251 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/imdn_error.png differ diff --git a/app/src/main/res/drawable-xhdpi/imdn_read.png b/app/src/main/res/drawable-xhdpi/imdn_read.png new file mode 100644 index 000000000..534ea87cf Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/imdn_read.png differ diff --git a/app/src/main/res/drawable-xhdpi/imdn_received.png b/app/src/main/res/drawable-xhdpi/imdn_received.png new file mode 100644 index 000000000..592684b07 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/imdn_received.png differ diff --git a/res/drawable-xhdpi/info.png b/app/src/main/res/drawable-xhdpi/info.png similarity index 100% rename from res/drawable-xhdpi/info.png rename to app/src/main/res/drawable-xhdpi/info.png diff --git a/res/drawable-xhdpi/led_connected.png b/app/src/main/res/drawable-xhdpi/led_connected.png similarity index 100% rename from res/drawable-xhdpi/led_connected.png rename to app/src/main/res/drawable-xhdpi/led_connected.png diff --git a/res/drawable-xhdpi/led_disconnected.png b/app/src/main/res/drawable-xhdpi/led_disconnected.png similarity index 100% rename from res/drawable-xhdpi/led_disconnected.png rename to app/src/main/res/drawable-xhdpi/led_disconnected.png diff --git a/res/drawable-xhdpi/led_error.png b/app/src/main/res/drawable-xhdpi/led_error.png similarity index 100% rename from res/drawable-xhdpi/led_error.png rename to app/src/main/res/drawable-xhdpi/led_error.png diff --git a/res/drawable-xhdpi/led_inprogress.png b/app/src/main/res/drawable-xhdpi/led_inprogress.png similarity index 100% rename from res/drawable-xhdpi/led_inprogress.png rename to app/src/main/res/drawable-xhdpi/led_inprogress.png diff --git a/res/drawable-xhdpi/linphone_logo.png b/app/src/main/res/drawable-xhdpi/linphone_logo.png similarity index 100% rename from res/drawable-xhdpi/linphone_logo.png rename to app/src/main/res/drawable-xhdpi/linphone_logo.png diff --git a/res/drawable-xhdpi/linphone_logo_orange.png b/app/src/main/res/drawable-xhdpi/linphone_logo_orange.png similarity index 100% rename from res/drawable-xhdpi/linphone_logo_orange.png rename to app/src/main/res/drawable-xhdpi/linphone_logo_orange.png diff --git a/res/drawable-xhdpi/linphone_notification_icon.png b/app/src/main/res/drawable-xhdpi/linphone_notification_icon.png similarity index 100% rename from res/drawable-xhdpi/linphone_notification_icon.png rename to app/src/main/res/drawable-xhdpi/linphone_notification_icon.png diff --git a/res/drawable-xhdpi/linphone_user.png b/app/src/main/res/drawable-xhdpi/linphone_user.png similarity index 100% rename from res/drawable-xhdpi/linphone_user.png rename to app/src/main/res/drawable-xhdpi/linphone_user.png diff --git a/app/src/main/res/drawable-xhdpi/list_details_default.png b/app/src/main/res/drawable-xhdpi/list_details_default.png new file mode 100644 index 000000000..756af521b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/list_details_default.png differ diff --git a/res/drawable-xhdpi/list_details_over.png b/app/src/main/res/drawable-xhdpi/list_details_over.png similarity index 100% rename from res/drawable-xhdpi/list_details_over.png rename to app/src/main/res/drawable-xhdpi/list_details_over.png diff --git a/app/src/main/res/drawable-xhdpi/menu_about_default.png b/app/src/main/res/drawable-xhdpi/menu_about_default.png new file mode 100644 index 000000000..37b24bbaa Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/menu_about_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/menu_assistant_default.png b/app/src/main/res/drawable-xhdpi/menu_assistant_default.png new file mode 100644 index 000000000..8b6955242 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/menu_assistant_default.png differ diff --git a/res/drawable-xhdpi/menu.png b/app/src/main/res/drawable-xhdpi/menu_default.png similarity index 100% rename from res/drawable-xhdpi/menu.png rename to app/src/main/res/drawable-xhdpi/menu_default.png diff --git a/app/src/main/res/drawable-xhdpi/menu_disabled.png b/app/src/main/res/drawable-xhdpi/menu_disabled.png new file mode 100644 index 000000000..072589694 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/menu_disabled.png differ diff --git a/app/src/main/res/drawable-xhdpi/menu_options_default.png b/app/src/main/res/drawable-xhdpi/menu_options_default.png new file mode 100644 index 000000000..3a1c7800f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/menu_options_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/menu_recordings_default.png b/app/src/main/res/drawable-xhdpi/menu_recordings_default.png new file mode 100644 index 000000000..3bef81d13 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/menu_recordings_default.png differ diff --git a/res/drawable-xhdpi/message_delivered.png b/app/src/main/res/drawable-xhdpi/message_delivered.png similarity index 100% rename from res/drawable-xhdpi/message_delivered.png rename to app/src/main/res/drawable-xhdpi/message_delivered.png diff --git a/res/drawable-xhdpi/message_read.png b/app/src/main/res/drawable-xhdpi/message_read.png similarity index 100% rename from res/drawable-xhdpi/message_read.png rename to app/src/main/res/drawable-xhdpi/message_read.png diff --git a/res/drawable-xhdpi/message_undelivered.png b/app/src/main/res/drawable-xhdpi/message_undelivered.png similarity index 100% rename from res/drawable-xhdpi/message_undelivered.png rename to app/src/main/res/drawable-xhdpi/message_undelivered.png diff --git a/res/drawable-xhdpi/micro_default.png b/app/src/main/res/drawable-xhdpi/micro_default.png similarity index 100% rename from res/drawable-xhdpi/micro_default.png rename to app/src/main/res/drawable-xhdpi/micro_default.png diff --git a/res/drawable-xhdpi/next.png b/app/src/main/res/drawable-xhdpi/next_default.png similarity index 100% rename from res/drawable-xhdpi/next.png rename to app/src/main/res/drawable-xhdpi/next_default.png diff --git a/res/drawable-xhdpi/numpad_0.png b/app/src/main/res/drawable-xhdpi/numpad_0.png similarity index 100% rename from res/drawable-xhdpi/numpad_0.png rename to app/src/main/res/drawable-xhdpi/numpad_0.png diff --git a/res/drawable-xhdpi/numpad_1.png b/app/src/main/res/drawable-xhdpi/numpad_1.png similarity index 100% rename from res/drawable-xhdpi/numpad_1.png rename to app/src/main/res/drawable-xhdpi/numpad_1.png diff --git a/res/drawable-xhdpi/numpad_2.png b/app/src/main/res/drawable-xhdpi/numpad_2.png similarity index 100% rename from res/drawable-xhdpi/numpad_2.png rename to app/src/main/res/drawable-xhdpi/numpad_2.png diff --git a/res/drawable-xhdpi/numpad_3.png b/app/src/main/res/drawable-xhdpi/numpad_3.png similarity index 100% rename from res/drawable-xhdpi/numpad_3.png rename to app/src/main/res/drawable-xhdpi/numpad_3.png diff --git a/res/drawable-xhdpi/numpad_4.png b/app/src/main/res/drawable-xhdpi/numpad_4.png similarity index 100% rename from res/drawable-xhdpi/numpad_4.png rename to app/src/main/res/drawable-xhdpi/numpad_4.png diff --git a/res/drawable-xhdpi/numpad_5.png b/app/src/main/res/drawable-xhdpi/numpad_5.png similarity index 100% rename from res/drawable-xhdpi/numpad_5.png rename to app/src/main/res/drawable-xhdpi/numpad_5.png diff --git a/res/drawable-xhdpi/numpad_6.png b/app/src/main/res/drawable-xhdpi/numpad_6.png similarity index 100% rename from res/drawable-xhdpi/numpad_6.png rename to app/src/main/res/drawable-xhdpi/numpad_6.png diff --git a/res/drawable-xhdpi/numpad_7.png b/app/src/main/res/drawable-xhdpi/numpad_7.png similarity index 100% rename from res/drawable-xhdpi/numpad_7.png rename to app/src/main/res/drawable-xhdpi/numpad_7.png diff --git a/res/drawable-xhdpi/numpad_8.png b/app/src/main/res/drawable-xhdpi/numpad_8.png similarity index 100% rename from res/drawable-xhdpi/numpad_8.png rename to app/src/main/res/drawable-xhdpi/numpad_8.png diff --git a/res/drawable-xhdpi/numpad_9.png b/app/src/main/res/drawable-xhdpi/numpad_9.png similarity index 100% rename from res/drawable-xhdpi/numpad_9.png rename to app/src/main/res/drawable-xhdpi/numpad_9.png diff --git a/res/drawable-xhdpi/numpad_hash.png b/app/src/main/res/drawable-xhdpi/numpad_hash.png similarity index 100% rename from res/drawable-xhdpi/numpad_hash.png rename to app/src/main/res/drawable-xhdpi/numpad_hash.png diff --git a/res/drawable-xhdpi/numpad_star.png b/app/src/main/res/drawable-xhdpi/numpad_star.png similarity index 100% rename from res/drawable-xhdpi/numpad_star.png rename to app/src/main/res/drawable-xhdpi/numpad_star.png diff --git a/res/drawable-xhdpi/options_add_call.png b/app/src/main/res/drawable-xhdpi/options_add_call_default.png similarity index 100% rename from res/drawable-xhdpi/options_add_call.png rename to app/src/main/res/drawable-xhdpi/options_add_call_default.png diff --git a/res/drawable-xhdpi/options_default.png b/app/src/main/res/drawable-xhdpi/options_default.png similarity index 100% rename from res/drawable-xhdpi/options_default.png rename to app/src/main/res/drawable-xhdpi/options_default.png diff --git a/app/src/main/res/drawable-xhdpi/options_rec_default.png b/app/src/main/res/drawable-xhdpi/options_rec_default.png new file mode 100644 index 000000000..075c09314 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/options_rec_default.png differ diff --git a/res/drawable-xhdpi/options_start_conference.png b/app/src/main/res/drawable-xhdpi/options_start_conference_default.png similarity index 100% rename from res/drawable-xhdpi/options_start_conference.png rename to app/src/main/res/drawable-xhdpi/options_start_conference_default.png diff --git a/res/drawable-xhdpi/options_transfer_call.png b/app/src/main/res/drawable-xhdpi/options_transfer_call_default.png similarity index 100% rename from res/drawable-xhdpi/options_transfer_call.png rename to app/src/main/res/drawable-xhdpi/options_transfer_call_default.png diff --git a/res/drawable-xhdpi/pause_big_default.png b/app/src/main/res/drawable-xhdpi/pause_default.png similarity index 100% rename from res/drawable-xhdpi/pause_big_default.png rename to app/src/main/res/drawable-xhdpi/pause_default.png diff --git a/res/drawable-xhdpi/pause_big_disabled.png b/app/src/main/res/drawable-xhdpi/pause_disabled.png similarity index 100% rename from res/drawable-xhdpi/pause_big_disabled.png rename to app/src/main/res/drawable-xhdpi/pause_disabled.png diff --git a/res/drawable-xhdpi/pause_big_over_selected.png b/app/src/main/res/drawable-xhdpi/pause_over_selected.png similarity index 100% rename from res/drawable-xhdpi/pause_big_over_selected.png rename to app/src/main/res/drawable-xhdpi/pause_over_selected.png diff --git a/res/drawable-xhdpi/quit_default.png b/app/src/main/res/drawable-xhdpi/quit_default.png similarity index 100% rename from res/drawable-xhdpi/quit_default.png rename to app/src/main/res/drawable-xhdpi/quit_default.png diff --git a/res/drawable-xhdpi/quit_over.png b/app/src/main/res/drawable-xhdpi/quit_over.png similarity index 100% rename from res/drawable-xhdpi/quit_over.png rename to app/src/main/res/drawable-xhdpi/quit_over.png diff --git a/app/src/main/res/drawable-xhdpi/record_pause.png b/app/src/main/res/drawable-xhdpi/record_pause.png new file mode 100644 index 000000000..3b9311e43 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/record_pause.png differ diff --git a/app/src/main/res/drawable-xhdpi/record_play.png b/app/src/main/res/drawable-xhdpi/record_play.png new file mode 100644 index 000000000..486dfb0de Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/record_play.png differ diff --git a/res/drawable-xhdpi/resizable_assistant_button.9.png b/app/src/main/res/drawable-xhdpi/resizable_assistant_button_default.9.png similarity index 100% rename from res/drawable-xhdpi/resizable_assistant_button.9.png rename to app/src/main/res/drawable-xhdpi/resizable_assistant_button_default.9.png diff --git a/res/drawable-xhdpi/resizable_assistant_button_disabled.9.png b/app/src/main/res/drawable-xhdpi/resizable_assistant_button_disabled.9.png similarity index 100% rename from res/drawable-xhdpi/resizable_assistant_button_disabled.9.png rename to app/src/main/res/drawable-xhdpi/resizable_assistant_button_disabled.9.png diff --git a/res/drawable-xhdpi/resizable_assistant_button_over.9.png b/app/src/main/res/drawable-xhdpi/resizable_assistant_button_over.9.png similarity index 100% rename from res/drawable-xhdpi/resizable_assistant_button_over.9.png rename to app/src/main/res/drawable-xhdpi/resizable_assistant_button_over.9.png diff --git a/res/drawable-xhdpi/resizable_chat_bubble_incoming.9.png b/app/src/main/res/drawable-xhdpi/resizable_chat_bubble_incoming.9.png similarity index 100% rename from res/drawable-xhdpi/resizable_chat_bubble_incoming.9.png rename to app/src/main/res/drawable-xhdpi/resizable_chat_bubble_incoming.9.png diff --git a/res/drawable-xhdpi/resizable_chat_bubble_outgoing.9.png b/app/src/main/res/drawable-xhdpi/resizable_chat_bubble_outgoing.9.png similarity index 100% rename from res/drawable-xhdpi/resizable_chat_bubble_outgoing.9.png rename to app/src/main/res/drawable-xhdpi/resizable_chat_bubble_outgoing.9.png diff --git a/res/drawable-xhdpi/resizable_confirm_delete_button.9.png b/app/src/main/res/drawable-xhdpi/resizable_confirm_delete_button.9.png similarity index 100% rename from res/drawable-xhdpi/resizable_confirm_delete_button.9.png rename to app/src/main/res/drawable-xhdpi/resizable_confirm_delete_button.9.png diff --git a/res/drawable-xhdpi/resizable_textfield.9.png b/app/src/main/res/drawable-xhdpi/resizable_textfield.9.png similarity index 100% rename from res/drawable-xhdpi/resizable_textfield.9.png rename to app/src/main/res/drawable-xhdpi/resizable_textfield.9.png diff --git a/res/drawable-xhdpi/resizable_textfield_error.9.png b/app/src/main/res/drawable-xhdpi/resizable_textfield_error.9.png similarity index 100% rename from res/drawable-xhdpi/resizable_textfield_error.9.png rename to app/src/main/res/drawable-xhdpi/resizable_textfield_error.9.png diff --git a/res/drawable-xhdpi/route_bluetooth.png b/app/src/main/res/drawable-xhdpi/route_bluetooth_default.png similarity index 100% rename from res/drawable-xhdpi/route_bluetooth.png rename to app/src/main/res/drawable-xhdpi/route_bluetooth_default.png diff --git a/res/drawable-xhdpi/route_earpiece.png b/app/src/main/res/drawable-xhdpi/route_earpiece_default.png similarity index 100% rename from res/drawable-xhdpi/route_earpiece.png rename to app/src/main/res/drawable-xhdpi/route_earpiece_default.png diff --git a/res/drawable-xhdpi/route_speaker.png b/app/src/main/res/drawable-xhdpi/route_speaker_default.png similarity index 100% rename from res/drawable-xhdpi/route_speaker.png rename to app/src/main/res/drawable-xhdpi/route_speaker_default.png diff --git a/res/drawable-xhdpi/routes_default.png b/app/src/main/res/drawable-xhdpi/routes_default.png similarity index 100% rename from res/drawable-xhdpi/routes_default.png rename to app/src/main/res/drawable-xhdpi/routes_default.png diff --git a/app/src/main/res/drawable-xhdpi/security_1_indicator.png b/app/src/main/res/drawable-xhdpi/security_1_indicator.png new file mode 100644 index 000000000..0aa4a2e04 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/security_1_indicator.png differ diff --git a/app/src/main/res/drawable-xhdpi/security_2_indicator.png b/app/src/main/res/drawable-xhdpi/security_2_indicator.png new file mode 100644 index 000000000..910e3d124 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/security_2_indicator.png differ diff --git a/app/src/main/res/drawable-xhdpi/security_alert_indicator.png b/app/src/main/res/drawable-xhdpi/security_alert_indicator.png new file mode 100644 index 000000000..54a340667 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/security_alert_indicator.png differ diff --git a/res/drawable-xhdpi/security_ko.png b/app/src/main/res/drawable-xhdpi/security_ko.png similarity index 100% rename from res/drawable-xhdpi/security_ko.png rename to app/src/main/res/drawable-xhdpi/security_ko.png diff --git a/res/drawable-xhdpi/security_ok.png b/app/src/main/res/drawable-xhdpi/security_ok.png similarity index 100% rename from res/drawable-xhdpi/security_ok.png rename to app/src/main/res/drawable-xhdpi/security_ok.png diff --git a/res/drawable-xhdpi/security_pending.png b/app/src/main/res/drawable-xhdpi/security_pending.png similarity index 100% rename from res/drawable-xhdpi/security_pending.png rename to app/src/main/res/drawable-xhdpi/security_pending.png diff --git a/app/src/main/res/drawable-xhdpi/security_toogle_icon_green.png b/app/src/main/res/drawable-xhdpi/security_toogle_icon_green.png new file mode 100644 index 000000000..368e30c79 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/security_toogle_icon_green.png differ diff --git a/app/src/main/res/drawable-xhdpi/security_toogle_icon_grey.png b/app/src/main/res/drawable-xhdpi/security_toogle_icon_grey.png new file mode 100644 index 000000000..c77235794 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/security_toogle_icon_grey.png differ diff --git a/res/drawable-xhdpi/select_all.png b/app/src/main/res/drawable-xhdpi/select_all_default.png similarity index 100% rename from res/drawable-xhdpi/select_all.png rename to app/src/main/res/drawable-xhdpi/select_all_default.png diff --git a/res/drawable-xhdpi/splashscreen.png b/app/src/main/res/drawable-xhdpi/splashscreen.png similarity index 100% rename from res/drawable-xhdpi/splashscreen.png rename to app/src/main/res/drawable-xhdpi/splashscreen.png diff --git a/res/drawable-xhdpi/topbar_avatar.png b/app/src/main/res/drawable-xhdpi/topbar_avatar.png similarity index 100% rename from res/drawable-xhdpi/topbar_avatar.png rename to app/src/main/res/drawable-xhdpi/topbar_avatar.png diff --git a/res/drawable-xhdpi/topbar_call_notification.png b/app/src/main/res/drawable-xhdpi/topbar_call_notification.png similarity index 100% rename from res/drawable-xhdpi/topbar_call_notification.png rename to app/src/main/res/drawable-xhdpi/topbar_call_notification.png diff --git a/res/drawable-xhdpi/topbar_chat_notification.png b/app/src/main/res/drawable-xhdpi/topbar_chat_notification.png similarity index 100% rename from res/drawable-xhdpi/topbar_chat_notification.png rename to app/src/main/res/drawable-xhdpi/topbar_chat_notification.png diff --git a/res/drawable-xhdpi/topbar_videocall_notification.png b/app/src/main/res/drawable-xhdpi/topbar_videocall_notification.png similarity index 100% rename from res/drawable-xhdpi/topbar_videocall_notification.png rename to app/src/main/res/drawable-xhdpi/topbar_videocall_notification.png diff --git a/res/drawable-xhdpi/valid.png b/app/src/main/res/drawable-xhdpi/valid_default.png similarity index 100% rename from res/drawable-xhdpi/valid.png rename to app/src/main/res/drawable-xhdpi/valid_default.png diff --git a/res/drawable-xhdpi/voicemail.png b/app/src/main/res/drawable-xhdpi/voicemail.png similarity index 100% rename from res/drawable-xhdpi/voicemail.png rename to app/src/main/res/drawable-xhdpi/voicemail.png diff --git a/res/drawable-xhdpi/waiting_time.png b/app/src/main/res/drawable-xhdpi/waiting_time.png similarity index 100% rename from res/drawable-xhdpi/waiting_time.png rename to app/src/main/res/drawable-xhdpi/waiting_time.png diff --git a/res/drawable/assistant_button.xml b/app/src/main/res/drawable/assistant_button.xml similarity index 83% rename from res/drawable/assistant_button.xml rename to app/src/main/res/drawable/assistant_button.xml index 1d6cbee82..6dd7bfb0d 100644 --- a/res/drawable/assistant_button.xml +++ b/app/src/main/res/drawable/assistant_button.xml @@ -5,6 +5,6 @@ + android:drawable="@drawable/resizable_assistant_button_default" /> diff --git a/res/drawable/assistant_button_text_color.xml b/app/src/main/res/drawable/assistant_button_text_color.xml similarity index 61% rename from res/drawable/assistant_button_text_color.xml rename to app/src/main/res/drawable/assistant_button_text_color.xml index 8e05da560..1797de9ce 100644 --- a/res/drawable/assistant_button_text_color.xml +++ b/app/src/main/res/drawable/assistant_button_text_color.xml @@ -1,7 +1,7 @@ + android:color="@color/black_color"/> + android:color="@color/light_grey_color"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/back.xml b/app/src/main/res/drawable/back.xml new file mode 100644 index 000000000..a298e171c --- /dev/null +++ b/app/src/main/res/drawable/back.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/backspace.xml b/app/src/main/res/drawable/backspace.xml new file mode 100644 index 000000000..70117222f --- /dev/null +++ b/app/src/main/res/drawable/backspace.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/button_background_dark.xml b/app/src/main/res/drawable/button_background_dark.xml new file mode 100644 index 000000000..034a4fd2e --- /dev/null +++ b/app/src/main/res/drawable/button_background_dark.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/button_background_light.xml b/app/src/main/res/drawable/button_background_light.xml new file mode 100644 index 000000000..c0df18fdd --- /dev/null +++ b/app/src/main/res/drawable/button_background_light.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/call.xml b/app/src/main/res/drawable/call.xml new file mode 100644 index 000000000..9ed0f637e --- /dev/null +++ b/app/src/main/res/drawable/call.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/call_alt_start.xml b/app/src/main/res/drawable/call_alt_start.xml new file mode 100644 index 000000000..43d9d8be7 --- /dev/null +++ b/app/src/main/res/drawable/call_alt_start.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/call_back.xml b/app/src/main/res/drawable/call_back.xml new file mode 100644 index 000000000..06367ef0d --- /dev/null +++ b/app/src/main/res/drawable/call_back.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/camera.xml b/app/src/main/res/drawable/camera.xml new file mode 100644 index 000000000..d30ef1407 --- /dev/null +++ b/app/src/main/res/drawable/camera.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/cancel_edit.xml b/app/src/main/res/drawable/cancel_edit.xml new file mode 100644 index 000000000..cc01397c9 --- /dev/null +++ b/app/src/main/res/drawable/cancel_edit.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/chat_bubble_incoming_full.xml b/app/src/main/res/drawable/chat_bubble_incoming_full.xml new file mode 100644 index 000000000..f541f2504 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_incoming_full.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/chat_bubble_incoming_split_1.xml b/app/src/main/res/drawable/chat_bubble_incoming_split_1.xml new file mode 100644 index 000000000..46f1fe172 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_incoming_split_1.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/chat_bubble_incoming_split_2.xml b/app/src/main/res/drawable/chat_bubble_incoming_split_2.xml new file mode 100644 index 000000000..a3a9cfcc2 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_incoming_split_2.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/chat_bubble_incoming_split_3.xml b/app/src/main/res/drawable/chat_bubble_incoming_split_3.xml new file mode 100644 index 000000000..af2af2629 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_incoming_split_3.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_full.xml b/app/src/main/res/drawable/chat_bubble_outgoing_full.xml new file mode 100644 index 000000000..53c64a052 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_outgoing_full.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_split_1.xml b/app/src/main/res/drawable/chat_bubble_outgoing_split_1.xml new file mode 100644 index 000000000..74894387e --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_outgoing_split_1.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_split_2.xml b/app/src/main/res/drawable/chat_bubble_outgoing_split_2.xml new file mode 100644 index 000000000..6d58ce71a --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_outgoing_split_2.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_split_3.xml b/app/src/main/res/drawable/chat_bubble_outgoing_split_3.xml new file mode 100644 index 000000000..e0df185f3 --- /dev/null +++ b/app/src/main/res/drawable/chat_bubble_outgoing_split_3.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/chat_file.xml b/app/src/main/res/drawable/chat_file.xml new file mode 100644 index 000000000..5a820aa57 --- /dev/null +++ b/app/src/main/res/drawable/chat_file.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/chat_group_new.xml b/app/src/main/res/drawable/chat_group_new.xml new file mode 100644 index 000000000..16fac5aa7 --- /dev/null +++ b/app/src/main/res/drawable/chat_group_new.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/chat_new.xml b/app/src/main/res/drawable/chat_new.xml new file mode 100644 index 000000000..fa5e61254 --- /dev/null +++ b/app/src/main/res/drawable/chat_new.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/chat_room_group_infos.xml b/app/src/main/res/drawable/chat_room_group_infos.xml new file mode 100644 index 000000000..96f25a4c2 --- /dev/null +++ b/app/src/main/res/drawable/chat_room_group_infos.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/chat_send_message.xml b/app/src/main/res/drawable/chat_send_message.xml new file mode 100644 index 000000000..47af131b5 --- /dev/null +++ b/app/src/main/res/drawable/chat_send_message.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/res/drawable/checkbox.xml b/app/src/main/res/drawable/checkbox.xml similarity index 61% rename from res/drawable/checkbox.xml rename to app/src/main/res/drawable/checkbox.xml index 8239dc456..accbf4d23 100644 --- a/res/drawable/checkbox.xml +++ b/app/src/main/res/drawable/checkbox.xml @@ -1,7 +1,7 @@ + android:drawable="@drawable/checkbox_unchecked"/> + android:drawable="@drawable/checkbox_checked"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/chevron_list_close.xml b/app/src/main/res/drawable/chevron_list_close.xml new file mode 100644 index 000000000..d826e42e9 --- /dev/null +++ b/app/src/main/res/drawable/chevron_list_close.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/chevron_list_open.xml b/app/src/main/res/drawable/chevron_list_open.xml new file mode 100644 index 000000000..37bcf729b --- /dev/null +++ b/app/src/main/res/drawable/chevron_list_open.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/res/drawable/clean_field.xml b/app/src/main/res/drawable/clean_field.xml similarity index 79% rename from res/drawable/clean_field.xml rename to app/src/main/res/drawable/clean_field.xml index 4136a6670..dd9843324 100644 --- a/res/drawable/clean_field.xml +++ b/app/src/main/res/drawable/clean_field.xml @@ -1,8 +1,8 @@ + - diff --git a/app/src/main/res/drawable/contact.xml b/app/src/main/res/drawable/contact.xml new file mode 100644 index 000000000..3a33fc108 --- /dev/null +++ b/app/src/main/res/drawable/contact.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/contact_add.xml b/app/src/main/res/drawable/contact_add.xml new file mode 100644 index 000000000..d3e79717c --- /dev/null +++ b/app/src/main/res/drawable/contact_add.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/contacts_all.xml b/app/src/main/res/drawable/contacts_all.xml new file mode 100644 index 000000000..b6ecf35ff --- /dev/null +++ b/app/src/main/res/drawable/contacts_all.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/contacts_sip.xml b/app/src/main/res/drawable/contacts_sip.xml new file mode 100644 index 000000000..33d9524f0 --- /dev/null +++ b/app/src/main/res/drawable/contacts_sip.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml new file mode 100644 index 000000000..a3000e1f3 --- /dev/null +++ b/app/src/main/res/drawable/delete.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/res/drawable/quit.xml b/app/src/main/res/drawable/delete_field.xml similarity index 60% rename from res/drawable/quit.xml rename to app/src/main/res/drawable/delete_field.xml index f85f9638b..ab5826102 100644 --- a/res/drawable/quit.xml +++ b/app/src/main/res/drawable/delete_field.xml @@ -1,7 +1,7 @@ + android:drawable="@drawable/delete_field_over" /> + android:drawable="@drawable/delete_field_default" /> diff --git a/app/src/main/res/drawable/deselect_all.xml b/app/src/main/res/drawable/deselect_all.xml new file mode 100644 index 000000000..2011e9ade --- /dev/null +++ b/app/src/main/res/drawable/deselect_all.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/dialer.xml b/app/src/main/res/drawable/dialer.xml new file mode 100644 index 000000000..ea7c17f05 --- /dev/null +++ b/app/src/main/res/drawable/dialer.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml similarity index 72% rename from res/drawable/divider.xml rename to app/src/main/res/drawable/divider.xml index 9afcc7d80..1ac7d6320 100644 --- a/res/drawable/divider.xml +++ b/app/src/main/res/drawable/divider.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/edit.xml b/app/src/main/res/drawable/edit.xml new file mode 100644 index 000000000..f692e5846 --- /dev/null +++ b/app/src/main/res/drawable/edit.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/edit_list.xml b/app/src/main/res/drawable/edit_list.xml new file mode 100644 index 000000000..fd959e163 --- /dev/null +++ b/app/src/main/res/drawable/edit_list.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/event_decoration_gray.xml b/app/src/main/res/drawable/event_decoration_gray.xml new file mode 100644 index 000000000..626663ad7 --- /dev/null +++ b/app/src/main/res/drawable/event_decoration_gray.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/event_decoration_red.xml b/app/src/main/res/drawable/event_decoration_red.xml new file mode 100644 index 000000000..f9e4927bf --- /dev/null +++ b/app/src/main/res/drawable/event_decoration_red.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/footer_button.xml b/app/src/main/res/drawable/footer_button.xml similarity index 59% rename from res/drawable/footer_button.xml rename to app/src/main/res/drawable/footer_button.xml index a621e8f73..e0e1f6cbb 100644 --- a/res/drawable/footer_button.xml +++ b/app/src/main/res/drawable/footer_button.xml @@ -1,10 +1,9 @@ + android:drawable="@color/primary_color" /> - + android:drawable="@color/dark_grey_color" /> + android:drawable="@color/dark_grey_color" /> diff --git a/app/src/main/res/drawable/generated_avatar_bg.xml b/app/src/main/res/drawable/generated_avatar_bg.xml new file mode 100644 index 000000000..52d05a405 --- /dev/null +++ b/app/src/main/res/drawable/generated_avatar_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/hangup.xml b/app/src/main/res/drawable/hangup.xml new file mode 100644 index 000000000..2e55eaf93 --- /dev/null +++ b/app/src/main/res/drawable/hangup.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/history_all.xml b/app/src/main/res/drawable/history_all.xml new file mode 100644 index 000000000..b3f33e5c1 --- /dev/null +++ b/app/src/main/res/drawable/history_all.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/history_missed.xml b/app/src/main/res/drawable/history_missed.xml new file mode 100644 index 000000000..4a634f2ca --- /dev/null +++ b/app/src/main/res/drawable/history_missed.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/res/drawable/list_detail.xml b/app/src/main/res/drawable/list_detail.xml similarity index 78% rename from res/drawable/list_detail.xml rename to app/src/main/res/drawable/list_detail.xml index 90a37d7ef..1ca68cb09 100644 --- a/res/drawable/list_detail.xml +++ b/app/src/main/res/drawable/list_detail.xml @@ -3,5 +3,5 @@ + android:drawable="@drawable/list_details_default" /> diff --git a/app/src/main/res/drawable/menu.xml b/app/src/main/res/drawable/menu.xml new file mode 100644 index 000000000..34af69cdb --- /dev/null +++ b/app/src/main/res/drawable/menu.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/menu_about.xml b/app/src/main/res/drawable/menu_about.xml new file mode 100644 index 000000000..9fbb4ec59 --- /dev/null +++ b/app/src/main/res/drawable/menu_about.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/menu_assistant.xml b/app/src/main/res/drawable/menu_assistant.xml new file mode 100644 index 000000000..44b13bd10 --- /dev/null +++ b/app/src/main/res/drawable/menu_assistant.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/menu_options.xml b/app/src/main/res/drawable/menu_options.xml new file mode 100644 index 000000000..056ff36da --- /dev/null +++ b/app/src/main/res/drawable/menu_options.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/menu_recordings.xml b/app/src/main/res/drawable/menu_recordings.xml new file mode 100644 index 000000000..e4d5f5e5e --- /dev/null +++ b/app/src/main/res/drawable/menu_recordings.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/micro.xml b/app/src/main/res/drawable/micro.xml new file mode 100644 index 000000000..888275bc4 --- /dev/null +++ b/app/src/main/res/drawable/micro.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/next.xml b/app/src/main/res/drawable/next.xml new file mode 100644 index 000000000..968192312 --- /dev/null +++ b/app/src/main/res/drawable/next.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_eight.xml b/app/src/main/res/drawable/numpad_eight.xml new file mode 100644 index 000000000..eba2595e3 --- /dev/null +++ b/app/src/main/res/drawable/numpad_eight.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_five.xml b/app/src/main/res/drawable/numpad_five.xml new file mode 100644 index 000000000..bd19a35a0 --- /dev/null +++ b/app/src/main/res/drawable/numpad_five.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_four.xml b/app/src/main/res/drawable/numpad_four.xml new file mode 100644 index 000000000..791010381 --- /dev/null +++ b/app/src/main/res/drawable/numpad_four.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_nine.xml b/app/src/main/res/drawable/numpad_nine.xml new file mode 100644 index 000000000..4fcf8ba72 --- /dev/null +++ b/app/src/main/res/drawable/numpad_nine.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_one.xml b/app/src/main/res/drawable/numpad_one.xml new file mode 100644 index 000000000..392b53e3f --- /dev/null +++ b/app/src/main/res/drawable/numpad_one.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_seven.xml b/app/src/main/res/drawable/numpad_seven.xml new file mode 100644 index 000000000..a8aad2d4f --- /dev/null +++ b/app/src/main/res/drawable/numpad_seven.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_sharp.xml b/app/src/main/res/drawable/numpad_sharp.xml new file mode 100644 index 000000000..84bbed502 --- /dev/null +++ b/app/src/main/res/drawable/numpad_sharp.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_six.xml b/app/src/main/res/drawable/numpad_six.xml new file mode 100644 index 000000000..de101da62 --- /dev/null +++ b/app/src/main/res/drawable/numpad_six.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_star_digit.xml b/app/src/main/res/drawable/numpad_star_digit.xml new file mode 100644 index 000000000..985778188 --- /dev/null +++ b/app/src/main/res/drawable/numpad_star_digit.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_three.xml b/app/src/main/res/drawable/numpad_three.xml new file mode 100644 index 000000000..ed428bb70 --- /dev/null +++ b/app/src/main/res/drawable/numpad_three.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_two.xml b/app/src/main/res/drawable/numpad_two.xml new file mode 100644 index 000000000..3762419f4 --- /dev/null +++ b/app/src/main/res/drawable/numpad_two.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/numpad_zero.xml b/app/src/main/res/drawable/numpad_zero.xml new file mode 100644 index 000000000..c43fe66e8 --- /dev/null +++ b/app/src/main/res/drawable/numpad_zero.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/options.xml b/app/src/main/res/drawable/options.xml new file mode 100644 index 000000000..4b8c7d20e --- /dev/null +++ b/app/src/main/res/drawable/options.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/options_add_call.xml b/app/src/main/res/drawable/options_add_call.xml new file mode 100644 index 000000000..97922cc63 --- /dev/null +++ b/app/src/main/res/drawable/options_add_call.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/options_rec.xml b/app/src/main/res/drawable/options_rec.xml new file mode 100644 index 000000000..3d75c109a --- /dev/null +++ b/app/src/main/res/drawable/options_rec.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/options_start_conference.xml b/app/src/main/res/drawable/options_start_conference.xml new file mode 100644 index 000000000..1a974f2e9 --- /dev/null +++ b/app/src/main/res/drawable/options_start_conference.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/options_transfer_call.xml b/app/src/main/res/drawable/options_transfer_call.xml new file mode 100644 index 000000000..d979d34db --- /dev/null +++ b/app/src/main/res/drawable/options_transfer_call.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/pause.xml b/app/src/main/res/drawable/pause.xml new file mode 100644 index 000000000..debf5030c --- /dev/null +++ b/app/src/main/res/drawable/pause.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/quit.xml b/app/src/main/res/drawable/quit.xml new file mode 100644 index 000000000..2369f4f01 --- /dev/null +++ b/app/src/main/res/drawable/quit.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/resizable_assistant_button.xml b/app/src/main/res/drawable/resizable_assistant_button.xml new file mode 100644 index 000000000..eeef6ce02 --- /dev/null +++ b/app/src/main/res/drawable/resizable_assistant_button.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/res/drawable/chat_send_file.xml b/app/src/main/res/drawable/round_button_background.xml similarity index 53% rename from res/drawable/chat_send_file.xml rename to app/src/main/res/drawable/round_button_background.xml index a26285ecf..315bdc63a 100644 --- a/res/drawable/chat_send_file.xml +++ b/app/src/main/res/drawable/round_button_background.xml @@ -1,8 +1,7 @@ - + android:drawable="@drawable/round_button_background_over" /> + - diff --git a/app/src/main/res/drawable/round_button_background_default.xml b/app/src/main/res/drawable/round_button_background_default.xml new file mode 100644 index 000000000..70045e2d8 --- /dev/null +++ b/app/src/main/res/drawable/round_button_background_default.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_button_background_over.xml b/app/src/main/res/drawable/round_button_background_over.xml new file mode 100644 index 000000000..211384486 --- /dev/null +++ b/app/src/main/res/drawable/round_button_background_over.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/res/drawable/dialer.xml b/app/src/main/res/drawable/round_orange_button_background.xml similarity index 51% rename from res/drawable/dialer.xml rename to app/src/main/res/drawable/round_orange_button_background.xml index cae05314b..941c2bcb9 100644 --- a/res/drawable/dialer.xml +++ b/app/src/main/res/drawable/round_orange_button_background.xml @@ -1,9 +1,7 @@ - + android:drawable="@drawable/round_orange_button_background_over" /> + android:drawable="@drawable/round_orange_button_background_default" /> diff --git a/app/src/main/res/drawable/round_orange_button_background_default.xml b/app/src/main/res/drawable/round_orange_button_background_default.xml new file mode 100644 index 000000000..211384486 --- /dev/null +++ b/app/src/main/res/drawable/round_orange_button_background_default.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/round_orange_button_background_over.xml b/app/src/main/res/drawable/round_orange_button_background_over.xml new file mode 100644 index 000000000..abad4e866 --- /dev/null +++ b/app/src/main/res/drawable/round_orange_button_background_over.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/route_bluetooth.xml b/app/src/main/res/drawable/route_bluetooth.xml new file mode 100644 index 000000000..ea4daaf07 --- /dev/null +++ b/app/src/main/res/drawable/route_bluetooth.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/route_earpiece.xml b/app/src/main/res/drawable/route_earpiece.xml new file mode 100644 index 000000000..f8d501869 --- /dev/null +++ b/app/src/main/res/drawable/route_earpiece.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/route_speaker.xml b/app/src/main/res/drawable/route_speaker.xml new file mode 100644 index 000000000..5121b9298 --- /dev/null +++ b/app/src/main/res/drawable/route_speaker.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/routes.xml b/app/src/main/res/drawable/routes.xml new file mode 100644 index 000000000..c3d06a0df --- /dev/null +++ b/app/src/main/res/drawable/routes.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/security_switch_thumb.xml b/app/src/main/res/drawable/security_switch_thumb.xml new file mode 100644 index 000000000..f5898deab --- /dev/null +++ b/app/src/main/res/drawable/security_switch_thumb.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/security_switch_track.xml b/app/src/main/res/drawable/security_switch_track.xml new file mode 100644 index 000000000..943b363d1 --- /dev/null +++ b/app/src/main/res/drawable/security_switch_track.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/select_all.xml b/app/src/main/res/drawable/select_all.xml new file mode 100644 index 000000000..294ddea41 --- /dev/null +++ b/app/src/main/res/drawable/select_all.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/speaker.xml b/app/src/main/res/drawable/speaker.xml new file mode 100644 index 000000000..b27e3b04b --- /dev/null +++ b/app/src/main/res/drawable/speaker.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/res/drawable/status_level.xml b/app/src/main/res/drawable/status_level.xml similarity index 100% rename from res/drawable/status_level.xml rename to app/src/main/res/drawable/status_level.xml diff --git a/res/drawable/switch_camera.xml b/app/src/main/res/drawable/switch_camera.xml similarity index 78% rename from res/drawable/switch_camera.xml rename to app/src/main/res/drawable/switch_camera.xml index b51a46377..5a4dc9418 100644 --- a/res/drawable/switch_camera.xml +++ b/app/src/main/res/drawable/switch_camera.xml @@ -3,5 +3,5 @@ + android:drawable="@drawable/camera_switch_default" /> diff --git a/app/src/main/res/drawable/unread_message_count_bg.xml b/app/src/main/res/drawable/unread_message_count_bg.xml new file mode 100644 index 000000000..940a12693 --- /dev/null +++ b/app/src/main/res/drawable/unread_message_count_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/valid.xml b/app/src/main/res/drawable/valid.xml new file mode 100644 index 000000000..3be5be72e --- /dev/null +++ b/app/src/main/res/drawable/valid.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout-land/about.xml b/app/src/main/res/layout-land/about.xml new file mode 100644 index 000000000..878d16e86 --- /dev/null +++ b/app/src/main/res/layout-land/about.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - diff --git a/res/layout/hidden.xml b/res/layout/hidden.xml deleted file mode 100644 index 28b0414e0..000000000 --- a/res/layout/hidden.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/res/layout/history.xml b/res/layout/history.xml deleted file mode 100644 index aa8a31ca0..000000000 --- a/res/layout/history.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/res/layout/history_cell.xml b/res/layout/history_cell.xml deleted file mode 100644 index 8cfd965a7..000000000 --- a/res/layout/history_cell.xml +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/history_detail.xml b/res/layout/history_detail.xml deleted file mode 100644 index d848ae759..000000000 --- a/res/layout/history_detail.xml +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/in_app.xml b/res/layout/in_app.xml deleted file mode 100644 index 64e33a927..000000000 --- a/res/layout/in_app.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/res/layout/in_app_list.xml b/res/layout/in_app_list.xml deleted file mode 100644 index d4f95743a..000000000 --- a/res/layout/in_app_list.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/res/layout/in_app_purchase_item.xml b/res/layout/in_app_purchase_item.xml deleted file mode 100644 index f9ca899c3..000000000 --- a/res/layout/in_app_purchase_item.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/in_app_store.xml b/res/layout/in_app_store.xml deleted file mode 100644 index 573d25a52..000000000 --- a/res/layout/in_app_store.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - -